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