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