1 /** 2 * Copyright (C) 2017 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 android.ext.services.notification; 18 19 import static android.app.NotificationManager.IMPORTANCE_LOW; 20 import static android.service.notification.Adjustment.KEY_IMPORTANCE; 21 22 import android.annotation.SuppressLint; 23 import android.app.Notification; 24 import android.app.NotificationChannel; 25 import android.content.pm.PackageManager; 26 import android.os.Bundle; 27 import android.os.UserHandle; 28 import android.service.notification.Adjustment; 29 import android.service.notification.NotificationAssistantService; 30 import android.service.notification.NotificationStats; 31 import android.service.notification.StatusBarNotification; 32 import android.util.ArrayMap; 33 import android.util.Log; 34 35 import androidx.annotation.NonNull; 36 import androidx.annotation.Nullable; 37 import androidx.annotation.VisibleForTesting; 38 39 import com.android.textclassifier.notification.SmartSuggestions; 40 import com.android.textclassifier.notification.SmartSuggestionsHelper; 41 42 import java.util.ArrayList; 43 import java.util.List; 44 import java.util.concurrent.ExecutorService; 45 import java.util.concurrent.Executors; 46 47 /** 48 * Notification assistant that provides guidance on notification channel blocking 49 */ 50 @SuppressLint("OverrideAbstract") 51 public class Assistant extends NotificationAssistantService { 52 private static final String TAG = "ExtAssistant"; 53 private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 54 55 // SBN key : entry 56 protected ArrayMap<String, NotificationEntry> mLiveNotifications = new ArrayMap<>(); 57 58 private PackageManager mPackageManager; 59 60 private final ExecutorService mSingleThreadExecutor = Executors.newSingleThreadExecutor(); 61 @VisibleForTesting 62 protected AssistantSettings.Factory mSettingsFactory = AssistantSettings.FACTORY; 63 @VisibleForTesting 64 protected AssistantSettings mSettings; 65 private SmsHelper mSmsHelper; 66 private SmartSuggestionsHelper mSmartSuggestionsHelper; 67 private NotificationCategorizer mNotificationCategorizer; 68 Assistant()69 public Assistant() { 70 } 71 72 @Override onCreate()73 public void onCreate() { 74 super.onCreate(); 75 // Contexts are correctly hooked up by the creation step, which is required for the observer 76 // to be hooked up/initialized. 77 mPackageManager = getPackageManager(); 78 mSettings = mSettingsFactory.createAndRegister(); 79 mSmartSuggestionsHelper = new SmartSuggestionsHelper(this, mSettings); 80 mNotificationCategorizer = new NotificationCategorizer(); 81 mSmsHelper = new SmsHelper(this); 82 mSmsHelper.initialize(); 83 } 84 85 @Override onDestroy()86 public void onDestroy() { 87 // This null check is only for the unit tests as ServiceTestCase.tearDown calls onDestroy 88 // without having first called onCreate. 89 if (mSmsHelper != null) { 90 mSmsHelper.destroy(); 91 } 92 super.onDestroy(); 93 } 94 95 @Override onNotificationEnqueued(StatusBarNotification sbn)96 public Adjustment onNotificationEnqueued(StatusBarNotification sbn) { 97 // we use the version with channel, so this is never called. 98 return null; 99 } 100 101 @Override onNotificationEnqueued(StatusBarNotification sbn, NotificationChannel channel)102 public Adjustment onNotificationEnqueued(StatusBarNotification sbn, 103 NotificationChannel channel) { 104 if (DEBUG) Log.i(TAG, "ENQUEUED " + sbn.getKey() + " on " + channel.getId()); 105 if (!isForCurrentUser(sbn)) { 106 return null; 107 } 108 mSingleThreadExecutor.submit(() -> { 109 NotificationEntry entry = 110 new NotificationEntry(this, mPackageManager, sbn, channel, mSmsHelper); 111 SmartSuggestions suggestions = mSmartSuggestionsHelper.onNotificationEnqueued(sbn); 112 if (DEBUG) { 113 Log.d(TAG, String.format( 114 "Creating Adjustment for %s, with %d actions, and %d replies.", 115 sbn.getKey(), 116 suggestions.getActions().size(), 117 suggestions.getReplies().size())); 118 } 119 Adjustment adjustment = createEnqueuedNotificationAdjustment( 120 entry, 121 new ArrayList<Notification.Action>(suggestions.getActions()), 122 new ArrayList<>(suggestions.getReplies())); 123 adjustNotification(adjustment); 124 }); 125 return null; 126 } 127 128 /** A convenience helper for creating an adjustment for an SBN. */ 129 @VisibleForTesting 130 @Nullable createEnqueuedNotificationAdjustment( @onNull NotificationEntry entry, @NonNull ArrayList<Notification.Action> smartActions, @NonNull ArrayList<CharSequence> smartReplies)131 Adjustment createEnqueuedNotificationAdjustment( 132 @NonNull NotificationEntry entry, 133 @NonNull ArrayList<Notification.Action> smartActions, 134 @NonNull ArrayList<CharSequence> smartReplies) { 135 Bundle signals = new Bundle(); 136 137 if (!smartActions.isEmpty()) { 138 signals.putParcelableArrayList(Adjustment.KEY_CONTEXTUAL_ACTIONS, smartActions); 139 } 140 if (!smartReplies.isEmpty()) { 141 signals.putCharSequenceArrayList(Adjustment.KEY_TEXT_REPLIES, smartReplies); 142 } 143 if (mNotificationCategorizer.shouldSilence(entry)) { 144 final int importance = entry.getImportance() < IMPORTANCE_LOW 145 ? entry.getImportance() : IMPORTANCE_LOW; 146 signals.putInt(KEY_IMPORTANCE, importance); 147 } else { 148 // Even if no change is made, send an identity adjustment for metric logging. 149 signals.putInt(KEY_IMPORTANCE, entry.getImportance()); 150 } 151 152 return new Adjustment( 153 entry.getSbn().getPackageName(), 154 entry.getSbn().getKey(), 155 signals, 156 "", 157 entry.getSbn().getUserId()); 158 } 159 160 @Override 161 public void onNotificationPosted(StatusBarNotification sbn, RankingMap rankingMap) { 162 if (DEBUG) Log.i(TAG, "POSTED " + sbn.getKey()); 163 try { 164 if (!isForCurrentUser(sbn)) { 165 return; 166 } 167 Ranking ranking = new Ranking(); 168 rankingMap.getRanking(sbn.getKey(), ranking); 169 if (ranking != null && ranking.getChannel() != null) { 170 NotificationEntry entry = new NotificationEntry(this, mPackageManager, 171 sbn, ranking.getChannel(), mSmsHelper); 172 mLiveNotifications.put(sbn.getKey(), entry); 173 } 174 } catch (Throwable e) { 175 Log.e(TAG, "Error occurred processing post", e); 176 } 177 } 178 179 @Override 180 public void onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap, 181 NotificationStats stats, int reason) { 182 try { 183 if (!isForCurrentUser(sbn)) { 184 return; 185 } 186 187 mLiveNotifications.remove(sbn.getKey()); 188 189 } catch (Throwable e) { 190 Log.e(TAG, "Error occurred processing removal of " + sbn.getKey(), e); 191 } 192 } 193 194 @Override 195 public void onNotificationSnoozedUntilContext(StatusBarNotification sbn, 196 String snoozeCriterionId) { 197 } 198 199 @Override 200 public void onNotificationsSeen(List<String> keys) { 201 } 202 203 @Override 204 public void onNotificationExpansionChanged(@NonNull String key, boolean isUserAction, 205 boolean isExpanded) { 206 if (DEBUG) { 207 Log.d(TAG, "onNotificationExpansionChanged() called with: key = [" + key 208 + "], isUserAction = [" + isUserAction + "], isExpanded = [" + isExpanded 209 + "]"); 210 } 211 NotificationEntry entry = mLiveNotifications.get(key); 212 213 if (entry != null) { 214 mSingleThreadExecutor.submit( 215 () -> mSmartSuggestionsHelper.onNotificationExpansionChanged( 216 entry.getSbn(), isExpanded)); 217 } 218 } 219 220 @Override onNotificationDirectReplied(@onNull String key)221 public void onNotificationDirectReplied(@NonNull String key) { 222 if (DEBUG) Log.i(TAG, "onNotificationDirectReplied " + key); 223 mSingleThreadExecutor.submit(() -> mSmartSuggestionsHelper.onNotificationDirectReplied(key)); 224 } 225 226 @Override onSuggestedReplySent(@onNull String key, @NonNull CharSequence reply, int source)227 public void onSuggestedReplySent(@NonNull String key, @NonNull CharSequence reply, 228 int source) { 229 if (DEBUG) { 230 Log.d(TAG, "onSuggestedReplySent() called with: key = [" + key + "], reply = [" + reply 231 + "], source = [" + source + "]"); 232 } 233 mSingleThreadExecutor.submit( 234 () -> mSmartSuggestionsHelper.onSuggestedReplySent(key, reply, source)); 235 } 236 237 @Override onActionInvoked(@onNull String key, @NonNull Notification.Action action, int source)238 public void onActionInvoked(@NonNull String key, @NonNull Notification.Action action, 239 int source) { 240 if (DEBUG) { 241 Log.d(TAG, 242 "onActionInvoked() called with: key = [" + key + "], action = [" + action.title 243 + "], source = [" + source + "]"); 244 } 245 mSingleThreadExecutor.submit( 246 () -> mSmartSuggestionsHelper.onActionClicked(key, action, source)); 247 } 248 249 @Override onListenerConnected()250 public void onListenerConnected() { 251 if (DEBUG) Log.i(TAG, "Connected"); 252 } 253 254 @Override onListenerDisconnected()255 public void onListenerDisconnected() { 256 } 257 isForCurrentUser(StatusBarNotification sbn)258 private boolean isForCurrentUser(StatusBarNotification sbn) { 259 return sbn != null && sbn.getUserId() == UserHandle.myUserId(); 260 } 261 } 262