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 17package com.android.storagemanager.automatic; 18 19import android.app.Notification; 20import android.app.NotificationChannel; 21import android.app.NotificationManager; 22import android.app.PendingIntent; 23import android.content.BroadcastReceiver; 24import android.content.Context; 25import android.content.Intent; 26import android.content.SharedPreferences; 27import android.content.res.Resources; 28import android.os.SystemProperties; 29import android.provider.Settings; 30import android.support.annotation.VisibleForTesting; 31import android.support.v4.os.BuildCompat; 32 33import com.android.storagemanager.R; 34 35import java.util.concurrent.TimeUnit; 36 37/** 38 * NotificationController handles the responses to the Automatic Storage Management low storage 39 * notification. 40 */ 41public 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 98 public void onReceive(Context context, Intent intent) { 99 switch (intent.getAction()) { 100 case INTENT_ACTION_ACTIVATE_ASM: 101 Settings.Secure.putInt(context.getContentResolver(), 102 Settings.Secure.AUTOMATIC_STORAGE_MANAGER_ENABLED, 103 1); 104 // Provide a warning if storage manager is not defaulted on. 105 if (!SystemProperties.getBoolean(STORAGE_MANAGER_PROPERTY, false)) { 106 Intent warningIntent = new Intent(context, WarningDialogActivity.class); 107 context.startActivity(warningIntent); 108 } 109 break; 110 case INTENT_ACTION_NO_THANKS: 111 delayNextNotification(context, NO_THANKS_DELAY); 112 break; 113 case INTENT_ACTION_DISMISS: 114 delayNextNotification(context, DISMISS_DELAY); 115 break; 116 case INTENT_ACTION_SHOW_NOTIFICATION: 117 maybeShowNotification(context); 118 return; 119 case INTENT_ACTION_DEBUG_NOTIFICATION: 120 showNotification(context); 121 return; 122 case INTENT_ACTION_TAP: 123 Intent storageIntent = new Intent(Settings.ACTION_INTERNAL_STORAGE_SETTINGS); 124 storageIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 125 context.startActivity(storageIntent); 126 break; 127 } 128 cancelNotification(context, intent); 129 } 130 131 /** 132 * Sets a time provider for the controller. 133 * @param clock The time provider. 134 */ 135 protected void setClock(Clock clock) { 136 mClock = clock; 137 } 138 139 /** 140 * If the conditions for showing the activation notification are met, show the activation 141 * notification. 142 * @param context Context to use for getting resources and to display the notification. 143 */ 144 private void maybeShowNotification(Context context) { 145 if (shouldShowNotification(context)) { 146 showNotification(context); 147 } 148 } 149 150 private boolean shouldShowNotification(Context context) { 151 SharedPreferences sp = context.getSharedPreferences( 152 SHARED_PREFERENCES_NAME, 153 Context.MODE_PRIVATE); 154 int timesShown = sp.getInt(NOTIFICATION_SHOWN_COUNT, 0); 155 int timesDismissed = sp.getInt(NOTIFICATION_DISMISS_COUNT, 0); 156 if (timesShown >= MAXIMUM_SHOWN_COUNT || timesDismissed >= MAXIMUM_DISMISS_COUNT) { 157 return false; 158 } 159 160 long nextTimeToShow = sp.getLong(NOTIFICATION_NEXT_SHOW_TIME, 0); 161 162 return getCurrentTime() >= nextTimeToShow; 163 } 164 165 private void showNotification(Context context) { 166 Resources res = context.getResources(); 167 Intent noThanksIntent = getBaseIntent(context, INTENT_ACTION_NO_THANKS); 168 noThanksIntent.putExtra(INTENT_EXTRA_ID, NOTIFICATION_ID); 169 Notification.Action.Builder cancelAction = new Notification.Action.Builder(null, 170 res.getString(R.string.automatic_storage_manager_cancel_button), 171 PendingIntent.getBroadcast(context, 0, noThanksIntent, 172 PendingIntent.FLAG_UPDATE_CURRENT)); 173 174 175 Intent activateIntent = getBaseIntent(context, INTENT_ACTION_ACTIVATE_ASM); 176 activateIntent.putExtra(INTENT_EXTRA_ID, NOTIFICATION_ID); 177 Notification.Action.Builder activateAutomaticAction = new Notification.Action.Builder(null, 178 res.getString(R.string.automatic_storage_manager_activate_button), 179 PendingIntent.getBroadcast(context, 0, activateIntent, 180 PendingIntent.FLAG_UPDATE_CURRENT)); 181 182 Intent dismissIntent = getBaseIntent(context, INTENT_ACTION_DISMISS); 183 dismissIntent.putExtra(INTENT_EXTRA_ID, NOTIFICATION_ID); 184 PendingIntent deleteIntent = PendingIntent.getBroadcast(context, 0, 185 dismissIntent, 186 PendingIntent.FLAG_ONE_SHOT); 187 188 Intent contentIntent = getBaseIntent(context, INTENT_ACTION_TAP); 189 contentIntent.putExtra(INTENT_EXTRA_ID, NOTIFICATION_ID); 190 PendingIntent tapIntent = PendingIntent.getBroadcast(context, 0, contentIntent, 191 PendingIntent.FLAG_ONE_SHOT); 192 193 Notification.Builder builder; 194 // We really should only have the path with the notification channel set. The other path is 195 // only for legacy Robolectric reasons -- Robolectric does not have the Notification 196 // builder with a channel id, so it crashes when it hits that code path. 197 if (BuildCompat.isAtLeastO()) { 198 makeNotificationChannel(context); 199 builder = new Notification.Builder(context, CHANNEL_ID); 200 } else { 201 builder = new Notification.Builder(context); 202 } 203 204 builder.setSmallIcon(R.drawable.ic_settings_24dp) 205 .setContentTitle( 206 res.getString(R.string.automatic_storage_manager_notification_title)) 207 .setContentText( 208 res.getString(R.string.automatic_storage_manager_notification_summary)) 209 .setStyle( 210 new Notification.BigTextStyle() 211 .bigText( 212 res.getString( 213 R.string 214 .automatic_storage_manager_notification_summary))) 215 .addAction(cancelAction.build()) 216 .addAction(activateAutomaticAction.build()) 217 .setContentIntent(tapIntent) 218 .setDeleteIntent(deleteIntent) 219 .setLocalOnly(true); 220 221 NotificationManager manager = 222 ((NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE)); 223 manager.notify(NOTIFICATION_ID, builder.build()); 224 } 225 226 private void makeNotificationChannel(Context context) { 227 final NotificationManager nm = context.getSystemService(NotificationManager.class); 228 final NotificationChannel channel = 229 new NotificationChannel( 230 CHANNEL_ID, 231 context.getString(R.string.app_name), 232 NotificationManager.IMPORTANCE_LOW); 233 nm.createNotificationChannel(channel); 234 } 235 236 private void cancelNotification(Context context, Intent intent) { 237 if (intent.getAction() == INTENT_ACTION_DISMISS) { 238 incrementNotificationDismissedCount(context); 239 } else { 240 incrementNotificationShownCount(context); 241 } 242 243 int id = intent.getIntExtra(INTENT_EXTRA_ID, -1); 244 if (id == -1) { 245 return; 246 } 247 NotificationManager manager = (NotificationManager) context 248 .getSystemService(Context.NOTIFICATION_SERVICE); 249 manager.cancel(id); 250 } 251 252 private void incrementNotificationShownCount(Context context) { 253 SharedPreferences sp = context.getSharedPreferences(SHARED_PREFERENCES_NAME, 254 Context.MODE_PRIVATE); 255 SharedPreferences.Editor editor = sp.edit(); 256 int shownCount = sp.getInt(NotificationController.NOTIFICATION_SHOWN_COUNT, 0) + 1; 257 editor.putInt(NotificationController.NOTIFICATION_SHOWN_COUNT, shownCount); 258 editor.apply(); 259 } 260 261 private void incrementNotificationDismissedCount(Context context) { 262 SharedPreferences sp = context.getSharedPreferences(SHARED_PREFERENCES_NAME, 263 Context.MODE_PRIVATE); 264 SharedPreferences.Editor editor = sp.edit(); 265 int dismissCount = sp.getInt(NOTIFICATION_DISMISS_COUNT, 0) + 1; 266 editor.putInt(NOTIFICATION_DISMISS_COUNT, dismissCount); 267 editor.apply(); 268 } 269 270 private void delayNextNotification(Context context, long timeInMillis) { 271 SharedPreferences sp = context.getSharedPreferences(SHARED_PREFERENCES_NAME, 272 Context.MODE_PRIVATE); 273 SharedPreferences.Editor editor = sp.edit(); 274 editor.putLong(NOTIFICATION_NEXT_SHOW_TIME, 275 getCurrentTime() + timeInMillis); 276 editor.apply(); 277 } 278 279 private long getCurrentTime() { 280 if (mClock == null) { 281 mClock = new Clock(); 282 } 283 284 return mClock.currentTimeMillis(); 285 } 286 287 @VisibleForTesting 288 Intent getBaseIntent(Context context, String action) { 289 return new Intent(context, NotificationController.class).setAction(action); 290 } 291 292 /** 293 * Clock provides the current time. 294 */ 295 protected static class Clock { 296 /** 297 * Returns the current time in milliseconds. 298 */ 299 public long currentTimeMillis() { 300 return System.currentTimeMillis(); 301 } 302 } 303} 304