Make MANAGE_DOCUMENTS permission optional
for those who can not or do not want to use it Fixes #58
This commit is contained in:
parent
a69794a6d0
commit
e9f3c08220
6 changed files with 190 additions and 110 deletions
|
@ -26,11 +26,11 @@ It uses the same internal APIs as `adb backup` which is deprecated and thus need
|
||||||
## Permissions
|
## Permissions
|
||||||
* `android.permission.BACKUP` to back up application data.
|
* `android.permission.BACKUP` to back up application data.
|
||||||
* `android.permission.ACCESS_NETWORK_STATE` to check if there is internet access when network storage is used.
|
* `android.permission.ACCESS_NETWORK_STATE` to check if there is internet access when network storage is used.
|
||||||
* `android.permission.MANAGE_DOCUMENTS` to retrieve the available storage roots.
|
|
||||||
* `android.permission.MANAGE_USB` to access the serial number of USB mass storage devices.
|
* `android.permission.MANAGE_USB` to access the serial number of USB mass storage devices.
|
||||||
* `android.permission.WRITE_SECURE_SETTINGS` to change system backup settings and enable call log backup.
|
* `android.permission.WRITE_SECURE_SETTINGS` to change system backup settings and enable call log backup.
|
||||||
* `android.permission.QUERY_ALL_PACKAGES` to get information about all installed apps for backup.
|
* `android.permission.QUERY_ALL_PACKAGES` to get information about all installed apps for backup.
|
||||||
* `android.permission.INSTALL_PACKAGES` to re-install apps when restoring from backup.
|
* `android.permission.INSTALL_PACKAGES` to re-install apps when restoring from backup.
|
||||||
|
* `android.permission.MANAGE_DOCUMENTS` to retrieve the available storage roots (optional) for better UX.
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
Bug reports and pull requests are welcome on GitHub at https://github.com/stevesoltys/seedvault.
|
Bug reports and pull requests are welcome on GitHub at https://github.com/stevesoltys/seedvault.
|
||||||
|
|
|
@ -1,7 +1,16 @@
|
||||||
package com.stevesoltys.seedvault.ui.storage
|
package com.stevesoltys.seedvault.ui.storage
|
||||||
|
|
||||||
|
import android.Manifest.permission.MANAGE_DOCUMENTS
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
|
||||||
|
import android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
|
import android.content.Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||||
|
import android.content.pm.PackageManager.PERMISSION_GRANTED
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts.OpenDocumentTree
|
||||||
import androidx.annotation.CallSuper
|
import androidx.annotation.CallSuper
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import com.stevesoltys.seedvault.R
|
import com.stevesoltys.seedvault.R
|
||||||
|
@ -17,6 +26,24 @@ class StorageActivity : BackupActivity() {
|
||||||
|
|
||||||
private lateinit var viewModel: StorageViewModel
|
private lateinit var viewModel: StorageViewModel
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The official way to get a SAF [Uri] which we only use if we don't have the
|
||||||
|
* [MANAGE_DOCUMENTS] permission (via [canUseStorageRootsFragment]).
|
||||||
|
*/
|
||||||
|
private val openDocumentTree = registerForActivityResult(OpenPersistableDocumentTree()) { uri ->
|
||||||
|
if (uri != null) {
|
||||||
|
Log.e(TAG, "OpenDocumentTree: $uri")
|
||||||
|
val authority = uri.authority ?: throw AssertionError("No authority in $uri")
|
||||||
|
val storageRoot = StorageRootResolver.getStorageRoots(this, authority).getOrNull(0)
|
||||||
|
if (storageRoot == null) {
|
||||||
|
viewModel.onUriPermissionResultReceived(null)
|
||||||
|
} else {
|
||||||
|
viewModel.onStorageRootChosen(storageRoot)
|
||||||
|
viewModel.onUriPermissionResultReceived(uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@CallSuper
|
@CallSuper
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
@ -47,7 +74,11 @@ class StorageActivity : BackupActivity() {
|
||||||
})
|
})
|
||||||
|
|
||||||
if (savedInstanceState == null) {
|
if (savedInstanceState == null) {
|
||||||
|
if (canUseStorageRootsFragment()) {
|
||||||
showFragment(StorageRootsFragment.newInstance(isRestore()))
|
showFragment(StorageRootsFragment.newInstance(isRestore()))
|
||||||
|
} else {
|
||||||
|
openDocumentTree.launch(null)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,13 +92,28 @@ class StorageActivity : BackupActivity() {
|
||||||
|
|
||||||
private fun onInvalidLocation(errorMsg: String) {
|
private fun onInvalidLocation(errorMsg: String) {
|
||||||
if (viewModel.isRestoreOperation) {
|
if (viewModel.isRestoreOperation) {
|
||||||
supportFragmentManager.popBackStack()
|
val dialog = AlertDialog.Builder(this)
|
||||||
AlertDialog.Builder(this)
|
|
||||||
.setTitle(getString(R.string.restore_invalid_location_title))
|
.setTitle(getString(R.string.restore_invalid_location_title))
|
||||||
.setMessage(errorMsg)
|
.setMessage(errorMsg)
|
||||||
.setPositiveButton(android.R.string.ok) { dialog, _ -> dialog.dismiss() }
|
.setPositiveButton(android.R.string.ok) { dialog, _ -> dialog.dismiss() }
|
||||||
.show()
|
if (canUseStorageRootsFragment()) {
|
||||||
|
// We have permission to use StorageRootsFragment,
|
||||||
|
// so pop the back stack to show it again
|
||||||
|
supportFragmentManager.popBackStack()
|
||||||
|
dialog.setPositiveButton(android.R.string.ok) { d, _ -> d.dismiss() }
|
||||||
} else {
|
} else {
|
||||||
|
// We don't have permission to use StorageRootsFragment,
|
||||||
|
// so give option to choose again or cancel.
|
||||||
|
dialog.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
|
openDocumentTree.launch(null)
|
||||||
|
}
|
||||||
|
dialog.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
|
finishAfterTransition()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dialog.show()
|
||||||
|
} else {
|
||||||
|
// just show error message, if this isn't restore
|
||||||
showFragment(StorageCheckFragment.newInstance(getCheckFragmentTitle(), errorMsg))
|
showFragment(StorageCheckFragment.newInstance(getCheckFragmentTitle(), errorMsg))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -80,6 +126,10 @@ class StorageActivity : BackupActivity() {
|
||||||
return intent?.getBooleanExtra(INTENT_EXTRA_IS_SETUP_WIZARD, false) ?: false
|
return intent?.getBooleanExtra(INTENT_EXTRA_IS_SETUP_WIZARD, false) ?: false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun canUseStorageRootsFragment(): Boolean {
|
||||||
|
return checkSelfPermission(MANAGE_DOCUMENTS) == PERMISSION_GRANTED
|
||||||
|
}
|
||||||
|
|
||||||
private fun getCheckFragmentTitle() = if (viewModel.isRestoreOperation) {
|
private fun getCheckFragmentTitle() = if (viewModel.isRestoreOperation) {
|
||||||
getString(R.string.storage_check_fragment_restore_title)
|
getString(R.string.storage_check_fragment_restore_title)
|
||||||
} else {
|
} else {
|
||||||
|
@ -87,3 +137,13 @@ class StorageActivity : BackupActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class OpenPersistableDocumentTree : OpenDocumentTree() {
|
||||||
|
override fun createIntent(context: Context, input: Uri?): Intent {
|
||||||
|
return super.createIntent(context, input).apply {
|
||||||
|
val flags = FLAG_GRANT_PERSISTABLE_URI_PERMISSION or
|
||||||
|
FLAG_GRANT_READ_URI_PERMISSION or FLAG_GRANT_WRITE_URI_PERMISSION
|
||||||
|
addFlags(flags)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -8,27 +8,15 @@ import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
import android.content.pm.PackageManager.GET_META_DATA
|
import android.content.pm.PackageManager.GET_META_DATA
|
||||||
import android.content.pm.ProviderInfo
|
import android.content.pm.ProviderInfo
|
||||||
import android.database.ContentObserver
|
import android.database.ContentObserver
|
||||||
import android.database.Cursor
|
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.provider.DocumentsContract
|
import android.provider.DocumentsContract
|
||||||
import android.provider.DocumentsContract.PROVIDER_INTERFACE
|
import android.provider.DocumentsContract.PROVIDER_INTERFACE
|
||||||
import android.provider.DocumentsContract.Root.COLUMN_AVAILABLE_BYTES
|
|
||||||
import android.provider.DocumentsContract.Root.COLUMN_DOCUMENT_ID
|
|
||||||
import android.provider.DocumentsContract.Root.COLUMN_FLAGS
|
|
||||||
import android.provider.DocumentsContract.Root.COLUMN_ICON
|
|
||||||
import android.provider.DocumentsContract.Root.COLUMN_ROOT_ID
|
|
||||||
import android.provider.DocumentsContract.Root.COLUMN_SUMMARY
|
|
||||||
import android.provider.DocumentsContract.Root.COLUMN_TITLE
|
|
||||||
import android.provider.DocumentsContract.Root.FLAG_LOCAL_ONLY
|
|
||||||
import android.provider.DocumentsContract.Root.FLAG_REMOVABLE_USB
|
|
||||||
import android.provider.DocumentsContract.Root.FLAG_SUPPORTS_CREATE
|
|
||||||
import android.provider.DocumentsContract.Root.FLAG_SUPPORTS_IS_CHILD
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.stevesoltys.seedvault.R
|
import com.stevesoltys.seedvault.R
|
||||||
import java.lang.Long.parseLong
|
import com.stevesoltys.seedvault.ui.storage.StorageRootResolver.getIcon
|
||||||
|
|
||||||
private val TAG = StorageRootFetcher::class.java.simpleName
|
private val TAG = StorageRootFetcher::class.java.simpleName
|
||||||
|
|
||||||
|
@ -116,51 +104,12 @@ internal class StorageRootFetcher(private val context: Context, private val isRe
|
||||||
private fun getRoots(providerInfo: ProviderInfo): List<StorageRoot> {
|
private fun getRoots(providerInfo: ProviderInfo): List<StorageRoot> {
|
||||||
val authority = providerInfo.authority
|
val authority = providerInfo.authority
|
||||||
val provider = packageManager.resolveContentProvider(authority, GET_META_DATA)
|
val provider = packageManager.resolveContentProvider(authority, GET_META_DATA)
|
||||||
if (provider == null || !provider.isSupported()) {
|
return if (provider == null || !provider.isSupported()) {
|
||||||
Log.w(TAG, "Failed to get provider info for $authority")
|
Log.w(TAG, "Failed to get provider info for $authority")
|
||||||
return emptyList()
|
emptyList()
|
||||||
|
} else {
|
||||||
|
StorageRootResolver.getStorageRoots(context, authority)
|
||||||
}
|
}
|
||||||
|
|
||||||
val roots = ArrayList<StorageRoot>()
|
|
||||||
val rootsUri = DocumentsContract.buildRootsUri(authority)
|
|
||||||
|
|
||||||
var cursor: Cursor? = null
|
|
||||||
try {
|
|
||||||
cursor = contentResolver.query(rootsUri, null, null, null, null)
|
|
||||||
while (cursor!!.moveToNext()) {
|
|
||||||
val root = getStorageRoot(authority, cursor)
|
|
||||||
if (root != null) roots.add(root)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.w(TAG, "Failed to load some roots from $authority", e)
|
|
||||||
} finally {
|
|
||||||
cursor?.close()
|
|
||||||
}
|
|
||||||
return roots
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getStorageRoot(authority: String, cursor: Cursor): StorageRoot? {
|
|
||||||
val flags = cursor.getInt(COLUMN_FLAGS)
|
|
||||||
val supportsCreate = flags and FLAG_SUPPORTS_CREATE != 0
|
|
||||||
val supportsIsChild = flags and FLAG_SUPPORTS_IS_CHILD != 0
|
|
||||||
if (!supportsCreate || !supportsIsChild) return null
|
|
||||||
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(
|
|
||||||
authority = authority,
|
|
||||||
rootId = rootId,
|
|
||||||
documentId = documentId,
|
|
||||||
icon = getIcon(context, authority, rootId, cursor.getInt(COLUMN_ICON)),
|
|
||||||
title = cursor.getString(COLUMN_TITLE)!!,
|
|
||||||
summary = cursor.getString(COLUMN_SUMMARY),
|
|
||||||
availableBytes = cursor.getLong(COLUMN_AVAILABLE_BYTES).let { bytes ->
|
|
||||||
// AOSP 11 reports -1 instead of null
|
|
||||||
if (bytes == -1L) null else bytes
|
|
||||||
},
|
|
||||||
isUsb = flags and FLAG_REMOVABLE_USB != 0,
|
|
||||||
requiresNetwork = flags and FLAG_LOCAL_ONLY == 0 // not local only == requires network
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun checkOrAddUsbRoot(roots: ArrayList<StorageRoot>) {
|
private fun checkOrAddUsbRoot(roots: ArrayList<StorageRoot>) {
|
||||||
|
@ -256,55 +205,9 @@ internal class StorageRootFetcher(private val context: Context, private val isRe
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isAuthoritySupported(authority: String): Boolean {
|
private fun isAuthoritySupported(authority: String): Boolean {
|
||||||
// just restrict where to store backups, restoring can be more free for forward compatibility
|
// just restrict where to store backups,
|
||||||
|
// restoring can be more free for forward compatibility
|
||||||
return isRestore || whitelistedAuthorities.contains(authority)
|
return isRestore || whitelistedAuthorities.contains(authority)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Cursor.getString(columnName: String): String? {
|
|
||||||
val index = getColumnIndex(columnName)
|
|
||||||
return if (index != -1) getString(index) else null
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Cursor.getInt(columnName: String): Int {
|
|
||||||
val index = getColumnIndex(columnName)
|
|
||||||
return if (index != -1) getInt(index) else 0
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Cursor.getLong(columnName: String): Long? {
|
|
||||||
val index = getColumnIndex(columnName)
|
|
||||||
if (index == -1) return null
|
|
||||||
val value = getString(index) ?: return null
|
|
||||||
return try {
|
|
||||||
parseLong(value)
|
|
||||||
} catch (e: NumberFormatException) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getIcon(context: Context, authority: String, rootId: String, icon: Int): Drawable? {
|
|
||||||
return getPackageIcon(context, authority, icon) ?: when {
|
|
||||||
authority == AUTHORITY_STORAGE && rootId == ROOT_ID_DEVICE -> {
|
|
||||||
context.getDrawable(R.drawable.ic_phone_android)
|
|
||||||
}
|
|
||||||
authority == AUTHORITY_STORAGE && rootId != ROOT_ID_HOME -> {
|
|
||||||
context.getDrawable(R.drawable.ic_usb)
|
|
||||||
}
|
|
||||||
authority == AUTHORITY_NEXTCLOUD -> {
|
|
||||||
context.getDrawable(R.drawable.nextcloud)
|
|
||||||
}
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getPackageIcon(context: Context, authority: String, icon: Int): Drawable? {
|
|
||||||
if (icon != 0) {
|
|
||||||
val pm = context.packageManager
|
|
||||||
val info = pm.resolveContentProvider(authority, 0)
|
|
||||||
if (info != null) {
|
|
||||||
return pm.getDrawable(info.packageName, icon, info.applicationInfo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,113 @@
|
||||||
|
package com.stevesoltys.seedvault.ui.storage
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.database.Cursor
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.provider.DocumentsContract
|
||||||
|
import android.provider.DocumentsContract.Root.COLUMN_AVAILABLE_BYTES
|
||||||
|
import android.provider.DocumentsContract.Root.COLUMN_DOCUMENT_ID
|
||||||
|
import android.provider.DocumentsContract.Root.COLUMN_FLAGS
|
||||||
|
import android.provider.DocumentsContract.Root.COLUMN_ICON
|
||||||
|
import android.provider.DocumentsContract.Root.COLUMN_ROOT_ID
|
||||||
|
import android.provider.DocumentsContract.Root.COLUMN_SUMMARY
|
||||||
|
import android.provider.DocumentsContract.Root.COLUMN_TITLE
|
||||||
|
import android.provider.DocumentsContract.Root.FLAG_LOCAL_ONLY
|
||||||
|
import android.provider.DocumentsContract.Root.FLAG_REMOVABLE_USB
|
||||||
|
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
|
||||||
|
|
||||||
|
object StorageRootResolver {
|
||||||
|
|
||||||
|
private val TAG = StorageRootResolver::class.java.simpleName
|
||||||
|
|
||||||
|
fun getStorageRoots(context: Context, authority: String): List<StorageRoot> {
|
||||||
|
val roots = ArrayList<StorageRoot>()
|
||||||
|
val rootsUri = DocumentsContract.buildRootsUri(authority)
|
||||||
|
|
||||||
|
try {
|
||||||
|
context.contentResolver.query(rootsUri, null, null, null, null)?.use { cursor ->
|
||||||
|
while (cursor.moveToNext()) {
|
||||||
|
val root = getStorageRoot(context, authority, cursor)
|
||||||
|
if (root != null) roots.add(root)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "Failed to load some roots from $authority", e)
|
||||||
|
}
|
||||||
|
return roots
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getStorageRoot(context: Context, authority: String, cursor: Cursor): StorageRoot? {
|
||||||
|
val flags = cursor.getInt(COLUMN_FLAGS)
|
||||||
|
val supportsCreate = flags and FLAG_SUPPORTS_CREATE != 0
|
||||||
|
val supportsIsChild = flags and FLAG_SUPPORTS_IS_CHILD != 0
|
||||||
|
if (!supportsCreate || !supportsIsChild) return null
|
||||||
|
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(
|
||||||
|
authority = authority,
|
||||||
|
rootId = rootId,
|
||||||
|
documentId = documentId,
|
||||||
|
icon = getIcon(context, authority, rootId, cursor.getInt(COLUMN_ICON)),
|
||||||
|
title = cursor.getString(COLUMN_TITLE)!!,
|
||||||
|
summary = cursor.getString(COLUMN_SUMMARY),
|
||||||
|
availableBytes = cursor.getLong(COLUMN_AVAILABLE_BYTES).let { bytes ->
|
||||||
|
// AOSP 11 reports -1 instead of null
|
||||||
|
if (bytes == -1L) null else bytes
|
||||||
|
},
|
||||||
|
isUsb = flags and FLAG_REMOVABLE_USB != 0,
|
||||||
|
requiresNetwork = flags and FLAG_LOCAL_ONLY == 0 // not local only == requires network
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Cursor.getString(columnName: String): String? {
|
||||||
|
val index = getColumnIndex(columnName)
|
||||||
|
return if (index != -1) getString(index) else null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Cursor.getInt(columnName: String): Int {
|
||||||
|
val index = getColumnIndex(columnName)
|
||||||
|
return if (index != -1) getInt(index) else 0
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Cursor.getLong(columnName: String): Long? {
|
||||||
|
val index = getColumnIndex(columnName)
|
||||||
|
if (index == -1) return null
|
||||||
|
val value = getString(index) ?: return null
|
||||||
|
return try {
|
||||||
|
java.lang.Long.parseLong(value)
|
||||||
|
} catch (e: NumberFormatException) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getIcon(context: Context, authority: String, rootId: String, icon: Int): Drawable? {
|
||||||
|
return getPackageIcon(context, authority, icon) ?: when {
|
||||||
|
authority == AUTHORITY_STORAGE && rootId == ROOT_ID_DEVICE -> {
|
||||||
|
context.getDrawable(R.drawable.ic_phone_android)
|
||||||
|
}
|
||||||
|
authority == AUTHORITY_STORAGE && rootId != ROOT_ID_HOME -> {
|
||||||
|
context.getDrawable(R.drawable.ic_usb)
|
||||||
|
}
|
||||||
|
authority == AUTHORITY_NEXTCLOUD -> {
|
||||||
|
context.getDrawable(R.drawable.nextcloud)
|
||||||
|
}
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getPackageIcon(context: Context, authority: String, icon: Int): Drawable? {
|
||||||
|
if (icon != 0) {
|
||||||
|
val pm = context.packageManager
|
||||||
|
val info = pm.resolveContentProvider(authority, 0)
|
||||||
|
if (info != null) {
|
||||||
|
return pm.getDrawable(info.packageName, icon, info.applicationInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
package com.stevesoltys.seedvault.ui.storage
|
package com.stevesoltys.seedvault.ui.storage
|
||||||
|
|
||||||
|
import android.Manifest.permission.MANAGE_DOCUMENTS
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
@ -17,6 +18,7 @@ import android.widget.ImageView
|
||||||
import android.widget.ProgressBar
|
import android.widget.ProgressBar
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.activity.result.contract.ActivityResultContracts.OpenDocumentTree
|
import androidx.activity.result.contract.ActivityResultContracts.OpenDocumentTree
|
||||||
|
import androidx.annotation.RequiresPermission
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
@ -27,6 +29,7 @@ import org.koin.androidx.viewmodel.ext.android.getSharedViewModel
|
||||||
internal class StorageRootsFragment : Fragment(), StorageRootClickedListener {
|
internal class StorageRootsFragment : Fragment(), StorageRootClickedListener {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@RequiresPermission(MANAGE_DOCUMENTS)
|
||||||
fun newInstance(isRestore: Boolean): StorageRootsFragment {
|
fun newInstance(isRestore: Boolean): StorageRootsFragment {
|
||||||
val f = StorageRootsFragment()
|
val f = StorageRootsFragment()
|
||||||
f.arguments = Bundle().apply {
|
f.arguments = Bundle().apply {
|
||||||
|
|
|
@ -2,4 +2,5 @@
|
||||||
<config>
|
<config>
|
||||||
<backup-transport-whitelisted-service
|
<backup-transport-whitelisted-service
|
||||||
service="com.stevesoltys.seedvault/.transport.ConfigurableBackupTransportService" />
|
service="com.stevesoltys.seedvault/.transport.ConfigurableBackupTransportService" />
|
||||||
|
<hidden-api-whitelisted-app package="com.stevesoltys.seedvault" />
|
||||||
</config>
|
</config>
|
||||||
|
|
Loading…
Reference in a new issue