Merge pull request #71 from grote/69-app-backup-status
Show list of apps and their backup status
This commit is contained in:
commit
3ae600ea8e
22 changed files with 474 additions and 140 deletions
|
@ -29,6 +29,9 @@ android {
|
|||
targetCompatibility 1.8
|
||||
sourceCompatibility 1.8
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
||||
}
|
||||
testOptions {
|
||||
unitTests.all {
|
||||
useJUnitPlatform()
|
||||
|
|
|
@ -7,7 +7,6 @@ import android.app.NotificationManager.IMPORTANCE_HIGH
|
|||
import android.app.NotificationManager.IMPORTANCE_LOW
|
||||
import android.app.PendingIntent
|
||||
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
|
||||
import android.app.backup.BackupTransport.TRANSPORT_PACKAGE_REJECTED
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager.NameNotFoundException
|
||||
|
@ -19,6 +18,7 @@ import androidx.core.app.NotificationCompat.PRIORITY_LOW
|
|||
import com.stevesoltys.seedvault.restore.ACTION_RESTORE_ERROR_UNINSTALL
|
||||
import com.stevesoltys.seedvault.restore.EXTRA_PACKAGE_NAME
|
||||
import com.stevesoltys.seedvault.restore.REQUEST_CODE_UNINSTALL
|
||||
import com.stevesoltys.seedvault.settings.ACTION_APP_STATUS_LIST
|
||||
import com.stevesoltys.seedvault.settings.SettingsActivity
|
||||
|
||||
private const val CHANNEL_ID_OBSERVER = "NotificationBackupObserver"
|
||||
|
@ -78,23 +78,6 @@ class BackupNotificationManager(private val context: Context) {
|
|||
nm.notify(NOTIFICATION_ID_OBSERVER, notification)
|
||||
}
|
||||
|
||||
fun onBackupResult(app: CharSequence, status: Int, userInitiated: Boolean) {
|
||||
val title = context.getString(when (status) {
|
||||
0 -> R.string.notification_backup_result_complete
|
||||
TRANSPORT_PACKAGE_REJECTED -> R.string.notification_backup_result_rejected
|
||||
else -> R.string.notification_backup_result_error
|
||||
})
|
||||
val notification = observerBuilder.apply {
|
||||
setContentTitle(title)
|
||||
setContentText(app)
|
||||
setOngoing(true)
|
||||
setShowWhen(false)
|
||||
setWhen(System.currentTimeMillis())
|
||||
priority = if (userInitiated) PRIORITY_DEFAULT else PRIORITY_LOW
|
||||
}.build()
|
||||
nm.notify(NOTIFICATION_ID_OBSERVER, notification)
|
||||
}
|
||||
|
||||
fun onBackupFinished(success: Boolean, notBackedUp: Int?, userInitiated: Boolean) {
|
||||
if (!userInitiated) {
|
||||
nm.cancel(NOTIFICATION_ID_OBSERVER)
|
||||
|
@ -104,11 +87,19 @@ class BackupNotificationManager(private val context: Context) {
|
|||
val contentText = if (notBackedUp == null) null else {
|
||||
context.getString(R.string.notification_success_num_not_backed_up, notBackedUp)
|
||||
}
|
||||
val iconRes = if (success) R.drawable.ic_cloud_done else R.drawable.ic_cloud_error
|
||||
val intent = Intent(context, SettingsActivity::class.java).apply {
|
||||
action = ACTION_APP_STATUS_LIST
|
||||
}
|
||||
val pendingIntent = PendingIntent.getActivity(context, 0, intent, 0)
|
||||
val notification = observerBuilder.apply {
|
||||
setContentTitle(context.getString(titleRes))
|
||||
setContentText(contentText)
|
||||
setOngoing(false)
|
||||
setShowWhen(true)
|
||||
setAutoCancel(true)
|
||||
setSmallIcon(iconRes)
|
||||
setContentIntent(pendingIntent)
|
||||
setWhen(System.currentTimeMillis())
|
||||
setProgress(0, 0, false)
|
||||
priority = PRIORITY_LOW
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package com.stevesoltys.seedvault.crypto
|
||||
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
import android.security.keystore.KeyProperties.BLOCK_MODE_GCM
|
||||
import android.security.keystore.KeyProperties.ENCRYPTION_PADDING_NONE
|
||||
import android.security.keystore.KeyProperties.PURPOSE_DECRYPT
|
||||
|
@ -48,7 +47,7 @@ internal class KeyManagerImpl : KeyManager {
|
|||
|
||||
override fun storeBackupKey(seed: ByteArray) {
|
||||
if (seed.size < KEY_SIZE_BYTES) throw IllegalArgumentException()
|
||||
// TODO check if using first 256 of 512 bytes produced by PBKDF2WithHmacSHA512 is safe!
|
||||
// TODO check if using first 256 of 512 bits produced by PBKDF2WithHmacSHA512 is safe!
|
||||
val secretKeySpec = SecretKeySpec(seed, 0, KEY_SIZE_BYTES, "AES")
|
||||
val ksEntry = SecretKeyEntry(secretKeySpec)
|
||||
keyStore.setEntry(KEY_ALIAS, ksEntry, getKeyProtection())
|
||||
|
@ -68,7 +67,7 @@ internal class KeyManagerImpl : KeyManager {
|
|||
.setEncryptionPaddings(ENCRYPTION_PADDING_NONE)
|
||||
.setRandomizedEncryptionRequired(true)
|
||||
// unlocking is required only for decryption, so when restoring from backup
|
||||
if (SDK_INT >= 28) builder.setUnlockedDeviceRequired(true)
|
||||
builder.setUnlockedDeviceRequired(true)
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
|
|
|
@ -8,6 +8,9 @@ import android.content.pm.PackageInfo
|
|||
import android.util.Log
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.distinctUntilChanged
|
||||
import com.stevesoltys.seedvault.Clock
|
||||
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
||||
import com.stevesoltys.seedvault.metadata.PackageState.APK_AND_DATA
|
||||
|
@ -37,6 +40,7 @@ class MetadataManager(
|
|||
// If this happens, it is hard to recover from this. Let's hope it never does.
|
||||
throw AssertionError("Error reading metadata from cache", e)
|
||||
}
|
||||
mLastBackupTime.postValue(field.time)
|
||||
}
|
||||
return field
|
||||
}
|
||||
|
@ -155,6 +159,7 @@ class MetadataManager(
|
|||
metadata = oldMetadata
|
||||
throw IOException(e)
|
||||
}
|
||||
mLastBackupTime.postValue(metadata.time)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -171,7 +176,10 @@ class MetadataManager(
|
|||
* Note that this might be a blocking I/O call.
|
||||
*/
|
||||
@Synchronized
|
||||
fun getLastBackupTime(): Long = metadata.time
|
||||
fun getLastBackupTime(): Long = mLastBackupTime.value ?: metadata.time
|
||||
|
||||
private val mLastBackupTime = MutableLiveData<Long>()
|
||||
internal val lastBackupTime: LiveData<Long> = mLastBackupTime.distinctUntilChanged()
|
||||
|
||||
@Synchronized
|
||||
fun getPackageMetadata(packageName: String): PackageMetadata? {
|
||||
|
|
|
@ -5,11 +5,7 @@ import android.view.View
|
|||
import android.view.View.INVISIBLE
|
||||
import android.view.View.VISIBLE
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView.Adapter
|
||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||
import androidx.recyclerview.widget.SortedList
|
||||
import androidx.recyclerview.widget.SortedListAdapterCallback
|
||||
import com.stevesoltys.seedvault.R
|
||||
|
@ -18,8 +14,9 @@ import com.stevesoltys.seedvault.transport.restore.ApkRestoreStatus.FAILED
|
|||
import com.stevesoltys.seedvault.transport.restore.ApkRestoreStatus.IN_PROGRESS
|
||||
import com.stevesoltys.seedvault.transport.restore.ApkRestoreStatus.QUEUED
|
||||
import com.stevesoltys.seedvault.transport.restore.ApkRestoreStatus.SUCCEEDED
|
||||
import com.stevesoltys.seedvault.ui.AppViewHolder
|
||||
|
||||
internal class InstallProgressAdapter : Adapter<AppViewHolder>() {
|
||||
internal class InstallProgressAdapter : Adapter<AppInstallViewHolder>() {
|
||||
|
||||
private val items = SortedList<ApkRestoreResult>(ApkRestoreResult::class.java, object : SortedListAdapterCallback<ApkRestoreResult>(this) {
|
||||
override fun areItemsTheSame(item1: ApkRestoreResult, item2: ApkRestoreResult) = item1.packageName == item2.packageName
|
||||
|
@ -27,14 +24,14 @@ internal class InstallProgressAdapter : Adapter<AppViewHolder>() {
|
|||
override fun compare(item1: ApkRestoreResult, item2: ApkRestoreResult) = item1.compareTo(item2)
|
||||
})
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AppViewHolder {
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AppInstallViewHolder {
|
||||
val v = LayoutInflater.from(parent.context).inflate(R.layout.list_item_app_status, parent, false)
|
||||
return AppViewHolder(v)
|
||||
return AppInstallViewHolder(v)
|
||||
}
|
||||
|
||||
override fun getItemCount() = items.size()
|
||||
|
||||
override fun onBindViewHolder(holder: AppViewHolder, position: Int) {
|
||||
override fun onBindViewHolder(holder: AppInstallViewHolder, position: Int) {
|
||||
holder.bind(items[position])
|
||||
}
|
||||
|
||||
|
@ -43,12 +40,7 @@ internal class InstallProgressAdapter : Adapter<AppViewHolder>() {
|
|||
}
|
||||
}
|
||||
|
||||
internal class AppViewHolder(v: View) : ViewHolder(v) {
|
||||
|
||||
private val appIcon: ImageView = v.findViewById(R.id.appIcon)
|
||||
private val appName: TextView = v.findViewById(R.id.appName)
|
||||
private val appStatus: ImageView = v.findViewById(R.id.appStatus)
|
||||
private val progressBar: ProgressBar = v.findViewById(R.id.progressBar)
|
||||
internal class AppInstallViewHolder(v: View) : AppViewHolder(v) {
|
||||
|
||||
fun bind(item: ApkRestoreResult) {
|
||||
appIcon.setImageDrawable(item.icon)
|
||||
|
|
|
@ -3,26 +3,13 @@ package com.stevesoltys.seedvault.restore
|
|||
import android.content.pm.PackageManager.NameNotFoundException
|
||||
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 android.widget.ImageView
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView.Adapter
|
||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
||||
import com.stevesoltys.seedvault.R
|
||||
import com.stevesoltys.seedvault.restore.AppRestoreStatus.FAILED
|
||||
import com.stevesoltys.seedvault.restore.AppRestoreStatus.FAILED_NOT_ALLOWED
|
||||
import com.stevesoltys.seedvault.restore.AppRestoreStatus.FAILED_NOT_INSTALLED
|
||||
import com.stevesoltys.seedvault.restore.AppRestoreStatus.FAILED_NO_DATA
|
||||
import com.stevesoltys.seedvault.restore.AppRestoreStatus.FAILED_QUOTA_EXCEEDED
|
||||
import com.stevesoltys.seedvault.restore.AppRestoreStatus.IN_PROGRESS
|
||||
import com.stevesoltys.seedvault.restore.AppRestoreStatus.SUCCEEDED
|
||||
import com.stevesoltys.seedvault.restore.RestoreProgressAdapter.PackageViewHolder
|
||||
import com.stevesoltys.seedvault.ui.AppViewHolder
|
||||
import java.util.*
|
||||
|
||||
internal class RestoreProgressAdapter : Adapter<PackageViewHolder>() {
|
||||
|
@ -63,16 +50,7 @@ internal class RestoreProgressAdapter : Adapter<PackageViewHolder>() {
|
|||
}
|
||||
}
|
||||
|
||||
inner class PackageViewHolder(v: View) : ViewHolder(v) {
|
||||
|
||||
private val context = v.context
|
||||
private val pm = context.packageManager
|
||||
private val appIcon: ImageView = v.findViewById(R.id.appIcon)
|
||||
private val appName: TextView = v.findViewById(R.id.appName)
|
||||
private val appInfo: TextView = v.findViewById(R.id.appInfo)
|
||||
private val appStatus: ImageView = v.findViewById(R.id.appStatus)
|
||||
private val progressBar: ProgressBar = v.findViewById(R.id.progressBar)
|
||||
|
||||
inner class PackageViewHolder(v: View) : AppViewHolder(v) {
|
||||
fun bind(item: AppRestoreResult) {
|
||||
appName.text = item.name
|
||||
if (item.packageName == MAGIC_PACKAGE_MANAGER) {
|
||||
|
@ -84,38 +62,8 @@ internal class RestoreProgressAdapter : Adapter<PackageViewHolder>() {
|
|||
appIcon.setImageResource(R.drawable.ic_launcher_default)
|
||||
}
|
||||
}
|
||||
if (item.status == IN_PROGRESS) {
|
||||
appInfo.visibility = GONE
|
||||
appStatus.visibility = INVISIBLE
|
||||
progressBar.visibility = VISIBLE
|
||||
} else {
|
||||
appStatus.visibility = VISIBLE
|
||||
progressBar.visibility = INVISIBLE
|
||||
appInfo.visibility = GONE
|
||||
when (item.status) {
|
||||
SUCCEEDED -> {
|
||||
appStatus.setImageResource(R.drawable.ic_check_green)
|
||||
}
|
||||
FAILED -> {
|
||||
appStatus.setImageResource(R.drawable.ic_cancel_red)
|
||||
}
|
||||
else -> {
|
||||
appStatus.setImageResource(R.drawable.ic_error_yellow)
|
||||
appInfo.text = getInfo(item.status)
|
||||
appInfo.visibility = VISIBLE
|
||||
}
|
||||
}
|
||||
}
|
||||
setStatus(item.status)
|
||||
}
|
||||
|
||||
private fun getInfo(status: AppRestoreStatus): String = when(status) {
|
||||
FAILED_NO_DATA -> context.getString(R.string.restore_app_no_data)
|
||||
FAILED_NOT_ALLOWED -> context.getString(R.string.restore_app_not_allowed)
|
||||
FAILED_NOT_INSTALLED -> context.getString(R.string.restore_app_not_installed)
|
||||
FAILED_QUOTA_EXCEEDED -> context.getString(R.string.restore_app_quota_exceeded)
|
||||
else -> "Please report a bug after you read this."
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
package com.stevesoltys.seedvault.settings
|
||||
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
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 com.stevesoltys.seedvault.R
|
||||
import com.stevesoltys.seedvault.restore.AppRestoreStatus
|
||||
import com.stevesoltys.seedvault.restore.AppRestoreStatus.SUCCEEDED
|
||||
import com.stevesoltys.seedvault.settings.AppStatusAdapter.AppStatusViewHolder
|
||||
import com.stevesoltys.seedvault.ui.AppViewHolder
|
||||
import com.stevesoltys.seedvault.ui.toRelativeTime
|
||||
|
||||
internal class AppStatusAdapter : Adapter<AppStatusViewHolder>() {
|
||||
|
||||
private val items = ArrayList<AppStatus>()
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AppStatusViewHolder {
|
||||
val v = LayoutInflater.from(parent.context).inflate(R.layout.list_item_app_status, parent, false)
|
||||
return AppStatusViewHolder(v)
|
||||
}
|
||||
|
||||
override fun getItemCount() = items.size
|
||||
|
||||
override fun onBindViewHolder(holder: AppStatusViewHolder, position: Int) {
|
||||
holder.bind(items[position])
|
||||
}
|
||||
|
||||
fun update(newItems: List<AppStatus>, diff: DiffResult) {
|
||||
items.clear()
|
||||
items.addAll(newItems)
|
||||
diff.dispatchUpdatesTo(this)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
internal data class AppStatus(
|
||||
val packageName: String,
|
||||
val icon: Drawable,
|
||||
val name: String,
|
||||
val time: Long,
|
||||
val status: AppRestoreStatus)
|
||||
|
||||
internal class AppStatusDiff(
|
||||
private val oldItems: List<AppStatus>,
|
||||
private val newItems: List<AppStatus>) : DiffUtil.Callback() {
|
||||
|
||||
override fun getOldListSize() = oldItems.size
|
||||
override fun getNewListSize() = newItems.size
|
||||
|
||||
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||
return oldItems[oldItemPosition].packageName == newItems[newItemPosition].packageName
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||
return oldItems[oldItemPosition] == newItems[newItemPosition]
|
||||
}
|
||||
}
|
||||
|
||||
internal class AppStatusResult(
|
||||
val appStatusList: List<AppStatus>,
|
||||
val diff: DiffResult
|
||||
)
|
|
@ -0,0 +1,45 @@
|
|||
package com.stevesoltys.seedvault.settings
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.View.INVISIBLE
|
||||
import android.view.View.VISIBLE
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.stevesoltys.seedvault.R
|
||||
import kotlinx.android.synthetic.main.fragment_app_status.*
|
||||
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
|
||||
|
||||
class AppStatusFragment : Fragment() {
|
||||
|
||||
private val viewModel: SettingsViewModel by sharedViewModel()
|
||||
|
||||
private val layoutManager = LinearLayoutManager(context)
|
||||
private val adapter = AppStatusAdapter()
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?): View? {
|
||||
return inflater.inflate(R.layout.fragment_app_status, container, false)
|
||||
}
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
|
||||
activity?.setTitle(R.string.settings_backup_status_title)
|
||||
|
||||
list.apply {
|
||||
layoutManager = this@AppStatusFragment.layoutManager
|
||||
adapter = this@AppStatusFragment.adapter
|
||||
}
|
||||
|
||||
progressBar.visibility = VISIBLE
|
||||
viewModel.appStatusList.observe(this, Observer { result ->
|
||||
adapter.update(result.appStatusList, result.diff)
|
||||
progressBar.visibility = INVISIBLE
|
||||
})
|
||||
}
|
||||
|
||||
}
|
|
@ -2,6 +2,9 @@ package com.stevesoltys.seedvault.settings
|
|||
|
||||
import android.os.Bundle
|
||||
import androidx.annotation.CallSuper
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import androidx.preference.PreferenceFragmentCompat.OnPreferenceStartFragmentCallback
|
||||
import com.stevesoltys.seedvault.BackupNotificationManager
|
||||
import com.stevesoltys.seedvault.R
|
||||
import com.stevesoltys.seedvault.ui.RequireProvisioningActivity
|
||||
|
@ -9,7 +12,9 @@ import com.stevesoltys.seedvault.ui.RequireProvisioningViewModel
|
|||
import org.koin.android.ext.android.inject
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
|
||||
class SettingsActivity : RequireProvisioningActivity() {
|
||||
internal const val ACTION_APP_STATUS_LIST = "com.stevesoltys.seedvault.APP_STATUS_LIST"
|
||||
|
||||
class SettingsActivity : RequireProvisioningActivity(), OnPreferenceStartFragmentCallback {
|
||||
|
||||
private val viewModel: SettingsViewModel by viewModel()
|
||||
private val notificationManager: BackupNotificationManager by inject()
|
||||
|
@ -23,7 +28,11 @@ class SettingsActivity : RequireProvisioningActivity() {
|
|||
|
||||
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
|
||||
|
||||
if (savedInstanceState == null) showFragment(SettingsFragment())
|
||||
if (intent?.action == ACTION_APP_STATUS_LIST) {
|
||||
showFragment(AppStatusFragment())
|
||||
} else if (savedInstanceState == null) {
|
||||
showFragment(SettingsFragment())
|
||||
}
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
|
@ -41,4 +50,14 @@ class SettingsActivity : RequireProvisioningActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
override fun onPreferenceStartFragment(caller: PreferenceFragmentCompat, pref: Preference): Boolean {
|
||||
val fragment = supportFragmentManager.fragmentFactory.instantiate(classLoader, pref.fragment)
|
||||
fragment.setTargetFragment(caller, 0)
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.fragment, fragment)
|
||||
.addToBackStack(null)
|
||||
.commit()
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -12,8 +12,6 @@ import android.os.Bundle
|
|||
import android.os.RemoteException
|
||||
import android.provider.Settings
|
||||
import android.provider.Settings.Secure.BACKUP_AUTO_RESTORE
|
||||
import android.text.format.DateUtils.MINUTE_IN_MILLIS
|
||||
import android.text.format.DateUtils.getRelativeTimeSpanString
|
||||
import android.util.Log
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
|
@ -28,6 +26,7 @@ import com.stevesoltys.seedvault.R
|
|||
import com.stevesoltys.seedvault.UsbMonitor
|
||||
import com.stevesoltys.seedvault.isMassStorage
|
||||
import com.stevesoltys.seedvault.restore.RestoreActivity
|
||||
import com.stevesoltys.seedvault.ui.toRelativeTime
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
|
||||
|
||||
|
@ -43,6 +42,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
|||
private lateinit var autoRestore: TwoStatePreference
|
||||
private lateinit var apkBackup: TwoStatePreference
|
||||
private lateinit var backupLocation: Preference
|
||||
private lateinit var backupStatus: Preference
|
||||
|
||||
private var menuBackupNow: MenuItem? = null
|
||||
private var menuRestore: MenuItem? = null
|
||||
|
@ -65,7 +65,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
|||
setPreferencesFromResource(R.xml.settings, rootKey)
|
||||
setHasOptionsMenu(true)
|
||||
|
||||
backup = findPreference<TwoStatePreference>("backup")!!
|
||||
backup = findPreference("backup")!!
|
||||
backup.onPreferenceChangeListener = OnPreferenceChangeListener { _, newValue ->
|
||||
val enabled = newValue as Boolean
|
||||
try {
|
||||
|
@ -78,13 +78,13 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
|||
}
|
||||
}
|
||||
|
||||
backupLocation = findPreference<Preference>("backup_location")!!
|
||||
backupLocation = findPreference("backup_location")!!
|
||||
backupLocation.setOnPreferenceClickListener {
|
||||
viewModel.chooseBackupLocation()
|
||||
true
|
||||
}
|
||||
|
||||
autoRestore = findPreference<TwoStatePreference>("auto_restore")!!
|
||||
autoRestore = findPreference("auto_restore")!!
|
||||
autoRestore.onPreferenceChangeListener = OnPreferenceChangeListener { _, newValue ->
|
||||
val enabled = newValue as Boolean
|
||||
try {
|
||||
|
@ -115,8 +115,9 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
|||
.show()
|
||||
return@OnPreferenceChangeListener false
|
||||
}
|
||||
backupStatus = findPreference("backup_status")!!
|
||||
|
||||
viewModel.lastBackupTime.observe(this, Observer { time -> setBackupLocationSummary(time) })
|
||||
viewModel.lastBackupTime.observe(this, Observer { time -> setAppBackupStatusSummary(time) })
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
|
@ -126,10 +127,10 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
|||
activity?.setTitle(R.string.backup)
|
||||
|
||||
storage = settingsManager.getStorage()
|
||||
setBackupState()
|
||||
setBackupEnabledState()
|
||||
setBackupLocationSummary()
|
||||
setAutoRestoreState()
|
||||
setMenuItemStates()
|
||||
viewModel.updateLastBackupTime()
|
||||
|
||||
if (storage?.isUsb == true) context?.registerReceiver(usbReceiver, usbFilter)
|
||||
}
|
||||
|
@ -150,23 +151,23 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
|||
setMenuItemStates()
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean = when {
|
||||
item.itemId == R.id.action_backup -> {
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
|
||||
R.id.action_backup -> {
|
||||
viewModel.backupNow()
|
||||
true
|
||||
}
|
||||
item.itemId == R.id.action_restore -> {
|
||||
R.id.action_restore -> {
|
||||
startActivity(Intent(requireContext(), RestoreActivity::class.java))
|
||||
true
|
||||
}
|
||||
item.itemId == R.id.action_about -> {
|
||||
R.id.action_about -> {
|
||||
AboutDialogFragment().show(fragmentManager!!, AboutDialogFragment.TAG)
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
private fun setBackupState() {
|
||||
private fun setBackupEnabledState() {
|
||||
try {
|
||||
backup.isChecked = backupManager.isBackupEnabled
|
||||
backup.isEnabled = true
|
||||
|
@ -189,17 +190,15 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun setBackupLocationSummary(lastBackupInMillis: Long) {
|
||||
private fun setBackupLocationSummary() {
|
||||
// get name of storage location
|
||||
val storageName = storage?.name ?: getString(R.string.settings_backup_location_none)
|
||||
backupLocation.summary = storage?.name ?: getString(R.string.settings_backup_location_none)
|
||||
}
|
||||
|
||||
private fun setAppBackupStatusSummary(lastBackupInMillis: Long) {
|
||||
// set time of last backup
|
||||
val lastBackup = if (lastBackupInMillis == 0L) {
|
||||
getString(R.string.settings_backup_last_backup_never)
|
||||
} else {
|
||||
getRelativeTimeSpanString(lastBackupInMillis, System.currentTimeMillis(), MINUTE_IN_MILLIS, 0)
|
||||
}
|
||||
backupLocation.summary = getString(R.string.settings_backup_location_summary, storageName, lastBackup)
|
||||
val lastBackup = lastBackupInMillis.toRelativeTime(requireContext())
|
||||
backupStatus.summary = getString(R.string.settings_backup_status_summary, lastBackup)
|
||||
}
|
||||
|
||||
private fun setMenuItemStates() {
|
||||
|
|
|
@ -1,12 +1,37 @@
|
|||
package com.stevesoltys.seedvault.settings
|
||||
|
||||
import android.app.Application
|
||||
import android.content.pm.PackageManager.NameNotFoundException
|
||||
import android.util.Log
|
||||
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
|
||||
import androidx.recyclerview.widget.DiffUtil.calculateDiff
|
||||
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
||||
import com.stevesoltys.seedvault.R
|
||||
import com.stevesoltys.seedvault.crypto.KeyManager
|
||||
import com.stevesoltys.seedvault.getAppName
|
||||
import com.stevesoltys.seedvault.metadata.MetadataManager
|
||||
import com.stevesoltys.seedvault.metadata.PackageState.APK_AND_DATA
|
||||
import com.stevesoltys.seedvault.metadata.PackageState.NOT_ALLOWED
|
||||
import com.stevesoltys.seedvault.metadata.PackageState.NO_DATA
|
||||
import com.stevesoltys.seedvault.metadata.PackageState.QUOTA_EXCEEDED
|
||||
import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR
|
||||
import com.stevesoltys.seedvault.metadata.isSystemApp
|
||||
import com.stevesoltys.seedvault.restore.AppRestoreStatus.FAILED
|
||||
import com.stevesoltys.seedvault.restore.AppRestoreStatus.FAILED_NOT_ALLOWED
|
||||
import com.stevesoltys.seedvault.restore.AppRestoreStatus.FAILED_NO_DATA
|
||||
import com.stevesoltys.seedvault.restore.AppRestoreStatus.FAILED_QUOTA_EXCEEDED
|
||||
import com.stevesoltys.seedvault.restore.AppRestoreStatus.SUCCEEDED
|
||||
import com.stevesoltys.seedvault.transport.requestBackup
|
||||
import com.stevesoltys.seedvault.ui.RequireProvisioningViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.*
|
||||
|
||||
private val TAG = SettingsViewModel::class.java.simpleName
|
||||
|
||||
class SettingsViewModel(
|
||||
app: Application,
|
||||
|
@ -17,15 +42,64 @@ class SettingsViewModel(
|
|||
|
||||
override val isRestoreOperation = false
|
||||
|
||||
private val _lastBackupTime = MutableLiveData<Long>()
|
||||
internal val lastBackupTime: LiveData<Long> = _lastBackupTime
|
||||
internal val lastBackupTime = metadataManager.lastBackupTime
|
||||
|
||||
internal fun updateLastBackupTime() {
|
||||
Thread { _lastBackupTime.postValue(metadataManager.getLastBackupTime()) }.start()
|
||||
private val mAppStatusList = switchMap(lastBackupTime) { getAppStatusResult() }
|
||||
internal val appStatusList: LiveData<AppStatusResult> = mAppStatusList
|
||||
|
||||
init {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
// ensures the lastBackupTime LiveData gets set
|
||||
metadataManager.getLastBackupTime()
|
||||
}
|
||||
}
|
||||
|
||||
internal fun backupNow() {
|
||||
Thread { requestBackup(app) }.start()
|
||||
}
|
||||
|
||||
private fun getAppStatusResult(): LiveData<AppStatusResult> = liveData(Dispatchers.Main) {
|
||||
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) {
|
||||
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
|
||||
}
|
||||
if (metadata?.hasApk() == false) {
|
||||
Log.w(TAG, "No APK stored for: ${it.packageName}")
|
||||
}
|
||||
AppStatus(
|
||||
packageName = 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))
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import android.app.backup.BackupTransport.TRANSPORT_PACKAGE_REJECTED
|
|||
import android.app.backup.BackupTransport.TRANSPORT_QUOTA_EXCEEDED
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageInfo
|
||||
import android.content.pm.PackageManager.GET_SIGNING_CERTIFICATES
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.util.Log
|
||||
import com.stevesoltys.seedvault.BackupNotificationManager
|
||||
|
@ -94,7 +95,20 @@ internal class BackupCoordinator(
|
|||
return targetPackage.packageName != plugin.providerPackageName
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask the transport about current quota for backup size of the package.
|
||||
*
|
||||
* @param packageName ID of package to provide the quota.
|
||||
* @param isFullBackup If set, transport should return limit for full data backup,
|
||||
* otherwise for key-value backup.
|
||||
* @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))
|
||||
|
||||
// report back quota
|
||||
Log.i(TAG, "Get backup quota for $packageName. Is full backup: $isFullBackup.")
|
||||
val quota = if (isFullBackup) full.getQuota() else kv.getQuota()
|
||||
Log.i(TAG, "Reported quota of $quota bytes.")
|
||||
|
@ -132,8 +146,7 @@ internal class BackupCoordinator(
|
|||
// hook in here to back up APKs of apps that are otherwise not allowed for backup
|
||||
backUpNotAllowedPackages()
|
||||
}
|
||||
val result = kv.performBackup(packageInfo, data, flags)
|
||||
return backUpApk(result, packageInfo)
|
||||
return kv.performBackup(packageInfo, data, flags)
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------
|
||||
|
@ -166,8 +179,7 @@ internal class BackupCoordinator(
|
|||
|
||||
fun performFullBackup(targetPackage: PackageInfo, fileDescriptor: ParcelFileDescriptor, flags: Int): Int {
|
||||
cancelReason = UNKNOWN_ERROR
|
||||
val result = full.performFullBackup(targetPackage, fileDescriptor, flags)
|
||||
return backUpApk(result, targetPackage)
|
||||
return full.performFullBackup(targetPackage, fileDescriptor, flags)
|
||||
}
|
||||
|
||||
fun sendBackupData(numBytes: Int) = full.sendBackupData(numBytes)
|
||||
|
@ -254,27 +266,24 @@ internal class BackupCoordinator(
|
|||
Log.d(TAG, "Checking if APKs of opt-out apps need backup...")
|
||||
packageService.notAllowedPackages.forEach { optOutPackageInfo ->
|
||||
try {
|
||||
backUpApk(0, optOutPackageInfo, NOT_ALLOWED)
|
||||
backUpApk(optOutPackageInfo, NOT_ALLOWED)
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "Error backing up opt-out APK of ${optOutPackageInfo.packageName}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun backUpApk(result: Int, packageInfo: PackageInfo, packageState: PackageState = UNKNOWN_ERROR): Int {
|
||||
private fun backUpApk(packageInfo: PackageInfo, packageState: PackageState = UNKNOWN_ERROR) {
|
||||
val packageName = packageInfo.packageName
|
||||
if (packageName == MAGIC_PACKAGE_MANAGER) return result
|
||||
return try {
|
||||
try {
|
||||
apkBackup.backupApkIfNecessary(packageInfo, packageState) {
|
||||
plugin.getApkOutputStream(packageInfo)
|
||||
}?.let { packageMetadata ->
|
||||
val outputStream = plugin.getMetadataOutputStream()
|
||||
metadataManager.onApkBackedUp(packageInfo, packageMetadata, outputStream)
|
||||
}
|
||||
result
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "Error while writing APK or metadata for $packageName", e)
|
||||
TRANSPORT_PACKAGE_REJECTED
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
package com.stevesoltys.seedvault.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.view.View
|
||||
import android.view.View.GONE
|
||||
import android.view.View.INVISIBLE
|
||||
import android.view.View.VISIBLE
|
||||
import android.widget.ImageView
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.stevesoltys.seedvault.R
|
||||
import com.stevesoltys.seedvault.restore.AppRestoreStatus
|
||||
import com.stevesoltys.seedvault.restore.AppRestoreStatus.FAILED
|
||||
import com.stevesoltys.seedvault.restore.AppRestoreStatus.FAILED_NOT_ALLOWED
|
||||
import com.stevesoltys.seedvault.restore.AppRestoreStatus.FAILED_NOT_INSTALLED
|
||||
import com.stevesoltys.seedvault.restore.AppRestoreStatus.FAILED_NO_DATA
|
||||
import com.stevesoltys.seedvault.restore.AppRestoreStatus.FAILED_QUOTA_EXCEEDED
|
||||
import com.stevesoltys.seedvault.restore.AppRestoreStatus.IN_PROGRESS
|
||||
import com.stevesoltys.seedvault.restore.AppRestoreStatus.SUCCEEDED
|
||||
|
||||
|
||||
internal open class AppViewHolder(v: View) : RecyclerView.ViewHolder(v) {
|
||||
|
||||
protected val context: Context = v.context
|
||||
protected val pm: PackageManager = context.packageManager
|
||||
|
||||
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 fun setStatus(status: AppRestoreStatus) {
|
||||
if (status == IN_PROGRESS) {
|
||||
appInfo.visibility = GONE
|
||||
appStatus.visibility = INVISIBLE
|
||||
progressBar.visibility = VISIBLE
|
||||
} else {
|
||||
appStatus.visibility = VISIBLE
|
||||
progressBar.visibility = INVISIBLE
|
||||
appInfo.visibility = GONE
|
||||
when (status) {
|
||||
SUCCEEDED -> appStatus.setImageResource(R.drawable.ic_check_green)
|
||||
FAILED -> appStatus.setImageResource(R.drawable.ic_cancel_red)
|
||||
else -> {
|
||||
when (status) {
|
||||
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.visibility = VISIBLE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun AppRestoreStatus.getInfo(): String = when (this) {
|
||||
FAILED_NO_DATA -> context.getString(R.string.restore_app_no_data)
|
||||
FAILED_NOT_ALLOWED -> context.getString(R.string.restore_app_not_allowed)
|
||||
FAILED_NOT_INSTALLED -> context.getString(R.string.restore_app_not_installed)
|
||||
FAILED_QUOTA_EXCEEDED -> context.getString(R.string.restore_app_quota_exceeded)
|
||||
else -> "Please report a bug after you read this."
|
||||
}
|
||||
|
||||
}
|
15
app/src/main/java/com/stevesoltys/seedvault/ui/UiUtils.kt
Normal file
15
app/src/main/java/com/stevesoltys/seedvault/ui/UiUtils.kt
Normal file
|
@ -0,0 +1,15 @@
|
|||
package com.stevesoltys.seedvault.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.text.format.DateUtils.MINUTE_IN_MILLIS
|
||||
import android.text.format.DateUtils.getRelativeTimeSpanString
|
||||
import com.stevesoltys.seedvault.R
|
||||
|
||||
fun Long.toRelativeTime(context: Context): CharSequence {
|
||||
return if (this == 0L) {
|
||||
context.getString(R.string.settings_backup_last_backup_never)
|
||||
} else {
|
||||
val now = System.currentTimeMillis()
|
||||
getRelativeTimeSpanString(this, now, MINUTE_IN_MILLIS, 0)
|
||||
}
|
||||
}
|
10
app/src/main/res/drawable/ic_apps.xml
Normal file
10
app/src/main/res/drawable/ic_apps.xml
Normal file
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?android:attr/textColorSecondary"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M4,8h4L8,4L4,4v4zM10,20h4v-4h-4v4zM4,20h4v-4L4,16v4zM4,14h4v-4L4,10v4zM10,14h4v-4h-4v4zM16,4v4h4L20,4h-4zM10,8h4L14,4h-4v4zM16,14h4v-4h-4v4zM16,20h4v-4h-4v4z" />
|
||||
</vector>
|
9
app/src/main/res/drawable/ic_block_yellow.xml
Normal file
9
app/src/main/res/drawable/ic_block_yellow.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<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>
|
10
app/src/main/res/drawable/ic_cloud_done.xml
Normal file
10
app/src/main/res/drawable/ic_cloud_done.xml
Normal file
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?android:attr/textColorSecondary"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M19.35,10.04C18.67,6.59 15.64,4 12,4 9.11,4 6.6,5.64 5.35,8.04 2.34,8.36 0,10.91 0,14c0,3.31 2.69,6 6,6h13c2.76,0 5,-2.24 5,-5 0,-2.64 -2.05,-4.78 -4.65,-4.96zM10,17l-3.5,-3.5 1.41,-1.41L10,14.17 15.18,9l1.41,1.41L10,17z" />
|
||||
</vector>
|
|
@ -0,0 +1,9 @@
|
|||
<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,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8z" />
|
||||
</vector>
|
30
app/src/main/res/layout/fragment_app_status.xml
Normal file
30
app/src/main/res/layout/fragment_app_status.xml
Normal file
|
@ -0,0 +1,30 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
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"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
style="?android:attr/progressBarStyleLarge"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="visible"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -14,17 +14,19 @@
|
|||
<string name="settings_backup_location_none">None</string>
|
||||
<string name="settings_backup_location_internal">Internal Storage</string>
|
||||
<string name="settings_backup_last_backup_never">Never</string>
|
||||
<string name="settings_backup_location_summary">%1$s · Last Backup %2$s</string>
|
||||
<string name="settings_info">All backups are encrypted on your phone. To restore from backup you will need your 12-word recovery code.</string>
|
||||
<string name="settings_auto_restore_title">Automatic restore</string>
|
||||
<string name="settings_auto_restore_summary">When reinstalling an app, restore backed up settings and data.</string>
|
||||
<string name="settings_auto_restore_summary_usb">Note: Your %1$s needs to be plugged in for this to work.</string>
|
||||
<string name="settings_category_app_data_backup">App data backup</string>
|
||||
<string name="settings_backup_apk_title">App backup</string>
|
||||
<string name="settings_backup_apk_summary">Back up the apps themselves. Otherwise, only app data would get backed up.</string>
|
||||
<string name="settings_backup_apk_dialog_title">Really disable app backup?</string>
|
||||
<string name="settings_backup_apk_dialog_message">Disabled app backup will still back up app data. However, it will not get restored automatically.\n\nYou will need to install all your apps manually while having \"Automatic Restore\" switched on.</string>
|
||||
<string name="settings_backup_apk_dialog_cancel">Cancel</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_summary">Last Backup: %1$s</string>
|
||||
<string name="settings_backup_now">Backup now</string>
|
||||
|
||||
<!-- Storage -->
|
||||
|
@ -102,7 +104,7 @@
|
|||
<string name="restore_restoring">Restoring Backup</string>
|
||||
<string name="restore_magic_package">System Package Manager</string>
|
||||
<string name="restore_app_no_data">App reported no data for backup</string>
|
||||
<string name="restore_app_not_allowed">Data backup was not allowed</string>
|
||||
<string name="restore_app_not_allowed">App doesn\'t allow backup</string>
|
||||
<string name="restore_app_not_installed">App not installed</string>
|
||||
<string name="restore_app_quota_exceeded">Backup quota exceeded</string>
|
||||
<string name="restore_finished_success">Restore complete</string>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
<androidx.preference.PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<androidx.preference.PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<androidx.preference.SwitchPreferenceCompat
|
||||
app:icon="@drawable/ic_cloud_upload"
|
||||
|
@ -20,12 +21,25 @@
|
|||
app:summary="@string/settings_auto_restore_summary"
|
||||
app:title="@string/settings_auto_restore_title" />
|
||||
|
||||
<androidx.preference.SwitchPreferenceCompat
|
||||
app:defaultValue="true"
|
||||
app:dependency="backup"
|
||||
app:key="backup_apk"
|
||||
app:summary="@string/settings_backup_apk_summary"
|
||||
app:title="@string/settings_backup_apk_title" />
|
||||
<androidx.preference.PreferenceCategory
|
||||
app:key="category_app_data_backup"
|
||||
app:title="@string/settings_category_app_data_backup">
|
||||
|
||||
<androidx.preference.Preference
|
||||
app:fragment="com.stevesoltys.seedvault.settings.AppStatusFragment"
|
||||
app:icon="@drawable/ic_apps"
|
||||
app:key="backup_status"
|
||||
app:title="@string/settings_backup_status_title"
|
||||
tools:summary="Last backup: Never" />
|
||||
|
||||
<androidx.preference.SwitchPreferenceCompat
|
||||
app:defaultValue="true"
|
||||
app:dependency="backup"
|
||||
app:key="backup_apk"
|
||||
app:summary="@string/settings_backup_apk_summary"
|
||||
app:title="@string/settings_backup_apk_title" />
|
||||
|
||||
</androidx.preference.PreferenceCategory>
|
||||
|
||||
<androidx.preference.Preference
|
||||
app:allowDividerAbove="true"
|
||||
|
|
|
@ -113,6 +113,7 @@ internal class BackupCoordinatorTest : BackupTest() {
|
|||
val isFullBackup = Random.nextBoolean()
|
||||
val quota = Random.nextLong()
|
||||
|
||||
expectApkBackupAndMetadataWrite()
|
||||
if (isFullBackup) {
|
||||
every { full.getQuota() } returns quota
|
||||
} else {
|
||||
|
@ -264,9 +265,9 @@ internal class BackupCoordinatorTest : BackupTest() {
|
|||
}
|
||||
|
||||
private fun expectApkBackupAndMetadataWrite() {
|
||||
every { apkBackup.backupApkIfNecessary(packageInfo, UNKNOWN_ERROR, any()) } returns packageMetadata
|
||||
every { apkBackup.backupApkIfNecessary(any(), UNKNOWN_ERROR, any()) } returns packageMetadata
|
||||
every { plugin.getMetadataOutputStream() } returns metadataOutputStream
|
||||
every { metadataManager.onApkBackedUp(packageInfo, packageMetadata, metadataOutputStream) } just Runs
|
||||
every { metadataManager.onApkBackedUp(any(), packageMetadata, metadataOutputStream) } just Runs
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue