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