Skip to content

Commit 81e0250

Browse files
authored
Camera permission request behavior changes for QR + PIN Auth, Fixes AB#3032520 (#2524)
There is a new CELA requirement to change the behavior of the camera permission request for QR + PIN Auth. The current behavior is that once we get the camera permission for the app we never ask again. The change is to ask every time we need to use the camera. In addition to this change we move some functionality to the new class CameraPermissionRequest and did some update on the name of the functions. [AB#3032520](https://identitydivision.visualstudio.com/fac9d424-53d2-45c0-91b5-ef6ba7a6bf26/_workitems/edit/3032520)
1 parent fc41b4c commit 81e0250

File tree

3 files changed

+126
-79
lines changed

3 files changed

+126
-79
lines changed

changelog.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
vNext
22
----------
3+
- [MINOR] Camera permission request behavior changes for QR + PIN Auth (#2524)
34
- [MINOR] Migrate Base64 away from Msebera httpclient (#2389)
45
- [PATCH] Provide empty image when no video is present on QR +PIN auth (#2424)
56
- [MINOR] On GetPreferredAuthMethod return NONE when Authenticator is not installed (#2523)
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// All rights reserved.
3+
//
4+
// This code is licensed under the MIT License.
5+
//
6+
// Permission is hereby granted, free of charge, to any person obtaining a copy
7+
// of this software and associated documentation files(the "Software"), to deal
8+
// in the Software without restriction, including without limitation the rights
9+
// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
10+
// copies of the Software, and to permit persons to whom the Software is
11+
// furnished to do so, subject to the following conditions :
12+
//
13+
// The above copyright notice and this permission notice shall be included in
14+
// all copies or substantial portions of the Software.
15+
//
16+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22+
// THE SOFTWARE.
23+
package com.microsoft.identity.common.internal.providers.oauth2
24+
25+
import android.os.Build
26+
import android.webkit.PermissionRequest
27+
import com.microsoft.identity.common.logging.Logger
28+
29+
/**
30+
* Class responsible for handling camera permission requests.
31+
* Note: This class is compatible only with API level 21 and above;
32+
* functionality will not behave as expected on lower versions.
33+
*/
34+
class CameraPermissionRequest(private val mCameraPermissionRequest: PermissionRequest) {
35+
/**
36+
* Check if the permission was granted.
37+
*
38+
* @return true if the permission is granted.
39+
*/
40+
var isGranted = false
41+
private set
42+
43+
/**
44+
* Call this method to grant the permission to access the camera resource.
45+
* The granted permission is only valid for the current WebView.
46+
*
47+
*
48+
* Note: This method is only available on API level 21 or higher.
49+
*/
50+
fun grant() {
51+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
52+
val cameraResource = arrayOf(
53+
PermissionRequest.RESOURCE_VIDEO_CAPTURE
54+
)
55+
mCameraPermissionRequest.grant(cameraResource)
56+
}
57+
isGranted = true
58+
}
59+
60+
/**
61+
* Call this method to deny the permission to access the camera resource.
62+
*
63+
*
64+
* Note: This method is only available on API level 21 or higher.
65+
*/
66+
fun deny() {
67+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
68+
mCameraPermissionRequest.deny()
69+
}
70+
isGranted = false
71+
}
72+
73+
companion object {
74+
private val TAG = CameraPermissionRequest::class.java.simpleName
75+
76+
/**
77+
* Determines whatever if the given permission request is for the camera resource.
78+
*
79+
*
80+
* Note: This method is only available on API level 21 or higher.
81+
* Devices running on lower API levels will not be able to grant or deny camera permission requests.
82+
* getResources() method is only available on API level 21 or higher.
83+
*
84+
* @param request The permission request.
85+
* @return true if the given permission request is for camera, false otherwise.
86+
*/
87+
@JvmStatic
88+
fun isValidRequest(request: PermissionRequest): Boolean {
89+
val methodTag = "$TAG:validateRequest"
90+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
91+
return request.resources.size == 1 && PermissionRequest.RESOURCE_VIDEO_CAPTURE == request.resources[0]
92+
}
93+
Logger.warn(
94+
methodTag, "PermissionRequest.getResources() method is not available on API:"
95+
+ Build.VERSION.SDK_INT + ". We cannot determine if the request is for camera."
96+
)
97+
return false
98+
}
99+
}
100+
}

common/src/main/java/com/microsoft/identity/common/internal/providers/oauth2/WebViewAuthorizationFragment.java

Lines changed: 25 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ public class WebViewAuthorizationFragment extends AuthorizationFragment {
116116

117117
private boolean webViewZoomEnabled;
118118

119-
private PermissionRequest mCameraPermissionRequest;
119+
private CameraPermissionRequest mCameraPermissionRequest;
120120

121121
// This is used by LegacyFido2ApiManager to launch a PendingIntent received by the legacy API.
122122
private ActivityResultLauncher<LegacyFido2ApiObject> mFidoLauncher;
@@ -277,29 +277,29 @@ public void onPermissionRequest(final PermissionRequest request) {
277277
// We can only grant or deny permissions for video capture/camera.
278278
// To avoid unintentionally granting requests for not defined permissions
279279
// we check if the request is for camera.
280-
if (!isPermissionRequestForCamera(request)) {
280+
if (!CameraPermissionRequest.isValidRequest(request)) {
281281
Logger.warn(methodTag, "Permission request is not for camera.");
282282
request.deny();
283283
return;
284284
}
285285
// There is a issue in ESTS UX where it sends multiple camera permission requests.
286286
// So, if there is already a camera permission request in progress we handle it here.
287287
if (mCameraPermissionRequest != null) {
288-
handleRepeatedRequests(request);
288+
Logger.info(methodTag, "Repeated request, granted? " + mCameraPermissionRequest.isGranted());
289+
handleRepeatedCameraRequests(request);
289290
return;
290291
}
291292
Logger.info(methodTag, "New camera request.");
292-
mCameraPermissionRequest = request;
293-
if (isCameraPermissionGranted()) {
293+
mCameraPermissionRequest = new CameraPermissionRequest(request);
294+
// If the OS level permission was granted previously,
295+
// we show the rationale to confirm the consent with the current user.
296+
// Otherwise, show the system prompt.
297+
if (isAppCameraPermissionGranted()) {
294298
Logger.info(methodTag, "Camera permission already granted.");
295-
acceptCameraRequest();
296-
} else if (shouldShowRequestPermissionRationale(Manifest.permission.CAMERA)) {
297-
Logger.info(methodTag, "Show camera rationale.");
298299
showCameraRationale();
299300
} else {
300-
requestCameraPermissionFromUser();
301+
requestCameraPermission();
301302
}
302-
303303
}
304304

305305
@Override
@@ -323,47 +323,12 @@ public Bitmap getDefaultVideoPoster() {
323323
*
324324
* @param request The permission request.
325325
*/
326-
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
327-
private void handleRepeatedRequests(@NonNull final PermissionRequest request) {
328-
final String methodTag = TAG + ":handleRepeatedRequests";
329-
if (isCameraPermissionGranted()) {
330-
Logger.info(methodTag, "Repeated request, granting the permission.");
331-
final String[] cameraPermission = new String[] {
332-
PermissionRequest.RESOURCE_VIDEO_CAPTURE
333-
};
334-
request.grant(cameraPermission);
326+
private void handleRepeatedCameraRequests(@NonNull final PermissionRequest request) {
327+
final CameraPermissionRequest duplicatedRequest = new CameraPermissionRequest(request);
328+
if (mCameraPermissionRequest.isGranted()) {
329+
duplicatedRequest.grant();
335330
} else {
336-
Logger.info(methodTag, "Repeated request, denying the permission");
337-
request.deny();
338-
}
339-
}
340-
341-
/**
342-
* Call this method to grant the permission to access the camera resource.
343-
* The granted permission is only valid for the current WebView.
344-
* <p>
345-
* Note: This method is only available on API level 21 or higher.
346-
*/
347-
private void acceptCameraRequest() {
348-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
349-
final String[] cameraPermission = new String[] {
350-
PermissionRequest.RESOURCE_VIDEO_CAPTURE
351-
};
352-
if (mCameraPermissionRequest != null) {
353-
mCameraPermissionRequest.grant(cameraPermission);
354-
}
355-
}
356-
}
357-
358-
/**
359-
* Call this method to deny the permission to access the camera resource.
360-
* <p>
361-
* Note: This method is only available on API level 21 or higher.
362-
*/
363-
private void denyCameraRequest() {
364-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP &&
365-
mCameraPermissionRequest != null) {
366-
mCameraPermissionRequest.deny();
331+
duplicatedRequest.deny();
367332
}
368333
}
369334

@@ -372,50 +337,32 @@ private void denyCameraRequest() {
372337
*
373338
* @return true if the camera permission has been granted, false otherwise.
374339
*/
375-
private boolean isCameraPermissionGranted() {
340+
private boolean isAppCameraPermissionGranted() {
376341
return ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.CAMERA)
377342
== PackageManager.PERMISSION_GRANTED;
378343
}
379344

380-
/**
381-
* Determines whatever if the given permission request is for the camera resource.
382-
* <p>
383-
* Note: This method is only available on API level 21 or higher.
384-
* Devices running on lower API levels will not be able to grant or deny camera permission requests.
385-
* getResources() method is only available on API level 21 or higher.
386-
*
387-
* @param request The permission request.
388-
* @return true if the given permission request is for camera, false otherwise.
389-
*/
390-
private boolean isPermissionRequestForCamera(final PermissionRequest request) {
391-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
392-
return request.getResources().length == 1 &&
393-
PermissionRequest.RESOURCE_VIDEO_CAPTURE.equals(request.getResources()[0]);
394-
}
395-
Logger.warn(TAG, "PermissionRequest.getResources() method is not available on API:"
396-
+ Build.VERSION.SDK_INT + ". We cannot determine if the request is for camera.");
397-
return false;
398-
}
399-
400345
private final ActivityResultLauncher<String> cameraRequestActivity = registerForActivityResult(
401346
new ActivityResultContracts.RequestPermission(),
402347
permissionGranted -> {
403348
Logger.info(TAG, "Camera permission granted: " + permissionGranted);
404349
if (permissionGranted) {
405-
acceptCameraRequest();
350+
mCameraPermissionRequest.grant();
406351
}
407352
else {
408-
denyCameraRequest();
353+
mCameraPermissionRequest.deny();
409354
}
410355
}
411356
);
412357

413358
/**
414359
* Launches the camera permission request for the app.
360+
* Note: if the permission was already granted or denied,
361+
* the user will not be prompted and it will go directly to the callback.
362+
* Using the current state of the permission.
363+
*
415364
*/
416-
private void requestCameraPermissionFromUser() {
417-
final String methodTAG = TAG + ":requestCameraPermissionFromUser";
418-
Logger.info(methodTAG, "Requesting camera permission.");
365+
private void requestCameraPermission() {
419366
cameraRequestActivity.launch(Manifest.permission.CAMERA);
420367
}
421368

@@ -429,12 +376,11 @@ private void showCameraRationale() {
429376
builder.setMessage(R.string.qr_code_rationale_message)
430377
.setTitle(R.string.qr_code_rationale_header)
431378
.setCancelable(false)
432-
.setPositiveButton(R.string.qr_code_rationale_allow, (dialog, id) -> requestCameraPermissionFromUser())
433-
.setNegativeButton(R.string.qr_code_rationale_block, (dialog, id) -> denyCameraRequest());
379+
.setPositiveButton(R.string.qr_code_rationale_allow, (dialog, id) -> requestCameraPermission())
380+
.setNegativeButton(R.string.qr_code_rationale_block, (dialog, id) -> mCameraPermissionRequest.deny());
434381
builder.show();
435382
}
436383

437-
438384
/**
439385
* Loads starting authorization request url into WebView.
440386
*/

0 commit comments

Comments
 (0)