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