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