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