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 SharedPreferences sp = context.getSharedPreferences( 154 SHARED_PREFERENCES_NAME, 155 Context.MODE_PRIVATE); 156 int timesShown = sp.getInt(NOTIFICATION_SHOWN_COUNT, 0); 157 int timesDismissed = sp.getInt(NOTIFICATION_DISMISS_COUNT, 0); 158 if (timesShown >= MAXIMUM_SHOWN_COUNT || timesDismissed >= MAXIMUM_DISMISS_COUNT) { 159 return false; 160 } 161 162 long nextTimeToShow = sp.getLong(NOTIFICATION_NEXT_SHOW_TIME, 0); 163 164 return getCurrentTime() >= nextTimeToShow; 165 } 166 showNotification(Context context)167 private void showNotification(Context context) { 168 Resources res = context.getResources(); 169 Intent noThanksIntent = getBaseIntent(context, INTENT_ACTION_NO_THANKS); 170 noThanksIntent.putExtra(INTENT_EXTRA_ID, NOTIFICATION_ID); 171 Notification.Action.Builder cancelAction = new Notification.Action.Builder(null, 172 res.getString(R.string.automatic_storage_manager_cancel_button), 173 PendingIntent.getBroadcast(context, 0, noThanksIntent, 174 PendingIntent.FLAG_UPDATE_CURRENT)); 175 176 177 Intent activateIntent = getBaseIntent(context, INTENT_ACTION_ACTIVATE_ASM); 178 activateIntent.putExtra(INTENT_EXTRA_ID, NOTIFICATION_ID); 179 Notification.Action.Builder activateAutomaticAction = new Notification.Action.Builder(null, 180 res.getString(R.string.automatic_storage_manager_activate_button), 181 PendingIntent.getBroadcast(context, 0, activateIntent, 182 PendingIntent.FLAG_UPDATE_CURRENT)); 183 184 Intent dismissIntent = getBaseIntent(context, INTENT_ACTION_DISMISS); 185 dismissIntent.putExtra(INTENT_EXTRA_ID, NOTIFICATION_ID); 186 PendingIntent deleteIntent = PendingIntent.getBroadcast(context, 0, 187 dismissIntent, 188 PendingIntent.FLAG_ONE_SHOT); 189 190 Intent contentIntent = getBaseIntent(context, INTENT_ACTION_TAP); 191 contentIntent.putExtra(INTENT_EXTRA_ID, NOTIFICATION_ID); 192 PendingIntent tapIntent = PendingIntent.getBroadcast(context, 0, contentIntent, 193 PendingIntent.FLAG_ONE_SHOT); 194 195 Notification.Builder builder; 196 // We really should only have the path with the notification channel set. The other path is 197 // only for legacy Robolectric reasons -- Robolectric does not have the Notification 198 // builder with a channel id, so it crashes when it hits that code path. 199 if (BuildCompat.isAtLeastO()) { 200 makeNotificationChannel(context); 201 builder = new Notification.Builder(context, CHANNEL_ID); 202 } else { 203 builder = new Notification.Builder(context); 204 } 205 206 builder.setSmallIcon(R.drawable.ic_settings_24dp) 207 .setContentTitle( 208 res.getString(R.string.automatic_storage_manager_notification_title)) 209 .setContentText( 210 res.getString(R.string.automatic_storage_manager_notification_summary)) 211 .setStyle( 212 new Notification.BigTextStyle() 213 .bigText( 214 res.getString( 215 R.string 216 .automatic_storage_manager_notification_summary))) 217 .addAction(cancelAction.build()) 218 .addAction(activateAutomaticAction.build()) 219 .setContentIntent(tapIntent) 220 .setDeleteIntent(deleteIntent) 221 .setLocalOnly(true); 222 223 NotificationManager manager = 224 ((NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE)); 225 manager.notify(NOTIFICATION_ID, builder.build()); 226 } 227 makeNotificationChannel(Context context)228 private void makeNotificationChannel(Context context) { 229 final NotificationManager nm = context.getSystemService(NotificationManager.class); 230 final NotificationChannel channel = 231 new NotificationChannel( 232 CHANNEL_ID, 233 context.getString(R.string.app_name), 234 NotificationManager.IMPORTANCE_LOW); 235 nm.createNotificationChannel(channel); 236 } 237 cancelNotification(Context context, Intent intent)238 private void cancelNotification(Context context, Intent intent) { 239 if (intent.getAction() == INTENT_ACTION_DISMISS) { 240 incrementNotificationDismissedCount(context); 241 } else { 242 incrementNotificationShownCount(context); 243 } 244 245 int id = intent.getIntExtra(INTENT_EXTRA_ID, -1); 246 if (id == -1) { 247 return; 248 } 249 NotificationManager manager = (NotificationManager) context 250 .getSystemService(Context.NOTIFICATION_SERVICE); 251 manager.cancel(id); 252 } 253 incrementNotificationShownCount(Context context)254 private void incrementNotificationShownCount(Context context) { 255 SharedPreferences sp = context.getSharedPreferences(SHARED_PREFERENCES_NAME, 256 Context.MODE_PRIVATE); 257 SharedPreferences.Editor editor = sp.edit(); 258 int shownCount = sp.getInt(NotificationController.NOTIFICATION_SHOWN_COUNT, 0) + 1; 259 editor.putInt(NotificationController.NOTIFICATION_SHOWN_COUNT, shownCount); 260 editor.apply(); 261 } 262 incrementNotificationDismissedCount(Context context)263 private void incrementNotificationDismissedCount(Context context) { 264 SharedPreferences sp = context.getSharedPreferences(SHARED_PREFERENCES_NAME, 265 Context.MODE_PRIVATE); 266 SharedPreferences.Editor editor = sp.edit(); 267 int dismissCount = sp.getInt(NOTIFICATION_DISMISS_COUNT, 0) + 1; 268 editor.putInt(NOTIFICATION_DISMISS_COUNT, dismissCount); 269 editor.apply(); 270 } 271 delayNextNotification(Context context, long timeInMillis)272 private void delayNextNotification(Context context, long timeInMillis) { 273 SharedPreferences sp = context.getSharedPreferences(SHARED_PREFERENCES_NAME, 274 Context.MODE_PRIVATE); 275 SharedPreferences.Editor editor = sp.edit(); 276 editor.putLong(NOTIFICATION_NEXT_SHOW_TIME, 277 getCurrentTime() + timeInMillis); 278 editor.apply(); 279 } 280 getCurrentTime()281 private long getCurrentTime() { 282 if (mClock == null) { 283 mClock = new Clock(); 284 } 285 286 return mClock.currentTimeMillis(); 287 } 288 289 @VisibleForTesting getBaseIntent(Context context, String action)290 Intent getBaseIntent(Context context, String action) { 291 return new Intent(context, NotificationController.class).setAction(action); 292 } 293 294 /** 295 * Clock provides the current time. 296 */ 297 protected static class Clock { 298 /** 299 * Returns the current time in milliseconds. 300 */ currentTimeMillis()301 public long currentTimeMillis() { 302 return System.currentTimeMillis(); 303 } 304 } 305 } 306