Skip to content

Commit

Permalink
Merge pull request #1651 from microsoft/fix/notification-permission
Browse files Browse the repository at this point in the history
Implement notification permission requesting for Android 13
  • Loading branch information
DmitriyKirakosyan authored Oct 4, 2022
2 parents a3e5483 + db87f6b commit 8fee1ff
Show file tree
Hide file tree
Showing 11 changed files with 620 additions and 35 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### AppCenter

* **[Fix]** Fix ignoring maximum storage size limit in case logs contain large payloads.
* **[Feature]** Add requesting notifications permission for Android 13 (notifications are used to inform about downloading/installing status if an application is in background)

___

Expand Down
9 changes: 7 additions & 2 deletions sdk/appcenter-distribute/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

<!--
Android 11 brings in a lot of changes regarding privacy. By default, list of installed apps is now hidden.
Expand All @@ -24,8 +25,12 @@
<application>
<activity
android:name="com.microsoft.appcenter.distribute.install.ReleaseInstallerActivity"
android:theme="@android:style/Theme.Translucent.NoTitleBar"
android:exported="false" />
android:exported="false"
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
<activity
android:name="com.microsoft.appcenter.distribute.permissions.PermissionRequestActivity"
android:exported="false"
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
<activity
android:name="com.microsoft.appcenter.distribute.DeepLinkActivity"
android:exported="true"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import static com.microsoft.appcenter.distribute.DistributeConstants.PREFERENCE_KEY_UPDATE_TOKEN;
import static com.microsoft.appcenter.distribute.DistributeConstants.SERVICE_NAME;

import android.Manifest;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.AlertDialog;
Expand Down Expand Up @@ -69,6 +70,8 @@
import com.microsoft.appcenter.distribute.ingestion.DistributeIngestion;
import com.microsoft.appcenter.distribute.ingestion.models.DistributionStartSessionLog;
import com.microsoft.appcenter.distribute.ingestion.models.json.DistributionStartSessionLogFactory;
import com.microsoft.appcenter.distribute.permissions.PermissionRequestActivity;
import com.microsoft.appcenter.distribute.permissions.PermissionUtils;
import com.microsoft.appcenter.http.HttpException;
import com.microsoft.appcenter.http.HttpResponse;
import com.microsoft.appcenter.http.HttpUtils;
Expand Down Expand Up @@ -595,7 +598,7 @@ public void accept(Boolean enabled) {
switch (updateAction) {

case UpdateAction.UPDATE:
enqueueDownloadOrShowUnknownSourcesDialog(mReleaseDetails);
enqueueDownloadAndRequestPermissions(mReleaseDetails);
break;

case UpdateAction.POSTPONE:
Expand Down Expand Up @@ -839,7 +842,7 @@ else if (mUnknownSourcesDialog != null) {
* We can start download if the setting is now enabled,
* otherwise restore dialog if activity rotated or was covered.
*/
enqueueDownloadOrShowUnknownSourcesDialog(mReleaseDetails);
enqueueDownloadAndRequestPermissions(mReleaseDetails);
}

/*
Expand Down Expand Up @@ -1433,7 +1436,7 @@ private synchronized void showUpdateDialog() {

@Override
public void onClick(DialogInterface dialog, int which) {
enqueueDownloadOrShowUnknownSourcesDialog(releaseDetails);
enqueueDownloadAndRequestPermissions(releaseDetails);
}
});
dialogBuilder.setCancelable(false);
Expand Down Expand Up @@ -1662,33 +1665,64 @@ private synchronized void postponeRelease(ReleaseDetails releaseDetails) {
*
* @param releaseDetails release details.
*/
synchronized void enqueueDownloadOrShowUnknownSourcesDialog(final ReleaseDetails releaseDetails) {
synchronized void enqueueDownloadAndRequestPermissions(final ReleaseDetails releaseDetails) {

if (releaseDetails == mReleaseDetails) {
if (InstallerUtils.isUnknownSourcesEnabled(mContext)) {
AppCenterLog.debug(LOG_TAG, "Schedule download...");
resumeDownload();
if (!InstallerUtils.isUnknownSourcesEnabled(mContext)) {
showUnknownSourcesDialog();
return;
}
requestPermissionsForDownload();
AppCenterLog.debug(LOG_TAG, "Schedule download...");
resumeDownload();

/* Refresh mandatory dialog progress or do nothing otherwise. */
showDownloadProgress();
/* Refresh mandatory dialog progress or do nothing otherwise. */
showDownloadProgress();

/*
* If we restored a cached dialog, we also started a new check release call.
* We might have time to click on download before the call completes (easy to
* reproduce with network down).
* In that case the download will start and we'll see a new update dialog if we
* don't cancel the call.
*/
if (mCheckReleaseApiCall != null) {
mCheckReleaseApiCall.cancel();
}
} else {
showUnknownSourcesDialog();
/*
* If we restored a cached dialog, we also started a new check release call.
* We might have time to click on download before the call completes (easy to
* reproduce with network down).
* In that case the download will start and we'll see a new update dialog if we
* don't cancel the call.
*/
if (mCheckReleaseApiCall != null) {
mCheckReleaseApiCall.cancel();
}
} else {
showDisabledToast();
}
}

private void requestPermissionsForDownload() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
AppCenterLog.debug(LOG_TAG, "There is no need to request permissions in runtime on Android earlier than 6.0.");
return;
}
if (PermissionUtils.permissionsAreGranted(mContext, Manifest.permission.POST_NOTIFICATIONS)) {
AppCenterLog.debug(LOG_TAG, "Post notification permission already granted.");
return;
}
AppCenterFuture<PermissionRequestActivity.Result> confirmFuture = PermissionUtils.requestPermissions(mContext, Manifest.permission.POST_NOTIFICATIONS);
if (confirmFuture == null) {
AppCenterLog.error(LOG_TAG, "Future to get the result of a permission request is null.");
return;
}
confirmFuture.thenAccept(new AppCenterConsumer<PermissionRequestActivity.Result>() {

@Override
public void accept(PermissionRequestActivity.Result result) {
if (result.exception != null) {
AppCenterLog.warn(LOG_TAG, "Error when trying to request permissions.", result.exception);
} else if (result.areAllPermissionsGranted()) {
AppCenterLog.info(LOG_TAG, "Permissions have been successfully granted.");
} else {
AppCenterLog.info(LOG_TAG, "Permissions were not granted.");
}
}
});
}

/**
* Show disabled toast so that user is not surprised why the dialog action does not work.
* Calling setEnabled(false) before actioning dialog is a corner case
Expand All @@ -1702,7 +1736,7 @@ private void showDisabledToast() {
/**
* Show toast using foreground activity context if possible.
*
* @param messageId the resource id of the string resource to use.
* @param messageId the resource id of the string resource to use.
*/
void showToast(int messageId) {

Expand Down Expand Up @@ -1926,4 +1960,4 @@ public void run() {
}
});
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/

package com.microsoft.appcenter.distribute.permissions;

import static com.microsoft.appcenter.distribute.DistributeConstants.LOG_TAG;

import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;

import com.microsoft.appcenter.utils.AppCenterLog;
import com.microsoft.appcenter.utils.async.AppCenterFuture;
import com.microsoft.appcenter.utils.async.DefaultAppCenterFuture;

import java.util.HashMap;
import java.util.Map;

/**
* Activity for requesting permissions.
*/
public class PermissionRequestActivity extends Activity {

/**
* Result of requesting permissions.
*/
public static class Result {
public final Exception exception;
public final Map<String, Boolean> permissionRequestResults;

public Result(@Nullable Map<String, Boolean> permissionRequestResults, @Nullable Exception exception) {
this.permissionRequestResults = permissionRequestResults;
this.exception = exception;
}

public boolean areAllPermissionsGranted() {
if (permissionRequestResults != null && permissionRequestResults.size() > 0) {
return !permissionRequestResults.containsValue(false);
}
return false;
}
}

@VisibleForTesting
static final String EXTRA_PERMISSIONS = "intent.extra.PERMISSIONS";

@VisibleForTesting
static final int REQUEST_CODE = PermissionRequestActivity.class.getName().hashCode();

/**
* Tracking last request to send result to the listener.
*/
@VisibleForTesting
static DefaultAppCenterFuture<Result> sResultFuture;

/**
* Start activity for requesting permissions.
*
* @param context The context from which the activity will be started.
* @param permissions List of requested permissions.
* @return Future with the result of a permissions request.
*/
public static AppCenterFuture<Result> requestPermissions(@NonNull Context context, String... permissions) {
if (sResultFuture != null) {
AppCenterLog.error(LOG_TAG, "Result future flag is null.");
return null;
}
sResultFuture = new DefaultAppCenterFuture<>();
Intent intent = new Intent(context, PermissionRequestActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
intent.putExtra(EXTRA_PERMISSIONS, permissions);
context.startActivity(intent);
return sResultFuture;
}

@VisibleForTesting
static void complete(@NonNull Result result) {
if (sResultFuture != null) {
sResultFuture.complete(result);
sResultFuture = null;
return;
}
AppCenterLog.debug(LOG_TAG, "The start of the activity was not called using the requestPermissions function or the future has already been completed");
}

@Nullable
private String[] getPermissionsList() {
Intent intent = getIntent();
if (intent == null) {
return null;
}
Bundle extras = intent.getExtras();
if (extras == null) {
return null;
}
return extras.getStringArray(EXTRA_PERMISSIONS);
}

private Map<String, Boolean> getPermissionsRequestResultMap(String[] permissions, int[] results) {
Map<String, Boolean> resultsMap = new HashMap<>();
if (permissions.length != results.length) {
AppCenterLog.error(LOG_TAG, "Invalid argument array sizes.");
return null;
}
for (int i = 0; i < permissions.length; i++) {
resultsMap.put(permissions[i], results[i] == PackageManager.PERMISSION_GRANTED);
}
return resultsMap;
}

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
Exception exception = new UnsupportedOperationException("There is no need to request permissions in runtime on Android earlier than 6.0.");
AppCenterLog.error(LOG_TAG, "Android version incompatible.", exception);
complete(new Result(null, exception));
finish();
return;
}
String[] permissions = getPermissionsList();
if (permissions == null) {
Exception exception = new IllegalArgumentException("Failed to get permissions list from intents extras.");
AppCenterLog.error(LOG_TAG, "Failed to get permissions list.", exception);
complete(new Result(null, exception));
finish();
return;
}
requestPermissions(permissions, REQUEST_CODE);
}

@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
if (requestCode == REQUEST_CODE) {
Map<String, Boolean> results = getPermissionsRequestResultMap(permissions, grantResults);
if (results == null || results.size() == 0) {
complete(new Result(null, new IllegalArgumentException("Error while getting permission request results.")));
return;
}
complete(new Result(results, null));
finish();
}
}

@Override
public void finish() {
super.finish();

/*
* Prevent closing animation because we don't need any animation or visual effect
* from this wrapper activity.
*/
overridePendingTransition(0, 0);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,15 @@
* Licensed under the MIT License.
*/

package com.microsoft.appcenter.distribute;
package com.microsoft.appcenter.distribute.permissions;

import android.content.Context;
import android.content.pm.PackageManager;

import androidx.annotation.NonNull;

import com.microsoft.appcenter.utils.async.AppCenterFuture;

/**
* Permission utils.
*/
Expand All @@ -32,6 +36,18 @@ public static int[] permissionsState(Context context, String... permissions) {
return state;
}

/**
* Checks if the specified permissions' states are equal to {@link PackageManager#PERMISSION_GRANTED}.
*
* @param context context.
* @param permissions an array with specified permissions.
* @return true if granted, false otherwise.
*/
public static boolean permissionsAreGranted(Context context, String... permissions) {
int[] permissionsState = permissionsState(context, permissions);
return permissionsAreGranted(permissionsState);
}

/**
* Checks if the specified permissions' states are equal to {@link PackageManager#PERMISSION_GRANTED}.
*
Expand All @@ -46,5 +62,9 @@ public static boolean permissionsAreGranted(int[] permissionsState) {
}
return true;
}

public static AppCenterFuture<PermissionRequestActivity.Result> requestPermissions(@NonNull Context context, String... permissions) {
return PermissionRequestActivity.requestPermissions(context, permissions);
}
}

Loading

0 comments on commit 8fee1ff

Please sign in to comment.