1 // Copyright 2015-present 650 Industries. All rights reserved.
2 package host.exp.exponent.utils
3 
4 import android.app.Application
5 import android.content.Context
6 import host.exp.exponent.kernel.ExperienceKey
7 import android.content.ContextWrapper
8 import host.exp.exponent.analytics.EXL
9 import android.content.SharedPreferences
10 import android.database.sqlite.SQLiteDatabase.CursorFactory
11 import android.database.sqlite.SQLiteDatabase
12 import android.database.DatabaseErrorHandler
13 import host.exp.exponent.Constants
14 import org.apache.commons.io.FileUtils
15 import java.io.File
16 import java.io.UnsupportedEncodingException
17 import java.lang.Exception
18 import kotlin.jvm.Throws
19 
20 class ScopedContext @Throws(UnsupportedEncodingException::class) constructor(context: Context?, experienceKey: ExperienceKey) : ContextWrapper(context) {
21   private val scope: String
22   private var filesDir: File
23   private var noBackupDir: File
24   private var cacheDir: File
25 
26   private val scopedApplicationContext by lazy { ScopedApplicationContext(baseContext.applicationContext as Application, this) }
27 
28   private fun migrateAllFiles(legacyDir: File, newDir: File) {
29     try {
30       migrateFilesRecursively(legacyDir, newDir)
31       val scopedFilesMigrationMarker = File(legacyDir, ".expo-migration")
32       scopedFilesMigrationMarker.createNewFile()
33     } catch (e: Exception) {
34       EXL.e(TAG, e)
35     }
36   }
37 
38   private fun migrateFilesRecursively(legacyDir: File, newDir: File) {
39     val files = legacyDir.listFiles()
40     for (file in files) {
41       val fileName = file.name
42       val newLocation = File(newDir, fileName)
43       if (file.isDirectory) {
44         if (!newLocation.exists()) {
45           newLocation.mkdirs()
46         }
47         migrateFilesRecursively(file, newLocation)
48       } else if (!newLocation.exists()) {
49         // if a file with the same name already exists in the new location, ignore
50         // we don't want to overwrite potentially newer files
51         try {
52           FileUtils.copyFile(file, newLocation)
53         } catch (e: Exception) {
54           EXL.e(TAG, e)
55         }
56       }
57     }
58   }
59 
60   override fun getApplicationContext(): Context = scopedApplicationContext
61 
62   override fun getPackageName(): String {
63     // Can't scope this because Google Apis rely on this being the same as the actual
64     // package name.
65     EXL.d(TAG, "WARNING: getPackageName called on ScopedContext")
66     return baseContext.packageName
67   }
68 
69   override fun getSharedPreferences(name: String, mode: Int): SharedPreferences {
70     return baseContext.getSharedPreferences(scope + name, mode)
71   }
72 
73   override fun moveSharedPreferencesFrom(context: Context, s: String): Boolean {
74     return baseContext.moveSharedPreferencesFrom(context, scope + s)
75   }
76 
77   override fun deleteSharedPreferences(s: String): Boolean {
78     return baseContext.deleteSharedPreferences(scope + s)
79   }
80 
81   // TODO: scope all file methods
82   override fun getFilesDir(): File {
83     return filesDir
84   }
85 
86   override fun getCacheDir(): File {
87     return cacheDir
88   }
89 
90   override fun getNoBackupFilesDir(): File {
91     // We only need to create the directory if someone
92     // asks for it - that's why .mkdirs() is not
93     // in the constructor.
94     noBackupDir.mkdirs()
95     return noBackupDir
96   }
97 
98   override fun openOrCreateDatabase(
99     name: String,
100     mode: Int,
101     factory: CursorFactory
102   ): SQLiteDatabase {
103     return baseContext.openOrCreateDatabase(scope + name, mode, factory)
104   }
105 
106   override fun openOrCreateDatabase(
107     name: String,
108     mode: Int,
109     factory: CursorFactory,
110     errorHandler: DatabaseErrorHandler?
111   ): SQLiteDatabase {
112     return baseContext.openOrCreateDatabase(scope + name, mode, factory, errorHandler)
113   }
114 
115   override fun moveDatabaseFrom(context: Context, s: String): Boolean {
116     return false
117   }
118 
119   override fun deleteDatabase(name: String): Boolean {
120     return baseContext.deleteDatabase(scope + name)
121   }
122 
123   override fun getDatabasePath(name: String): File {
124     return baseContext.getDatabasePath(scope + name)
125   }
126 
127   override fun databaseList(): Array<String> {
128     val list = baseContext.databaseList()
129     return list.filter {
130       it.startsWith(scope)
131     }.map {
132       it.substring(scope.length)
133     }.toTypedArray()
134   }
135 
136   val context: Context = baseContext
137 
138   companion object {
139     private val TAG = ScopedContext::class.java.simpleName
140   }
141 
142   init {
143     val scopeKey = experienceKey.getUrlEncodedScopeKey()
144 
145     scope = "$scopeKey-"
146 
147     val scopedFilesDir = File(baseContext.filesDir.toString() + "/ExperienceData/" + scopeKey)
148     filesDir = scopedFilesDir
149     cacheDir = File(baseContext.cacheDir.toString() + "/ExperienceData/" + scopeKey)
150     noBackupDir = File(baseContext.noBackupFilesDir.toString() + "/ExperienceData/" + scopeKey)
151 
152     if (Constants.isStandaloneApp()) {
153       val scopedFilesMigrationMarker = File(scopedFilesDir, ".expo-migration")
154       if (scopedFilesDir.exists() && !scopedFilesMigrationMarker.exists()) {
155         migrateAllFiles(scopedFilesDir, baseContext.filesDir)
156       }
157       filesDir = baseContext.filesDir
158       cacheDir = baseContext.cacheDir
159       noBackupDir = baseContext.noBackupFilesDir
160     }
161   }
162 }
163