Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit c5ea08b2 authored by Ricky Wai's avatar Ricky Wai
Browse files

Show app installed notification from PackageInstaller

Bug: 111214100
Change-Id: If3f82bfac740f4f3cf8312f396943f2ebd447c12
Test: Notifications are showing correctly after new apps installed
parent 068923f3
Loading
Loading
Loading
Loading
+13 −0
Original line number Diff line number Diff line
@@ -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";
+1 −0
Original line number Diff line number Diff line
@@ -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">
+1 −0
Original line number Diff line number Diff line
@@ -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" />

+10 −0
Original line number Diff line number Diff line
@@ -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>
+347 −0
Original line number Diff line number Diff line
/*
 * 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());
    }
}
Loading