diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java index 9e90d19fd6a09..3d5711ff20f98 100644 --- a/services/core/java/com/android/server/appop/AppOpsService.java +++ b/services/core/java/com/android/server/appop/AppOpsService.java @@ -33,7 +33,6 @@ import static android.app.AppOpsManager.MODE_DEFAULT; import static android.app.AppOpsManager.MODE_ERRORED; import static android.app.AppOpsManager.MODE_FOREGROUND; import static android.app.AppOpsManager.MODE_IGNORED; -import static android.app.AppOpsManager.NoteOpEvent; import static android.app.AppOpsManager.OP_CAMERA; import static android.app.AppOpsManager.OP_FLAGS_ALL; import static android.app.AppOpsManager.OP_FLAG_SELF; @@ -45,7 +44,6 @@ import static android.app.AppOpsManager.OP_RECORD_AUDIO; import static android.app.AppOpsManager.OP_RECORD_AUDIO_HOTWORD; import static android.app.AppOpsManager.OP_VIBRATE; import static android.app.AppOpsManager.OnOpStartedListener.START_TYPE_FAILED; -import static android.app.AppOpsManager.OnOpStartedListener.START_TYPE_RESUMED; import static android.app.AppOpsManager.OnOpStartedListener.START_TYPE_STARTED; import static android.app.AppOpsManager.OpEventProxyInfo; import static android.app.AppOpsManager.RestrictionBypass; @@ -57,7 +55,6 @@ import static android.app.AppOpsManager.SECURITY_EXCEPTION_ON_INVALID_ATTRIBUTIO import static android.app.AppOpsManager._NUM_OP; import static android.app.AppOpsManager.extractFlagsFromKey; import static android.app.AppOpsManager.extractUidStateFromKey; -import static android.app.AppOpsManager.makeKey; import static android.app.AppOpsManager.modeToName; import static android.app.AppOpsManager.opAllowSystemBypassRestriction; import static android.app.AppOpsManager.opRestrictsRead; @@ -131,7 +128,6 @@ import android.util.ArraySet; import android.util.AtomicFile; import android.util.IndentingPrintWriter; import android.util.KeyValueListParser; -import android.util.LongSparseArray; import android.util.Pair; import android.util.Pools; import android.util.Pools.SimplePool; @@ -827,578 +823,6 @@ public class AppOpsService extends IAppOpsService.Stub implements PersistenceSch } } - final class AttributedOp { - private final @NonNull AppOpsService mAppOpsService; - public final @Nullable String tag; - public final @NonNull Op parent; - - /** - * Last successful accesses (noteOp + finished startOp) for each uidState/opFlag combination - * - *

Key is {@link AppOpsManager#makeKey} - */ - @GuardedBy("AppOpsService.this") - private @Nullable LongSparseArray mAccessEvents; - - /** - * Last rejected accesses for each uidState/opFlag combination - * - *

Key is {@link AppOpsManager#makeKey} - */ - @GuardedBy("AppOpsService.this") - private @Nullable LongSparseArray mRejectEvents; - - /** - * Currently in progress startOp events - * - *

Key is clientId - */ - @GuardedBy("AppOpsService.this") - @Nullable ArrayMap mInProgressEvents; - - /** - * Currently paused startOp events - * - *

Key is clientId - */ - @GuardedBy("AppOpsService.this") - @Nullable ArrayMap mPausedInProgressEvents; - - AttributedOp(@NonNull AppOpsService appOpsService, @Nullable String tag, - @NonNull Op parent) { - this.mAppOpsService = appOpsService; - this.tag = tag; - this.parent = parent; - } - - /** - * Update state when noteOp was rejected or startOp->finishOp event finished - * - * @param proxyUid The uid of the proxy - * @param proxyPackageName The package name of the proxy - * @param proxyAttributionTag the attributionTag in the proxies package - * @param uidState UID state of the app noteOp/startOp was called for - * @param flags OpFlags of the call - */ - public void accessed(int proxyUid, @Nullable String proxyPackageName, - @Nullable String proxyAttributionTag, @AppOpsManager.UidState int uidState, - @OpFlags int flags) { - long accessTime = System.currentTimeMillis(); - accessed(accessTime, -1, proxyUid, proxyPackageName, - proxyAttributionTag, uidState, flags); - - mAppOpsService.mHistoricalRegistry.incrementOpAccessedCount(parent.op, parent.uid, - parent.packageName, tag, uidState, flags, accessTime, - AppOpsManager.ATTRIBUTION_FLAGS_NONE, AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE); - } - - /** - * Add an access that was previously collected. - * - * @param noteTime The time of the event - * @param duration The duration of the event - * @param proxyUid The uid of the proxy - * @param proxyPackageName The package name of the proxy - * @param proxyAttributionTag the attributionTag in the proxies package - * @param uidState UID state of the app noteOp/startOp was called for - * @param flags OpFlags of the call - */ - public void accessed(long noteTime, long duration, int proxyUid, - @Nullable String proxyPackageName, @Nullable String proxyAttributionTag, - @AppOpsManager.UidState int uidState, @OpFlags int flags) { - long key = makeKey(uidState, flags); - - if (mAccessEvents == null) { - mAccessEvents = new LongSparseArray<>(1); - } - - OpEventProxyInfo proxyInfo = null; - if (proxyUid != Process.INVALID_UID) { - proxyInfo = mAppOpsService.mOpEventProxyInfoPool.acquire(proxyUid, proxyPackageName, - proxyAttributionTag); - } - - NoteOpEvent existingEvent = mAccessEvents.get(key); - if (existingEvent != null) { - existingEvent.reinit(noteTime, duration, proxyInfo, - mAppOpsService.mOpEventProxyInfoPool); - } else { - mAccessEvents.put(key, new NoteOpEvent(noteTime, duration, proxyInfo)); - } - } - - /** - * Update state when noteOp/startOp was rejected. - * - * @param uidState UID state of the app noteOp is called for - * @param flags OpFlags of the call - */ - public void rejected(@AppOpsManager.UidState int uidState, @OpFlags int flags) { - rejected(System.currentTimeMillis(), uidState, flags); - - mAppOpsService.mHistoricalRegistry.incrementOpRejected(parent.op, parent.uid, - parent.packageName, tag, uidState, flags); - } - - /** - * Add an rejection that was previously collected - * - * @param noteTime The time of the event - * @param uidState UID state of the app noteOp/startOp was called for - * @param flags OpFlags of the call - */ - public void rejected(long noteTime, @AppOpsManager.UidState int uidState, - @OpFlags int flags) { - long key = makeKey(uidState, flags); - - if (mRejectEvents == null) { - mRejectEvents = new LongSparseArray<>(1); - } - - // We do not collect proxy information for rejections yet - NoteOpEvent existingEvent = mRejectEvents.get(key); - if (existingEvent != null) { - existingEvent.reinit(noteTime, -1, null, mAppOpsService.mOpEventProxyInfoPool); - } else { - mRejectEvents.put(key, new NoteOpEvent(noteTime, -1, null)); - } - } - - /** - * Update state when start was called - * - * @param clientId Id of the startOp caller - * @param proxyUid The UID of the proxy app - * @param proxyPackageName The package name of the proxy app - * @param proxyAttributionTag The attribution tag of the proxy app - * @param uidState UID state of the app startOp is called for - * @param flags The proxy flags - * @param attributionFlags The attribution flags associated with this operation. - * @param attributionChainId The if of the attribution chain this operations is a part of. - */ - public void started(@NonNull IBinder clientId, int proxyUid, - @Nullable String proxyPackageName, @Nullable String proxyAttributionTag, - @AppOpsManager.UidState int uidState, @OpFlags int flags, @AttributionFlags - int attributionFlags, int attributionChainId) throws RemoteException { - started(clientId, proxyUid, proxyPackageName, proxyAttributionTag, - uidState, flags,/*triggerCallbackIfNeeded*/ true, attributionFlags, - attributionChainId); - } - - private void started(@NonNull IBinder clientId, int proxyUid, - @Nullable String proxyPackageName, @Nullable String proxyAttributionTag, - @AppOpsManager.UidState int uidState, @OpFlags int flags, - boolean triggerCallbackIfNeeded, @AttributionFlags int attributionFlags, - int attributionChainId) throws RemoteException { - startedOrPaused(clientId, proxyUid, proxyPackageName, - proxyAttributionTag, uidState, flags, triggerCallbackIfNeeded, - /*triggerCallbackIfNeeded*/ true, attributionFlags, attributionChainId); - } - - private void startedOrPaused(@NonNull IBinder clientId, int proxyUid, - @Nullable String proxyPackageName, @Nullable String proxyAttributionTag, - @AppOpsManager.UidState int uidState, @OpFlags int flags, - boolean triggerCallbackIfNeeded, boolean isStarted, @AttributionFlags - int attributionFlags, int attributionChainId) throws RemoteException { - if (triggerCallbackIfNeeded && !parent.isRunning() && isStarted) { - mAppOpsService.scheduleOpActiveChangedIfNeededLocked(parent.op, parent.uid, - parent.packageName, tag, true, attributionFlags, attributionChainId); - } - - if (isStarted && mInProgressEvents == null) { - mInProgressEvents = new ArrayMap<>(1); - } else if (!isStarted && mPausedInProgressEvents == null) { - mPausedInProgressEvents = new ArrayMap<>(1); - } - ArrayMap events = isStarted - ? mInProgressEvents : mPausedInProgressEvents; - - long startTime = System.currentTimeMillis(); - InProgressStartOpEvent event = events.get(clientId); - if (event == null) { - event = mAppOpsService.mInProgressStartOpEventPool.acquire(startTime, - SystemClock.elapsedRealtime(), clientId, tag, - PooledLambda.obtainRunnable(AppOpsService::onClientDeath, this, clientId), - proxyUid, proxyPackageName, proxyAttributionTag, uidState, flags, - attributionFlags, attributionChainId); - events.put(clientId, event); - } else { - if (uidState != event.getUidState()) { - onUidStateChanged(uidState); - } - } - - event.numUnfinishedStarts++; - - if (isStarted) { - mAppOpsService.mHistoricalRegistry.incrementOpAccessedCount(parent.op, parent.uid, - parent.packageName, tag, uidState, flags, startTime, attributionFlags, - attributionChainId); - } - } - - /** - * Update state when finishOp was called. Will finish started ops, and delete paused ops. - * - * @param clientId Id of the finishOp caller - */ - public void finished(@NonNull IBinder clientId) { - finished(clientId, true); - } - - private void finished(@NonNull IBinder clientId, boolean triggerCallbackIfNeeded) { - finishOrPause(clientId, triggerCallbackIfNeeded, false); - } - - /** - * Update state when paused or finished is called. If pausing, it records the op as - * stopping in the HistoricalRegistry, but does not delete it. - */ - private void finishOrPause(@NonNull IBinder clientId, boolean triggerCallbackIfNeeded, - boolean isPausing) { - int indexOfToken = isRunning() ? mInProgressEvents.indexOfKey(clientId) : -1; - if (indexOfToken < 0) { - finishPossiblyPaused(clientId, isPausing); - return; - } - - InProgressStartOpEvent event = mInProgressEvents.valueAt(indexOfToken); - if (!isPausing) { - event.numUnfinishedStarts--; - } - // If we are pausing, create a NoteOpEvent, but don't change the InProgress event - if (event.numUnfinishedStarts == 0 || isPausing) { - if (!isPausing) { - event.finish(); - mInProgressEvents.removeAt(indexOfToken); - } - - if (mAccessEvents == null) { - mAccessEvents = new LongSparseArray<>(1); - } - - OpEventProxyInfo proxyCopy = event.getProxy() != null - ? new OpEventProxyInfo(event.getProxy()) : null; - - long accessDurationMillis = - SystemClock.elapsedRealtime() - event.getStartElapsedTime(); - NoteOpEvent finishedEvent = new NoteOpEvent(event.getStartTime(), - accessDurationMillis, proxyCopy); - mAccessEvents.put(makeKey(event.getUidState(), event.getFlags()), - finishedEvent); - - mAppOpsService.mHistoricalRegistry.increaseOpAccessDuration(parent.op, parent.uid, - parent.packageName, tag, event.getUidState(), - event.getFlags(), finishedEvent.getNoteTime(), finishedEvent.getDuration(), - event.getAttributionFlags(), event.getAttributionChainId()); - - if (!isPausing) { - mAppOpsService.mInProgressStartOpEventPool.release(event); - if (mInProgressEvents.isEmpty()) { - mInProgressEvents = null; - - // TODO ntmyren: Also callback for single attribution tag activity changes - if (triggerCallbackIfNeeded && !parent.isRunning()) { - mAppOpsService.scheduleOpActiveChangedIfNeededLocked(parent.op, - parent.uid, parent.packageName, tag, false, - event.getAttributionFlags(), event.getAttributionChainId()); - } - } - } - } - } - - // Finish or pause (no-op) an already paused op - private void finishPossiblyPaused(@NonNull IBinder clientId, boolean isPausing) { - if (!isPaused()) { - Slog.wtf(TAG, "No ops running or paused"); - return; - } - - int indexOfToken = mPausedInProgressEvents.indexOfKey(clientId); - if (indexOfToken < 0) { - Slog.wtf(TAG, "No op running or paused for the client"); - return; - } else if (isPausing) { - // already paused - return; - } - - // no need to record a paused event finishing. - InProgressStartOpEvent event = mPausedInProgressEvents.valueAt(indexOfToken); - event.numUnfinishedStarts--; - if (event.numUnfinishedStarts == 0) { - mPausedInProgressEvents.removeAt(indexOfToken); - mAppOpsService.mInProgressStartOpEventPool.release(event); - if (mPausedInProgressEvents.isEmpty()) { - mPausedInProgressEvents = null; - } - } - } - - /** - * Create an event that will be started, if the op is unpaused. - */ - public void createPaused(@NonNull IBinder clientId, int proxyUid, - @Nullable String proxyPackageName, @Nullable String proxyAttributionTag, - @AppOpsManager.UidState int uidState, @OpFlags int flags, @AttributionFlags - int attributionFlags, int attributionChainId) throws RemoteException { - startedOrPaused(clientId, proxyUid, proxyPackageName, proxyAttributionTag, - uidState, flags, true, false, attributionFlags, attributionChainId); - } - - /** - * Pause all currently started ops. This will create a HistoricalRegistry - */ - public void pause() { - if (!isRunning()) { - return; - } - - if (mPausedInProgressEvents == null) { - mPausedInProgressEvents = new ArrayMap<>(1); - } - - for (int i = 0; i < mInProgressEvents.size(); i++) { - InProgressStartOpEvent event = mInProgressEvents.valueAt(i); - mPausedInProgressEvents.put(event.getClientId(), event); - finishOrPause(event.getClientId(), true, true); - - mAppOpsService.scheduleOpActiveChangedIfNeededLocked(parent.op, parent.uid, - parent.packageName, tag, false, - event.getAttributionFlags(), event.getAttributionChainId()); - } - mInProgressEvents = null; - } - - /** - * Unpause all currently paused ops. This will reinitialize their start and duration - * times, but keep all other values the same - */ - public void resume() { - if (!isPaused()) { - return; - } - - if (mInProgressEvents == null) { - mInProgressEvents = new ArrayMap<>(mPausedInProgressEvents.size()); - } - boolean shouldSendActive = !mPausedInProgressEvents.isEmpty() - && mInProgressEvents.isEmpty(); - - long startTime = System.currentTimeMillis(); - for (int i = 0; i < mPausedInProgressEvents.size(); i++) { - InProgressStartOpEvent event = mPausedInProgressEvents.valueAt(i); - mInProgressEvents.put(event.getClientId(), event); - event.setStartElapsedTime(SystemClock.elapsedRealtime()); - event.setStartTime(startTime); - mAppOpsService.mHistoricalRegistry.incrementOpAccessedCount(parent.op, parent.uid, - parent.packageName, tag, event.getUidState(), event.getFlags(), startTime, - event.getAttributionFlags(), event.getAttributionChainId()); - if (shouldSendActive) { - mAppOpsService.scheduleOpActiveChangedIfNeededLocked(parent.op, parent.uid, - parent.packageName, tag, true, event.getAttributionFlags(), - event.getAttributionChainId()); - } - // Note: this always sends MODE_ALLOWED, even if the mode is FOREGROUND - // TODO ntmyren: figure out how to get the real mode. - mAppOpsService.scheduleOpStartedIfNeededLocked(parent.op, parent.uid, - parent.packageName, tag, event.getFlags(), MODE_ALLOWED, START_TYPE_RESUMED, - event.getAttributionFlags(), event.getAttributionChainId()); - } - mPausedInProgressEvents = null; - } - - /** - * Called in the case the client dies without calling finish first - * - * @param clientId The client that died - */ - void onClientDeath(@NonNull IBinder clientId) { - synchronized (mAppOpsService) { - if (!isPaused() && !isRunning()) { - return; - } - - ArrayMap events = isPaused() - ? mPausedInProgressEvents : mInProgressEvents; - InProgressStartOpEvent deadEvent = events.get(clientId); - if (deadEvent != null) { - deadEvent.numUnfinishedStarts = 1; - } - - finished(clientId); - } - } - - /** - * Notify that the state of the uid changed - * - * @param newState The new state - */ - public void onUidStateChanged(@AppOpsManager.UidState int newState) { - if (!isPaused() && !isRunning()) { - return; - } - - boolean isRunning = isRunning(); - ArrayMap events = - isRunning ? mInProgressEvents : mPausedInProgressEvents; - - int numInProgressEvents = events.size(); - List binders = new ArrayList<>(events.keySet()); - for (int i = 0; i < numInProgressEvents; i++) { - InProgressStartOpEvent event = events.get(binders.get(i)); - - if (event != null && event.getUidState() != newState) { - try { - // Remove all but one unfinished start count and then call finished() to - // remove start event object - int numPreviousUnfinishedStarts = event.numUnfinishedStarts; - event.numUnfinishedStarts = 1; - OpEventProxyInfo proxy = event.getProxy(); - - finished(event.getClientId(), false); - - // Call started() to add a new start event object and then add the - // previously removed unfinished start counts back - if (proxy != null) { - startedOrPaused(event.getClientId(), proxy.getUid(), - proxy.getPackageName(), proxy.getAttributionTag(), newState, - event.getFlags(), false, isRunning, - event.getAttributionFlags(), event.getAttributionChainId()); - } else { - startedOrPaused(event.getClientId(), Process.INVALID_UID, null, null, - newState, event.getFlags(), false, isRunning, - event.getAttributionFlags(), event.getAttributionChainId()); - } - - events = isRunning ? mInProgressEvents : mPausedInProgressEvents; - InProgressStartOpEvent newEvent = events.get(binders.get(i)); - if (newEvent != null) { - newEvent.numUnfinishedStarts += numPreviousUnfinishedStarts - 1; - } - } catch (RemoteException e) { - if (DEBUG) Slog.e(TAG, "Cannot switch to new uidState " + newState); - } - } - } - } - - /** - * Combine {@code a} and {@code b} and return the result. The result might be {@code a} - * or {@code b}. If there is an event for the same key in both the later event is retained. - */ - private @Nullable LongSparseArray add(@Nullable LongSparseArray a, - @Nullable LongSparseArray b) { - if (a == null) { - return b; - } - - if (b == null) { - return a; - } - - int numEventsToAdd = b.size(); - for (int i = 0; i < numEventsToAdd; i++) { - long keyOfEventToAdd = b.keyAt(i); - NoteOpEvent bEvent = b.valueAt(i); - NoteOpEvent aEvent = a.get(keyOfEventToAdd); - - if (aEvent == null || bEvent.getNoteTime() > aEvent.getNoteTime()) { - a.put(keyOfEventToAdd, bEvent); - } - } - - return a; - } - - /** - * Add all data from the {@code opToAdd} to this op. - * - *

If there is an event for the same key in both the later event is retained. - *

{@code opToAdd} should not be used after this method is called. - * - * @param opToAdd The op to add - */ - public void add(@NonNull AttributedOp opToAdd) { - if (opToAdd.isRunning() || opToAdd.isPaused()) { - ArrayMap ignoredEvents = opToAdd.isRunning() - ? opToAdd.mInProgressEvents : opToAdd.mPausedInProgressEvents; - Slog.w(TAG, "Ignoring " + ignoredEvents.size() + " app-ops, running: " - + opToAdd.isRunning()); - - int numInProgressEvents = ignoredEvents.size(); - for (int i = 0; i < numInProgressEvents; i++) { - InProgressStartOpEvent event = ignoredEvents.valueAt(i); - - event.finish(); - mAppOpsService.mInProgressStartOpEventPool.release(event); - } - } - - mAccessEvents = add(mAccessEvents, opToAdd.mAccessEvents); - mRejectEvents = add(mRejectEvents, opToAdd.mRejectEvents); - } - - public boolean isRunning() { - return mInProgressEvents != null && !mInProgressEvents.isEmpty(); - } - - public boolean isPaused() { - return mPausedInProgressEvents != null && !mPausedInProgressEvents.isEmpty(); - } - - boolean hasAnyTime() { - return (mAccessEvents != null && mAccessEvents.size() > 0) - || (mRejectEvents != null && mRejectEvents.size() > 0); - } - - /** - * Clone a {@link LongSparseArray} and clone all values. - */ - private @Nullable LongSparseArray deepClone( - @Nullable LongSparseArray original) { - if (original == null) { - return original; - } - - int size = original.size(); - LongSparseArray clone = new LongSparseArray<>(size); - for (int i = 0; i < size; i++) { - clone.put(original.keyAt(i), new NoteOpEvent(original.valueAt(i))); - } - - return clone; - } - - @NonNull AttributedOpEntry createAttributedOpEntryLocked() { - LongSparseArray accessEvents = deepClone(mAccessEvents); - - // Add in progress events as access events - if (isRunning()) { - long now = SystemClock.elapsedRealtime(); - int numInProgressEvents = mInProgressEvents.size(); - - if (accessEvents == null) { - accessEvents = new LongSparseArray<>(numInProgressEvents); - } - - for (int i = 0; i < numInProgressEvents; i++) { - InProgressStartOpEvent event = mInProgressEvents.valueAt(i); - - accessEvents.append(makeKey(event.getUidState(), event.getFlags()), - new NoteOpEvent(event.getStartTime(), now - event.getStartElapsedTime(), - event.getProxy())); - } - } - - LongSparseArray rejectEvents = deepClone(mRejectEvents); - - return new AttributedOpEntry(parent.op, isRunning(), accessEvents, rejectEvents); - } - } - final class Op { int op; int uid; diff --git a/services/core/java/com/android/server/appop/AttributedOp.java b/services/core/java/com/android/server/appop/AttributedOp.java new file mode 100644 index 0000000000000..c37876bc889f7 --- /dev/null +++ b/services/core/java/com/android/server/appop/AttributedOp.java @@ -0,0 +1,626 @@ +/* + * 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.appop; + +import static android.app.AppOpsManager.MODE_ALLOWED; +import static android.app.AppOpsManager.OnOpStartedListener.START_TYPE_RESUMED; +import static android.app.AppOpsManager.makeKey; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.AppOpsManager; +import android.os.IBinder; +import android.os.Process; +import android.os.RemoteException; +import android.os.SystemClock; +import android.util.ArrayMap; +import android.util.LongSparseArray; +import android.util.Slog; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.util.function.pooled.PooledLambda; + +import java.util.ArrayList; +import java.util.List; + +final class AttributedOp { + private final @NonNull AppOpsService mAppOpsService; + public final @Nullable String tag; + public final @NonNull AppOpsService.Op parent; + + /** + * Last successful accesses (noteOp + finished startOp) for each uidState/opFlag combination + * + *

Key is {@link AppOpsManager#makeKey} + */ + @GuardedBy("mAppOpsService") + private @Nullable LongSparseArray mAccessEvents; + + /** + * Last rejected accesses for each uidState/opFlag combination + * + *

Key is {@link AppOpsManager#makeKey} + */ + @GuardedBy("mAppOpsService") + private @Nullable LongSparseArray mRejectEvents; + + /** + * Currently in progress startOp events + * + *

Key is clientId + */ + @GuardedBy("mAppOpsService") + @Nullable ArrayMap mInProgressEvents; + + /** + * Currently paused startOp events + * + *

Key is clientId + */ + @GuardedBy("mAppOpsService") + @Nullable ArrayMap mPausedInProgressEvents; + + AttributedOp(@NonNull AppOpsService appOpsService, @Nullable String tag, + @NonNull AppOpsService.Op parent) { + mAppOpsService = appOpsService; + this.tag = tag; + this.parent = parent; + } + + /** + * Update state when noteOp was rejected or startOp->finishOp event finished + * + * @param proxyUid The uid of the proxy + * @param proxyPackageName The package name of the proxy + * @param proxyAttributionTag the attributionTag in the proxies package + * @param uidState UID state of the app noteOp/startOp was called for + * @param flags OpFlags of the call + */ + public void accessed(int proxyUid, @Nullable String proxyPackageName, + @Nullable String proxyAttributionTag, @AppOpsManager.UidState int uidState, + @AppOpsManager.OpFlags int flags) { + long accessTime = System.currentTimeMillis(); + accessed(accessTime, -1, proxyUid, proxyPackageName, + proxyAttributionTag, uidState, flags); + + mAppOpsService.mHistoricalRegistry.incrementOpAccessedCount(parent.op, parent.uid, + parent.packageName, tag, uidState, flags, accessTime, + AppOpsManager.ATTRIBUTION_FLAGS_NONE, AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE); + } + + /** + * Add an access that was previously collected. + * + * @param noteTime The time of the event + * @param duration The duration of the event + * @param proxyUid The uid of the proxy + * @param proxyPackageName The package name of the proxy + * @param proxyAttributionTag the attributionTag in the proxies package + * @param uidState UID state of the app noteOp/startOp was called for + * @param flags OpFlags of the call + */ + @SuppressWarnings("GuardedBy") // Lock is held on mAppOpsService + public void accessed(long noteTime, long duration, int proxyUid, + @Nullable String proxyPackageName, @Nullable String proxyAttributionTag, + @AppOpsManager.UidState int uidState, @AppOpsManager.OpFlags int flags) { + long key = makeKey(uidState, flags); + + if (mAccessEvents == null) { + mAccessEvents = new LongSparseArray<>(1); + } + + AppOpsManager.OpEventProxyInfo proxyInfo = null; + if (proxyUid != Process.INVALID_UID) { + proxyInfo = mAppOpsService.mOpEventProxyInfoPool.acquire(proxyUid, proxyPackageName, + proxyAttributionTag); + } + + AppOpsManager.NoteOpEvent existingEvent = mAccessEvents.get(key); + if (existingEvent != null) { + existingEvent.reinit(noteTime, duration, proxyInfo, + mAppOpsService.mOpEventProxyInfoPool); + } else { + mAccessEvents.put(key, new AppOpsManager.NoteOpEvent(noteTime, duration, proxyInfo)); + } + } + + /** + * Update state when noteOp/startOp was rejected. + * + * @param uidState UID state of the app noteOp is called for + * @param flags OpFlags of the call + */ + public void rejected(@AppOpsManager.UidState int uidState, @AppOpsManager.OpFlags int flags) { + rejected(System.currentTimeMillis(), uidState, flags); + + mAppOpsService.mHistoricalRegistry.incrementOpRejected(parent.op, parent.uid, + parent.packageName, tag, uidState, flags); + } + + /** + * Add an rejection that was previously collected + * + * @param noteTime The time of the event + * @param uidState UID state of the app noteOp/startOp was called for + * @param flags OpFlags of the call + */ + @SuppressWarnings("GuardedBy") // Lock is held on mAppOpsService + public void rejected(long noteTime, @AppOpsManager.UidState int uidState, + @AppOpsManager.OpFlags int flags) { + long key = makeKey(uidState, flags); + + if (mRejectEvents == null) { + mRejectEvents = new LongSparseArray<>(1); + } + + // We do not collect proxy information for rejections yet + AppOpsManager.NoteOpEvent existingEvent = mRejectEvents.get(key); + if (existingEvent != null) { + existingEvent.reinit(noteTime, -1, null, mAppOpsService.mOpEventProxyInfoPool); + } else { + mRejectEvents.put(key, new AppOpsManager.NoteOpEvent(noteTime, -1, null)); + } + } + + /** + * Update state when start was called + * + * @param clientId Id of the startOp caller + * @param proxyUid The UID of the proxy app + * @param proxyPackageName The package name of the proxy app + * @param proxyAttributionTag The attribution tag of the proxy app + * @param uidState UID state of the app startOp is called for + * @param flags The proxy flags + * @param attributionFlags The attribution flags associated with this operation. + * @param attributionChainId The if of the attribution chain this operations is a part of. + */ + public void started(@NonNull IBinder clientId, int proxyUid, + @Nullable String proxyPackageName, @Nullable String proxyAttributionTag, + @AppOpsManager.UidState int uidState, @AppOpsManager.OpFlags int flags, + @AppOpsManager.AttributionFlags + int attributionFlags, int attributionChainId) throws RemoteException { + started(clientId, proxyUid, proxyPackageName, proxyAttributionTag, + uidState, flags, /*triggerCallbackIfNeeded*/ true, attributionFlags, + attributionChainId); + } + + private void started(@NonNull IBinder clientId, int proxyUid, + @Nullable String proxyPackageName, @Nullable String proxyAttributionTag, + @AppOpsManager.UidState int uidState, @AppOpsManager.OpFlags int flags, + boolean triggerCallbackIfNeeded, @AppOpsManager.AttributionFlags int attributionFlags, + int attributionChainId) throws RemoteException { + startedOrPaused(clientId, proxyUid, proxyPackageName, + proxyAttributionTag, uidState, flags, triggerCallbackIfNeeded, + /*triggerCallbackIfNeeded*/ true, attributionFlags, attributionChainId); + } + + @SuppressWarnings("GuardedBy") // Lock is held on mAppOpsService + private void startedOrPaused(@NonNull IBinder clientId, int proxyUid, + @Nullable String proxyPackageName, @Nullable String proxyAttributionTag, + @AppOpsManager.UidState int uidState, @AppOpsManager.OpFlags int flags, + boolean triggerCallbackIfNeeded, boolean isStarted, @AppOpsManager.AttributionFlags + int attributionFlags, int attributionChainId) throws RemoteException { + if (triggerCallbackIfNeeded && !parent.isRunning() && isStarted) { + mAppOpsService.scheduleOpActiveChangedIfNeededLocked(parent.op, parent.uid, + parent.packageName, tag, true, attributionFlags, attributionChainId); + } + + if (isStarted && mInProgressEvents == null) { + mInProgressEvents = new ArrayMap<>(1); + } else if (!isStarted && mPausedInProgressEvents == null) { + mPausedInProgressEvents = new ArrayMap<>(1); + } + ArrayMap events = isStarted + ? mInProgressEvents : mPausedInProgressEvents; + + long startTime = System.currentTimeMillis(); + AppOpsService.InProgressStartOpEvent event = events.get(clientId); + if (event == null) { + event = mAppOpsService.mInProgressStartOpEventPool.acquire(startTime, + SystemClock.elapsedRealtime(), clientId, tag, + PooledLambda.obtainRunnable(AppOpsService::onClientDeath, this, clientId), + proxyUid, proxyPackageName, proxyAttributionTag, uidState, flags, + attributionFlags, attributionChainId); + events.put(clientId, event); + } else { + if (uidState != event.getUidState()) { + onUidStateChanged(uidState); + } + } + + event.numUnfinishedStarts++; + + if (isStarted) { + mAppOpsService.mHistoricalRegistry.incrementOpAccessedCount(parent.op, parent.uid, + parent.packageName, tag, uidState, flags, startTime, attributionFlags, + attributionChainId); + } + } + + /** + * Update state when finishOp was called. Will finish started ops, and delete paused ops. + * + * @param clientId Id of the finishOp caller + */ + public void finished(@NonNull IBinder clientId) { + finished(clientId, true); + } + + private void finished(@NonNull IBinder clientId, boolean triggerCallbackIfNeeded) { + finishOrPause(clientId, triggerCallbackIfNeeded, false); + } + + /** + * Update state when paused or finished is called. If pausing, it records the op as + * stopping in the HistoricalRegistry, but does not delete it. + */ + @SuppressWarnings("GuardedBy") // Lock is held on mAppOpsService + private void finishOrPause(@NonNull IBinder clientId, boolean triggerCallbackIfNeeded, + boolean isPausing) { + int indexOfToken = isRunning() ? mInProgressEvents.indexOfKey(clientId) : -1; + if (indexOfToken < 0) { + finishPossiblyPaused(clientId, isPausing); + return; + } + + AppOpsService.InProgressStartOpEvent event = mInProgressEvents.valueAt(indexOfToken); + if (!isPausing) { + event.numUnfinishedStarts--; + } + // If we are pausing, create a NoteOpEvent, but don't change the InProgress event + if (event.numUnfinishedStarts == 0 || isPausing) { + if (!isPausing) { + event.finish(); + mInProgressEvents.removeAt(indexOfToken); + } + + if (mAccessEvents == null) { + mAccessEvents = new LongSparseArray<>(1); + } + + AppOpsManager.OpEventProxyInfo proxyCopy = event.getProxy() != null + ? new AppOpsManager.OpEventProxyInfo(event.getProxy()) : null; + + long accessDurationMillis = + SystemClock.elapsedRealtime() - event.getStartElapsedTime(); + AppOpsManager.NoteOpEvent finishedEvent = new AppOpsManager.NoteOpEvent( + event.getStartTime(), + accessDurationMillis, proxyCopy); + mAccessEvents.put(makeKey(event.getUidState(), event.getFlags()), + finishedEvent); + + mAppOpsService.mHistoricalRegistry.increaseOpAccessDuration(parent.op, parent.uid, + parent.packageName, tag, event.getUidState(), + event.getFlags(), finishedEvent.getNoteTime(), finishedEvent.getDuration(), + event.getAttributionFlags(), event.getAttributionChainId()); + + if (!isPausing) { + mAppOpsService.mInProgressStartOpEventPool.release(event); + if (mInProgressEvents.isEmpty()) { + mInProgressEvents = null; + + // TODO ntmyren: Also callback for single attribution tag activity changes + if (triggerCallbackIfNeeded && !parent.isRunning()) { + mAppOpsService.scheduleOpActiveChangedIfNeededLocked(parent.op, + parent.uid, parent.packageName, tag, false, + event.getAttributionFlags(), event.getAttributionChainId()); + } + } + } + } + } + + // Finish or pause (no-op) an already paused op + @SuppressWarnings("GuardedBy") // Lock is held on mAppOpsService + private void finishPossiblyPaused(@NonNull IBinder clientId, boolean isPausing) { + if (!isPaused()) { + Slog.wtf(AppOpsService.TAG, "No ops running or paused"); + return; + } + + int indexOfToken = mPausedInProgressEvents.indexOfKey(clientId); + if (indexOfToken < 0) { + Slog.wtf(AppOpsService.TAG, "No op running or paused for the client"); + return; + } else if (isPausing) { + // already paused + return; + } + + // no need to record a paused event finishing. + AppOpsService.InProgressStartOpEvent event = mPausedInProgressEvents.valueAt(indexOfToken); + event.numUnfinishedStarts--; + if (event.numUnfinishedStarts == 0) { + mPausedInProgressEvents.removeAt(indexOfToken); + mAppOpsService.mInProgressStartOpEventPool.release(event); + if (mPausedInProgressEvents.isEmpty()) { + mPausedInProgressEvents = null; + } + } + } + + /** + * Create an event that will be started, if the op is unpaused. + */ + public void createPaused(@NonNull IBinder clientId, int proxyUid, + @Nullable String proxyPackageName, @Nullable String proxyAttributionTag, + @AppOpsManager.UidState int uidState, @AppOpsManager.OpFlags int flags, + @AppOpsManager.AttributionFlags + int attributionFlags, int attributionChainId) throws RemoteException { + startedOrPaused(clientId, proxyUid, proxyPackageName, proxyAttributionTag, + uidState, flags, true, false, attributionFlags, attributionChainId); + } + + /** + * Pause all currently started ops. This will create a HistoricalRegistry + */ + public void pause() { + if (!isRunning()) { + return; + } + + if (mPausedInProgressEvents == null) { + mPausedInProgressEvents = new ArrayMap<>(1); + } + + for (int i = 0; i < mInProgressEvents.size(); i++) { + AppOpsService.InProgressStartOpEvent event = mInProgressEvents.valueAt(i); + mPausedInProgressEvents.put(event.getClientId(), event); + finishOrPause(event.getClientId(), true, true); + + mAppOpsService.scheduleOpActiveChangedIfNeededLocked(parent.op, parent.uid, + parent.packageName, tag, false, + event.getAttributionFlags(), event.getAttributionChainId()); + } + mInProgressEvents = null; + } + + /** + * Unpause all currently paused ops. This will reinitialize their start and duration + * times, but keep all other values the same + */ + public void resume() { + if (!isPaused()) { + return; + } + + if (mInProgressEvents == null) { + mInProgressEvents = new ArrayMap<>(mPausedInProgressEvents.size()); + } + boolean shouldSendActive = !mPausedInProgressEvents.isEmpty() + && mInProgressEvents.isEmpty(); + + long startTime = System.currentTimeMillis(); + for (int i = 0; i < mPausedInProgressEvents.size(); i++) { + AppOpsService.InProgressStartOpEvent event = mPausedInProgressEvents.valueAt(i); + mInProgressEvents.put(event.getClientId(), event); + event.setStartElapsedTime(SystemClock.elapsedRealtime()); + event.setStartTime(startTime); + mAppOpsService.mHistoricalRegistry.incrementOpAccessedCount(parent.op, parent.uid, + parent.packageName, tag, event.getUidState(), event.getFlags(), startTime, + event.getAttributionFlags(), event.getAttributionChainId()); + if (shouldSendActive) { + mAppOpsService.scheduleOpActiveChangedIfNeededLocked(parent.op, parent.uid, + parent.packageName, tag, true, event.getAttributionFlags(), + event.getAttributionChainId()); + } + // Note: this always sends MODE_ALLOWED, even if the mode is FOREGROUND + // TODO ntmyren: figure out how to get the real mode. + mAppOpsService.scheduleOpStartedIfNeededLocked(parent.op, parent.uid, + parent.packageName, tag, event.getFlags(), MODE_ALLOWED, START_TYPE_RESUMED, + event.getAttributionFlags(), event.getAttributionChainId()); + } + mPausedInProgressEvents = null; + } + + /** + * Called in the case the client dies without calling finish first + * + * @param clientId The client that died + */ + void onClientDeath(@NonNull IBinder clientId) { + synchronized (mAppOpsService) { + if (!isPaused() && !isRunning()) { + return; + } + + ArrayMap events = isPaused() + ? mPausedInProgressEvents : mInProgressEvents; + AppOpsService.InProgressStartOpEvent deadEvent = events.get(clientId); + if (deadEvent != null) { + deadEvent.numUnfinishedStarts = 1; + } + + finished(clientId); + } + } + + /** + * Notify that the state of the uid changed + * + * @param newState The new state + */ + public void onUidStateChanged(@AppOpsManager.UidState int newState) { + if (!isPaused() && !isRunning()) { + return; + } + + boolean isRunning = isRunning(); + ArrayMap events = + isRunning ? mInProgressEvents : mPausedInProgressEvents; + + int numInProgressEvents = events.size(); + List binders = new ArrayList<>(events.keySet()); + for (int i = 0; i < numInProgressEvents; i++) { + AppOpsService.InProgressStartOpEvent event = events.get(binders.get(i)); + + if (event != null && event.getUidState() != newState) { + try { + // Remove all but one unfinished start count and then call finished() to + // remove start event object + int numPreviousUnfinishedStarts = event.numUnfinishedStarts; + event.numUnfinishedStarts = 1; + AppOpsManager.OpEventProxyInfo proxy = event.getProxy(); + + finished(event.getClientId(), false); + + // Call started() to add a new start event object and then add the + // previously removed unfinished start counts back + if (proxy != null) { + startedOrPaused(event.getClientId(), proxy.getUid(), + proxy.getPackageName(), proxy.getAttributionTag(), newState, + event.getFlags(), false, isRunning, + event.getAttributionFlags(), event.getAttributionChainId()); + } else { + startedOrPaused(event.getClientId(), Process.INVALID_UID, null, null, + newState, event.getFlags(), false, isRunning, + event.getAttributionFlags(), event.getAttributionChainId()); + } + + events = isRunning ? mInProgressEvents : mPausedInProgressEvents; + AppOpsService.InProgressStartOpEvent newEvent = events.get(binders.get(i)); + if (newEvent != null) { + newEvent.numUnfinishedStarts += numPreviousUnfinishedStarts - 1; + } + } catch (RemoteException e) { + if (AppOpsService.DEBUG) { + Slog.e(AppOpsService.TAG, + "Cannot switch to new uidState " + newState); + } + } + } + } + } + + /** + * Combine {@code a} and {@code b} and return the result. The result might be {@code a} + * or {@code b}. If there is an event for the same key in both the later event is retained. + */ + private @Nullable LongSparseArray add( + @Nullable LongSparseArray a, + @Nullable LongSparseArray b) { + if (a == null) { + return b; + } + + if (b == null) { + return a; + } + + int numEventsToAdd = b.size(); + for (int i = 0; i < numEventsToAdd; i++) { + long keyOfEventToAdd = b.keyAt(i); + AppOpsManager.NoteOpEvent bEvent = b.valueAt(i); + AppOpsManager.NoteOpEvent aEvent = a.get(keyOfEventToAdd); + + if (aEvent == null || bEvent.getNoteTime() > aEvent.getNoteTime()) { + a.put(keyOfEventToAdd, bEvent); + } + } + + return a; + } + + /** + * Add all data from the {@code opToAdd} to this op. + * + *

If there is an event for the same key in both the later event is retained. + *

{@code opToAdd} should not be used after this method is called. + * + * @param opToAdd The op to add + */ + @SuppressWarnings("GuardedBy") // Lock is held on mAppOpsService + public void add(@NonNull AttributedOp opToAdd) { + if (opToAdd.isRunning() || opToAdd.isPaused()) { + ArrayMap ignoredEvents = + opToAdd.isRunning() + ? opToAdd.mInProgressEvents : opToAdd.mPausedInProgressEvents; + Slog.w(AppOpsService.TAG, "Ignoring " + ignoredEvents.size() + " app-ops, running: " + + opToAdd.isRunning()); + + int numInProgressEvents = ignoredEvents.size(); + for (int i = 0; i < numInProgressEvents; i++) { + AppOpsService.InProgressStartOpEvent event = ignoredEvents.valueAt(i); + + event.finish(); + mAppOpsService.mInProgressStartOpEventPool.release(event); + } + } + + mAccessEvents = add(mAccessEvents, opToAdd.mAccessEvents); + mRejectEvents = add(mRejectEvents, opToAdd.mRejectEvents); + } + + public boolean isRunning() { + return mInProgressEvents != null && !mInProgressEvents.isEmpty(); + } + + public boolean isPaused() { + return mPausedInProgressEvents != null && !mPausedInProgressEvents.isEmpty(); + } + + boolean hasAnyTime() { + return (mAccessEvents != null && mAccessEvents.size() > 0) + || (mRejectEvents != null && mRejectEvents.size() > 0); + } + + /** + * Clone a {@link LongSparseArray} and clone all values. + */ + private @Nullable LongSparseArray deepClone( + @Nullable LongSparseArray original) { + if (original == null) { + return original; + } + + int size = original.size(); + LongSparseArray clone = new LongSparseArray<>(size); + for (int i = 0; i < size; i++) { + clone.put(original.keyAt(i), new AppOpsManager.NoteOpEvent(original.valueAt(i))); + } + + return clone; + } + + @NonNull AppOpsManager.AttributedOpEntry createAttributedOpEntryLocked() { + LongSparseArray accessEvents = deepClone(mAccessEvents); + + // Add in progress events as access events + if (isRunning()) { + long now = SystemClock.elapsedRealtime(); + int numInProgressEvents = mInProgressEvents.size(); + + if (accessEvents == null) { + accessEvents = new LongSparseArray<>(numInProgressEvents); + } + + for (int i = 0; i < numInProgressEvents; i++) { + AppOpsService.InProgressStartOpEvent event = mInProgressEvents.valueAt(i); + + accessEvents.append(makeKey(event.getUidState(), event.getFlags()), + new AppOpsManager.NoteOpEvent(event.getStartTime(), + now - event.getStartElapsedTime(), + event.getProxy())); + } + } + + LongSparseArray rejectEvents = deepClone(mRejectEvents); + + return new AppOpsManager.AttributedOpEntry(parent.op, isRunning(), accessEvents, + rejectEvents); + } +}