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