/*
|
* 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 static com.android.systemui.Dependency.BG_LOOPER_NAME;
|
|
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 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.Named;
|
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.OnOpActiveChangedListener,
|
AppOpsManager.OnOpNotedListener, Dumpable {
|
|
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<AppOpsController.Callback> mCallbacks = new ArrayList<>();
|
private final ArrayMap<Integer, Set<Callback>> mCallbacksByCode = new ArrayMap<>();
|
|
@GuardedBy("mActiveItems")
|
private final List<AppOpItem> mActiveItems = new ArrayList<>();
|
@GuardedBy("mNotedItems")
|
private final List<AppOpItem> 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, @Named(BG_LOOPER_NAME) Looper bgLooper) {
|
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<>());
|
}
|
}
|
|
@VisibleForTesting
|
protected void setBGHandler(H handler) {
|
mBGHandler = handler;
|
}
|
|
@VisibleForTesting
|
protected void setListening(boolean 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);
|
}
|
|
private AppOpItem getAppOpItem(List<AppOpItem> 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 = getAppOpItem(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 = getAppOpItem(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 = getAppOpItem(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 = getAppOpItem(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<AppOpItem> 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<AppOpItem> getActiveAppOpsForUser(int userId) {
|
List<AppOpItem> 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 = getAppOpItem(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 = getAppOpItem(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(" 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);
|
}
|
}
|
}
|