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