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