parent
c92b9a3606
commit
324da2a9e9
14 changed files with 185 additions and 24 deletions
|
@ -68,7 +68,7 @@ internal class RestoreProgressAdapter : Adapter<PackageViewHolder>() {
|
|||
|
||||
}
|
||||
|
||||
internal enum class AppRestoreStatus {
|
||||
enum class AppRestoreStatus {
|
||||
IN_PROGRESS,
|
||||
SUCCEEDED,
|
||||
FAILED,
|
||||
|
|
|
@ -3,11 +3,14 @@ package com.stevesoltys.seedvault.settings
|
|||
import android.graphics.drawable.Drawable
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.View.GONE
|
||||
import android.view.View.INVISIBLE
|
||||
import android.view.View.VISIBLE
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.DiffUtil.DiffResult
|
||||
import androidx.recyclerview.widget.RecyclerView.Adapter
|
||||
import androidx.recyclerview.widget.RecyclerView.NO_POSITION
|
||||
import com.stevesoltys.seedvault.R
|
||||
import com.stevesoltys.seedvault.restore.AppRestoreStatus
|
||||
import com.stevesoltys.seedvault.restore.AppRestoreStatus.SUCCEEDED
|
||||
|
@ -15,9 +18,10 @@ import com.stevesoltys.seedvault.settings.AppStatusAdapter.AppStatusViewHolder
|
|||
import com.stevesoltys.seedvault.ui.AppViewHolder
|
||||
import com.stevesoltys.seedvault.ui.toRelativeTime
|
||||
|
||||
internal class AppStatusAdapter : Adapter<AppStatusViewHolder>() {
|
||||
internal class AppStatusAdapter(private val toggleListener: AppStatusToggleListener) : Adapter<AppStatusViewHolder>() {
|
||||
|
||||
private val items = ArrayList<AppStatus>()
|
||||
private var editMode = false
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AppStatusViewHolder {
|
||||
val v = LayoutInflater.from(parent.context).inflate(R.layout.list_item_app_status, parent, false)
|
||||
|
@ -30,28 +34,67 @@ internal class AppStatusAdapter : Adapter<AppStatusViewHolder>() {
|
|||
holder.bind(items[position])
|
||||
}
|
||||
|
||||
fun setEditMode(enabled: Boolean) {
|
||||
editMode = enabled
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
fun update(newItems: List<AppStatus>, diff: DiffResult) {
|
||||
items.clear()
|
||||
items.addAll(newItems)
|
||||
diff.dispatchUpdatesTo(this)
|
||||
}
|
||||
|
||||
fun onItemChanged(item: AppStatus) {
|
||||
val pos = items.indexOfFirst { it.packageName == item.packageName }
|
||||
if (pos != NO_POSITION) notifyItemChanged(pos, item)
|
||||
}
|
||||
|
||||
inner class AppStatusViewHolder(v: View) : AppViewHolder(v) {
|
||||
fun bind(item: AppStatus) {
|
||||
appName.text = item.name
|
||||
appIcon.setImageDrawable(item.icon)
|
||||
setStatus(item.status)
|
||||
if (item.status == SUCCEEDED) {
|
||||
appInfo.text = item.time.toRelativeTime(context)
|
||||
appInfo.visibility = VISIBLE
|
||||
if (editMode) {
|
||||
v.background = clickableBackground
|
||||
v.setOnClickListener {
|
||||
switchView.toggle()
|
||||
item.enabled = switchView.isChecked
|
||||
toggleListener.onAppStatusToggled(item)
|
||||
}
|
||||
appInfo.visibility = GONE
|
||||
appStatus.visibility = INVISIBLE
|
||||
progressBar.visibility = INVISIBLE
|
||||
switchView.visibility = VISIBLE
|
||||
switchView.isChecked = item.enabled
|
||||
} else {
|
||||
v.background = null
|
||||
v.setOnClickListener(null)
|
||||
setStatus(item.status)
|
||||
if (item.status == SUCCEEDED) {
|
||||
appInfo.text = item.time.toRelativeTime(context)
|
||||
appInfo.visibility = VISIBLE
|
||||
}
|
||||
switchView.visibility = INVISIBLE
|
||||
}
|
||||
// show disabled items differently
|
||||
showEnabled(item.enabled)
|
||||
}
|
||||
|
||||
private fun showEnabled(enabled: Boolean) {
|
||||
val alpha = if (enabled) 1.0f else 0.5f
|
||||
// setting the alpha on root view v only doesn't work as the ItemAnimator messes with it
|
||||
appIcon.alpha = alpha
|
||||
appName.alpha = alpha
|
||||
appInfo.alpha = alpha
|
||||
appStatus.alpha = alpha
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
internal data class AppStatus(
|
||||
data class AppStatus(
|
||||
val packageName: String,
|
||||
var enabled: Boolean,
|
||||
val icon: Drawable,
|
||||
val name: String,
|
||||
val time: Long,
|
||||
|
|
|
@ -2,6 +2,9 @@ package com.stevesoltys.seedvault.settings
|
|||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.View.INVISIBLE
|
||||
import android.view.View.VISIBLE
|
||||
|
@ -13,15 +16,21 @@ import com.stevesoltys.seedvault.R
|
|||
import kotlinx.android.synthetic.main.fragment_app_status.*
|
||||
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
|
||||
|
||||
class AppStatusFragment : Fragment() {
|
||||
internal interface AppStatusToggleListener {
|
||||
fun onAppStatusToggled(status: AppStatus)
|
||||
}
|
||||
|
||||
class AppStatusFragment : Fragment(), AppStatusToggleListener {
|
||||
|
||||
private val viewModel: SettingsViewModel by sharedViewModel()
|
||||
|
||||
private val layoutManager = LinearLayoutManager(context)
|
||||
private val adapter = AppStatusAdapter()
|
||||
private val adapter = AppStatusAdapter(this)
|
||||
private lateinit var appEditMenuItem: MenuItem
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?): View? {
|
||||
setHasOptionsMenu(true)
|
||||
return inflater.inflate(R.layout.fragment_app_status, container, false)
|
||||
}
|
||||
|
||||
|
@ -42,4 +51,29 @@ class AppStatusFragment : Fragment() {
|
|||
})
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
super.onCreateOptionsMenu(menu, inflater)
|
||||
inflater.inflate(R.menu.app_status_menu, menu)
|
||||
appEditMenuItem = menu.findItem(R.id.edit_app_blacklist)
|
||||
|
||||
// observe edit mode changes here where we are sure to have the MenuItem
|
||||
viewModel.appEditMode.observe(this, Observer { enabled ->
|
||||
appEditMenuItem.isChecked = enabled
|
||||
adapter.setEditMode(enabled)
|
||||
})
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
|
||||
R.id.edit_app_blacklist -> {
|
||||
viewModel.setEditMode(!item.isChecked)
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
override fun onAppStatusToggled(status: AppStatus) {
|
||||
adapter.onItemChanged(status)
|
||||
viewModel.onAppStatusToggled(status)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package com.stevesoltys.seedvault.settings
|
|||
import android.content.Context
|
||||
import android.hardware.usb.UsbDevice
|
||||
import android.net.Uri
|
||||
import androidx.annotation.UiThread
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.preference.PreferenceManager
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
@ -18,12 +19,18 @@ private const val PREF_KEY_FLASH_DRIVE_SERIAL_NUMBER = "flashSerialNumber"
|
|||
private const val PREF_KEY_FLASH_DRIVE_VENDOR_ID = "flashDriveVendorId"
|
||||
private const val PREF_KEY_FLASH_DRIVE_PRODUCT_ID = "flashDriveProductId"
|
||||
|
||||
private const val PREF_KEY_BACKUP_APP_BLACKLIST = "backupAppBlacklist"
|
||||
|
||||
class SettingsManager(context: Context) {
|
||||
|
||||
private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
|
||||
private var isStorageChanging: AtomicBoolean = AtomicBoolean(false)
|
||||
|
||||
private val blacklistedApps: HashSet<String> by lazy {
|
||||
prefs.getStringSet(PREF_KEY_BACKUP_APP_BLACKLIST, emptySet()).toHashSet()
|
||||
}
|
||||
|
||||
// FIXME Storage is currently plugin specific and not generic
|
||||
fun setStorage(storage: Storage) {
|
||||
prefs.edit()
|
||||
|
@ -76,6 +83,15 @@ class SettingsManager(context: Context) {
|
|||
return prefs.getBoolean(PREF_KEY_BACKUP_APK, true)
|
||||
}
|
||||
|
||||
fun isBackupEnabled(packageName: String) = !blacklistedApps.contains(packageName)
|
||||
|
||||
@UiThread
|
||||
fun onAppBackupStatusChanged(status: AppStatus) {
|
||||
if (status.enabled) blacklistedApps.remove(status.packageName)
|
||||
else blacklistedApps.add(status.packageName)
|
||||
prefs.edit().putStringSet(PREF_KEY_BACKUP_APP_BLACKLIST, blacklistedApps).apply()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
data class Storage(
|
||||
|
|
|
@ -3,8 +3,10 @@ package com.stevesoltys.seedvault.settings
|
|||
import android.app.Application
|
||||
import android.content.pm.PackageManager.NameNotFoundException
|
||||
import android.util.Log
|
||||
import androidx.annotation.UiThread
|
||||
import androidx.core.content.ContextCompat.getDrawable
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.Transformations.switchMap
|
||||
import androidx.lifecycle.liveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
|
@ -47,6 +49,9 @@ class SettingsViewModel(
|
|||
private val mAppStatusList = switchMap(lastBackupTime) { getAppStatusResult() }
|
||||
internal val appStatusList: LiveData<AppStatusResult> = mAppStatusList
|
||||
|
||||
private val mAppEditMode = MutableLiveData<Boolean>()
|
||||
internal val appEditMode: LiveData<Boolean> = mAppEditMode
|
||||
|
||||
init {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
// ensures the lastBackupTime LiveData gets set
|
||||
|
@ -91,6 +96,7 @@ class SettingsViewModel(
|
|||
}
|
||||
AppStatus(
|
||||
packageName = it.packageName,
|
||||
enabled = settingsManager.isBackupEnabled(it.packageName),
|
||||
icon = icon,
|
||||
name = getAppName(app, it.packageName).toString(),
|
||||
time = time,
|
||||
|
@ -102,4 +108,14 @@ class SettingsViewModel(
|
|||
emit(AppStatusResult(list, diff))
|
||||
}
|
||||
|
||||
@UiThread
|
||||
fun setEditMode(enabled: Boolean) {
|
||||
mAppEditMode.value = enabled
|
||||
}
|
||||
|
||||
@UiThread
|
||||
fun onAppStatusToggled(status: AppStatus) {
|
||||
settingsManager.onAppBackupStatusChanged(status)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -90,9 +90,13 @@ internal class BackupCoordinator(
|
|||
}
|
||||
|
||||
fun isAppEligibleForBackup(targetPackage: PackageInfo, @Suppress("UNUSED_PARAMETER") isFullBackup: Boolean): Boolean {
|
||||
val packageName = targetPackage.packageName
|
||||
// Check that the app is not blacklisted by the user
|
||||
val enabled = settingsManager.isBackupEnabled(packageName)
|
||||
if (!enabled) Log.w(TAG, "Backup of $packageName disabled by user.")
|
||||
// We need to exclude the DocumentsProvider used to store backup data.
|
||||
// Otherwise, it gets killed when we back it up, terminating our backup.
|
||||
return targetPackage.packageName != plugin.providerPackageName
|
||||
return enabled && targetPackage.packageName != plugin.providerPackageName
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -104,9 +108,10 @@ internal class BackupCoordinator(
|
|||
* @return Current limit on backup size in bytes.
|
||||
*/
|
||||
fun getBackupQuota(packageName: String, isFullBackup: Boolean): Long {
|
||||
// try to back up APK here as later methods are sometimes not called called
|
||||
val pm = context.packageManager
|
||||
backUpApk(pm.getPackageInfo(packageName, GET_SIGNING_CERTIFICATES))
|
||||
if (packageName != MAGIC_PACKAGE_MANAGER) {
|
||||
// try to back up APK here as later methods are sometimes not called called
|
||||
backUpApk(context.packageManager.getPackageInfo(packageName, GET_SIGNING_CERTIFICATES))
|
||||
}
|
||||
|
||||
// report back quota
|
||||
Log.i(TAG, "Get backup quota for $packageName. Is full backup: $isFullBackup.")
|
||||
|
|
|
@ -8,6 +8,7 @@ import android.view.View.INVISIBLE
|
|||
import android.view.View.VISIBLE
|
||||
import android.widget.ImageView
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.Switch
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.stevesoltys.seedvault.R
|
||||
|
@ -21,18 +22,26 @@ import com.stevesoltys.seedvault.restore.AppRestoreStatus.IN_PROGRESS
|
|||
import com.stevesoltys.seedvault.restore.AppRestoreStatus.SUCCEEDED
|
||||
|
||||
|
||||
internal open class AppViewHolder(v: View) : RecyclerView.ViewHolder(v) {
|
||||
internal open class AppViewHolder(protected val v: View) : RecyclerView.ViewHolder(v) {
|
||||
|
||||
protected val context: Context = v.context
|
||||
protected val pm: PackageManager = context.packageManager
|
||||
|
||||
protected val clickableBackground = v.background!!
|
||||
protected val appIcon: ImageView = v.findViewById(R.id.appIcon)
|
||||
protected val appName: TextView = v.findViewById(R.id.appName)
|
||||
protected val appInfo: TextView = v.findViewById(R.id.appInfo)
|
||||
protected val appStatus: ImageView = v.findViewById(R.id.appStatus)
|
||||
protected val progressBar: ProgressBar = v.findViewById(R.id.progressBar)
|
||||
protected val switchView: Switch = v.findViewById(R.id.switchView)
|
||||
|
||||
init {
|
||||
// don't use clickable background by default
|
||||
v.background = null
|
||||
}
|
||||
|
||||
protected fun setStatus(status: AppRestoreStatus) {
|
||||
v.background = null
|
||||
if (status == IN_PROGRESS) {
|
||||
appInfo.visibility = GONE
|
||||
appStatus.visibility = INVISIBLE
|
||||
|
|
|
@ -8,7 +8,7 @@ import com.stevesoltys.seedvault.ui.storage.StorageViewModel
|
|||
|
||||
abstract class RequireProvisioningViewModel(
|
||||
protected val app: Application,
|
||||
private val settingsManager: SettingsManager,
|
||||
protected val settingsManager: SettingsManager,
|
||||
private val keyManager: KeyManager
|
||||
) : AndroidViewModel(app) {
|
||||
|
||||
|
|
|
@ -8,9 +8,6 @@
|
|||
android:id="@+id/list"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:scrollbarStyle="outsideOverlay"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
|
|
|
@ -58,7 +58,8 @@
|
|||
android:id="@+id/appList"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_margin="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
app:layout_constraintBottom_toTopOf="@+id/button"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/backupNameView"
|
||||
|
|
|
@ -4,8 +4,11 @@
|
|||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="8dp">
|
||||
android:background="?android:selectableItemBackground"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingBottom="8dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/appIcon"
|
||||
|
@ -26,7 +29,7 @@
|
|||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
app:layout_constraintBottom_toTopOf="@+id/appInfo"
|
||||
app:layout_constraintEnd_toStartOf="@+id/appStatus"
|
||||
app:layout_constraintEnd_toStartOf="@+id/switchView"
|
||||
app:layout_constraintStart_toEndOf="@+id/appIcon"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="Seedvault Backup" />
|
||||
|
@ -57,10 +60,20 @@
|
|||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
style="?android:attr/progressBarStyle"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<Switch
|
||||
android:id="@+id/switchView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="invisible"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
10
app/src/main/res/menu/app_status_menu.xml
Normal file
10
app/src/main/res/menu/app_status_menu.xml
Normal file
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item
|
||||
android:id="@+id/edit_app_blacklist"
|
||||
android:checkable="true"
|
||||
android:title="@string/settings_backup_exclude_apps"
|
||||
app:showAsAction="never" />
|
||||
</menu>
|
|
@ -27,6 +27,7 @@
|
|||
<string name="settings_backup_apk_dialog_disable">Disable app backup</string>
|
||||
<string name="settings_backup_status_title">App backup status</string>
|
||||
<string name="settings_backup_status_summary">Last Backup: %1$s</string>
|
||||
<string name="settings_backup_exclude_apps">Exclude apps</string>
|
||||
<string name="settings_backup_now">Backup now</string>
|
||||
|
||||
<!-- Storage -->
|
||||
|
|
|
@ -23,7 +23,9 @@ import io.mockk.just
|
|||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
import org.junit.jupiter.api.Assertions.assertThrows
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.io.IOException
|
||||
import java.io.OutputStream
|
||||
|
@ -122,6 +124,20 @@ internal class BackupCoordinatorTest : BackupTest() {
|
|||
assertEquals(quota, backup.getBackupQuota(packageInfo.packageName, isFullBackup))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isAppEligibleForBackup() exempts plugin provider and blacklisted apps`() {
|
||||
every {
|
||||
settingsManager.isBackupEnabled(packageInfo.packageName)
|
||||
} returns true andThen false andThen true
|
||||
every {
|
||||
plugin.providerPackageName
|
||||
} returns packageInfo.packageName andThen "new.package" andThen "new.package"
|
||||
|
||||
assertFalse(backup.isAppEligibleForBackup(packageInfo, true))
|
||||
assertFalse(backup.isAppEligibleForBackup(packageInfo, true))
|
||||
assertTrue(backup.isAppEligibleForBackup(packageInfo, true))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clearing KV backup data throws`() {
|
||||
every { kv.clearBackupData(packageInfo) } throws IOException()
|
||||
|
|
Loading…
Reference in a new issue