Refactor StorageRoots to be more generic to prepare for non-SAF storage plugins
and to make it easier to add placeholders for storage options
This commit is contained in:
parent
b1ca70193c
commit
4ee7605b50
8 changed files with 233 additions and 156 deletions
|
@ -0,0 +1,123 @@
|
|||
package com.stevesoltys.seedvault.ui.storage
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.Intent.ACTION_VIEW
|
||||
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
import android.net.Uri
|
||||
import com.stevesoltys.seedvault.R
|
||||
import com.stevesoltys.seedvault.ui.storage.StorageOption.SafOption
|
||||
import com.stevesoltys.seedvault.ui.storage.StorageRootResolver.getIcon
|
||||
|
||||
private const val NEXTCLOUD_PACKAGE = "com.nextcloud.client"
|
||||
private const val NEXTCLOUD_ACTIVITY = "com.owncloud.android.authentication.AuthenticatorActivity"
|
||||
|
||||
/**
|
||||
* A class for storage option placeholders that need to be shown under certain circumstances.
|
||||
* E.g. a way to install an app when needed for restore.
|
||||
*/
|
||||
internal class SafStorageOptions(
|
||||
private val context: Context,
|
||||
private val isRestore: Boolean,
|
||||
private val whitelistedAuthorities: Array<String>,
|
||||
) {
|
||||
|
||||
private val packageManager = context.packageManager
|
||||
|
||||
internal fun checkOrAddExtraRoots(roots: ArrayList<SafOption>) {
|
||||
checkOrAddUsbRoot(roots)
|
||||
checkOrAddNextCloudRoot(roots)
|
||||
}
|
||||
|
||||
private fun checkOrAddUsbRoot(roots: ArrayList<SafOption>) {
|
||||
if (doNotInclude(AUTHORITY_STORAGE, roots) { it.isUsb }) return
|
||||
|
||||
val root = SafOption(
|
||||
authority = AUTHORITY_STORAGE,
|
||||
rootId = "usb",
|
||||
documentId = "fake",
|
||||
icon = getIcon(context, AUTHORITY_STORAGE, "usb", 0),
|
||||
title = context.getString(R.string.storage_fake_drive_title),
|
||||
summary = context.getString(R.string.storage_fake_drive_summary),
|
||||
availableBytes = null,
|
||||
isUsb = true,
|
||||
requiresNetwork = false,
|
||||
enabled = false
|
||||
)
|
||||
roots.add(root)
|
||||
}
|
||||
|
||||
/**
|
||||
* This adds a fake Nextcloud entry if no real one was found.
|
||||
*
|
||||
* If Nextcloud is *not* installed,
|
||||
* the user will always have the option to install it by clicking the entry.
|
||||
*
|
||||
* If it *is* installed and this is restore, the user can set up a new account by clicking.
|
||||
* FIXME: If this isn't restore, the entry should be disabled,
|
||||
* because we don't know if there's just no account or an activated passcode
|
||||
* (which hides existing accounts).
|
||||
*/
|
||||
private fun checkOrAddNextCloudRoot(roots: ArrayList<SafOption>) {
|
||||
if (doNotInclude(AUTHORITY_NEXTCLOUD, roots)) return
|
||||
|
||||
val intent = Intent().apply {
|
||||
addFlags(FLAG_ACTIVITY_NEW_TASK)
|
||||
setClassName(NEXTCLOUD_PACKAGE, NEXTCLOUD_ACTIVITY)
|
||||
// setting a nc:// Uri prevents FirstRunActivity to show
|
||||
data = Uri.parse("nc://login/server:")
|
||||
putExtra("onlyAdd", true)
|
||||
}
|
||||
val marketIntent =
|
||||
Intent(ACTION_VIEW, Uri.parse("market://details?id=$NEXTCLOUD_PACKAGE")).apply {
|
||||
addFlags(FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
val isInstalled = packageManager.resolveActivity(intent, 0) != null
|
||||
val canInstall = packageManager.resolveActivity(marketIntent, 0) != null
|
||||
val summaryRes = if (isInstalled) {
|
||||
if (isRestore) R.string.storage_fake_nextcloud_summary_installed
|
||||
else R.string.storage_fake_nextcloud_summary_unavailable
|
||||
} else {
|
||||
if (canInstall) R.string.storage_fake_nextcloud_summary
|
||||
else R.string.storage_fake_nextcloud_summary_unavailable_market
|
||||
}
|
||||
val root = SafOption(
|
||||
authority = AUTHORITY_NEXTCLOUD,
|
||||
rootId = "fake",
|
||||
documentId = "fake",
|
||||
icon = getIcon(context, AUTHORITY_NEXTCLOUD, "fake", 0),
|
||||
title = context.getString(R.string.storage_fake_nextcloud_title),
|
||||
summary = context.getString(summaryRes),
|
||||
availableBytes = null,
|
||||
isUsb = false,
|
||||
requiresNetwork = true,
|
||||
enabled = isInstalled || canInstall,
|
||||
nonDefaultAction = {
|
||||
if (isInstalled) context.startActivity(intent)
|
||||
else if (canInstall) context.startActivity(marketIntent)
|
||||
}
|
||||
)
|
||||
roots.add(root)
|
||||
}
|
||||
|
||||
private fun doNotInclude(
|
||||
authority: String,
|
||||
roots: ArrayList<SafOption>,
|
||||
doNotIncludeIfTrue: ((SafOption) -> Boolean)? = null,
|
||||
): Boolean {
|
||||
if (!isAuthoritySupported(authority)) return true
|
||||
for (root in roots) {
|
||||
if (root.authority == authority && doNotIncludeIfTrue?.invoke(root) != false) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun isAuthoritySupported(authority: String): Boolean {
|
||||
// just restrict where to store backups,
|
||||
// restoring can be more free for forward compatibility
|
||||
return isRestore || whitelistedAuthorities.contains(authority)
|
||||
}
|
||||
|
||||
}
|
|
@ -37,7 +37,7 @@ class StorageActivity : BackupActivity() {
|
|||
if (storageRoot == null) {
|
||||
viewModel.onUriPermissionResultReceived(null)
|
||||
} else {
|
||||
viewModel.onStorageRootChosen(storageRoot)
|
||||
viewModel.onSafOptionChosen(storageRoot)
|
||||
viewModel.onUriPermissionResultReceived(uri)
|
||||
}
|
||||
}
|
||||
|
@ -72,7 +72,7 @@ class StorageActivity : BackupActivity() {
|
|||
|
||||
if (savedInstanceState == null) {
|
||||
if (canUseStorageRootsFragment()) {
|
||||
showFragment(StorageRootsFragment.newInstance(isRestore()))
|
||||
showFragment(StorageOptionsFragment.newInstance(isRestore()))
|
||||
} else {
|
||||
openDocumentTree.launch(null)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
package com.stevesoltys.seedvault.ui.storage
|
||||
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.net.Uri
|
||||
import android.provider.DocumentsContract.buildTreeDocumentUri
|
||||
|
||||
internal sealed class StorageOption {
|
||||
abstract val id: String
|
||||
abstract val icon: Drawable?
|
||||
abstract val title: String
|
||||
abstract val summary: String?
|
||||
abstract val availableBytes: Long?
|
||||
abstract val requiresNetwork: Boolean
|
||||
abstract val enabled: Boolean
|
||||
abstract val nonDefaultAction: (() -> Unit)?
|
||||
|
||||
data class SafOption(
|
||||
override val icon: Drawable?,
|
||||
override val title: String,
|
||||
override val summary: String?,
|
||||
override val availableBytes: Long?,
|
||||
val authority: String,
|
||||
val rootId: String,
|
||||
val documentId: String,
|
||||
val isUsb: Boolean,
|
||||
override val requiresNetwork: Boolean,
|
||||
override val enabled: Boolean = true,
|
||||
override val nonDefaultAction: (() -> Unit)? = null,
|
||||
) : StorageOption() {
|
||||
override val id: String = "saf-$authority"
|
||||
|
||||
val uri: Uri by lazy {
|
||||
buildTreeDocumentUri(authority, documentId)
|
||||
}
|
||||
|
||||
fun isInternal(): Boolean {
|
||||
return authority == AUTHORITY_STORAGE && !isUsb
|
||||
}
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
return other is StorageOption && other.id == id
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return id.hashCode()
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
package com.stevesoltys.seedvault.ui.storage
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.text.format.Formatter
|
||||
import android.view.LayoutInflater
|
||||
|
@ -13,40 +14,42 @@ import androidx.appcompat.app.AlertDialog
|
|||
import androidx.recyclerview.widget.RecyclerView.Adapter
|
||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||
import com.stevesoltys.seedvault.R
|
||||
import com.stevesoltys.seedvault.ui.storage.StorageRootAdapter.StorageRootViewHolder
|
||||
import com.stevesoltys.seedvault.ui.storage.StorageOption.SafOption
|
||||
import com.stevesoltys.seedvault.ui.storage.StorageOptionAdapter.StorageOptionViewHolder
|
||||
|
||||
internal class StorageRootAdapter(
|
||||
internal class StorageOptionAdapter(
|
||||
private val isRestore: Boolean,
|
||||
private val listener: StorageRootClickedListener,
|
||||
) : Adapter<StorageRootViewHolder>() {
|
||||
private val listener: StorageOptionClickedListener,
|
||||
) : Adapter<StorageOptionViewHolder>() {
|
||||
|
||||
private val items = ArrayList<StorageRoot>()
|
||||
private val items = ArrayList<StorageOption>()
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StorageRootViewHolder {
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StorageOptionViewHolder {
|
||||
val v = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.list_item_storage_root, parent, false) as View
|
||||
return StorageRootViewHolder(v)
|
||||
return StorageOptionViewHolder(v)
|
||||
}
|
||||
|
||||
override fun getItemCount() = items.size
|
||||
|
||||
override fun onBindViewHolder(holder: StorageRootViewHolder, position: Int) {
|
||||
override fun onBindViewHolder(holder: StorageOptionViewHolder, position: Int) {
|
||||
holder.bind(items[position])
|
||||
}
|
||||
|
||||
internal fun setItems(items: List<StorageRoot>) {
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
internal fun setItems(items: List<StorageOption>) {
|
||||
this.items.clear()
|
||||
this.items.addAll(items)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
internal inner class StorageRootViewHolder(private val v: View) : ViewHolder(v) {
|
||||
internal inner class StorageOptionViewHolder(private val v: View) : ViewHolder(v) {
|
||||
|
||||
private val iconView = v.findViewById<ImageView>(R.id.iconView)
|
||||
private val titleView = v.findViewById<TextView>(R.id.titleView)
|
||||
private val summaryView = v.findViewById<TextView>(R.id.summaryView)
|
||||
|
||||
internal fun bind(item: StorageRoot) {
|
||||
internal fun bind(item: StorageOption) {
|
||||
if (item.enabled) {
|
||||
v.isEnabled = true
|
||||
v.alpha = 1f
|
||||
|
@ -63,16 +66,16 @@ internal class StorageRootAdapter(
|
|||
summaryView.visibility = VISIBLE
|
||||
}
|
||||
item.availableBytes != null -> {
|
||||
val str = Formatter.formatFileSize(v.context, item.availableBytes)
|
||||
val str = Formatter.formatFileSize(v.context, item.availableBytes!!)
|
||||
summaryView.text = v.context.getString(R.string.storage_available_bytes, str)
|
||||
summaryView.visibility = VISIBLE
|
||||
}
|
||||
else -> summaryView.visibility = GONE
|
||||
}
|
||||
v.setOnClickListener {
|
||||
if (item.overrideClickListener != null) {
|
||||
item.overrideClickListener.invoke()
|
||||
} else if (!isRestore && item.isInternal()) {
|
||||
if (item.nonDefaultAction != null) {
|
||||
item.nonDefaultAction?.invoke()
|
||||
} else if (!isRestore && item is SafOption && item.isInternal()) {
|
||||
showWarningDialog(v.context, item)
|
||||
} else {
|
||||
listener.onClick(item)
|
||||
|
@ -82,7 +85,7 @@ internal class StorageRootAdapter(
|
|||
|
||||
}
|
||||
|
||||
private fun showWarningDialog(context: Context, item: StorageRoot) {
|
||||
private fun showWarningDialog(context: Context, item: StorageOption) {
|
||||
AlertDialog.Builder(context)
|
||||
.setTitle(R.string.storage_internal_warning_title)
|
||||
.setMessage(R.string.storage_internal_warning_message)
|
|
@ -1,8 +1,8 @@
|
|||
package com.stevesoltys.seedvault.ui.storage
|
||||
|
||||
import android.Manifest.permission.MANAGE_DOCUMENTS
|
||||
import android.app.Activity.RESULT_FIRST_USER
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity.RESULT_FIRST_USER
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
|
||||
|
@ -24,14 +24,15 @@ import androidx.fragment.app.Fragment
|
|||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.stevesoltys.seedvault.R
|
||||
import com.stevesoltys.seedvault.ui.INTENT_EXTRA_IS_RESTORE
|
||||
import com.stevesoltys.seedvault.ui.storage.StorageOption.SafOption
|
||||
import org.koin.androidx.viewmodel.ext.android.getSharedViewModel
|
||||
|
||||
internal class StorageRootsFragment : Fragment(), StorageRootClickedListener {
|
||||
internal class StorageOptionsFragment : Fragment(), StorageOptionClickedListener {
|
||||
|
||||
companion object {
|
||||
@RequiresPermission(MANAGE_DOCUMENTS)
|
||||
fun newInstance(isRestore: Boolean): StorageRootsFragment {
|
||||
val f = StorageRootsFragment()
|
||||
fun newInstance(isRestore: Boolean): StorageOptionsFragment {
|
||||
val f = StorageOptionsFragment()
|
||||
f.arguments = Bundle().apply {
|
||||
putBoolean(INTENT_EXTRA_IS_RESTORE, isRestore)
|
||||
}
|
||||
|
@ -48,7 +49,7 @@ internal class StorageRootsFragment : Fragment(), StorageRootClickedListener {
|
|||
private lateinit var progressBar: ProgressBar
|
||||
private lateinit var skipView: TextView
|
||||
|
||||
private val adapter by lazy { StorageRootAdapter(viewModel.isRestoreOperation, this) }
|
||||
private val adapter by lazy { StorageOptionAdapter(viewModel.isRestoreOperation, this) }
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
|
@ -97,7 +98,7 @@ internal class StorageRootsFragment : Fragment(), StorageRootClickedListener {
|
|||
|
||||
listView.adapter = adapter
|
||||
|
||||
viewModel.storageRoots.observe(viewLifecycleOwner, { roots ->
|
||||
viewModel.storageOptions.observe(viewLifecycleOwner, { roots ->
|
||||
onRootsLoaded(roots)
|
||||
})
|
||||
}
|
||||
|
@ -107,7 +108,7 @@ internal class StorageRootsFragment : Fragment(), StorageRootClickedListener {
|
|||
viewModel.loadStorageRoots()
|
||||
}
|
||||
|
||||
private fun onRootsLoaded(roots: List<StorageRoot>) {
|
||||
private fun onRootsLoaded(roots: List<StorageOption>) {
|
||||
progressBar.visibility = INVISIBLE
|
||||
adapter.setItems(roots)
|
||||
}
|
||||
|
@ -116,15 +117,19 @@ internal class StorageRootsFragment : Fragment(), StorageRootClickedListener {
|
|||
viewModel.onUriPermissionResultReceived(uri)
|
||||
}
|
||||
|
||||
override fun onClick(root: StorageRoot) {
|
||||
viewModel.onStorageRootChosen(root)
|
||||
openDocumentTree.launch(root.uri)
|
||||
override fun onClick(storageOption: StorageOption) {
|
||||
if (storageOption is SafOption) {
|
||||
viewModel.onSafOptionChosen(storageOption)
|
||||
openDocumentTree.launch(storageOption.uri)
|
||||
} else {
|
||||
throw IllegalArgumentException("Non-SAF storage not yet supported")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
internal interface StorageRootClickedListener {
|
||||
fun onClick(root: StorageRoot)
|
||||
internal interface StorageOptionClickedListener {
|
||||
fun onClick(storageOption: StorageOption)
|
||||
}
|
||||
|
||||
private class OpenSeedvaultTree : OpenDocumentTree() {
|
|
@ -3,20 +3,17 @@ package com.stevesoltys.seedvault.ui.storage
|
|||
import android.Manifest.permission.MANAGE_DOCUMENTS
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.Intent.ACTION_VIEW
|
||||
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
import android.content.pm.PackageManager.GET_META_DATA
|
||||
import android.content.pm.ProviderInfo
|
||||
import android.database.ContentObserver
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.net.Uri
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.provider.DocumentsContract
|
||||
import android.provider.DocumentsContract.PROVIDER_INTERFACE
|
||||
import android.provider.DocumentsContract.buildRootsUri
|
||||
import android.util.Log
|
||||
import com.stevesoltys.seedvault.R
|
||||
import com.stevesoltys.seedvault.ui.storage.StorageRootResolver.getIcon
|
||||
import com.stevesoltys.seedvault.ui.storage.StorageOption.SafOption
|
||||
|
||||
private val TAG = StorageRootFetcher::class.java.simpleName
|
||||
|
||||
|
@ -27,32 +24,6 @@ const val ROOT_ID_HOME = "home"
|
|||
const val AUTHORITY_DOWNLOADS = "com.android.providers.downloads.documents"
|
||||
const val AUTHORITY_NEXTCLOUD = "org.nextcloud.documents"
|
||||
|
||||
private const val NEXTCLOUD_PACKAGE = "com.nextcloud.client"
|
||||
private const val NEXTCLOUD_ACTIVITY = "com.owncloud.android.authentication.AuthenticatorActivity"
|
||||
|
||||
data class StorageRoot(
|
||||
internal val authority: String,
|
||||
internal val rootId: String,
|
||||
internal val documentId: String,
|
||||
internal val icon: Drawable?,
|
||||
internal val title: String,
|
||||
internal val summary: String?,
|
||||
internal val availableBytes: Long?,
|
||||
internal val isUsb: Boolean,
|
||||
internal val requiresNetwork: Boolean,
|
||||
internal val enabled: Boolean = true,
|
||||
internal val overrideClickListener: (() -> Unit)? = null,
|
||||
) {
|
||||
|
||||
internal val uri: Uri by lazy {
|
||||
DocumentsContract.buildTreeDocumentUri(authority, documentId)
|
||||
}
|
||||
|
||||
fun isInternal(): Boolean {
|
||||
return authority == AUTHORITY_STORAGE && !isUsb
|
||||
}
|
||||
}
|
||||
|
||||
internal interface RemovableStorageListener {
|
||||
fun onStorageChanged()
|
||||
}
|
||||
|
@ -63,6 +34,7 @@ internal class StorageRootFetcher(private val context: Context, private val isRe
|
|||
private val contentResolver = context.contentResolver
|
||||
private val whitelistedAuthorities =
|
||||
context.resources.getStringArray(R.array.storage_authority_whitelist)
|
||||
private val safStorageOptions = SafStorageOptions(context, isRestore, whitelistedAuthorities)
|
||||
|
||||
private var listener: RemovableStorageListener? = null
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
|
@ -76,7 +48,7 @@ internal class StorageRootFetcher(private val context: Context, private val isRe
|
|||
internal fun setRemovableStorageListener(listener: RemovableStorageListener?) {
|
||||
this.listener = listener
|
||||
if (listener != null) {
|
||||
val rootsUri = DocumentsContract.buildRootsUri(AUTHORITY_STORAGE)
|
||||
val rootsUri = buildRootsUri(AUTHORITY_STORAGE)
|
||||
contentResolver.registerContentObserver(rootsUri, true, observer)
|
||||
} else {
|
||||
contentResolver.unregisterContentObserver(observer)
|
||||
|
@ -85,8 +57,8 @@ internal class StorageRootFetcher(private val context: Context, private val isRe
|
|||
|
||||
internal fun getRemovableStorageListener() = listener
|
||||
|
||||
internal fun getStorageRoots(): List<StorageRoot> {
|
||||
val roots = ArrayList<StorageRoot>()
|
||||
internal fun getStorageOptions(): List<StorageOption> {
|
||||
val roots = ArrayList<SafOption>()
|
||||
val intent = Intent(PROVIDER_INTERFACE)
|
||||
val providers = packageManager.queryIntentContentProviders(intent, 0)
|
||||
for (info in providers) {
|
||||
|
@ -96,12 +68,12 @@ internal class StorageRootFetcher(private val context: Context, private val isRe
|
|||
roots.addAll(getRoots(providerInfo))
|
||||
}
|
||||
}
|
||||
checkOrAddUsbRoot(roots)
|
||||
checkOrAddNextCloudRoot(roots)
|
||||
// there's a couple of options, we still want to show, even if no roots are found for them
|
||||
safStorageOptions.checkOrAddExtraRoots(roots)
|
||||
return roots
|
||||
}
|
||||
|
||||
private fun getRoots(providerInfo: ProviderInfo): List<StorageRoot> {
|
||||
private fun getRoots(providerInfo: ProviderInfo): List<SafOption> {
|
||||
val authority = providerInfo.authority
|
||||
val provider = packageManager.resolveContentProvider(authority, GET_META_DATA)
|
||||
return if (provider == null || !provider.isSupported()) {
|
||||
|
@ -112,82 +84,6 @@ internal class StorageRootFetcher(private val context: Context, private val isRe
|
|||
}
|
||||
}
|
||||
|
||||
private fun checkOrAddUsbRoot(roots: ArrayList<StorageRoot>) {
|
||||
if (!isAuthoritySupported(AUTHORITY_STORAGE)) return
|
||||
|
||||
for (root in roots) {
|
||||
// return if we already have a USB storage root
|
||||
if (root.authority == AUTHORITY_STORAGE && root.isUsb) return
|
||||
}
|
||||
val root = StorageRoot(
|
||||
authority = AUTHORITY_STORAGE,
|
||||
rootId = "usb",
|
||||
documentId = "fake",
|
||||
icon = getIcon(context, AUTHORITY_STORAGE, "usb", 0),
|
||||
title = context.getString(R.string.storage_fake_drive_title),
|
||||
summary = context.getString(R.string.storage_fake_drive_summary),
|
||||
availableBytes = null,
|
||||
isUsb = true,
|
||||
requiresNetwork = false,
|
||||
enabled = false
|
||||
)
|
||||
roots.add(root)
|
||||
}
|
||||
|
||||
/**
|
||||
* This adds a fake Nextcloud entry if no real one was found.
|
||||
*
|
||||
* If Nextcloud is *not* installed,
|
||||
* the user will always have the option to install it by clicking the entry.
|
||||
*
|
||||
* If it *is* installed and this is restore, the user can set up a new account by clicking.
|
||||
* If this isn't restore, the entry will be disabled,
|
||||
* because we don't know if there's no account or an activated passcode.
|
||||
*/
|
||||
private fun checkOrAddNextCloudRoot(roots: ArrayList<StorageRoot>) {
|
||||
for (root in roots) {
|
||||
// return if we already have a NextCloud storage root
|
||||
if (root.authority == AUTHORITY_NEXTCLOUD) return
|
||||
}
|
||||
val intent = Intent().apply {
|
||||
addFlags(FLAG_ACTIVITY_NEW_TASK)
|
||||
setClassName(NEXTCLOUD_PACKAGE, NEXTCLOUD_ACTIVITY)
|
||||
// setting a nc:// Uri prevents FirstRunActivity to show
|
||||
data = Uri.parse("nc://login/server:")
|
||||
putExtra("onlyAdd", true)
|
||||
}
|
||||
val marketIntent =
|
||||
Intent(ACTION_VIEW, Uri.parse("market://details?id=$NEXTCLOUD_PACKAGE")).apply {
|
||||
addFlags(FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
val isInstalled = packageManager.resolveActivity(intent, 0) != null
|
||||
val canInstall = packageManager.resolveActivity(marketIntent, 0) != null
|
||||
val summaryRes = if (isInstalled) {
|
||||
if (isRestore) R.string.storage_fake_nextcloud_summary_installed
|
||||
else R.string.storage_fake_nextcloud_summary_unavailable
|
||||
} else {
|
||||
if (canInstall) R.string.storage_fake_nextcloud_summary
|
||||
else R.string.storage_fake_nextcloud_summary_unavailable_market
|
||||
}
|
||||
val root = StorageRoot(
|
||||
authority = AUTHORITY_NEXTCLOUD,
|
||||
rootId = "fake",
|
||||
documentId = "fake",
|
||||
icon = getIcon(context, AUTHORITY_NEXTCLOUD, "fake", 0),
|
||||
title = context.getString(R.string.storage_fake_nextcloud_title),
|
||||
summary = context.getString(summaryRes),
|
||||
availableBytes = null,
|
||||
isUsb = false,
|
||||
requiresNetwork = true,
|
||||
enabled = isInstalled || canInstall,
|
||||
overrideClickListener = {
|
||||
if (isInstalled) context.startActivity(intent)
|
||||
else if (canInstall) context.startActivity(marketIntent)
|
||||
}
|
||||
)
|
||||
roots.add(root)
|
||||
}
|
||||
|
||||
private fun ProviderInfo.isSupported(): Boolean {
|
||||
return if (!exported) {
|
||||
Log.w(TAG, "Provider is not exported")
|
||||
|
|
|
@ -17,13 +17,14 @@ import android.provider.DocumentsContract.Root.FLAG_SUPPORTS_CREATE
|
|||
import android.provider.DocumentsContract.Root.FLAG_SUPPORTS_IS_CHILD
|
||||
import android.util.Log
|
||||
import com.stevesoltys.seedvault.R
|
||||
import com.stevesoltys.seedvault.ui.storage.StorageOption.SafOption
|
||||
|
||||
object StorageRootResolver {
|
||||
internal object StorageRootResolver {
|
||||
|
||||
private val TAG = StorageRootResolver::class.java.simpleName
|
||||
|
||||
fun getStorageRoots(context: Context, authority: String): List<StorageRoot> {
|
||||
val roots = ArrayList<StorageRoot>()
|
||||
fun getStorageRoots(context: Context, authority: String): List<SafOption> {
|
||||
val roots = ArrayList<SafOption>()
|
||||
val rootsUri = DocumentsContract.buildRootsUri(authority)
|
||||
|
||||
try {
|
||||
|
@ -39,7 +40,7 @@ object StorageRootResolver {
|
|||
return roots
|
||||
}
|
||||
|
||||
private fun getStorageRoot(context: Context, authority: String, cursor: Cursor): StorageRoot? {
|
||||
private fun getStorageRoot(context: Context, authority: String, cursor: Cursor): SafOption? {
|
||||
val flags = cursor.getInt(COLUMN_FLAGS)
|
||||
val supportsCreate = flags and FLAG_SUPPORTS_CREATE != 0
|
||||
val supportsIsChild = flags and FLAG_SUPPORTS_IS_CHILD != 0
|
||||
|
@ -47,7 +48,7 @@ object StorageRootResolver {
|
|||
val rootId = cursor.getString(COLUMN_ROOT_ID)!!
|
||||
if (authority == AUTHORITY_STORAGE && rootId == ROOT_ID_HOME) return null
|
||||
val documentId = cursor.getString(COLUMN_DOCUMENT_ID) ?: return null
|
||||
return StorageRoot(
|
||||
return SafOption(
|
||||
authority = authority,
|
||||
rootId = rootId,
|
||||
documentId = documentId,
|
||||
|
|
|
@ -20,6 +20,7 @@ import com.stevesoltys.seedvault.settings.SettingsManager
|
|||
import com.stevesoltys.seedvault.settings.Storage
|
||||
import com.stevesoltys.seedvault.ui.LiveEvent
|
||||
import com.stevesoltys.seedvault.ui.MutableLiveEvent
|
||||
import com.stevesoltys.seedvault.ui.storage.StorageOption.SafOption
|
||||
|
||||
private val TAG = StorageViewModel::class.java.simpleName
|
||||
|
||||
|
@ -28,8 +29,8 @@ internal abstract class StorageViewModel(
|
|||
protected val settingsManager: SettingsManager,
|
||||
) : AndroidViewModel(app), RemovableStorageListener {
|
||||
|
||||
private val mStorageRoots = MutableLiveData<List<StorageRoot>>()
|
||||
internal val storageRoots: LiveData<List<StorageRoot>> get() = mStorageRoots
|
||||
private val mStorageOptions = MutableLiveData<List<StorageOption>>()
|
||||
internal val storageOptions: LiveData<List<StorageOption>> get() = mStorageOptions
|
||||
|
||||
private val mLocationSet = MutableLiveEvent<Boolean>()
|
||||
internal val locationSet: LiveEvent<Boolean> get() = mLocationSet
|
||||
|
@ -38,7 +39,7 @@ internal abstract class StorageViewModel(
|
|||
internal val locationChecked: LiveEvent<LocationResult> get() = mLocationChecked
|
||||
|
||||
private val storageRootFetcher by lazy { StorageRootFetcher(app, isRestoreOperation) }
|
||||
private var storageRoot: StorageRoot? = null
|
||||
private var safOption: SafOption? = null
|
||||
|
||||
internal var isSetupWizard: Boolean = false
|
||||
internal val hasStorageSet: Boolean
|
||||
|
@ -63,14 +64,14 @@ internal abstract class StorageViewModel(
|
|||
storageRootFetcher.setRemovableStorageListener(this)
|
||||
}
|
||||
Thread {
|
||||
mStorageRoots.postValue(storageRootFetcher.getStorageRoots())
|
||||
mStorageOptions.postValue(storageRootFetcher.getStorageOptions())
|
||||
}.start()
|
||||
}
|
||||
|
||||
override fun onStorageChanged() = loadStorageRoots()
|
||||
|
||||
fun onStorageRootChosen(root: StorageRoot) {
|
||||
storageRoot = root
|
||||
fun onSafOptionChosen(option: SafOption) {
|
||||
safOption = option
|
||||
}
|
||||
|
||||
internal fun onUriPermissionResultReceived(uri: Uri?) {
|
||||
|
@ -91,13 +92,13 @@ internal abstract class StorageViewModel(
|
|||
}
|
||||
|
||||
/**
|
||||
* Saves the storage behind the given [Uri] (and saved [storageRoot]).
|
||||
* Saves the storage behind the given [Uri] (and saved [safOption]).
|
||||
*
|
||||
* @return true if the storage is a USB flash drive, false otherwise.
|
||||
*/
|
||||
protected fun saveStorage(uri: Uri): Boolean {
|
||||
// store backup storage location in settings
|
||||
val root = storageRoot ?: throw IllegalStateException("no storage root")
|
||||
val root = safOption ?: throw IllegalStateException("no storage root")
|
||||
val name = if (root.isInternal()) {
|
||||
"${root.title} (${app.getString(R.string.settings_backup_location_internal)})"
|
||||
} else {
|
||||
|
|
Loading…
Reference in a new issue