1 /*
2  * Copyright 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.packageinstaller;
18 
19 import android.annotation.NonNull;
20 import android.app.Notification;
21 import android.app.NotificationChannel;
22 import android.app.NotificationManager;
23 import android.app.PendingIntent;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.content.pm.ApplicationInfo;
27 import android.content.pm.PackageItemInfo;
28 import android.content.pm.PackageManager;
29 import android.content.pm.ResolveInfo;
30 import android.content.res.Resources;
31 import android.graphics.drawable.Icon;
32 import android.net.Uri;
33 import android.os.Bundle;
34 import android.provider.Settings;
35 import android.util.Log;
36 
37 /**
38  * A util class that handle and post new app installed notifications.
39  */
40 class PackageInstalledNotificationUtils {
41     private static final String TAG = PackageInstalledNotificationUtils.class.getSimpleName();
42 
43     private static final String NEW_APP_INSTALLED_CHANNEL_ID_PREFIX = "INSTALLER:";
44     private static final String META_DATA_INSTALLER_NOTIFICATION_SMALL_ICON_KEY =
45             "com.android.packageinstaller.notification.smallIcon";
46     private static final String META_DATA_INSTALLER_NOTIFICATION_COLOR_KEY =
47             "com.android.packageinstaller.notification.color";
48 
49     private static final float DEFAULT_MAX_LABEL_SIZE_PX = 500f;
50 
51     private final Context mContext;
52     private final NotificationManager mNotificationManager;
53 
54     private final String mInstallerPackage;
55     private final String mInstallerAppLabel;
56     private final Icon mInstallerAppSmallIcon;
57     private final Integer mInstallerAppColor;
58 
59     private final String mInstalledPackage;
60     private final String mInstalledAppLabel;
61     private final Icon mInstalledAppLargeIcon;
62 
63     private final String mChannelId;
64 
PackageInstalledNotificationUtils(@onNull Context context, @NonNull String installerPackage, @NonNull String installedPackage)65     PackageInstalledNotificationUtils(@NonNull Context context, @NonNull String installerPackage,
66             @NonNull String installedPackage) {
67         mContext = context;
68         mNotificationManager = context.getSystemService(NotificationManager.class);
69         ApplicationInfo installerAppInfo;
70         ApplicationInfo installedAppInfo;
71 
72         try {
73             installerAppInfo = context.getPackageManager().getApplicationInfo(installerPackage,
74                     PackageManager.GET_META_DATA);
75         } catch (PackageManager.NameNotFoundException e) {
76             // Should not happen
77             throw new IllegalStateException("Unable to get application info: " + installerPackage);
78         }
79         try {
80             installedAppInfo = context.getPackageManager().getApplicationInfo(installedPackage,
81                     PackageManager.GET_META_DATA);
82         } catch (PackageManager.NameNotFoundException e) {
83             // Should not happen
84             throw new IllegalStateException("Unable to get application info: " + installedPackage);
85         }
86         mInstallerPackage = installerPackage;
87         mInstallerAppLabel = getAppLabel(context, installerAppInfo, installerPackage);
88         mInstallerAppSmallIcon = getAppNotificationIcon(context, installerAppInfo);
89         mInstallerAppColor = getAppNotificationColor(context, installerAppInfo);
90 
91         mInstalledPackage = installedPackage;
92         mInstalledAppLabel = getAppLabel(context, installedAppInfo, installerPackage);
93         mInstalledAppLargeIcon = getAppLargeIcon(installedAppInfo);
94 
95         mChannelId = NEW_APP_INSTALLED_CHANNEL_ID_PREFIX + installerPackage;
96     }
97 
98     /**
99      * Get app label from app's manifest.
100      *
101      * @param context     A context of the current app
102      * @param appInfo     Application info of targeted app
103      * @param packageName Package name of targeted app
104      * @return The label of targeted application, or package name if label is not found
105      */
getAppLabel(@onNull Context context, @NonNull ApplicationInfo appInfo, @NonNull String packageName)106     private static String getAppLabel(@NonNull Context context, @NonNull ApplicationInfo appInfo,
107             @NonNull String packageName) {
108         CharSequence label = appInfo.loadSafeLabel(context.getPackageManager(),
109                 DEFAULT_MAX_LABEL_SIZE_PX,
110                 PackageItemInfo.SAFE_LABEL_FLAG_TRIM
111                         | PackageItemInfo.SAFE_LABEL_FLAG_FIRST_LINE).toString();
112         if (label != null) {
113             return label.toString();
114         }
115         return packageName;
116     }
117 
118     /**
119      * The app icon from app's manifest.
120      *
121      * @param appInfo Application info of targeted app
122      * @return App icon of targeted app, or Android default app icon if icon is not found
123      */
getAppLargeIcon(@onNull ApplicationInfo appInfo)124     private static Icon getAppLargeIcon(@NonNull ApplicationInfo appInfo) {
125         if (appInfo.icon != 0) {
126             return Icon.createWithResource(appInfo.packageName, appInfo.icon);
127         } else {
128             return Icon.createWithResource("android", android.R.drawable.sym_def_app_icon);
129         }
130     }
131 
132     /**
133      * Get notification icon from installer's manifest meta-data.
134      *
135      * @param context A context of the current app
136      * @param appInfo Installer application info
137      * @return Notification icon that listed in installer's manifest meta-data.
138      * If icon is not found in meta-data, then it returns Android default download icon.
139      */
getAppNotificationIcon(@onNull Context context, @NonNull ApplicationInfo appInfo)140     private static Icon getAppNotificationIcon(@NonNull Context context,
141             @NonNull ApplicationInfo appInfo) {
142         if (appInfo.metaData == null) {
143             return Icon.createWithResource(context, R.drawable.ic_file_download);
144         }
145 
146         int iconResId = appInfo.metaData.getInt(
147                 META_DATA_INSTALLER_NOTIFICATION_SMALL_ICON_KEY, 0);
148         if (iconResId != 0) {
149             return Icon.createWithResource(appInfo.packageName, iconResId);
150         }
151         return Icon.createWithResource(context, R.drawable.ic_file_download);
152     }
153 
154     /**
155      * Get notification color from installer's manifest meta-data.
156      *
157      * @param context A context of the current app
158      * @param appInfo Installer application info
159      * @return Notification color that listed in installer's manifest meta-data, or null if
160      * meta-data is not found.
161      */
getAppNotificationColor(@onNull Context context, @NonNull ApplicationInfo appInfo)162     private static Integer getAppNotificationColor(@NonNull Context context,
163             @NonNull ApplicationInfo appInfo) {
164         if (appInfo.metaData == null) {
165             return null;
166         }
167 
168         int colorResId = appInfo.metaData.getInt(
169                 META_DATA_INSTALLER_NOTIFICATION_COLOR_KEY, 0);
170         if (colorResId != 0) {
171             try {
172                 PackageManager pm = context.getPackageManager();
173                 Resources resources = pm.getResourcesForApplication(appInfo.packageName);
174                 return resources.getColor(colorResId, context.getTheme());
175             } catch (PackageManager.NameNotFoundException e) {
176                 Log.e(TAG, "Error while loading notification color: " + colorResId + " for "
177                         + appInfo.packageName);
178             }
179         }
180         return null;
181     }
182 
getAppDetailIntent(@onNull String packageName)183     private static Intent getAppDetailIntent(@NonNull String packageName) {
184         Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
185         intent.setData(Uri.fromParts("package", packageName, null));
186         return intent;
187     }
188 
resolveIntent(@onNull Context context, @NonNull Intent i)189     private static Intent resolveIntent(@NonNull Context context, @NonNull Intent i) {
190         ResolveInfo result = context.getPackageManager().resolveActivity(i, 0);
191         if (result == null) {
192             return null;
193         }
194         return new Intent(i.getAction()).setClassName(result.activityInfo.packageName,
195                 result.activityInfo.name);
196     }
197 
getAppStoreLink(@onNull Context context, @NonNull String installerPackageName, @NonNull String packageName)198     private static Intent getAppStoreLink(@NonNull Context context,
199             @NonNull String installerPackageName, @NonNull String packageName) {
200         Intent intent = new Intent(Intent.ACTION_SHOW_APP_INFO)
201                 .setPackage(installerPackageName);
202 
203         Intent result = resolveIntent(context, intent);
204         if (result != null) {
205             result.putExtra(Intent.EXTRA_PACKAGE_NAME, packageName);
206             return result;
207         }
208         return null;
209     }
210 
211     /**
212      * Create notification channel for showing apps installed notifications.
213      */
createChannel()214     private void createChannel() {
215         NotificationChannel channel = new NotificationChannel(mChannelId, mInstallerAppLabel,
216                 NotificationManager.IMPORTANCE_DEFAULT);
217         channel.setDescription(
218                 mContext.getString(R.string.app_installed_notification_channel_description));
219         channel.enableVibration(false);
220         channel.setSound(null, null);
221         channel.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE);
222         channel.setBlockableSystem(true);
223 
224         mNotificationManager.createNotificationChannel(channel);
225     }
226 
227     /**
228      * Returns a pending intent when user clicks on apps installed notification.
229      * It should launch the app if possible, otherwise it will return app store's app page.
230      * If app store's app page is not available, it will return Android app details page.
231      */
getInstalledAppLaunchIntent()232     private PendingIntent getInstalledAppLaunchIntent() {
233         Intent intent = mContext.getPackageManager().getLaunchIntentForPackage(mInstalledPackage);
234 
235         // If installed app does not have a launch intent, bring user to app store page
236         if (intent == null) {
237             intent = getAppStoreLink(mContext, mInstallerPackage, mInstalledPackage);
238         }
239 
240         // If app store cannot handle this, bring user to app settings page
241         if (intent == null) {
242             intent = getAppDetailIntent(mInstalledPackage);
243         }
244 
245         intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
246         return PendingIntent.getActivity(mContext,
247                 0 /* request code */, intent, PendingIntent.FLAG_UPDATE_CURRENT);
248     }
249 
250     /**
251      * Returns a pending intent that starts installer's launch intent.
252      * If it doesn't have a launch intent, it will return installer's Android app details page.
253      */
getInstallerEntranceIntent()254     private PendingIntent getInstallerEntranceIntent() {
255         Intent intent = mContext.getPackageManager().getLaunchIntentForPackage(mInstallerPackage);
256 
257         // If installer does not have a launch intent, bring user to app settings page
258         if (intent == null) {
259             intent = getAppDetailIntent(mInstallerPackage);
260         }
261 
262         intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
263         return PendingIntent.getActivity(mContext,
264                 0 /* request code */, intent, PendingIntent.FLAG_UPDATE_CURRENT);
265     }
266 
267     /**
268      * Returns a notification builder for grouped notifications.
269      */
getGroupNotificationBuilder()270     private Notification.Builder getGroupNotificationBuilder() {
271         PendingIntent contentIntent = getInstallerEntranceIntent();
272 
273         Bundle extras = new Bundle();
274         extras.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME, mInstallerAppLabel);
275 
276         Notification.Builder builder =
277                 new Notification.Builder(mContext, mChannelId)
278                         .setSmallIcon(mInstallerAppSmallIcon)
279                         .setGroup(mChannelId)
280                         .setExtras(extras)
281                         .setLocalOnly(true)
282                         .setCategory(Notification.CATEGORY_STATUS)
283                         .setContentIntent(contentIntent)
284                         .setGroupSummary(true);
285 
286         if (mInstallerAppColor != null) {
287             builder.setColor(mInstallerAppColor);
288         }
289         return builder;
290     }
291 
292     /**
293      * Returns notification build for individual installed applications.
294      */
getAppInstalledNotificationBuilder()295     private Notification.Builder getAppInstalledNotificationBuilder() {
296         PendingIntent contentIntent = getInstalledAppLaunchIntent();
297 
298         Bundle extras = new Bundle();
299         extras.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME, mInstallerAppLabel);
300 
301         String tickerText = String.format(
302                 mContext.getString(R.string.notification_installation_success_status),
303                 mInstalledAppLabel);
304 
305         Notification.Builder builder =
306                 new Notification.Builder(mContext, mChannelId)
307                         .setAutoCancel(true)
308                         .setSmallIcon(mInstallerAppSmallIcon)
309                         .setContentTitle(mInstalledAppLabel)
310                         .setContentText(mContext.getString(
311                                 R.string.notification_installation_success_message))
312                         .setContentIntent(contentIntent)
313                         .setTicker(tickerText)
314                         .setCategory(Notification.CATEGORY_STATUS)
315                         .setShowWhen(true)
316                         .setWhen(System.currentTimeMillis())
317                         .setLocalOnly(true)
318                         .setGroup(mChannelId)
319                         .addExtras(extras)
320                         .setStyle(new Notification.BigTextStyle());
321 
322         if (mInstalledAppLargeIcon != null) {
323             builder.setLargeIcon(mInstalledAppLargeIcon);
324         }
325         if (mInstallerAppColor != null) {
326             builder.setColor(mInstallerAppColor);
327         }
328         return builder;
329     }
330 
331     /**
332      * Post new app installed notification.
333      */
postAppInstalledNotification()334     void postAppInstalledNotification() {
335         createChannel();
336 
337         // Post app installed notification
338         Notification.Builder appNotificationBuilder = getAppInstalledNotificationBuilder();
339         mNotificationManager.notify(mInstalledPackage, mInstalledPackage.hashCode(),
340                 appNotificationBuilder.build());
341 
342         // Post installer group notification
343         Notification.Builder groupNotificationBuilder = getGroupNotificationBuilder();
344         mNotificationManager.notify(mInstallerPackage, mInstallerPackage.hashCode(),
345                 groupNotificationBuilder.build());
346     }
347 }
348