• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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