| /* |
| * Copyright (C) 2021 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.appop; |
| |
| import static android.app.AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE; |
| import static android.app.AppOpsManager.ATTRIBUTION_FLAG_ACCESSOR; |
| import static android.app.AppOpsManager.ATTRIBUTION_FLAG_RECEIVER; |
| import static android.app.AppOpsManager.ATTRIBUTION_FLAG_TRUSTED; |
| import static android.app.AppOpsManager.FILTER_BY_ATTRIBUTION_TAG; |
| import static android.app.AppOpsManager.FILTER_BY_OP_NAMES; |
| import static android.app.AppOpsManager.FILTER_BY_PACKAGE_NAME; |
| import static android.app.AppOpsManager.FILTER_BY_UID; |
| import static android.app.AppOpsManager.OP_CAMERA; |
| import static android.app.AppOpsManager.OP_COARSE_LOCATION; |
| import static android.app.AppOpsManager.OP_FINE_LOCATION; |
| import static android.app.AppOpsManager.OP_FLAGS_ALL; |
| import static android.app.AppOpsManager.OP_FLAG_SELF; |
| import static android.app.AppOpsManager.OP_FLAG_TRUSTED_PROXIED; |
| import static android.app.AppOpsManager.OP_FLAG_TRUSTED_PROXY; |
| import static android.app.AppOpsManager.OP_NONE; |
| import static android.app.AppOpsManager.OP_PHONE_CALL_CAMERA; |
| import static android.app.AppOpsManager.OP_PHONE_CALL_MICROPHONE; |
| import static android.app.AppOpsManager.OP_RECORD_AUDIO; |
| import static android.app.AppOpsManager.flagsToString; |
| import static android.app.AppOpsManager.getUidStateName; |
| |
| import static java.lang.Long.min; |
| import static java.lang.Math.max; |
| |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.app.AppOpsManager; |
| import android.os.AsyncTask; |
| import android.os.Build; |
| import android.os.Environment; |
| import android.os.FileUtils; |
| import android.provider.DeviceConfig; |
| import android.util.ArrayMap; |
| import android.util.AtomicFile; |
| import android.util.Slog; |
| import android.util.TypedXmlPullParser; |
| import android.util.TypedXmlSerializer; |
| import android.util.Xml; |
| |
| import com.android.internal.annotations.GuardedBy; |
| import com.android.internal.util.ArrayUtils; |
| import com.android.internal.util.XmlUtils; |
| |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.FileNotFoundException; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.io.PrintWriter; |
| import java.text.SimpleDateFormat; |
| import java.time.Duration; |
| import java.time.Instant; |
| import java.time.temporal.ChronoUnit; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.Date; |
| import java.util.List; |
| import java.util.Objects; |
| import java.util.Set; |
| |
| /** |
| * This class manages information about recent accesses to ops for permission usage timeline. |
| * |
| * The discrete history is kept for limited time (initial default is 24 hours, set in |
| * {@link DiscreteRegistry#sDiscreteHistoryCutoff) and discarded after that. |
| * |
| * Discrete history is quantized to reduce resources footprint. By default quantization is set to |
| * one minute in {@link DiscreteRegistry#sDiscreteHistoryQuantization}. All access times are aligned |
| * to the closest quantized time. All durations (except -1, meaning no duration) are rounded up to |
| * the closest quantized interval. |
| * |
| * When data is queried through API, events are deduplicated and for every time quant there can |
| * be only one {@link AppOpsManager.AttributedOpEntry}. Each entry contains information about |
| * different accesses which happened in specified time quant - across dimensions of |
| * {@link AppOpsManager.UidState} and {@link AppOpsManager.OpFlags}. For each dimension |
| * it is only possible to know if at least one access happened in the time quant. |
| * |
| * Every time state is saved (default is 30 minutes), memory state is dumped to a |
| * new file and memory state is cleared. Files older than time limit are deleted |
| * during the process. |
| * |
| * When request comes in, files are read and requested information is collected |
| * and delivered. Information is cached in memory until the next state save (up to 30 minutes), to |
| * avoid reading disk if more API calls come in a quick succession. |
| * |
| * THREADING AND LOCKING: |
| * For in-memory transactions this class relies on {@link DiscreteRegistry#mInMemoryLock}. It is |
| * assumed that the same lock is used for in-memory transactions in {@link AppOpsService}, |
| * {@link HistoricalRegistry}, and {@link DiscreteRegistry}. |
| * {@link DiscreteRegistry#recordDiscreteAccess(int, String, int, String, int, int, long, long)} |
| * must only be called while holding this lock. |
| * {@link DiscreteRegistry#mOnDiskLock} is used when disk transactions are performed. |
| * It is very important to release {@link DiscreteRegistry#mInMemoryLock} as soon as possible, as |
| * no AppOps related transactions across the system can be performed while it is held. |
| * |
| * INITIALIZATION: We can initialize persistence only after the system is ready |
| * as we need to check the optional configuration override from the settings |
| * database which is not initialized at the time the app ops service is created. This class |
| * relies on {@link HistoricalRegistry} for controlling that no calls are allowed until then. All |
| * outside calls are going through {@link HistoricalRegistry}, where |
| * {@link HistoricalRegistry#isPersistenceInitializedMLocked()} check is done. |
| * |
| */ |
| |
| final class DiscreteRegistry { |
| static final String DISCRETE_HISTORY_FILE_SUFFIX = "tl"; |
| private static final String TAG = DiscreteRegistry.class.getSimpleName(); |
| |
| private static final String PROPERTY_DISCRETE_HISTORY_CUTOFF = "discrete_history_cutoff_millis"; |
| private static final String PROPERTY_DISCRETE_HISTORY_QUANTIZATION = |
| "discrete_history_quantization_millis"; |
| private static final String PROPERTY_DISCRETE_FLAGS = "discrete_history_op_flags"; |
| private static final String PROPERTY_DISCRETE_OPS_LIST = "discrete_history_ops_cslist"; |
| private static final String DEFAULT_DISCRETE_OPS = OP_FINE_LOCATION + "," + OP_COARSE_LOCATION |
| + "," + OP_CAMERA + "," + OP_RECORD_AUDIO + "," + OP_PHONE_CALL_MICROPHONE + "," |
| + OP_PHONE_CALL_CAMERA; |
| private static final long DEFAULT_DISCRETE_HISTORY_CUTOFF = Duration.ofHours(24).toMillis(); |
| private static final long MAXIMUM_DISCRETE_HISTORY_CUTOFF = Duration.ofDays(30).toMillis(); |
| private static final long DEFAULT_DISCRETE_HISTORY_QUANTIZATION = |
| Duration.ofMinutes(1).toMillis(); |
| |
| private static long sDiscreteHistoryCutoff; |
| private static long sDiscreteHistoryQuantization; |
| private static int[] sDiscreteOps; |
| private static int sDiscreteFlags; |
| |
| private static final String TAG_HISTORY = "h"; |
| private static final String ATTR_VERSION = "v"; |
| private static final String ATTR_LARGEST_CHAIN_ID = "lc"; |
| private static final int CURRENT_VERSION = 1; |
| |
| private static final String TAG_UID = "u"; |
| private static final String ATTR_UID = "ui"; |
| |
| private static final String TAG_PACKAGE = "p"; |
| private static final String ATTR_PACKAGE_NAME = "pn"; |
| |
| private static final String TAG_OP = "o"; |
| private static final String ATTR_OP_ID = "op"; |
| |
| private static final String TAG_TAG = "a"; |
| private static final String ATTR_TAG = "at"; |
| |
| private static final String TAG_ENTRY = "e"; |
| private static final String ATTR_NOTE_TIME = "nt"; |
| private static final String ATTR_NOTE_DURATION = "nd"; |
| private static final String ATTR_UID_STATE = "us"; |
| private static final String ATTR_FLAGS = "f"; |
| private static final String ATTR_ATTRIBUTION_FLAGS = "af"; |
| private static final String ATTR_CHAIN_ID = "ci"; |
| |
| private static final int OP_FLAGS_DISCRETE = OP_FLAG_SELF | OP_FLAG_TRUSTED_PROXIED |
| | OP_FLAG_TRUSTED_PROXY; |
| |
| // Lock for read/write access to on disk state |
| private final Object mOnDiskLock = new Object(); |
| |
| //Lock for read/write access to in memory state |
| private final @NonNull Object mInMemoryLock; |
| |
| @GuardedBy("mOnDiskLock") |
| private File mDiscreteAccessDir; |
| |
| @GuardedBy("mInMemoryLock") |
| private DiscreteOps mDiscreteOps; |
| |
| @GuardedBy("mOnDiskLock") |
| private DiscreteOps mCachedOps = null; |
| |
| private boolean mDebugMode = false; |
| |
| DiscreteRegistry(Object inMemoryLock) { |
| mInMemoryLock = inMemoryLock; |
| synchronized (mOnDiskLock) { |
| mDiscreteAccessDir = new File( |
| new File(Environment.getDataSystemDirectory(), "appops"), |
| "discrete"); |
| createDiscreteAccessDirLocked(); |
| int largestChainId = readLargestChainIdFromDiskLocked(); |
| synchronized (mInMemoryLock) { |
| mDiscreteOps = new DiscreteOps(largestChainId); |
| } |
| } |
| } |
| |
| void systemReady() { |
| DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_PRIVACY, |
| AsyncTask.THREAD_POOL_EXECUTOR, (DeviceConfig.Properties p) -> { |
| setDiscreteHistoryParameters(p); |
| }); |
| setDiscreteHistoryParameters(DeviceConfig.getProperties(DeviceConfig.NAMESPACE_PRIVACY)); |
| } |
| |
| private void setDiscreteHistoryParameters(DeviceConfig.Properties p) { |
| if (p.getKeyset().contains(PROPERTY_DISCRETE_HISTORY_CUTOFF)) { |
| sDiscreteHistoryCutoff = p.getLong(PROPERTY_DISCRETE_HISTORY_CUTOFF, |
| DEFAULT_DISCRETE_HISTORY_CUTOFF); |
| if (!Build.IS_DEBUGGABLE && !mDebugMode) { |
| sDiscreteHistoryCutoff = min(MAXIMUM_DISCRETE_HISTORY_CUTOFF, |
| sDiscreteHistoryCutoff); |
| } |
| } else { |
| sDiscreteHistoryCutoff = DEFAULT_DISCRETE_HISTORY_CUTOFF; |
| } |
| if (p.getKeyset().contains(PROPERTY_DISCRETE_HISTORY_QUANTIZATION)) { |
| sDiscreteHistoryQuantization = p.getLong(PROPERTY_DISCRETE_HISTORY_QUANTIZATION, |
| DEFAULT_DISCRETE_HISTORY_QUANTIZATION); |
| if (!Build.IS_DEBUGGABLE && !mDebugMode) { |
| sDiscreteHistoryQuantization = max(DEFAULT_DISCRETE_HISTORY_QUANTIZATION, |
| sDiscreteHistoryQuantization); |
| } |
| } else { |
| sDiscreteHistoryQuantization = DEFAULT_DISCRETE_HISTORY_QUANTIZATION; |
| } |
| sDiscreteFlags = p.getKeyset().contains(PROPERTY_DISCRETE_FLAGS) ? sDiscreteFlags = |
| p.getInt(PROPERTY_DISCRETE_FLAGS, OP_FLAGS_DISCRETE) : OP_FLAGS_DISCRETE; |
| sDiscreteOps = p.getKeyset().contains(PROPERTY_DISCRETE_OPS_LIST) ? parseOpsList( |
| p.getString(PROPERTY_DISCRETE_OPS_LIST, DEFAULT_DISCRETE_OPS)) : parseOpsList( |
| DEFAULT_DISCRETE_OPS); |
| } |
| |
| void recordDiscreteAccess(int uid, String packageName, int op, @Nullable String attributionTag, |
| @AppOpsManager.OpFlags int flags, @AppOpsManager.UidState int uidState, long accessTime, |
| long accessDuration, @AppOpsManager.AttributionFlags int attributionFlags, |
| int attributionChainId) { |
| if (!isDiscreteOp(op, flags)) { |
| return; |
| } |
| synchronized (mInMemoryLock) { |
| mDiscreteOps.addDiscreteAccess(op, uid, packageName, attributionTag, flags, uidState, |
| accessTime, accessDuration, attributionFlags, attributionChainId); |
| } |
| } |
| |
| void writeAndClearAccessHistory() { |
| synchronized (mOnDiskLock) { |
| if (mDiscreteAccessDir == null) { |
| Slog.d(TAG, "State not saved - persistence not initialized."); |
| return; |
| } |
| DiscreteOps discreteOps; |
| synchronized (mInMemoryLock) { |
| discreteOps = mDiscreteOps; |
| mDiscreteOps = new DiscreteOps(discreteOps.mChainIdOffset); |
| mCachedOps = null; |
| } |
| deleteOldDiscreteHistoryFilesLocked(); |
| if (!discreteOps.isEmpty()) { |
| persistDiscreteOpsLocked(discreteOps); |
| } |
| } |
| } |
| |
| void addFilteredDiscreteOpsToHistoricalOps(AppOpsManager.HistoricalOps result, |
| long beginTimeMillis, long endTimeMillis, |
| @AppOpsManager.HistoricalOpsRequestFilter int filter, int uidFilter, |
| @Nullable String packageNameFilter, @Nullable String[] opNamesFilter, |
| @Nullable String attributionTagFilter, @AppOpsManager.OpFlags int flagsFilter, |
| Set<String> attributionExemptPkgs) { |
| boolean assembleChains = attributionExemptPkgs != null; |
| DiscreteOps discreteOps = getAllDiscreteOps(); |
| ArrayMap<Integer, AttributionChain> attributionChains = new ArrayMap<>(); |
| if (assembleChains) { |
| attributionChains = createAttributionChains(discreteOps, attributionExemptPkgs); |
| } |
| beginTimeMillis = max(beginTimeMillis, Instant.now().minus(sDiscreteHistoryCutoff, |
| ChronoUnit.MILLIS).toEpochMilli()); |
| discreteOps.filter(beginTimeMillis, endTimeMillis, filter, uidFilter, packageNameFilter, |
| opNamesFilter, attributionTagFilter, flagsFilter, attributionChains); |
| discreteOps.applyToHistoricalOps(result, attributionChains); |
| return; |
| } |
| |
| private int readLargestChainIdFromDiskLocked() { |
| final File[] files = mDiscreteAccessDir.listFiles(); |
| if (files != null && files.length > 0) { |
| File latestFile = null; |
| long latestFileTimestamp = 0; |
| for (File f : files) { |
| final String fileName = f.getName(); |
| if (!fileName.endsWith(DISCRETE_HISTORY_FILE_SUFFIX)) { |
| continue; |
| } |
| long timestamp = Long.valueOf(fileName.substring(0, |
| fileName.length() - DISCRETE_HISTORY_FILE_SUFFIX.length())); |
| if (latestFileTimestamp < timestamp) { |
| latestFile = f; |
| latestFileTimestamp = timestamp; |
| } |
| } |
| if (latestFile == null) { |
| return 0; |
| } |
| FileInputStream stream; |
| try { |
| stream = new FileInputStream(latestFile); |
| } catch (FileNotFoundException e) { |
| return 0; |
| } |
| try { |
| TypedXmlPullParser parser = Xml.resolvePullParser(stream); |
| XmlUtils.beginDocument(parser, TAG_HISTORY); |
| |
| final int largestChainId = parser.getAttributeInt(null, ATTR_LARGEST_CHAIN_ID, 0); |
| return largestChainId; |
| } catch (Throwable t) { |
| return 0; |
| } finally { |
| try { |
| stream.close(); |
| } catch (IOException e) { |
| } |
| } |
| } else { |
| return 0; |
| } |
| } |
| |
| private ArrayMap<Integer, AttributionChain> createAttributionChains( |
| DiscreteOps discreteOps, Set<String> attributionExemptPkgs) { |
| ArrayMap<Integer, AttributionChain> chains = new ArrayMap<>(); |
| int nUids = discreteOps.mUids.size(); |
| for (int uidNum = 0; uidNum < nUids; uidNum++) { |
| ArrayMap<String, DiscretePackageOps> pkgs = discreteOps.mUids.valueAt(uidNum).mPackages; |
| int uid = discreteOps.mUids.keyAt(uidNum); |
| int nPackages = pkgs.size(); |
| for (int pkgNum = 0; pkgNum < nPackages; pkgNum++) { |
| ArrayMap<Integer, DiscreteOp> ops = pkgs.valueAt(pkgNum).mPackageOps; |
| String pkg = pkgs.keyAt(pkgNum); |
| int nOps = ops.size(); |
| for (int opNum = 0; opNum < nOps; opNum++) { |
| ArrayMap<String, List<DiscreteOpEvent>> attrOps = |
| ops.valueAt(opNum).mAttributedOps; |
| int op = ops.keyAt(opNum); |
| int nAttrOps = attrOps.size(); |
| for (int attrOpNum = 0; attrOpNum < nAttrOps; attrOpNum++) { |
| List<DiscreteOpEvent> opEvents = attrOps.valueAt(attrOpNum); |
| String attributionTag = attrOps.keyAt(attrOpNum); |
| int nOpEvents = opEvents.size(); |
| for (int opEventNum = 0; opEventNum < nOpEvents; opEventNum++) { |
| DiscreteOpEvent event = opEvents.get(opEventNum); |
| if (event == null |
| || event.mAttributionChainId == ATTRIBUTION_CHAIN_ID_NONE |
| || (event.mAttributionFlags & ATTRIBUTION_FLAG_TRUSTED) == 0) { |
| continue; |
| } |
| |
| if (!chains.containsKey(event.mAttributionChainId)) { |
| chains.put(event.mAttributionChainId, |
| new AttributionChain(attributionExemptPkgs)); |
| } |
| chains.get(event.mAttributionChainId) |
| .addEvent(pkg, uid, attributionTag, op, event); |
| } |
| } |
| } |
| } |
| } |
| return chains; |
| } |
| |
| private void readDiscreteOpsFromDisk(DiscreteOps discreteOps) { |
| synchronized (mOnDiskLock) { |
| long beginTimeMillis = Instant.now().minus(sDiscreteHistoryCutoff, |
| ChronoUnit.MILLIS).toEpochMilli(); |
| |
| final File[] files = mDiscreteAccessDir.listFiles(); |
| if (files != null && files.length > 0) { |
| for (File f : files) { |
| final String fileName = f.getName(); |
| if (!fileName.endsWith(DISCRETE_HISTORY_FILE_SUFFIX)) { |
| continue; |
| } |
| long timestamp = Long.valueOf(fileName.substring(0, |
| fileName.length() - DISCRETE_HISTORY_FILE_SUFFIX.length())); |
| if (timestamp < beginTimeMillis) { |
| continue; |
| } |
| discreteOps.readFromFile(f, beginTimeMillis); |
| } |
| } |
| } |
| } |
| |
| void clearHistory() { |
| synchronized (mOnDiskLock) { |
| synchronized (mInMemoryLock) { |
| mDiscreteOps = new DiscreteOps(0); |
| } |
| clearOnDiskHistoryLocked(); |
| } |
| } |
| |
| void clearHistory(int uid, String packageName) { |
| synchronized (mOnDiskLock) { |
| DiscreteOps discreteOps; |
| synchronized (mInMemoryLock) { |
| discreteOps = getAllDiscreteOps(); |
| clearHistory(); |
| } |
| discreteOps.clearHistory(uid, packageName); |
| persistDiscreteOpsLocked(discreteOps); |
| } |
| } |
| |
| void offsetHistory(long offset) { |
| synchronized (mOnDiskLock) { |
| DiscreteOps discreteOps; |
| synchronized (mInMemoryLock) { |
| discreteOps = getAllDiscreteOps(); |
| clearHistory(); |
| } |
| discreteOps.offsetHistory(offset); |
| persistDiscreteOpsLocked(discreteOps); |
| } |
| } |
| |
| void dump(@NonNull PrintWriter pw, int uidFilter, @Nullable String packageNameFilter, |
| @Nullable String attributionTagFilter, |
| @AppOpsManager.HistoricalOpsRequestFilter int filter, int dumpOp, |
| @NonNull SimpleDateFormat sdf, @NonNull Date date, @NonNull String prefix, |
| int nDiscreteOps) { |
| DiscreteOps discreteOps = getAllDiscreteOps(); |
| String[] opNamesFilter = dumpOp == OP_NONE ? null |
| : new String[]{AppOpsManager.opToPublicName(dumpOp)}; |
| discreteOps.filter(0, Instant.now().toEpochMilli(), filter, uidFilter, packageNameFilter, |
| opNamesFilter, attributionTagFilter, OP_FLAGS_ALL, new ArrayMap<>()); |
| pw.print(prefix); |
| pw.print("Largest chain id: "); |
| pw.print(mDiscreteOps.mLargestChainId); |
| pw.println(); |
| discreteOps.dump(pw, sdf, date, prefix, nDiscreteOps); |
| } |
| |
| private void clearOnDiskHistoryLocked() { |
| mCachedOps = null; |
| FileUtils.deleteContentsAndDir(mDiscreteAccessDir); |
| createDiscreteAccessDir(); |
| } |
| |
| private DiscreteOps getAllDiscreteOps() { |
| DiscreteOps discreteOps = new DiscreteOps(0); |
| |
| synchronized (mOnDiskLock) { |
| synchronized (mInMemoryLock) { |
| discreteOps.merge(mDiscreteOps); |
| } |
| if (mCachedOps == null) { |
| mCachedOps = new DiscreteOps(0); |
| readDiscreteOpsFromDisk(mCachedOps); |
| } |
| discreteOps.merge(mCachedOps); |
| return discreteOps; |
| } |
| } |
| |
| /** |
| * Represents a chain of usages, each attributing its usage to the one before it |
| */ |
| private static final class AttributionChain { |
| private static final class OpEvent { |
| String mPkgName; |
| int mUid; |
| String mAttributionTag; |
| int mOpCode; |
| DiscreteOpEvent mOpEvent; |
| |
| OpEvent(String pkgName, int uid, String attributionTag, int opCode, |
| DiscreteOpEvent event) { |
| mPkgName = pkgName; |
| mUid = uid; |
| mAttributionTag = attributionTag; |
| mOpCode = opCode; |
| mOpEvent = event; |
| } |
| |
| public boolean matches(String pkgName, int uid, String attributionTag, int opCode, |
| DiscreteOpEvent event) { |
| return Objects.equals(pkgName, mPkgName) && mUid == uid |
| && Objects.equals(attributionTag, mAttributionTag) && mOpCode == opCode |
| && mOpEvent.mAttributionChainId == event.mAttributionChainId |
| && mOpEvent.mAttributionFlags == event.mAttributionFlags |
| && mOpEvent.mNoteTime == event.mNoteTime; |
| } |
| |
| public boolean packageOpEquals(OpEvent other) { |
| return Objects.equals(other.mPkgName, mPkgName) && other.mUid == mUid |
| && Objects.equals(other.mAttributionTag, mAttributionTag) |
| && mOpCode == other.mOpCode; |
| } |
| |
| public boolean equalsExceptDuration(OpEvent other) { |
| if (other.mOpEvent.mNoteDuration == mOpEvent.mNoteDuration) { |
| return false; |
| } |
| return packageOpEquals(other) && mOpEvent.equalsExceptDuration(other.mOpEvent); |
| } |
| } |
| |
| ArrayList<OpEvent> mChain = new ArrayList<>(); |
| Set<String> mExemptPkgs; |
| OpEvent mStartEvent = null; |
| OpEvent mLastVisibleEvent = null; |
| |
| AttributionChain(Set<String> exemptPkgs) { |
| mExemptPkgs = exemptPkgs; |
| } |
| |
| boolean isComplete() { |
| return !mChain.isEmpty() && getStart() != null && isEnd(mChain.get(mChain.size() - 1)); |
| } |
| |
| boolean isStart(String pkgName, int uid, String attributionTag, int op, |
| DiscreteOpEvent opEvent) { |
| if (mStartEvent == null || opEvent == null) { |
| return false; |
| } |
| return mStartEvent.matches(pkgName, uid, attributionTag, op, opEvent); |
| } |
| |
| private OpEvent getStart() { |
| return mChain.isEmpty() || !isStart(mChain.get(0)) ? null : mChain.get(0); |
| } |
| |
| private OpEvent getLastVisible() { |
| // Search all nodes but the first one, which is the start node |
| for (int i = mChain.size() - 1; i > 0; i--) { |
| OpEvent event = mChain.get(i); |
| if (!mExemptPkgs.contains(event.mPkgName)) { |
| return event; |
| } |
| } |
| return null; |
| } |
| |
| void addEvent(String pkgName, int uid, String attributionTag, int op, |
| DiscreteOpEvent opEvent) { |
| OpEvent event = new OpEvent(pkgName, uid, attributionTag, op, opEvent); |
| |
| // check if we have a matching event, without duration, replacing duration otherwise |
| for (int i = 0; i < mChain.size(); i++) { |
| OpEvent item = mChain.get(i); |
| if (item.equalsExceptDuration(event)) { |
| if (event.mOpEvent.mNoteDuration != -1) { |
| item.mOpEvent = event.mOpEvent; |
| } |
| return; |
| } |
| } |
| |
| if (mChain.isEmpty() || isEnd(event)) { |
| mChain.add(event); |
| } else if (isStart(event)) { |
| mChain.add(0, event); |
| |
| } else { |
| for (int i = 0; i < mChain.size(); i++) { |
| OpEvent currEvent = mChain.get(i); |
| if ((!isStart(currEvent) |
| && currEvent.mOpEvent.mNoteTime > event.mOpEvent.mNoteTime) |
| || i == mChain.size() - 1 && isEnd(currEvent)) { |
| mChain.add(i, event); |
| break; |
| } else if (i == mChain.size() - 1) { |
| mChain.add(event); |
| break; |
| } |
| } |
| } |
| mStartEvent = isComplete() ? getStart() : null; |
| mLastVisibleEvent = isComplete() ? getLastVisible() : null; |
| } |
| |
| private boolean isEnd(OpEvent event) { |
| return event != null |
| && (event.mOpEvent.mAttributionFlags & ATTRIBUTION_FLAG_ACCESSOR) != 0; |
| } |
| |
| private boolean isStart(OpEvent event) { |
| return event != null |
| && (event.mOpEvent.mAttributionFlags & ATTRIBUTION_FLAG_RECEIVER) != 0; |
| } |
| } |
| |
| private final class DiscreteOps { |
| ArrayMap<Integer, DiscreteUidOps> mUids; |
| int mChainIdOffset; |
| int mLargestChainId; |
| |
| DiscreteOps(int chainIdOffset) { |
| mUids = new ArrayMap<>(); |
| mChainIdOffset = chainIdOffset; |
| mLargestChainId = chainIdOffset; |
| } |
| |
| boolean isEmpty() { |
| return mUids.isEmpty(); |
| } |
| |
| void merge(DiscreteOps other) { |
| mLargestChainId = max(mLargestChainId, other.mLargestChainId); |
| int nUids = other.mUids.size(); |
| for (int i = 0; i < nUids; i++) { |
| int uid = other.mUids.keyAt(i); |
| DiscreteUidOps uidOps = other.mUids.valueAt(i); |
| getOrCreateDiscreteUidOps(uid).merge(uidOps); |
| } |
| } |
| |
| void addDiscreteAccess(int op, int uid, @NonNull String packageName, |
| @Nullable String attributionTag, @AppOpsManager.OpFlags int flags, |
| @AppOpsManager.UidState int uidState, long accessTime, long accessDuration, |
| @AppOpsManager.AttributionFlags int attributionFlags, int attributionChainId) { |
| int offsetChainId = attributionChainId; |
| if (attributionChainId != ATTRIBUTION_CHAIN_ID_NONE) { |
| offsetChainId = attributionChainId + mChainIdOffset; |
| if (offsetChainId > mLargestChainId) { |
| mLargestChainId = offsetChainId; |
| } else if (offsetChainId < 0) { |
| // handle overflow |
| offsetChainId = 0; |
| mLargestChainId = 0; |
| mChainIdOffset = -1 * attributionChainId; |
| } |
| } |
| getOrCreateDiscreteUidOps(uid).addDiscreteAccess(op, packageName, attributionTag, flags, |
| uidState, accessTime, accessDuration, attributionFlags, offsetChainId); |
| } |
| |
| private void filter(long beginTimeMillis, long endTimeMillis, |
| @AppOpsManager.HistoricalOpsRequestFilter int filter, int uidFilter, |
| @Nullable String packageNameFilter, @Nullable String[] opNamesFilter, |
| @Nullable String attributionTagFilter, @AppOpsManager.OpFlags int flagsFilter, |
| ArrayMap<Integer, AttributionChain> attributionChains) { |
| if ((filter & FILTER_BY_UID) != 0) { |
| ArrayMap<Integer, DiscreteUidOps> uids = new ArrayMap<>(); |
| uids.put(uidFilter, getOrCreateDiscreteUidOps(uidFilter)); |
| mUids = uids; |
| } |
| int nUids = mUids.size(); |
| for (int i = nUids - 1; i >= 0; i--) { |
| mUids.valueAt(i).filter(beginTimeMillis, endTimeMillis, filter, packageNameFilter, |
| opNamesFilter, attributionTagFilter, flagsFilter, mUids.keyAt(i), |
| attributionChains); |
| if (mUids.valueAt(i).isEmpty()) { |
| mUids.removeAt(i); |
| } |
| } |
| } |
| |
| private void offsetHistory(long offset) { |
| int nUids = mUids.size(); |
| for (int i = 0; i < nUids; i++) { |
| mUids.valueAt(i).offsetHistory(offset); |
| } |
| } |
| |
| private void clearHistory(int uid, String packageName) { |
| if (mUids.containsKey(uid)) { |
| mUids.get(uid).clearPackage(packageName); |
| if (mUids.get(uid).isEmpty()) { |
| mUids.remove(uid); |
| } |
| } |
| } |
| |
| private void applyToHistoricalOps(AppOpsManager.HistoricalOps result, |
| ArrayMap<Integer, AttributionChain> attributionChains) { |
| int nUids = mUids.size(); |
| for (int i = 0; i < nUids; i++) { |
| mUids.valueAt(i).applyToHistory(result, mUids.keyAt(i), attributionChains); |
| } |
| } |
| |
| private void writeToStream(FileOutputStream stream) throws Exception { |
| TypedXmlSerializer out = Xml.resolveSerializer(stream); |
| |
| out.startDocument(null, true); |
| out.startTag(null, TAG_HISTORY); |
| out.attributeInt(null, ATTR_VERSION, CURRENT_VERSION); |
| out.attributeInt(null, ATTR_LARGEST_CHAIN_ID, mLargestChainId); |
| |
| int nUids = mUids.size(); |
| for (int i = 0; i < nUids; i++) { |
| out.startTag(null, TAG_UID); |
| out.attributeInt(null, ATTR_UID, mUids.keyAt(i)); |
| mUids.valueAt(i).serialize(out); |
| out.endTag(null, TAG_UID); |
| } |
| out.endTag(null, TAG_HISTORY); |
| out.endDocument(); |
| } |
| |
| private void dump(@NonNull PrintWriter pw, @NonNull SimpleDateFormat sdf, |
| @NonNull Date date, @NonNull String prefix, int nDiscreteOps) { |
| int nUids = mUids.size(); |
| for (int i = 0; i < nUids; i++) { |
| pw.print(prefix); |
| pw.print("Uid: "); |
| pw.print(mUids.keyAt(i)); |
| pw.println(); |
| mUids.valueAt(i).dump(pw, sdf, date, prefix + " ", nDiscreteOps); |
| } |
| } |
| |
| private DiscreteUidOps getOrCreateDiscreteUidOps(int uid) { |
| DiscreteUidOps result = mUids.get(uid); |
| if (result == null) { |
| result = new DiscreteUidOps(); |
| mUids.put(uid, result); |
| } |
| return result; |
| } |
| |
| private void readFromFile(File f, long beginTimeMillis) { |
| FileInputStream stream; |
| try { |
| stream = new FileInputStream(f); |
| } catch (FileNotFoundException e) { |
| return; |
| } |
| try { |
| TypedXmlPullParser parser = Xml.resolvePullParser(stream); |
| XmlUtils.beginDocument(parser, TAG_HISTORY); |
| |
| // We haven't released version 1 and have more detailed |
| // accounting - just nuke the current state |
| final int version = parser.getAttributeInt(null, ATTR_VERSION); |
| if (version != CURRENT_VERSION) { |
| throw new IllegalStateException("Dropping unsupported discrete history " + f); |
| } |
| int depth = parser.getDepth(); |
| while (XmlUtils.nextElementWithin(parser, depth)) { |
| if (TAG_UID.equals(parser.getName())) { |
| int uid = parser.getAttributeInt(null, ATTR_UID, -1); |
| getOrCreateDiscreteUidOps(uid).deserialize(parser, beginTimeMillis); |
| } |
| } |
| } catch (Throwable t) { |
| Slog.e(TAG, "Failed to read file " + f.getName() + " " + t.getMessage() + " " |
| + Arrays.toString(t.getStackTrace())); |
| } finally { |
| try { |
| stream.close(); |
| } catch (IOException e) { |
| } |
| } |
| } |
| } |
| |
| private void createDiscreteAccessDir() { |
| if (!mDiscreteAccessDir.exists()) { |
| if (!mDiscreteAccessDir.mkdirs()) { |
| Slog.e(TAG, "Failed to create DiscreteRegistry directory"); |
| } |
| FileUtils.setPermissions(mDiscreteAccessDir.getPath(), |
| FileUtils.S_IRWXU | FileUtils.S_IRWXG | FileUtils.S_IXOTH, -1, -1); |
| } |
| } |
| |
| private void persistDiscreteOpsLocked(DiscreteOps discreteOps) { |
| long currentTimeStamp = Instant.now().toEpochMilli(); |
| final AtomicFile file = new AtomicFile(new File(mDiscreteAccessDir, |
| currentTimeStamp + DISCRETE_HISTORY_FILE_SUFFIX)); |
| FileOutputStream stream = null; |
| try { |
| stream = file.startWrite(); |
| discreteOps.writeToStream(stream); |
| file.finishWrite(stream); |
| } catch (Throwable t) { |
| Slog.e(TAG, |
| "Error writing timeline state: " + t.getMessage() + " " |
| + Arrays.toString(t.getStackTrace())); |
| if (stream != null) { |
| file.failWrite(stream); |
| } |
| } |
| } |
| |
| private void deleteOldDiscreteHistoryFilesLocked() { |
| final File[] files = mDiscreteAccessDir.listFiles(); |
| if (files != null && files.length > 0) { |
| for (File f : files) { |
| final String fileName = f.getName(); |
| if (!fileName.endsWith(DISCRETE_HISTORY_FILE_SUFFIX)) { |
| continue; |
| } |
| try { |
| long timestamp = Long.valueOf(fileName.substring(0, |
| fileName.length() - DISCRETE_HISTORY_FILE_SUFFIX.length())); |
| if (Instant.now().minus(sDiscreteHistoryCutoff, |
| ChronoUnit.MILLIS).toEpochMilli() > timestamp) { |
| f.delete(); |
| Slog.e(TAG, "Deleting file " + fileName); |
| |
| } |
| } catch (Throwable t) { |
| Slog.e(TAG, "Error while cleaning timeline files: " + t.getMessage() + " " |
| + t.getStackTrace()); |
| } |
| } |
| } |
| } |
| |
| private void createDiscreteAccessDirLocked() { |
| if (!mDiscreteAccessDir.exists()) { |
| if (!mDiscreteAccessDir.mkdirs()) { |
| Slog.e(TAG, "Failed to create DiscreteRegistry directory"); |
| } |
| FileUtils.setPermissions(mDiscreteAccessDir.getPath(), |
| FileUtils.S_IRWXU | FileUtils.S_IRWXG | FileUtils.S_IXOTH, -1, -1); |
| } |
| } |
| |
| private final class DiscreteUidOps { |
| ArrayMap<String, DiscretePackageOps> mPackages; |
| |
| DiscreteUidOps() { |
| mPackages = new ArrayMap<>(); |
| } |
| |
| boolean isEmpty() { |
| return mPackages.isEmpty(); |
| } |
| |
| void merge(DiscreteUidOps other) { |
| int nPackages = other.mPackages.size(); |
| for (int i = 0; i < nPackages; i++) { |
| String packageName = other.mPackages.keyAt(i); |
| DiscretePackageOps p = other.mPackages.valueAt(i); |
| getOrCreateDiscretePackageOps(packageName).merge(p); |
| } |
| } |
| |
| private void filter(long beginTimeMillis, long endTimeMillis, |
| @AppOpsManager.HistoricalOpsRequestFilter int filter, |
| @Nullable String packageNameFilter, @Nullable String[] opNamesFilter, |
| @Nullable String attributionTagFilter, @AppOpsManager.OpFlags int flagsFilter, |
| int currentUid, ArrayMap<Integer, AttributionChain> attributionChains) { |
| if ((filter & FILTER_BY_PACKAGE_NAME) != 0) { |
| ArrayMap<String, DiscretePackageOps> packages = new ArrayMap<>(); |
| packages.put(packageNameFilter, getOrCreateDiscretePackageOps(packageNameFilter)); |
| mPackages = packages; |
| } |
| int nPackages = mPackages.size(); |
| for (int i = nPackages - 1; i >= 0; i--) { |
| mPackages.valueAt(i).filter(beginTimeMillis, endTimeMillis, filter, opNamesFilter, |
| attributionTagFilter, flagsFilter, currentUid, mPackages.keyAt(i), |
| attributionChains); |
| if (mPackages.valueAt(i).isEmpty()) { |
| mPackages.removeAt(i); |
| } |
| } |
| } |
| |
| private void offsetHistory(long offset) { |
| int nPackages = mPackages.size(); |
| for (int i = 0; i < nPackages; i++) { |
| mPackages.valueAt(i).offsetHistory(offset); |
| } |
| } |
| |
| private void clearPackage(String packageName) { |
| mPackages.remove(packageName); |
| } |
| |
| void addDiscreteAccess(int op, @NonNull String packageName, @Nullable String attributionTag, |
| @AppOpsManager.OpFlags int flags, @AppOpsManager.UidState int uidState, |
| long accessTime, long accessDuration, |
| @AppOpsManager.AttributionFlags int attributionFlags, int attributionChainId) { |
| getOrCreateDiscretePackageOps(packageName).addDiscreteAccess(op, attributionTag, flags, |
| uidState, accessTime, accessDuration, attributionFlags, attributionChainId); |
| } |
| |
| private DiscretePackageOps getOrCreateDiscretePackageOps(String packageName) { |
| DiscretePackageOps result = mPackages.get(packageName); |
| if (result == null) { |
| result = new DiscretePackageOps(); |
| mPackages.put(packageName, result); |
| } |
| return result; |
| } |
| |
| private void applyToHistory(AppOpsManager.HistoricalOps result, int uid, |
| @NonNull ArrayMap<Integer, AttributionChain> attributionChains) { |
| int nPackages = mPackages.size(); |
| for (int i = 0; i < nPackages; i++) { |
| mPackages.valueAt(i).applyToHistory(result, uid, mPackages.keyAt(i), |
| attributionChains); |
| } |
| } |
| |
| void serialize(TypedXmlSerializer out) throws Exception { |
| int nPackages = mPackages.size(); |
| for (int i = 0; i < nPackages; i++) { |
| out.startTag(null, TAG_PACKAGE); |
| out.attribute(null, ATTR_PACKAGE_NAME, mPackages.keyAt(i)); |
| mPackages.valueAt(i).serialize(out); |
| out.endTag(null, TAG_PACKAGE); |
| } |
| } |
| |
| private void dump(@NonNull PrintWriter pw, @NonNull SimpleDateFormat sdf, |
| @NonNull Date date, @NonNull String prefix, int nDiscreteOps) { |
| int nPackages = mPackages.size(); |
| for (int i = 0; i < nPackages; i++) { |
| pw.print(prefix); |
| pw.print("Package: "); |
| pw.print(mPackages.keyAt(i)); |
| pw.println(); |
| mPackages.valueAt(i).dump(pw, sdf, date, prefix + " ", nDiscreteOps); |
| } |
| } |
| |
| void deserialize(TypedXmlPullParser parser, long beginTimeMillis) throws Exception { |
| int depth = parser.getDepth(); |
| while (XmlUtils.nextElementWithin(parser, depth)) { |
| if (TAG_PACKAGE.equals(parser.getName())) { |
| String packageName = parser.getAttributeValue(null, ATTR_PACKAGE_NAME); |
| getOrCreateDiscretePackageOps(packageName).deserialize(parser, beginTimeMillis); |
| } |
| } |
| } |
| } |
| |
| private final class DiscretePackageOps { |
| ArrayMap<Integer, DiscreteOp> mPackageOps; |
| |
| DiscretePackageOps() { |
| mPackageOps = new ArrayMap<>(); |
| } |
| |
| boolean isEmpty() { |
| return mPackageOps.isEmpty(); |
| } |
| |
| void addDiscreteAccess(int op, @Nullable String attributionTag, |
| @AppOpsManager.OpFlags int flags, @AppOpsManager.UidState int uidState, |
| long accessTime, long accessDuration, |
| @AppOpsManager.AttributionFlags int attributionFlags, int attributionChainId) { |
| getOrCreateDiscreteOp(op).addDiscreteAccess(attributionTag, flags, uidState, accessTime, |
| accessDuration, attributionFlags, attributionChainId); |
| } |
| |
| void merge(DiscretePackageOps other) { |
| int nOps = other.mPackageOps.size(); |
| for (int i = 0; i < nOps; i++) { |
| int opId = other.mPackageOps.keyAt(i); |
| DiscreteOp op = other.mPackageOps.valueAt(i); |
| getOrCreateDiscreteOp(opId).merge(op); |
| } |
| } |
| |
| private void filter(long beginTimeMillis, long endTimeMillis, |
| @AppOpsManager.HistoricalOpsRequestFilter int filter, |
| @Nullable String[] opNamesFilter, @Nullable String attributionTagFilter, |
| @AppOpsManager.OpFlags int flagsFilter, int currentUid, String currentPkgName, |
| ArrayMap<Integer, AttributionChain> attributionChains) { |
| int nOps = mPackageOps.size(); |
| for (int i = nOps - 1; i >= 0; i--) { |
| int opId = mPackageOps.keyAt(i); |
| if ((filter & FILTER_BY_OP_NAMES) != 0 && !ArrayUtils.contains(opNamesFilter, |
| AppOpsManager.opToPublicName(opId))) { |
| mPackageOps.removeAt(i); |
| continue; |
| } |
| mPackageOps.valueAt(i).filter(beginTimeMillis, endTimeMillis, filter, |
| attributionTagFilter, flagsFilter, currentUid, currentPkgName, |
| mPackageOps.keyAt(i), attributionChains); |
| if (mPackageOps.valueAt(i).isEmpty()) { |
| mPackageOps.removeAt(i); |
| } |
| } |
| } |
| |
| private void offsetHistory(long offset) { |
| int nOps = mPackageOps.size(); |
| for (int i = 0; i < nOps; i++) { |
| mPackageOps.valueAt(i).offsetHistory(offset); |
| } |
| } |
| |
| private DiscreteOp getOrCreateDiscreteOp(int op) { |
| DiscreteOp result = mPackageOps.get(op); |
| if (result == null) { |
| result = new DiscreteOp(); |
| mPackageOps.put(op, result); |
| } |
| return result; |
| } |
| |
| private void applyToHistory(AppOpsManager.HistoricalOps result, int uid, |
| @NonNull String packageName, |
| @NonNull ArrayMap<Integer, AttributionChain> attributionChains) { |
| int nPackageOps = mPackageOps.size(); |
| for (int i = 0; i < nPackageOps; i++) { |
| mPackageOps.valueAt(i).applyToHistory(result, uid, packageName, |
| mPackageOps.keyAt(i), attributionChains); |
| } |
| } |
| |
| void serialize(TypedXmlSerializer out) throws Exception { |
| int nOps = mPackageOps.size(); |
| for (int i = 0; i < nOps; i++) { |
| out.startTag(null, TAG_OP); |
| out.attributeInt(null, ATTR_OP_ID, mPackageOps.keyAt(i)); |
| mPackageOps.valueAt(i).serialize(out); |
| out.endTag(null, TAG_OP); |
| } |
| } |
| |
| private void dump(@NonNull PrintWriter pw, @NonNull SimpleDateFormat sdf, |
| @NonNull Date date, @NonNull String prefix, int nDiscreteOps) { |
| int nOps = mPackageOps.size(); |
| for (int i = 0; i < nOps; i++) { |
| pw.print(prefix); |
| pw.print(AppOpsManager.opToName(mPackageOps.keyAt(i))); |
| pw.println(); |
| mPackageOps.valueAt(i).dump(pw, sdf, date, prefix + " ", nDiscreteOps); |
| } |
| } |
| |
| void deserialize(TypedXmlPullParser parser, long beginTimeMillis) throws Exception { |
| int depth = parser.getDepth(); |
| while (XmlUtils.nextElementWithin(parser, depth)) { |
| if (TAG_OP.equals(parser.getName())) { |
| int op = parser.getAttributeInt(null, ATTR_OP_ID); |
| getOrCreateDiscreteOp(op).deserialize(parser, beginTimeMillis); |
| } |
| } |
| } |
| } |
| |
| private final class DiscreteOp { |
| ArrayMap<String, List<DiscreteOpEvent>> mAttributedOps; |
| |
| DiscreteOp() { |
| mAttributedOps = new ArrayMap<>(); |
| } |
| |
| boolean isEmpty() { |
| return mAttributedOps.isEmpty(); |
| } |
| |
| void merge(DiscreteOp other) { |
| int nTags = other.mAttributedOps.size(); |
| for (int i = 0; i < nTags; i++) { |
| String tag = other.mAttributedOps.keyAt(i); |
| List<DiscreteOpEvent> otherEvents = other.mAttributedOps.valueAt(i); |
| List<DiscreteOpEvent> events = getOrCreateDiscreteOpEventsList(tag); |
| mAttributedOps.put(tag, stableListMerge(events, otherEvents)); |
| } |
| } |
| |
| private void filter(long beginTimeMillis, long endTimeMillis, |
| @AppOpsManager.HistoricalOpsRequestFilter int filter, |
| @Nullable String attributionTagFilter, @AppOpsManager.OpFlags int flagsFilter, |
| int currentUid, String currentPkgName, int currentOp, |
| ArrayMap<Integer, AttributionChain> attributionChains) { |
| if ((filter & FILTER_BY_ATTRIBUTION_TAG) != 0) { |
| ArrayMap<String, List<DiscreteOpEvent>> attributedOps = new ArrayMap<>(); |
| attributedOps.put(attributionTagFilter, |
| getOrCreateDiscreteOpEventsList(attributionTagFilter)); |
| mAttributedOps = attributedOps; |
| } |
| |
| int nTags = mAttributedOps.size(); |
| for (int i = nTags - 1; i >= 0; i--) { |
| String tag = mAttributedOps.keyAt(i); |
| List<DiscreteOpEvent> list = mAttributedOps.valueAt(i); |
| list = filterEventsList(list, beginTimeMillis, endTimeMillis, flagsFilter, |
| currentUid, currentPkgName, currentOp, mAttributedOps.keyAt(i), |
| attributionChains); |
| mAttributedOps.put(tag, list); |
| if (list.size() == 0) { |
| mAttributedOps.removeAt(i); |
| } |
| } |
| } |
| |
| private void offsetHistory(long offset) { |
| int nTags = mAttributedOps.size(); |
| for (int i = 0; i < nTags; i++) { |
| List<DiscreteOpEvent> list = mAttributedOps.valueAt(i); |
| |
| int n = list.size(); |
| for (int j = 0; j < n; j++) { |
| DiscreteOpEvent event = list.get(j); |
| list.set(j, new DiscreteOpEvent(event.mNoteTime - offset, event.mNoteDuration, |
| event.mUidState, event.mOpFlag, event.mAttributionFlags, |
| event.mAttributionChainId)); |
| } |
| } |
| } |
| |
| void addDiscreteAccess(@Nullable String attributionTag, |
| @AppOpsManager.OpFlags int flags, @AppOpsManager.UidState int uidState, |
| long accessTime, long accessDuration, |
| @AppOpsManager.AttributionFlags int attributionFlags, int attributionChainId) { |
| List<DiscreteOpEvent> attributedOps = getOrCreateDiscreteOpEventsList( |
| attributionTag); |
| |
| int nAttributedOps = attributedOps.size(); |
| int i = nAttributedOps; |
| for (; i > 0; i--) { |
| DiscreteOpEvent previousOp = attributedOps.get(i - 1); |
| if (discretizeTimeStamp(previousOp.mNoteTime) < discretizeTimeStamp(accessTime)) { |
| break; |
| } |
| if (previousOp.mOpFlag == flags && previousOp.mUidState == uidState |
| && previousOp.mAttributionFlags == attributionFlags |
| && previousOp.mAttributionChainId == attributionChainId) { |
| if (discretizeDuration(accessDuration) != discretizeDuration( |
| previousOp.mNoteDuration)) { |
| break; |
| } else { |
| return; |
| } |
| } |
| } |
| attributedOps.add(i, new DiscreteOpEvent(accessTime, accessDuration, uidState, flags, |
| attributionFlags, attributionChainId)); |
| } |
| |
| private List<DiscreteOpEvent> getOrCreateDiscreteOpEventsList(String attributionTag) { |
| List<DiscreteOpEvent> result = mAttributedOps.get(attributionTag); |
| if (result == null) { |
| result = new ArrayList<>(); |
| mAttributedOps.put(attributionTag, result); |
| } |
| return result; |
| } |
| |
| private void applyToHistory(AppOpsManager.HistoricalOps result, int uid, |
| @NonNull String packageName, int op, |
| @NonNull ArrayMap<Integer, AttributionChain> attributionChains) { |
| int nOps = mAttributedOps.size(); |
| for (int i = 0; i < nOps; i++) { |
| String tag = mAttributedOps.keyAt(i); |
| List<DiscreteOpEvent> events = mAttributedOps.valueAt(i); |
| int nEvents = events.size(); |
| for (int j = 0; j < nEvents; j++) { |
| DiscreteOpEvent event = events.get(j); |
| AppOpsManager.OpEventProxyInfo proxy = null; |
| if (event.mAttributionChainId != ATTRIBUTION_CHAIN_ID_NONE |
| && attributionChains != null) { |
| AttributionChain chain = attributionChains.get(event.mAttributionChainId); |
| if (chain != null && chain.isComplete() |
| && chain.isStart(packageName, uid, tag, op, event) |
| && chain.mLastVisibleEvent != null) { |
| AttributionChain.OpEvent proxyEvent = chain.mLastVisibleEvent; |
| proxy = new AppOpsManager.OpEventProxyInfo(proxyEvent.mUid, |
| proxyEvent.mPkgName, proxyEvent.mAttributionTag); |
| } |
| } |
| result.addDiscreteAccess(op, uid, packageName, tag, event.mUidState, |
| event.mOpFlag, discretizeTimeStamp(event.mNoteTime), |
| discretizeDuration(event.mNoteDuration), proxy); |
| } |
| } |
| } |
| |
| private void dump(@NonNull PrintWriter pw, @NonNull SimpleDateFormat sdf, |
| @NonNull Date date, @NonNull String prefix, int nDiscreteOps) { |
| int nAttributions = mAttributedOps.size(); |
| for (int i = 0; i < nAttributions; i++) { |
| pw.print(prefix); |
| pw.print("Attribution: "); |
| pw.print(mAttributedOps.keyAt(i)); |
| pw.println(); |
| List<DiscreteOpEvent> ops = mAttributedOps.valueAt(i); |
| int nOps = ops.size(); |
| int first = nDiscreteOps < 1 ? 0 : max(0, nOps - nDiscreteOps); |
| for (int j = first; j < nOps; j++) { |
| ops.get(j).dump(pw, sdf, date, prefix + " "); |
| |
| } |
| } |
| } |
| |
| void serialize(TypedXmlSerializer out) throws Exception { |
| int nAttributions = mAttributedOps.size(); |
| for (int i = 0; i < nAttributions; i++) { |
| out.startTag(null, TAG_TAG); |
| String tag = mAttributedOps.keyAt(i); |
| if (tag != null) { |
| out.attribute(null, ATTR_TAG, mAttributedOps.keyAt(i)); |
| } |
| List<DiscreteOpEvent> ops = mAttributedOps.valueAt(i); |
| int nOps = ops.size(); |
| for (int j = 0; j < nOps; j++) { |
| out.startTag(null, TAG_ENTRY); |
| ops.get(j).serialize(out); |
| out.endTag(null, TAG_ENTRY); |
| } |
| out.endTag(null, TAG_TAG); |
| } |
| } |
| |
| void deserialize(TypedXmlPullParser parser, long beginTimeMillis) throws Exception { |
| int outerDepth = parser.getDepth(); |
| while (XmlUtils.nextElementWithin(parser, outerDepth)) { |
| if (TAG_TAG.equals(parser.getName())) { |
| String attributionTag = parser.getAttributeValue(null, ATTR_TAG); |
| List<DiscreteOpEvent> events = getOrCreateDiscreteOpEventsList( |
| attributionTag); |
| int innerDepth = parser.getDepth(); |
| while (XmlUtils.nextElementWithin(parser, innerDepth)) { |
| if (TAG_ENTRY.equals(parser.getName())) { |
| long noteTime = parser.getAttributeLong(null, ATTR_NOTE_TIME); |
| long noteDuration = parser.getAttributeLong(null, ATTR_NOTE_DURATION, |
| -1); |
| int uidState = parser.getAttributeInt(null, ATTR_UID_STATE); |
| int opFlags = parser.getAttributeInt(null, ATTR_FLAGS); |
| int attributionFlags = parser.getAttributeInt(null, |
| ATTR_ATTRIBUTION_FLAGS, AppOpsManager.ATTRIBUTION_FLAGS_NONE); |
| int attributionChainId = parser.getAttributeInt(null, ATTR_CHAIN_ID, |
| AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE); |
| if (noteTime + noteDuration < beginTimeMillis) { |
| continue; |
| } |
| DiscreteOpEvent event = new DiscreteOpEvent(noteTime, noteDuration, |
| uidState, opFlags, attributionFlags, attributionChainId); |
| events.add(event); |
| } |
| } |
| Collections.sort(events, (a, b) -> a.mNoteTime < b.mNoteTime ? -1 |
| : (a.mNoteTime == b.mNoteTime ? 0 : 1)); |
| } |
| } |
| } |
| } |
| |
| private final class DiscreteOpEvent { |
| final long mNoteTime; |
| final long mNoteDuration; |
| final @AppOpsManager.UidState int mUidState; |
| final @AppOpsManager.OpFlags int mOpFlag; |
| final @AppOpsManager.AttributionFlags int mAttributionFlags; |
| final int mAttributionChainId; |
| |
| DiscreteOpEvent(long noteTime, long noteDuration, @AppOpsManager.UidState int uidState, |
| @AppOpsManager.OpFlags int opFlag, |
| @AppOpsManager.AttributionFlags int attributionFlags, int attributionChainId) { |
| mNoteTime = noteTime; |
| mNoteDuration = noteDuration; |
| mUidState = uidState; |
| mOpFlag = opFlag; |
| mAttributionFlags = attributionFlags; |
| mAttributionChainId = attributionChainId; |
| } |
| |
| public boolean equalsExceptDuration(DiscreteOpEvent o) { |
| return mNoteTime == o.mNoteTime && mUidState == o.mUidState && mOpFlag == o.mOpFlag |
| && mAttributionFlags == o.mAttributionFlags |
| && mAttributionChainId == o.mAttributionChainId; |
| |
| } |
| |
| private void dump(@NonNull PrintWriter pw, @NonNull SimpleDateFormat sdf, |
| @NonNull Date date, @NonNull String prefix) { |
| pw.print(prefix); |
| pw.print("Access ["); |
| pw.print(getUidStateName(mUidState)); |
| pw.print("-"); |
| pw.print(flagsToString(mOpFlag)); |
| pw.print("] at "); |
| date.setTime(discretizeTimeStamp(mNoteTime)); |
| pw.print(sdf.format(date)); |
| if (mNoteDuration != -1) { |
| pw.print(" for "); |
| pw.print(discretizeDuration(mNoteDuration)); |
| pw.print(" milliseconds "); |
| } |
| if (mAttributionFlags != AppOpsManager.ATTRIBUTION_FLAGS_NONE) { |
| pw.print(" attribution flags="); |
| pw.print(mAttributionFlags); |
| pw.print(" with chainId="); |
| pw.print(mAttributionChainId); |
| } |
| pw.println(); |
| } |
| |
| private void serialize(TypedXmlSerializer out) throws Exception { |
| out.attributeLong(null, ATTR_NOTE_TIME, mNoteTime); |
| if (mNoteDuration != -1) { |
| out.attributeLong(null, ATTR_NOTE_DURATION, mNoteDuration); |
| } |
| if (mAttributionFlags != AppOpsManager.ATTRIBUTION_FLAGS_NONE) { |
| out.attributeInt(null, ATTR_ATTRIBUTION_FLAGS, mAttributionFlags); |
| } |
| if (mAttributionChainId != AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE) { |
| out.attributeInt(null, ATTR_CHAIN_ID, mAttributionChainId); |
| } |
| out.attributeInt(null, ATTR_UID_STATE, mUidState); |
| out.attributeInt(null, ATTR_FLAGS, mOpFlag); |
| } |
| } |
| |
| private static int[] parseOpsList(String opsList) { |
| String[] strArr; |
| if (opsList.isEmpty()) { |
| strArr = new String[0]; |
| } else { |
| strArr = opsList.split(","); |
| } |
| int nOps = strArr.length; |
| int[] result = new int[nOps]; |
| try { |
| for (int i = 0; i < nOps; i++) { |
| result[i] = Integer.parseInt(strArr[i]); |
| } |
| } catch (NumberFormatException e) { |
| Slog.e(TAG, "Failed to parse Discrete ops list: " + e.getMessage()); |
| return parseOpsList(DEFAULT_DISCRETE_OPS); |
| } |
| return result; |
| } |
| |
| private static List<DiscreteOpEvent> stableListMerge(List<DiscreteOpEvent> a, |
| List<DiscreteOpEvent> b) { |
| int nA = a.size(); |
| int nB = b.size(); |
| int i = 0; |
| int k = 0; |
| List<DiscreteOpEvent> result = new ArrayList<>(nA + nB); |
| while (i < nA || k < nB) { |
| if (i == nA) { |
| result.add(b.get(k++)); |
| } else if (k == nB) { |
| result.add(a.get(i++)); |
| } else if (a.get(i).mNoteTime < b.get(k).mNoteTime) { |
| result.add(a.get(i++)); |
| } else { |
| result.add(b.get(k++)); |
| } |
| } |
| return result; |
| } |
| |
| private static List<DiscreteOpEvent> filterEventsList(List<DiscreteOpEvent> list, |
| long beginTimeMillis, long endTimeMillis, @AppOpsManager.OpFlags int flagsFilter, |
| int currentUid, String currentPackageName, int currentOp, String currentAttrTag, |
| ArrayMap<Integer, AttributionChain> attributionChains) { |
| int n = list.size(); |
| List<DiscreteOpEvent> result = new ArrayList<>(n); |
| for (int i = 0; i < n; i++) { |
| DiscreteOpEvent event = list.get(i); |
| AttributionChain chain = attributionChains.get(event.mAttributionChainId); |
| // If we have an attribution chain, and this event isn't the beginning node, remove it |
| if (chain != null && !chain.isStart(currentPackageName, currentUid, currentAttrTag, |
| currentOp, event) && chain.isComplete() |
| && event.mAttributionChainId != ATTRIBUTION_CHAIN_ID_NONE) { |
| continue; |
| } |
| if ((event.mOpFlag & flagsFilter) != 0 |
| && event.mNoteTime + event.mNoteDuration > beginTimeMillis |
| && event.mNoteTime < endTimeMillis) { |
| result.add(event); |
| } |
| } |
| return result; |
| } |
| |
| private static boolean isDiscreteOp(int op, @AppOpsManager.OpFlags int flags) { |
| if (!ArrayUtils.contains(sDiscreteOps, op)) { |
| return false; |
| } |
| if ((flags & (sDiscreteFlags)) == 0) { |
| return false; |
| } |
| return true; |
| } |
| |
| private static long discretizeTimeStamp(long timeStamp) { |
| return timeStamp / sDiscreteHistoryQuantization * sDiscreteHistoryQuantization; |
| |
| } |
| |
| private static long discretizeDuration(long duration) { |
| return duration == -1 ? -1 : (duration + sDiscreteHistoryQuantization - 1) |
| / sDiscreteHistoryQuantization * sDiscreteHistoryQuantization; |
| } |
| |
| void setDebugMode(boolean debugMode) { |
| this.mDebugMode = debugMode; |
| } |
| } |
| |