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.IOException
17 import java.io.UnsupportedEncodingException
18 import java.lang.Exception
19 import kotlin.jvm.Throws
20 
21 class ScopedContext @Throws(UnsupportedEncodingException::class) constructor(context: Context?, experienceKey: ExperienceKey) : ContextWrapper(context) {
22   private val scope: String
23   private var filesDir: File
24   private var noBackupDir: File
25   private var cacheDir: File
26 
<lambda>null27   private val scopedApplicationContext by lazy { ScopedApplicationContext(baseContext.applicationContext as Application, this) }
28 
migrateAllFilesnull29   private fun migrateAllFiles(legacyDir: File, newDir: File) {
30     try {
31       migrateFilesRecursively(legacyDir, newDir)
32       val scopedFilesMigrationMarker = File(legacyDir, ".expo-migration")
33       scopedFilesMigrationMarker.createNewFile()
34     } catch (e: Exception) {
35       EXL.e(TAG, e)
36     }
37   }
38 
migrateFilesRecursivelynull39   private fun migrateFilesRecursively(legacyDir: File, newDir: File) {
40     val files = legacyDir.listFiles()
41     for (file in files) {
42       val fileName = file.name
43       val newLocation = File(newDir, fileName)
44       if (file.isDirectory) {
45         if (!newLocation.exists()) {
46           newLocation.mkdirs()
47         }
48         migrateFilesRecursively(file, newLocation)
49       } else if (!newLocation.exists()) {
50         // if a file with the same name already exists in the new location, ignore
51         // we don't want to overwrite potentially newer files
52         try {
53           FileUtils.copyFile(file, newLocation)
54         } catch (e: Exception) {
55           EXL.e(TAG, e)
56         }
57       }
58     }
59   }
60 
getApplicationContextnull61   override fun getApplicationContext(): Context = scopedApplicationContext
62 
63   override fun getPackageName(): String {
64     // Can't scope this because Google Apis rely on this being the same as the actual
65     // package name.
66     EXL.d(TAG, "WARNING: getPackageName called on ScopedContext")
67     return baseContext.packageName
68   }
69 
getSharedPreferencesnull70   override fun getSharedPreferences(name: String, mode: Int): SharedPreferences {
71     return baseContext.getSharedPreferences(scope + name, mode)
72   }
73 
moveSharedPreferencesFromnull74   override fun moveSharedPreferencesFrom(context: Context, s: String): Boolean {
75     return baseContext.moveSharedPreferencesFrom(context, scope + s)
76   }
77 
deleteSharedPreferencesnull78   override fun deleteSharedPreferences(s: String): Boolean {
79     return baseContext.deleteSharedPreferences(scope + s)
80   }
81 
82   // TODO: scope all file methods
getFilesDirnull83   override fun getFilesDir(): File {
84     return filesDir
85   }
86 
getCacheDirnull87   override fun getCacheDir(): File {
88     return cacheDir
89   }
90 
getNoBackupFilesDirnull91   override fun getNoBackupFilesDir(): File {
92     // We only need to create the directory if someone
93     // asks for it - that's why .mkdirs() is not
94     // in the constructor.
95     noBackupDir.mkdirs()
96     return noBackupDir
97   }
98 
openOrCreateDatabasenull99   override fun openOrCreateDatabase(
100     name: String,
101     mode: Int,
102     factory: CursorFactory
103   ): SQLiteDatabase {
104     return baseContext.openOrCreateDatabase(scope + name, mode, factory)
105   }
106 
openOrCreateDatabasenull107   override fun openOrCreateDatabase(
108     name: String,
109     mode: Int,
110     factory: CursorFactory,
111     errorHandler: DatabaseErrorHandler?
112   ): SQLiteDatabase {
113     return baseContext.openOrCreateDatabase(scope + name, mode, factory, errorHandler)
114   }
115 
moveDatabaseFromnull116   override fun moveDatabaseFrom(context: Context, s: String): Boolean {
117     return false
118   }
119 
deleteDatabasenull120   override fun deleteDatabase(name: String): Boolean {
121     return baseContext.deleteDatabase(scope + name)
122   }
123 
getDatabasePathnull124   override fun getDatabasePath(name: String): File {
125     return baseContext.getDatabasePath(scope + name)
126   }
127 
databaseListnull128   override fun databaseList(): Array<String> {
129     val list = baseContext.databaseList()
130     return list.filter {
131       it.startsWith(scope)
132     }.map {
133       it.substring(scope.length)
134     }.toTypedArray()
135   }
136 
ensureDirExistsnull137   private fun ensureDirExists(file: File) {
138     if (!file.exists()) {
139       val success = file.mkdirs()
140       if (!success) {
141         throw IOException("Unable to create scoped directory at path ${file.path}")
142       }
143     }
144   }
145 
146   val context: Context = baseContext
147 
148   companion object {
149     private val TAG = ScopedContext::class.java.simpleName
150   }
151 
152   init {
153     val scopeKey = experienceKey.getUrlEncodedScopeKey()
154 
155     scope = "$scopeKey-"
156 
157     val scopedFilesDir = File(baseContext.filesDir.toString() + "/ExperienceData/" + scopeKey)
158     filesDir = scopedFilesDir
159     cacheDir = File(baseContext.cacheDir.toString() + "/ExperienceData/" + scopeKey)
160     noBackupDir = File(baseContext.noBackupFilesDir.toString() + "/ExperienceData/" + scopeKey)
161 
162     if (Constants.isStandaloneApp()) {
163       val scopedFilesMigrationMarker = File(scopedFilesDir, ".expo-migration")
164       if (scopedFilesDir.exists() && !scopedFilesMigrationMarker.exists()) {
165         migrateAllFiles(scopedFilesDir, baseContext.filesDir)
166       }
167       filesDir = baseContext.filesDir
168       cacheDir = baseContext.cacheDir
169       noBackupDir = baseContext.noBackupFilesDir
170     }
171 
<lambda>null172     listOf(filesDir, cacheDir, noBackupDir).forEach {
173       ensureDirExists(it)
174     }
175   }
176 }
177