/* * 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 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 groups, @Nullable PackageOps lastUsage, @Nullable HistoricalPackageOps historicalUsage, @Nullable ArrayList 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 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 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> getAllDiscreteAccessTime() { List> allDiscreteAccessTime = new ArrayList<>(); if (!hasDiscreteData()) { return allDiscreteAccessTime; } Set 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 allOps = getAllOps(mGroup); final List 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 extractor) { long aggregate = 0; Set 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 extractor) { long aggregate = 0; Set allOps = getAllOps(mGroup); final List 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 getAllOps(AppPermissionGroup appPermissionGroup) { Set allOps = new HashSet<>(); List 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 getAttributionTags() { if (mHistoricalUsage == null || mHistoricalUsage.getAttributedOpsCount() == 0) { return null; } ArrayList 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 getAttributionTagToLabelMap( Attribution[] attributions) { Map 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 getAttributionLabelledGroupUsages() { Map attributionTagToLabelMap = getAttributionTagToLabelMap(getGroup().getApp().attributions); Set allOps = getAllOps(mGroup); // we need to collect discreteAccessTime for each label Map 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. * *

-1 as label means that there was no entry for the attribution tag in the * manifest.

*/ public static class AttributionLabelledGroupUsage implements TimelineUsage { private final int mLabel; private final AppPermissionGroup mAppPermissionGroup; private final List mAttributionTags; private final List> mDiscreteAccessTime; AttributionLabelledGroupUsage(int label, AppPermissionGroup appPermissionGroup, List attributionTags, List> 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> getAllDiscreteAccessTime() { return mDiscreteAccessTime; } @Override public List getAttributionTags() { return mAttributionTags; } @Override public AppPermissionGroup getGroup() { return mAppPermissionGroup; } static class Builder { private final int mLabel; private final AppPermissionGroup mAppPermissionGroup; private Set mAttributionTags; private List> 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 discreteAccessTime) { mDiscreteAccessTime.add(discreteAccessTime); return this; } AttributionLabelledGroupUsage build() { return new AttributionLabelledGroupUsage(mLabel, mAppPermissionGroup, new ArrayList() {{ addAll(mAttributionTags); }}, mDiscreteAccessTime); } } } } public static class Builder { private final @NonNull List mGroups = new ArrayList<>(); private final @NonNull PermissionApp mPermissionApp; private @Nullable PackageOps mLastUsage; private @Nullable HistoricalPackageOps mHistoricalUsage; private @Nullable ArrayList 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 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> getAllDiscreteAccessTime(); /** * Returns attribution tags for the usage. */ List getAttributionTags(); /** * Returns the permission group of the usage. */ AppPermissionGroup getGroup(); /** * Returns the user facing string's resource id. * *

-1 means show the app name otherwise get the string resource from the app * context.

*/ int getLabel(); } }