1 /* 2 * Copyright (C) 2018 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.systemui.appops; 18 19 import android.app.AppOpsManager; 20 import android.content.Context; 21 import android.os.Handler; 22 import android.os.Looper; 23 import android.os.UserHandle; 24 import android.util.ArrayMap; 25 import android.util.ArraySet; 26 import android.util.Log; 27 28 import com.android.internal.annotations.GuardedBy; 29 import com.android.internal.annotations.VisibleForTesting; 30 import com.android.systemui.Dumpable; 31 import com.android.systemui.dagger.qualifiers.Background; 32 import com.android.systemui.dump.DumpManager; 33 34 import java.io.FileDescriptor; 35 import java.io.PrintWriter; 36 import java.util.ArrayList; 37 import java.util.List; 38 import java.util.Set; 39 40 import javax.inject.Inject; 41 import javax.inject.Singleton; 42 43 /** 44 * Controller to keep track of applications that have requested access to given App Ops 45 * 46 * It can be subscribed to with callbacks. Additionally, it passes on the information to 47 * NotificationPresenter to be displayed to the user. 48 */ 49 @Singleton 50 public class AppOpsControllerImpl implements AppOpsController, 51 AppOpsManager.OnOpActiveChangedInternalListener, 52 AppOpsManager.OnOpNotedListener, Dumpable { 53 54 // This is the minimum time that we will keep AppOps that are noted on record. If multiple 55 // occurrences of the same (op, package, uid) happen in a shorter interval, they will not be 56 // notified to listeners. 57 private static final long NOTED_OP_TIME_DELAY_MS = 5000; 58 private static final String TAG = "AppOpsControllerImpl"; 59 private static final boolean DEBUG = false; 60 private final Context mContext; 61 62 private final AppOpsManager mAppOps; 63 private H mBGHandler; 64 private final List<AppOpsController.Callback> mCallbacks = new ArrayList<>(); 65 private final ArrayMap<Integer, Set<Callback>> mCallbacksByCode = new ArrayMap<>(); 66 private boolean mListening; 67 68 @GuardedBy("mActiveItems") 69 private final List<AppOpItem> mActiveItems = new ArrayList<>(); 70 @GuardedBy("mNotedItems") 71 private final List<AppOpItem> mNotedItems = new ArrayList<>(); 72 73 protected static final int[] OPS = new int[] { 74 AppOpsManager.OP_CAMERA, 75 AppOpsManager.OP_SYSTEM_ALERT_WINDOW, 76 AppOpsManager.OP_RECORD_AUDIO, 77 AppOpsManager.OP_COARSE_LOCATION, 78 AppOpsManager.OP_FINE_LOCATION 79 }; 80 81 @Inject AppOpsControllerImpl( Context context, @Background Looper bgLooper, DumpManager dumpManager)82 public AppOpsControllerImpl( 83 Context context, 84 @Background Looper bgLooper, 85 DumpManager dumpManager) { 86 mContext = context; 87 mAppOps = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE); 88 mBGHandler = new H(bgLooper); 89 final int numOps = OPS.length; 90 for (int i = 0; i < numOps; i++) { 91 mCallbacksByCode.put(OPS[i], new ArraySet<>()); 92 } 93 dumpManager.registerDumpable(TAG, this); 94 } 95 96 @VisibleForTesting setBGHandler(H handler)97 protected void setBGHandler(H handler) { 98 mBGHandler = handler; 99 } 100 101 @VisibleForTesting setListening(boolean listening)102 protected void setListening(boolean listening) { 103 mListening = listening; 104 if (listening) { 105 mAppOps.startWatchingActive(OPS, this); 106 mAppOps.startWatchingNoted(OPS, this); 107 } else { 108 mAppOps.stopWatchingActive(this); 109 mAppOps.stopWatchingNoted(this); 110 mBGHandler.removeCallbacksAndMessages(null); // null removes all 111 synchronized (mActiveItems) { 112 mActiveItems.clear(); 113 } 114 synchronized (mNotedItems) { 115 mNotedItems.clear(); 116 } 117 } 118 } 119 120 /** 121 * Adds a callback that will get notifified when an AppOp of the type the controller tracks 122 * changes 123 * 124 * @param callback Callback to report changes 125 * @param opsCodes App Ops the callback is interested in checking 126 * 127 * @see #removeCallback(int[], Callback) 128 */ 129 @Override addCallback(int[] opsCodes, AppOpsController.Callback callback)130 public void addCallback(int[] opsCodes, AppOpsController.Callback callback) { 131 boolean added = false; 132 final int numCodes = opsCodes.length; 133 for (int i = 0; i < numCodes; i++) { 134 if (mCallbacksByCode.containsKey(opsCodes[i])) { 135 mCallbacksByCode.get(opsCodes[i]).add(callback); 136 added = true; 137 } else { 138 if (DEBUG) Log.wtf(TAG, "APP_OP " + opsCodes[i] + " not supported"); 139 } 140 } 141 if (added) mCallbacks.add(callback); 142 if (!mCallbacks.isEmpty()) setListening(true); 143 } 144 145 /** 146 * Removes a callback from those notified when an AppOp of the type the controller tracks 147 * changes 148 * 149 * @param callback Callback to stop reporting changes 150 * @param opsCodes App Ops the callback was interested in checking 151 * 152 * @see #addCallback(int[], Callback) 153 */ 154 @Override removeCallback(int[] opsCodes, AppOpsController.Callback callback)155 public void removeCallback(int[] opsCodes, AppOpsController.Callback callback) { 156 final int numCodes = opsCodes.length; 157 for (int i = 0; i < numCodes; i++) { 158 if (mCallbacksByCode.containsKey(opsCodes[i])) { 159 mCallbacksByCode.get(opsCodes[i]).remove(callback); 160 } 161 } 162 mCallbacks.remove(callback); 163 if (mCallbacks.isEmpty()) setListening(false); 164 } 165 166 // Find item number in list, only call if the list passed is locked getAppOpItemLocked(List<AppOpItem> appOpList, int code, int uid, String packageName)167 private AppOpItem getAppOpItemLocked(List<AppOpItem> appOpList, int code, int uid, 168 String packageName) { 169 final int itemsQ = appOpList.size(); 170 for (int i = 0; i < itemsQ; i++) { 171 AppOpItem item = appOpList.get(i); 172 if (item.getCode() == code && item.getUid() == uid 173 && item.getPackageName().equals(packageName)) { 174 return item; 175 } 176 } 177 return null; 178 } 179 updateActives(int code, int uid, String packageName, boolean active)180 private boolean updateActives(int code, int uid, String packageName, boolean active) { 181 synchronized (mActiveItems) { 182 AppOpItem item = getAppOpItemLocked(mActiveItems, code, uid, packageName); 183 if (item == null && active) { 184 item = new AppOpItem(code, uid, packageName, System.currentTimeMillis()); 185 mActiveItems.add(item); 186 if (DEBUG) Log.w(TAG, "Added item: " + item.toString()); 187 return true; 188 } else if (item != null && !active) { 189 mActiveItems.remove(item); 190 if (DEBUG) Log.w(TAG, "Removed item: " + item.toString()); 191 return true; 192 } 193 return false; 194 } 195 } 196 removeNoted(int code, int uid, String packageName)197 private void removeNoted(int code, int uid, String packageName) { 198 AppOpItem item; 199 synchronized (mNotedItems) { 200 item = getAppOpItemLocked(mNotedItems, code, uid, packageName); 201 if (item == null) return; 202 mNotedItems.remove(item); 203 if (DEBUG) Log.w(TAG, "Removed item: " + item.toString()); 204 } 205 boolean active; 206 // Check if the item is also active 207 synchronized (mActiveItems) { 208 active = getAppOpItemLocked(mActiveItems, code, uid, packageName) != null; 209 } 210 if (!active) { 211 notifySuscribers(code, uid, packageName, false); 212 } 213 } 214 addNoted(int code, int uid, String packageName)215 private boolean addNoted(int code, int uid, String packageName) { 216 AppOpItem item; 217 boolean createdNew = false; 218 synchronized (mNotedItems) { 219 item = getAppOpItemLocked(mNotedItems, code, uid, packageName); 220 if (item == null) { 221 item = new AppOpItem(code, uid, packageName, System.currentTimeMillis()); 222 mNotedItems.add(item); 223 if (DEBUG) Log.w(TAG, "Added item: " + item.toString()); 224 createdNew = true; 225 } 226 } 227 // We should keep this so we make sure it cannot time out. 228 mBGHandler.removeCallbacksAndMessages(item); 229 mBGHandler.scheduleRemoval(item, NOTED_OP_TIME_DELAY_MS); 230 return createdNew; 231 } 232 233 /** 234 * Returns a copy of the list containing all the active AppOps that the controller tracks. 235 * 236 * @return List of active AppOps information 237 */ getActiveAppOps()238 public List<AppOpItem> getActiveAppOps() { 239 return getActiveAppOpsForUser(UserHandle.USER_ALL); 240 } 241 242 /** 243 * Returns a copy of the list containing all the active AppOps that the controller tracks, for 244 * a given user id. 245 * 246 * @param userId User id to track, can be {@link UserHandle#USER_ALL} 247 * 248 * @return List of active AppOps information for that user id 249 */ getActiveAppOpsForUser(int userId)250 public List<AppOpItem> getActiveAppOpsForUser(int userId) { 251 List<AppOpItem> list = new ArrayList<>(); 252 synchronized (mActiveItems) { 253 final int numActiveItems = mActiveItems.size(); 254 for (int i = 0; i < numActiveItems; i++) { 255 AppOpItem item = mActiveItems.get(i); 256 if ((userId == UserHandle.USER_ALL 257 || UserHandle.getUserId(item.getUid()) == userId)) { 258 list.add(item); 259 } 260 } 261 } 262 synchronized (mNotedItems) { 263 final int numNotedItems = mNotedItems.size(); 264 for (int i = 0; i < numNotedItems; i++) { 265 AppOpItem item = mNotedItems.get(i); 266 if ((userId == UserHandle.USER_ALL 267 || UserHandle.getUserId(item.getUid()) == userId)) { 268 list.add(item); 269 } 270 } 271 } 272 return list; 273 } 274 275 @Override onOpActiveChanged(int code, int uid, String packageName, boolean active)276 public void onOpActiveChanged(int code, int uid, String packageName, boolean active) { 277 if (DEBUG) { 278 Log.w(TAG, String.format("onActiveChanged(%d,%d,%s,%s", code, uid, packageName, 279 Boolean.toString(active))); 280 } 281 boolean activeChanged = updateActives(code, uid, packageName, active); 282 if (!activeChanged) return; // early return 283 // Check if the item is also noted, in that case, there's no update. 284 boolean alsoNoted; 285 synchronized (mNotedItems) { 286 alsoNoted = getAppOpItemLocked(mNotedItems, code, uid, packageName) != null; 287 } 288 // If active is true, we only send the update if the op is not actively noted (already true) 289 // If active is false, we only send the update if the op is not actively noted (prevent 290 // early removal) 291 if (!alsoNoted) { 292 mBGHandler.post(() -> notifySuscribers(code, uid, packageName, active)); 293 } 294 } 295 296 @Override onOpNoted(int code, int uid, String packageName, int result)297 public void onOpNoted(int code, int uid, String packageName, int result) { 298 if (DEBUG) { 299 Log.w(TAG, "Noted op: " + code + " with result " 300 + AppOpsManager.MODE_NAMES[result] + " for package " + packageName); 301 } 302 if (result != AppOpsManager.MODE_ALLOWED) return; 303 boolean notedAdded = addNoted(code, uid, packageName); 304 if (!notedAdded) return; // early return 305 boolean alsoActive; 306 synchronized (mActiveItems) { 307 alsoActive = getAppOpItemLocked(mActiveItems, code, uid, packageName) != null; 308 } 309 if (!alsoActive) { 310 mBGHandler.post(() -> notifySuscribers(code, uid, packageName, true)); 311 } 312 } 313 notifySuscribers(int code, int uid, String packageName, boolean active)314 private void notifySuscribers(int code, int uid, String packageName, boolean active) { 315 if (mCallbacksByCode.containsKey(code)) { 316 if (DEBUG) Log.d(TAG, "Notifying of change in package " + packageName); 317 for (Callback cb: mCallbacksByCode.get(code)) { 318 cb.onActiveStateChanged(code, uid, packageName, active); 319 } 320 } 321 } 322 323 @Override dump(FileDescriptor fd, PrintWriter pw, String[] args)324 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 325 pw.println("AppOpsController state:"); 326 pw.println(" Listening: " + mListening); 327 pw.println(" Active Items:"); 328 for (int i = 0; i < mActiveItems.size(); i++) { 329 final AppOpItem item = mActiveItems.get(i); 330 pw.print(" "); pw.println(item.toString()); 331 } 332 pw.println(" Noted Items:"); 333 for (int i = 0; i < mNotedItems.size(); i++) { 334 final AppOpItem item = mNotedItems.get(i); 335 pw.print(" "); pw.println(item.toString()); 336 } 337 338 } 339 340 protected class H extends Handler { H(Looper looper)341 H(Looper looper) { 342 super(looper); 343 } 344 scheduleRemoval(AppOpItem item, long timeToRemoval)345 public void scheduleRemoval(AppOpItem item, long timeToRemoval) { 346 removeCallbacksAndMessages(item); 347 postDelayed(new Runnable() { 348 @Override 349 public void run() { 350 removeNoted(item.getCode(), item.getUid(), item.getPackageName()); 351 } 352 }, item, timeToRemoval); 353 } 354 } 355 } 356