blob: e9e5a99286c74b949b8ecc0097e335b20ebf6dc8 [file] [log] [blame]
/*
* Copyright (C) 2020 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.permissioncontroller.permission.model;
import static android.Manifest.permission_group.MICROPHONE;
import android.Manifest;
import android.app.AppOpsManager;
import android.app.AppOpsManager.AttributedHistoricalOps;
import android.app.AppOpsManager.AttributedOpEntry;
import android.app.AppOpsManager.HistoricalOp;
import android.app.AppOpsManager.HistoricalPackageOps;
import android.app.AppOpsManager.OpEntry;
import android.app.AppOpsManager.OpEventProxyInfo;
import android.app.AppOpsManager.PackageOps;
import android.content.pm.Attribution;
import android.media.AudioRecordingConfiguration;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import com.android.permissioncontroller.permission.model.legacy.PermissionApps.PermissionApp;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import kotlin.Triple;
/**
* Stats for permission usage of an app. This data is for a given time period,
* i.e. does not contain the full history.
*/
public final class AppPermissionUsage {
private final @NonNull List<GroupUsage> mGroupUsages = new ArrayList<>();
private final @NonNull PermissionApp mPermissionApp;
// TODO: theianchen move them to SystemApi
private static final String OPSTR_PHONE_CALL_MICROPHONE = "android:phone_call_microphone";
private static final String OPSTR_PHONE_CALL_CAMERA = "android:phone_call_camera";
private static final int PRIVACY_HUB_FLAGS = AppOpsManager.OP_FLAG_SELF
| AppOpsManager.OP_FLAG_TRUSTED_PROXIED | AppOpsManager.OP_FLAG_TRUSTED_PROXY;
private AppPermissionUsage(@NonNull PermissionApp permissionApp,
@NonNull List<AppPermissionGroup> groups, @Nullable PackageOps lastUsage,
@Nullable HistoricalPackageOps historicalUsage,
@Nullable ArrayList<AudioRecordingConfiguration> recordings) {
mPermissionApp = permissionApp;
final int groupCount = groups.size();
for (int i = 0; i < groupCount; i++) {
final AppPermissionGroup group = groups.get(i);
/**
* TODO: HACK HACK HACK.
*
* Exclude for the UIDs that are currently silenced. This happens if an app keeps
* recording while in the background for more than a few seconds.
*/
if (recordings != null && group.getName().equals(MICROPHONE)) {
boolean isSilenced = false;
int recordingsCount = recordings.size();
for (int recordingNum = 0; recordingNum < recordingsCount; recordingNum++) {
AudioRecordingConfiguration recording = recordings.get(recordingNum);
if (recording.isClientSilenced()) {
isSilenced = true;
break;
}
}
if (isSilenced) {
continue;
}
}
mGroupUsages.add(new GroupUsage(group, lastUsage, historicalUsage));
}
}
public @NonNull PermissionApp getApp() {
return mPermissionApp;
}
public @NonNull String getPackageName() {
return mPermissionApp.getPackageName();
}
public int getUid() {
return mPermissionApp.getUid();
}
public long getLastAccessTime() {
long lastAccessTime = 0;
final int permissionCount = mGroupUsages.size();
for (int i = 0; i < permissionCount; i++) {
final GroupUsage groupUsage = mGroupUsages.get(i);
lastAccessTime = Math.max(lastAccessTime, groupUsage.getLastAccessTime());
}
return lastAccessTime;
}
public long getAccessCount() {
long accessCount = 0;
final int permissionCount = mGroupUsages.size();
for (int i = 0; i < permissionCount; i++) {
final GroupUsage permission = mGroupUsages.get(i);
accessCount += permission.getAccessCount();
}
return accessCount;
}
public @NonNull List<GroupUsage> getGroupUsages() {
return mGroupUsages;
}
/**
* Stats for permission usage of a permission group. This data is for a
* given time period, i.e. does not contain the full history.
*/
public static class GroupUsage implements TimelineUsage {
private final @NonNull AppPermissionGroup mGroup;
private final @Nullable PackageOps mLastUsage;
private final @Nullable HistoricalPackageOps mHistoricalUsage;
public GroupUsage(@NonNull AppPermissionGroup group, @Nullable PackageOps lastUsage,
@Nullable HistoricalPackageOps historicalUsage) {
mGroup = group;
mLastUsage = lastUsage;
mHistoricalUsage = historicalUsage;
}
public long getLastAccessTime() {
if (mLastUsage == null) {
return 0;
}
return lastAccessAggregate((op) -> op.getLastAccessTime(PRIVACY_HUB_FLAGS));
}
public long getLastAccessForegroundTime() {
if (mLastUsage == null) {
return 0;
}
return lastAccessAggregate((op) -> op.getLastAccessForegroundTime(PRIVACY_HUB_FLAGS));
}
public long getLastAccessBackgroundTime() {
if (mLastUsage == null) {
return 0;
}
return lastAccessAggregate((op) -> op.getLastAccessBackgroundTime(PRIVACY_HUB_FLAGS));
}
public long getForegroundAccessCount() {
if (mHistoricalUsage == null) {
return 0;
}
return extractAggregate((HistoricalOp op)
-> op.getForegroundAccessCount(PRIVACY_HUB_FLAGS));
}
public long getBackgroundAccessCount() {
if (mHistoricalUsage == null) {
return 0;
}
return extractAggregate((HistoricalOp op)
-> op.getBackgroundAccessCount(PRIVACY_HUB_FLAGS));
}
public long getAccessCount() {
if (mHistoricalUsage == null) {
return 0;
}
return extractAggregate((HistoricalOp op) ->
op.getForegroundAccessCount(PRIVACY_HUB_FLAGS)
+ op.getBackgroundAccessCount(PRIVACY_HUB_FLAGS)
);
}
/**
* Get the last access duration.
*/
public long getLastAccessDuration() {
if (mLastUsage == null) {
return 0;
}
return lastAccessAggregate(
(op) -> op.getLastDuration(AppOpsManager.OP_FLAGS_ALL_TRUSTED));
}
/**
* Get the access duration.
*/
public long getAccessDuration() {
if (mHistoricalUsage == null) {
return 0;
}
return extractAggregate((HistoricalOp op) ->
op.getForegroundAccessDuration(AppOpsManager.OP_FLAGS_ALL_TRUSTED)
+ op.getBackgroundAccessDuration(AppOpsManager.OP_FLAGS_ALL_TRUSTED)
);
}
@Override
public boolean hasDiscreteData() {
if (mHistoricalUsage == null) {
return false;
}
Set<String> allOps = getAllOps(mGroup);
for (String opName : allOps) {
final HistoricalOp historicalOp = mHistoricalUsage.getOp(opName);
if (historicalOp != null && historicalOp.getDiscreteAccessCount() > 0) {
return true;
}
}
return false;
}
@Override
public List<Triple<Long, Long, OpEventProxyInfo>> getAllDiscreteAccessTime() {
List<Triple<Long, Long, OpEventProxyInfo>> allDiscreteAccessTime = new ArrayList<>();
if (!hasDiscreteData()) {
return allDiscreteAccessTime;
}
Set<String> allOps = getAllOps(mGroup);
for (String opName : allOps) {
final HistoricalOp historicalOp = mHistoricalUsage.getOp(opName);
if (historicalOp == null) {
continue;
}
int discreteAccessCount = historicalOp.getDiscreteAccessCount();
for (int j = 0; j < discreteAccessCount; j++) {
AppOpsManager.AttributedOpEntry opEntry = historicalOp.getDiscreteAccessAt(j);
allDiscreteAccessTime.add(new Triple<>(
opEntry.getLastAccessTime(PRIVACY_HUB_FLAGS),
opEntry.getLastDuration(PRIVACY_HUB_FLAGS),
opEntry.getLastProxyInfo(PRIVACY_HUB_FLAGS)));
}
}
return allDiscreteAccessTime;
}
public boolean isRunning() {
if (mLastUsage == null) {
return false;
}
Set<String> allOps = getAllOps(mGroup);
final List<OpEntry> ops = mLastUsage.getOps();
final int opCount = ops.size();
for (int j = 0; j < opCount; j++) {
final OpEntry op = ops.get(j);
if (allOps.contains(op.getOpStr()) && op.isRunning()) {
return true;
}
}
return false;
}
private long extractAggregate(@NonNull Function<HistoricalOp, Long> extractor) {
long aggregate = 0;
Set<String> allOps = getAllOps(mGroup);
for (String opName : allOps) {
final HistoricalOp historicalOp = mHistoricalUsage.getOp(opName);
if (historicalOp != null) {
aggregate += extractor.apply(historicalOp);
}
}
return aggregate;
}
private long lastAccessAggregate(@NonNull Function<OpEntry, Long> extractor) {
long aggregate = 0;
Set<String> allOps = getAllOps(mGroup);
final List<OpEntry> ops = mLastUsage.getOps();
final int opCount = ops.size();
for (int opNum = 0; opNum < opCount; opNum++) {
final OpEntry op = ops.get(opNum);
if (allOps.contains(op.getOpStr())) {
aggregate = Math.max(aggregate, extractor.apply(op));
}
}
return aggregate;
}
private static Set<String> getAllOps(AppPermissionGroup appPermissionGroup) {
Set<String> allOps = new HashSet<>();
List<Permission> permissions = appPermissionGroup.getPermissions();
final int permissionCount = permissions.size();
for (int permissionNum = 0; permissionNum < permissionCount; permissionNum++) {
final Permission permission = permissions.get(permissionNum);
final String opName = permission.getAppOp();
if (opName != null) {
allOps.add(opName);
}
}
if (appPermissionGroup.getName().equals(Manifest.permission_group.MICROPHONE)) {
allOps.add(OPSTR_PHONE_CALL_MICROPHONE);
}
if (appPermissionGroup.getName().equals(Manifest.permission_group.CAMERA)) {
allOps.add(OPSTR_PHONE_CALL_CAMERA);
}
return allOps;
}
@Override
public @NonNull AppPermissionGroup getGroup() {
return mGroup;
}
@Override
public int getLabel() {
return -1;
}
@Override
public @Nullable ArrayList<String> getAttributionTags() {
if (mHistoricalUsage == null || mHistoricalUsage.getAttributedOpsCount() == 0) {
return null;
}
ArrayList<String> attributionTags = new ArrayList<>();
int count = mHistoricalUsage.getAttributedOpsCount();
for (int i = 0; i < count; i++) {
attributionTags.add(mHistoricalUsage.getAttributedOpsAt(i).getTag());
}
return attributionTags;
}
/** Creates a lookup from the attribution tag to its label. **/
@RequiresApi(Build.VERSION_CODES.S)
private static Map<String, Integer> getAttributionTagToLabelMap(
Attribution[] attributions) {
Map<String, Integer> attributionTagToLabelMap = new HashMap<>();
for (Attribution attribution : attributions) {
attributionTagToLabelMap.put(attribution.getTag(), attribution.getLabel());
}
return attributionTagToLabelMap;
}
/** Partitions the usages based on the attribution tag label. */
@RequiresApi(Build.VERSION_CODES.S)
public List<AttributionLabelledGroupUsage> getAttributionLabelledGroupUsages() {
Map<String, Integer> attributionTagToLabelMap =
getAttributionTagToLabelMap(getGroup().getApp().attributions);
Set<String> allOps = getAllOps(mGroup);
// we need to collect discreteAccessTime for each label
Map<Integer, AttributionLabelledGroupUsage.Builder> labelDiscreteAccessMap =
new HashMap<>();
for (int i = 0; i < mHistoricalUsage.getAttributedOpsCount(); i++) {
AttributedHistoricalOps attributedOp = mHistoricalUsage.getAttributedOpsAt(i);
String attributionTag = attributedOp.getTag();
for (String opName : allOps) {
final HistoricalOp historicalOp = attributedOp.getOp(opName);
if (historicalOp == null) {
continue;
}
int discreteAccessCount = historicalOp.getDiscreteAccessCount();
for (int j = 0; j < discreteAccessCount; j++) {
AttributedOpEntry opEntry = historicalOp.getDiscreteAccessAt(j);
Integer label = attributionTagToLabelMap.get(attributedOp.getTag());
if (!labelDiscreteAccessMap.containsKey(label)) {
labelDiscreteAccessMap.put(label,
new AttributionLabelledGroupUsage.Builder(label, getGroup()));
}
labelDiscreteAccessMap.get(label).addAttributionTag(attributionTag);
labelDiscreteAccessMap.get(label).addDiscreteAccessTime(new Triple<>(
opEntry.getLastAccessTime(PRIVACY_HUB_FLAGS),
opEntry.getLastDuration(PRIVACY_HUB_FLAGS),
opEntry.getLastProxyInfo(PRIVACY_HUB_FLAGS)));
}
}
}
return labelDiscreteAccessMap.entrySet().stream()
.map(e -> e.getValue().build())
.collect(Collectors.toList());
}
/**
* Represents the slice of {@link GroupUsage} with a label.
*
* <p> -1 as label means that there was no entry for the attribution tag in the
* manifest.</p>
*/
public static class AttributionLabelledGroupUsage implements TimelineUsage {
private final int mLabel;
private final AppPermissionGroup mAppPermissionGroup;
private final List<String> mAttributionTags;
private final List<Triple<Long, Long, OpEventProxyInfo>> mDiscreteAccessTime;
AttributionLabelledGroupUsage(int label,
AppPermissionGroup appPermissionGroup,
List<String> attributionTags,
List<Triple<Long, Long, OpEventProxyInfo>> discreteAccessTime) {
mLabel = label;
mAppPermissionGroup = appPermissionGroup;
mAttributionTags = attributionTags;
mDiscreteAccessTime = discreteAccessTime;
}
@Override
public int getLabel() {
return mLabel;
}
@Override
public boolean hasDiscreteData() {
return mDiscreteAccessTime.size() > 0;
}
@Override
public List<Triple<Long, Long, OpEventProxyInfo>> getAllDiscreteAccessTime() {
return mDiscreteAccessTime;
}
@Override
public List<String> getAttributionTags() {
return mAttributionTags;
}
@Override
public AppPermissionGroup getGroup() {
return mAppPermissionGroup;
}
static class Builder {
private final int mLabel;
private final AppPermissionGroup mAppPermissionGroup;
private Set<String> mAttributionTags;
private List<Triple<Long, Long, OpEventProxyInfo>> mDiscreteAccessTime;
Builder(int label, AppPermissionGroup appPermissionGroup) {
mLabel = label;
mAppPermissionGroup = appPermissionGroup;
mAttributionTags = new HashSet<>();
mDiscreteAccessTime = new ArrayList<>();
}
@NonNull Builder addAttributionTag(String attributionTag) {
mAttributionTags.add(attributionTag);
return this;
}
@NonNull
Builder addDiscreteAccessTime(
Triple<Long, Long, OpEventProxyInfo> discreteAccessTime) {
mDiscreteAccessTime.add(discreteAccessTime);
return this;
}
AttributionLabelledGroupUsage build() {
return new AttributionLabelledGroupUsage(mLabel,
mAppPermissionGroup,
new ArrayList<String>() {{
addAll(mAttributionTags);
}}, mDiscreteAccessTime);
}
}
}
}
public static class Builder {
private final @NonNull List<AppPermissionGroup> mGroups = new ArrayList<>();
private final @NonNull PermissionApp mPermissionApp;
private @Nullable PackageOps mLastUsage;
private @Nullable HistoricalPackageOps mHistoricalUsage;
private @Nullable ArrayList<AudioRecordingConfiguration> mAudioRecordingConfigurations;
public Builder(@NonNull PermissionApp permissionApp) {
mPermissionApp = permissionApp;
}
public @NonNull Builder addGroup(@NonNull AppPermissionGroup group) {
mGroups.add(group);
return this;
}
public @NonNull Builder setLastUsage(@Nullable PackageOps lastUsage) {
mLastUsage = lastUsage;
return this;
}
public @NonNull Builder setHistoricalUsage(@Nullable HistoricalPackageOps historicalUsage) {
mHistoricalUsage = historicalUsage;
return this;
}
public @NonNull Builder setRecordingConfiguration(
@Nullable ArrayList<AudioRecordingConfiguration> recordings) {
mAudioRecordingConfigurations = recordings;
return this;
}
public @NonNull AppPermissionUsage build() {
if (mGroups.isEmpty()) {
throw new IllegalStateException("mGroups cannot be empty.");
}
return new AppPermissionUsage(mPermissionApp, mGroups, mLastUsage, mHistoricalUsage,
mAudioRecordingConfigurations);
}
}
/** Usage for showing timeline view for a specific permission group with a label. */
public interface TimelineUsage {
/**
* Returns whether the usage has discrete data.
*/
boolean hasDiscreteData();
/**
* Returns all discrete access time in millis.
* Returns a list of triples of (access time, access duration, proxy)
*/
List<Triple<Long, Long, OpEventProxyInfo>> getAllDiscreteAccessTime();
/**
* Returns attribution tags for the usage.
*/
List<String> getAttributionTags();
/**
* Returns the permission group of the usage.
*/
AppPermissionGroup getGroup();
/**
* Returns the user facing string's resource id.
*
* <p> -1 means show the app name otherwise get the string resource from the app
* context.</p>
*/
int getLabel();
}
}