/* * Copyright (C) 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui.appops; import android.app.AppOpsManager; import android.content.Context; import android.os.Handler; import android.os.Looper; import android.os.UserHandle; import android.util.ArrayMap; import android.util.ArraySet; import android.util.Log; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.systemui.Dumpable; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dump.DumpManager; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; import java.util.List; import java.util.Set; import javax.inject.Inject; import javax.inject.Singleton; /** * Controller to keep track of applications that have requested access to given App Ops * * It can be subscribed to with callbacks. Additionally, it passes on the information to * NotificationPresenter to be displayed to the user. */ @Singleton public class AppOpsControllerImpl implements AppOpsController, AppOpsManager.OnOpActiveChangedInternalListener, AppOpsManager.OnOpNotedListener, Dumpable { // This is the minimum time that we will keep AppOps that are noted on record. If multiple // occurrences of the same (op, package, uid) happen in a shorter interval, they will not be // notified to listeners. private static final long NOTED_OP_TIME_DELAY_MS = 5000; private static final String TAG = "AppOpsControllerImpl"; private static final boolean DEBUG = false; private final Context mContext; private final AppOpsManager mAppOps; private H mBGHandler; private final List mCallbacks = new ArrayList<>(); private final ArrayMap> mCallbacksByCode = new ArrayMap<>(); private boolean mListening; @GuardedBy("mActiveItems") private final List mActiveItems = new ArrayList<>(); @GuardedBy("mNotedItems") private final List mNotedItems = new ArrayList<>(); protected static final int[] OPS = new int[] { AppOpsManager.OP_CAMERA, AppOpsManager.OP_SYSTEM_ALERT_WINDOW, AppOpsManager.OP_RECORD_AUDIO, AppOpsManager.OP_COARSE_LOCATION, AppOpsManager.OP_FINE_LOCATION }; @Inject public AppOpsControllerImpl( Context context, @Background Looper bgLooper, DumpManager dumpManager) { mContext = context; mAppOps = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE); mBGHandler = new H(bgLooper); final int numOps = OPS.length; for (int i = 0; i < numOps; i++) { mCallbacksByCode.put(OPS[i], new ArraySet<>()); } dumpManager.registerDumpable(TAG, this); } @VisibleForTesting protected void setBGHandler(H handler) { mBGHandler = handler; } @VisibleForTesting protected void setListening(boolean listening) { mListening = listening; if (listening) { mAppOps.startWatchingActive(OPS, this); mAppOps.startWatchingNoted(OPS, this); } else { mAppOps.stopWatchingActive(this); mAppOps.stopWatchingNoted(this); mBGHandler.removeCallbacksAndMessages(null); // null removes all synchronized (mActiveItems) { mActiveItems.clear(); } synchronized (mNotedItems) { mNotedItems.clear(); } } } /** * Adds a callback that will get notifified when an AppOp of the type the controller tracks * changes * * @param callback Callback to report changes * @param opsCodes App Ops the callback is interested in checking * * @see #removeCallback(int[], Callback) */ @Override public void addCallback(int[] opsCodes, AppOpsController.Callback callback) { boolean added = false; final int numCodes = opsCodes.length; for (int i = 0; i < numCodes; i++) { if (mCallbacksByCode.containsKey(opsCodes[i])) { mCallbacksByCode.get(opsCodes[i]).add(callback); added = true; } else { if (DEBUG) Log.wtf(TAG, "APP_OP " + opsCodes[i] + " not supported"); } } if (added) mCallbacks.add(callback); if (!mCallbacks.isEmpty()) setListening(true); } /** * Removes a callback from those notified when an AppOp of the type the controller tracks * changes * * @param callback Callback to stop reporting changes * @param opsCodes App Ops the callback was interested in checking * * @see #addCallback(int[], Callback) */ @Override public void removeCallback(int[] opsCodes, AppOpsController.Callback callback) { final int numCodes = opsCodes.length; for (int i = 0; i < numCodes; i++) { if (mCallbacksByCode.containsKey(opsCodes[i])) { mCallbacksByCode.get(opsCodes[i]).remove(callback); } } mCallbacks.remove(callback); if (mCallbacks.isEmpty()) setListening(false); } // Find item number in list, only call if the list passed is locked private AppOpItem getAppOpItemLocked(List appOpList, int code, int uid, String packageName) { final int itemsQ = appOpList.size(); for (int i = 0; i < itemsQ; i++) { AppOpItem item = appOpList.get(i); if (item.getCode() == code && item.getUid() == uid && item.getPackageName().equals(packageName)) { return item; } } return null; } private boolean updateActives(int code, int uid, String packageName, boolean active) { synchronized (mActiveItems) { AppOpItem item = getAppOpItemLocked(mActiveItems, code, uid, packageName); if (item == null && active) { item = new AppOpItem(code, uid, packageName, System.currentTimeMillis()); mActiveItems.add(item); if (DEBUG) Log.w(TAG, "Added item: " + item.toString()); return true; } else if (item != null && !active) { mActiveItems.remove(item); if (DEBUG) Log.w(TAG, "Removed item: " + item.toString()); return true; } return false; } } private void removeNoted(int code, int uid, String packageName) { AppOpItem item; synchronized (mNotedItems) { item = getAppOpItemLocked(mNotedItems, code, uid, packageName); if (item == null) return; mNotedItems.remove(item); if (DEBUG) Log.w(TAG, "Removed item: " + item.toString()); } boolean active; // Check if the item is also active synchronized (mActiveItems) { active = getAppOpItemLocked(mActiveItems, code, uid, packageName) != null; } if (!active) { notifySuscribers(code, uid, packageName, false); } } private boolean addNoted(int code, int uid, String packageName) { AppOpItem item; boolean createdNew = false; synchronized (mNotedItems) { item = getAppOpItemLocked(mNotedItems, code, uid, packageName); if (item == null) { item = new AppOpItem(code, uid, packageName, System.currentTimeMillis()); mNotedItems.add(item); if (DEBUG) Log.w(TAG, "Added item: " + item.toString()); createdNew = true; } } // We should keep this so we make sure it cannot time out. mBGHandler.removeCallbacksAndMessages(item); mBGHandler.scheduleRemoval(item, NOTED_OP_TIME_DELAY_MS); return createdNew; } /** * Returns a copy of the list containing all the active AppOps that the controller tracks. * * @return List of active AppOps information */ public List getActiveAppOps() { return getActiveAppOpsForUser(UserHandle.USER_ALL); } /** * Returns a copy of the list containing all the active AppOps that the controller tracks, for * a given user id. * * @param userId User id to track, can be {@link UserHandle#USER_ALL} * * @return List of active AppOps information for that user id */ public List getActiveAppOpsForUser(int userId) { List list = new ArrayList<>(); synchronized (mActiveItems) { final int numActiveItems = mActiveItems.size(); for (int i = 0; i < numActiveItems; i++) { AppOpItem item = mActiveItems.get(i); if ((userId == UserHandle.USER_ALL || UserHandle.getUserId(item.getUid()) == userId)) { list.add(item); } } } synchronized (mNotedItems) { final int numNotedItems = mNotedItems.size(); for (int i = 0; i < numNotedItems; i++) { AppOpItem item = mNotedItems.get(i); if ((userId == UserHandle.USER_ALL || UserHandle.getUserId(item.getUid()) == userId)) { list.add(item); } } } return list; } @Override public void onOpActiveChanged(int code, int uid, String packageName, boolean active) { if (DEBUG) { Log.w(TAG, String.format("onActiveChanged(%d,%d,%s,%s", code, uid, packageName, Boolean.toString(active))); } boolean activeChanged = updateActives(code, uid, packageName, active); if (!activeChanged) return; // early return // Check if the item is also noted, in that case, there's no update. boolean alsoNoted; synchronized (mNotedItems) { alsoNoted = getAppOpItemLocked(mNotedItems, code, uid, packageName) != null; } // If active is true, we only send the update if the op is not actively noted (already true) // If active is false, we only send the update if the op is not actively noted (prevent // early removal) if (!alsoNoted) { mBGHandler.post(() -> notifySuscribers(code, uid, packageName, active)); } } @Override public void onOpNoted(int code, int uid, String packageName, int result) { if (DEBUG) { Log.w(TAG, "Noted op: " + code + " with result " + AppOpsManager.MODE_NAMES[result] + " for package " + packageName); } if (result != AppOpsManager.MODE_ALLOWED) return; boolean notedAdded = addNoted(code, uid, packageName); if (!notedAdded) return; // early return boolean alsoActive; synchronized (mActiveItems) { alsoActive = getAppOpItemLocked(mActiveItems, code, uid, packageName) != null; } if (!alsoActive) { mBGHandler.post(() -> notifySuscribers(code, uid, packageName, true)); } } private void notifySuscribers(int code, int uid, String packageName, boolean active) { if (mCallbacksByCode.containsKey(code)) { if (DEBUG) Log.d(TAG, "Notifying of change in package " + packageName); for (Callback cb: mCallbacksByCode.get(code)) { cb.onActiveStateChanged(code, uid, packageName, active); } } } @Override public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { pw.println("AppOpsController state:"); pw.println(" Listening: " + mListening); pw.println(" Active Items:"); for (int i = 0; i < mActiveItems.size(); i++) { final AppOpItem item = mActiveItems.get(i); pw.print(" "); pw.println(item.toString()); } pw.println(" Noted Items:"); for (int i = 0; i < mNotedItems.size(); i++) { final AppOpItem item = mNotedItems.get(i); pw.print(" "); pw.println(item.toString()); } } protected class H extends Handler { H(Looper looper) { super(looper); } public void scheduleRemoval(AppOpItem item, long timeToRemoval) { removeCallbacksAndMessages(item); postDelayed(new Runnable() { @Override public void run() { removeNoted(item.getCode(), item.getUid(), item.getPackageName()); } }, item, timeToRemoval); } } }