1 /* 2 * Copyright (C) 2016 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.storagemanager.automatic; 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.BroadcastReceiver; 24 import android.content.Context; 25 import android.content.Intent; 26 import android.content.SharedPreferences; 27 import android.content.res.Resources; 28 import android.os.SystemProperties; 29 import android.provider.Settings; 30 import androidx.annotation.VisibleForTesting; 31 import androidx.core.os.BuildCompat; 32 33 import com.android.storagemanager.R; 34 35 import java.util.concurrent.TimeUnit; 36 37 /** 38 * NotificationController handles the responses to the Automatic Storage Management low storage 39 * notification. 40 */ 41 public class NotificationController extends BroadcastReceiver { 42 /** 43 * Intent action for if the user taps "Turn on" for the automatic storage manager. 44 */ 45 public static final String INTENT_ACTION_ACTIVATE_ASM = 46 "com.android.storagemanager.automatic.ACTIVATE"; 47 48 /** 49 * Intent action for if the user swipes the notification away. 50 */ 51 public static final String INTENT_ACTION_DISMISS = 52 "com.android.storagemanager.automatic.DISMISS"; 53 54 /** 55 * Intent action for if the user explicitly hits "No thanks" on the notification. 56 */ 57 public static final String INTENT_ACTION_NO_THANKS = 58 "com.android.storagemanager.automatic.NO_THANKS"; 59 60 /** 61 * Intent action to maybe show the ASM upsell notification. 62 */ 63 public static final String INTENT_ACTION_SHOW_NOTIFICATION = 64 "com.android.storagemanager.automatic.show_notification"; 65 66 /** 67 * Intent action for forcefully showing the notification, even if the conditions are not valid. 68 */ 69 private static final String INTENT_ACTION_DEBUG_NOTIFICATION = 70 "com.android.storagemanager.automatic.DEBUG_SHOW_NOTIFICATION"; 71 72 /** Intent action for if the user taps on the notification. */ 73 @VisibleForTesting 74 static final String INTENT_ACTION_TAP = "com.android.storagemanager.automatic.SHOW_SETTINGS"; 75 76 /** 77 * Intent extra for the notification id. 78 */ 79 public static final String INTENT_EXTRA_ID = "id"; 80 81 private static final String SHARED_PREFERENCES_NAME = "NotificationController"; 82 private static final String NOTIFICATION_NEXT_SHOW_TIME = "notification_next_show_time"; 83 private static final String NOTIFICATION_SHOWN_COUNT = "notification_shown_count"; 84 private static final String NOTIFICATION_DISMISS_COUNT = "notification_dismiss_count"; 85 private static final String STORAGE_MANAGER_PROPERTY = "ro.storage_manager.enabled"; 86 private static final String CHANNEL_ID = "storage"; 87 88 private static final long DISMISS_DELAY = TimeUnit.DAYS.toMillis(14); 89 private static final long NO_THANKS_DELAY = TimeUnit.DAYS.toMillis(90); 90 private static final long MAXIMUM_SHOWN_COUNT = 4; 91 private static final long MAXIMUM_DISMISS_COUNT = 9; 92 private static final int NOTIFICATION_ID = 0; 93 94 // Keeps the time for test purposes. 95 private Clock mClock; 96 97 @Override onReceive(Context context, Intent intent)98 public void onReceive(Context context, Intent intent) { 99 switch (intent.getAction()) { 100 case INTENT_ACTION_ACTIVATE_ASM: 101 Settings.Secure.putInt( 102 context.getContentResolver(), 103 Settings.Secure.AUTOMATIC_STORAGE_MANAGER_ENABLED, 104 1); 105 // Provide a warning if storage manager is not defaulted on. 106 if (!SystemProperties.getBoolean(STORAGE_MANAGER_PROPERTY, false)) { 107 Intent warningIntent = new Intent(context, WarningDialogActivity.class); 108 warningIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 109 context.startActivity(warningIntent); 110 } 111 break; 112 case INTENT_ACTION_NO_THANKS: 113 delayNextNotification(context, NO_THANKS_DELAY); 114 break; 115 case INTENT_ACTION_DISMISS: 116 delayNextNotification(context, DISMISS_DELAY); 117 break; 118 case INTENT_ACTION_SHOW_NOTIFICATION: 119 maybeShowNotification(context); 120 return; 121 case INTENT_ACTION_DEBUG_NOTIFICATION: 122 showNotification(context); 123 return; 124 case INTENT_ACTION_TAP: 125 Intent storageIntent = new Intent(Settings.ACTION_INTERNAL_STORAGE_SETTINGS); 126 storageIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 127 context.startActivity(storageIntent); 128 break; 129 } 130 cancelNotification(context, intent); 131 } 132 133 /** 134 * Sets a time provider for the controller. 135 * @param clock The time provider. 136 */ setClock(Clock clock)137 protected void setClock(Clock clock) { 138 mClock = clock; 139 } 140 141 /** 142 * If the conditions for showing the activation notification are met, show the activation 143 * notification. 144 * @param context Context to use for getting resources and to display the notification. 145 */ maybeShowNotification(Context context)146 private void maybeShowNotification(Context context) { 147 if (shouldShowNotification(context)) { 148 showNotification(context); 149 } 150 } 151 shouldShowNotification(Context context)152 private boolean shouldShowNotification(Context context) { 153 boolean showNotificationConfigEnabled = 154 context.getResources().getBoolean(R.bool.enable_low_storage_notification); 155 if (!showNotificationConfigEnabled) { 156 return false; 157 } 158 159 SharedPreferences sp = context.getSharedPreferences( 160 SHARED_PREFERENCES_NAME, 161 Context.MODE_PRIVATE); 162 int timesShown = sp.getInt(NOTIFICATION_SHOWN_COUNT, 0); 163 int timesDismissed = sp.getInt(NOTIFICATION_DISMISS_COUNT, 0); 164 if (timesShown >= MAXIMUM_SHOWN_COUNT || timesDismissed >= MAXIMUM_DISMISS_COUNT) { 165 return false; 166 } 167 168 long nextTimeToShow = sp.getLong(NOTIFICATION_NEXT_SHOW_TIME, 0); 169 170 return getCurrentTime() >= nextTimeToShow; 171 } 172 showNotification(Context context)173 private void showNotification(Context context) { 174 Resources res = context.getResources(); 175 Intent noThanksIntent = getBaseIntent(context, INTENT_ACTION_NO_THANKS); 176 noThanksIntent.putExtra(INTENT_EXTRA_ID, NOTIFICATION_ID); 177 Notification.Action.Builder cancelAction = new Notification.Action.Builder(null, 178 res.getString(R.string.automatic_storage_manager_cancel_button), 179 PendingIntent.getBroadcast(context, 0, noThanksIntent, 180 PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE)); 181 182 183 Intent activateIntent = getBaseIntent(context, INTENT_ACTION_ACTIVATE_ASM); 184 activateIntent.putExtra(INTENT_EXTRA_ID, NOTIFICATION_ID); 185 Notification.Action.Builder activateAutomaticAction = new Notification.Action.Builder(null, 186 res.getString(R.string.automatic_storage_manager_activate_button), 187 PendingIntent.getBroadcast(context, 0, activateIntent, 188 PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE)); 189 190 Intent dismissIntent = getBaseIntent(context, INTENT_ACTION_DISMISS); 191 dismissIntent.putExtra(INTENT_EXTRA_ID, NOTIFICATION_ID); 192 PendingIntent deleteIntent = PendingIntent.getBroadcast(context, 0, 193 dismissIntent, 194 PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_IMMUTABLE); 195 196 Intent contentIntent = getBaseIntent(context, INTENT_ACTION_TAP); 197 contentIntent.putExtra(INTENT_EXTRA_ID, NOTIFICATION_ID); 198 PendingIntent tapIntent = PendingIntent.getBroadcast(context, 0, contentIntent, 199 PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_IMMUTABLE); 200 201 Notification.Builder builder; 202 // We really should only have the path with the notification channel set. The other path is 203 // only for legacy Robolectric reasons -- Robolectric does not have the Notification 204 // builder with a channel id, so it crashes when it hits that code path. 205 if (BuildCompat.isAtLeastO()) { 206 makeNotificationChannel(context); 207 builder = new Notification.Builder(context, CHANNEL_ID); 208 } else { 209 builder = new Notification.Builder(context); 210 } 211 212 builder.setSmallIcon(R.drawable.ic_settings_24dp) 213 .setContentTitle( 214 res.getString(R.string.automatic_storage_manager_notification_title)) 215 .setContentText( 216 res.getString(R.string.automatic_storage_manager_notification_summary)) 217 .setStyle( 218 new Notification.BigTextStyle() 219 .bigText( 220 res.getString( 221 R.string 222 .automatic_storage_manager_notification_summary))) 223 .addAction(cancelAction.build()) 224 .addAction(activateAutomaticAction.build()) 225 .setContentIntent(tapIntent) 226 .setDeleteIntent(deleteIntent) 227 .setLocalOnly(true); 228 229 NotificationManager manager = 230 ((NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE)); 231 manager.notify(NOTIFICATION_ID, builder.build()); 232 } 233 makeNotificationChannel(Context context)234 private void makeNotificationChannel(Context context) { 235 final NotificationManager nm = context.getSystemService(NotificationManager.class); 236 final NotificationChannel channel = 237 new NotificationChannel( 238 CHANNEL_ID, 239 context.getString(R.string.app_name), 240 NotificationManager.IMPORTANCE_LOW); 241 nm.createNotificationChannel(channel); 242 } 243 cancelNotification(Context context, Intent intent)244 private void cancelNotification(Context context, Intent intent) { 245 if (intent.getAction() == INTENT_ACTION_DISMISS) { 246 incrementNotificationDismissedCount(context); 247 } else { 248 incrementNotificationShownCount(context); 249 } 250 251 int id = intent.getIntExtra(INTENT_EXTRA_ID, -1); 252 if (id == -1) { 253 return; 254 } 255 NotificationManager manager = (NotificationManager) context 256 .getSystemService(Context.NOTIFICATION_SERVICE); 257 manager.cancel(id); 258 } 259 incrementNotificationShownCount(Context context)260 private void incrementNotificationShownCount(Context context) { 261 SharedPreferences sp = context.getSharedPreferences(SHARED_PREFERENCES_NAME, 262 Context.MODE_PRIVATE); 263 SharedPreferences.Editor editor = sp.edit(); 264 int shownCount = sp.getInt(NotificationController.NOTIFICATION_SHOWN_COUNT, 0) + 1; 265 editor.putInt(NotificationController.NOTIFICATION_SHOWN_COUNT, shownCount); 266 editor.apply(); 267 } 268 incrementNotificationDismissedCount(Context context)269 private void incrementNotificationDismissedCount(Context context) { 270 SharedPreferences sp = context.getSharedPreferences(SHARED_PREFERENCES_NAME, 271 Context.MODE_PRIVATE); 272 SharedPreferences.Editor editor = sp.edit(); 273 int dismissCount = sp.getInt(NOTIFICATION_DISMISS_COUNT, 0) + 1; 274 editor.putInt(NOTIFICATION_DISMISS_COUNT, dismissCount); 275 editor.apply(); 276 } 277 delayNextNotification(Context context, long timeInMillis)278 private void delayNextNotification(Context context, long timeInMillis) { 279 SharedPreferences sp = context.getSharedPreferences(SHARED_PREFERENCES_NAME, 280 Context.MODE_PRIVATE); 281 SharedPreferences.Editor editor = sp.edit(); 282 editor.putLong(NOTIFICATION_NEXT_SHOW_TIME, 283 getCurrentTime() + timeInMillis); 284 editor.apply(); 285 } 286 getCurrentTime()287 private long getCurrentTime() { 288 if (mClock == null) { 289 mClock = new Clock(); 290 } 291 292 return mClock.currentTimeMillis(); 293 } 294 295 @VisibleForTesting getBaseIntent(Context context, String action)296 Intent getBaseIntent(Context context, String action) { 297 return new Intent(context, NotificationController.class).setAction(action); 298 } 299 300 /** 301 * Clock provides the current time. 302 */ 303 protected static class Clock { 304 /** 305 * Returns the current time in milliseconds. 306 */ currentTimeMillis()307 public long currentTimeMillis() { 308 return System.currentTimeMillis(); 309 } 310 } 311 } 312