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