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.setBlockable(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, 0 /* request code */, intent, 247 PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); 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, 0 /* request code */, intent, 264 PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); 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