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 static com.android.systemui.Dependency.BG_LOOPER_NAME; 20 21 import android.app.AppOpsManager; 22 import android.content.Context; 23 import android.os.Handler; 24 import android.os.Looper; 25 import android.os.UserHandle; 26 import android.util.ArrayMap; 27 import android.util.ArraySet; 28 import android.util.Log; 29 30 import com.android.internal.annotations.GuardedBy; 31 import com.android.internal.annotations.VisibleForTesting; 32 import com.android.systemui.Dumpable; 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.Named; 42 import javax.inject.Singleton; 43 44 /** 45 * Controller to keep track of applications that have requested access to given App Ops 46 * 47 * It can be subscribed to with callbacks. Additionally, it passes on the information to 48 * NotificationPresenter to be displayed to the user. 49 */ 50 @Singleton 51 public class AppOpsControllerImpl implements AppOpsController, 52 AppOpsManager.OnOpActiveChangedListener, 53 AppOpsManager.OnOpNotedListener, Dumpable { 54 55 private static final long NOTED_OP_TIME_DELAY_MS = 5000; 56 private static final String TAG = "AppOpsControllerImpl"; 57 private static final boolean DEBUG = false; 58 private final Context mContext; 59 60 private final AppOpsManager mAppOps; 61 private H mBGHandler; 62 private final List<AppOpsController.Callback> mCallbacks = new ArrayList<>(); 63 private final ArrayMap<Integer, Set<Callback>> mCallbacksByCode = new ArrayMap<>(); 64 65 @GuardedBy("mActiveItems") 66 private final List<AppOpItem> mActiveItems = new ArrayList<>(); 67 @GuardedBy("mNotedItems") 68 private final List<AppOpItem> mNotedItems = new ArrayList<>(); 69 70 protected static final int[] OPS = new int[] { 71 AppOpsManager.OP_CAMERA, 72 AppOpsManager.OP_SYSTEM_ALERT_WINDOW, 73 AppOpsManager.OP_RECORD_AUDIO, 74 AppOpsManager.OP_COARSE_LOCATION, 75 AppOpsManager.OP_FINE_LOCATION 76 }; 77 78 @Inject AppOpsControllerImpl(Context context, @Named(BG_LOOPER_NAME) Looper bgLooper)79 public AppOpsControllerImpl(Context context, @Named(BG_LOOPER_NAME) Looper bgLooper) { 80 mContext = context; 81 mAppOps = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE); 82 mBGHandler = new H(bgLooper); 83 final int numOps = OPS.length; 84 for (int i = 0; i < numOps; i++) { 85 mCallbacksByCode.put(OPS[i], new ArraySet<>()); 86 } 87 } 88 89 @VisibleForTesting setBGHandler(H handler)90 protected void setBGHandler(H handler) { 91 mBGHandler = handler; 92 } 93 94 @VisibleForTesting setListening(boolean listening)95 protected void setListening(boolean listening) { 96 if (listening) { 97 mAppOps.startWatchingActive(OPS, this); 98 mAppOps.startWatchingNoted(OPS, this); 99 } else { 100 mAppOps.stopWatchingActive(this); 101 mAppOps.stopWatchingNoted(this); 102 mBGHandler.removeCallbacksAndMessages(null); // null removes all 103 synchronized (mActiveItems) { 104 mActiveItems.clear(); 105 } 106 synchronized (mNotedItems) { 107 mNotedItems.clear(); 108 } 109 } 110 } 111 112 /** 113 * Adds a callback that will get notifified when an AppOp of the type the controller tracks 114 * changes 115 * 116 * @param callback Callback to report changes 117 * @param opsCodes App Ops the callback is interested in checking 118 * 119 * @see #removeCallback(int[], Callback) 120 */ 121 @Override addCallback(int[] opsCodes, AppOpsController.Callback callback)122 public void addCallback(int[] opsCodes, AppOpsController.Callback callback) { 123 boolean added = false; 124 final int numCodes = opsCodes.length; 125 for (int i = 0; i < numCodes; i++) { 126 if (mCallbacksByCode.containsKey(opsCodes[i])) { 127 mCallbacksByCode.get(opsCodes[i]).add(callback); 128 added = true; 129 } else { 130 if (DEBUG) Log.wtf(TAG, "APP_OP " + opsCodes[i] + " not supported"); 131 } 132 } 133 if (added) mCallbacks.add(callback); 134 if (!mCallbacks.isEmpty()) setListening(true); 135 } 136 137 /** 138 * Removes a callback from those notified when an AppOp of the type the controller tracks 139 * changes 140 * 141 * @param callback Callback to stop reporting changes 142 * @param opsCodes App Ops the callback was interested in checking 143 * 144 * @see #addCallback(int[], Callback) 145 */ 146 @Override removeCallback(int[] opsCodes, AppOpsController.Callback callback)147 public void removeCallback(int[] opsCodes, AppOpsController.Callback callback) { 148 final int numCodes = opsCodes.length; 149 for (int i = 0; i < numCodes; i++) { 150 if (mCallbacksByCode.containsKey(opsCodes[i])) { 151 mCallbacksByCode.get(opsCodes[i]).remove(callback); 152 } 153 } 154 mCallbacks.remove(callback); 155 if (mCallbacks.isEmpty()) setListening(false); 156 } 157 getAppOpItem(List<AppOpItem> appOpList, int code, int uid, String packageName)158 private AppOpItem getAppOpItem(List<AppOpItem> appOpList, int code, int uid, 159 String packageName) { 160 final int itemsQ = appOpList.size(); 161 for (int i = 0; i < itemsQ; i++) { 162 AppOpItem item = appOpList.get(i); 163 if (item.getCode() == code && item.getUid() == uid 164 && item.getPackageName().equals(packageName)) { 165 return item; 166 } 167 } 168 return null; 169 } 170 updateActives(int code, int uid, String packageName, boolean active)171 private boolean updateActives(int code, int uid, String packageName, boolean active) { 172 synchronized (mActiveItems) { 173 AppOpItem item = getAppOpItem(mActiveItems, code, uid, packageName); 174 if (item == null && active) { 175 item = new AppOpItem(code, uid, packageName, System.currentTimeMillis()); 176 mActiveItems.add(item); 177 if (DEBUG) Log.w(TAG, "Added item: " + item.toString()); 178 return true; 179 } else if (item != null && !active) { 180 mActiveItems.remove(item); 181 if (DEBUG) Log.w(TAG, "Removed item: " + item.toString()); 182 return true; 183 } 184 return false; 185 } 186 } 187 removeNoted(int code, int uid, String packageName)188 private void removeNoted(int code, int uid, String packageName) { 189 AppOpItem item; 190 synchronized (mNotedItems) { 191 item = getAppOpItem(mNotedItems, code, uid, packageName); 192 if (item == null) return; 193 mNotedItems.remove(item); 194 if (DEBUG) Log.w(TAG, "Removed item: " + item.toString()); 195 } 196 notifySuscribers(code, uid, packageName, false); 197 } 198 addNoted(int code, int uid, String packageName)199 private void addNoted(int code, int uid, String packageName) { 200 AppOpItem item; 201 synchronized (mNotedItems) { 202 item = getAppOpItem(mNotedItems, code, uid, packageName); 203 if (item == null) { 204 item = new AppOpItem(code, uid, packageName, System.currentTimeMillis()); 205 mNotedItems.add(item); 206 if (DEBUG) Log.w(TAG, "Added item: " + item.toString()); 207 } 208 } 209 mBGHandler.scheduleRemoval(item, NOTED_OP_TIME_DELAY_MS); 210 } 211 212 /** 213 * Returns a copy of the list containing all the active AppOps that the controller tracks. 214 * 215 * @return List of active AppOps information 216 */ getActiveAppOps()217 public List<AppOpItem> getActiveAppOps() { 218 return getActiveAppOpsForUser(UserHandle.USER_ALL); 219 } 220 221 /** 222 * Returns a copy of the list containing all the active AppOps that the controller tracks, for 223 * a given user id. 224 * 225 * @param userId User id to track, can be {@link UserHandle#USER_ALL} 226 * 227 * @return List of active AppOps information for that user id 228 */ getActiveAppOpsForUser(int userId)229 public List<AppOpItem> getActiveAppOpsForUser(int userId) { 230 List<AppOpItem> list = new ArrayList<>(); 231 synchronized (mActiveItems) { 232 final int numActiveItems = mActiveItems.size(); 233 for (int i = 0; i < numActiveItems; i++) { 234 AppOpItem item = mActiveItems.get(i); 235 if ((userId == UserHandle.USER_ALL 236 || UserHandle.getUserId(item.getUid()) == userId)) { 237 list.add(item); 238 } 239 } 240 } 241 synchronized (mNotedItems) { 242 final int numNotedItems = mNotedItems.size(); 243 for (int i = 0; i < numNotedItems; i++) { 244 AppOpItem item = mNotedItems.get(i); 245 if ((userId == UserHandle.USER_ALL 246 || UserHandle.getUserId(item.getUid()) == userId)) { 247 list.add(item); 248 } 249 } 250 } 251 return list; 252 } 253 254 @Override onOpActiveChanged(int code, int uid, String packageName, boolean active)255 public void onOpActiveChanged(int code, int uid, String packageName, boolean active) { 256 if (updateActives(code, uid, packageName, active)) { 257 notifySuscribers(code, uid, packageName, active); 258 } 259 } 260 261 @Override onOpNoted(int code, int uid, String packageName, int result)262 public void onOpNoted(int code, int uid, String packageName, int result) { 263 if (DEBUG) { 264 Log.w(TAG, "Op: " + code + " with result " + AppOpsManager.MODE_NAMES[result]); 265 } 266 if (result != AppOpsManager.MODE_ALLOWED) return; 267 addNoted(code, uid, packageName); 268 notifySuscribers(code, uid, packageName, true); 269 } 270 notifySuscribers(int code, int uid, String packageName, boolean active)271 private void notifySuscribers(int code, int uid, String packageName, boolean active) { 272 if (mCallbacksByCode.containsKey(code)) { 273 for (Callback cb: mCallbacksByCode.get(code)) { 274 cb.onActiveStateChanged(code, uid, packageName, active); 275 } 276 } 277 } 278 279 @Override dump(FileDescriptor fd, PrintWriter pw, String[] args)280 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 281 pw.println("AppOpsController state:"); 282 pw.println(" Active Items:"); 283 for (int i = 0; i < mActiveItems.size(); i++) { 284 final AppOpItem item = mActiveItems.get(i); 285 pw.print(" "); pw.println(item.toString()); 286 } 287 pw.println(" Noted Items:"); 288 for (int i = 0; i < mNotedItems.size(); i++) { 289 final AppOpItem item = mNotedItems.get(i); 290 pw.print(" "); pw.println(item.toString()); 291 } 292 293 } 294 295 protected final class H extends Handler { H(Looper looper)296 H(Looper looper) { 297 super(looper); 298 } 299 scheduleRemoval(AppOpItem item, long timeToRemoval)300 public void scheduleRemoval(AppOpItem item, long timeToRemoval) { 301 removeCallbacksAndMessages(item); 302 postDelayed(new Runnable() { 303 @Override 304 public void run() { 305 removeNoted(item.getCode(), item.getUid(), item.getPackageName()); 306 } 307 }, item, timeToRemoval); 308 } 309 } 310 } 311