1 /* <lambda>null2 * Copyright (C) 2022 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.permissioncontroller.safetylabel 18 19 import android.content.Context 20 import android.os.Build 21 import android.provider.DeviceConfig 22 import android.util.AtomicFile 23 import android.util.Log 24 import android.util.Xml 25 import androidx.annotation.RequiresApi 26 import com.android.permissioncontroller.safetylabel.AppsSafetyLabelHistory.AppInfo 27 import com.android.permissioncontroller.safetylabel.AppsSafetyLabelHistory.AppSafetyLabelDiff 28 import com.android.permissioncontroller.safetylabel.AppsSafetyLabelHistory.AppSafetyLabelHistory 29 import com.android.permissioncontroller.safetylabel.AppsSafetyLabelHistory.DataCategory 30 import com.android.permissioncontroller.safetylabel.AppsSafetyLabelHistory.DataLabel 31 import com.android.permissioncontroller.safetylabel.AppsSafetyLabelHistory.SafetyLabel 32 import java.io.File 33 import java.io.FileNotFoundException 34 import java.io.FileOutputStream 35 import java.io.IOException 36 import java.nio.charset.StandardCharsets 37 import java.time.Instant 38 import org.xmlpull.v1.XmlPullParser 39 import org.xmlpull.v1.XmlPullParserException 40 import org.xmlpull.v1.XmlSerializer 41 42 /** Persists safety label history to disk and allows reading from and writing to this storage */ 43 @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) 44 object AppsSafetyLabelHistoryPersistence { 45 private const val TAG_DATA_SHARED_MAP = "shared" 46 private const val TAG_DATA_SHARED_ENTRY = "entry" 47 private const val TAG_APP_INFO = "app-info" 48 private const val TAG_DATA_LABEL = "data-lbl" 49 private const val TAG_SAFETY_LABEL = "sfty-lbl" 50 private const val TAG_APP_SAFETY_LABEL_HISTORY = "app-hstry" 51 private const val TAG_APPS_SAFETY_LABEL_HISTORY = "apps-hstry" 52 private const val ATTRIBUTE_VERSION = "vrs" 53 private const val ATTRIBUTE_PACKAGE_NAME = "pkg-name" 54 private const val ATTRIBUTE_RECEIVED_AT = "rcvd" 55 private const val ATTRIBUTE_CATEGORY = "cat" 56 private const val ATTRIBUTE_CONTAINS_ADS = "ads" 57 private const val CURRENT_VERSION = 0 58 private const val INITIAL_VERSION = 0 59 60 /** The name of the file used to persist Safety Label history. */ 61 private const val APPS_SAFETY_LABEL_HISTORY_PERSISTENCE_FILE_NAME = 62 "apps_safety_label_history_persistence.xml" 63 private val LOG_TAG = "AppsSafetyLabelHistoryPersistence".take(23) 64 private val readWriteLock = Any() 65 66 private var listeners = mutableSetOf<ChangeListener>() 67 68 /** Adds a listener to listen for changes to persisted safety labels. */ 69 fun addListener(listener: ChangeListener) { 70 synchronized(readWriteLock) { listeners.add(listener) } 71 } 72 73 /** Removes a listener from listening for changes to persisted safety labels. */ 74 fun removeListener(listener: ChangeListener) { 75 synchronized(readWriteLock) { listeners.remove(listener) } 76 } 77 78 /** 79 * Reads the provided file storing safety label history and returns the parsed 80 * [AppsSafetyLabelHistoryFileContent]. 81 */ 82 fun read(file: File): AppsSafetyLabelHistoryFileContent { 83 val parser = Xml.newPullParser() 84 try { 85 AtomicFile(file).openRead().let { inputStream -> 86 parser.setInput(inputStream, StandardCharsets.UTF_8.name()) 87 return parser.parseHistoryFile() 88 } 89 } catch (e: FileNotFoundException) { 90 Log.e(LOG_TAG, "File not found: $file") 91 } catch (e: IOException) { 92 Log.e( 93 LOG_TAG, 94 "Failed to read file: $file, encountered exception ${e.localizedMessage}" 95 ) 96 } catch (e: XmlPullParserException) { 97 Log.e( 98 LOG_TAG, 99 "Failed to parse file: $file, encountered exception ${e.localizedMessage}" 100 ) 101 } 102 103 return AppsSafetyLabelHistoryFileContent(appsSafetyLabelHistory = null, INITIAL_VERSION) 104 } 105 106 /** Returns the last updated time for each stored [AppSafetyLabelHistory]. */ 107 fun getSafetyLabelsLastUpdatedTimes(file: File): Map<AppInfo, Instant> { 108 synchronized(readWriteLock) { 109 val appHistories = 110 read(file).appsSafetyLabelHistory?.appSafetyLabelHistories ?: return emptyMap() 111 112 val lastUpdatedTimes = mutableMapOf<AppInfo, Instant>() 113 for (appHistory in appHistories) { 114 val lastSafetyLabelReceiptTime: Instant? = appHistory.getLastReceiptTime() 115 if (lastSafetyLabelReceiptTime != null) { 116 lastUpdatedTimes[appHistory.appInfo] = lastSafetyLabelReceiptTime 117 } 118 } 119 120 return lastUpdatedTimes 121 } 122 } 123 124 /** 125 * Writes a new safety label to the provided file, if the provided safety label has changed from 126 * the last recorded. 127 */ 128 fun recordSafetyLabel(safetyLabel: SafetyLabel, file: File) { 129 synchronized(readWriteLock) { 130 val currentAppsSafetyLabelHistory = 131 read(file).appsSafetyLabelHistory ?: AppsSafetyLabelHistory(listOf()) 132 val appInfo = safetyLabel.appInfo 133 val currentHistories = currentAppsSafetyLabelHistory.appSafetyLabelHistories 134 135 val updatedAppsSafetyLabelHistory: AppsSafetyLabelHistory = 136 if (currentHistories.all { it.appInfo != appInfo }) { 137 AppsSafetyLabelHistory( 138 currentHistories.toMutableList().apply { 139 add(AppSafetyLabelHistory(appInfo, listOf(safetyLabel))) 140 } 141 ) 142 } else { 143 AppsSafetyLabelHistory( 144 currentHistories.map { 145 if (it.appInfo != appInfo) it 146 else it.addSafetyLabelIfChanged(safetyLabel) 147 } 148 ) 149 } 150 151 write(file, updatedAppsSafetyLabelHistory) 152 } 153 } 154 155 /** 156 * Writes new safety labels to the provided file, if the provided safety labels have changed 157 * from the last recorded (when considered in order of [SafetyLabel.receivedAt]). 158 */ 159 fun recordSafetyLabels(safetyLabelsToAdd: Set<SafetyLabel>, file: File) { 160 if (safetyLabelsToAdd.isEmpty()) return 161 162 synchronized(readWriteLock) { 163 val currentAppsSafetyLabelHistory = 164 read(file).appsSafetyLabelHistory ?: AppsSafetyLabelHistory(listOf()) 165 val appInfoToOrderedSafetyLabels = 166 safetyLabelsToAdd 167 .groupBy { it.appInfo } 168 .mapValues { (_, safetyLabels) -> safetyLabels.sortedBy { it.receivedAt } } 169 val currentAppHistories = currentAppsSafetyLabelHistory.appSafetyLabelHistories 170 val newApps = 171 appInfoToOrderedSafetyLabels.keys - currentAppHistories.map { it.appInfo }.toSet() 172 173 // For apps that already have some safety labels persisted, add the provided safety 174 // labels to the history. 175 var updatedAppHistories: List<AppSafetyLabelHistory> = 176 currentAppHistories.map { currentAppHistory: AppSafetyLabelHistory -> 177 val safetyLabels = appInfoToOrderedSafetyLabels[currentAppHistory.appInfo] 178 if (safetyLabels != null) { 179 currentAppHistory.addSafetyLabelsIfChanged(safetyLabels) 180 } else { 181 currentAppHistory 182 } 183 } 184 185 // For apps that don't already have some safety labels persisted, add new 186 // AppSafetyLabelHistory instances for them with the provided safety labels. 187 updatedAppHistories = 188 updatedAppHistories.toMutableList().apply { 189 newApps.forEach { newAppInfo -> 190 val safetyLabelsForNewApp = appInfoToOrderedSafetyLabels[newAppInfo] 191 if (safetyLabelsForNewApp != null) { 192 add(AppSafetyLabelHistory(newAppInfo, safetyLabelsForNewApp)) 193 } 194 } 195 } 196 197 write(file, AppsSafetyLabelHistory(updatedAppHistories)) 198 } 199 } 200 201 /** Deletes stored safety labels for all apps in [appInfosToRemove]. */ 202 fun deleteSafetyLabelsForApps(appInfosToRemove: Set<AppInfo>, file: File) { 203 if (appInfosToRemove.isEmpty()) return 204 205 synchronized(readWriteLock) { 206 val currentAppsSafetyLabelHistory = 207 read(file).appsSafetyLabelHistory ?: AppsSafetyLabelHistory(listOf()) 208 val historiesWithAppsRemoved = 209 currentAppsSafetyLabelHistory.appSafetyLabelHistories.filter { 210 it.appInfo !in appInfosToRemove 211 } 212 213 write(file, AppsSafetyLabelHistory(historiesWithAppsRemoved)) 214 } 215 } 216 217 /** 218 * Deletes stored safety labels so that there is at most one safety label for each app with 219 * [SafetyLabel.receivedAt] earlier that [startTime]. 220 */ 221 fun deleteSafetyLabelsOlderThan(startTime: Instant, file: File) { 222 synchronized(readWriteLock) { 223 val currentAppsSafetyLabelHistory = 224 read(file).appsSafetyLabelHistory ?: AppsSafetyLabelHistory(listOf()) 225 val updatedAppHistories = 226 currentAppsSafetyLabelHistory.appSafetyLabelHistories.map { appHistory -> 227 val history = appHistory.safetyLabelHistory 228 // Retrieve the last safety label that was received prior to startTime. 229 val last = 230 history.indexOfLast { safetyLabels -> safetyLabels.receivedAt <= startTime } 231 if (last <= 0) { 232 // If there is only one or no safety labels received prior to startTime, 233 // then return the history as is. 234 appHistory 235 } else { 236 // Else, discard all safety labels other than the last safety label prior 237 // to startTime. The aim is retain one safety label prior to start time to 238 // be used as the "before" safety label when determining updates. 239 AppSafetyLabelHistory( 240 appHistory.appInfo, 241 history.subList(last, history.size) 242 ) 243 } 244 } 245 246 write(file, AppsSafetyLabelHistory(updatedAppHistories)) 247 } 248 } 249 250 /** 251 * Serializes and writes the provided [AppsSafetyLabelHistory] with [CURRENT_VERSION] schema to 252 * the provided file. 253 */ 254 fun write(file: File, appsSafetyLabelHistory: AppsSafetyLabelHistory) { 255 write(file, AppsSafetyLabelHistoryFileContent(appsSafetyLabelHistory, CURRENT_VERSION)) 256 } 257 258 /** 259 * Serializes and writes the provided [AppsSafetyLabelHistoryFileContent] to the provided file. 260 */ 261 fun write(file: File, fileContent: AppsSafetyLabelHistoryFileContent) { 262 val atomicFile = AtomicFile(file) 263 var outputStream: FileOutputStream? = null 264 265 try { 266 outputStream = atomicFile.startWrite() 267 val serializer = Xml.newSerializer() 268 serializer.setOutput(outputStream, StandardCharsets.UTF_8.name()) 269 serializer.startDocument(null, true) 270 serializer.serializeAllAppSafetyLabelHistory(fileContent) 271 serializer.endDocument() 272 atomicFile.finishWrite(outputStream) 273 listeners.forEach { it.onSafetyLabelHistoryChanged() } 274 } catch (e: Exception) { 275 Log.i( 276 LOG_TAG, 277 "Failed to write to $file. Previous version of file will be restored.", 278 e 279 ) 280 atomicFile.failWrite(outputStream) 281 } finally { 282 try { 283 outputStream?.close() 284 } catch (e: Exception) { 285 Log.e(LOG_TAG, "Failed to close $file.", e) 286 } 287 } 288 } 289 290 /** Reads the provided history file and returns all safety label changes since [startTime]. */ 291 fun getAppSafetyLabelDiffs(startTime: Instant, file: File): List<AppSafetyLabelDiff> { 292 val currentAppsSafetyLabelHistory = 293 read(file).appsSafetyLabelHistory ?: AppsSafetyLabelHistory(listOf()) 294 295 return currentAppsSafetyLabelHistory.appSafetyLabelHistories.mapNotNull { 296 val before = it.getSafetyLabelAt(startTime) 297 val after = it.getLatestSafetyLabel() 298 if ( 299 before == null || 300 after == null || 301 before == after || 302 before.receivedAt.isAfter(after.receivedAt) 303 ) 304 null 305 else AppSafetyLabelDiff(before, after) 306 } 307 } 308 309 /** Clears the file. */ 310 fun clear(file: File) { 311 AtomicFile(file).delete() 312 } 313 314 /** Returns the file persisting safety label history for installed apps. */ 315 fun getSafetyLabelHistoryFile(context: Context): File = 316 File(context.filesDir, APPS_SAFETY_LABEL_HISTORY_PERSISTENCE_FILE_NAME) 317 318 private fun AppSafetyLabelHistory.getLastReceiptTime(): Instant? = 319 this.safetyLabelHistory.lastOrNull()?.receivedAt 320 321 private fun XmlPullParser.parseHistoryFile(): AppsSafetyLabelHistoryFileContent { 322 if (eventType != XmlPullParser.START_DOCUMENT) { 323 throw IllegalArgumentException() 324 } 325 nextTag() 326 327 val appsSafetyLabelHistory = parseAppsSafetyLabelHistory() 328 329 while (eventType == XmlPullParser.TEXT && isWhitespace) { 330 next() 331 } 332 if (eventType != XmlPullParser.END_DOCUMENT) { 333 throw IllegalArgumentException("Unexpected extra element") 334 } 335 336 return appsSafetyLabelHistory 337 } 338 339 private fun XmlPullParser.parseAppsSafetyLabelHistory(): AppsSafetyLabelHistoryFileContent { 340 checkTagStart(TAG_APPS_SAFETY_LABEL_HISTORY) 341 var version: Int? = null 342 for (i in 0 until attributeCount) { 343 when (getAttributeName(i)) { 344 ATTRIBUTE_VERSION -> version = getAttributeValue(i).toInt() 345 else -> 346 throw IllegalArgumentException( 347 "Unexpected attribute ${getAttributeName(i)} in tag" + 348 " $TAG_APPS_SAFETY_LABEL_HISTORY" 349 ) 350 } 351 } 352 if (version == null) { 353 version = INITIAL_VERSION 354 Log.w(LOG_TAG, "Missing $ATTRIBUTE_VERSION in $TAG_APPS_SAFETY_LABEL_HISTORY") 355 } 356 nextTag() 357 358 val appSafetyLabelHistories = mutableListOf<AppSafetyLabelHistory>() 359 while (eventType == XmlPullParser.START_TAG && name == TAG_APP_SAFETY_LABEL_HISTORY) { 360 appSafetyLabelHistories.add(parseAppSafetyLabelHistory()) 361 } 362 363 checkTagEnd(TAG_APPS_SAFETY_LABEL_HISTORY) 364 next() 365 366 return AppsSafetyLabelHistoryFileContent( 367 AppsSafetyLabelHistory(appSafetyLabelHistories), 368 version 369 ) 370 } 371 372 private fun XmlPullParser.parseAppSafetyLabelHistory(): AppSafetyLabelHistory { 373 checkTagStart(TAG_APP_SAFETY_LABEL_HISTORY) 374 nextTag() 375 376 val appInfo = parseAppInfo() 377 378 val safetyLabels = mutableListOf<SafetyLabel>() 379 while (eventType == XmlPullParser.START_TAG && name == TAG_SAFETY_LABEL) { 380 safetyLabels.add(parseSafetyLabel(appInfo)) 381 } 382 383 checkTagEnd(TAG_APP_SAFETY_LABEL_HISTORY) 384 nextTag() 385 386 return AppSafetyLabelHistory(appInfo, safetyLabels) 387 } 388 389 private fun XmlPullParser.parseSafetyLabel(appInfo: AppInfo): SafetyLabel { 390 checkTagStart(TAG_SAFETY_LABEL) 391 392 var receivedAt: Instant? = null 393 for (i in 0 until attributeCount) { 394 when (getAttributeName(i)) { 395 ATTRIBUTE_RECEIVED_AT -> receivedAt = parseInstant(getAttributeValue(i)) 396 else -> 397 throw IllegalArgumentException( 398 "Unexpected attribute ${getAttributeName(i)} in tag $TAG_SAFETY_LABEL" 399 ) 400 } 401 } 402 if (receivedAt == null) { 403 throw IllegalArgumentException("Missing $ATTRIBUTE_RECEIVED_AT in $TAG_SAFETY_LABEL") 404 } 405 nextTag() 406 407 val dataLabel = parseDataLabel() 408 409 checkTagEnd(TAG_SAFETY_LABEL) 410 nextTag() 411 412 return SafetyLabel(appInfo, receivedAt, dataLabel) 413 } 414 415 private fun XmlPullParser.parseDataLabel(): DataLabel { 416 checkTagStart(TAG_DATA_LABEL) 417 nextTag() 418 419 val dataSharing = parseDataShared() 420 421 checkTagEnd(TAG_DATA_LABEL) 422 nextTag() 423 424 return DataLabel(dataSharing) 425 } 426 427 private fun XmlPullParser.parseDataShared(): Map<String, DataCategory> { 428 checkTagStart(TAG_DATA_SHARED_MAP) 429 nextTag() 430 431 val sharedCategories = mutableListOf<Pair<String, DataCategory>>() 432 while (eventType == XmlPullParser.START_TAG && name == TAG_DATA_SHARED_ENTRY) { 433 sharedCategories.add(parseDataSharedEntry()) 434 } 435 436 checkTagEnd(TAG_DATA_SHARED_MAP) 437 nextTag() 438 439 return sharedCategories.associate { it.first to it.second } 440 } 441 442 private fun XmlPullParser.parseDataSharedEntry(): Pair<String, DataCategory> { 443 checkTagStart(TAG_DATA_SHARED_ENTRY) 444 var category: String? = null 445 var hasAds: Boolean? = null 446 for (i in 0 until attributeCount) { 447 when (getAttributeName(i)) { 448 ATTRIBUTE_CATEGORY -> category = getAttributeValue(i) 449 ATTRIBUTE_CONTAINS_ADS -> hasAds = getAttributeValue(i).toBoolean() 450 else -> 451 throw IllegalArgumentException( 452 "Unexpected attribute ${getAttributeName(i)} in tag $TAG_DATA_SHARED_ENTRY" 453 ) 454 } 455 } 456 if (category == null) { 457 throw IllegalArgumentException("Missing $ATTRIBUTE_CATEGORY in $TAG_DATA_SHARED_ENTRY") 458 } 459 if (hasAds == null) { 460 throw IllegalArgumentException( 461 "Missing $ATTRIBUTE_CONTAINS_ADS in $TAG_DATA_SHARED_ENTRY" 462 ) 463 } 464 nextTag() 465 466 checkTagEnd(TAG_DATA_SHARED_ENTRY) 467 nextTag() 468 469 return category to DataCategory(hasAds) 470 } 471 472 private fun XmlPullParser.parseAppInfo(): AppInfo { 473 checkTagStart(TAG_APP_INFO) 474 var packageName: String? = null 475 for (i in 0 until attributeCount) { 476 when (getAttributeName(i)) { 477 ATTRIBUTE_PACKAGE_NAME -> packageName = getAttributeValue(i) 478 else -> 479 throw IllegalArgumentException( 480 "Unexpected attribute ${getAttributeName(i)} in tag $TAG_APP_INFO" 481 ) 482 } 483 } 484 if (packageName == null) { 485 throw IllegalArgumentException("Missing $ATTRIBUTE_PACKAGE_NAME in $TAG_APP_INFO") 486 } 487 488 nextTag() 489 checkTagEnd(TAG_APP_INFO) 490 nextTag() 491 return AppInfo(packageName) 492 } 493 494 private fun XmlPullParser.checkTagStart(tag: String) { 495 check(eventType == XmlPullParser.START_TAG && tag == name) 496 } 497 498 private fun XmlPullParser.checkTagEnd(tag: String) { 499 check(eventType == XmlPullParser.END_TAG && tag == name) 500 } 501 502 private fun parseInstant(value: String): Instant { 503 return try { 504 Instant.ofEpochMilli(value.toLong()) 505 } catch (e: Exception) { 506 throw IllegalArgumentException("Could not parse $value as Instant") 507 } 508 } 509 510 private fun XmlSerializer.serializeAllAppSafetyLabelHistory( 511 fileContent: AppsSafetyLabelHistoryFileContent 512 ) { 513 startTag(null, TAG_APPS_SAFETY_LABEL_HISTORY) 514 attribute(null, ATTRIBUTE_VERSION, fileContent.version.toString()) 515 fileContent.appsSafetyLabelHistory?.appSafetyLabelHistories?.forEach { 516 serializeAppSafetyLabelHistory(it) 517 } 518 endTag(null, TAG_APPS_SAFETY_LABEL_HISTORY) 519 } 520 521 private fun XmlSerializer.serializeAppSafetyLabelHistory( 522 appSafetyLabelHistory: AppSafetyLabelHistory 523 ) { 524 startTag(null, TAG_APP_SAFETY_LABEL_HISTORY) 525 serializeAppInfo(appSafetyLabelHistory.appInfo) 526 appSafetyLabelHistory.safetyLabelHistory.forEach { serializeSafetyLabel(it) } 527 endTag(null, TAG_APP_SAFETY_LABEL_HISTORY) 528 } 529 530 private fun XmlSerializer.serializeAppInfo(appInfo: AppInfo) { 531 startTag(null, TAG_APP_INFO) 532 attribute(null, ATTRIBUTE_PACKAGE_NAME, appInfo.packageName) 533 endTag(null, TAG_APP_INFO) 534 } 535 536 private fun XmlSerializer.serializeSafetyLabel(safetyLabel: SafetyLabel) { 537 startTag(null, TAG_SAFETY_LABEL) 538 attribute(null, ATTRIBUTE_RECEIVED_AT, safetyLabel.receivedAt.toEpochMilli().toString()) 539 serializeDataLabel(safetyLabel.dataLabel) 540 endTag(null, TAG_SAFETY_LABEL) 541 } 542 543 private fun XmlSerializer.serializeDataLabel(dataLabel: DataLabel) { 544 startTag(null, TAG_DATA_LABEL) 545 serializeDataSharedMap(dataLabel.dataShared) 546 endTag(null, TAG_DATA_LABEL) 547 } 548 549 private fun XmlSerializer.serializeDataSharedMap(dataShared: Map<String, DataCategory>) { 550 startTag(null, TAG_DATA_SHARED_MAP) 551 dataShared.entries.forEach { serializeDataSharedEntry(it) } 552 endTag(null, TAG_DATA_SHARED_MAP) 553 } 554 555 private fun XmlSerializer.serializeDataSharedEntry( 556 dataSharedEntry: Map.Entry<String, DataCategory> 557 ) { 558 startTag(null, TAG_DATA_SHARED_ENTRY) 559 attribute(null, ATTRIBUTE_CATEGORY, dataSharedEntry.key) 560 attribute( 561 null, 562 ATTRIBUTE_CONTAINS_ADS, 563 dataSharedEntry.value.containsAdvertisingPurpose.toString() 564 ) 565 endTag(null, TAG_DATA_SHARED_ENTRY) 566 } 567 568 private fun AppSafetyLabelHistory.addSafetyLabelIfChanged( 569 safetyLabel: SafetyLabel 570 ): AppSafetyLabelHistory { 571 val latestSafetyLabel = safetyLabelHistory.lastOrNull() 572 return if (latestSafetyLabel?.dataLabel == safetyLabel.dataLabel) this 573 else this.withSafetyLabel(safetyLabel, getMaxSafetyLabelsToPersist()) 574 } 575 576 private fun AppSafetyLabelHistory.addSafetyLabelsIfChanged( 577 safetyLabels: List<SafetyLabel> 578 ): AppSafetyLabelHistory { 579 var updatedAppHistory = this 580 val maxSafetyLabels = getMaxSafetyLabelsToPersist() 581 for (safetyLabel in safetyLabels) { 582 val latestSafetyLabel = updatedAppHistory.safetyLabelHistory.lastOrNull() 583 if (latestSafetyLabel?.dataLabel != safetyLabel.dataLabel) { 584 updatedAppHistory = updatedAppHistory.withSafetyLabel(safetyLabel, maxSafetyLabels) 585 } 586 } 587 588 return updatedAppHistory 589 } 590 591 private fun AppSafetyLabelHistory.getLatestSafetyLabel() = safetyLabelHistory.lastOrNull() 592 593 /** 594 * Return the safety label known to be the current safety label for the app at the provided 595 * time, if available in the history. 596 */ 597 private fun AppSafetyLabelHistory.getSafetyLabelAt(startTime: Instant) = 598 safetyLabelHistory.lastOrNull { 599 // the last received safety label before or at startTime 600 it.receivedAt.isBefore(startTime) || it.receivedAt == startTime 601 } 602 ?: // the first safety label received after startTime, as a fallback 603 safetyLabelHistory.firstOrNull { it.receivedAt.isAfter(startTime) } 604 605 private const val PROPERTY_MAX_SAFETY_LABELS_PERSISTED_PER_APP = 606 "max_safety_labels_persisted_per_app" 607 608 /** 609 * Returns the maximum number of safety labels to persist per app. 610 * 611 * Note that this will be checked at the time of adding a new safety label to storage for an 612 * app; simply changing this Device Config property will not result in any storage being purged. 613 */ 614 private fun getMaxSafetyLabelsToPersist() = 615 DeviceConfig.getInt( 616 DeviceConfig.NAMESPACE_PRIVACY, 617 PROPERTY_MAX_SAFETY_LABELS_PERSISTED_PER_APP, 618 20 619 ) 620 621 /** An interface to listen to changes to persisted safety labels. */ 622 interface ChangeListener { 623 /** Callback when the persisted safety labels are changed. */ 624 fun onSafetyLabelHistoryChanged() 625 } 626 627 /** Data class to hold an [AppsSafetyLabelHistory] along with the file schema version. */ 628 data class AppsSafetyLabelHistoryFileContent( 629 val appsSafetyLabelHistory: AppsSafetyLabelHistory?, 630 val version: Int, 631 ) 632 } 633