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