• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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