summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/java/android/content/pm/PackageInstaller.java13
-rw-r--r--data/etc/privapp-permissions-platform.xml1
-rw-r--r--packages/PackageInstaller/AndroidManifest.xml1
-rw-r--r--packages/PackageInstaller/res/values/strings.xml10
-rw-r--r--packages/PackageInstaller/src/com/android/packageinstaller/PackageInstalledNotificationUtils.java347
-rw-r--r--packages/PackageInstaller/src/com/android/packageinstaller/PackageInstalledReceiver.java42
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
+ * &lt;application> as follows: </p>
+ * <pre>
+ * &lt;meta-data android:name="com.android.packageinstaller.notification.smallIcon"
+ * android:resource="@drawable/installer_notification_icon"/>
+ * </pre>
+ * <pre>
+ * &lt;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();
+ }
}
}