Skip to content

Commit

Permalink
Handle new notification permission. (#370)
Browse files Browse the repository at this point in the history
Handle new notification permission.
  • Loading branch information
mvano authored May 11, 2022
1 parent 71abc55 commit a65882a
Show file tree
Hide file tree
Showing 8 changed files with 346 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,24 @@
import androidx.browser.trusted.Token;
import androidx.browser.trusted.TokenStore;
import androidx.browser.trusted.TrustedWebActivityCallbackRemote;
import androidx.browser.trusted.TrustedWebActivityService;

import java.util.ArrayList;
import java.util.List;

/**
* An extension of {@link androidx.browser.trusted.TrustedWebActivityService} that implements
* {@link androidx.browser.trusted.TrustedWebActivityService#getTokenStore()} using a
* An extension of {@link TrustedWebActivityService} that implements
* {@link TrustedWebActivityService#getTokenStore()} using a
* {@link SharedPreferencesTokenStore}.
*/
public class DelegationService extends androidx.browser.trusted.TrustedWebActivityService {
public class DelegationService extends TrustedWebActivityService {
private final List<ExtraCommandHandler> mExtraCommandHandlers = new ArrayList<>();
private SharedPreferencesTokenStore mTokenStore;

public DelegationService() {
registerExtraCommandHandler(new NotificationDelegationExtraCommandHandler());
}

@NonNull
@Override
@SuppressLint("WrongThread")
Expand All @@ -60,7 +65,7 @@ public TokenStore getTokenStore() {
@Nullable
@Override
public Bundle onExtraCommand(
String commandName, Bundle args, @Nullable TrustedWebActivityCallbackRemote callback) {
@NonNull String commandName, @NonNull Bundle args, @Nullable TrustedWebActivityCallbackRemote callback) {
for (ExtraCommandHandler handler : mExtraCommandHandlers) {
Bundle result = handler.handleExtraCommand(this, commandName, args, callback);
if (result.getBoolean(ExtraCommandHandler.EXTRA_COMMAND_SUCCESS)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Copyright 2022 Google Inc. All Rights Reserved.
//
// 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
//
// http://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.androidbrowserhelper.trusted;

import android.app.PendingIntent;
import android.content.Context;
import android.os.Bundle;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.browser.trusted.TrustedWebActivityCallbackRemote;

/**
* Handles extra commands related to notification delegation such as checking and requesting permission.
*/
public class NotificationDelegationExtraCommandHandler implements ExtraCommandHandler {
static final String COMMAND_CHECK_NOTIFICATION_PERMISSION =
"checkNotificationPermission";
private static final String COMMAND_GET_NOTIFICATION_PERMISSION_REQUEST_PENDING_INTENT =
"getNotificationPermissionRequestPendingIntent";
private static final String KEY_NOTIFICATION_CHANNEL_NAME = "notificationChannelName";
private static final String KEY_NOTIFICATION_PERMISSION_REQUEST_PENDING_INTENT =
"notificationPermissionRequestPendingIntent";

@NonNull
@Override
public Bundle handleExtraCommand(Context context, @NonNull String commandName, @NonNull Bundle args,
@Nullable TrustedWebActivityCallbackRemote callback) {
Bundle commandResult = new Bundle();
commandResult.putBoolean(EXTRA_COMMAND_SUCCESS, false);
String channelName = args.getString(KEY_NOTIFICATION_CHANNEL_NAME);
switch (commandName) {
case COMMAND_CHECK_NOTIFICATION_PERMISSION:
if (TextUtils.isEmpty(channelName)) break;

boolean enabled = NotificationUtils.areNotificationsEnabled(context, channelName);
@PermissionStatus int status = enabled ? PermissionStatus.ALLOW : PermissionStatus.BLOCK;
if (status == PermissionStatus.BLOCK && !PrefUtils.hasRequestedNotificationPermission(context)) {
status = PermissionStatus.ASK;
}
commandResult.putInt(NotificationPermissionRequestActivity.KEY_PERMISSION_STATUS, status);
commandResult.putBoolean(EXTRA_COMMAND_SUCCESS, true);
break;
case COMMAND_GET_NOTIFICATION_PERMISSION_REQUEST_PENDING_INTENT:
if (TextUtils.isEmpty(channelName)) break;

PendingIntent pendingIntent = NotificationPermissionRequestActivity.createPermissionRequestPendingIntent(
context, channelName);
commandResult.putParcelable(KEY_NOTIFICATION_PERMISSION_REQUEST_PENDING_INTENT, pendingIntent);
commandResult.putBoolean(EXTRA_COMMAND_SUCCESS, true);
break;
}
return commandResult;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// Copyright 2022 Google Inc. All Rights Reserved.
//
// 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
//
// http://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.androidbrowserhelper.trusted;

import android.app.Activity;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException;

import android.util.Log;
import androidx.core.app.ActivityCompat;

/**
* A simple transparent activity for requesting the notification permission. On either approve or disapprove, this will
* send the result via the {@link Messenger} provided with the intent, and then finish.
*/
public class NotificationPermissionRequestActivity extends Activity {
private static final String TAG = "Notifications";

static final String KEY_PERMISSION_STATUS = "permissionStatus";

// TODO: Use Manifest.permission.POST_NOTIFICATIONS when it is released.
private static final String PERMISSION_POST_NOTIFICATIONS = "android.permission.POST_NOTIFICATIONS";

// TODO: Use Build.VERSION_CODES when it is released.
private static final int VERSION_T = 33;

private static final String EXTRA_NOTIFICATION_CHANNEL_NAME = "notificationChannelName";
private static final String EXTRA_MESSENGER = "messenger";

private String mChannelName;
private Messenger mMessenger;

/**
* Creates a {@link PendingIntent} for launching this activity to request the notification permission. It is mutable
* so that a messenger extra can be added for returning the permission request result.
*/
public static PendingIntent createPermissionRequestPendingIntent(Context context, String channelName) {
Intent intent = new Intent(context.getApplicationContext(), NotificationPermissionRequestActivity.class);
intent.putExtra(EXTRA_NOTIFICATION_CHANNEL_NAME, channelName);
// Starting with Build.VERSION_CODES.S it is required to explicitly specify the mutability of PendingIntents.
int flags = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S ? PendingIntent.FLAG_MUTABLE : 0;
return PendingIntent.getActivity(context.getApplicationContext(),0, intent, flags);
}

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

mChannelName = getIntent().getStringExtra(EXTRA_NOTIFICATION_CHANNEL_NAME);
mMessenger = getIntent().getParcelableExtra(EXTRA_MESSENGER);
if (mChannelName == null || mMessenger == null) {
Log.w(TAG, "Finishing because no channel name or messenger for returning the result was provided.");
finish();
return;
}

// When running on T or greater, with the app targeting less than T, creating a channel for the first time will
// trigger the permission dialog.
if (Build.VERSION.SDK_INT >= VERSION_T && getApplicationContext().getApplicationInfo().targetSdkVersion < VERSION_T) {
NotificationUtils.createNotificationChannel(this, mChannelName);
}

ActivityCompat.requestPermissions(this, new String[]{PERMISSION_POST_NOTIFICATIONS}, 0);
}

@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
boolean enabled = false;
for (int i = 0; i < permissions.length; i++) {
if (!permissions[i].equals(PERMISSION_POST_NOTIFICATIONS)) continue;

PrefUtils.setHasRequestedNotificationPermission(this);
enabled = grantResults[i] == PackageManager.PERMISSION_GRANTED;
break;
}

// This method will only receive the notification permission and its grant result when running on and
// targeting >= T. Check whether notifications are actually enabled, perhaps because the system displayed a
// permission dialog after the first notification channel was created and the user approved it.
if (!enabled) {
enabled = NotificationUtils.areNotificationsEnabled(this, mChannelName);
}

sendPermissionMessage(mMessenger, enabled);
finish();
}

/**
* Sends a message to the messenger containing the permission status.
*/
private static void sendPermissionMessage(Messenger messenger, boolean enabled) {
Bundle data = new Bundle();
@PermissionStatus int status = enabled ? PermissionStatus.ALLOW : PermissionStatus.BLOCK;
data.putInt(KEY_PERMISSION_STATUS, status);
Message message = Message.obtain();
message.setData(data);

try {
messenger.send(message);
} catch (RemoteException e) {
e.printStackTrace();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Copyright 2022 Google Inc. All Rights Reserved.
//
// 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
//
// http://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.androidbrowserhelper.trusted;

import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.Context;
import android.os.Build;
import androidx.core.app.NotificationManagerCompat;
import java.util.Locale;

/**
* Helper for interacting with the notification manager and channels.
*/
public class NotificationUtils {
private NotificationUtils() {}

/**
* Returns true if notifications are enabled and either the channel does not exist or it has not been disabled.
*/
public static boolean areNotificationsEnabled(Context context, String channelName) {
if (!NotificationManagerCompat.from(context).areNotificationsEnabled()) return false;

if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return true;

NotificationChannel channel =
NotificationManagerCompat.from(context).getNotificationChannel(channelNameToId(channelName));
return channel == null || channel.getImportance() != NotificationManager.IMPORTANCE_NONE;
}

/**
* Creates a notification channel using the given channel name.
*/
public static void createNotificationChannel(Context context, String channelName) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return;

NotificationChannel channel = new NotificationChannel(channelNameToId(channelName),
channelName, NotificationManager.IMPORTANCE_DEFAULT);
NotificationManagerCompat.from(context).createNotificationChannel(channel);
}

/**
* Generates a notification channel id from a channel name.
* TODO: Remove this when we can use the method defined in AndroidX instead.
*/
private static String channelNameToId(String name) {
return name.toLowerCase(Locale.ROOT).replace(' ', '_') + "_channel_id";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Copyright 2022 Google Inc. All Rights Reserved.
//
// 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
//
// http://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.androidbrowserhelper.trusted;

import androidx.annotation.IntDef;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

/**
* Represents the permission state in TWA service calls.
*/
@IntDef({
PermissionStatus.ALLOW, PermissionStatus.BLOCK, PermissionStatus.ASK
})
@Retention(RetentionPolicy.SOURCE)
public @interface PermissionStatus {
int ALLOW = 0;
int BLOCK = 1;
int ASK = 2;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Copyright 2022 Google Inc. All Rights Reserved.
//
// 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
//
// http://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.androidbrowserhelper.trusted;

import android.content.Context;
import android.content.SharedPreferences;

/**
* Helper for using application level {@link SharedPreferences} in a consistent way, with the same
* file name and using the application context.
*/
public class PrefUtils {
private PrefUtils() {}

private static final String SHARED_PREFERENCES_NAME = "com.google.androidbrowserhelper";
private static final String KEY_HAS_REQUESTED_NOTIFICATION_PERMISSION = "HAS_REQUESTED_NOTIFICATION_PERMISSION";

/**
* Returns the application level {@link SharedPreferences} using the application context.
*/
public static SharedPreferences getAppSharedPreferences(Context context) {
return context.getApplicationContext().getSharedPreferences(SHARED_PREFERENCES_NAME,
Context.MODE_PRIVATE);
}

public static boolean hasRequestedNotificationPermission(Context context) {
return getAppSharedPreferences(context).getBoolean(KEY_HAS_REQUESTED_NOTIFICATION_PERMISSION, false);
}

public static void setHasRequestedNotificationPermission(Context context) {
getAppSharedPreferences(context).edit().putBoolean(KEY_HAS_REQUESTED_NOTIFICATION_PERMISSION, true).apply();
}
}
Loading

0 comments on commit a65882a

Please sign in to comment.