From 77ce3f6fe8c693a2cd490a1c6ec07970262ab643 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Thu, 13 Aug 2020 15:43:43 -0300 Subject: [PATCH] Make app blacklist accessible by multiple threads Might fix #83 --- .../seedvault/settings/SettingsManager.kt | 67 +++++++++-------- .../seedvault/settings/SettingsViewModel.kt | 73 ++++++++++--------- 2 files changed, 76 insertions(+), 64 deletions(-) diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsManager.kt b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsManager.kt index 913e3a21..d5673a36 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsManager.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsManager.kt @@ -6,6 +6,7 @@ import android.net.Uri import androidx.annotation.UiThread import androidx.documentfile.provider.DocumentFile import androidx.preference.PreferenceManager +import java.util.concurrent.ConcurrentSkipListSet import java.util.concurrent.atomic.AtomicBoolean internal const val PREF_KEY_BACKUP_APK = "backup_apk" @@ -27,24 +28,30 @@ class SettingsManager(context: Context) { private var isStorageChanging: AtomicBoolean = AtomicBoolean(false) - private val blacklistedApps: HashSet by lazy { - prefs.getStringSet(PREF_KEY_BACKUP_APP_BLACKLIST, emptySet()).toHashSet() + /** + * This gets accessed by non-UI threads when saving with [PreferenceManager] + * and when [isBackupEnabled] is called during a backup run. + * Therefore, it is implemented with a thread-safe [ConcurrentSkipListSet]. + */ + private val blacklistedApps: MutableSet by lazy { + ConcurrentSkipListSet(prefs.getStringSet(PREF_KEY_BACKUP_APP_BLACKLIST, emptySet())) } // FIXME Storage is currently plugin specific and not generic fun setStorage(storage: Storage) { prefs.edit() - .putString(PREF_KEY_STORAGE_URI, storage.uri.toString()) - .putString(PREF_KEY_STORAGE_NAME, storage.name) - .putBoolean(PREF_KEY_STORAGE_IS_USB, storage.isUsb) - .apply() + .putString(PREF_KEY_STORAGE_URI, storage.uri.toString()) + .putString(PREF_KEY_STORAGE_NAME, storage.name) + .putBoolean(PREF_KEY_STORAGE_IS_USB, storage.isUsb) + .apply() isStorageChanging.set(true) } fun getStorage(): Storage? { val uriStr = prefs.getString(PREF_KEY_STORAGE_URI, null) ?: return null val uri = Uri.parse(uriStr) - val name = prefs.getString(PREF_KEY_STORAGE_NAME, null) ?: throw IllegalStateException("no storage name") + val name = prefs.getString(PREF_KEY_STORAGE_NAME, null) + ?: throw IllegalStateException("no storage name") val isUsb = prefs.getBoolean(PREF_KEY_STORAGE_IS_USB, false) return Storage(uri, name, isUsb) } @@ -57,18 +64,18 @@ class SettingsManager(context: Context) { fun setFlashDrive(usb: FlashDrive?) { if (usb == null) { prefs.edit() - .remove(PREF_KEY_FLASH_DRIVE_NAME) - .remove(PREF_KEY_FLASH_DRIVE_SERIAL_NUMBER) - .remove(PREF_KEY_FLASH_DRIVE_VENDOR_ID) - .remove(PREF_KEY_FLASH_DRIVE_PRODUCT_ID) - .apply() + .remove(PREF_KEY_FLASH_DRIVE_NAME) + .remove(PREF_KEY_FLASH_DRIVE_SERIAL_NUMBER) + .remove(PREF_KEY_FLASH_DRIVE_VENDOR_ID) + .remove(PREF_KEY_FLASH_DRIVE_PRODUCT_ID) + .apply() } else { prefs.edit() - .putString(PREF_KEY_FLASH_DRIVE_NAME, usb.name) - .putString(PREF_KEY_FLASH_DRIVE_SERIAL_NUMBER, usb.serialNumber) - .putInt(PREF_KEY_FLASH_DRIVE_VENDOR_ID, usb.vendorId) - .putInt(PREF_KEY_FLASH_DRIVE_PRODUCT_ID, usb.productId) - .apply() + .putString(PREF_KEY_FLASH_DRIVE_NAME, usb.name) + .putString(PREF_KEY_FLASH_DRIVE_SERIAL_NUMBER, usb.serialNumber) + .putInt(PREF_KEY_FLASH_DRIVE_VENDOR_ID, usb.vendorId) + .putInt(PREF_KEY_FLASH_DRIVE_PRODUCT_ID, usb.productId) + .apply() } } @@ -96,24 +103,26 @@ class SettingsManager(context: Context) { } data class Storage( - val uri: Uri, - val name: String, - val isUsb: Boolean) { + val uri: Uri, + val name: String, + val isUsb: Boolean +) { fun getDocumentFile(context: Context) = DocumentFile.fromTreeUri(context, uri) - ?: throw AssertionError("Should only happen on API < 21.") + ?: throw AssertionError("Should only happen on API < 21.") } data class FlashDrive( - val name: String, - val serialNumber: String?, - val vendorId: Int, - val productId: Int) { + val name: String, + val serialNumber: String?, + val vendorId: Int, + val productId: Int +) { companion object { fun from(device: UsbDevice) = FlashDrive( - name = "${device.manufacturerName} ${device.productName}", - serialNumber = device.serialNumber, - vendorId = device.vendorId, - productId = device.productId + name = "${device.manufacturerName} ${device.productName}", + serialNumber = device.serialNumber, + vendorId = device.vendorId, + productId = device.productId ) } } diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsViewModel.kt b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsViewModel.kt index 8a86a1b2..2208e8cf 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsViewModel.kt @@ -46,7 +46,10 @@ class SettingsViewModel( internal val lastBackupTime = metadataManager.lastBackupTime - private val mAppStatusList = switchMap(lastBackupTime) { getAppStatusResult() } + private val mAppStatusList = switchMap(lastBackupTime) { + // updates app list when lastBackupTime changes + getAppStatusResult() + } internal val appStatusList: LiveData = mAppStatusList private val mAppEditMode = MutableLiveData() @@ -63,46 +66,46 @@ class SettingsViewModel( Thread { requestBackup(app) }.start() } - private fun getAppStatusResult(): LiveData = liveData(Dispatchers.Main) { + private fun getAppStatusResult(): LiveData = liveData { val pm = app.packageManager val locale = Locale.getDefault() val list = pm.getInstalledPackages(0) - .filter { !it.isSystemApp() } - .map { - val icon = if (it.packageName == MAGIC_PACKAGE_MANAGER) { + .filter { !it.isSystemApp() } + .map { + val icon = if (it.packageName == MAGIC_PACKAGE_MANAGER) { + getDrawable(app, R.drawable.ic_launcher_default)!! + } else { + try { + pm.getApplicationIcon(it.packageName) + } catch (e: NameNotFoundException) { getDrawable(app, R.drawable.ic_launcher_default)!! - } else { - try { - pm.getApplicationIcon(it.packageName) - } catch (e: NameNotFoundException) { - getDrawable(app, R.drawable.ic_launcher_default)!! - } } - val metadata = metadataManager.getPackageMetadata(it.packageName) - val time = metadata?.time ?: 0 - val status = when (metadata?.state) { - null -> { - Log.w(TAG, "No metadata available for: ${it.packageName}") - FAILED - } - NO_DATA -> FAILED_NO_DATA - NOT_ALLOWED -> FAILED_NOT_ALLOWED - QUOTA_EXCEEDED -> FAILED_QUOTA_EXCEEDED - UNKNOWN_ERROR -> FAILED - APK_AND_DATA -> SUCCEEDED + } + val metadata = metadataManager.getPackageMetadata(it.packageName) + val time = metadata?.time ?: 0 + val status = when (metadata?.state) { + null -> { + Log.w(TAG, "No metadata available for: ${it.packageName}") + FAILED } - if (metadata?.hasApk() == false) { - Log.w(TAG, "No APK stored for: ${it.packageName}") - } - AppStatus( - packageName = it.packageName, - enabled = settingsManager.isBackupEnabled(it.packageName), - icon = icon, - name = getAppName(app, it.packageName).toString(), - time = time, - status = status - ) - }.sortedBy { it.name.toLowerCase(locale) } + NO_DATA -> FAILED_NO_DATA + NOT_ALLOWED -> FAILED_NOT_ALLOWED + QUOTA_EXCEEDED -> FAILED_QUOTA_EXCEEDED + UNKNOWN_ERROR -> FAILED + APK_AND_DATA -> SUCCEEDED + } + if (metadata?.hasApk() == false) { + Log.w(TAG, "No APK stored for: ${it.packageName}") + } + AppStatus( + packageName = it.packageName, + enabled = settingsManager.isBackupEnabled(it.packageName), + icon = icon, + name = getAppName(app, it.packageName).toString(), + time = time, + status = status + ) + }.sortedBy { it.name.toLowerCase(locale) } val oldList = mAppStatusList.value?.appStatusList ?: emptyList() val diff = calculateDiff(AppStatusDiff(oldList, list)) emit(AppStatusResult(list, diff))