| /* |
| * Copyright 2018 The Android Open Source Project |
| * |
| * 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 androidx.browser.trusted; |
| |
| import android.annotation.SuppressLint; |
| import android.app.Notification; |
| import android.app.NotificationManager; |
| import android.app.Service; |
| import android.content.ComponentName; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.pm.PackageManager; |
| import android.content.pm.ServiceInfo; |
| import android.graphics.BitmapFactory; |
| import android.os.Build; |
| import android.os.Bundle; |
| import android.os.IBinder; |
| import android.os.Parcelable; |
| import android.support.customtabs.trusted.ITrustedWebActivityService; |
| |
| import androidx.annotation.BinderThread; |
| import androidx.annotation.CallSuper; |
| import androidx.annotation.MainThread; |
| import androidx.annotation.NonNull; |
| import androidx.annotation.Nullable; |
| import androidx.annotation.RestrictTo; |
| import androidx.browser.trusted.TrustedWebActivityServiceConnection.ActiveNotificationsArgs; |
| import androidx.browser.trusted.TrustedWebActivityServiceConnection.CancelNotificationArgs; |
| import androidx.browser.trusted.TrustedWebActivityServiceConnection.NotificationsEnabledArgs; |
| import androidx.browser.trusted.TrustedWebActivityServiceConnection.NotifyNotificationArgs; |
| import androidx.browser.trusted.TrustedWebActivityServiceConnection.ResultArgs; |
| import androidx.core.app.NotificationManagerCompat; |
| |
| import java.util.Locale; |
| |
| /** |
| * The TrustedWebActivityService lives in a client app and serves requests from a Trusted Web |
| * Activity provider. At present it only serves requests to do with notifications. |
| * <p> |
| * When the provider receives a notification from a scope that is associated with a Trusted Web |
| * Activity client app, it will attempt to connect to a TrustedWebActivityService and forward calls. |
| * This allows the client app to display the notifications itself, meaning it is attributable to the |
| * client app and is managed by notification permissions of the client app, not the provider. |
| * <p> |
| * TrustedWebActivityService is usable as it is, by adding the following to your AndroidManifest: |
| * |
| * <pre> |
| * {@code |
| * <service |
| * android:name="androidx.browser.trusted.TrustedWebActivityService" |
| * android:enabled="true" |
| * android:exported="true"> |
| * |
| * <meta-data android:name="android.support.customtabs.trusted.SMALL_ICON" |
| * android:resource="@drawable/ic_notification_icon" /> |
| * |
| * <intent-filter> |
| * <action android:name="android.support.customtabs.trusted.TRUSTED_WEB_ACTIVITY_SERVICE"/> |
| * <category android:name="android.intent.category.DEFAULT"/> |
| * </intent-filter> |
| * </service> |
| * } |
| * </pre> |
| * |
| * The SMALL_ICON resource should point to a drawable to be used for the notification's small icon. |
| * <p> |
| * Alternatively for greater customization, TrustedWebActivityService can be extended and |
| * overridden. In this case the manifest entry should be updated to point to the extending class. |
| * <p> |
| * As this is an AIDL Service, calls may come in from different Binder threads, so overriding |
| * implementations need to be thread safe [1]. |
| * <p> |
| * For security, the TrustedWebActivityService will check that whatever connects to it matches the |
| * {@link Token} stored in the {@link TokenStore} returned by {@link #getTokenStore}. |
| * This is because we don't want to allow any app on the users device to connect to this Service |
| * be able to make it display notifications. |
| * |
| * [1]: https://developer.android.com/guide/components/aidl.html |
| */ |
| public abstract class TrustedWebActivityService extends Service { |
| /** An Intent Action used by the provider to find the TrustedWebActivityService or subclass. */ |
| @SuppressLint({ |
| "ActionValue", // This value was being used before being moved into AndroidX. |
| "ServiceName", // This variable is an Action, but Metalava thinks it's a Service. |
| }) |
| public static final String ACTION_TRUSTED_WEB_ACTIVITY_SERVICE = |
| "android.support.customtabs.trusted.TRUSTED_WEB_ACTIVITY_SERVICE"; |
| |
| /** The Android Manifest meta-data name to specify a small icon id to use. */ |
| public static final String META_DATA_NAME_SMALL_ICON = |
| "android.support.customtabs.trusted.SMALL_ICON"; |
| |
| /** |
| * The key to use to store a Bitmap to return from the {@link #onGetSmallIconBitmap()} method. |
| */ |
| public static final String KEY_SMALL_ICON_BITMAP = |
| "android.support.customtabs.trusted.SMALL_ICON_BITMAP"; |
| |
| /** |
| * The key to use to store a boolean in the returns bundle of {@link #onExtraCommand} method, |
| * to indicate whether the command is executed successfully. |
| */ |
| public static final String KEY_SUCCESS = "androidx.browser.trusted.SUCCESS"; |
| |
| /** Used as a return value of {@link #onGetSmallIconId} when the icon is not provided. */ |
| public static final int SMALL_ICON_NOT_SET = -1; |
| |
| private NotificationManager mNotificationManager; |
| |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| int mVerifiedUid = -1; |
| |
| private final ITrustedWebActivityService.Stub mBinder = |
| new ITrustedWebActivityService.Stub() { |
| @Override |
| public Bundle areNotificationsEnabled(Bundle bundle) { |
| checkCaller(); |
| |
| NotificationsEnabledArgs args = NotificationsEnabledArgs.fromBundle(bundle); |
| boolean result = |
| TrustedWebActivityService.this.onAreNotificationsEnabled(args.channelName); |
| |
| return new ResultArgs(result).toBundle(); |
| } |
| |
| @Override |
| public Bundle notifyNotificationWithChannel(Bundle bundle) { |
| checkCaller(); |
| |
| NotifyNotificationArgs args = NotifyNotificationArgs.fromBundle(bundle); |
| |
| boolean success = TrustedWebActivityService.this.onNotifyNotificationWithChannel( |
| args.platformTag, args.platformId, args.notification, args.channelName); |
| |
| return new ResultArgs(success).toBundle(); |
| } |
| |
| @Override |
| public void cancelNotification(Bundle bundle) { |
| checkCaller(); |
| |
| CancelNotificationArgs args = CancelNotificationArgs.fromBundle(bundle); |
| |
| TrustedWebActivityService.this.onCancelNotification(args.platformTag, args.platformId); |
| } |
| |
| @Override |
| public Bundle getActiveNotifications() { |
| checkCaller(); |
| |
| return new ActiveNotificationsArgs( |
| TrustedWebActivityService.this.onGetActiveNotifications()).toBundle(); |
| } |
| |
| @Override |
| public int getSmallIconId() { |
| checkCaller(); |
| |
| return TrustedWebActivityService.this.onGetSmallIconId(); |
| } |
| |
| @Override |
| public Bundle getSmallIconBitmap() { |
| checkCaller(); |
| |
| return TrustedWebActivityService.this.onGetSmallIconBitmap(); |
| } |
| |
| @SuppressWarnings("NullAway") // TODO: b/142938599 |
| @Override |
| public Bundle extraCommand(String commandName, Bundle args, IBinder callback) { |
| checkCaller(); |
| |
| return TrustedWebActivityService.this.onExtraCommand(commandName, args, |
| TrustedWebActivityCallbackRemote.fromBinder(callback)); |
| } |
| |
| private void checkCaller() { |
| if (mVerifiedUid == -1) { |
| String[] packages = getPackageManager().getPackagesForUid(getCallingUid()); |
| |
| if (packages == null) { |
| packages = new String[]{}; |
| } |
| |
| Token verifiedProvider = getTokenStore().load(); |
| PackageManager pm = getPackageManager(); |
| |
| if (verifiedProvider != null) { |
| for (String packageName : packages) { |
| if (verifiedProvider.matches(packageName, pm)) { |
| mVerifiedUid = getCallingUid(); |
| break; |
| } |
| } |
| } |
| } |
| |
| if (mVerifiedUid == getCallingUid()) return; |
| |
| throw new SecurityException("Caller is not verified as Trusted Web Activity provider."); |
| } |
| }; |
| |
| /** |
| * Called by the system when the service is first created. Do not call this method directly. |
| * Overrides must call {@code super.onCreate()}. |
| */ |
| @Override |
| @CallSuper |
| @MainThread |
| public void onCreate() { |
| super.onCreate(); |
| mNotificationManager = |
| (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); |
| } |
| |
| /** |
| * Checks whether notifications are enabled. |
| * @param channelName The name of the notification channel to be used on Android O+. |
| * @return Whether notifications are enabled. |
| */ |
| @BinderThread |
| public boolean onAreNotificationsEnabled(@NonNull String channelName) { |
| ensureOnCreateCalled(); |
| |
| if (!NotificationManagerCompat.from(this).areNotificationsEnabled()) return false; |
| |
| if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return true; |
| |
| return NotificationApiHelperForO.isChannelEnabled(mNotificationManager, |
| channelNameToId(channelName)); |
| } |
| |
| /** |
| * Displays a notification. |
| * @param platformTag The notification tag, see |
| * {@link NotificationManager#notify(String, int, Notification)}. |
| * @param platformId The notification id, see |
| * {@link NotificationManager#notify(String, int, Notification)}. |
| * @param notification The notification to be displayed, constructed by the provider. |
| * @param channelName The name of the notification channel that the notification should be |
| * displayed on. This method gets or creates a channel from the name and |
| * modifies the notification to use that channel. |
| * @return Whether the notification was successfully displayed (the channel/app may be blocked |
| * by the user). |
| */ |
| @BinderThread |
| public boolean onNotifyNotificationWithChannel(@NonNull String platformTag, int platformId, |
| @NonNull Notification notification, @NonNull String channelName) { |
| ensureOnCreateCalled(); |
| |
| if (!NotificationManagerCompat.from(this).areNotificationsEnabled()) return false; |
| |
| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { |
| String channelId = channelNameToId(channelName); |
| notification = NotificationApiHelperForO.copyNotificationOntoChannel(this, |
| mNotificationManager, notification, channelId, channelName); |
| |
| if (!NotificationApiHelperForO.isChannelEnabled(mNotificationManager, channelId)) { |
| return false; |
| } |
| } |
| |
| mNotificationManager.notify(platformTag, platformId, notification); |
| return true; |
| } |
| |
| /** |
| * Cancels a notification. |
| * @param platformTag The notification tag, see |
| * {@link NotificationManager#cancel(String, int)}. |
| * @param platformId The notification id, see |
| * {@link NotificationManager#cancel(String, int)}. |
| */ |
| @BinderThread |
| public void onCancelNotification(@NonNull String platformTag, int platformId) { |
| ensureOnCreateCalled(); |
| mNotificationManager.cancel(platformTag, platformId); |
| } |
| |
| /** |
| * Returns a list of active notifications, essentially calling |
| * NotificationManager#getActiveNotifications. The default implementation does not work on |
| * pre-Android M. |
| * @return An array of StatusBarNotifications as Parcelables. |
| * |
| * @hide |
| */ |
| @NonNull |
| @BinderThread |
| @RestrictTo(RestrictTo.Scope.LIBRARY) |
| public Parcelable[] onGetActiveNotifications() { |
| ensureOnCreateCalled(); |
| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { |
| return NotificationApiHelperForM.getActiveNotifications(mNotificationManager); |
| } |
| throw new IllegalStateException("onGetActiveNotifications cannot be called pre-M."); |
| } |
| |
| /** |
| * Returns a Bundle containing a bitmap to be use as the small icon for any notifications. |
| * @return A Bundle that may contain a Bitmap contained with key {@link #KEY_SMALL_ICON_BITMAP}. |
| * The bundle may be empty if the client app does not provide a small icon. |
| */ |
| @BinderThread |
| public @NonNull Bundle onGetSmallIconBitmap() { |
| int id = onGetSmallIconId(); |
| Bundle bundle = new Bundle(); |
| if (id == SMALL_ICON_NOT_SET) { |
| return bundle; |
| } |
| bundle.putParcelable(KEY_SMALL_ICON_BITMAP, |
| BitmapFactory.decodeResource(getResources(), id)); |
| return bundle; |
| } |
| |
| /** |
| * Returns the Android resource id of a drawable to be used for the small icon of the |
| * notification. This is called by the provider as it is constructing the notification so a |
| * complete notification can be passed to the client. |
| * |
| * Default behaviour looks for meta-data with the name {@link #META_DATA_NAME_SMALL_ICON} in |
| * service section of the manifest. |
| * @return A resource id for the small icon, or {@link #SMALL_ICON_NOT_SET} if not found. |
| */ |
| @BinderThread |
| public int onGetSmallIconId() { |
| try { |
| ServiceInfo info = getPackageManager().getServiceInfo( |
| new ComponentName(this, getClass()), PackageManager.GET_META_DATA); |
| |
| if (info.metaData == null) return SMALL_ICON_NOT_SET; |
| |
| return info.metaData.getInt(META_DATA_NAME_SMALL_ICON, SMALL_ICON_NOT_SET); |
| } catch (PackageManager.NameNotFoundException e) { |
| // Will only happen if the package provided (the one we are running in) is not |
| // installed - so should never happen. |
| return SMALL_ICON_NOT_SET; |
| } |
| } |
| |
| @Override |
| @Nullable |
| @MainThread |
| public final IBinder onBind(@Nullable Intent intent) { |
| return mBinder; |
| } |
| |
| @Override |
| @MainThread |
| public final boolean onUnbind(@Nullable Intent intent) { |
| mVerifiedUid = -1; |
| |
| return super.onUnbind(intent); |
| } |
| |
| /** |
| * Returns a {@link TokenStore} that is used to determine whether the connecting package is |
| * allowed to connect to this service. |
| * @return An {@link TokenStore} containing the verified provider. |
| */ |
| @BinderThread |
| @NonNull |
| public abstract TokenStore getTokenStore(); |
| |
| /** |
| * Contains a free form command from the browser. The client and browser will need to agree on |
| * an additional API to use in advanced. This call can be used for testing or experimental |
| * purposes. |
| * |
| * A return value of {@code null} will be used to signify that the client does not know how to |
| * handle the request. |
| * |
| * As optional best practices, {@link #KEY_SUCCESS} could be use to identify |
| * that command was *successfully* handled. For example, when returning a message with result: |
| * <pre><code> |
| * Bundle result = new Bundle(); |
| * result.putString("message", message); |
| * if (success) |
| * result.putBoolean(KEY_SUCCESS, true); |
| * return result; |
| * </code></pre> |
| * On the caller side: |
| * <pre><code> |
| * Bundle result = service.extraCommand(commandName, args); |
| * if (result.getBoolean(service.KEY_SUCCESS)) { |
| * // Command was successfully handled |
| * } |
| * </code></pre> |
| * |
| * @param commandName Name of the command to execute. |
| * @param args Arguments to the command. |
| * @param callbackRemote Contains the callback that passed with the command. |
| * @return The result {@link Bundle} or {@code null}. |
| */ |
| @BinderThread |
| @Nullable |
| public Bundle onExtraCommand(@NonNull String commandName, @NonNull Bundle args, |
| @Nullable TrustedWebActivityCallbackRemote callbackRemote) { |
| return null; |
| } |
| |
| private static String channelNameToId(String name) { |
| return name.toLowerCase(Locale.ROOT).replace(' ', '_') + "_channel_id"; |
| } |
| |
| private void ensureOnCreateCalled() { |
| if (mNotificationManager != null) return; |
| throw new IllegalStateException("TrustedWebActivityService has not been properly " |
| + "initialized. Did onCreate() call super.onCreate()?"); |
| } |
| } |