• 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.safetycenter.testing
18 
19 import android.Manifest.permission.READ_DEVICE_CONFIG
20 import android.Manifest.permission.WRITE_ALLOWLISTED_DEVICE_CONFIG
21 import android.Manifest.permission.WRITE_DEVICE_CONFIG
22 import android.annotation.TargetApi
23 import android.app.job.JobInfo
24 import android.content.Context
25 import android.content.pm.PackageManager
26 import android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE
27 import android.provider.DeviceConfig
28 import android.provider.DeviceConfig.NAMESPACE_PRIVACY
29 import android.provider.DeviceConfig.Properties
30 import android.safetycenter.SafetyCenterManager.REFRESH_REASON_DEVICE_LOCALE_CHANGE
31 import android.safetycenter.SafetyCenterManager.REFRESH_REASON_DEVICE_REBOOT
32 import android.safetycenter.SafetyCenterManager.REFRESH_REASON_OTHER
33 import android.safetycenter.SafetyCenterManager.REFRESH_REASON_PAGE_OPEN
34 import android.safetycenter.SafetyCenterManager.REFRESH_REASON_PERIODIC
35 import android.safetycenter.SafetyCenterManager.REFRESH_REASON_RESCAN_BUTTON_CLICK
36 import android.safetycenter.SafetyCenterManager.REFRESH_REASON_SAFETY_CENTER_ENABLED
37 import android.safetycenter.SafetySourceData
38 import android.util.Log
39 import com.android.modules.utils.build.SdkLevel
40 import com.android.safetycenter.testing.Coroutines.TEST_TIMEOUT
41 import com.android.safetycenter.testing.Coroutines.TIMEOUT_LONG
42 import com.android.safetycenter.testing.ShellPermissions.callWithShellPermissionIdentity
43 import com.android.settingslib.widget.SettingsThemeHelper
44 import java.time.Duration
45 import kotlin.reflect.KProperty
46 
47 /** A class that facilitates working with Safety Center flags. */
48 object SafetyCenterFlags {
49 
50     /** This is a hidden API constant within [DeviceConfig]. */
51     private const val NAMESPACE_SETTINGS_UI = "settings_ui"
52 
53     /** Flag that determines whether Safety Center is enabled. */
54     private val isEnabledFlag =
55         Flag("safety_center_is_enabled", defaultValue = SdkLevel.isAtLeastU(), BooleanParser())
56 
57     /** Flag that determines whether Safety Center can send notifications. */
58     private val notificationsFlag =
59         Flag("safety_center_notifications_enabled", defaultValue = false, BooleanParser())
60 
61     /**
62      * Flag that determines the minimum delay before Safety Center can send a notification for an
63      * issue with [SafetySourceIssue.NOTIFICATION_BEHAVIOR_DELAYED].
64      *
65      * The actual delay used may be longer.
66      */
67     private val notificationsMinDelayFlag =
68         Flag(
69             "safety_center_notifications_min_delay",
70             defaultValue = Duration.ofHours(2),
71             DurationParser(),
72         )
73 
74     /**
75      * Flag containing a comma delimited list of IDs of sources that Safety Center can send
76      * notifications about, in addition to those permitted by the current XML config.
77      */
78     private val notificationsAllowedSourcesFlag =
79         Flag(
80             "safety_center_notifications_allowed_sources",
81             defaultValue = emptySet(),
82             SetParser(StringParser()),
83         )
84 
85     /**
86      * Flag containing a comma-delimited list of the issue type IDs for which, if otherwise
87      * undefined, Safety Center should use [SafetySourceIssue.NOTIFICATION_BEHAVIOR_IMMEDIATELY].
88      */
89     private val immediateNotificationBehaviorIssuesFlag =
90         Flag(
91             "safety_center_notifications_immediate_behavior_issues",
92             defaultValue = emptySet(),
93             SetParser(StringParser()),
94         )
95 
96     /**
97      * Flag for the minimum interval which must elapse before Safety Center can resurface a
98      * notification after it was dismissed. A negative [Duration] (the default) means that dismissed
99      * notifications cannot resurface.
100      *
101      * There may be other conditions for resurfacing a notification and the actual delay may be
102      * longer than this.
103      */
104     private val notificationResurfaceIntervalFlag =
105         Flag(
106             "safety_center_notification_resurface_interval",
107             defaultValue = Duration.ofDays(-1),
108             DurationParser(),
109         )
110 
111     /** Flag that determines whether we should replace the IconAction of the lock screen source. */
112     private val replaceLockScreenIconActionFlag =
113         Flag("safety_center_replace_lock_screen_icon_action", defaultValue = true, BooleanParser())
114 
115     /**
116      * Flag that determines the time for which a Safety Center refresh is allowed to wait for a
117      * source to respond to a refresh request before timing out and marking the refresh as finished,
118      * depending on the refresh reason.
119      *
120      * Unlike the production code, this flag is set to [TEST_TIMEOUT] for all refresh reasons by
121      * default for convenience. UI tests typically will set some data manually rather than going
122      * through a full refresh, and we don't want to timeout the refresh and potentially end up with
123      * error entries in this case (as it could lead to flakyness).
124      */
125     private val refreshSourceTimeoutsFlag =
126         Flag(
127             "safety_center_refresh_sources_timeouts_millis",
128             defaultValue = getAllRefreshTimeoutsMap(TEST_TIMEOUT),
129             MapParser(IntParser(), DurationParser()),
130         )
131 
132     /**
133      * Flag that determines the time for which Safety Center will wait for a source to respond to a
134      * resolving action before timing out.
135      */
136     private val resolveActionTimeoutFlag =
137         Flag(
138             "safety_center_resolve_action_timeout_millis",
139             defaultValue = TIMEOUT_LONG,
140             DurationParser(),
141         )
142 
143     /** Flag that determines a duration after which a temporarily hidden issue will resurface. */
144     private val tempHiddenIssueResurfaceDelayFlag =
145         Flag(
146             "safety_center_temp_hidden_issue_resurface_delay_millis",
147             defaultValue = Duration.ofDays(2),
148             DurationParser(),
149         )
150 
151     /**
152      * Flag that determines how long Safety Center will wait before hiding the resolved issue UI.
153      */
154     private val hideResolveUiTransitionDelayFlag =
155         Flag(
156             "safety_center_hide_resolved_ui_transition_delay_millis",
157             defaultValue = Duration.ofMillis(400),
158             DurationParser(),
159         )
160 
161     /**
162      * Flag that determines how long an expressive BannerMessagePreference will wait before hiding
163      * the resolved UI.
164      */
165     private val bannerMessagePrefHideResolvedContentTransitionDelayFlag =
166         Flag(
167             "banner_message_pref_hide_resolved_content_delay_millis",
168             defaultValue = Duration.ofMillis(400),
169             DurationParser(),
170             namespace = NAMESPACE_SETTINGS_UI,
171             // This flag is only writeable on BP2A builds built after 3 March 2025
172             writeMustSucceed = false,
173         )
174 
175     /**
176      * Flag containing a comma delimited lists of source IDs that we won't track when deciding if a
177      * broadcast is completed. We still send broadcasts to (and handle API calls from) these sources
178      * as normal.
179      */
180     private val untrackedSourcesFlag =
181         Flag(
182             "safety_center_untracked_sources",
183             defaultValue = emptySet(),
184             SetParser(StringParser()),
185         )
186 
187     /**
188      * Flag containing a map (a comma separated list of colon separated pairs) where the key is an
189      * issue [SafetySourceData.SeverityLevel] and the value is the number of times an issue of this
190      * [SafetySourceData.SeverityLevel] should be resurfaced.
191      */
192     private val resurfaceIssueMaxCountsFlag =
193         Flag(
194             "safety_center_resurface_issue_max_counts",
195             defaultValue = emptyMap(),
196             MapParser(IntParser(), LongParser()),
197         )
198 
199     /**
200      * Flag containing a map (a comma separated list of colon separated pairs) where the key is an
201      * issue [SafetySourceData.SeverityLevel] and the value is the time after which a dismissed
202      * issue of this [SafetySourceData.SeverityLevel] will resurface if it has not reached the
203      * maximum count for which a dismissed issue of this [SafetySourceData.SeverityLevel] should be
204      * resurfaced.
205      */
206     private val resurfaceIssueDelaysFlag =
207         Flag(
208             "safety_center_resurface_issue_delays_millis",
209             defaultValue = emptyMap(),
210             MapParser(IntParser(), DurationParser()),
211         )
212 
213     /**
214      * Flag containing a map (a comma separated list of colon separated pairs) where the key is an
215      * issue [SafetySourceIssue.IssueCategory] and the value is a vertical-bar-delimited list of IDs
216      * of safety sources that are allowed to send issues with this category.
217      */
218     private val issueCategoryAllowlistsFlag =
219         Flag(
220             "safety_center_issue_category_allowlists",
221             defaultValue = emptyMap(),
222             MapParser(IntParser(), SetParser(StringParser(), delimiter = "|")),
223         )
224 
225     /**
226      * Flag containing a map (a comma separated list of colon separated pairs) where the key is a
227      * Safety Source ID and the value is a vertical-bar-delimited list of Action IDs that should
228      * have their PendingIntent replaced with the source's default PendingIntent.
229      */
230     private val actionsToOverrideWithDefaultIntentFlag =
231         Flag(
232             "safety_center_actions_to_override_with_default_intent",
233             defaultValue = emptyMap(),
234             MapParser(StringParser(), SetParser(StringParser(), delimiter = "|")),
235         )
236 
237     /**
238      * Flag that represents a comma delimited list of IDs of sources that should only be refreshed
239      * when Safety Center is on screen. We will refresh these sources only on page open and when the
240      * scan button is clicked.
241      */
242     private val backgroundRefreshDeniedSourcesFlag =
243         Flag(
244             "safety_center_background_refresh_denied_sources",
245             defaultValue = emptySet(),
246             SetParser(StringParser()),
247         )
248 
249     /**
250      * Flag that determines whether statsd logging is allowed.
251      *
252      * This is useful to allow testing statsd logs in some specific tests, while keeping the other
253      * tests from polluting our statsd logs.
254      */
255     private val allowStatsdLoggingFlag =
256         Flag("safety_center_allow_statsd_logging", defaultValue = false, BooleanParser())
257 
258     /**
259      * The Package Manager flag used while toggling the QS tile component.
260      *
261      * This is to make sure that the SafetyCenter is not killed while toggling the QS tile component
262      * during the tests, which causes flakiness in them.
263      */
264     private val qsTileComponentSettingFlag =
265         Flag(
266             "safety_center_qs_tile_component_setting_flags",
267             defaultValue = PackageManager.DONT_KILL_APP,
268             IntParser(),
269         )
270 
271     /**
272      * Flag that determines whether to show subpages in the Safety Center UI instead of the
273      * expand-and-collapse list.
274      */
275     private val showSubpagesFlag =
276         Flag("safety_center_show_subpages", defaultValue = SdkLevel.isAtLeastU(), BooleanParser())
277 
278     private val overrideRefreshOnPageOpenSourcesFlag =
279         Flag(
280             "safety_center_override_refresh_on_page_open_sources",
281             defaultValue = setOf(),
282             SetParser(StringParser()),
283         )
284 
285     /**
286      * Flag that enables both one-off and periodic background refreshes in
287      * [SafetyCenterBackgroundRefreshJobService].
288      */
289     private val backgroundRefreshIsEnabledFlag =
290         Flag(
291             "safety_center_background_refresh_is_enabled",
292             // do not set defaultValue to true, do not want background refreshes running
293             // during other tests
294             defaultValue = false,
295             BooleanParser(),
296         )
297 
298     /**
299      * Flag that determines how often periodic background refreshes are run in
300      * [SafetyCenterBackgroundRefreshJobService]. See [JobInfo.setPeriodic] for details.
301      *
302      * Note that jobs may take longer than this to be scheduled, or may possibly never run,
303      * depending on whether the other constraints on the job get satisfied.
304      */
305     private val periodicBackgroundRefreshIntervalFlag =
306         Flag(
307             "safety_center_periodic_background_interval_millis",
308             defaultValue = Duration.ofDays(1),
309             DurationParser(),
310         )
311 
312     /** Flag for allowlisting additional certificates for a given package. */
313     private val allowedAdditionalPackageCertsFlag =
314         Flag(
315             "safety_center_additional_allow_package_certs",
316             defaultValue = emptyMap(),
317             MapParser(StringParser(), SetParser(StringParser(), delimiter = "|")),
318         )
319 
320     /** Every Safety Center flag. */
321     private val FLAGS: List<Flag<*>> =
322         listOf(
323             isEnabledFlag,
324             notificationsFlag,
325             notificationsAllowedSourcesFlag,
326             notificationsMinDelayFlag,
327             immediateNotificationBehaviorIssuesFlag,
328             notificationResurfaceIntervalFlag,
329             replaceLockScreenIconActionFlag,
330             refreshSourceTimeoutsFlag,
331             resolveActionTimeoutFlag,
332             tempHiddenIssueResurfaceDelayFlag,
333             hideResolveUiTransitionDelayFlag,
334             bannerMessagePrefHideResolvedContentTransitionDelayFlag,
335             untrackedSourcesFlag,
336             resurfaceIssueMaxCountsFlag,
337             resurfaceIssueDelaysFlag,
338             issueCategoryAllowlistsFlag,
339             actionsToOverrideWithDefaultIntentFlag,
340             allowedAdditionalPackageCertsFlag,
341             backgroundRefreshDeniedSourcesFlag,
342             allowStatsdLoggingFlag,
343             qsTileComponentSettingFlag,
344             showSubpagesFlag,
345             overrideRefreshOnPageOpenSourcesFlag,
346             backgroundRefreshIsEnabledFlag,
347             periodicBackgroundRefreshIntervalFlag,
348         )
349 
350     /** All the Safety Center flags that should be written to during setup and reset. */
351     private val SETUP_FLAGS =
352         FLAGS.filter { it.name != isEnabledFlag.name }
353             .filter {
354                 // This flag is only writeable on BP2A builds built after 3 March 2025
355                 // Don't set it up on versions we know will trigger a write error.
356                 it.name != bannerMessagePrefHideResolvedContentTransitionDelayFlag.name ||
357                     SdkLevel.isAtLeastB()
358             }
359 
360     /** A property that allows getting and setting the [isEnabledFlag]. */
361     var isEnabled: Boolean by isEnabledFlag
362 
363     /** A property that allows getting and setting the [notificationsFlag]. */
364     var notificationsEnabled: Boolean by notificationsFlag
365 
366     /** A property that allows getting and setting the [notificationsAllowedSourcesFlag]. */
367     var notificationsAllowedSources: Set<String> by notificationsAllowedSourcesFlag
368 
369     /** A property that allows getting and setting the [notificationsMinDelayFlag]. */
370     var notificationsMinDelay: Duration by notificationsMinDelayFlag
371 
372     /** A property that allows getting and setting the [immediateNotificationBehaviorIssuesFlag]. */
373     var immediateNotificationBehaviorIssues: Set<String> by immediateNotificationBehaviorIssuesFlag
374 
375     /** A property that allows getting and setting the [notificationResurfaceIntervalFlag]. */
376     var notificationResurfaceInterval: Duration by notificationResurfaceIntervalFlag
377 
378     /** A property that allows getting and setting the [replaceLockScreenIconActionFlag]. */
379     var replaceLockScreenIconAction: Boolean by replaceLockScreenIconActionFlag
380 
381     /** A property that allows getting and setting the [refreshSourceTimeoutsFlag]. */
382     private var refreshTimeouts: Map<Int, Duration> by refreshSourceTimeoutsFlag
383 
384     /** A property that allows getting and setting the [resolveActionTimeoutFlag]. */
385     var resolveActionTimeout: Duration by resolveActionTimeoutFlag
386 
387     /** A property that allows getting and setting the [tempHiddenIssueResurfaceDelayFlag]. */
388     var tempHiddenIssueResurfaceDelay: Duration by tempHiddenIssueResurfaceDelayFlag
389 
390     // TODO: b/379849464 - replace remaining usages and make this private
391     /** A property that allows getting and setting the [hideResolveUiTransitionDelayFlag]. */
392     var hideResolvedIssueUiTransitionDelay: Duration by hideResolveUiTransitionDelayFlag
393 
394     /**
395      * A property that allows getting and setting the
396      * [bannerMessagePrefHideResolvedContentTransitionDelayFlag]
397      */
398     private var bannerMessagePrefHideResolvedContentTransitionDelay: Duration by
399         bannerMessagePrefHideResolvedContentTransitionDelayFlag
400 
401     /**
402      * Sets the proper hide_resolved_issue_ui_transition_delay flag based on expressive design
403      * state.
404      */
405     fun setHideResolvedIssueUiTransitionDelay(context: Context, value: Duration) =
406         if (SettingsThemeHelper.isExpressiveTheme(context)) {
407             bannerMessagePrefHideResolvedContentTransitionDelay = value
408         } else {
409             hideResolvedIssueUiTransitionDelay = value
410         }
411 
412     /** A property that allows getting and setting the [untrackedSourcesFlag]. */
413     var untrackedSources: Set<String> by untrackedSourcesFlag
414 
415     /** A property that allows getting and setting the [resurfaceIssueMaxCountsFlag]. */
416     var resurfaceIssueMaxCounts: Map<Int, Long> by resurfaceIssueMaxCountsFlag
417 
418     /** A property that allows getting and setting the [resurfaceIssueDelaysFlag]. */
419     var resurfaceIssueDelays: Map<Int, Duration> by resurfaceIssueDelaysFlag
420 
421     /** A property that allows getting and setting the [issueCategoryAllowlistsFlag]. */
422     var issueCategoryAllowlists: Map<Int, Set<String>> by issueCategoryAllowlistsFlag
423 
424     /** A property that allows getting and setting the [actionsToOverrideWithDefaultIntentFlag]. */
425     var actionsToOverrideWithDefaultIntent: Map<String, Set<String>> by
426         actionsToOverrideWithDefaultIntentFlag
427 
428     var allowedAdditionalPackageCerts: Map<String, Set<String>> by allowedAdditionalPackageCertsFlag
429 
430     /** A property that allows getting and setting the [backgroundRefreshDeniedSourcesFlag]. */
431     var backgroundRefreshDeniedSources: Set<String> by backgroundRefreshDeniedSourcesFlag
432 
433     /** A property that allows getting and setting the [allowStatsdLoggingFlag]. */
434     var allowStatsdLogging: Boolean by allowStatsdLoggingFlag
435 
436     /** A property that allows getting and setting the [showSubpagesFlag]. */
437     var showSubpages: Boolean by showSubpagesFlag
438 
439     /** A property that allows getting and setting the [overrideRefreshOnPageOpenSourcesFlag]. */
440     var overrideRefreshOnPageOpenSources: Set<String> by overrideRefreshOnPageOpenSourcesFlag
441 
442     /**
443      * Returns a snapshot of all the Safety Center flags.
444      *
445      * This snapshot is only taken once and cached afterwards. [setup] must be called at least once
446      * prior to modifying any flag for the snapshot to be taken with the right values.
447      */
448     @Volatile lateinit var snapshot: Map<String, Properties>
449 
450     private val lazySnapshot: Map<String, Properties> by lazy {
451         callWithShellPermissionIdentity(READ_DEVICE_CONFIG) {
452             mapOf(
453                 NAMESPACE_PRIVACY to fetchPropertiesForNamespace(NAMESPACE_PRIVACY),
454                 NAMESPACE_SETTINGS_UI to fetchPropertiesForNamespace(NAMESPACE_SETTINGS_UI),
455             )
456         }
457     }
458 
459     private fun fetchPropertiesForNamespace(namespace: String) =
460         DeviceConfig.getProperties(
461             namespace,
462             *FLAGS.filter { it.namespace == namespace }.map { it.name }.toTypedArray(),
463         )
464 
465     /**
466      * Takes a snapshot of all Safety Center flags and sets them up to their default values.
467      *
468      * This doesn't apply to [isEnabled] as it is handled separately by [SafetyCenterTestHelper]:
469      * there is a listener that listens to changes to this flag in system server, and we need to
470      * ensure we wait for it to complete when modifying this flag.
471      */
472     fun setup() {
473         snapshot = lazySnapshot
474         SETUP_FLAGS.forEach { it.writeToDeviceConfig(it.defaultStringValue) }
475     }
476 
477     /**
478      * Resets the Safety Center flags based on the existing [snapshot] captured during [setup].
479      *
480      * This doesn't apply to [isEnabled] as it is handled separately by [SafetyCenterTestHelper]:
481      * there is a listener that listens to changes to this flag in system server, and we need to
482      * ensure we wait for it to complete when modifying this flag.
483      */
484     fun reset() {
485         // Write flags one by one instead of using `DeviceConfig#setProperties` as the latter does
486         // not work when DeviceConfig sync is disabled and does not take uninitialized values into
487         // account.
488         SETUP_FLAGS.forEach {
489             val key = it.name
490             val value = snapshot[it.namespace]?.getString(key, /* defaultValue */ null)
491             it.writeToDeviceConfig(value)
492         }
493     }
494 
495     /** Sets the [refreshTimeouts] for all refresh reasons to the given [refreshTimeout]. */
496     fun setAllRefreshTimeoutsTo(refreshTimeout: Duration) {
497         refreshTimeouts = getAllRefreshTimeoutsMap(refreshTimeout)
498     }
499 
500     /** Returns the [isEnabledFlag] value of the Safety Center flags snapshot. */
501     fun Map<String, Properties>.isSafetyCenterEnabled(): Boolean =
502         this[NAMESPACE_PRIVACY]!!.getBoolean(isEnabledFlag.name, isEnabledFlag.defaultValue)
503 
504     @TargetApi(UPSIDE_DOWN_CAKE)
505     private fun getAllRefreshTimeoutsMap(refreshTimeout: Duration): Map<Int, Duration> =
506         mapOf(
507             REFRESH_REASON_PAGE_OPEN to refreshTimeout,
508             REFRESH_REASON_RESCAN_BUTTON_CLICK to refreshTimeout,
509             REFRESH_REASON_DEVICE_REBOOT to refreshTimeout,
510             REFRESH_REASON_DEVICE_LOCALE_CHANGE to refreshTimeout,
511             REFRESH_REASON_SAFETY_CENTER_ENABLED to refreshTimeout,
512             REFRESH_REASON_OTHER to refreshTimeout,
513             REFRESH_REASON_PERIODIC to refreshTimeout,
514         )
515 
516     private interface Parser<T> {
517         fun parseFromString(stringValue: String): T
518 
519         fun toString(value: T): String = value.toString()
520     }
521 
522     private class StringParser : Parser<String> {
523         override fun parseFromString(stringValue: String) = stringValue
524     }
525 
526     private class BooleanParser : Parser<Boolean> {
527         override fun parseFromString(stringValue: String) = stringValue.toBoolean()
528     }
529 
530     private class IntParser : Parser<Int> {
531         override fun parseFromString(stringValue: String) = stringValue.toInt()
532     }
533 
534     private class LongParser : Parser<Long> {
535         override fun parseFromString(stringValue: String) = stringValue.toLong()
536     }
537 
538     private class DurationParser : Parser<Duration> {
539         override fun parseFromString(stringValue: String) = Duration.ofMillis(stringValue.toLong())
540 
541         override fun toString(value: Duration) = value.toMillis().toString()
542     }
543 
544     private class SetParser<T>(
545         private val elementParser: Parser<T>,
546         private val delimiter: String = ",",
547     ) : Parser<Set<T>> {
548         override fun parseFromString(stringValue: String) =
549             stringValue.split(delimiter).map(elementParser::parseFromString).toSet()
550 
551         override fun toString(value: Set<T>) =
552             value.joinToString(delimiter, transform = elementParser::toString)
553     }
554 
555     private class MapParser<K, V>(
556         private val keyParser: Parser<K>,
557         private val valueParser: Parser<V>,
558         private val entriesDelimiter: String = ",",
559         private val pairDelimiter: String = ":",
560     ) : Parser<Map<K, V>> {
561         override fun parseFromString(stringValue: String) =
562             stringValue.split(entriesDelimiter).associate { pair ->
563                 val (keyString, valueString) = pair.split(pairDelimiter)
564                 keyParser.parseFromString(keyString) to valueParser.parseFromString(valueString)
565             }
566 
567         override fun toString(value: Map<K, V>) =
568             value
569                 .map {
570                     "${keyParser.toString(it.key)}${pairDelimiter}${valueParser.toString(it.value)}"
571                 }
572                 .joinToString(entriesDelimiter)
573     }
574 
575     private class Flag<T>(
576         val name: String,
577         val defaultValue: T,
578         private val parser: Parser<T>,
579         val namespace: String = NAMESPACE_PRIVACY,
580         val writeMustSucceed: Boolean = true,
581     ) {
582         val defaultStringValue = parser.toString(defaultValue)
583 
584         operator fun getValue(thisRef: Any?, property: KProperty<*>): T =
585             readFromDeviceConfig(name)?.let(parser::parseFromString) ?: defaultValue
586 
587         private fun readFromDeviceConfig(name: String): String? =
588             callWithShellPermissionIdentity(READ_DEVICE_CONFIG) {
589                 DeviceConfig.getProperty(namespace, name)
590             }
591 
592         operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
593             writeToDeviceConfig(parser.toString(value))
594         }
595 
596         fun writeToDeviceConfig(stringValue: String?) {
597             callWithShellPermissionIdentity(WRITE_DEVICE_CONFIG, WRITE_ALLOWLISTED_DEVICE_CONFIG) {
598                 val valueWasSet =
599                     try {
600                         DeviceConfig.setProperty(
601                             namespace,
602                             name,
603                             stringValue,
604                             /* makeDefault */ false,
605                         )
606                     } catch (e: Exception) {
607                         Log.w(TAG, "Error while setting $name to: $stringValue", e)
608                         false
609                     }
610 
611                 if (writeMustSucceed) {
612                     require(valueWasSet) { "Could not set $name to: $stringValue" }
613                 }
614             }
615         }
616     }
617 
618     private const val TAG = "SafetyCenterFlags"
619 }
620