blob: 18a91535a34cf9149a6d020ebb0fa4c9ead341a6 [file] [log] [blame]
/*
* Copyright (C) 2022 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.server.am;
import static android.Manifest.permission.ACCESS_FINE_LOCATION;
import static android.Manifest.permission.CAMERA;
import static android.Manifest.permission.RECORD_AUDIO;
import static android.app.AppOpsManager.OPSTR_CAMERA;
import static android.app.AppOpsManager.OPSTR_FINE_LOCATION;
import static android.app.AppOpsManager.OPSTR_RECORD_AUDIO;
import static android.app.AppOpsManager.OP_NONE;
import static android.app.AppOpsManager.opToPublicName;
import static android.app.AppOpsManager.strOpToOp;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.os.Process.SYSTEM_UID;
import static com.android.server.am.ActivityManagerDebugConfig.TAG_AM;
import static com.android.server.am.ActivityManagerDebugConfig.TAG_WITH_CLASS_NAME;
import static com.android.server.am.AppBatteryExemptionTracker.DEFAULT_NAME;
import static com.android.server.am.AppRestrictionController.DEVICE_CONFIG_SUBNAMESPACE_PREFIX;
import static com.android.server.am.BaseAppStateTracker.STATE_TYPE_PERMISSION;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.AppOpsManager;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager.OnPermissionsChangedListener;
import android.content.pm.PackageManagerInternal;
import android.os.Handler;
import android.os.Message;
import android.os.Process;
import android.os.RemoteException;
import android.os.SystemClock;
import android.os.UserHandle;
import android.permission.PermissionManager;
import android.provider.DeviceConfig;
import android.text.TextUtils;
import android.util.ArraySet;
import android.util.Pair;
import android.util.Slog;
import android.util.SparseArray;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.app.IAppOpsCallback;
import com.android.internal.app.IAppOpsService;
import com.android.server.am.AppPermissionTracker.AppPermissionPolicy;
import com.android.server.am.AppRestrictionController.TrackerType;
import com.android.server.pm.permission.PermissionManagerServiceInternal;
import java.io.PrintWriter;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
/**
* The tracker for monitoring selected permission state of apps.
*/
final class AppPermissionTracker extends BaseAppStateTracker<AppPermissionPolicy>
implements OnPermissionsChangedListener {
static final String TAG = TAG_WITH_CLASS_NAME ? "AppPermissionTracker" : TAG_AM;
static final boolean DEBUG_PERMISSION_TRACKER = false;
private final MyHandler mHandler;
/**
* Keep a new instance of callback for each appop we're monitoring,
* as the AppOpsService doesn't support monitoring multiple appops with single callback
* instance (except the ALL_OPS case).
*/
@GuardedBy("mAppOpsCallbacks")
private final SparseArray<MyAppOpsCallback> mAppOpsCallbacks = new SparseArray<>();
@GuardedBy("mLock")
private SparseArray<ArraySet<UidGrantedPermissionState>> mUidGrantedPermissionsInMonitor =
new SparseArray<>();
private volatile boolean mLockedBootCompleted = false;
AppPermissionTracker(Context context, AppRestrictionController controller) {
this(context, controller, null, null);
}
AppPermissionTracker(Context context, AppRestrictionController controller,
Constructor<? extends Injector<AppPermissionPolicy>> injector, Object outerContext) {
super(context, controller, injector, outerContext);
mHandler = new MyHandler(this);
mInjector.setPolicy(new AppPermissionPolicy(mInjector, this));
}
@Override
@TrackerType int getType() {
return AppRestrictionController.TRACKER_TYPE_PERMISSION;
}
@Override
public void onPermissionsChanged(int uid) {
mHandler.obtainMessage(MyHandler.MSG_PERMISSIONS_CHANGED, uid, 0).sendToTarget();
}
private void handleAppOpsInit() {
final ArrayList<Integer> ops = new ArrayList<>();
final Pair[] permissions = mInjector.getPolicy().getBgPermissionsInMonitor();
for (int i = 0; i < permissions.length; i++) {
final Pair<String, Integer> pair = permissions[i];
if (pair.second != OP_NONE) {
ops.add(pair.second);
}
}
startWatchingMode(ops.toArray(new Integer[ops.size()]));
}
private void handlePermissionsInit() {
final int[] allUsers = mInjector.getUserManagerInternal().getUserIds();
final PackageManagerInternal pmi = mInjector.getPackageManagerInternal();
final PermissionManagerServiceInternal pm = mInjector.getPermissionManagerServiceInternal();
final Pair[] permissions = mInjector.getPolicy().getBgPermissionsInMonitor();
final SparseArray<ArraySet<UidGrantedPermissionState>> uidPerms =
mUidGrantedPermissionsInMonitor;
for (int userId : allUsers) {
final List<ApplicationInfo> apps = pmi.getInstalledApplications(0, userId, SYSTEM_UID);
if (apps == null) {
continue;
}
final long now = SystemClock.elapsedRealtime();
for (int i = 0, size = apps.size(); i < size; i++) {
final ApplicationInfo ai = apps.get(i);
for (Pair<String, Integer> permission : permissions) {
final UidGrantedPermissionState state = new UidGrantedPermissionState(
ai.uid, permission.first, permission.second);
if (!state.isGranted()) {
// No need to track it.
continue;
}
synchronized (mLock) {
ArraySet<UidGrantedPermissionState> grantedPermissions =
uidPerms.get(ai.uid);
if (grantedPermissions == null) {
grantedPermissions = new ArraySet<UidGrantedPermissionState>();
uidPerms.put(ai.uid, grantedPermissions);
// This UID has at least one active permission-in-interest now,
// let the listeners know.
notifyListenersOnStateChange(ai.uid, DEFAULT_NAME, true, now,
STATE_TYPE_PERMISSION);
}
grantedPermissions.add(state);
}
}
}
}
}
private void handleAppOpsDestroy() {
stopWatchingMode();
}
private void handlePermissionsDestroy() {
synchronized (mLock) {
final SparseArray<ArraySet<UidGrantedPermissionState>> uidPerms =
mUidGrantedPermissionsInMonitor;
final long now = SystemClock.elapsedRealtime();
for (int i = 0, size = uidPerms.size(); i < size; i++) {
final int uid = uidPerms.keyAt(i);
final ArraySet<UidGrantedPermissionState> grantedPermissions = uidPerms.valueAt(i);
if (grantedPermissions.size() > 0) {
notifyListenersOnStateChange(uid, DEFAULT_NAME, false, now,
STATE_TYPE_PERMISSION);
}
}
uidPerms.clear();
}
}
private void handleOpChanged(int op, int uid, String packageName) {
if (DEBUG_PERMISSION_TRACKER) {
final IAppOpsService appOpsService = mInjector.getIAppOpsService();
try {
final int mode = appOpsService.checkOperation(op, uid, packageName);
Slog.i(TAG, "onOpChanged: " + opToPublicName(op)
+ " " + UserHandle.formatUid(uid)
+ " " + packageName + " " + mode);
} catch (RemoteException e) {
// Intra-process call, should never happen.
}
}
final Pair[] permissions = mInjector.getPolicy().getBgPermissionsInMonitor();
if (permissions != null && permissions.length > 0) {
for (int i = 0; i < permissions.length; i++) {
final Pair<String, Integer> pair = permissions[i];
if (pair.second != op) {
continue;
}
final UidGrantedPermissionState state =
new UidGrantedPermissionState(uid, pair.first, op);
synchronized (mLock) {
handlePermissionsChangedLocked(uid, new UidGrantedPermissionState[] {state});
}
break;
}
}
}
private void handlePermissionsChanged(int uid) {
if (DEBUG_PERMISSION_TRACKER) {
Slog.i(TAG, "handlePermissionsChanged " + UserHandle.formatUid(uid));
}
final Pair[] permissions = mInjector.getPolicy().getBgPermissionsInMonitor();
if (permissions != null && permissions.length > 0) {
final PermissionManagerServiceInternal pm =
mInjector.getPermissionManagerServiceInternal();
final UidGrantedPermissionState[] states =
new UidGrantedPermissionState[permissions.length];
for (int i = 0; i < permissions.length; i++) {
final Pair<String, Integer> pair = permissions[i];
states[i] = new UidGrantedPermissionState(uid, pair.first, pair.second);
if (DEBUG_PERMISSION_TRACKER) {
Slog.i(TAG, states[i].toString());
}
}
synchronized (mLock) {
handlePermissionsChangedLocked(uid, states);
}
}
}
@GuardedBy("mLock")
private void handlePermissionsChangedLocked(int uid, UidGrantedPermissionState[] states) {
final int index = mUidGrantedPermissionsInMonitor.indexOfKey(uid);
ArraySet<UidGrantedPermissionState> grantedPermissions = index >= 0
? mUidGrantedPermissionsInMonitor.valueAt(index) : null;
final long now = SystemClock.elapsedRealtime();
for (int i = 0; i < states.length; i++) {
final boolean granted = states[i].isGranted();
boolean changed = false;
if (granted) {
if (grantedPermissions == null) {
grantedPermissions = new ArraySet<>();
mUidGrantedPermissionsInMonitor.put(uid, grantedPermissions);
changed = true;
}
grantedPermissions.add(states[i]);
} else if (grantedPermissions != null && !grantedPermissions.isEmpty()) {
if (grantedPermissions.remove(states[i]) && grantedPermissions.isEmpty()) {
mUidGrantedPermissionsInMonitor.removeAt(index);
changed = true;
}
}
if (changed) {
notifyListenersOnStateChange(uid, DEFAULT_NAME, granted, now,
STATE_TYPE_PERMISSION);
}
}
}
/**
* Represents the grant state of a permission + appop of the given UID.
*/
private class UidGrantedPermissionState {
final int mUid;
final @Nullable String mPermission;
final int mAppOp;
private boolean mPermissionGranted;
private boolean mAppOpAllowed;
UidGrantedPermissionState(int uid, @Nullable String permission, int appOp) {
mUid = uid;
mPermission = permission;
mAppOp = appOp;
updatePermissionState();
updateAppOps();
}
void updatePermissionState() {
if (TextUtils.isEmpty(mPermission)) {
mPermissionGranted = true;
return;
}
mPermissionGranted = mContext.checkPermission(mPermission, Process.INVALID_PID, mUid)
== PERMISSION_GRANTED;
}
void updateAppOps() {
if (mAppOp == OP_NONE) {
mAppOpAllowed = true;
return;
}
final String[] packages = mInjector.getPackageManager().getPackagesForUid(mUid);
if (packages != null) {
final IAppOpsService appOpsService = mInjector.getIAppOpsService();
for (String pkg : packages) {
try {
final int mode = appOpsService.checkOperation(mAppOp, mUid, pkg);
if (mode == AppOpsManager.MODE_ALLOWED) {
mAppOpAllowed = true;
return;
}
} catch (RemoteException e) {
// Intra-process call, should never happen.
}
}
}
mAppOpAllowed = false;
}
boolean isGranted() {
return mPermissionGranted && mAppOpAllowed;
}
@Override
public boolean equals(Object other) {
if (other == null || !(other instanceof UidGrantedPermissionState)) {
return false;
}
final UidGrantedPermissionState otherState = (UidGrantedPermissionState) other;
return mUid == otherState.mUid && mAppOp == otherState.mAppOp
&& Objects.equals(mPermission, otherState.mPermission);
}
@Override
public int hashCode() {
return (Integer.hashCode(mUid) * 31 + Integer.hashCode(mAppOp)) * 31
+ (mPermission == null ? 0 : mPermission.hashCode());
}
@Override
public String toString() {
String s = "UidGrantedPermissionState{"
+ System.identityHashCode(this) + " "
+ UserHandle.formatUid(mUid) + ": ";
final boolean emptyPermissionName = TextUtils.isEmpty(mPermission);
if (!emptyPermissionName) {
s += mPermission + "=" + mPermissionGranted;
}
if (mAppOp != OP_NONE) {
if (!emptyPermissionName) {
s += ",";
}
s += opToPublicName(mAppOp) + "=" + mAppOpAllowed;
}
s += "}";
return s;
}
}
private void startWatchingMode(@NonNull Integer[] ops) {
synchronized (mAppOpsCallbacks) {
stopWatchingMode();
final IAppOpsService appOpsService = mInjector.getIAppOpsService();
try {
for (int op: ops) {
final MyAppOpsCallback cb = new MyAppOpsCallback();
mAppOpsCallbacks.put(op, cb);
appOpsService.startWatchingModeWithFlags(op, null,
AppOpsManager.WATCH_FOREGROUND_CHANGES, cb);
}
} catch (RemoteException e) {
// Intra-process call, should never happen.
}
}
}
private void stopWatchingMode() {
synchronized (mAppOpsCallbacks) {
final IAppOpsService appOpsService = mInjector.getIAppOpsService();
for (int i = mAppOpsCallbacks.size() - 1; i >= 0; i--) {
try {
appOpsService.stopWatchingMode(mAppOpsCallbacks.valueAt(i));
} catch (RemoteException e) {
// Intra-process call, should never happen.
}
}
mAppOpsCallbacks.clear();
}
}
private class MyAppOpsCallback extends IAppOpsCallback.Stub {
@Override
public void opChanged(int op, int uid, String packageName) {
mHandler.obtainMessage(MyHandler.MSG_APPOPS_CHANGED, op, uid, packageName)
.sendToTarget();
}
}
private static class MyHandler extends Handler {
static final int MSG_PERMISSIONS_INIT = 0;
static final int MSG_PERMISSIONS_DESTROY = 1;
static final int MSG_PERMISSIONS_CHANGED = 2;
static final int MSG_APPOPS_CHANGED = 3;
private @NonNull AppPermissionTracker mTracker;
MyHandler(@NonNull AppPermissionTracker tracker) {
super(tracker.mBgHandler.getLooper());
mTracker = tracker;
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_PERMISSIONS_INIT:
mTracker.handleAppOpsInit();
mTracker.handlePermissionsInit();
break;
case MSG_PERMISSIONS_DESTROY:
mTracker.handlePermissionsDestroy();
mTracker.handleAppOpsDestroy();
break;
case MSG_PERMISSIONS_CHANGED:
mTracker.handlePermissionsChanged(msg.arg1);
break;
case MSG_APPOPS_CHANGED:
mTracker.handleOpChanged(msg.arg1, msg.arg2, (String) msg.obj);
break;
}
}
}
private void onPermissionTrackerEnabled(boolean enabled) {
if (!mLockedBootCompleted) {
// Not ready, bail out.
return;
}
final PermissionManager pm = mInjector.getPermissionManager();
if (enabled) {
pm.addOnPermissionsChangeListener(this);
mHandler.obtainMessage(MyHandler.MSG_PERMISSIONS_INIT).sendToTarget();
} else {
pm.removeOnPermissionsChangeListener(this);
mHandler.obtainMessage(MyHandler.MSG_PERMISSIONS_DESTROY).sendToTarget();
}
}
@Override
void onLockedBootCompleted() {
mLockedBootCompleted = true;
onPermissionTrackerEnabled(mInjector.getPolicy().isEnabled());
}
@Override
void dump(PrintWriter pw, String prefix) {
pw.print(prefix);
pw.println("APP PERMISSIONS TRACKER:");
final Pair[] permissions = mInjector.getPolicy().getBgPermissionsInMonitor();
final String prefixMore = " " + prefix;
final String prefixMoreMore = " " + prefixMore;
for (Pair<String, Integer> permission : permissions) {
pw.print(prefixMore);
final boolean emptyPermissionName = TextUtils.isEmpty(permission.first);
if (!emptyPermissionName) {
pw.print(permission.first);
}
if (permission.second != OP_NONE) {
if (!emptyPermissionName) {
pw.print('+');
}
pw.print(opToPublicName(permission.second));
}
pw.println(':');
synchronized (mLock) {
final SparseArray<ArraySet<UidGrantedPermissionState>> uidPerms =
mUidGrantedPermissionsInMonitor;
pw.print(prefixMoreMore);
pw.print('[');
boolean needDelimiter = false;
for (int i = 0, size = uidPerms.size(); i < size; i++) {
final ArraySet<UidGrantedPermissionState> uidPerm = uidPerms.valueAt(i);
for (int j = uidPerm.size() - 1; j >= 0; j--) {
final UidGrantedPermissionState state = uidPerm.valueAt(j);
if (state.mAppOp == permission.second
&& TextUtils.equals(state.mPermission, permission.first)) {
if (needDelimiter) {
pw.print(',');
}
needDelimiter = true;
pw.print(UserHandle.formatUid(state.mUid));
break;
}
}
}
pw.println(']');
}
}
super.dump(pw, prefix);
}
static final class AppPermissionPolicy extends BaseAppStatePolicy<AppPermissionTracker> {
/**
* Whether or not we should enable the monitoring on app permissions.
*/
static final String KEY_BG_PERMISSION_MONITOR_ENABLED =
DEVICE_CONFIG_SUBNAMESPACE_PREFIX + "permission_monitor_enabled";
/**
* The names of the permissions we're monitoring its changes.
*/
static final String KEY_BG_PERMISSIONS_IN_MONITOR =
DEVICE_CONFIG_SUBNAMESPACE_PREFIX + "permission_in_monitor";
/**
* Default value to {@link #mTrackerEnabled}.
*/
static final boolean DEFAULT_BG_PERMISSION_MONITOR_ENABLED = true;
/**
* Default value to {@link #mBgPermissionsInMonitor}, it comes in pair;
* the first string strings in the pair is the permission name, and the second string
* is the appops name, if they are associated.
*/
static final String[] DEFAULT_BG_PERMISSIONS_IN_MONITOR = new String[] {
ACCESS_FINE_LOCATION, OPSTR_FINE_LOCATION,
CAMERA, OPSTR_CAMERA,
RECORD_AUDIO, OPSTR_RECORD_AUDIO,
};
/**
* @see #KEY_BG_PERMISSIONS_IN_MONITOR.
*/
volatile @NonNull Pair[] mBgPermissionsInMonitor;
AppPermissionPolicy(@NonNull Injector injector, @NonNull AppPermissionTracker tracker) {
super(injector, tracker, KEY_BG_PERMISSION_MONITOR_ENABLED,
DEFAULT_BG_PERMISSION_MONITOR_ENABLED);
mBgPermissionsInMonitor = parsePermissionConfig(DEFAULT_BG_PERMISSIONS_IN_MONITOR);
}
@Override
public void onSystemReady() {
super.onSystemReady();
updateBgPermissionsInMonitor();
}
@Override
public void onPropertiesChanged(String name) {
switch (name) {
case KEY_BG_PERMISSIONS_IN_MONITOR:
updateBgPermissionsInMonitor();
break;
default:
super.onPropertiesChanged(name);
break;
}
}
Pair[] getBgPermissionsInMonitor() {
return mBgPermissionsInMonitor;
}
private @NonNull Pair[] parsePermissionConfig(@NonNull String[] perms) {
final Pair[] result = new Pair[perms.length / 2];
for (int i = 0, j = 0; i < perms.length; i += 2, j++) {
try {
result[j] = Pair.create(TextUtils.isEmpty(perms[i]) ? null : perms[i],
TextUtils.isEmpty(perms[i + 1]) ? OP_NONE : strOpToOp(perms[i + 1]));
} catch (Exception e) {
// Ignore.
}
}
return result;
}
private void updateBgPermissionsInMonitor() {
final String config = DeviceConfig.getString(
DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
KEY_BG_PERMISSIONS_IN_MONITOR,
null);
final Pair[] newPermsInMonitor = parsePermissionConfig(
config != null ? config.split(",") : DEFAULT_BG_PERMISSIONS_IN_MONITOR);
if (!Arrays.equals(mBgPermissionsInMonitor, newPermsInMonitor)) {
mBgPermissionsInMonitor = newPermsInMonitor;
if (isEnabled()) {
// Trigger a reload.
onTrackerEnabled(false);
onTrackerEnabled(true);
}
}
}
@Override
public void onTrackerEnabled(boolean enabled) {
mTracker.onPermissionTrackerEnabled(enabled);
}
@Override
void dump(PrintWriter pw, String prefix) {
pw.print(prefix);
pw.println("APP PERMISSION TRACKER POLICY SETTINGS:");
prefix = " " + prefix;
super.dump(pw, prefix);
pw.print(prefix);
pw.print(KEY_BG_PERMISSIONS_IN_MONITOR);
pw.print('=');
pw.print('[');
for (int i = 0; i < mBgPermissionsInMonitor.length; i++) {
if (i > 0) {
pw.print(',');
}
final Pair<String, Integer> pair = mBgPermissionsInMonitor[i];
if (pair.first != null) {
pw.print(pair.first);
}
pw.print(',');
if (pair.second != OP_NONE) {
pw.print(opToPublicName(pair.second));
}
}
pw.println(']');
}
}
}