Merge pull request #72 from grote/70-app-backup-toggle

Allow the user to exclude apps from backup
This commit is contained in:
Steve Soltys 2020-01-20 14:03:44 -05:00 committed by GitHub
commit ee6d359c50
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 214 additions and 78 deletions

View file

@ -56,7 +56,7 @@ internal class AppInstallViewHolder(v: View) : AppViewHolder(v) {
progressBar.visibility = INVISIBLE progressBar.visibility = INVISIBLE
} }
FAILED -> { FAILED -> {
appStatus.setImageResource(R.drawable.ic_cancel_red) appStatus.setImageResource(R.drawable.ic_error_red)
appStatus.visibility = VISIBLE appStatus.visibility = VISIBLE
progressBar.visibility = INVISIBLE progressBar.visibility = INVISIBLE
} }

View file

@ -19,8 +19,6 @@ class RestoreActivity : RequireProvisioningActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
if (isSetupWizard) hideSystemUI()
setContentView(R.layout.activity_fragment_container) setContentView(R.layout.activity_fragment_container)
viewModel.displayFragment.observeEvent(this, LiveEventHandler { fragment -> viewModel.displayFragment.observeEvent(this, LiveEventHandler { fragment ->

View file

@ -68,7 +68,7 @@ internal class RestoreProgressAdapter : Adapter<PackageViewHolder>() {
} }
internal enum class AppRestoreStatus { enum class AppRestoreStatus {
IN_PROGRESS, IN_PROGRESS,
SUCCEEDED, SUCCEEDED,
FAILED, FAILED,

View file

@ -23,6 +23,7 @@ class AboutDialogFragment : DialogFragment() {
val linkMovementMethod = LinkMovementMethod.getInstance() val linkMovementMethod = LinkMovementMethod.getInstance()
licenseView.movementMethod = linkMovementMethod licenseView.movementMethod = linkMovementMethod
authorView.movementMethod = linkMovementMethod authorView.movementMethod = linkMovementMethod
designView.movementMethod = linkMovementMethod
sponsorView.movementMethod = linkMovementMethod sponsorView.movementMethod = linkMovementMethod
} }

View file

@ -3,11 +3,14 @@ package com.stevesoltys.seedvault.settings
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.View.GONE
import android.view.View.INVISIBLE
import android.view.View.VISIBLE import android.view.View.VISIBLE
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.DiffUtil.DiffResult import androidx.recyclerview.widget.DiffUtil.DiffResult
import androidx.recyclerview.widget.RecyclerView.Adapter import androidx.recyclerview.widget.RecyclerView.Adapter
import androidx.recyclerview.widget.RecyclerView.NO_POSITION
import com.stevesoltys.seedvault.R import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.restore.AppRestoreStatus import com.stevesoltys.seedvault.restore.AppRestoreStatus
import com.stevesoltys.seedvault.restore.AppRestoreStatus.SUCCEEDED 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.AppViewHolder
import com.stevesoltys.seedvault.ui.toRelativeTime 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 val items = ArrayList<AppStatus>()
private var editMode = false
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AppStatusViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AppStatusViewHolder {
val v = LayoutInflater.from(parent.context).inflate(R.layout.list_item_app_status, parent, false) 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]) holder.bind(items[position])
} }
fun setEditMode(enabled: Boolean) {
editMode = enabled
notifyDataSetChanged()
}
fun update(newItems: List<AppStatus>, diff: DiffResult) { fun update(newItems: List<AppStatus>, diff: DiffResult) {
items.clear() items.clear()
items.addAll(newItems) items.addAll(newItems)
diff.dispatchUpdatesTo(this) 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) { inner class AppStatusViewHolder(v: View) : AppViewHolder(v) {
fun bind(item: AppStatus) { fun bind(item: AppStatus) {
appName.text = item.name appName.text = item.name
appIcon.setImageDrawable(item.icon) appIcon.setImageDrawable(item.icon)
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) setStatus(item.status)
if (item.status == SUCCEEDED) { if (item.status == SUCCEEDED) {
appInfo.text = item.time.toRelativeTime(context) appInfo.text = item.time.toRelativeTime(context)
appInfo.visibility = VISIBLE 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, val packageName: String,
var enabled: Boolean,
val icon: Drawable, val icon: Drawable,
val name: String, val name: String,
val time: Long, val time: Long,

View file

@ -2,6 +2,9 @@ package com.stevesoltys.seedvault.settings
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View import android.view.View
import android.view.View.INVISIBLE import android.view.View.INVISIBLE
import android.view.View.VISIBLE import android.view.View.VISIBLE
@ -13,15 +16,21 @@ import com.stevesoltys.seedvault.R
import kotlinx.android.synthetic.main.fragment_app_status.* import kotlinx.android.synthetic.main.fragment_app_status.*
import org.koin.androidx.viewmodel.ext.android.sharedViewModel 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 viewModel: SettingsViewModel by sharedViewModel()
private val layoutManager = LinearLayoutManager(context) 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?, override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? { savedInstanceState: Bundle?): View? {
setHasOptionsMenu(true)
return inflater.inflate(R.layout.fragment_app_status, container, false) 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)
}
} }

View file

@ -3,6 +3,7 @@ package com.stevesoltys.seedvault.settings
import android.content.Context import android.content.Context
import android.hardware.usb.UsbDevice import android.hardware.usb.UsbDevice
import android.net.Uri import android.net.Uri
import androidx.annotation.UiThread
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import java.util.concurrent.atomic.AtomicBoolean 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_VENDOR_ID = "flashDriveVendorId"
private const val PREF_KEY_FLASH_DRIVE_PRODUCT_ID = "flashDriveProductId" private const val PREF_KEY_FLASH_DRIVE_PRODUCT_ID = "flashDriveProductId"
private const val PREF_KEY_BACKUP_APP_BLACKLIST = "backupAppBlacklist"
class SettingsManager(context: Context) { class SettingsManager(context: Context) {
private val prefs = PreferenceManager.getDefaultSharedPreferences(context) private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
private var isStorageChanging: AtomicBoolean = AtomicBoolean(false) 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 // FIXME Storage is currently plugin specific and not generic
fun setStorage(storage: Storage) { fun setStorage(storage: Storage) {
prefs.edit() prefs.edit()
@ -76,6 +83,15 @@ class SettingsManager(context: Context) {
return prefs.getBoolean(PREF_KEY_BACKUP_APK, true) 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( data class Storage(

View file

@ -3,8 +3,10 @@ package com.stevesoltys.seedvault.settings
import android.app.Application import android.app.Application
import android.content.pm.PackageManager.NameNotFoundException import android.content.pm.PackageManager.NameNotFoundException
import android.util.Log import android.util.Log
import androidx.annotation.UiThread
import androidx.core.content.ContextCompat.getDrawable import androidx.core.content.ContextCompat.getDrawable
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Transformations.switchMap import androidx.lifecycle.Transformations.switchMap
import androidx.lifecycle.liveData import androidx.lifecycle.liveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
@ -47,6 +49,9 @@ class SettingsViewModel(
private val mAppStatusList = switchMap(lastBackupTime) { getAppStatusResult() } private val mAppStatusList = switchMap(lastBackupTime) { getAppStatusResult() }
internal val appStatusList: LiveData<AppStatusResult> = mAppStatusList internal val appStatusList: LiveData<AppStatusResult> = mAppStatusList
private val mAppEditMode = MutableLiveData<Boolean>()
internal val appEditMode: LiveData<Boolean> = mAppEditMode
init { init {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
// ensures the lastBackupTime LiveData gets set // ensures the lastBackupTime LiveData gets set
@ -91,6 +96,7 @@ class SettingsViewModel(
} }
AppStatus( AppStatus(
packageName = it.packageName, packageName = it.packageName,
enabled = settingsManager.isBackupEnabled(it.packageName),
icon = icon, icon = icon,
name = getAppName(app, it.packageName).toString(), name = getAppName(app, it.packageName).toString(),
time = time, time = time,
@ -102,4 +108,14 @@ class SettingsViewModel(
emit(AppStatusResult(list, diff)) emit(AppStatusResult(list, diff))
} }
@UiThread
fun setEditMode(enabled: Boolean) {
mAppEditMode.value = enabled
}
@UiThread
fun onAppStatusToggled(status: AppStatus) {
settingsManager.onAppBackupStatusChanged(status)
}
} }

View file

@ -90,9 +90,13 @@ internal class BackupCoordinator(
} }
fun isAppEligibleForBackup(targetPackage: PackageInfo, @Suppress("UNUSED_PARAMETER") isFullBackup: Boolean): Boolean { 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. // We need to exclude the DocumentsProvider used to store backup data.
// Otherwise, it gets killed when we back it up, terminating our backup. // 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. * @return Current limit on backup size in bytes.
*/ */
fun getBackupQuota(packageName: String, isFullBackup: Boolean): Long { fun getBackupQuota(packageName: String, isFullBackup: Boolean): Long {
if (packageName != MAGIC_PACKAGE_MANAGER) {
// try to back up APK here as later methods are sometimes not called called // try to back up APK here as later methods are sometimes not called called
val pm = context.packageManager backUpApk(context.packageManager.getPackageInfo(packageName, GET_SIGNING_CERTIFICATES))
backUpApk(pm.getPackageInfo(packageName, GET_SIGNING_CERTIFICATES)) }
// report back quota // report back quota
Log.i(TAG, "Get backup quota for $packageName. Is full backup: $isFullBackup.") Log.i(TAG, "Get backup quota for $packageName. Is full backup: $isFullBackup.")

View file

@ -8,6 +8,7 @@ import android.view.View.INVISIBLE
import android.view.View.VISIBLE import android.view.View.VISIBLE
import android.widget.ImageView import android.widget.ImageView
import android.widget.ProgressBar import android.widget.ProgressBar
import android.widget.Switch
import android.widget.TextView import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.stevesoltys.seedvault.R import com.stevesoltys.seedvault.R
@ -21,18 +22,26 @@ import com.stevesoltys.seedvault.restore.AppRestoreStatus.IN_PROGRESS
import com.stevesoltys.seedvault.restore.AppRestoreStatus.SUCCEEDED 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 context: Context = v.context
protected val pm: PackageManager = context.packageManager protected val pm: PackageManager = context.packageManager
protected val clickableBackground = v.background!!
protected val appIcon: ImageView = v.findViewById(R.id.appIcon) protected val appIcon: ImageView = v.findViewById(R.id.appIcon)
protected val appName: TextView = v.findViewById(R.id.appName) protected val appName: TextView = v.findViewById(R.id.appName)
protected val appInfo: TextView = v.findViewById(R.id.appInfo) protected val appInfo: TextView = v.findViewById(R.id.appInfo)
protected val appStatus: ImageView = v.findViewById(R.id.appStatus) protected val appStatus: ImageView = v.findViewById(R.id.appStatus)
protected val progressBar: ProgressBar = v.findViewById(R.id.progressBar) 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) { protected fun setStatus(status: AppRestoreStatus) {
v.background = null
if (status == IN_PROGRESS) { if (status == IN_PROGRESS) {
appInfo.visibility = GONE appInfo.visibility = GONE
appStatus.visibility = INVISIBLE appStatus.visibility = INVISIBLE
@ -43,13 +52,9 @@ internal open class AppViewHolder(v: View) : RecyclerView.ViewHolder(v) {
appInfo.visibility = GONE appInfo.visibility = GONE
when (status) { when (status) {
SUCCEEDED -> appStatus.setImageResource(R.drawable.ic_check_green) SUCCEEDED -> appStatus.setImageResource(R.drawable.ic_check_green)
FAILED -> appStatus.setImageResource(R.drawable.ic_cancel_red) FAILED -> appStatus.setImageResource(R.drawable.ic_error_red)
else -> { else -> {
when (status) { appStatus.setImageResource(R.drawable.ic_warning_yellow)
FAILED_NO_DATA -> appStatus.setImageResource(R.drawable.ic_radio_button_unchecked_yellow)
FAILED_NOT_ALLOWED -> appStatus.setImageResource(R.drawable.ic_block_yellow)
else -> appStatus.setImageResource(R.drawable.ic_error_yellow)
}
appInfo.text = status.getInfo() appInfo.text = status.getInfo()
appInfo.visibility = VISIBLE appInfo.visibility = VISIBLE
} }

View file

@ -1,7 +1,6 @@
package com.stevesoltys.seedvault.ui package com.stevesoltys.seedvault.ui
import android.view.MenuItem import android.view.MenuItem
import android.view.View
import androidx.annotation.CallSuper import androidx.annotation.CallSuper
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
@ -10,8 +9,8 @@ import com.stevesoltys.seedvault.R
abstract class BackupActivity : AppCompatActivity() { abstract class BackupActivity : AppCompatActivity() {
@CallSuper @CallSuper
override fun onOptionsItemSelected(item: MenuItem): Boolean = when { override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
item.itemId == android.R.id.home -> { android.R.id.home -> {
onBackPressed() onBackPressed()
true true
} }
@ -25,10 +24,4 @@ abstract class BackupActivity : AppCompatActivity() {
fragmentTransaction.commit() fragmentTransaction.commit()
} }
protected fun hideSystemUI() {
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_IMMERSIVE
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_FULLSCREEN)
}
} }

View file

@ -24,7 +24,7 @@ private val TAG = RequireProvisioningActivity::class.java.name
*/ */
abstract class RequireProvisioningActivity : BackupActivity() { abstract class RequireProvisioningActivity : BackupActivity() {
protected val isSetupWizard: Boolean private val isSetupWizard: Boolean
get() = intent?.action == ACTION_SETUP_WIZARD get() = intent?.action == ACTION_SETUP_WIZARD
protected abstract fun getViewModel(): RequireProvisioningViewModel protected abstract fun getViewModel(): RequireProvisioningViewModel

View file

@ -8,7 +8,7 @@ import com.stevesoltys.seedvault.ui.storage.StorageViewModel
abstract class RequireProvisioningViewModel( abstract class RequireProvisioningViewModel(
protected val app: Application, protected val app: Application,
private val settingsManager: SettingsManager, protected val settingsManager: SettingsManager,
private val keyManager: KeyManager private val keyManager: KeyManager
) : AndroidViewModel(app) { ) : AndroidViewModel(app) {

View file

@ -5,7 +5,6 @@ import android.view.MenuItem
import com.stevesoltys.seedvault.R import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.ui.BackupActivity import com.stevesoltys.seedvault.ui.BackupActivity
import com.stevesoltys.seedvault.ui.INTENT_EXTRA_IS_RESTORE import com.stevesoltys.seedvault.ui.INTENT_EXTRA_IS_RESTORE
import com.stevesoltys.seedvault.ui.INTENT_EXTRA_IS_SETUP_WIZARD
import com.stevesoltys.seedvault.ui.LiveEventHandler import com.stevesoltys.seedvault.ui.LiveEventHandler
import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.androidx.viewmodel.ext.android.viewModel
@ -16,8 +15,6 @@ class RecoveryCodeActivity : BackupActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
if (isSetupWizard()) hideSystemUI()
setContentView(R.layout.activity_recovery_code) setContentView(R.layout.activity_recovery_code)
viewModel.isRestore = isRestore() viewModel.isRestore = isRestore()
@ -38,8 +35,8 @@ class RecoveryCodeActivity : BackupActivity() {
} }
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when { return when (item.itemId) {
item.itemId == android.R.id.home -> { android.R.id.home -> {
onBackPressed() onBackPressed()
true true
} }
@ -65,8 +62,4 @@ class RecoveryCodeActivity : BackupActivity() {
return intent?.getBooleanExtra(INTENT_EXTRA_IS_RESTORE, false) ?: false return intent?.getBooleanExtra(INTENT_EXTRA_IS_RESTORE, false) ?: false
} }
private fun isSetupWizard(): Boolean {
return intent?.getBooleanExtra(INTENT_EXTRA_IS_SETUP_WIZARD, false) ?: false
}
} }

View file

@ -22,8 +22,6 @@ class StorageActivity : BackupActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
if (isSetupWizard()) hideSystemUI()
setContentView(R.layout.activity_fragment_container) setContentView(R.layout.activity_fragment_container)
viewModel = if (isRestore()) { viewModel = if (isRestore()) {

View file

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="@color/yellow"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM4,12c0,-4.42 3.58,-8 8,-8 1.85,0 3.55,0.63 4.9,1.69L5.69,16.9C4.63,15.55 4,13.85 4,12zM12,20c-1.85,0 -3.55,-0.63 -4.9,-1.69L18.31,7.1C19.37,8.45 20,10.15 20,12c0,4.42 -3.58,8 -8,8z" />
</vector>

View file

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="@color/red"
android:pathData="M12,2C6.47,2 2,6.47 2,12s4.47,10 10,10 10,-4.47 10,-10S17.53,2 12,2zM17,15.59L15.59,17 12,13.41 8.41,17 7,15.59 10.59,12 7,8.41 8.41,7 12,10.59 15.59,7 17,8.41 13.41,12 17,15.59z" />
</vector>

View file

@ -4,6 +4,6 @@
android:viewportWidth="24.0" android:viewportWidth="24.0"
android:viewportHeight="24.0"> android:viewportHeight="24.0">
<path <path
android:fillColor="@color/yellow" android:fillColor="@color/red"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-2h2v2zM13,13h-2L11,7h2v6z" /> android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-2h2v2zM13,13h-2L11,7h2v6z" />
</vector> </vector>

View file

@ -5,5 +5,5 @@
android:viewportHeight="24.0"> android:viewportHeight="24.0">
<path <path
android:fillColor="@color/yellow" android:fillColor="@color/yellow"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8z" /> android:pathData="M1,21h22L12,2 1,21zM13,18h-2v-2h2v2zM13,14h-2v-4h2v4z" />
</vector> </vector>

View file

@ -88,6 +88,19 @@
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/licenseView" /> app:layout_constraintTop_toBottomOf="@+id/licenseView" />
<TextView
android:id="@+id/designView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:text="@string/about_design"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/authorView" />
<TextView <TextView
android:id="@+id/sponsorView" android:id="@+id/sponsorView"
android:layout_width="wrap_content" android:layout_width="wrap_content"
@ -99,7 +112,7 @@
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0" app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/authorView" /> app:layout_constraintTop_toBottomOf="@+id/designView" />
<TextView <TextView
android:id="@+id/sourceCodeView" android:id="@+id/sourceCodeView"
@ -109,10 +122,10 @@
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp" android:layout_marginBottom="16dp"
android:autoLink="web"
android:text="@string/about_source_code" android:text="@string/about_source_code"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
android:autoLink="web"
app:layout_constraintHorizontal_bias="0.0" app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/sponsorView" app:layout_constraintTop_toBottomOf="@+id/sponsorView"

View file

@ -8,9 +8,6 @@
android:id="@+id/list" android:id="@+id/list"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:scrollbarStyle="outsideOverlay"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"

View file

@ -58,7 +58,8 @@
android:id="@+id/appList" android:id="@+id/appList"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="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_constraintBottom_toTopOf="@+id/button"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/backupNameView" app:layout_constraintTop_toBottomOf="@+id/backupNameView"

View file

@ -4,8 +4,11 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="8dp" android:background="?android:selectableItemBackground"
android:layout_marginBottom="8dp"> android:paddingStart="16dp"
android:paddingTop="8dp"
android:paddingEnd="16dp"
android:paddingBottom="8dp">
<ImageView <ImageView
android:id="@+id/appIcon" android:id="@+id/appIcon"
@ -26,7 +29,7 @@
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
app:layout_constraintBottom_toTopOf="@+id/appInfo" 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_constraintStart_toEndOf="@+id/appIcon"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
tools:text="Seedvault Backup" /> tools:text="Seedvault Backup" />
@ -46,8 +49,8 @@
<ImageView <ImageView
android:id="@+id/appStatus" android:id="@+id/appStatus"
android:layout_width="32dp" android:layout_width="24dp"
android:layout_height="32dp" android:layout_height="24dp"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
@ -57,10 +60,20 @@
<ProgressBar <ProgressBar
android:id="@+id/progressBar" android:id="@+id/progressBar"
style="?android:attr/progressBarStyle" style="?android:attr/progressBarStyle"
android:layout_width="32dp" android:layout_width="24dp"
android:layout_height="32dp" android:layout_height="24dp"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="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> </androidx.constraintlayout.widget.ConstraintLayout>

View 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>

View file

@ -27,6 +27,7 @@
<string name="settings_backup_apk_dialog_disable">Disable app backup</string> <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_title">App backup status</string>
<string name="settings_backup_status_summary">Last Backup: %1$s</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> <string name="settings_backup_now">Backup now</string>
<!-- Storage --> <!-- Storage -->
@ -120,6 +121,7 @@
<string name="about_summary">A backup application using Android\'s internal backup API.</string> <string name="about_summary">A backup application using Android\'s internal backup API.</string>
<string name="about_license">License: <a href="https://www.apache.org/licenses/LICENSE-2.0">Apache2</a></string> <string name="about_license">License: <a href="https://www.apache.org/licenses/LICENSE-2.0">Apache2</a></string>
<string name="about_author">Written by: <a href="https://github.com/stevesoltys">Steve Soltys</a> and <a href="https://blog.grobox.de">Torsten Grote</a></string> <string name="about_author">Written by: <a href="https://github.com/stevesoltys">Steve Soltys</a> and <a href="https://blog.grobox.de">Torsten Grote</a></string>
<string name="about_design">Design by: <a href="https://www.glennsorrentino.com/">Glenn Sorrentino</a></string>
<string name="about_sponsor">Sponsored by: <a href="https://www.calyxinstitute.org">Calyx Institute</a> for use in <a href="https://calyxos.org">CalyxOS</a></string> <string name="about_sponsor">Sponsored by: <a href="https://www.calyxinstitute.org">Calyx Institute</a> for use in <a href="https://calyxos.org">CalyxOS</a></string>
<string name="about_source_code">Source Code: https://github.com/stevesoltys/seedvault</string> <string name="about_source_code">Source Code: https://github.com/stevesoltys/seedvault</string>

View file

@ -23,7 +23,9 @@ import io.mockk.just
import io.mockk.mockk import io.mockk.mockk
import io.mockk.verify import io.mockk.verify
import org.junit.jupiter.api.Assertions.assertEquals 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.assertThrows
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import java.io.IOException import java.io.IOException
import java.io.OutputStream import java.io.OutputStream
@ -122,6 +124,20 @@ internal class BackupCoordinatorTest : BackupTest() {
assertEquals(quota, backup.getBackupQuota(packageInfo.packageName, isFullBackup)) 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 @Test
fun `clearing KV backup data throws`() { fun `clearing KV backup data throws`() {
every { kv.clearBackupData(packageInfo) } throws IOException() every { kv.clearBackupData(packageInfo) } throws IOException()