Skip to content

Commit

Permalink
Implement Permission.calendarReadOnly and `Permission.calendarFullA…
Browse files Browse the repository at this point in the history
…ccess` (#1189)

* Clean up `determinePermissionStatus`

* Implement new calendar permissions
  • Loading branch information
JeroenWeener authored Oct 13, 2023
1 parent c2f3055 commit 627daa9
Show file tree
Hide file tree
Showing 6 changed files with 116 additions and 33 deletions.
4 changes: 4 additions & 0 deletions permission_handler_android/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 11.1.0

* Implements the `Permission.calendarReadOnly` and `PermissionCalendarFullAccess` permissions.

## 11.0.5

* Removes the obsolete `updatePermissionShouldShowStatus` method from the Java code base.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@ final class PermissionConstants {
static final int PERMISSION_CODE_ACCESS_NOTIFICATION_POLICY = 213;
static final int PERMISSION_CODE_SCHEDULE_EXACT_ALARM = 214;

//PERMISSION_GROUP

// PERMISSION_GROUP

// Deprecated in favor of PERMISSION_GROUP_CALENDAR_READ_ONLY and
// PERMISSION_GROUP_CALENDAR_FULL_ACCESS.
static final int PERMISSION_GROUP_CALENDAR = 0;
static final int PERMISSION_GROUP_CAMERA = 1;
static final int PERMISSION_GROUP_CONTACTS = 2;
Expand Down Expand Up @@ -54,6 +58,8 @@ final class PermissionConstants {
static final int PERMISSION_GROUP_AUDIO = 33;
static final int PERMISSION_GROUP_SCHEDULE_EXACT_ALARM = 34;
static final int PERMISSION_GROUP_SENSORS_ALWAYS = 35;
static final int PERMISSION_GROUP_CALENDAR_READ_ONLY = 36;
static final int PERMISSION_GROUP_CALENDAR_FULL_ACCESS = 37;

@Retention(RetentionPolicy.SOURCE)
@IntDef({
Expand Down Expand Up @@ -89,7 +95,9 @@ final class PermissionConstants {
PERMISSION_GROUP_NEARBY_WIFI_DEVICES,
PERMISSION_GROUP_VIDEOS,
PERMISSION_GROUP_AUDIO,
PERMISSION_GROUP_SCHEDULE_EXACT_ALARM
PERMISSION_GROUP_SCHEDULE_EXACT_ALARM,
PERMISSION_GROUP_CALENDAR_READ_ONLY,
PERMISSION_GROUP_CALENDAR_FULL_ACCESS
})
@interface PermissionGroup {
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import androidx.core.content.ContextCompat;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
Expand Down Expand Up @@ -165,9 +166,38 @@ public boolean onRequestPermissionsResult(
return false;
}

// Calendar permissions are split between READ and WRITE in Android, and split between READ
// and FULL ACCESS in the plugin. We need special logic for this translation.
final List<String> permissionList = Arrays.asList(permissions);
final int calendarReadIndex = permissionList.indexOf(Manifest.permission.READ_CALENDAR);
final int calendarWriteIndex = permissionList.indexOf(Manifest.permission.WRITE_CALENDAR);
// READ -> READ.
if (calendarReadIndex >= 0) {
final int readGrantResult = grantResults[calendarReadIndex];
final @PermissionConstants.PermissionStatus int readStatus =
PermissionUtils.toPermissionStatus(this.activity, Manifest.permission.READ_CALENDAR, readGrantResult);
requestResults.put(PermissionConstants.PERMISSION_GROUP_CALENDAR_READ_ONLY, readStatus);

// READ + WRITE -> FULL ACCESS.
if (calendarWriteIndex >= 0) {
final int writeGrantResult = grantResults[calendarWriteIndex];
final @PermissionConstants.PermissionStatus int writeStatus =
PermissionUtils.toPermissionStatus(this.activity, Manifest.permission.WRITE_CALENDAR, writeGrantResult);
final @PermissionConstants.PermissionStatus int fullAccessStatus = strictestStatus(readStatus, writeStatus);
requestResults.put(PermissionConstants.PERMISSION_GROUP_CALENDAR_FULL_ACCESS, fullAccessStatus);
// Support deprecated CALENDAR permission.
requestResults.put(PermissionConstants.PERMISSION_GROUP_CALENDAR, fullAccessStatus);
}
}

for (int i = 0; i < permissions.length; i++) {
final String permissionName = permissions[i];

// READ_CALENDAR and WRITE_CALENDAR permission results have already been handled.
if (permissionName.equals(Manifest.permission.READ_CALENDAR) || permissionName.equals(Manifest.permission.WRITE_CALENDAR)) {
continue;
}

@PermissionConstants.PermissionGroup final int permission =
PermissionUtils.parseManifestName(permissionName);

Expand Down Expand Up @@ -378,6 +408,17 @@ void requestPermissions(
launchSpecialPermission(
Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM,
PermissionConstants.PERMISSION_CODE_SCHEDULE_EXACT_ALARM);
} else if (permission == PermissionConstants.PERMISSION_GROUP_CALENDAR_FULL_ACCESS || permission == PermissionConstants.PERMISSION_GROUP_CALENDAR) {
// Deny CALENDAR_FULL_ACCESS permission if manifest is not listing both read- and write permissions.
// Otherwise, we will only ask for READ permission and think full access is granted.
final boolean isValidManifest = isValidManifestForCalendarFullAccess();
if (isValidManifest) {
permissionsToRequest.add(Manifest.permission.READ_CALENDAR);
permissionsToRequest.add(Manifest.permission.WRITE_CALENDAR);
pendingRequestCount += 2;
} else {
requestResults.put(permission, PermissionConstants.PERMISSION_STATUS_DENIED);
}
} else {
permissionsToRequest.addAll(names);
pendingRequestCount += names.size();
Expand Down Expand Up @@ -418,6 +459,13 @@ private int determinePermissionStatus(final @PermissionConstants.PermissionGroup
}
}

// Inspect the manifest for CALENDAR_FULL_ACCESS, as a misconfigured manifest will give a false positive if READ access has been provided.
if (permission == PermissionConstants.PERMISSION_GROUP_CALENDAR_FULL_ACCESS || permission == PermissionConstants.PERMISSION_GROUP_CALENDAR) {
boolean isValidManifest = isValidManifestForCalendarFullAccess();
if (!isValidManifest)
return PermissionConstants.PERMISSION_STATUS_DENIED;
}

final List<String> names = PermissionUtils.getManifestNames(context, permission);

if (names == null) {
Expand Down Expand Up @@ -451,24 +499,19 @@ private int determinePermissionStatus(final @PermissionConstants.PermissionGroup
: PermissionConstants.PERMISSION_STATUS_DENIED;
}

final boolean targetsMOrHigher = context.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.M;
final boolean requiresExplicitPermission = context.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.M;

if (requiresExplicitPermission) {
final Set<@PermissionConstants.PermissionStatus Integer> permissionStatuses = new HashSet<>();

Set<@PermissionConstants.PermissionStatus Integer> permissionStatuses = new HashSet<>();
for (String name : names) {
// Only handle them if the client app actually targets a API level greater than M.
if (targetsMOrHigher) {
for (String name : names) {
if (permission == PermissionConstants.PERMISSION_GROUP_IGNORE_BATTERY_OPTIMIZATIONS) {
String packageName = context.getPackageName();
PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
// PowerManager.isIgnoringBatteryOptimizations has been included in Android M first.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (pm != null && pm.isIgnoringBatteryOptimizations(packageName)) {
permissionStatuses.add(PermissionConstants.PERMISSION_STATUS_GRANTED);
} else {
permissionStatuses.add(PermissionConstants.PERMISSION_STATUS_DENIED);
}
if (pm != null && pm.isIgnoringBatteryOptimizations(packageName)) {
permissionStatuses.add(PermissionConstants.PERMISSION_STATUS_GRANTED);
} else {
permissionStatuses.add(PermissionConstants.PERMISSION_STATUS_RESTRICTED);
permissionStatuses.add(PermissionConstants.PERMISSION_STATUS_DENIED);
}
} else if (permission == PermissionConstants.PERMISSION_GROUP_MANAGE_EXTERNAL_STORAGE) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
Expand All @@ -480,12 +523,10 @@ private int determinePermissionStatus(final @PermissionConstants.PermissionGroup
: PermissionConstants.PERMISSION_STATUS_DENIED;
permissionStatuses.add(status);
} else if (permission == PermissionConstants.PERMISSION_GROUP_SYSTEM_ALERT_WINDOW) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
int status = Settings.canDrawOverlays(context)
? PermissionConstants.PERMISSION_STATUS_GRANTED
: PermissionConstants.PERMISSION_STATUS_DENIED;
permissionStatuses.add(status);
}
int status = Settings.canDrawOverlays(context)
? PermissionConstants.PERMISSION_STATUS_GRANTED
: PermissionConstants.PERMISSION_STATUS_DENIED;
permissionStatuses.add(status);
} else if (permission == PermissionConstants.PERMISSION_GROUP_REQUEST_INSTALL_PACKAGES) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
int status = context.getPackageManager().canRequestPackageInstalls()
Expand All @@ -494,13 +535,11 @@ private int determinePermissionStatus(final @PermissionConstants.PermissionGroup
permissionStatuses.add(status);
}
} else if (permission == PermissionConstants.PERMISSION_GROUP_ACCESS_NOTIFICATION_POLICY) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Application.NOTIFICATION_SERVICE);
int status = notificationManager.isNotificationPolicyAccessGranted()
? PermissionConstants.PERMISSION_STATUS_GRANTED
: PermissionConstants.PERMISSION_STATUS_DENIED;
permissionStatuses.add(status);
}
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Application.NOTIFICATION_SERVICE);
int status = notificationManager.isNotificationPolicyAccessGranted()
? PermissionConstants.PERMISSION_STATUS_GRANTED
: PermissionConstants.PERMISSION_STATUS_DENIED;
permissionStatuses.add(status);
} else if (permission == PermissionConstants.PERMISSION_GROUP_SCHEDULE_EXACT_ALARM) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
Expand All @@ -518,11 +557,11 @@ private int determinePermissionStatus(final @PermissionConstants.PermissionGroup
}
}
}
if (!permissionStatuses.isEmpty()) {
return strictestStatus(permissionStatuses);
}
}

if (!permissionStatuses.isEmpty()) {
return strictestStatus(permissionStatuses);
}
return PermissionConstants.PERMISSION_STATUS_GRANTED;
}

Expand Down Expand Up @@ -609,4 +648,23 @@ private int checkBluetoothPermissionStatus() {
}
return PermissionConstants.PERMISSION_STATUS_GRANTED;
}

/**
* Checks if the manifest contains both {@link Manifest.permission#READ_CALENDAR} and
* {@link Manifest.permission#WRITE_CALENDAR} permission declarations.
*/
private boolean isValidManifestForCalendarFullAccess() {
List<String> names = PermissionUtils.getManifestNames(context, PermissionConstants.PERMISSION_GROUP_CALENDAR_FULL_ACCESS);
final boolean readInManifest = names != null && names.contains(Manifest.permission.READ_CALENDAR);
final boolean writeInManifest = names != null && names.contains(Manifest.permission.WRITE_CALENDAR);
final boolean validManifest = readInManifest && writeInManifest;
if (!validManifest) {
if (!readInManifest)
Log.d(PermissionConstants.LOG_TAG, Manifest.permission.READ_CALENDAR + " missing in manifest");
if (!writeInManifest)
Log.d(PermissionConstants.LOG_TAG, Manifest.permission.WRITE_CALENDAR + " missing in manifest");
return false;
}
return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,12 @@ static List<String> getManifestNames(Context context, @PermissionConstants.Permi
final ArrayList<String> permissionNames = new ArrayList<>();

switch (permission) {
case PermissionConstants.PERMISSION_GROUP_CALENDAR_READ_ONLY:
if (hasPermissionInManifest(context, permissionNames, Manifest.permission.READ_CALENDAR))
permissionNames.add(Manifest.permission.READ_CALENDAR);
break;

case PermissionConstants.PERMISSION_GROUP_CALENDAR_FULL_ACCESS:
case PermissionConstants.PERMISSION_GROUP_CALENDAR:
if (hasPermissionInManifest(context, permissionNames, Manifest.permission.READ_CALENDAR))
permissionNames.add(Manifest.permission.READ_CALENDAR);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@
xmlns:tools="http://schemas.android.com/tools"
package="com.baseflow.permissionhandler.example">

<uses-feature
android:name="android.hardware.telephony"
android:required="false" />
<uses-feature
android:name="android.hardware.camera"
android:required="false" />

<!--
Internet permissions do not affect the `permission_handler` plugin, but are required if your app needs access to
the internet.
Expand Down
4 changes: 2 additions & 2 deletions permission_handler_android/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: permission_handler_android
description: Permission plugin for Flutter. This plugin provides the Android API to request and check permissions.
homepage: https://github.com/baseflow/flutter-permission-handler
version: 11.0.5
version: 11.1.0

environment:
sdk: ">=2.15.0 <4.0.0"
Expand All @@ -18,7 +18,7 @@ flutter:
dependencies:
flutter:
sdk: flutter
permission_handler_platform_interface: ^3.11.2
permission_handler_platform_interface: ^3.12.0

dev_dependencies:
flutter_lints: ^1.0.4
Expand Down

0 comments on commit 627daa9

Please sign in to comment.