1 package com.android.onboarding.versions
2 
3 import android.content.Context
4 import android.content.pm.PackageManager
5 import android.content.pm.PackageManager.NameNotFoundException
6 import android.provider.Settings
7 import android.util.Log
8 import com.android.onboarding.versions.annotations.ChangeId
9 import com.android.onboarding.versions.annotations.ChangeRadius
10 import com.android.onboarding.versions.annotations.ValidChangeId
11 import com.android.onboarding.versions.changes.ALL_CHANGE_IDS
12 import java.lang.IllegalArgumentException
13 import java.lang.IllegalStateException
14 import java.lang.UnsupportedOperationException
15 
16 private const val LOG_TAG = "DefaultOnboardingChanges"
17 
18 /**
19  * Default implementation of [OnboardingChanges].
20  *
21  * For local testing, use `adb shell settings put global package.name.changeid 1` to enable and `adb
22  * shell settings put global package.name.changeid 0` to disable. For example to enable change ID
23  * 1234 on package `com.android.setup` we would use `adb shell settings put global
24  * com.android.setup.1234 1`. You can also use `adb shell settings delete global
25  * package.name.changeid` to use the default configured value.
26  */
27 class DefaultOnboardingChanges(val context: Context) : OnboardingChanges {
28 
29   private val version = Versions(context)
30 
31   /**
32    * Map of package to the compliance date for that package.
33    *
34    * If a package has failed to be loaded, the value will be null. If it has not been attempted the
35    * key will be non-present.
36    */
37   private val packageComplianceDates =
<lambda>null38     mutableMapOf<String, ComplianceDate?>().apply {
39       put(context.packageName, version.myComplianceDate)
40     }
41 
currentProcessSupportsChangenull42   override fun currentProcessSupportsChange(@ValidChangeId changeId: Long): Boolean {
43     return componentSupportsChange(context.packageName, changeId)
44   }
45 
requireCurrentProcessSupportsChangenull46   override fun requireCurrentProcessSupportsChange(@ValidChangeId changeId: Long) {
47     if (currentProcessSupportsChange(changeId)) {
48       return
49     }
50 
51     // currentProcessSupportsChange will throw if this is null
52     val changeData = ALL_CHANGE_IDS[changeId]!!
53 
54     if (changeData.available == ChangeId.NOT_AVAILABLE) {
55       throw UnsupportedOperationException(
56         "This API is not available. The change ($changeId) is set to not available. See OnboardingChanges for options for enabling this change during test and development."
57       )
58     }
59 
60     throw UnsupportedOperationException(
61       "This API is not available. The change ($changeId) is available as of ${changeData.available} but the compliance date of this package is ${version.myComplianceDate}"
62     )
63   }
64 
loadSupportedChangesnull65   override fun loadSupportedChanges(component: String) {
66     val packageName = extractPackageFromComponent(component)
67 
68     if (packageName == context.packageName) {
69       return
70     }
71 
72     try {
73       val packageInfo =
74         context.packageManager.getPackageInfo(packageName, PackageManager.GET_META_DATA)
75 
76       val complianceDateString =
77         packageInfo.applicationInfo?.metaData?.getString("onboarding-compliance-date")
78           ?: ComplianceDate.EARLIEST_COMPLIANCE_DATE_STRING
79       packageComplianceDates[packageName] = ComplianceDate(complianceDateString)
80     } catch (e: NameNotFoundException) {
81       Log.w(LOG_TAG, "Could not find package info for $packageName", e)
82       packageComplianceDates[packageName] = null
83     }
84   }
85 
componentSupportsChangenull86   override fun componentSupportsChange(
87     component: String,
88     @ValidChangeId(
89       allowedChangeRadii = [ChangeRadius.MULTI_COMPONENT],
90       disallowedChangeRadiiError =
91         "You cannot query changes which are SINGLE_COMPONENT across process boundaries.",
92     )
93     changeId: Long,
94   ): Boolean {
95     val packageName = extractPackageFromComponent(component)
96 
97     val changeData =
98       ALL_CHANGE_IDS[changeId]
99         ?: throw IllegalStateException(
100           "Invalid change ID $changeId - this should have been blocked by the conformance checker"
101         )
102 
103     if (
104       changeData.changeRadius == ChangeRadius.SINGLE_COMPONENT && packageName != context.packageName
105     ) {
106       // This should be blocked by conformance
107       throw IllegalArgumentException(
108         "You cannot query changes which are SINGLE_COMPONENT across process boundaries."
109       )
110     }
111 
112     val changeOverride = getChangeOverride(packageName, changeId)
113     if (changeOverride != null) {
114       return changeOverride
115     }
116 
117     // If it's released, we assume it's available everywhere
118     // (this call should be blocked by conformance)
119     if (changeData.released != ChangeId.NOT_RELEASED) {
120       return true
121     }
122 
123     if (!packageComplianceDates.containsKey(packageName)) {
124       loadSupportedChanges(component)
125     }
126 
127     if (packageComplianceDates[packageName] == null) {
128       // If we can't fetch the supported features we assume no non-released features
129       return false
130     }
131 
132     return packageComplianceDates[packageName]!!.isAvailable(changeData)
133   }
134 
getChangeOverridenull135   private fun getChangeOverride(packageName: String, changeId: Long): Boolean? {
136     val settingKey = "$packageName.$changeId"
137 
138     return try {
139       Settings.Global.getInt(context.contentResolver, settingKey) == 1
140     } catch (e: Settings.SettingNotFoundException) {
141       null
142     }
143   }
144 
extractPackageFromComponentnull145   private fun extractPackageFromComponent(component: String) = component.split("/", limit = 2)[0]
146 }
147