Skip to content

Commit bed4868

Browse files
QuickEditor: Handle CAMERA permission (#394)
* Check CAMERA permission before taking photo * Generate quickeditor.api file
1 parent 10e422f commit bed4868

File tree

6 files changed

+134
-11
lines changed

6 files changed

+134
-11
lines changed

demo-app/src/main/AndroidManifest.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
xmlns:tools="http://schemas.android.com/tools">
44

55
<uses-permission android:name="android.permission.INTERNET" />
6+
<!-- <uses-permission android:name="android.permission.CAMERA" />-->
67

78
<application
89
android:allowBackup="true"

gravatar-quickeditor/api/gravatar-quickeditor.api

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,17 @@ public final class com/gravatar/quickeditor/ui/components/ComposableSingletons$A
2828
public final fun getLambda-3$gravatar_quickeditor_release ()Lkotlin/jvm/functions/Function2;
2929
}
3030

31+
public final class com/gravatar/quickeditor/ui/components/ComposableSingletons$CameraPermissionRationaleDialogKt {
32+
public static final field INSTANCE Lcom/gravatar/quickeditor/ui/components/ComposableSingletons$CameraPermissionRationaleDialogKt;
33+
public static field lambda-1 Lkotlin/jvm/functions/Function3;
34+
public static field lambda-2 Lkotlin/jvm/functions/Function2;
35+
public static field lambda-3 Lkotlin/jvm/functions/Function2;
36+
public fun <init> ()V
37+
public final fun getLambda-1$gravatar_quickeditor_release ()Lkotlin/jvm/functions/Function3;
38+
public final fun getLambda-2$gravatar_quickeditor_release ()Lkotlin/jvm/functions/Function2;
39+
public final fun getLambda-3$gravatar_quickeditor_release ()Lkotlin/jvm/functions/Function2;
40+
}
41+
3142
public final class com/gravatar/quickeditor/ui/components/ComposableSingletons$EmailLabelKt {
3243
public static final field INSTANCE Lcom/gravatar/quickeditor/ui/components/ComposableSingletons$EmailLabelKt;
3344
public static field lambda-1 Lkotlin/jvm/functions/Function2;

gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/ui/components/AvatarsSection.kt

Lines changed: 80 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
package com.gravatar.quickeditor.ui.components
22

3+
import android.Manifest
4+
import android.content.Context
5+
import android.content.pm.PackageManager
36
import android.net.Uri
47
import androidx.activity.compose.rememberLauncherForActivityResult
8+
import androidx.activity.result.ActivityResultLauncher
59
import androidx.activity.result.PickVisualMediaRequest
610
import androidx.activity.result.contract.ActivityResultContracts
711
import androidx.annotation.StringRes
@@ -13,11 +17,14 @@ import androidx.compose.runtime.setValue
1317
import androidx.compose.ui.Modifier
1418
import androidx.compose.ui.platform.LocalContext
1519
import androidx.compose.ui.tooling.preview.Preview
20+
import androidx.core.app.ActivityCompat
21+
import androidx.core.content.ContextCompat
1622
import com.gravatar.quickeditor.QuickEditorFileProvider
1723
import com.gravatar.quickeditor.R
1824
import com.gravatar.quickeditor.ui.avatarpicker.AvatarUi
1925
import com.gravatar.quickeditor.ui.avatarpicker.AvatarsSectionUiState
2026
import com.gravatar.quickeditor.ui.editor.AvatarPickerContentLayout
27+
import com.gravatar.quickeditor.ui.oauth.findComponentActivity
2128
import com.gravatar.restapi.models.Avatar
2229
import com.gravatar.ui.GravatarTheme
2330
import java.net.URI
@@ -30,6 +37,7 @@ internal fun AvatarsSection(
3037
modifier: Modifier = Modifier,
3138
) {
3239
val context = LocalContext.current
40+
var cameraPermissionDialogRationaleVisible by rememberSaveable { mutableStateOf(false) }
3341
var photoImageUri by rememberSaveable { mutableStateOf<Uri?>(null) }
3442
val pickMedia = rememberLauncherForActivityResult(ActivityResultContracts.PickVisualMedia()) { uri ->
3543
uri?.let { onLocalImageSelected(it) }
@@ -39,6 +47,32 @@ internal fun AvatarsSection(
3947
if (success && takenPictureUri != null) onLocalImageSelected(takenPictureUri)
4048
}
4149

50+
val takePhotoCallback = {
51+
val imageUri = QuickEditorFileProvider.getTempCameraImageUri(context)
52+
photoImageUri = imageUri
53+
takePhoto.launch(imageUri)
54+
}
55+
56+
val cameraPermissionLauncher = rememberLauncherForActivityResult(
57+
ActivityResultContracts.RequestPermission(),
58+
) { isGranted: Boolean ->
59+
if (isGranted) {
60+
takePhotoCallback()
61+
} else {
62+
cameraPermissionDialogRationaleVisible = true
63+
}
64+
}
65+
66+
val permissionAwareTakePhotoCallback = {
67+
context.withCameraPermission(
68+
cameraPermissionLauncher = cameraPermissionLauncher,
69+
onShowRationale = { cameraPermissionDialogRationaleVisible = true },
70+
grantedCallback = {
71+
takePhotoCallback()
72+
},
73+
)
74+
}
75+
4276
when (state.avatarPickerContentLayout) {
4377
AvatarPickerContentLayout.Vertical -> {
4478
VerticalAvatarsSection(
@@ -48,11 +82,7 @@ internal fun AvatarsSection(
4882
onChoosePhotoClick = {
4983
pickMedia.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly))
5084
},
51-
onTakePhotoClick = {
52-
val imageUri = QuickEditorFileProvider.getTempCameraImageUri(context)
53-
photoImageUri = imageUri
54-
takePhoto.launch(imageUri)
55-
},
85+
onTakePhotoClick = permissionAwareTakePhotoCallback,
5686
)
5787
}
5888

@@ -61,17 +91,50 @@ internal fun AvatarsSection(
6191
state = state,
6292
modifier = modifier,
6393
onAvatarSelected = onAvatarSelected,
64-
onTakePhotoClick = {
65-
val imageUri = QuickEditorFileProvider.getTempCameraImageUri(context)
66-
photoImageUri = imageUri
67-
takePhoto.launch(imageUri)
68-
},
94+
onTakePhotoClick = permissionAwareTakePhotoCallback,
6995
onChoosePhotoClick = {
7096
pickMedia.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly))
7197
},
7298
)
7399
}
74100
}
101+
102+
CameraPermissionRationaleDialog(
103+
cameraPermissionDialogRationaleVisible,
104+
onDismiss = { cameraPermissionDialogRationaleVisible = false },
105+
onConfirmation = {
106+
cameraPermissionDialogRationaleVisible = false
107+
},
108+
)
109+
}
110+
111+
internal fun Context.withCameraPermission(
112+
cameraPermissionLauncher: ActivityResultLauncher<String>,
113+
onShowRationale: () -> Unit = {},
114+
grantedCallback: () -> Unit,
115+
) {
116+
if (hasCameraPermissionInManifest()) {
117+
val activity = findComponentActivity()
118+
when {
119+
ContextCompat.checkSelfPermission(
120+
this,
121+
Manifest.permission.CAMERA,
122+
) == PackageManager.PERMISSION_GRANTED -> {
123+
grantedCallback()
124+
}
125+
126+
activity != null &&
127+
ActivityCompat.shouldShowRequestPermissionRationale(activity, Manifest.permission.CAMERA) -> {
128+
onShowRationale()
129+
}
130+
131+
else -> {
132+
cameraPermissionLauncher.launch(Manifest.permission.CAMERA)
133+
}
134+
}
135+
} else {
136+
grantedCallback()
137+
}
75138
}
76139

77140
internal val AvatarsSectionUiState.titleRes: Int
@@ -155,3 +218,10 @@ private fun AvatarSectionEmptyPreview() {
155218
)
156219
}
157220
}
221+
222+
private fun Context.hasCameraPermissionInManifest(): Boolean {
223+
val packageInfo = packageManager.getPackageInfo(packageName, PackageManager.GET_PERMISSIONS)
224+
val permissions = packageInfo.requestedPermissions
225+
226+
return permissions?.any { perm -> perm == Manifest.permission.CAMERA } ?: false
227+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package com.gravatar.quickeditor.ui.components
2+
3+
import androidx.compose.material3.AlertDialog
4+
import androidx.compose.material3.Text
5+
import androidx.compose.material3.TextButton
6+
import androidx.compose.runtime.Composable
7+
import androidx.compose.ui.res.stringResource
8+
import com.gravatar.quickeditor.R
9+
10+
@Composable
11+
internal fun CameraPermissionRationaleDialog(
12+
isVisible: Boolean,
13+
onConfirmation: () -> Unit,
14+
onDismiss: () -> Unit = {},
15+
) {
16+
if (isVisible) {
17+
AlertDialog(
18+
title = {
19+
Text(text = stringResource(R.string.gravatar_qe_permission_required_alert_title))
20+
},
21+
text = {
22+
Text(
23+
text = stringResource(R.string.gravatar_qe_camera_permission_rationale_message),
24+
)
25+
},
26+
onDismissRequest = onDismiss,
27+
confirmButton = {
28+
TextButton(
29+
onClick = {
30+
onConfirmation()
31+
},
32+
) {
33+
Text(stringResource(R.string.gravatar_qe_dismiss))
34+
}
35+
},
36+
)
37+
}
38+
}

gravatar-quickeditor/src/main/java/com/gravatar/quickeditor/ui/oauth/OAuthPage.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ private fun launchCustomTab(context: Context, oauthParams: OAuthParams, email: E
172172
)
173173
}
174174

175-
private fun Context.findComponentActivity(): ComponentActivity? = when (this) {
175+
internal fun Context.findComponentActivity(): ComponentActivity? = when (this) {
176176
is ComponentActivity -> this
177177
is ContextWrapper -> baseContext.findComponentActivity()
178178
else -> null

gravatar-quickeditor/src/main/res/values/strings.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,7 @@
3434
<string name="avatar_upload_failure_dialog_title">Couldn\'t upload image</string>
3535
<string name="avatar_upload_failure_dialog_remove_upload">Remove upload</string>
3636
<string name="oauth_email_associated_error_message">Something went wrong when verifying your email.</string>
37+
<string name="gravatar_qe_permission_required_alert_title">Permission required</string>
38+
<string name="gravatar_qe_camera_permission_rationale_message">To take a picture, you need to grant camera permission. You can grant it in the app settings.</string>
39+
<string name="gravatar_qe_dismiss">Dismiss</string>
3740
</resources>

0 commit comments

Comments
 (0)