Home | History | Annotate | Download | only in automatic
      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 android.support.annotation.VisibleForTesting;
     31 import android.support.v4.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
     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