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