• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.server.appop;
18 
19 import static android.app.AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE;
20 import static android.app.AppOpsManager.ATTRIBUTION_FLAG_ACCESSOR;
21 import static android.app.AppOpsManager.ATTRIBUTION_FLAG_RECEIVER;
22 import static android.app.AppOpsManager.ATTRIBUTION_FLAG_TRUSTED;
23 import static android.app.AppOpsManager.flagsToString;
24 import static android.app.AppOpsManager.getUidStateName;
25 
26 import android.annotation.NonNull;
27 import android.annotation.Nullable;
28 import android.app.AppOpsManager;
29 import android.content.Context;
30 import android.os.Handler;
31 import android.os.Looper;
32 import android.os.Message;
33 import android.os.Process;
34 import android.util.ArraySet;
35 import android.util.IntArray;
36 import android.util.LongSparseArray;
37 import android.util.Slog;
38 
39 import com.android.server.ServiceThread;
40 
41 import java.io.File;
42 import java.io.PrintWriter;
43 import java.text.SimpleDateFormat;
44 import java.time.Duration;
45 import java.time.Instant;
46 import java.time.temporal.ChronoUnit;
47 import java.util.ArrayList;
48 import java.util.Date;
49 import java.util.List;
50 import java.util.Objects;
51 import java.util.Set;
52 
53 /**
54  * This class handles sqlite persistence layer for discrete ops.
55  */
56 public class DiscreteOpsSqlRegistry extends DiscreteOpsRegistry {
57     private static final String TAG = "DiscreteOpsSqlRegistry";
58 
59     private static final long DB_WRITE_INTERVAL = Duration.ofMinutes(10).toMillis();
60     private static final long EXPIRED_ENTRY_DELETION_INTERVAL = Duration.ofHours(6).toMillis();
61 
62     // Event type handled by SqliteWriteHandler
63     private static final int WRITE_DATABASE_RECURRING = 1;
64     private static final int DELETE_EXPIRED_ENTRIES = 2;
65     private static final int WRITE_DATABASE_CACHE_FULL = 3;
66 
67     private final Context mContext;
68     private final DiscreteOpsDbHelper mDiscreteOpsDbHelper;
69     private final SqliteWriteHandler mSqliteWriteHandler;
70     private final DiscreteOpCache mDiscreteOpCache = new DiscreteOpCache(512);
71     // Attribution chain id is used to identify an attribution source chain, This is
72     // set for startOp only. PermissionManagerService resets this ID on device restart, so
73     // we use previously persisted chain id as offset, and add it to chain id received from
74     // permission manager service.
75     private long mChainIdOffset;
76     private final File mDatabaseFile;
77 
DiscreteOpsSqlRegistry(Context context)78     DiscreteOpsSqlRegistry(Context context) {
79         this(context, DiscreteOpsDbHelper.getDatabaseFile());
80     }
81 
DiscreteOpsSqlRegistry(Context context, File databaseFile)82     DiscreteOpsSqlRegistry(Context context, File databaseFile) {
83         ServiceThread thread = new ServiceThread(TAG, Process.THREAD_PRIORITY_BACKGROUND, true);
84         thread.start();
85         mContext = context;
86         mDatabaseFile = databaseFile;
87         mSqliteWriteHandler = new SqliteWriteHandler(thread.getLooper());
88         mDiscreteOpsDbHelper = new DiscreteOpsDbHelper(context, databaseFile);
89         mChainIdOffset = mDiscreteOpsDbHelper.getLargestAttributionChainId();
90         mSqliteWriteHandler.sendEmptyMessageDelayed(WRITE_DATABASE_RECURRING, DB_WRITE_INTERVAL);
91         mSqliteWriteHandler.sendEmptyMessageDelayed(DELETE_EXPIRED_ENTRIES,
92                 EXPIRED_ENTRY_DELETION_INTERVAL);
93     }
94 
95     @Override
recordDiscreteAccess(int uid, String packageName, @NonNull String deviceId, int op, @Nullable String attributionTag, int flags, int uidState, long accessTime, long accessDuration, int attributionFlags, int attributionChainId)96     void recordDiscreteAccess(int uid, String packageName,
97             @NonNull String deviceId, int op,
98             @Nullable String attributionTag, int flags, int uidState,
99             long accessTime, long accessDuration, int attributionFlags, int attributionChainId) {
100         if (!isDiscreteOp(op, flags)) {
101             return;
102         }
103 
104         long offsetChainId = attributionChainId;
105         if (attributionChainId != ATTRIBUTION_CHAIN_ID_NONE) {
106             offsetChainId = attributionChainId + mChainIdOffset;
107             // PermissionManagerService chain id reached the max value,
108             // reset offset, it's going to be very rare.
109             if (attributionChainId == Integer.MAX_VALUE) {
110                 mChainIdOffset = offsetChainId;
111             }
112         }
113         DiscreteOp discreteOpEvent = new DiscreteOp(uid, packageName, attributionTag, deviceId, op,
114                 flags, attributionFlags, uidState, offsetChainId, accessTime, accessDuration);
115         mDiscreteOpCache.add(discreteOpEvent);
116     }
117 
118     @Override
shutdown()119     void shutdown() {
120         mSqliteWriteHandler.removeAllPendingMessages();
121         mDiscreteOpsDbHelper.insertDiscreteOps(mDiscreteOpCache.evictAllAppOpEvents());
122     }
123 
124     @Override
writeAndClearOldAccessHistory()125     void writeAndClearOldAccessHistory() {
126         // no-op
127     }
128 
129     @Override
clearHistory()130     void clearHistory() {
131         mDiscreteOpCache.clear();
132         mDiscreteOpsDbHelper.execSQL(DiscreteOpsTable.DELETE_TABLE_DATA);
133     }
134 
135     @Override
clearHistory(int uid, String packageName)136     void clearHistory(int uid, String packageName) {
137         mDiscreteOpCache.clear(uid, packageName);
138         mDiscreteOpsDbHelper.execSQL(DiscreteOpsTable.DELETE_DATA_FOR_UID_PACKAGE,
139                 new Object[]{uid, packageName});
140     }
141 
142     @Override
offsetHistory(long offset)143     void offsetHistory(long offset) {
144         mDiscreteOpCache.offsetTimestamp(offset);
145         mDiscreteOpsDbHelper.execSQL(DiscreteOpsTable.OFFSET_ACCESS_TIME,
146                 new Object[]{offset});
147     }
148 
getAppOpCodes(@ppOpsManager.HistoricalOpsRequestFilter int filter, @Nullable String[] opNamesFilter)149     private IntArray getAppOpCodes(@AppOpsManager.HistoricalOpsRequestFilter int filter,
150             @Nullable String[] opNamesFilter) {
151         if ((filter & AppOpsManager.FILTER_BY_OP_NAMES) != 0) {
152             IntArray opCodes = new IntArray(opNamesFilter.length);
153             for (int i = 0; i < opNamesFilter.length; i++) {
154                 int op;
155                 try {
156                     op = AppOpsManager.strOpToOp(opNamesFilter[i]);
157                 } catch (IllegalArgumentException ex) {
158                     Slog.w(TAG, "Appop `" + opNamesFilter[i] + "` is not recognized.");
159                     continue;
160                 }
161                 opCodes.add(op);
162             }
163             return opCodes;
164         }
165         return null;
166     }
167 
168     @Override
addFilteredDiscreteOpsToHistoricalOps(AppOpsManager.HistoricalOps result, long beginTimeMillis, long endTimeMillis, int filter, int uidFilter, @Nullable String packageNameFilter, @Nullable String[] opNamesFilter, @Nullable String attributionTagFilter, int opFlagsFilter, Set<String> attributionExemptPkgs)169     void addFilteredDiscreteOpsToHistoricalOps(AppOpsManager.HistoricalOps result,
170             long beginTimeMillis, long endTimeMillis, int filter, int uidFilter,
171             @Nullable String packageNameFilter,
172             @Nullable String[] opNamesFilter,
173             @Nullable String attributionTagFilter, int opFlagsFilter,
174             Set<String> attributionExemptPkgs) {
175         IntArray opCodes = getAppOpCodes(filter, opNamesFilter);
176         // flush the cache into database before read.
177         if (opCodes != null) {
178             mDiscreteOpsDbHelper.insertDiscreteOps(mDiscreteOpCache.evictAppOpEvents(opCodes));
179         } else {
180             mDiscreteOpsDbHelper.insertDiscreteOps(mDiscreteOpCache.evictAllAppOpEvents());
181         }
182         boolean assembleChains = attributionExemptPkgs != null;
183         beginTimeMillis = Math.max(beginTimeMillis, Instant.now().minus(sDiscreteHistoryCutoff,
184                 ChronoUnit.MILLIS).toEpochMilli());
185         List<DiscreteOp> discreteOps = mDiscreteOpsDbHelper.getDiscreteOps(filter, uidFilter,
186                 packageNameFilter, attributionTagFilter, opCodes, opFlagsFilter, beginTimeMillis,
187                 endTimeMillis, -1, null, false);
188 
189         LongSparseArray<AttributionChain> attributionChains = null;
190         if (assembleChains) {
191             attributionChains = createAttributionChains(discreteOps, attributionExemptPkgs);
192         }
193 
194         int nEvents = discreteOps.size();
195         for (int j = 0; j < nEvents; j++) {
196             DiscreteOp event = discreteOps.get(j);
197             AppOpsManager.OpEventProxyInfo proxy = null;
198             if (assembleChains && event.mChainId != ATTRIBUTION_CHAIN_ID_NONE) {
199                 AttributionChain chain = attributionChains.get(event.mChainId);
200                 if (chain != null && chain.isComplete()
201                         && chain.isStart(event)
202                         && chain.mLastVisibleEvent != null) {
203                     DiscreteOp proxyEvent = chain.mLastVisibleEvent;
204                     proxy = new AppOpsManager.OpEventProxyInfo(proxyEvent.mUid,
205                             proxyEvent.mPackageName, proxyEvent.mAttributionTag);
206                 }
207             }
208             result.addDiscreteAccess(event.mOpCode, event.mUid, event.mPackageName,
209                     event.mAttributionTag, event.mUidState, event.mOpFlags,
210                     event.mDiscretizedAccessTime, event.mDiscretizedDuration, proxy);
211         }
212     }
213 
214     @Override
dump(@onNull 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)215     void dump(@NonNull PrintWriter pw, int uidFilter, @Nullable String packageNameFilter,
216             @Nullable String attributionTagFilter,
217             @AppOpsManager.HistoricalOpsRequestFilter int filter, int dumpOp,
218             @NonNull SimpleDateFormat sdf, @NonNull Date date, @NonNull String prefix,
219             int nDiscreteOps) {
220         // flush the cache into database before dump.
221         mDiscreteOpsDbHelper.insertDiscreteOps(mDiscreteOpCache.evictAllAppOpEvents());
222         IntArray opCodes = new IntArray();
223         if (dumpOp != AppOpsManager.OP_NONE) {
224             opCodes.add(dumpOp);
225         }
226         List<DiscreteOp> discreteOps = mDiscreteOpsDbHelper.getDiscreteOps(filter, uidFilter,
227                 packageNameFilter, attributionTagFilter, opCodes, 0, -1,
228                 -1, nDiscreteOps, DiscreteOpsTable.Columns.ACCESS_TIME, false);
229 
230         pw.print(prefix);
231         pw.print("Largest chain id: ");
232         pw.print(mDiscreteOpsDbHelper.getLargestAttributionChainId());
233         pw.println();
234         pw.println("UID|PACKAGE_NAME|DEVICE_ID|OP_NAME|ATTRIBUTION_TAG|UID_STATE|OP_FLAGS|"
235                 + "ATTR_FLAGS|CHAIN_ID|ACCESS_TIME|DURATION");
236         int discreteOpsCount = discreteOps.size();
237         for (int i = 0; i < discreteOpsCount; i++) {
238             DiscreteOp event = discreteOps.get(i);
239             date.setTime(event.mAccessTime);
240             pw.println(event.mUid + "|" + event.mPackageName + "|" + event.mDeviceId + "|"
241                     + AppOpsManager.opToName(event.mOpCode) + "|" + event.mAttributionTag + "|"
242                     + getUidStateName(event.mUidState) + "|"
243                     + flagsToString(event.mOpFlags) + "|" + event.mAttributionFlags + "|"
244                     + event.mChainId + "|"
245                     + sdf.format(date) + "|" + event.mDuration);
246         }
247         pw.println();
248     }
249 
migrateXmlData(List<DiscreteOp> opEvents, int chainIdOffset)250     void migrateXmlData(List<DiscreteOp> opEvents, int chainIdOffset) {
251         mChainIdOffset = chainIdOffset;
252         mDiscreteOpsDbHelper.insertDiscreteOps(opEvents);
253     }
254 
createAttributionChains( List<DiscreteOp> discreteOps, Set<String> attributionExemptPkgs)255     LongSparseArray<AttributionChain> createAttributionChains(
256             List<DiscreteOp> discreteOps, Set<String> attributionExemptPkgs) {
257         LongSparseArray<AttributionChain> chains = new LongSparseArray<>();
258         final int count = discreteOps.size();
259 
260         for (int i = 0; i < count; i++) {
261             DiscreteOp opEvent = discreteOps.get(i);
262             if (opEvent.mChainId == ATTRIBUTION_CHAIN_ID_NONE
263                     || (opEvent.mAttributionFlags & ATTRIBUTION_FLAG_TRUSTED) == 0) {
264                 continue;
265             }
266             AttributionChain chain = chains.get(opEvent.mChainId);
267             if (chain == null) {
268                 chain = new AttributionChain(attributionExemptPkgs);
269                 chains.put(opEvent.mChainId, chain);
270             }
271             chain.addEvent(opEvent);
272         }
273         return chains;
274     }
275 
276     static class AttributionChain {
277         List<DiscreteOp> mChain = new ArrayList<>();
278         Set<String> mExemptPkgs;
279         DiscreteOp mStartEvent = null;
280         DiscreteOp mLastVisibleEvent = null;
281 
AttributionChain(Set<String> exemptPkgs)282         AttributionChain(Set<String> exemptPkgs) {
283             mExemptPkgs = exemptPkgs;
284         }
285 
isComplete()286         boolean isComplete() {
287             return !mChain.isEmpty() && getStart() != null && isEnd(mChain.get(mChain.size() - 1));
288         }
289 
getStart()290         DiscreteOp getStart() {
291             return mChain.isEmpty() || !isStart(mChain.get(0)) ? null : mChain.get(0);
292         }
293 
isEnd(DiscreteOp event)294         private boolean isEnd(DiscreteOp event) {
295             return event != null
296                     && (event.mAttributionFlags & ATTRIBUTION_FLAG_ACCESSOR) != 0;
297         }
298 
isStart(DiscreteOp event)299         private boolean isStart(DiscreteOp event) {
300             return event != null
301                     && (event.mAttributionFlags & ATTRIBUTION_FLAG_RECEIVER) != 0;
302         }
303 
getLastVisible()304         DiscreteOp getLastVisible() {
305             // Search all nodes but the first one, which is the start node
306             for (int i = mChain.size() - 1; i > 0; i--) {
307                 DiscreteOp event = mChain.get(i);
308                 if (!mExemptPkgs.contains(event.mPackageName)) {
309                     return event;
310                 }
311             }
312             return null;
313         }
314 
addEvent(DiscreteOp opEvent)315         void addEvent(DiscreteOp opEvent) {
316             // check if we have a matching event except duration.
317             DiscreteOp matchingItem = null;
318             for (int i = 0; i < mChain.size(); i++) {
319                 DiscreteOp item = mChain.get(i);
320                 if (item.equalsExceptDuration(opEvent)) {
321                     matchingItem = item;
322                     break;
323                 }
324             }
325 
326             if (matchingItem != null) {
327                 // exact match or existing event has longer duration
328                 if (matchingItem.mDuration == opEvent.mDuration
329                         || matchingItem.mDuration > opEvent.mDuration) {
330                     return;
331                 }
332                 mChain.remove(matchingItem);
333             }
334 
335             if (mChain.isEmpty() || isEnd(opEvent)) {
336                 mChain.add(opEvent);
337             } else if (isStart(opEvent)) {
338                 mChain.add(0, opEvent);
339             } else {
340                 for (int i = 0; i < mChain.size(); i++) {
341                     DiscreteOp currEvent = mChain.get(i);
342                     if ((!isStart(currEvent)
343                             && currEvent.mAccessTime > opEvent.mAccessTime)
344                             || (i == mChain.size() - 1 && isEnd(currEvent))) {
345                         mChain.add(i, opEvent);
346                         break;
347                     } else if (i == mChain.size() - 1) {
348                         mChain.add(opEvent);
349                         break;
350                     }
351                 }
352             }
353             mStartEvent = isComplete() ? getStart() : null;
354             mLastVisibleEvent = isComplete() ? getLastVisible() : null;
355         }
356     }
357 
358     /**
359      * Handler to write asynchronously to sqlite database.
360      */
361     class SqliteWriteHandler extends Handler {
SqliteWriteHandler(Looper looper)362         SqliteWriteHandler(Looper looper) {
363             super(looper);
364         }
365 
366         @Override
handleMessage(Message msg)367         public void handleMessage(Message msg) {
368             switch (msg.what) {
369                 case WRITE_DATABASE_RECURRING -> {
370                     try {
371                         List<DiscreteOp> evictedEvents;
372                         synchronized (mDiscreteOpCache) {
373                             evictedEvents = mDiscreteOpCache.evictOldAppOpEvents();
374                         }
375                         mDiscreteOpsDbHelper.insertDiscreteOps(evictedEvents);
376                     } finally {
377                         mSqliteWriteHandler.sendEmptyMessageDelayed(WRITE_DATABASE_RECURRING,
378                                 DB_WRITE_INTERVAL);
379                         // Schedule a cleanup to truncate older (before cutoff time) entries.
380                         if (!mSqliteWriteHandler.hasMessages(DELETE_EXPIRED_ENTRIES)) {
381                             mSqliteWriteHandler.sendEmptyMessageDelayed(DELETE_EXPIRED_ENTRIES,
382                                     EXPIRED_ENTRY_DELETION_INTERVAL);
383                         }
384                     }
385                 }
386                 case DELETE_EXPIRED_ENTRIES -> {
387                     long cutOffTimeStamp = System.currentTimeMillis() - sDiscreteHistoryCutoff;
388                     mDiscreteOpsDbHelper.execSQL(
389                             DiscreteOpsTable.DELETE_TABLE_DATA_BEFORE_ACCESS_TIME,
390                             new Object[]{cutOffTimeStamp});
391                 }
392                 case WRITE_DATABASE_CACHE_FULL -> {
393                     try {
394                         List<DiscreteOp> evictedEvents;
395                         synchronized (mDiscreteOpCache) {
396                             evictedEvents = mDiscreteOpCache.evictOldAppOpEvents();
397                             // if nothing to evict, just write the whole cache to database.
398                             if (evictedEvents.isEmpty()
399                                     && mDiscreteOpCache.size() >= mDiscreteOpCache.capacity()) {
400                                 evictedEvents.addAll(mDiscreteOpCache.mCache);
401                                 mDiscreteOpCache.clear();
402                             }
403                         }
404                         mDiscreteOpsDbHelper.insertDiscreteOps(evictedEvents);
405                     } finally {
406                         // Just in case initial message is not scheduled.
407                         if (!mSqliteWriteHandler.hasMessages(WRITE_DATABASE_RECURRING)) {
408                             mSqliteWriteHandler.sendEmptyMessageDelayed(WRITE_DATABASE_RECURRING,
409                                     DB_WRITE_INTERVAL);
410                         }
411                     }
412                 }
413                 default -> throw new IllegalStateException("Unexpected value: " + msg.what);
414             }
415         }
416 
removeAllPendingMessages()417         void removeAllPendingMessages() {
418             removeMessages(WRITE_DATABASE_RECURRING);
419             removeMessages(DELETE_EXPIRED_ENTRIES);
420             removeMessages(WRITE_DATABASE_CACHE_FULL);
421         }
422     }
423 
424     /**
425      * A write cache for discrete ops. The noteOp, start/finishOp discrete op events are written to
426      * the cache first.
427      * <p>
428      * These events are persisted into sqlite database
429      * 1) Periodic interval, controlled by {@link AppOpsService}
430      * 2) When total events in the cache exceeds cache limit.
431      * 3) During read call we flush the whole cache to sqlite.
432      * 4) During shutdown.
433      */
434     class DiscreteOpCache {
435         private static final String TAG = "DiscreteOpCache";
436         private final int mCapacity;
437         private final ArraySet<DiscreteOp> mCache;
438 
DiscreteOpCache(int capacity)439         DiscreteOpCache(int capacity) {
440             mCapacity = capacity;
441             mCache = new ArraySet<>();
442         }
443 
add(DiscreteOp opEvent)444         public void add(DiscreteOp opEvent) {
445             synchronized (this) {
446                 if (mCache.contains(opEvent)) {
447                     return;
448                 }
449                 mCache.add(opEvent);
450 
451                 if (mCache.size() >= mCapacity) {
452                     mSqliteWriteHandler.sendEmptyMessage(WRITE_DATABASE_CACHE_FULL);
453                 }
454             }
455         }
456 
457         /**
458          * Evict entries older than {@link DiscreteOpsRegistry#sDiscreteHistoryQuantization} i.e.
459          * app op events older than one minute (default quantization) will be evicted.
460          */
evictOldAppOpEvents()461         private List<DiscreteOp> evictOldAppOpEvents() {
462             synchronized (this) {
463                 List<DiscreteOp> evictedEvents = new ArrayList<>();
464                 Set<DiscreteOp> snapshot = new ArraySet<>(mCache);
465                 long evictionTimestamp = System.currentTimeMillis() - sDiscreteHistoryQuantization;
466                 evictionTimestamp = discretizeTimeStamp(evictionTimestamp);
467                 for (DiscreteOp opEvent : snapshot) {
468                     if (opEvent.mDiscretizedAccessTime <= evictionTimestamp) {
469                         evictedEvents.add(opEvent);
470                         mCache.remove(opEvent);
471                     }
472                 }
473                 return evictedEvents;
474             }
475         }
476 
477         /**
478          * Evict all app op entries from cache, and return the list of removed ops.
479          */
evictAllAppOpEvents()480         public List<DiscreteOp> evictAllAppOpEvents() {
481             synchronized (this) {
482                 List<DiscreteOp> cachedOps = new ArrayList<>(mCache.size());
483                 if (mCache.isEmpty()) {
484                     return cachedOps;
485                 }
486                 cachedOps.addAll(mCache);
487                 mCache.clear();
488                 return cachedOps;
489             }
490         }
491 
492         /**
493          * Evict specified app ops from cache, and return the list of evicted ops.
494          */
evictAppOpEvents(IntArray ops)495         public List<DiscreteOp> evictAppOpEvents(IntArray ops) {
496             synchronized (this) {
497                 List<DiscreteOp> evictedOps = new ArrayList<>();
498                 if (mCache.isEmpty()) {
499                     return evictedOps;
500                 }
501                 for (DiscreteOp discreteOp: mCache) {
502                     if (ops.contains(discreteOp.getOpCode())) {
503                         evictedOps.add(discreteOp);
504                     }
505                 }
506                 evictedOps.forEach(mCache::remove);
507                 return evictedOps;
508             }
509         }
510 
size()511         int size() {
512             return mCache.size();
513         }
514 
capacity()515         int capacity() {
516             return mCapacity;
517         }
518 
519         /**
520          * Remove all entries from the cache.
521          */
clear()522         public void clear() {
523             synchronized (this) {
524                 mCache.clear();
525             }
526         }
527 
528         /**
529          * Offset access time by given offset milliseconds.
530          */
offsetTimestamp(long offsetMillis)531         public void offsetTimestamp(long offsetMillis) {
532             synchronized (this) {
533                 List<DiscreteOp> cachedOps = new ArrayList<>(mCache);
534                 mCache.clear();
535                 for (DiscreteOp discreteOp : cachedOps) {
536                     add(new DiscreteOp(discreteOp.getUid(), discreteOp.mPackageName,
537                             discreteOp.getAttributionTag(), discreteOp.getDeviceId(),
538                             discreteOp.mOpCode, discreteOp.mOpFlags,
539                             discreteOp.getAttributionFlags(), discreteOp.getUidState(),
540                             discreteOp.getChainId(), discreteOp.mAccessTime - offsetMillis,
541                             discreteOp.getDuration())
542                     );
543                 }
544             }
545         }
546 
547         /** Remove cached events for given UID and package. */
clear(int uid, String packageName)548         public void clear(int uid, String packageName) {
549             synchronized (this) {
550                 Set<DiscreteOp> snapshot = new ArraySet<>(mCache);
551                 for (DiscreteOp currentEvent : snapshot) {
552                     if (Objects.equals(packageName, currentEvent.mPackageName)
553                             && uid == currentEvent.getUid()) {
554                         mCache.remove(currentEvent);
555                     }
556                 }
557             }
558         }
559     }
560 
561     /** Immutable discrete op object. */
562     static class DiscreteOp {
563         private final int mUid;
564         private final String mPackageName;
565         private final String mAttributionTag;
566         private final String mDeviceId;
567         private final int mOpCode;
568         private final int mOpFlags;
569         private final int mAttributionFlags;
570         private final int mUidState;
571         private final long mChainId;
572         private final long mAccessTime;
573         private final long mDuration;
574         // store discretized timestamp to avoid repeated calculations.
575         private final long mDiscretizedAccessTime;
576         private final long mDiscretizedDuration;
577 
DiscreteOp(int uid, String packageName, String attributionTag, String deviceId, int opCode, int mOpFlags, int mAttributionFlags, int uidState, long chainId, long accessTime, long duration)578         DiscreteOp(int uid, String packageName, String attributionTag, String deviceId,
579                 int opCode,
580                 int mOpFlags, int mAttributionFlags, int uidState, long chainId, long accessTime,
581                 long duration) {
582             this.mUid = uid;
583             this.mPackageName = packageName.intern();
584             this.mAttributionTag = attributionTag;
585             this.mDeviceId = deviceId;
586             this.mOpCode = opCode;
587             this.mOpFlags = mOpFlags;
588             this.mAttributionFlags = mAttributionFlags;
589             this.mUidState = uidState;
590             this.mChainId = chainId;
591             this.mAccessTime = accessTime;
592             this.mDiscretizedAccessTime = discretizeTimeStamp(accessTime);
593             this.mDuration = duration;
594             this.mDiscretizedDuration = discretizeDuration(duration);
595         }
596 
597         @Override
equals(Object o)598         public boolean equals(Object o) {
599             if (this == o) return true;
600             if (!(o instanceof DiscreteOp that)) return false;
601 
602             if (mUid != that.mUid) return false;
603             if (mOpCode != that.mOpCode) return false;
604             if (mOpFlags != that.mOpFlags) return false;
605             if (mAttributionFlags != that.mAttributionFlags) return false;
606             if (mUidState != that.mUidState) return false;
607             if (mChainId != that.mChainId) return false;
608             if (!Objects.equals(mPackageName, that.mPackageName)) {
609                 return false;
610             }
611             if (!Objects.equals(mAttributionTag, that.mAttributionTag)) {
612                 return false;
613             }
614             if (!Objects.equals(mDeviceId, that.mDeviceId)) {
615                 return false;
616             }
617             if (mDiscretizedAccessTime != that.mDiscretizedAccessTime) {
618                 return false;
619             }
620             return mDiscretizedDuration == that.mDiscretizedDuration;
621         }
622 
623         @Override
hashCode()624         public int hashCode() {
625             int result = mUid;
626             result = 31 * result + (mPackageName != null ? mPackageName.hashCode() : 0);
627             result = 31 * result + (mAttributionTag != null ? mAttributionTag.hashCode() : 0);
628             result = 31 * result + (mDeviceId != null ? mDeviceId.hashCode() : 0);
629             result = 31 * result + mOpCode;
630             result = 31 * result + mOpFlags;
631             result = 31 * result + mAttributionFlags;
632             result = 31 * result + mUidState;
633             result = 31 * result + Objects.hash(mChainId);
634             result = 31 * result + Objects.hash(mDiscretizedAccessTime);
635             result = 31 * result + Objects.hash(mDiscretizedDuration);
636             return result;
637         }
638 
equalsExceptDuration(DiscreteOp that)639         public boolean equalsExceptDuration(DiscreteOp that) {
640             if (mUid != that.mUid) return false;
641             if (mOpCode != that.mOpCode) return false;
642             if (mOpFlags != that.mOpFlags) return false;
643             if (mAttributionFlags != that.mAttributionFlags) return false;
644             if (mUidState != that.mUidState) return false;
645             if (mChainId != that.mChainId) return false;
646             if (!Objects.equals(mPackageName, that.mPackageName)) {
647                 return false;
648             }
649             if (!Objects.equals(mAttributionTag, that.mAttributionTag)) {
650                 return false;
651             }
652             if (!Objects.equals(mDeviceId, that.mDeviceId)) {
653                 return false;
654             }
655             return mAccessTime == that.mAccessTime;
656         }
657 
658         @Override
toString()659         public String toString() {
660             return "DiscreteOp{"
661                     + "uid=" + mUid
662                     + ", packageName='" + mPackageName + '\''
663                     + ", attributionTag='" + mAttributionTag + '\''
664                     + ", deviceId='" + mDeviceId + '\''
665                     + ", opCode=" + AppOpsManager.opToName(mOpCode)
666                     + ", opFlag=" + flagsToString(mOpFlags)
667                     + ", attributionFlag=" + mAttributionFlags
668                     + ", uidState=" + getUidStateName(mUidState)
669                     + ", chainId=" + mChainId
670                     + ", accessTime=" + mAccessTime
671                     + ", mDiscretizedAccessTime=" + mDiscretizedAccessTime
672                     + ", duration=" + mDuration
673                     + ", mDiscretizedDuration=" + mDiscretizedDuration
674                     + '}';
675         }
676 
getUid()677         public int getUid() {
678             return mUid;
679         }
680 
getPackageName()681         public String getPackageName() {
682             return mPackageName;
683         }
684 
getAttributionTag()685         public String getAttributionTag() {
686             return mAttributionTag;
687         }
688 
getDeviceId()689         public String getDeviceId() {
690             return mDeviceId;
691         }
692 
getOpCode()693         public int getOpCode() {
694             return mOpCode;
695         }
696 
697         @AppOpsManager.OpFlags
getOpFlags()698         public int getOpFlags() {
699             return mOpFlags;
700         }
701 
702 
703         @AppOpsManager.AttributionFlags
getAttributionFlags()704         public int getAttributionFlags() {
705             return mAttributionFlags;
706         }
707 
708         @AppOpsManager.UidState
getUidState()709         public int getUidState() {
710             return mUidState;
711         }
712 
getChainId()713         public long getChainId() {
714             return mChainId;
715         }
716 
getAccessTime()717         public long getAccessTime() {
718             return mAccessTime;
719         }
720 
getDuration()721         public long getDuration() {
722             return mDuration;
723         }
724     }
725 
726     // API for tests only, can be removed or changed.
recordDiscreteAccess(DiscreteOp discreteOpEvent)727     void recordDiscreteAccess(DiscreteOp discreteOpEvent) {
728         mDiscreteOpCache.add(discreteOpEvent);
729     }
730 
731     // API for tests only, can be removed or changed.
getCachedDiscreteOps()732     List<DiscreteOp> getCachedDiscreteOps() {
733         return new ArrayList<>(mDiscreteOpCache.mCache);
734     }
735 
736     // API for tests only, can be removed or changed.
getAllDiscreteOps()737     List<DiscreteOp> getAllDiscreteOps() {
738         List<DiscreteOp> ops = new ArrayList<>(mDiscreteOpCache.mCache);
739         ops.addAll(mDiscreteOpsDbHelper.getAllDiscreteOps(DiscreteOpsTable.SELECT_TABLE_DATA));
740         return ops;
741     }
742 
743     // API for testing and migration
getLargestAttributionChainId()744     long getLargestAttributionChainId() {
745         return mDiscreteOpsDbHelper.getLargestAttributionChainId();
746     }
747 
748     // API for testing and migration
deleteDatabase()749     void deleteDatabase() {
750         mDiscreteOpsDbHelper.close();
751         mContext.deleteDatabase(mDatabaseFile.getName());
752     }
753 }
754