diff options
6 files changed, 412 insertions, 2 deletions
diff --git a/core/java/android/content/pm/PackageInstaller.java b/core/java/android/content/pm/PackageInstaller.java index 316ace16f0a4..b446c689cd00 100644 --- a/core/java/android/content/pm/PackageInstaller.java +++ b/core/java/android/content/pm/PackageInstaller.java @@ -85,6 +85,19 @@ import java.util.List; * <p> * The ApiDemos project contains examples of using this API: * <code>ApiDemos/src/com/example/android/apis/content/InstallApk*.java</code>. + * <p> + * On Android Q or above, an app installed notification will be posted + * by system after a new app is installed. + * To customize installer's notification icon, you should declare the following in the manifest + * <application> as follows: </p> + * <pre> + * <meta-data android:name="com.android.packageinstaller.notification.smallIcon" + * android:resource="@drawable/installer_notification_icon"/> + * </pre> + * <pre> + * <meta-data android:name="com.android.packageinstaller.notification.color" + * android:resource="@color/installer_notification_color"/> + * </pre> */ public class PackageInstaller { private static final String TAG = "PackageInstaller"; diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml index 616a8d64b5b6..dd9ed74b98bc 100644 --- a/data/etc/privapp-permissions-platform.xml +++ b/data/etc/privapp-permissions-platform.xml @@ -139,6 +139,7 @@ applications that come with the platform <permission name="android.permission.USE_RESERVED_DISK"/> <permission name="android.permission.MANAGE_USERS"/> <permission name="android.permission.UPDATE_APP_OPS_STATS"/> + <permission name="android.permission.SUBSTITUTE_NOTIFICATION_APP_NAME"/> </privapp-permissions> <privapp-permissions package="com.android.permissioncontroller"> diff --git a/packages/PackageInstaller/AndroidManifest.xml b/packages/PackageInstaller/AndroidManifest.xml index 7c8139928476..4801f62bae67 100644 --- a/packages/PackageInstaller/AndroidManifest.xml +++ b/packages/PackageInstaller/AndroidManifest.xml @@ -12,6 +12,7 @@ <uses-permission android:name="android.permission.UPDATE_APP_OPS_STATS" /> <uses-permission android:name="android.permission.MANAGE_APP_OPS_MODES" /> <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" /> + <uses-permission android:name="android.permission.SUBSTITUTE_NOTIFICATION_APP_NAME" /> <uses-permission android:name="com.google.android.permission.INSTALL_WEARABLE_PACKAGES" /> diff --git a/packages/PackageInstaller/res/values/strings.xml b/packages/PackageInstaller/res/values/strings.xml index ba81278fd7da..0f065ab95e2d 100644 --- a/packages/PackageInstaller/res/values/strings.xml +++ b/packages/PackageInstaller/res/values/strings.xml @@ -228,4 +228,14 @@ <!-- Label for the notification channel containing notifications for embedded app operations [CHAR LIMIT=40] --> <string name="wear_app_channel">Installing/uninstalling wear apps</string> + <!-- Description for the app installer notification channel [CHAR LIMIT=40] --> + <string name="app_installed_notification_channel_description">App installed notification</string> + + <!-- Notification message shown in status bar when an application is successfully installed. + [CHAR LIMIT=30] --> + <string name="notification_installation_success_message">Successfully installed</string> + + <!-- Notification shown in status bar when an application is successfully installed. + [CHAR LIMIT=50] --> + <string name="notification_installation_success_status">Successfully installed \u201c<xliff:g id="appname" example="Package Installer">%1$s</xliff:g>\u201d</string> </resources> diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstalledNotificationUtils.java b/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstalledNotificationUtils.java new file mode 100644 index 000000000000..2ebbefaef85b --- /dev/null +++ b/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstalledNotificationUtils.java @@ -0,0 +1,347 @@ +/* + * 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 com.android.packageinstaller; + +import android.annotation.NonNull; +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageItemInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.res.Resources; +import android.graphics.drawable.Icon; +import android.net.Uri; +import android.os.Bundle; +import android.provider.Settings; +import android.util.Log; + +/** + * A util class that handle and post new app installed notifications. + */ +class PackageInstalledNotificationUtils { + private static final String TAG = PackageInstalledNotificationUtils.class.getSimpleName(); + + private static final String NEW_APP_INSTALLED_CHANNEL_ID_PREFIX = "INSTALLER:"; + private static final String META_DATA_INSTALLER_NOTIFICATION_SMALL_ICON_KEY = + "com.android.packageinstaller.notification.smallIcon"; + private static final String META_DATA_INSTALLER_NOTIFICATION_COLOR_KEY = + "com.android.packageinstaller.notification.color"; + + private static final float DEFAULT_MAX_LABEL_SIZE_PX = 500f; + + private final Context mContext; + private final NotificationManager mNotificationManager; + + private final String mInstallerPackage; + private final String mInstallerAppLabel; + private final Icon mInstallerAppSmallIcon; + private final Integer mInstallerAppColor; + + private final String mInstalledPackage; + private final String mInstalledAppLabel; + private final Icon mInstalledAppLargeIcon; + + private final String mChannelId; + + PackageInstalledNotificationUtils(@NonNull Context context, @NonNull String installerPackage, + @NonNull String installedPackage) { + mContext = context; + mNotificationManager = context.getSystemService(NotificationManager.class); + ApplicationInfo installerAppInfo; + ApplicationInfo installedAppInfo; + + try { + installerAppInfo = context.getPackageManager().getApplicationInfo(installerPackage, + PackageManager.GET_META_DATA); + } catch (PackageManager.NameNotFoundException e) { + // Should not happen + throw new IllegalStateException("Unable to get application info: " + installerPackage); + } + try { + installedAppInfo = context.getPackageManager().getApplicationInfo(installedPackage, + PackageManager.GET_META_DATA); + } catch (PackageManager.NameNotFoundException e) { + // Should not happen + throw new IllegalStateException("Unable to get application info: " + installedPackage); + } + mInstallerPackage = installerPackage; + mInstallerAppLabel = getAppLabel(context, installerAppInfo, installerPackage); + mInstallerAppSmallIcon = getAppNotificationIcon(context, installerAppInfo); + mInstallerAppColor = getAppNotificationColor(context, installerAppInfo); + + mInstalledPackage = installedPackage; + mInstalledAppLabel = getAppLabel(context, installedAppInfo, installerPackage); + mInstalledAppLargeIcon = getAppLargeIcon(installedAppInfo); + + mChannelId = NEW_APP_INSTALLED_CHANNEL_ID_PREFIX + installerPackage; + } + + /** + * Get app label from app's manifest. + * + * @param context A context of the current app + * @param appInfo Application info of targeted app + * @param packageName Package name of targeted app + * @return The label of targeted application, or package name if label is not found + */ + private static String getAppLabel(@NonNull Context context, @NonNull ApplicationInfo appInfo, + @NonNull String packageName) { + CharSequence label = appInfo.loadSafeLabel(context.getPackageManager(), + DEFAULT_MAX_LABEL_SIZE_PX, + PackageItemInfo.SAFE_LABEL_FLAG_TRIM + | PackageItemInfo.SAFE_LABEL_FLAG_FIRST_LINE).toString(); + if (label != null) { + return label.toString(); + } + return packageName; + } + + /** + * The app icon from app's manifest. + * + * @param appInfo Application info of targeted app + * @return App icon of targeted app, or Android default app icon if icon is not found + */ + private static Icon getAppLargeIcon(@NonNull ApplicationInfo appInfo) { + if (appInfo.icon != 0) { + return Icon.createWithResource(appInfo.packageName, appInfo.icon); + } else { + return Icon.createWithResource("android", android.R.drawable.sym_def_app_icon); + } + } + + /** + * Get notification icon from installer's manifest meta-data. + * + * @param context A context of the current app + * @param appInfo Installer application info + * @return Notification icon that listed in installer's manifest meta-data. + * If icon is not found in meta-data, then it returns Android default download icon. + */ + private static Icon getAppNotificationIcon(@NonNull Context context, + @NonNull ApplicationInfo appInfo) { + if (appInfo.metaData == null) { + return Icon.createWithResource(context, R.drawable.ic_file_download); + } + + int iconResId = appInfo.metaData.getInt( + META_DATA_INSTALLER_NOTIFICATION_SMALL_ICON_KEY, 0); + if (iconResId != 0) { + return Icon.createWithResource(appInfo.packageName, iconResId); + } + return Icon.createWithResource(context, R.drawable.ic_file_download); + } + + /** + * Get notification color from installer's manifest meta-data. + * + * @param context A context of the current app + * @param appInfo Installer application info + * @return Notification color that listed in installer's manifest meta-data, or null if + * meta-data is not found. + */ + private static Integer getAppNotificationColor(@NonNull Context context, + @NonNull ApplicationInfo appInfo) { + if (appInfo.metaData == null) { + return null; + } + + int colorResId = appInfo.metaData.getInt( + META_DATA_INSTALLER_NOTIFICATION_COLOR_KEY, 0); + if (colorResId != 0) { + try { + PackageManager pm = context.getPackageManager(); + Resources resources = pm.getResourcesForApplication(appInfo.packageName); + return resources.getColor(colorResId, context.getTheme()); + } catch (PackageManager.NameNotFoundException e) { + Log.e(TAG, "Error while loading notification color: " + colorResId + " for " + + appInfo.packageName); + } + } + return null; + } + + private static Intent getAppDetailIntent(@NonNull String packageName) { + Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + intent.setData(Uri.fromParts("package", packageName, null)); + return intent; + } + + private static Intent resolveIntent(@NonNull Context context, @NonNull Intent i) { + ResolveInfo result = context.getPackageManager().resolveActivity(i, 0); + if (result == null) { + return null; + } + return new Intent(i.getAction()).setClassName(result.activityInfo.packageName, + result.activityInfo.name); + } + + private static Intent getAppStoreLink(@NonNull Context context, + @NonNull String installerPackageName, @NonNull String packageName) { + Intent intent = new Intent(Intent.ACTION_SHOW_APP_INFO) + .setPackage(installerPackageName); + + Intent result = resolveIntent(context, intent); + if (result != null) { + result.putExtra(Intent.EXTRA_PACKAGE_NAME, packageName); + return result; + } + return null; + } + + /** + * Create notification channel for showing apps installed notifications. + */ + private void createChannel() { + NotificationChannel channel = new NotificationChannel(mChannelId, mInstallerAppLabel, + NotificationManager.IMPORTANCE_DEFAULT); + channel.setDescription( + mContext.getString(R.string.app_installed_notification_channel_description)); + channel.enableVibration(false); + channel.setSound(null, null); + channel.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE); + channel.setBlockableSystem(true); + + mNotificationManager.createNotificationChannel(channel); + } + + /** + * Returns a pending intent when user clicks on apps installed notification. + * It should launch the app if possible, otherwise it will return app store's app page. + * If app store's app page is not available, it will return Android app details page. + */ + private PendingIntent getInstalledAppLaunchIntent() { + Intent intent = mContext.getPackageManager().getLaunchIntentForPackage(mInstalledPackage); + + // If installed app does not have a launch intent, bring user to app store page + if (intent == null) { + intent = getAppStoreLink(mContext, mInstallerPackage, mInstalledPackage); + } + + // If app store cannot handle this, bring user to app settings page + if (intent == null) { + intent = getAppDetailIntent(mInstalledPackage); + } + + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + return PendingIntent.getActivity(mContext, + 0 /* request code */, intent, PendingIntent.FLAG_UPDATE_CURRENT); + } + + /** + * Returns a pending intent that starts installer's launch intent. + * If it doesn't have a launch intent, it will return installer's Android app details page. + */ + private PendingIntent getInstallerEntranceIntent() { + Intent intent = mContext.getPackageManager().getLaunchIntentForPackage(mInstallerPackage); + + // If installer does not have a launch intent, bring user to app settings page + if (intent == null) { + intent = getAppDetailIntent(mInstallerPackage); + } + + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + return PendingIntent.getActivity(mContext, + 0 /* request code */, intent, PendingIntent.FLAG_UPDATE_CURRENT); + } + + /** + * Returns a notification builder for grouped notifications. + */ + private Notification.Builder getGroupNotificationBuilder() { + PendingIntent contentIntent = getInstallerEntranceIntent(); + + Bundle extras = new Bundle(); + extras.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME, mInstallerAppLabel); + + Notification.Builder builder = + new Notification.Builder(mContext, mChannelId) + .setSmallIcon(mInstallerAppSmallIcon) + .setGroup(mChannelId) + .setExtras(extras) + .setLocalOnly(true) + .setCategory(Notification.CATEGORY_STATUS) + .setContentIntent(contentIntent) + .setGroupSummary(true); + + if (mInstallerAppColor != null) { + builder.setColor(mInstallerAppColor); + } + return builder; + } + + /** + * Returns notification build for individual installed applications. + */ + private Notification.Builder getAppInstalledNotificationBuilder() { + PendingIntent contentIntent = getInstalledAppLaunchIntent(); + + Bundle extras = new Bundle(); + extras.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME, mInstallerAppLabel); + + String tickerText = String.format( + mContext.getString(R.string.notification_installation_success_status), + mInstalledAppLabel); + + Notification.Builder builder = + new Notification.Builder(mContext, mChannelId) + .setAutoCancel(true) + .setSmallIcon(mInstallerAppSmallIcon) + .setContentTitle(mInstalledAppLabel) + .setContentText(mContext.getString( + R.string.notification_installation_success_message)) + .setContentIntent(contentIntent) + .setTicker(tickerText) + .setCategory(Notification.CATEGORY_STATUS) + .setShowWhen(true) + .setWhen(System.currentTimeMillis()) + .setLocalOnly(true) + .setGroup(mChannelId) + .addExtras(extras) + .setStyle(new Notification.BigTextStyle()); + + if (mInstalledAppLargeIcon != null) { + builder.setLargeIcon(mInstalledAppLargeIcon); + } + if (mInstallerAppColor != null) { + builder.setColor(mInstallerAppColor); + } + return builder; + } + + /** + * Post new app installed notification. + */ + void postAppInstalledNotification() { + createChannel(); + + // Post app installed notification + Notification.Builder appNotificationBuilder = getAppInstalledNotificationBuilder(); + mNotificationManager.notify(mInstalledPackage, mInstalledPackage.hashCode(), + appNotificationBuilder.build()); + + // Post installer group notification + Notification.Builder groupNotificationBuilder = getGroupNotificationBuilder(); + mNotificationManager.notify(mInstallerPackage, mInstallerPackage.hashCode(), + groupNotificationBuilder.build()); + } +} diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstalledReceiver.java b/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstalledReceiver.java index 67ac99fb12a6..1eb423e53267 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstalledReceiver.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstalledReceiver.java @@ -19,16 +19,54 @@ package com.android.packageinstaller; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; +import android.net.Uri; +import android.util.Log; /** * Receive new app installed broadcast and notify user new app installed. */ public class PackageInstalledReceiver extends BroadcastReceiver { + private static final String TAG = PackageInstalledReceiver.class.getSimpleName(); - private static final String TAG = "PackageInstalledReceiver"; + private static final boolean DEBUG = false; + private static final boolean APP_INSTALLED_NOTIFICATION_ENABLED = false; @Override public void onReceive(Context context, Intent intent) { - // TODO: Add logic to handle new app installed. + if (!APP_INSTALLED_NOTIFICATION_ENABLED) { + return; + } + + String action = intent.getAction(); + + if (DEBUG) { + Log.i(TAG, "Received action: " + action); + } + + if (Intent.ACTION_PACKAGE_ADDED.equals(action)) { + Uri packageUri = intent.getData(); + if (packageUri == null) { + return; + } + + String packageName = packageUri.getSchemeSpecificPart(); + if (packageName == null) { + Log.e(TAG, "No package name"); + return; + } + + if (intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) { + if (DEBUG) { + Log.i(TAG, "Not new app, skip it: " + packageName); + } + return; + } + + // TODO: Make sure the installer information here is accurate + String installer = + context.getPackageManager().getInstallerPackageName(packageName); + new PackageInstalledNotificationUtils(context, installer, + packageName).postAppInstalledNotification(); + } } } |