1 /* 2 * Copyright (C) 2024 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.healthfitness.flags; 18 19 import static com.android.healthfitness.flags.DatabaseVersions.DB_VERSION_ACTIVITY_INTENSITY; 20 import static com.android.healthfitness.flags.DatabaseVersions.DB_VERSION_CLOUD_BACKUP_AND_RESTORE; 21 import static com.android.healthfitness.flags.DatabaseVersions.DB_VERSION_ECOSYSTEM_METRICS; 22 import static com.android.healthfitness.flags.DatabaseVersions.DB_VERSION_PERSONAL_HEALTH_RECORD; 23 import static com.android.healthfitness.flags.DatabaseVersions.LAST_ROLLED_OUT_DB_VERSION; 24 import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE; 25 26 import com.android.internal.annotations.VisibleForTesting; 27 28 import java.util.Map; 29 import java.util.TreeMap; 30 import java.util.function.BooleanSupplier; 31 32 /** 33 * A helper class to act as the source of truth for whether a feature is enabled or not by taking 34 * into account both feature flag and DB flag. See go/hc-aconfig-and-db. 35 * 36 * @hide 37 */ 38 public final class AconfigFlagHelper { 39 private static final String TAG = "HC" + AconfigFlagHelper.class.getSimpleName(); 40 41 // For testing purposes, this field needs to be made public instead of package-private so the 42 // unit tests can access it. This is due to tests don't run in the same classloader as the 43 // framework. See 44 // https://groups.google.com/a/google.com/g/android-chatty-eng/c/TymmRzs3UcY/m/_JeFcynRBwAJ. 45 @VisibleForTesting(visibility = PRIVATE) 46 // Using BooleanSupplier instead of Boolean due to b/370447278#comment2 47 public static final TreeMap<Integer, BooleanSupplier> DB_VERSION_TO_DB_FLAG_MAP = 48 new TreeMap<>(); 49 50 /** 51 * Returns the DB version based on DB flag values, this DB version is used to initialize {@link 52 * android.database.sqlite.SQLiteOpenHelper} to dictate which DB upgrades will be executed. 53 */ getDbVersion()54 public static synchronized int getDbVersion() { 55 if (!Flags.infraToGuardDbChanges()) { 56 return LAST_ROLLED_OUT_DB_VERSION; 57 } 58 59 int dbVersion = LAST_ROLLED_OUT_DB_VERSION; 60 for (Map.Entry<Integer, BooleanSupplier> entry : getDbVersionToDbFlagMap().entrySet()) { 61 if (!entry.getValue().getAsBoolean()) { 62 break; 63 } 64 dbVersion = entry.getKey(); 65 } 66 67 return dbVersion; 68 } 69 70 /** 71 * Returns whether the DB flag of a feature is enabled. 72 * 73 * <p>A DB flag is deemed to be enabled if and only if the DB flag as well as all other features 74 * with smaller version numbers have their DB flags enabled. 75 * 76 * <p>For example, if DB_VERSION_TO_DB_FLAG_MAP contains these: 77 * 78 * <pre>{@code 79 * DB_F1 = true 80 * DB_F2 = true 81 * DB_F3 = true 82 * DB_F4 = false 83 * }</pre> 84 * 85 * Then isDbFlagEnabled(3) will return true and isDbFlagEnabled(4) will return false. 86 * 87 * <p>In case the map contains a disconnected line of "true"s before the last "false" like this: 88 * 89 * <pre>{@code 90 * DB_F1 = true 91 * DB_F2 = false 92 * DB_F3 = true 93 * DB_F4 = false 94 * }</pre> 95 * 96 * Then isDbFlagEnabled(3) will return false even though DB_F3 is mapped to true. 97 * 98 * @see #getDbVersion() 99 * @see ag/28760234 for example of how to use this method 100 */ isDbFlagEnabled(int dbVersion)101 private static synchronized boolean isDbFlagEnabled(int dbVersion) { 102 return getDbVersion() >= dbVersion; 103 } 104 AconfigFlagHelper()105 private AconfigFlagHelper() {} 106 107 // ============================================================================================= 108 // Only things in below this comment should be updated when we move DB schema changes of a 109 // feature from "under development" to "finalized". "finalized" here means the DB schema changes 110 // won't be changed again, they will be assigned a DB version and a DB flag, if further changes 111 // are required to the DB schema, then new DB version and DB flag are required. 112 // ============================================================================================= 113 114 /** 115 * Returns a map of DB version => DB flag with the DB versions being keys and ordered. 116 * 117 * <p>Note: Because the map is initialized with aconfig flag values, hence it needs to be 118 * initialized at run time via a method call rather than static block or static field, otherwise 119 * the <code>@EnableFlags</code> annotations won't work in unit tests due to its evaluation 120 * being done after the map has already been initialized. 121 */ getDbVersionToDbFlagMap()122 private static Map<Integer, BooleanSupplier> getDbVersionToDbFlagMap() { 123 if (!DB_VERSION_TO_DB_FLAG_MAP.isEmpty()) { 124 return DB_VERSION_TO_DB_FLAG_MAP; 125 } 126 127 DB_VERSION_TO_DB_FLAG_MAP.put( 128 DB_VERSION_PERSONAL_HEALTH_RECORD, Flags::personalHealthRecordDatabase); 129 DB_VERSION_TO_DB_FLAG_MAP.put(DB_VERSION_ACTIVITY_INTENSITY, Flags::activityIntensityDb); 130 DB_VERSION_TO_DB_FLAG_MAP.put( 131 DB_VERSION_ECOSYSTEM_METRICS, Flags::ecosystemMetricsDbChanges); 132 DB_VERSION_TO_DB_FLAG_MAP.put( 133 DB_VERSION_CLOUD_BACKUP_AND_RESTORE, Flags::cloudBackupAndRestoreDb); 134 135 return DB_VERSION_TO_DB_FLAG_MAP; 136 } 137 138 /** Returns a boolean indicating whether PHR feature is enabled. */ isPersonalHealthRecordEnabled()139 public static synchronized boolean isPersonalHealthRecordEnabled() { 140 return Flags.personalHealthRecord() && isDbFlagEnabled(DB_VERSION_PERSONAL_HEALTH_RECORD); 141 } 142 143 /** Returns a boolean indicating whether Activity Intensity data type is enabled. */ isActivityIntensityEnabled()144 public static boolean isActivityIntensityEnabled() { 145 return Flags.activityIntensity() 146 && isDbFlagEnabled(DB_VERSION_ACTIVITY_INTENSITY) 147 && Flags.healthConnectMappings(); 148 } 149 150 /** Returns a boolean indicating whether Ecosystem Metrics is enabled. */ isEcosystemMetricsEnabled()151 public static boolean isEcosystemMetricsEnabled() { 152 return Flags.ecosystemMetrics() && isDbFlagEnabled(DB_VERSION_ECOSYSTEM_METRICS); 153 } 154 155 /** Returns a boolean indicating whether cloud backup & restore is enabled. */ isCloudBackupRestoreEnabled()156 public static boolean isCloudBackupRestoreEnabled() { 157 return Flags.cloudBackupAndRestore() 158 && isDbFlagEnabled(DB_VERSION_CLOUD_BACKUP_AND_RESTORE); 159 } 160 } 161