blob: ace086f7af2c2ca1237ba584950b6e74744853eb [file] [log] [blame]
Fabian Kozynski1f32cf02018-10-23 12:23:31 -04001/*
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
17package com.android.systemui.appops;
18
Jason Monk196d6392018-12-20 13:25:34 -050019import static com.android.systemui.Dependency.BG_LOOPER_NAME;
20
Fabian Kozynski1f32cf02018-10-23 12:23:31 -040021import android.app.AppOpsManager;
22import android.content.Context;
Philip P. Moltmanne3dbb5a2019-03-07 10:06:11 -080023import android.content.pm.PackageManager;
Fabian Kozynski1f32cf02018-10-23 12:23:31 -040024import android.os.Handler;
25import android.os.Looper;
26import android.os.UserHandle;
27import android.util.ArrayMap;
28import android.util.ArraySet;
29import android.util.Log;
30
31import com.android.internal.annotations.GuardedBy;
32import com.android.internal.annotations.VisibleForTesting;
Fabian Kozynskib7b58792019-01-09 13:07:11 -050033import com.android.systemui.Dumpable;
Fabian Kozynski1f32cf02018-10-23 12:23:31 -040034
Fabian Kozynskib7b58792019-01-09 13:07:11 -050035import java.io.FileDescriptor;
36import java.io.PrintWriter;
Fabian Kozynski1f32cf02018-10-23 12:23:31 -040037import java.util.ArrayList;
38import java.util.List;
39import java.util.Set;
40
Jason Monk196d6392018-12-20 13:25:34 -050041import javax.inject.Inject;
42import javax.inject.Named;
43import javax.inject.Singleton;
44
Fabian Kozynski1f32cf02018-10-23 12:23:31 -040045/**
46 * Controller to keep track of applications that have requested access to given App Ops
47 *
48 * It can be subscribed to with callbacks. Additionally, it passes on the information to
49 * NotificationPresenter to be displayed to the user.
50 */
Jason Monk196d6392018-12-20 13:25:34 -050051@Singleton
Fabian Kozynski1f32cf02018-10-23 12:23:31 -040052public class AppOpsControllerImpl implements AppOpsController,
Fabian Kozynski510585d2018-12-19 13:59:17 -050053 AppOpsManager.OnOpActiveChangedListener,
Fabian Kozynskib7b58792019-01-09 13:07:11 -050054 AppOpsManager.OnOpNotedListener, Dumpable {
Fabian Kozynski1f32cf02018-10-23 12:23:31 -040055
Fabian Kozynski510585d2018-12-19 13:59:17 -050056 private static final long NOTED_OP_TIME_DELAY_MS = 5000;
Fabian Kozynski1f32cf02018-10-23 12:23:31 -040057 private static final String TAG = "AppOpsControllerImpl";
58 private static final boolean DEBUG = false;
59 private final Context mContext;
60
Fabian Kozynski510585d2018-12-19 13:59:17 -050061 private final AppOpsManager mAppOps;
62 private H mBGHandler;
Fabian Kozynski1f32cf02018-10-23 12:23:31 -040063 private final List<AppOpsController.Callback> mCallbacks = new ArrayList<>();
64 private final ArrayMap<Integer, Set<Callback>> mCallbacksByCode = new ArrayMap<>();
Fabian Kozynski510585d2018-12-19 13:59:17 -050065
Fabian Kozynski1f32cf02018-10-23 12:23:31 -040066 @GuardedBy("mActiveItems")
67 private final List<AppOpItem> mActiveItems = new ArrayList<>();
Fabian Kozynski510585d2018-12-19 13:59:17 -050068 @GuardedBy("mNotedItems")
69 private final List<AppOpItem> mNotedItems = new ArrayList<>();
Fabian Kozynski1f32cf02018-10-23 12:23:31 -040070
Fabian Kozynski0b4592c2018-12-20 12:58:45 -050071 protected static final int[] OPS = new int[] {
72 AppOpsManager.OP_CAMERA,
73 AppOpsManager.OP_SYSTEM_ALERT_WINDOW,
74 AppOpsManager.OP_RECORD_AUDIO,
75 AppOpsManager.OP_COARSE_LOCATION,
76 AppOpsManager.OP_FINE_LOCATION
77 };
Fabian Kozynski1f32cf02018-10-23 12:23:31 -040078
Jason Monk196d6392018-12-20 13:25:34 -050079 @Inject
80 public AppOpsControllerImpl(Context context, @Named(BG_LOOPER_NAME) Looper bgLooper) {
Fabian Kozynski1f32cf02018-10-23 12:23:31 -040081 mContext = context;
82 mAppOps = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
83 mBGHandler = new H(bgLooper);
84 final int numOps = OPS.length;
85 for (int i = 0; i < numOps; i++) {
86 mCallbacksByCode.put(OPS[i], new ArraySet<>());
87 }
88 }
89
90 @VisibleForTesting
Fabian Kozynski510585d2018-12-19 13:59:17 -050091 protected void setBGHandler(H handler) {
92 mBGHandler = handler;
93 }
94
95 @VisibleForTesting
Fabian Kozynski1f32cf02018-10-23 12:23:31 -040096 protected void setListening(boolean listening) {
97 if (listening) {
98 mAppOps.startWatchingActive(OPS, this);
Fabian Kozynski0b4592c2018-12-20 12:58:45 -050099 mAppOps.startWatchingNoted(OPS, this);
Fabian Kozynski1f32cf02018-10-23 12:23:31 -0400100 } else {
101 mAppOps.stopWatchingActive(this);
Fabian Kozynski510585d2018-12-19 13:59:17 -0500102 mAppOps.stopWatchingNoted(this);
Fabian Kozynski1f32cf02018-10-23 12:23:31 -0400103 }
104 }
105
106 /**
107 * Adds a callback that will get notifified when an AppOp of the type the controller tracks
108 * changes
109 *
110 * @param callback Callback to report changes
111 * @param opsCodes App Ops the callback is interested in checking
112 *
113 * @see #removeCallback(int[], Callback)
114 */
115 @Override
116 public void addCallback(int[] opsCodes, AppOpsController.Callback callback) {
117 boolean added = false;
118 final int numCodes = opsCodes.length;
119 for (int i = 0; i < numCodes; i++) {
120 if (mCallbacksByCode.containsKey(opsCodes[i])) {
121 mCallbacksByCode.get(opsCodes[i]).add(callback);
122 added = true;
123 } else {
124 if (DEBUG) Log.wtf(TAG, "APP_OP " + opsCodes[i] + " not supported");
125 }
126 }
127 if (added) mCallbacks.add(callback);
128 if (!mCallbacks.isEmpty()) setListening(true);
129 }
130
131 /**
132 * Removes a callback from those notified when an AppOp of the type the controller tracks
133 * changes
134 *
135 * @param callback Callback to stop reporting changes
136 * @param opsCodes App Ops the callback was interested in checking
137 *
138 * @see #addCallback(int[], Callback)
139 */
140 @Override
141 public void removeCallback(int[] opsCodes, AppOpsController.Callback callback) {
142 final int numCodes = opsCodes.length;
143 for (int i = 0; i < numCodes; i++) {
144 if (mCallbacksByCode.containsKey(opsCodes[i])) {
145 mCallbacksByCode.get(opsCodes[i]).remove(callback);
146 }
147 }
148 mCallbacks.remove(callback);
149 if (mCallbacks.isEmpty()) setListening(false);
150 }
151
Fabian Kozynski510585d2018-12-19 13:59:17 -0500152 private AppOpItem getAppOpItem(List<AppOpItem> appOpList, int code, int uid,
153 String packageName) {
154 final int itemsQ = appOpList.size();
Fabian Kozynski1f32cf02018-10-23 12:23:31 -0400155 for (int i = 0; i < itemsQ; i++) {
Fabian Kozynski510585d2018-12-19 13:59:17 -0500156 AppOpItem item = appOpList.get(i);
Fabian Kozynski1f32cf02018-10-23 12:23:31 -0400157 if (item.getCode() == code && item.getUid() == uid
158 && item.getPackageName().equals(packageName)) {
159 return item;
160 }
161 }
162 return null;
163 }
164
165 private boolean updateActives(int code, int uid, String packageName, boolean active) {
166 synchronized (mActiveItems) {
Fabian Kozynski510585d2018-12-19 13:59:17 -0500167 AppOpItem item = getAppOpItem(mActiveItems, code, uid, packageName);
Fabian Kozynski1f32cf02018-10-23 12:23:31 -0400168 if (item == null && active) {
169 item = new AppOpItem(code, uid, packageName, System.currentTimeMillis());
170 mActiveItems.add(item);
Fabian Kozynski1f32cf02018-10-23 12:23:31 -0400171 if (DEBUG) Log.w(TAG, "Added item: " + item.toString());
172 return true;
173 } else if (item != null && !active) {
174 mActiveItems.remove(item);
175 if (DEBUG) Log.w(TAG, "Removed item: " + item.toString());
176 return true;
Fabian Kozynski1f32cf02018-10-23 12:23:31 -0400177 }
178 return false;
179 }
180 }
181
Fabian Kozynski510585d2018-12-19 13:59:17 -0500182 private void removeNoted(int code, int uid, String packageName) {
183 AppOpItem item;
184 synchronized (mNotedItems) {
185 item = getAppOpItem(mNotedItems, code, uid, packageName);
186 if (item == null) return;
187 mNotedItems.remove(item);
188 if (DEBUG) Log.w(TAG, "Removed item: " + item.toString());
189 }
190 notifySuscribers(code, uid, packageName, false);
191 }
192
193 private void addNoted(int code, int uid, String packageName) {
194 AppOpItem item;
195 synchronized (mNotedItems) {
196 item = getAppOpItem(mNotedItems, code, uid, packageName);
197 if (item == null) {
198 item = new AppOpItem(code, uid, packageName, System.currentTimeMillis());
199 mNotedItems.add(item);
200 if (DEBUG) Log.w(TAG, "Added item: " + item.toString());
201 }
202 }
203 mBGHandler.scheduleRemoval(item, NOTED_OP_TIME_DELAY_MS);
204 }
205
Fabian Kozynski1f32cf02018-10-23 12:23:31 -0400206 /**
Philip P. Moltmanne3dbb5a2019-03-07 10:06:11 -0800207 * Does the app-op item refer to a user sensitive permission. Only user sensitive permission
208 * should be shown to the user by default.
209 *
210 * @param item The item
211 *
212 * @return {@code true} iff the app-op item is user sensitive
213 */
214 private boolean isUserSensitive(AppOpItem item) {
215 String permission = AppOpsManager.opToPermission(item.getCode());
216 if (permission == null) {
217 return false;
218 }
219 int permFlags = mContext.getPackageManager().getPermissionFlags(permission,
220 item.getPackageName(), UserHandle.getUserHandleForUid(item.getUid()));
221 return (permFlags & PackageManager.FLAG_PERMISSION_USER_SENSITIVE_WHEN_GRANTED) != 0;
222 }
223
224 /**
Fabian Kozynski1f32cf02018-10-23 12:23:31 -0400225 * Returns a copy of the list containing all the active AppOps that the controller tracks.
226 *
227 * @return List of active AppOps information
228 */
229 public List<AppOpItem> getActiveAppOps() {
Philip P. Moltmanne3dbb5a2019-03-07 10:06:11 -0800230 return getActiveAppOpsForUser(UserHandle.USER_ALL);
Fabian Kozynski1f32cf02018-10-23 12:23:31 -0400231 }
232
233 /**
234 * Returns a copy of the list containing all the active AppOps that the controller tracks, for
235 * a given user id.
236 *
Philip P. Moltmanne3dbb5a2019-03-07 10:06:11 -0800237 * @param userId User id to track, can be {@link UserHandle#USER_ALL}
Fabian Kozynski1f32cf02018-10-23 12:23:31 -0400238 *
239 * @return List of active AppOps information for that user id
240 */
241 public List<AppOpItem> getActiveAppOpsForUser(int userId) {
242 List<AppOpItem> list = new ArrayList<>();
243 synchronized (mActiveItems) {
244 final int numActiveItems = mActiveItems.size();
245 for (int i = 0; i < numActiveItems; i++) {
246 AppOpItem item = mActiveItems.get(i);
Philip P. Moltmanne3dbb5a2019-03-07 10:06:11 -0800247 if ((userId == UserHandle.USER_ALL || UserHandle.getUserId(item.getUid()) == userId)
248 && isUserSensitive(item)) {
Fabian Kozynski1f32cf02018-10-23 12:23:31 -0400249 list.add(item);
250 }
251 }
252 }
Fabian Kozynski510585d2018-12-19 13:59:17 -0500253 synchronized (mNotedItems) {
254 final int numNotedItems = mNotedItems.size();
255 for (int i = 0; i < numNotedItems; i++) {
256 AppOpItem item = mNotedItems.get(i);
Philip P. Moltmanne3dbb5a2019-03-07 10:06:11 -0800257 if ((userId == UserHandle.USER_ALL || UserHandle.getUserId(item.getUid()) == userId)
258 && isUserSensitive(item)) {
Fabian Kozynski510585d2018-12-19 13:59:17 -0500259 list.add(item);
260 }
261 }
262 }
Fabian Kozynski1f32cf02018-10-23 12:23:31 -0400263 return list;
264 }
265
266 @Override
267 public void onOpActiveChanged(int code, int uid, String packageName, boolean active) {
268 if (updateActives(code, uid, packageName, active)) {
Fabian Kozynski510585d2018-12-19 13:59:17 -0500269 notifySuscribers(code, uid, packageName, active);
270 }
271 }
272
273 @Override
Fabian Kozynski0b4592c2018-12-20 12:58:45 -0500274 public void onOpNoted(int code, int uid, String packageName, int result) {
Fabian Kozynski510585d2018-12-19 13:59:17 -0500275 if (DEBUG) {
276 Log.w(TAG, "Op: " + code + " with result " + AppOpsManager.MODE_NAMES[result]);
277 }
278 if (result != AppOpsManager.MODE_ALLOWED) return;
Fabian Kozynski0b4592c2018-12-20 12:58:45 -0500279 addNoted(code, uid, packageName);
280 notifySuscribers(code, uid, packageName, true);
Fabian Kozynski510585d2018-12-19 13:59:17 -0500281 }
282
283 private void notifySuscribers(int code, int uid, String packageName, boolean active) {
284 if (mCallbacksByCode.containsKey(code)) {
Fabian Kozynski1f32cf02018-10-23 12:23:31 -0400285 for (Callback cb: mCallbacksByCode.get(code)) {
286 cb.onActiveStateChanged(code, uid, packageName, active);
287 }
288 }
289 }
290
Fabian Kozynskib7b58792019-01-09 13:07:11 -0500291 @Override
292 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
293 pw.println("AppOpsController state:");
294 pw.println(" Active Items:");
295 for (int i = 0; i < mActiveItems.size(); i++) {
296 final AppOpItem item = mActiveItems.get(i);
297 pw.print(" "); pw.println(item.toString());
298 }
299 pw.println(" Noted Items:");
300 for (int i = 0; i < mNotedItems.size(); i++) {
301 final AppOpItem item = mNotedItems.get(i);
302 pw.print(" "); pw.println(item.toString());
303 }
304
305 }
306
Fabian Kozynski510585d2018-12-19 13:59:17 -0500307 protected final class H extends Handler {
Fabian Kozynski1f32cf02018-10-23 12:23:31 -0400308 H(Looper looper) {
309 super(looper);
310 }
311
312 public void scheduleRemoval(AppOpItem item, long timeToRemoval) {
313 removeCallbacksAndMessages(item);
314 postDelayed(new Runnable() {
315 @Override
316 public void run() {
Fabian Kozynski510585d2018-12-19 13:59:17 -0500317 removeNoted(item.getCode(), item.getUid(), item.getPackageName());
Fabian Kozynski1f32cf02018-10-23 12:23:31 -0400318 }
319 }, item, timeToRemoval);
320 }
321 }
322}