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 Assistant()68 public Assistant() { 69 } 70 71 @Override onCreate()72 public void onCreate() { 73 super.onCreate(); 74 // Contexts are correctly hooked up by the creation step, which is required for the observer 75 // to be hooked up/initialized. 76 mPackageManager = getPackageManager(); 77 mSettings = mSettingsFactory.createAndRegister(); 78 mSmartSuggestionsHelper = new SmartSuggestionsHelper(this, mSettings); 79 mSmsHelper = new SmsHelper(this); 80 mSmsHelper.initialize(); 81 } 82 83 @Override onDestroy()84 public void onDestroy() { 85 // This null check is only for the unit tests as ServiceTestCase.tearDown calls onDestroy 86 // without having first called onCreate. 87 if (mSmsHelper != null) { 88 mSmsHelper.destroy(); 89 } 90 super.onDestroy(); 91 } 92 93 @Override onNotificationEnqueued(StatusBarNotification sbn)94 public Adjustment onNotificationEnqueued(StatusBarNotification sbn) { 95 // we use the version with channel, so this is never called. 96 return null; 97 } 98 99 @Override onNotificationEnqueued(StatusBarNotification sbn, NotificationChannel channel)100 public Adjustment onNotificationEnqueued(StatusBarNotification sbn, 101 NotificationChannel channel) { 102 if (DEBUG) Log.i(TAG, "ENQUEUED " + sbn.getKey() + " on " + channel.getId()); 103 if (!isForCurrentUser(sbn)) { 104 return null; 105 } 106 mSingleThreadExecutor.submit(() -> { 107 NotificationEntry entry = 108 new NotificationEntry(this, mPackageManager, sbn, channel, mSmsHelper); 109 SmartSuggestions suggestions = mSmartSuggestionsHelper.onNotificationEnqueued(sbn); 110 if (DEBUG) { 111 Log.d(TAG, String.format( 112 "Creating Adjustment for %s, with %d actions, and %d replies.", 113 sbn.getKey(), 114 suggestions.getActions().size(), 115 suggestions.getReplies().size())); 116 } 117 Adjustment adjustment = createEnqueuedNotificationAdjustment( 118 entry, 119 new ArrayList<Notification.Action>(suggestions.getActions()), 120 new ArrayList<>(suggestions.getReplies())); 121 adjustNotification(adjustment); 122 }); 123 return null; 124 } 125 126 /** A convenience helper for creating an adjustment for an SBN. */ 127 @VisibleForTesting 128 @Nullable createEnqueuedNotificationAdjustment( @onNull NotificationEntry entry, @NonNull ArrayList<Notification.Action> smartActions, @NonNull ArrayList<CharSequence> smartReplies)129 Adjustment createEnqueuedNotificationAdjustment( 130 @NonNull NotificationEntry entry, 131 @NonNull ArrayList<Notification.Action> smartActions, 132 @NonNull ArrayList<CharSequence> smartReplies) { 133 Bundle signals = new Bundle(); 134 135 if (!smartActions.isEmpty()) { 136 signals.putParcelableArrayList(Adjustment.KEY_CONTEXTUAL_ACTIONS, smartActions); 137 } 138 if (!smartReplies.isEmpty()) { 139 signals.putCharSequenceArrayList(Adjustment.KEY_TEXT_REPLIES, smartReplies); 140 } 141 142 return new Adjustment( 143 entry.getSbn().getPackageName(), 144 entry.getSbn().getKey(), 145 signals, 146 "", 147 entry.getSbn().getUserId()); 148 } 149 150 @Override onNotificationPosted(StatusBarNotification sbn, RankingMap rankingMap)151 public void onNotificationPosted(StatusBarNotification sbn, RankingMap rankingMap) { 152 if (DEBUG) Log.i(TAG, "POSTED " + sbn.getKey()); 153 try { 154 if (!isForCurrentUser(sbn)) { 155 return; 156 } 157 Ranking ranking = new Ranking(); 158 rankingMap.getRanking(sbn.getKey(), ranking); 159 if (ranking != null && ranking.getChannel() != null) { 160 NotificationEntry entry = new NotificationEntry(this, mPackageManager, 161 sbn, ranking.getChannel(), mSmsHelper); 162 mLiveNotifications.put(sbn.getKey(), entry); 163 } 164 } catch (Throwable e) { 165 Log.e(TAG, "Error occurred processing post", e); 166 } 167 } 168 169 @Override onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap, NotificationStats stats, int reason)170 public void onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap, 171 NotificationStats stats, int reason) { 172 try { 173 if (!isForCurrentUser(sbn)) { 174 return; 175 } 176 177 mLiveNotifications.remove(sbn.getKey()); 178 179 } catch (Throwable e) { 180 Log.e(TAG, "Error occurred processing removal of " + sbn.getKey(), e); 181 } 182 } 183 184 @Override onNotificationSnoozedUntilContext(StatusBarNotification sbn, String snoozeCriterionId)185 public void onNotificationSnoozedUntilContext(StatusBarNotification sbn, 186 String snoozeCriterionId) { 187 } 188 189 @Override onNotificationsSeen(List<String> keys)190 public void onNotificationsSeen(List<String> keys) { 191 } 192 193 @Override onNotificationExpansionChanged(@onNull String key, boolean isUserAction, boolean isExpanded)194 public void onNotificationExpansionChanged(@NonNull String key, boolean isUserAction, 195 boolean isExpanded) { 196 if (DEBUG) { 197 Log.d(TAG, "onNotificationExpansionChanged() called with: key = [" + key 198 + "], isUserAction = [" + isUserAction + "], isExpanded = [" + isExpanded 199 + "]"); 200 } 201 NotificationEntry entry = mLiveNotifications.get(key); 202 203 if (entry != null) { 204 mSingleThreadExecutor.submit( 205 () -> mSmartSuggestionsHelper.onNotificationExpansionChanged( 206 entry.getSbn(), isExpanded)); 207 } 208 } 209 210 @Override onNotificationDirectReplied(@onNull String key)211 public void onNotificationDirectReplied(@NonNull String key) { 212 if (DEBUG) Log.i(TAG, "onNotificationDirectReplied " + key); 213 mSingleThreadExecutor.submit(() -> mSmartSuggestionsHelper.onNotificationDirectReplied(key)); 214 } 215 216 @Override onSuggestedReplySent(@onNull String key, @NonNull CharSequence reply, int source)217 public void onSuggestedReplySent(@NonNull String key, @NonNull CharSequence reply, 218 int source) { 219 if (DEBUG) { 220 Log.d(TAG, "onSuggestedReplySent() called with: key = [" + key + "], reply = [" + reply 221 + "], source = [" + source + "]"); 222 } 223 mSingleThreadExecutor.submit( 224 () -> mSmartSuggestionsHelper.onSuggestedReplySent(key, reply, source)); 225 } 226 227 @Override onActionInvoked(@onNull String key, @NonNull Notification.Action action, int source)228 public void onActionInvoked(@NonNull String key, @NonNull Notification.Action action, 229 int source) { 230 if (DEBUG) { 231 Log.d(TAG, 232 "onActionInvoked() called with: key = [" + key + "], action = [" + action.title 233 + "], source = [" + source + "]"); 234 } 235 mSingleThreadExecutor.submit( 236 () -> mSmartSuggestionsHelper.onActionClicked(key, action, source)); 237 } 238 239 @Override onListenerConnected()240 public void onListenerConnected() { 241 if (DEBUG) Log.i(TAG, "Connected"); 242 } 243 244 @Override onListenerDisconnected()245 public void onListenerDisconnected() { 246 } 247 isForCurrentUser(StatusBarNotification sbn)248 private boolean isForCurrentUser(StatusBarNotification sbn) { 249 return sbn != null && sbn.getUserId() == UserHandle.myUserId(); 250 } 251 } 252