| /* |
| * Copyright (C) 2016 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.storagemanager.automatic; |
| |
| import android.app.Notification; |
| import android.app.NotificationChannel; |
| import android.app.NotificationManager; |
| import android.app.PendingIntent; |
| import android.content.BroadcastReceiver; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.SharedPreferences; |
| import android.content.res.Resources; |
| import android.os.SystemProperties; |
| import android.provider.Settings; |
| import androidx.annotation.VisibleForTesting; |
| import androidx.core.os.BuildCompat; |
| |
| import com.android.storagemanager.R; |
| |
| import java.util.concurrent.TimeUnit; |
| |
| /** |
| * NotificationController handles the responses to the Automatic Storage Management low storage |
| * notification. |
| */ |
| public class NotificationController extends BroadcastReceiver { |
| /** |
| * Intent action for if the user taps "Turn on" for the automatic storage manager. |
| */ |
| public static final String INTENT_ACTION_ACTIVATE_ASM = |
| "com.android.storagemanager.automatic.ACTIVATE"; |
| |
| /** |
| * Intent action for if the user swipes the notification away. |
| */ |
| public static final String INTENT_ACTION_DISMISS = |
| "com.android.storagemanager.automatic.DISMISS"; |
| |
| /** |
| * Intent action for if the user explicitly hits "No thanks" on the notification. |
| */ |
| public static final String INTENT_ACTION_NO_THANKS = |
| "com.android.storagemanager.automatic.NO_THANKS"; |
| |
| /** |
| * Intent action to maybe show the ASM upsell notification. |
| */ |
| public static final String INTENT_ACTION_SHOW_NOTIFICATION = |
| "com.android.storagemanager.automatic.show_notification"; |
| |
| /** |
| * Intent action for forcefully showing the notification, even if the conditions are not valid. |
| */ |
| private static final String INTENT_ACTION_DEBUG_NOTIFICATION = |
| "com.android.storagemanager.automatic.DEBUG_SHOW_NOTIFICATION"; |
| |
| /** Intent action for if the user taps on the notification. */ |
| @VisibleForTesting |
| static final String INTENT_ACTION_TAP = "com.android.storagemanager.automatic.SHOW_SETTINGS"; |
| |
| /** |
| * Intent extra for the notification id. |
| */ |
| public static final String INTENT_EXTRA_ID = "id"; |
| |
| private static final String SHARED_PREFERENCES_NAME = "NotificationController"; |
| private static final String NOTIFICATION_NEXT_SHOW_TIME = "notification_next_show_time"; |
| private static final String NOTIFICATION_SHOWN_COUNT = "notification_shown_count"; |
| private static final String NOTIFICATION_DISMISS_COUNT = "notification_dismiss_count"; |
| private static final String STORAGE_MANAGER_PROPERTY = "ro.storage_manager.enabled"; |
| private static final String CHANNEL_ID = "storage"; |
| |
| private static final long DISMISS_DELAY = TimeUnit.DAYS.toMillis(14); |
| private static final long NO_THANKS_DELAY = TimeUnit.DAYS.toMillis(90); |
| private static final long MAXIMUM_SHOWN_COUNT = 4; |
| private static final long MAXIMUM_DISMISS_COUNT = 9; |
| private static final int NOTIFICATION_ID = 0; |
| |
| // Keeps the time for test purposes. |
| private Clock mClock; |
| |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| switch (intent.getAction()) { |
| case INTENT_ACTION_ACTIVATE_ASM: |
| Settings.Secure.putInt( |
| context.getContentResolver(), |
| Settings.Secure.AUTOMATIC_STORAGE_MANAGER_ENABLED, |
| 1); |
| // Provide a warning if storage manager is not defaulted on. |
| if (!SystemProperties.getBoolean(STORAGE_MANAGER_PROPERTY, false)) { |
| Intent warningIntent = new Intent(context, WarningDialogActivity.class); |
| warningIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); |
| context.startActivity(warningIntent); |
| } |
| break; |
| case INTENT_ACTION_NO_THANKS: |
| delayNextNotification(context, NO_THANKS_DELAY); |
| break; |
| case INTENT_ACTION_DISMISS: |
| delayNextNotification(context, DISMISS_DELAY); |
| break; |
| case INTENT_ACTION_SHOW_NOTIFICATION: |
| maybeShowNotification(context); |
| return; |
| case INTENT_ACTION_DEBUG_NOTIFICATION: |
| showNotification(context); |
| return; |
| case INTENT_ACTION_TAP: |
| Intent storageIntent = new Intent(Settings.ACTION_INTERNAL_STORAGE_SETTINGS); |
| storageIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); |
| context.startActivity(storageIntent); |
| break; |
| } |
| cancelNotification(context, intent); |
| } |
| |
| /** |
| * Sets a time provider for the controller. |
| * @param clock The time provider. |
| */ |
| protected void setClock(Clock clock) { |
| mClock = clock; |
| } |
| |
| /** |
| * If the conditions for showing the activation notification are met, show the activation |
| * notification. |
| * @param context Context to use for getting resources and to display the notification. |
| */ |
| private void maybeShowNotification(Context context) { |
| if (shouldShowNotification(context)) { |
| showNotification(context); |
| } |
| } |
| |
| private boolean shouldShowNotification(Context context) { |
| boolean showNotificationConfigEnabled = |
| context.getResources().getBoolean(R.bool.enable_low_storage_notification); |
| if (!showNotificationConfigEnabled) { |
| return false; |
| } |
| |
| SharedPreferences sp = context.getSharedPreferences( |
| SHARED_PREFERENCES_NAME, |
| Context.MODE_PRIVATE); |
| int timesShown = sp.getInt(NOTIFICATION_SHOWN_COUNT, 0); |
| int timesDismissed = sp.getInt(NOTIFICATION_DISMISS_COUNT, 0); |
| if (timesShown >= MAXIMUM_SHOWN_COUNT || timesDismissed >= MAXIMUM_DISMISS_COUNT) { |
| return false; |
| } |
| |
| long nextTimeToShow = sp.getLong(NOTIFICATION_NEXT_SHOW_TIME, 0); |
| |
| return getCurrentTime() >= nextTimeToShow; |
| } |
| |
| private void showNotification(Context context) { |
| Resources res = context.getResources(); |
| Intent noThanksIntent = getBaseIntent(context, INTENT_ACTION_NO_THANKS); |
| noThanksIntent.putExtra(INTENT_EXTRA_ID, NOTIFICATION_ID); |
| Notification.Action.Builder cancelAction = new Notification.Action.Builder(null, |
| res.getString(R.string.automatic_storage_manager_cancel_button), |
| PendingIntent.getBroadcast(context, 0, noThanksIntent, |
| PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE)); |
| |
| |
| Intent activateIntent = getBaseIntent(context, INTENT_ACTION_ACTIVATE_ASM); |
| activateIntent.putExtra(INTENT_EXTRA_ID, NOTIFICATION_ID); |
| Notification.Action.Builder activateAutomaticAction = new Notification.Action.Builder(null, |
| res.getString(R.string.automatic_storage_manager_activate_button), |
| PendingIntent.getBroadcast(context, 0, activateIntent, |
| PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE)); |
| |
| Intent dismissIntent = getBaseIntent(context, INTENT_ACTION_DISMISS); |
| dismissIntent.putExtra(INTENT_EXTRA_ID, NOTIFICATION_ID); |
| PendingIntent deleteIntent = PendingIntent.getBroadcast(context, 0, |
| dismissIntent, |
| PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_IMMUTABLE); |
| |
| Intent contentIntent = getBaseIntent(context, INTENT_ACTION_TAP); |
| contentIntent.putExtra(INTENT_EXTRA_ID, NOTIFICATION_ID); |
| PendingIntent tapIntent = PendingIntent.getBroadcast(context, 0, contentIntent, |
| PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_IMMUTABLE); |
| |
| Notification.Builder builder; |
| // We really should only have the path with the notification channel set. The other path is |
| // only for legacy Robolectric reasons -- Robolectric does not have the Notification |
| // builder with a channel id, so it crashes when it hits that code path. |
| if (BuildCompat.isAtLeastO()) { |
| makeNotificationChannel(context); |
| builder = new Notification.Builder(context, CHANNEL_ID); |
| } else { |
| builder = new Notification.Builder(context); |
| } |
| |
| builder.setSmallIcon(R.drawable.ic_settings_24dp) |
| .setContentTitle( |
| res.getString(R.string.automatic_storage_manager_notification_title)) |
| .setContentText( |
| res.getString(R.string.automatic_storage_manager_notification_summary)) |
| .setStyle( |
| new Notification.BigTextStyle() |
| .bigText( |
| res.getString( |
| R.string |
| .automatic_storage_manager_notification_summary))) |
| .addAction(cancelAction.build()) |
| .addAction(activateAutomaticAction.build()) |
| .setContentIntent(tapIntent) |
| .setDeleteIntent(deleteIntent) |
| .setLocalOnly(true); |
| |
| NotificationManager manager = |
| ((NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE)); |
| manager.notify(NOTIFICATION_ID, builder.build()); |
| } |
| |
| private void makeNotificationChannel(Context context) { |
| final NotificationManager nm = context.getSystemService(NotificationManager.class); |
| final NotificationChannel channel = |
| new NotificationChannel( |
| CHANNEL_ID, |
| context.getString(R.string.app_name), |
| NotificationManager.IMPORTANCE_LOW); |
| nm.createNotificationChannel(channel); |
| } |
| |
| private void cancelNotification(Context context, Intent intent) { |
| if (intent.getAction() == INTENT_ACTION_DISMISS) { |
| incrementNotificationDismissedCount(context); |
| } else { |
| incrementNotificationShownCount(context); |
| } |
| |
| int id = intent.getIntExtra(INTENT_EXTRA_ID, -1); |
| if (id == -1) { |
| return; |
| } |
| NotificationManager manager = (NotificationManager) context |
| .getSystemService(Context.NOTIFICATION_SERVICE); |
| manager.cancel(id); |
| } |
| |
| private void incrementNotificationShownCount(Context context) { |
| SharedPreferences sp = context.getSharedPreferences(SHARED_PREFERENCES_NAME, |
| Context.MODE_PRIVATE); |
| SharedPreferences.Editor editor = sp.edit(); |
| int shownCount = sp.getInt(NotificationController.NOTIFICATION_SHOWN_COUNT, 0) + 1; |
| editor.putInt(NotificationController.NOTIFICATION_SHOWN_COUNT, shownCount); |
| editor.apply(); |
| } |
| |
| private void incrementNotificationDismissedCount(Context context) { |
| SharedPreferences sp = context.getSharedPreferences(SHARED_PREFERENCES_NAME, |
| Context.MODE_PRIVATE); |
| SharedPreferences.Editor editor = sp.edit(); |
| int dismissCount = sp.getInt(NOTIFICATION_DISMISS_COUNT, 0) + 1; |
| editor.putInt(NOTIFICATION_DISMISS_COUNT, dismissCount); |
| editor.apply(); |
| } |
| |
| private void delayNextNotification(Context context, long timeInMillis) { |
| SharedPreferences sp = context.getSharedPreferences(SHARED_PREFERENCES_NAME, |
| Context.MODE_PRIVATE); |
| SharedPreferences.Editor editor = sp.edit(); |
| editor.putLong(NOTIFICATION_NEXT_SHOW_TIME, |
| getCurrentTime() + timeInMillis); |
| editor.apply(); |
| } |
| |
| private long getCurrentTime() { |
| if (mClock == null) { |
| mClock = new Clock(); |
| } |
| |
| return mClock.currentTimeMillis(); |
| } |
| |
| @VisibleForTesting |
| Intent getBaseIntent(Context context, String action) { |
| return new Intent(context, NotificationController.class).setAction(action); |
| } |
| |
| /** |
| * Clock provides the current time. |
| */ |
| protected static class Clock { |
| /** |
| * Returns the current time in milliseconds. |
| */ |
| public long currentTimeMillis() { |
| return System.currentTimeMillis(); |
| } |
| } |
| } |