1 /*
2  * Copyright (C) 2020 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.server.notification;
18 
19 import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_CACHED;
20 import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_DYNAMIC;
21 import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED_BY_ANY_LAUNCHER;
22 
23 import android.annotation.NonNull;
24 import android.content.IntentFilter;
25 import android.content.pm.LauncherApps;
26 import android.content.pm.ShortcutInfo;
27 import android.content.pm.ShortcutServiceInternal;
28 import android.os.Binder;
29 import android.os.Handler;
30 import android.os.UserHandle;
31 import android.text.TextUtils;
32 import android.util.Slog;
33 
34 import com.android.internal.annotations.VisibleForTesting;
35 
36 import java.util.ArrayList;
37 import java.util.Arrays;
38 import java.util.Collections;
39 import java.util.HashMap;
40 import java.util.HashSet;
41 import java.util.List;
42 import java.util.Set;
43 
44 /**
45  * Helper for querying shortcuts.
46  */
47 public class ShortcutHelper {
48     private static final String TAG = "ShortcutHelper";
49 
50     private static final IntentFilter SHARING_FILTER = new IntentFilter();
51     static {
52         try {
53             SHARING_FILTER.addDataType("*/*");
54         } catch (IntentFilter.MalformedMimeTypeException e) {
55             Slog.e(TAG, "Bad mime type", e);
56         }
57     }
58 
59     /**
60      * Listener to call when a shortcut we're tracking has been removed.
61      */
62     interface ShortcutListener {
onShortcutRemoved(String key)63         void onShortcutRemoved(String key);
64     }
65 
66     private LauncherApps mLauncherAppsService;
67     private ShortcutListener mShortcutListener;
68     private ShortcutServiceInternal mShortcutServiceInternal;
69 
70     // Key: packageName Value: <shortcutId, notifId>
71     private HashMap<String, HashMap<String, String>> mActiveShortcutBubbles = new HashMap<>();
72     private boolean mLauncherAppsCallbackRegistered;
73 
74     // Bubbles can be created based on a shortcut, we need to listen for changes to
75     // that shortcut so that we may update the bubble appropriately.
76     private final LauncherApps.Callback mLauncherAppsCallback = new LauncherApps.Callback() {
77         @Override
78         public void onPackageRemoved(String packageName, UserHandle user) {
79         }
80 
81         @Override
82         public void onPackageAdded(String packageName, UserHandle user) {
83         }
84 
85         @Override
86         public void onPackageChanged(String packageName, UserHandle user) {
87         }
88 
89         @Override
90         public void onPackagesAvailable(String[] packageNames, UserHandle user,
91                 boolean replacing) {
92         }
93 
94         @Override
95         public void onPackagesUnavailable(String[] packageNames, UserHandle user,
96                 boolean replacing) {
97         }
98 
99         @Override
100         public void onShortcutsChanged(@NonNull String packageName,
101                 @NonNull List<ShortcutInfo> shortcuts, @NonNull UserHandle user) {
102             HashMap<String, String> shortcutBubbles = mActiveShortcutBubbles.get(packageName);
103             ArrayList<String> bubbleKeysToRemove = new ArrayList<>();
104             if (shortcutBubbles != null) {
105                 // Copy to avoid a concurrent modification exception when we remove bubbles from
106                 // shortcutBubbles.
107                 final Set<String> shortcutIds = new HashSet<>(shortcutBubbles.keySet());
108 
109                 // If we can't find one of our bubbles in the shortcut list, that bubble needs
110                 // to be removed.
111                 for (String shortcutId : shortcutIds) {
112                     boolean foundShortcut = false;
113                     for (int i = 0; i < shortcuts.size(); i++) {
114                         if (shortcuts.get(i).getId().equals(shortcutId)) {
115                             foundShortcut = true;
116                             break;
117                         }
118                     }
119                     if (!foundShortcut) {
120                         bubbleKeysToRemove.add(shortcutBubbles.get(shortcutId));
121                         shortcutBubbles.remove(shortcutId);
122                         if (shortcutBubbles.isEmpty()) {
123                             mActiveShortcutBubbles.remove(packageName);
124                             if (mLauncherAppsCallbackRegistered
125                                     && mActiveShortcutBubbles.isEmpty()) {
126                                 mLauncherAppsService.unregisterCallback(mLauncherAppsCallback);
127                                 mLauncherAppsCallbackRegistered = false;
128                             }
129                         }
130                     }
131                 }
132             }
133 
134             // Let NoMan know about the updates
135             for (int i = 0; i < bubbleKeysToRemove.size(); i++) {
136                 // update flag bubble
137                 String bubbleKey = bubbleKeysToRemove.get(i);
138                 if (mShortcutListener != null) {
139                     mShortcutListener.onShortcutRemoved(bubbleKey);
140                 }
141             }
142         }
143     };
144 
ShortcutHelper(LauncherApps launcherApps, ShortcutListener listener, ShortcutServiceInternal shortcutServiceInternal)145     ShortcutHelper(LauncherApps launcherApps, ShortcutListener listener,
146             ShortcutServiceInternal shortcutServiceInternal) {
147         mLauncherAppsService = launcherApps;
148         mShortcutListener = listener;
149         mShortcutServiceInternal = shortcutServiceInternal;
150     }
151 
152     @VisibleForTesting
setLauncherApps(LauncherApps launcherApps)153     void setLauncherApps(LauncherApps launcherApps) {
154         mLauncherAppsService = launcherApps;
155     }
156 
157     @VisibleForTesting
setShortcutServiceInternal(ShortcutServiceInternal shortcutServiceInternal)158     void setShortcutServiceInternal(ShortcutServiceInternal shortcutServiceInternal) {
159         mShortcutServiceInternal = shortcutServiceInternal;
160     }
161 
162     /**
163      * Returns whether the given shortcut info is a conversation shortcut.
164      */
isConversationShortcut( ShortcutInfo shortcutInfo, ShortcutServiceInternal mShortcutServiceInternal, int callingUserId)165     public static boolean isConversationShortcut(
166             ShortcutInfo shortcutInfo, ShortcutServiceInternal mShortcutServiceInternal,
167             int callingUserId) {
168         if (shortcutInfo == null || !shortcutInfo.isLongLived() || !shortcutInfo.isEnabled()) {
169             return false;
170         }
171         // TODO (b/155016294) uncomment when sharing shortcuts are required
172         /*
173         mShortcutServiceInternal.isSharingShortcut(callingUserId, "android",
174                 shortcutInfo.getPackage(), shortcutInfo.getId(), shortcutInfo.getUserId(),
175                 SHARING_FILTER);
176          */
177         return true;
178     }
179 
180     /**
181      * Only returns shortcut info if it's found and if it's a conversation shortcut.
182      */
getValidShortcutInfo(String shortcutId, String packageName, UserHandle user)183     ShortcutInfo getValidShortcutInfo(String shortcutId, String packageName, UserHandle user) {
184         if (mLauncherAppsService == null) {
185             return null;
186         }
187         final long token = Binder.clearCallingIdentity();
188         try {
189             if (shortcutId == null || packageName == null || user == null) {
190                 return null;
191             }
192             LauncherApps.ShortcutQuery query = new LauncherApps.ShortcutQuery();
193             query.setPackage(packageName);
194             query.setShortcutIds(Arrays.asList(shortcutId));
195             query.setQueryFlags(
196                     FLAG_MATCH_DYNAMIC | FLAG_MATCH_PINNED_BY_ANY_LAUNCHER | FLAG_MATCH_CACHED);
197             List<ShortcutInfo> shortcuts = mLauncherAppsService.getShortcuts(query, user);
198             ShortcutInfo info = shortcuts != null && shortcuts.size() > 0
199                     ? shortcuts.get(0)
200                     : null;
201             if (isConversationShortcut(info, mShortcutServiceInternal, user.getIdentifier())) {
202                 return info;
203             }
204             return null;
205         } finally {
206             Binder.restoreCallingIdentity(token);
207         }
208     }
209 
210     /**
211      * Caches the given shortcut in Shortcut Service.
212      */
cacheShortcut(ShortcutInfo shortcutInfo, UserHandle user)213     void cacheShortcut(ShortcutInfo shortcutInfo, UserHandle user) {
214         if (shortcutInfo.isLongLived() && !shortcutInfo.isCached()) {
215             mShortcutServiceInternal.cacheShortcuts(user.getIdentifier(), "android",
216                     shortcutInfo.getPackage(), Collections.singletonList(shortcutInfo.getId()),
217                     shortcutInfo.getUserId(), ShortcutInfo.FLAG_CACHED_NOTIFICATIONS);
218         }
219     }
220 
221     /**
222      * Shortcut based bubbles require some extra work to listen for shortcut changes.
223      *
224      * @param r the notification record to check
225      * @param removedNotification true if this notification is being removed
226      * @param handler handler to register the callback with
227      */
maybeListenForShortcutChangesForBubbles(NotificationRecord r, boolean removedNotification, Handler handler)228     void maybeListenForShortcutChangesForBubbles(NotificationRecord r,
229             boolean removedNotification,
230             Handler handler) {
231         final String shortcutId = r.getNotification().getBubbleMetadata() != null
232                 ? r.getNotification().getBubbleMetadata().getShortcutId()
233                 : null;
234         if (!removedNotification
235                 && !TextUtils.isEmpty(shortcutId)
236                 && r.getShortcutInfo() != null
237                 && r.getShortcutInfo().getId().equals(shortcutId)) {
238             // Must track shortcut based bubbles in case the shortcut is removed
239             HashMap<String, String> packageBubbles = mActiveShortcutBubbles.get(
240                     r.getSbn().getPackageName());
241             if (packageBubbles == null) {
242                 packageBubbles = new HashMap<>();
243             }
244             packageBubbles.put(shortcutId, r.getKey());
245             mActiveShortcutBubbles.put(r.getSbn().getPackageName(), packageBubbles);
246             if (!mLauncherAppsCallbackRegistered) {
247                 mLauncherAppsService.registerCallback(mLauncherAppsCallback, handler);
248                 mLauncherAppsCallbackRegistered = true;
249             }
250         } else {
251             // No longer track shortcut
252             HashMap<String, String> packageBubbles = mActiveShortcutBubbles.get(
253                     r.getSbn().getPackageName());
254             if (packageBubbles != null) {
255                 if (!TextUtils.isEmpty(shortcutId)) {
256                     packageBubbles.remove(shortcutId);
257                 } else {
258                     // Copy the shortcut IDs to avoid a concurrent modification exception.
259                     final Set<String> shortcutIds = new HashSet<>(packageBubbles.keySet());
260 
261                     // Check if there was a matching entry
262                     for (String pkgShortcutId : shortcutIds) {
263                         String entryKey = packageBubbles.get(pkgShortcutId);
264                         if (r.getKey().equals(entryKey)) {
265                             // No longer has shortcut id so remove it
266                             packageBubbles.remove(pkgShortcutId);
267                         }
268                     }
269                 }
270                 if (packageBubbles.isEmpty()) {
271                     mActiveShortcutBubbles.remove(r.getSbn().getPackageName());
272                 }
273             }
274             if (mLauncherAppsCallbackRegistered && mActiveShortcutBubbles.isEmpty()) {
275                 mLauncherAppsService.unregisterCallback(mLauncherAppsCallback);
276                 mLauncherAppsCallbackRegistered = false;
277             }
278         }
279     }
280 }
281