1 /* 2 * Copyright 2022 Google LLC 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 package com.google.android.libraries.mobiledatadownload.foreground; 17 18 import android.annotation.SuppressLint; 19 import android.app.NotificationChannel; 20 import android.app.PendingIntent; 21 import android.content.Context; 22 import android.content.Intent; 23 import android.os.Build.VERSION; 24 import android.os.Build.VERSION_CODES; 25 26 import androidx.annotation.RequiresApi; 27 import androidx.core.app.NotificationCompat; 28 import androidx.core.app.NotificationManagerCompat; 29 import androidx.core.content.ContextCompat; 30 31 import com.google.common.base.Preconditions; 32 33 import javax.annotation.Nullable; 34 35 /** Utilities for creating and managing notifications. */ 36 // TODO(b/148401016): Add UI test for NotificationUtil. 37 public final class NotificationUtil { 38 public static final String CANCEL_ACTION_EXTRA = "cancel-action"; 39 public static final String KEY_EXTRA = "key"; 40 public static final String STOP_SERVICE_EXTRA = "stop-service"; 41 NotificationUtil()42 private NotificationUtil() { 43 } 44 45 public static final String NOTIFICATION_CHANNEL_ID = "download-notification-channel-id"; 46 47 /** Create the NotificationBuilder for the Foreground Download Service */ createForegroundServiceNotificationBuilder( Context context)48 public static NotificationCompat.Builder createForegroundServiceNotificationBuilder( 49 Context context) { 50 return getNotificationBuilder(context) 51 .setContentTitle("Downloading") 52 .setSmallIcon(android.R.drawable.stat_notify_sync_noanim); 53 } 54 55 /** Create a Notification.Builder. */ createNotificationBuilder( Context context, int size, String contentTitle, String contentText)56 public static NotificationCompat.Builder createNotificationBuilder( 57 Context context, int size, String contentTitle, String contentText) { 58 return getNotificationBuilder(context) 59 .setContentTitle(contentTitle) 60 .setContentText(contentText) 61 .setSmallIcon(android.R.drawable.stat_sys_download) 62 .setOngoing(true) 63 .setProgress(size, 0, false); 64 } 65 getNotificationBuilder(Context context)66 private static NotificationCompat.Builder getNotificationBuilder(Context context) { 67 return new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID) 68 .setCategory(NotificationCompat.CATEGORY_SERVICE) 69 .setOnlyAlertOnce(true); 70 } 71 72 /** 73 * Create a Notification for a key. 74 * 75 * @param key Key to identify the download this notification is created for. 76 */ cancelNotificationForKey(Context context, String key)77 public static void cancelNotificationForKey(Context context, String key) { 78 NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context); 79 notificationManager.cancel(notificationKeyForKey(key)); 80 } 81 82 /** Create the Cancel Menu Action which will be attach to the download notification. */ 83 // FLAG_IMMUTABLE is only for api >= 23, however framework still recommends to use this: 84 // <internal> 85 @SuppressLint("InlinedApi") createCancelAction( Context context, Class<?> foregroundDownloadServiceClass, String key, NotificationCompat.Builder notification, int notificationKey)86 public static void createCancelAction( 87 Context context, 88 Class<?> foregroundDownloadServiceClass, 89 String key, 90 NotificationCompat.Builder notification, 91 int notificationKey) { 92 SaferIntentUtils intentUtils = new SaferIntentUtils() { 93 }; 94 95 Intent cancelIntent = new Intent(context, foregroundDownloadServiceClass); 96 cancelIntent.setPackage(context.getPackageName()); 97 cancelIntent.putExtra(CANCEL_ACTION_EXTRA, notificationKey); 98 cancelIntent.putExtra(KEY_EXTRA, key); 99 100 // It should be safe since we are using SaferPendingIntent, setting Package and 101 // Component, and 102 // use PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_IMMUTABLE. 103 PendingIntent pendingCancelIntent; 104 if (VERSION.SDK_INT >= VERSION_CODES.O) { 105 pendingCancelIntent = 106 intentUtils.getForegroundService( 107 context, 108 notificationKey, 109 cancelIntent, 110 PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_IMMUTABLE); 111 } else { 112 pendingCancelIntent = 113 intentUtils.getService( 114 context, 115 notificationKey, 116 cancelIntent, 117 PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_IMMUTABLE); 118 } 119 NotificationCompat.Action action = 120 new NotificationCompat.Action.Builder( 121 android.R.drawable.stat_sys_warning, 122 "Cancel", 123 Preconditions.checkNotNull(pendingCancelIntent)) 124 .build(); 125 notification.addAction(action); 126 } 127 128 /** Generate the Notification Key for the Key */ notificationKeyForKey(String key)129 public static int notificationKeyForKey(String key) { 130 // Consider if we could have collision. 131 // Heavier alternative is Hashing.goodFastHash(32).hashUnencodedChars(key).asInt(); 132 return key.hashCode(); 133 } 134 135 /** Send intent to start the DownloadService in foreground. */ startForegroundDownloadService( Context context, Class<?> foregroundDownloadService, String key)136 public static void startForegroundDownloadService( 137 Context context, Class<?> foregroundDownloadService, String key) { 138 Intent intent = new Intent(context, foregroundDownloadService); 139 intent.putExtra(KEY_EXTRA, key); 140 141 // Start ForegroundDownloadService to download in the foreground. 142 ContextCompat.startForegroundService(context, intent); 143 } 144 145 /** Sending the intent to stop the foreground download service */ stopForegroundDownloadService( Context context, Class<?> foregroundDownloadService, String key)146 public static void stopForegroundDownloadService( 147 Context context, Class<?> foregroundDownloadService, String key) { 148 Intent intent = new Intent(context, foregroundDownloadService); 149 intent.putExtra(STOP_SERVICE_EXTRA, true); 150 intent.putExtra(KEY_EXTRA, key); 151 152 // This will send the intent to stop the service. 153 ContextCompat.startForegroundService(context, intent); 154 } 155 156 /** 157 * Return the String message to display in Notification when the download is paused to wait for 158 * network connection. 159 */ getDownloadPausedMessage(Context context)160 public static String getDownloadPausedMessage(Context context) { 161 return "Waiting for network connection"; 162 } 163 164 /** 165 * Return the String message to display in Notification when the download is paused due to a 166 * missing wifi connection. 167 */ getDownloadPausedWifiMessage(Context context)168 public static String getDownloadPausedWifiMessage(Context context) { 169 return "Waiting for WiFi connection"; 170 } 171 172 /** Return the String message to display in Notification when the download is failed. */ getDownloadFailedMessage(Context context)173 public static String getDownloadFailedMessage(Context context) { 174 return "Download failed"; 175 } 176 177 /** Return the String message to display in Notification when the download is success. */ getDownloadSuccessMessage(Context context)178 public static String getDownloadSuccessMessage(Context context) { 179 return "Downloaded"; 180 } 181 182 /** Create the Notification Channel for Downloading. */ createNotificationChannel(Context context)183 public static void createNotificationChannel(Context context) { 184 if (VERSION.SDK_INT >= VERSION_CODES.O) { 185 NotificationChannel notificationChannel = 186 new NotificationChannel( 187 NOTIFICATION_CHANNEL_ID, 188 "Data Download Notification Channel", 189 android.app.NotificationManager.IMPORTANCE_DEFAULT); 190 191 android.app.NotificationManager manager = 192 context.getSystemService(android.app.NotificationManager.class); 193 manager.createNotificationChannel(notificationChannel); 194 } 195 } 196 197 /** Utilities for safely accessing PendingIntent APIs. */ 198 private interface SaferIntentUtils { 199 200 @Nullable 201 @RequiresApi(VERSION_CODES.O) // to match PendingIntent.getForegroundService() getForegroundService( Context context, int requestCode, Intent intent, int flags)202 default PendingIntent getForegroundService( 203 Context context, int requestCode, Intent intent, int flags) { 204 return PendingIntent.getForegroundService(context, requestCode, intent, flags); 205 } 206 207 @Nullable getService(Context context, int requestCode, Intent intent, int flags)208 default PendingIntent getService(Context context, int requestCode, Intent intent, 209 int flags) { 210 return PendingIntent.getService(context, requestCode, intent, flags); 211 } 212 } 213 } 214