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