Skip to content
This repository has been archived by the owner on Feb 22, 2024. It is now read-only.

Commit

Permalink
Add RequestAccess ActivityResultContract to request the right storage…
Browse files Browse the repository at this point in the history
… permissions based (#84)

Update sample to use RequestAccess
Update permissions guide to include RequestAccess
Add navigation link to changelogs generated by GitHub
  • Loading branch information
yrezgui authored Jan 21, 2022
1 parent 9aad0dd commit c61c6c5
Show file tree
Hide file tree
Showing 7 changed files with 242 additions and 26 deletions.
106 changes: 101 additions & 5 deletions docs/permissions.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ To help you navigate common use cases, check out the below table:
> 1️⃣ When editing or deleting media files created by other apps on API 29+ (Android 10), you have to
> request explicitly user's consent. Read more [here][edit_media_scoped_storage].
## Check if app can read files
## Check if app can access files

```kotlin
// Check if the app can read image & document files created by itself
Expand All @@ -65,11 +65,7 @@ storagePermissions.hasAccess(
types = listOf(FileType.Video, FileType.Audio),
createdBy = StoragePermissions.CreatedBy.AllApps
)
```

## Check if app can read and write files

```kotlin
// Check if the app can read & write image & document files created by itself
val storagePermissions = StoragePermissions(context)

Expand All @@ -87,6 +83,106 @@ storagePermissions.hasAccess(
)
```

## Get storage permissions

!!! note ""

If the method returns an empty list, it means your app on the current device, given the defined
usage, doesn't need any permissions.

```kotlin
// Get required permissions to read & write video & audio files created by all apps
storagePermissions.getPermissions(
action = Action.READ,
types = listOf(FileType.Video, FileType.Audio),
createdBy = StoragePermissions.CreatedBy.AllApps
)

// Get required permissions to read & write image & document files created by the app itself
StoragePermissions.getPermissions(
action = Action.READ_AND_WRITE,
types = listOf(FileType.Image, FileType.Document),
createdBy = StoragePermissions.CreatedBy.Self
)
```

## Request storage permissions

While you can use the `ActivityResultContracts.RequestPermission` provided by default with the
Jetpack Activity or Fragment library to request storage permissions with input from
`StoragePermissions.getPermissions`, `{{ artifact }}` bundles a custom ActivityResultContract named
`RequestAccess` to request the right storage permissions to simplify the logic for you.

=== "Compose"

```kotlin
@Composable
fun RequestAccessExample() {
// Register a callback for the Activity Result
val requestAccess = rememberLauncherForActivityResult(RequestAccess()) { hasAccess ->
if (hasAccess) {
// write logic here
}
}

Column {
Button(onClick = {
// Request permission to read video & audio files created by all apps
requestAccess.launch(
action = Action.READ,
types = listOf(
StoragePermissions.FileType.Video,
StoragePermissions.FileType.Audio
),
createdBy = StoragePermissions.CreatedBy.AllApps
)
}) {
Text("I want to read all video & audio files")
}

Button(onClick = {
// Request permission to read & write image & document files created by the app itself
requestAccess.launch(
action = Action.READ_AND_WRITE,
types = listOf(
StoragePermissions.FileType.Image,
StoragePermissions.FileType.Document
),
createdBy = StoragePermissions.CreatedBy.Self
)
}) {
Text("I want to read & write the app's image & document files")
}
}
}
```

=== "Views"

```kotlin
// Register a callback for the Activity Result
val requestAccess = registerForActivityResult(RequestAccess()) { hasAccess ->
if (hasAccess) {
// write logic here
}
}


// Request permission to read video & audio files created by all apps
requestAccess.launch(
action = Action.READ,
types = listOf(StoragePermissions.FileType.Video, StoragePermissions.FileType.Audio),
createdBy = StoragePermissions.CreatedBy.AllApps
)

// Request permission to read & write image & document files created by the app itself
requestAccess.launch(
action = Action.READ_AND_WRITE,
types = listOf(StoragePermissions.FileType.Image, StoragePermissions.FileType.Document),
createdBy = StoragePermissions.CreatedBy.Self
)
```

[api_reference]: /modernstorage/api/permissions/
[saf_guide]: https://developer.android.com/training/data-storage/shared/documents-files
[edit_media_scoped_storage]: https://developer.android.com/training/data-storage/shared/media#update-other-apps-files
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ android.enableJetifier=false
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
GROUP=com.google.modernstorage
VERSION_NAME=1.0.0-alpha04
VERSION_NAME=1.0.0-alpha05
POM_DESCRIPTION=Utility libraries for storage interactions on Android
POM_URL=https://github.com/google/modernstorage/
POM_SCM_URL=https://github.com/google/modernstorage/
Expand Down
3 changes: 2 additions & 1 deletion mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ theme:

nav:
- 'Overview': README.md
- 'Changelog': https://github.com/google/modernstorage/releases
- 'Permissions': permissions.md
- 'Photo Picker': photopicker.md
- 'Storage interactions': storage.md
Expand Down Expand Up @@ -46,4 +47,4 @@ plugins:
- macros

extra:
lib_version: "1.0.0-alpha04"
lib_version: "1.0.0-alpha05"
17 changes: 17 additions & 0 deletions permissions/api/current.api
Original file line number Diff line number Diff line change
@@ -1,6 +1,23 @@
// Signature format: 4.0
package com.google.modernstorage.permissions {

public final class RequestAccess extends androidx.activity.result.contract.ActivityResultContract<com.google.modernstorage.permissions.RequestAccess.Args,java.lang.Boolean> {
ctor public RequestAccess();
method public android.content.Intent createIntent(android.content.Context context, com.google.modernstorage.permissions.RequestAccess.Args input);
method public androidx.activity.result.contract.ActivityResultContract.SynchronousResult<java.lang.Boolean>? getSynchronousResult(android.content.Context context, com.google.modernstorage.permissions.RequestAccess.Args input);
method public Boolean parseResult(int resultCode, android.content.Intent? intent);
}

public static final class RequestAccess.Args {
ctor public RequestAccess.Args(com.google.modernstorage.permissions.StoragePermissions.Action action, java.util.List<? extends com.google.modernstorage.permissions.StoragePermissions.FileType> types, com.google.modernstorage.permissions.StoragePermissions.CreatedBy createdBy);
method public com.google.modernstorage.permissions.StoragePermissions.Action getAction();
method public com.google.modernstorage.permissions.StoragePermissions.CreatedBy getCreatedBy();
method public java.util.List<com.google.modernstorage.permissions.StoragePermissions.FileType> getTypes();
property public final com.google.modernstorage.permissions.StoragePermissions.Action action;
property public final com.google.modernstorage.permissions.StoragePermissions.CreatedBy createdBy;
property public final java.util.List<com.google.modernstorage.permissions.StoragePermissions.FileType> types;
}

public final class StoragePermissions {
ctor public StoragePermissions(android.content.Context context);
method @Deprecated public boolean canReadAndWriteFiles(java.util.List<? extends com.google.modernstorage.permissions.StoragePermissions.FileType> types, com.google.modernstorage.permissions.StoragePermissions.CreatedBy createdBy);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.modernstorage.permissions

import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import androidx.activity.result.contract.ActivityResultContract
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions.EXTRA_PERMISSION_GRANT_RESULTS
import androidx.core.content.ContextCompat

class RequestAccess : ActivityResultContract<RequestAccess.Args, Boolean>() {
class Args(
val action: StoragePermissions.Action,
val types: List<StoragePermissions.FileType>,
val createdBy: StoragePermissions.CreatedBy
)

override fun createIntent(context: Context, input: Args): Intent {
val permissions =
StoragePermissions.getPermissions(input.action, input.types, input.createdBy)
return Intent(ActivityResultContracts.RequestMultiplePermissions.ACTION_REQUEST_PERMISSIONS).putExtra(
ActivityResultContracts.RequestMultiplePermissions.EXTRA_PERMISSIONS,
permissions.toTypedArray()
)
}

override fun getSynchronousResult(
context: Context,
input: Args
): SynchronousResult<Boolean>? {
val permissions =
StoragePermissions.getPermissions(input.action, input.types, input.createdBy)

if (permissions.isEmpty()) {
return SynchronousResult(true)
}

val allGranted = permissions.all { permission ->
ContextCompat.checkSelfPermission(
context,
permission
) == PackageManager.PERMISSION_GRANTED
}

return if (allGranted) SynchronousResult(true) else null
}

override fun parseResult(
resultCode: Int,
intent: Intent?
): Boolean {
if (resultCode != Activity.RESULT_OK) return false
if (intent == null) return false
val permissions =
intent.getStringArrayExtra(ActivityResultContracts.RequestMultiplePermissions.EXTRA_PERMISSIONS)
val grantResults = intent.getIntArrayExtra(EXTRA_PERMISSION_GRANT_RESULTS)
if (grantResults == null || permissions == null) return false

return grantResults.all { result -> result == PackageManager.PERMISSION_GRANTED }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,7 @@
*/
package com.google.modernstorage.sample.mediastore

import android.Manifest
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
Expand Down Expand Up @@ -46,7 +44,9 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import com.google.modernstorage.permissions.RequestAccess
import com.google.modernstorage.permissions.StoragePermissions
import com.google.modernstorage.permissions.StoragePermissions.Action
import com.google.modernstorage.sample.Demos
import com.google.modernstorage.sample.HomeRoute
import com.google.modernstorage.sample.R
Expand All @@ -68,8 +68,8 @@ fun AddFileToDownloadsScreen(
val permissions = StoragePermissions(LocalContext.current)
val toastMessage = stringResource(R.string.authorization_dialog_success_toast)
val requestPermission =
rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->
if (isGranted) {
rememberLauncherForActivityResult(RequestAccess()) { hasAccess ->
if (hasAccess) {
scope.launch {
scaffoldState.snackbarHostState.showSnackbar(toastMessage)
}
Expand All @@ -84,7 +84,13 @@ fun AddFileToDownloadsScreen(
TextButton(
onClick = {
openPermissionDialog = false
requestPermission.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)
requestPermission.launch(
RequestAccess.Args(
action = Action.READ_AND_WRITE,
types = listOf(StoragePermissions.FileType.Document),
createdBy = StoragePermissions.CreatedBy.Self
)
)
}
) {
Text(stringResource(R.string.authorization_dialog_confirm_label))
Expand All @@ -100,7 +106,8 @@ fun AddFileToDownloadsScreen(
}

fun checkAndRequestStoragePermission(onSuccess: () -> Unit) {
val isGranted = permissions.canReadAndWriteFiles(
val isGranted = permissions.hasAccess(
action = Action.READ_AND_WRITE,
types = listOf(StoragePermissions.FileType.Document),
createdBy = StoragePermissions.CreatedBy.Self
)
Expand Down Expand Up @@ -131,7 +138,9 @@ fun AddFileToDownloadsScreen(
LazyColumn(Modifier.padding(paddingValues)) {
item {
Button(
modifier = Modifier.padding(4.dp).fillMaxWidth(),
modifier = Modifier
.padding(4.dp)
.fillMaxWidth(),
onClick = {
checkAndRequestStoragePermission { viewModel.addDocument(DocumentType.TEXT) }
}
Expand All @@ -141,7 +150,9 @@ fun AddFileToDownloadsScreen(
}
item {
Button(
modifier = Modifier.padding(4.dp).fillMaxWidth(),
modifier = Modifier
.padding(4.dp)
.fillMaxWidth(),
onClick = {
checkAndRequestStoragePermission { viewModel.addDocument(DocumentType.PDF) }
}
Expand All @@ -151,7 +162,9 @@ fun AddFileToDownloadsScreen(
}
item {
Button(
modifier = Modifier.padding(4.dp).fillMaxWidth(),
modifier = Modifier
.padding(4.dp)
.fillMaxWidth(),
onClick = {
checkAndRequestStoragePermission { viewModel.addDocument(DocumentType.ZIP) }
}
Expand Down
Loading

0 comments on commit c61c6c5

Please sign in to comment.