1 /* 2 * Copyright (C) 2023 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.settings.localepicker; 18 19 import android.content.Context; 20 import android.os.SystemClock; 21 import android.os.SystemProperties; 22 import android.util.Log; 23 24 import androidx.annotation.NonNull; 25 import androidx.annotation.VisibleForTesting; 26 27 import java.util.Calendar; 28 import java.util.Set; 29 30 /** 31 * A controller that evaluates whether the notification can be triggered and update the 32 * SharedPreference. 33 */ 34 public class NotificationController { 35 private static final String TAG = NotificationController.class.getSimpleName(); 36 private static final int DISMISS_COUNT_THRESHOLD = 2; 37 private static final int NOTIFICATION_COUNT_THRESHOLD = 2; 38 private static final int MULTIPLE_BASE = 2; 39 // seven days: 7 * 24 * 60 40 private static final int MIN_DURATION_BETWEEN_NOTIFICATIONS_MIN = 10080; 41 private static final String PROPERTY_MIN_DURATION = 42 "android.localenotification.duration.threshold"; 43 44 private static NotificationController sInstance = null; 45 46 private final LocaleNotificationDataManager mDataManager; 47 48 /** 49 * Get {@link NotificationController} instance. 50 * 51 * @param context The context 52 * @return {@link NotificationController} instance 53 */ getInstance(@onNull Context context)54 public static synchronized NotificationController getInstance(@NonNull Context context) { 55 if (sInstance == null) { 56 sInstance = new NotificationController(context); 57 } 58 return sInstance; 59 } 60 NotificationController(Context context)61 private NotificationController(Context context) { 62 mDataManager = new LocaleNotificationDataManager(context); 63 } 64 65 @VisibleForTesting getDataManager()66 LocaleNotificationDataManager getDataManager() { 67 return mDataManager; 68 } 69 70 /** 71 * Increment the dismissCount of the notification. 72 * 73 * @param locale A locale used to query the {@link NotificationInfo} 74 */ incrementDismissCount(@onNull String locale)75 public void incrementDismissCount(@NonNull String locale) { 76 NotificationInfo currentInfo = mDataManager.getNotificationInfo(locale); 77 NotificationInfo newInfo = new NotificationInfo(currentInfo.getUidCollection(), 78 currentInfo.getNotificationCount(), 79 currentInfo.getDismissCount() + 1, 80 currentInfo.getLastNotificationTimeMs(), 81 currentInfo.getNotificationId()); 82 mDataManager.putNotificationInfo(locale, newInfo); 83 } 84 85 /** 86 * Whether the notification can be triggered or not. 87 * 88 * @param uid The application's uid. 89 * @param locale The application's locale which the user updated to. 90 * @return true if the notification needs to be triggered. Otherwise, false. 91 */ shouldTriggerNotification(int uid, @NonNull String locale)92 public boolean shouldTriggerNotification(int uid, @NonNull String locale) { 93 if (LocaleUtils.isInSystemLocale(locale)) { 94 return false; 95 } else { 96 // Add the uid into the locale's uid list and update the notification count if the 97 // notification can be triggered. 98 return updateLocaleNotificationInfo(uid, locale); 99 } 100 } 101 102 /** 103 * Get the notification id 104 * 105 * @param locale The locale which the application sets to 106 * @return the notification id 107 */ getNotificationId(@onNull String locale)108 public int getNotificationId(@NonNull String locale) { 109 NotificationInfo info = mDataManager.getNotificationInfo(locale); 110 return (info != null) ? info.getNotificationId() : -1; 111 } 112 113 /** 114 * Remove the {@link NotificationInfo} with the corresponding locale 115 * 116 * @param locale The locale which the application sets to 117 */ removeNotificationInfo(@onNull String locale)118 public void removeNotificationInfo(@NonNull String locale) { 119 mDataManager.removeNotificationInfo(locale); 120 } 121 updateLocaleNotificationInfo(int uid, String locale)122 private boolean updateLocaleNotificationInfo(int uid, String locale) { 123 NotificationInfo info = mDataManager.getNotificationInfo(locale); 124 if (info == null) { 125 // Create an empty record with the uid and update the SharedPreference. 126 NotificationInfo emptyInfo = new NotificationInfo(Set.of(uid), 0, 0, 0, 0); 127 mDataManager.putNotificationInfo(locale, emptyInfo); 128 return false; 129 } 130 Set uidCollection = info.getUidCollection(); 131 if (uidCollection.contains(uid)) { 132 return false; 133 } 134 135 NotificationInfo newInfo = 136 createNotificationInfoWithNewUidAndCount(uidCollection, uid, info); 137 mDataManager.putNotificationInfo(locale, newInfo); 138 return newInfo.getNotificationCount() > info.getNotificationCount(); 139 } 140 createNotificationInfoWithNewUidAndCount( Set<Integer> uidSet, int uid, NotificationInfo info)141 private NotificationInfo createNotificationInfoWithNewUidAndCount( 142 Set<Integer> uidSet, int uid, NotificationInfo info) { 143 int dismissCount = info.getDismissCount(); 144 int notificationCount = info.getNotificationCount(); 145 long lastNotificationTime = info.getLastNotificationTimeMs(); 146 int notificationId = info.getNotificationId(); 147 if (dismissCount < DISMISS_COUNT_THRESHOLD 148 && notificationCount < NOTIFICATION_COUNT_THRESHOLD) { 149 // Add the uid into the locale's uid list 150 uidSet.add(uid); 151 // Notification should fire on multiples of 2 apps using the locale. 152 if (uidSet.size() % MULTIPLE_BASE == 0 153 && !isNotificationFrequent(lastNotificationTime)) { 154 // Increment the count because the notification can be triggered. 155 notificationCount = info.getNotificationCount() + 1; 156 lastNotificationTime = Calendar.getInstance().getTimeInMillis(); 157 Log.i(TAG, "notificationCount:" + notificationCount); 158 if (notificationCount == 1) { 159 notificationId = (int) SystemClock.uptimeMillis(); 160 } 161 } 162 } 163 return new NotificationInfo(uidSet, notificationCount, dismissCount, lastNotificationTime, 164 notificationId); 165 } 166 167 /** 168 * Evaluates if the notification is triggered frequently. 169 * 170 * @param lastNotificationTime The timestamp that the last notification was triggered. 171 * @return true if the duration of the two continuous notifications is smaller than the 172 * threshold. 173 * Otherwise, false. 174 */ isNotificationFrequent(long lastNotificationTime)175 private boolean isNotificationFrequent(long lastNotificationTime) { 176 Calendar time = Calendar.getInstance(); 177 int threshold = SystemProperties.getInt(PROPERTY_MIN_DURATION, 178 MIN_DURATION_BETWEEN_NOTIFICATIONS_MIN); 179 time.add(Calendar.MINUTE, threshold * -1); 180 return time.getTimeInMillis() < lastNotificationTime; 181 } 182 } 183