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