blob: 2cb7c8cec4f87c960a2a24262e62a17fd8610dca [file] [log] [blame]
/*
* 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();
}
}
}