diff options
Diffstat (limited to 'service/java/com/android')
10 files changed, 1305 insertions, 47 deletions
diff --git a/service/java/com/android/server/deviceconfig/BootNotificationCreator.java b/service/java/com/android/server/deviceconfig/BootNotificationCreator.java new file mode 100644 index 0000000..d29f317 --- /dev/null +++ b/service/java/com/android/server/deviceconfig/BootNotificationCreator.java @@ -0,0 +1,231 @@ +package com.android.server.deviceconfig; + +import android.annotation.NonNull; +import android.app.AlarmManager; +import android.app.Notification; +import android.app.Notification.Action; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.IntentFilter; +import android.content.BroadcastReceiver; +import android.content.Intent; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.Context; +import android.graphics.drawable.Icon; +import android.os.PowerManager; +import android.provider.DeviceConfig.OnPropertiesChangedListener; +import android.provider.DeviceConfig.Properties; +import android.util.Slog; +import com.android.server.deviceconfig.resources.R; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.io.IOException; +import java.util.Map; +import java.util.Set; + +import static android.app.NotificationManager.IMPORTANCE_HIGH; +import static java.time.temporal.ChronoUnit.SECONDS; + +/** + * Creates notifications when aconfig flags are staged on the device. + * + * The notification alerts the user to reboot, to apply the staged flags. + * + * @hide + */ +class BootNotificationCreator implements OnPropertiesChangedListener { + private static final String TAG = "DeviceConfigBootNotificationCreator"; + + private static final String RESOURCES_PACKAGE = + "com.android.server.deviceconfig.resources"; + + private static final String REBOOT_REASON = "DeviceConfig"; + + private static final String ACTION_TRIGGER_HARD_REBOOT = + "com.android.server.deviceconfig.TRIGGER_HARD_REBOOT"; + private static final String ACTION_POST_NOTIFICATION = + "com.android.server.deviceconfig.POST_NOTIFICATION"; + + private static final String CHANNEL_ID = "trunk-stable-flags"; + private static final String CHANNEL_NAME = "Trunkfood flags"; + private static final int NOTIFICATION_ID = 111555; + + private NotificationManager notificationManager; + private PowerManager powerManager; + private AlarmManager alarmManager; + + private Context context; + + private static final int REBOOT_HOUR = 10; + private static final int REBOOT_MINUTE = 0; + private static final int MIN_SECONDS_TO_SHOW_NOTIF = 86400; + + private LocalDateTime lastReboot; + + private Map<String, Set<String>> aconfigFlags; + + public BootNotificationCreator(@NonNull Context context, + Map<String, Set<String>> aconfigFlags) { + this.context = context; + this.aconfigFlags = aconfigFlags; + + this.context.registerReceiver( + new HardRebootBroadcastReceiver(), + new IntentFilter(ACTION_TRIGGER_HARD_REBOOT), + Context.RECEIVER_EXPORTED); + this.context.registerReceiver( + new PostNotificationBroadcastReceiver(), + new IntentFilter(ACTION_POST_NOTIFICATION), + Context.RECEIVER_EXPORTED); + + this.lastReboot = LocalDateTime.now(ZoneId.systemDefault()); + } + + @Override + public void onPropertiesChanged(Properties properties) { + if (!containsAconfigChanges(properties)) { + return; + } + + if (!tryInitializeDependenciesIfNeeded()) { + Slog.i(TAG, "not posting notif; service dependencies not ready"); + return; + } + + PendingIntent pendingIntent = + PendingIntent.getBroadcast( + context, + /* requestCode= */ 1, + new Intent(ACTION_POST_NOTIFICATION), + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); + + ZonedDateTime now = Instant + .ofEpochMilli(System.currentTimeMillis()) + .atZone(ZoneId.systemDefault()); + + LocalDateTime currentTime = now.toLocalDateTime(); + LocalDateTime postTime = now.toLocalDate().atTime(REBOOT_HOUR, REBOOT_MINUTE); + + LocalDateTime scheduledPostTime = + currentTime.isBefore(postTime) ? postTime : postTime.plusDays(1); + long scheduledPostTimeLong = scheduledPostTime + .atZone(ZoneId.systemDefault()) + .toInstant() + .toEpochMilli(); + + alarmManager.setExact( + AlarmManager.RTC_WAKEUP, scheduledPostTimeLong, pendingIntent); + } + + private boolean containsAconfigChanges(Properties properties) { + for (String namespaceAndFlag : properties.getKeyset()) { + int firstStarIndex = namespaceAndFlag.indexOf("*"); + if (firstStarIndex == -1 || firstStarIndex == 0 + || firstStarIndex == namespaceAndFlag.length() - 1) { + Slog.w(TAG, "detected malformed staged flag: " + namespaceAndFlag); + continue; + } + + String namespace = namespaceAndFlag.substring(0, firstStarIndex); + String flag = namespaceAndFlag.substring(firstStarIndex + 1); + + if (aconfigFlags.get(namespace) != null && aconfigFlags.get(namespace).contains(flag)) { + return true; + } + } + return false; + } + + private class PostNotificationBroadcastReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + LocalDateTime now = LocalDateTime.now(ZoneId.systemDefault()); + + if (lastReboot.until(now, SECONDS) < MIN_SECONDS_TO_SHOW_NOTIF) { + Slog.w(TAG, "not enough time passed, punting"); + tryAgainIn24Hours(now); + return; + } + + PendingIntent pendingIntent = + PendingIntent.getBroadcast( + context, + /* requestCode= */ 1, + new Intent(ACTION_TRIGGER_HARD_REBOOT), + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); + + try { + Context resourcesContext = context.createPackageContext(RESOURCES_PACKAGE, 0); + Action action = new Action.Builder( + Icon.createWithResource(resourcesContext, R.drawable.ic_restart), + resourcesContext.getString(R.string.boot_notification_action_text), + pendingIntent).build(); + Notification notification = new Notification.Builder(context, CHANNEL_ID) + .setContentText(resourcesContext.getString(R.string.boot_notification_content)) + .setContentTitle(resourcesContext.getString(R.string.boot_notification_title)) + .setSmallIcon(Icon.createWithResource(resourcesContext, R.drawable.ic_flag)) + .addAction(action) + .build(); + notificationManager.notify(NOTIFICATION_ID, notification); + } catch (NameNotFoundException e) { + Slog.e(TAG, "failed to post boot notification", e); + } + } + + private void tryAgainIn24Hours(LocalDateTime currentTime) { + PendingIntent pendingIntent = + PendingIntent.getBroadcast( + context, + /* requestCode= */ 1, + new Intent(ACTION_POST_NOTIFICATION), + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); + + LocalDateTime postTime = + currentTime.toLocalDate().atTime(REBOOT_HOUR, REBOOT_MINUTE).plusDays(1); + long scheduledPostTimeLong = postTime + .atZone(ZoneId.systemDefault()) + .toInstant() + .toEpochMilli(); + alarmManager.setExact( + AlarmManager.RTC_WAKEUP, scheduledPostTimeLong, pendingIntent); + } + } + + private class HardRebootBroadcastReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + powerManager.reboot(REBOOT_REASON); + } + } + + /** + * If deps are not initialized yet, try to initialize them. + * + * @return true if the dependencies are newly or already initialized, + * or false if they are not ready yet + */ + private boolean tryInitializeDependenciesIfNeeded() { + if (notificationManager == null) { + notificationManager = context.getSystemService(NotificationManager.class); + if (notificationManager != null) { + notificationManager.createNotificationChannel( + new NotificationChannel(CHANNEL_ID, CHANNEL_NAME, IMPORTANCE_HIGH)); + } + } + + if (alarmManager == null) { + alarmManager = context.getSystemService(AlarmManager.class); + } + + if (powerManager == null) { + powerManager = context.getSystemService(PowerManager.class); + } + + return notificationManager != null + && alarmManager != null + && powerManager != null; + } +} diff --git a/service/java/com/android/server/deviceconfig/DeviceConfigBootstrapValues.java b/service/java/com/android/server/deviceconfig/DeviceConfigBootstrapValues.java new file mode 100644 index 0000000..b480bc2 --- /dev/null +++ b/service/java/com/android/server/deviceconfig/DeviceConfigBootstrapValues.java @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.deviceconfig; + +import android.annotation.SuppressLint; +import android.provider.DeviceConfig; +import android.util.Slog; + +import java.io.IOException; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.stream.Stream; + +/** + * @hide + */ +public class DeviceConfigBootstrapValues { + private static final String TAG = "DeviceConfig"; + private static final String SYSTEM_OVERRIDES_PATH = "file:///system/etc/device-config-defaults"; + private static final String META_NAMESPACE = "DeviceConfigBootstrapValues"; + private static final String META_KEY = "processed_values"; + + private final String defaultValuesPath; + + public DeviceConfigBootstrapValues() { + this(SYSTEM_OVERRIDES_PATH); + } + + public DeviceConfigBootstrapValues(String defaultValuesPath) { + this.defaultValuesPath = defaultValuesPath; + } + + /** + * Performs the logic to apply bootstrap values when needed. + * + * If a file with the bootstrap values exists and they haven't been parsed before, + * it will parse the file and apply the values. + * + * @throws IOException if there's a problem reading the bootstrap file + * @throws RuntimeException if setting the values in DeviceConfig throws an exception + */ + public void applyValuesIfNeeded() throws IOException { + if (getPath().toFile().exists()) { + if (checkIfHasAlreadyParsedBootstrapValues()) { + Slog.i(TAG, "Bootstrap values already parsed, not processing again"); + } else { + parseAndApplyBootstrapValues(); + Slog.i(TAG, "Parsed bootstrap values"); + } + } else { + Slog.i(TAG, "Bootstrap values not found"); + } + } + + @SuppressLint("MissingPermission") + private boolean checkIfHasAlreadyParsedBootstrapValues() { + DeviceConfig.Properties properties = DeviceConfig.getProperties(META_NAMESPACE); + return properties.getKeyset().size() > 0; + } + + @SuppressLint("MissingPermission") + private void parseAndApplyBootstrapValues() throws IOException { + Path path = getPath(); + try (Stream<String> lines = Files.lines(path)) { + lines.forEach(line -> processLine(line)); + } + // store a property in DeviceConfig so that we know we have successufully + // processed this + writeToDeviceConfig(META_NAMESPACE, META_KEY, "true"); + } + + private void processLine(String line) { + // contents for each line: + // <namespace>:<package>.<flag-name>=[enabled|disabled] + // we actually use <package>.<flag-name> combined in calls into DeviceConfig + int namespaceDelimiter = line.indexOf(':'); + String namespace = line.substring(0, namespaceDelimiter); + if (namespaceDelimiter < 1) { + throw new IllegalArgumentException("Unexpectedly found : at index " + + namespaceDelimiter); + } + int valueDelimiter = line.indexOf('='); + if (valueDelimiter < 5) { + throw new IllegalArgumentException("Unexpectedly found = at index " + valueDelimiter); + } + String key = line.substring(namespaceDelimiter + 1, valueDelimiter); + String value = line.substring(valueDelimiter + 1); + String val; + if ("enabled".equals(value)) { + val = "true"; + } else if ("disabled".equals(value)) { + val = "false"; + } else { + throw new IllegalArgumentException("Received unexpected value: " + value); + } + writeToDeviceConfig(namespace, key, val); + } + + @SuppressLint("MissingPermission") + private void writeToDeviceConfig(String namespace, String key, String value) { + boolean result = DeviceConfig.setProperty(namespace, key, value, /* makeDefault= */ true); + if (!result) { + throw new RuntimeException("Failed to set DeviceConfig property [" + namespace + "] " + + key + "=" + value); + } + } + + private Path getPath() { + return Path.of(URI.create(defaultValuesPath)); + } +} diff --git a/service/java/com/android/server/deviceconfig/DeviceConfigInit.java b/service/java/com/android/server/deviceconfig/DeviceConfigInit.java index 0921d81..b1ed0d6 100644 --- a/service/java/com/android/server/deviceconfig/DeviceConfigInit.java +++ b/service/java/com/android/server/deviceconfig/DeviceConfigInit.java @@ -1,18 +1,45 @@ package com.android.server.deviceconfig; +import static com.android.server.deviceconfig.Flags.enableRebootNotification; +import static com.android.server.deviceconfig.Flags.enableUnattendedReboot; + +import java.io.IOException; import java.io.FileDescriptor; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.HashSet; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import android.aconfig.Aconfig.parsed_flags; +import android.aconfig.Aconfig.parsed_flag; +import android.content.Intent; import android.annotation.NonNull; import android.annotation.SystemApi; +import android.content.BroadcastReceiver; import android.content.Context; +import android.os.AsyncTask; import android.os.Binder; +import android.content.IntentFilter; +import android.provider.DeviceConfig; import android.provider.UpdatableDeviceConfigServiceReadiness; - +import android.content.ServiceConnection; +import android.os.IBinder; +import android.content.ComponentName; +import android.util.Slog; +import com.android.modules.utils.build.SdkLevel; import com.android.server.SystemService; /** @hide */ @SystemApi(client = SystemApi.Client.SYSTEM_SERVER) public class DeviceConfigInit { + private static final String TAG = "DEVICE_CONFIG_INIT"; + private static final String STAGED_NAMESPACE = "staged"; + + private static final String SYSTEM_FLAGS_PATH = "/system/etc/aconfig_flags.pb"; + private static final String SYSTEM_EXT_FLAGS_PATH = "/system_ext/etc/aconfig_flags.pb"; + private static final String VENDOR_FLAGS_PATH = "/vendor/etc/aconfig_flags.pb"; private DeviceConfigInit() { // do not instantiate @@ -21,24 +48,88 @@ public class DeviceConfigInit { /** @hide */ @SystemApi(client = SystemApi.Client.SYSTEM_SERVER) public static class Lifecycle extends SystemService { - private DeviceConfigShellService mShellService; + private DeviceConfigServiceImpl mService; + private UnattendedRebootManager mUnattendedRebootManager; /** @hide */ @SystemApi(client = SystemApi.Client.SYSTEM_SERVER) public Lifecycle(@NonNull Context context) { super(context); - // this service is always instantiated but should only launch subsequent services + // this service is always instantiated but should only launch subsequent service(s) // if the module is ready if (UpdatableDeviceConfigServiceReadiness.shouldStartUpdatableService()) { - mShellService = new DeviceConfigShellService(); + mService = new DeviceConfigServiceImpl(getContext()); + publishBinderService(DeviceConfig.SERVICE_NAME, mService); } + applyBootstrapValues(); } - /** @hide */ + /** + * @hide + */ @Override public void onStart() { - if (UpdatableDeviceConfigServiceReadiness.shouldStartUpdatableService()) { - publishBinderService("device_config_updatable", mShellService); + if (enableRebootNotification()) { + Map<String, Set<String>> aconfigFlags = new HashMap<>(); + try { + addAconfigFlagsFromFile(aconfigFlags, SYSTEM_FLAGS_PATH); + addAconfigFlagsFromFile(aconfigFlags, SYSTEM_EXT_FLAGS_PATH); + addAconfigFlagsFromFile(aconfigFlags, VENDOR_FLAGS_PATH); + } catch (IOException e) { + Slog.e(TAG, "error loading aconfig flags", e); + } + + BootNotificationCreator notifCreator = new BootNotificationCreator( + getContext().getApplicationContext(), + aconfigFlags); + + DeviceConfig.addOnPropertiesChangedListener( + STAGED_NAMESPACE, + AsyncTask.THREAD_POOL_EXECUTOR, + notifCreator); + } + + if (enableUnattendedReboot()) { + mUnattendedRebootManager = + new UnattendedRebootManager(getContext().getApplicationContext()); + getContext() + .registerReceiver( + new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + mUnattendedRebootManager.prepareUnattendedReboot(); + mUnattendedRebootManager.scheduleReboot(); + } + }, + new IntentFilter(Intent.ACTION_BOOT_COMPLETED)); + } + } + + private void addAconfigFlagsFromFile(Map<String, Set<String>> aconfigFlags, + String fileName) throws IOException { + byte[] contents = (new FileInputStream(fileName)).readAllBytes(); + parsed_flags parsedFlags = parsed_flags.parseFrom(contents); + for (parsed_flag flag : parsedFlags.getParsedFlagList()) { + if (aconfigFlags.get(flag.getNamespace()) == null) { + aconfigFlags.put(flag.getNamespace(), new HashSet<>()); + aconfigFlags.get(flag.getNamespace()).add(flag.getName()); + } else { + aconfigFlags.get(flag.getNamespace()).add(flag.getName()); + } + } + } + + private void applyBootstrapValues() { + if (SdkLevel.isAtLeastV()) { + try { + new DeviceConfigBootstrapValues().applyValuesIfNeeded(); + } catch (RuntimeException e) { + Slog.e(TAG, "Failed to load boot overrides", e); + throw e; + } catch (IOException e) { + Slog.e(TAG, "Failed to load boot overrides", e); + throw new RuntimeException(e); + } } } } diff --git a/service/java/com/android/server/deviceconfig/DeviceConfigServiceImpl.java b/service/java/com/android/server/deviceconfig/DeviceConfigServiceImpl.java new file mode 100644 index 0000000..ddc69db --- /dev/null +++ b/service/java/com/android/server/deviceconfig/DeviceConfigServiceImpl.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.deviceconfig; + +import android.annotation.NonNull; +import android.content.Context; +import android.os.ParcelFileDescriptor; +import android.os.RemoteException; +import android.provider.aidl.IDeviceConfigManager; +import android.provider.DeviceConfigInitializer; + +import com.android.server.deviceconfig.db.DeviceConfigDbAdapter; +import com.android.server.deviceconfig.db.DeviceConfigDbHelper; + +import java.io.PrintWriter; +import java.util.Map; + +import com.android.modules.utils.BasicShellCommandHandler; + +/** + * DeviceConfig Service implementation (updatable via Mainline) that uses a SQLite database as a storage mechanism + * for the configuration values. + * + * @hide + */ +public class DeviceConfigServiceImpl extends IDeviceConfigManager.Stub { + private final DeviceConfigDbAdapter mDbAdapter; + + public DeviceConfigServiceImpl(Context context) { + DeviceConfigDbHelper dbHelper = new DeviceConfigDbHelper(context); + mDbAdapter = new DeviceConfigDbAdapter(dbHelper.getWritableDatabase()); + + DeviceConfigInitializer.getDeviceConfigServiceManager() + .getDeviceConfigUpdatableServiceRegisterer() + .register(this); + } + + @Override + public Map<String, String> getProperties(String namespace, String[] names) throws RemoteException { + return mDbAdapter.getValuesForNamespace(namespace, names); + } + + @Override + public boolean setProperties(String namespace, Map<String, String> values) { + return mDbAdapter.setValues(namespace, values); + } + + @Override + public boolean setProperty(String namespace, String key, String value, boolean makeDefault) { + return mDbAdapter.setValue(namespace, key, value, makeDefault); + } + + @Override + public boolean deleteProperty(String namespace, String key) { + return mDbAdapter.deleteValue(namespace, key); + } + + @Override + public int handleShellCommand(@NonNull ParcelFileDescriptor in, + @NonNull ParcelFileDescriptor out, @NonNull ParcelFileDescriptor err, + @NonNull String[] args) { + return (new MyShellCommand()).exec( + this, in.getFileDescriptor(), out.getFileDescriptor(), err.getFileDescriptor(), + args); + } + + static final class MyShellCommand extends BasicShellCommandHandler { + // TODO(b/265948938) implement this + + @Override + public int onCommand(String cmd) { + if (cmd == null || "help".equals(cmd) || "-h".equals(cmd)) { + onHelp(); + return -1; + } + return -1; + } + + @Override + public void onHelp() { + PrintWriter pw = getOutPrintWriter(); + pw.println("Device Config implemented in mainline"); + } + } +} diff --git a/service/java/com/android/server/deviceconfig/DeviceConfigShellService.java b/service/java/com/android/server/deviceconfig/DeviceConfigShellService.java deleted file mode 100644 index 14ced76..0000000 --- a/service/java/com/android/server/deviceconfig/DeviceConfigShellService.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.android.server.deviceconfig; - -import java.io.PrintWriter; - -import android.annotation.NonNull; -import android.os.Binder; -import android.os.ParcelFileDescriptor; - -import com.android.modules.utils.BasicShellCommandHandler; - -/** @hide */ -public class DeviceConfigShellService extends Binder { - - @Override - public int handleShellCommand(@NonNull ParcelFileDescriptor in, - @NonNull ParcelFileDescriptor out, @NonNull ParcelFileDescriptor err, - @NonNull String[] args) { - return (new MyShellCommand()).exec( - this, in.getFileDescriptor(), out.getFileDescriptor(), err.getFileDescriptor(), - args); - } - - static final class MyShellCommand extends BasicShellCommandHandler { - - @Override - public int onCommand(String cmd) { - if (cmd == null || "help".equals(cmd) || "-h".equals(cmd)) { - onHelp(); - return -1; - } - return -1; - } - - @Override - public void onHelp() { - PrintWriter pw = getOutPrintWriter(); - pw.println("Device Config implemented in mainline"); - } - } -} diff --git a/service/java/com/android/server/deviceconfig/SimPinReplayManager.java b/service/java/com/android/server/deviceconfig/SimPinReplayManager.java new file mode 100644 index 0000000..93c3f5f --- /dev/null +++ b/service/java/com/android/server/deviceconfig/SimPinReplayManager.java @@ -0,0 +1,144 @@ +package com.android.server.deviceconfig; + +import android.content.Context; +import android.content.res.Resources; +import android.os.PersistableBundle; +import android.telephony.CarrierConfigManager; +import android.telephony.SubscriptionManager; +import android.telephony.TelephonyManager; +import android.util.Log; + +import com.google.common.collect.ImmutableList; + +/** + * If device contains a SIM PIN, must prepare <a + * href="https://source.android.com/docs/core/ota/resume-on-reboot#sim-pin-replay">Sim Pin + * Replay</a> to unlock the device post reboot. + * + * @hide + */ +public class SimPinReplayManager { + + private static final String TAG = "UnattendedRebootManager"; + + // The identifier of the system resource value that determines whether auto-sim-unlock feature is + // enabled/disabled for the device. + private static final String SYSTEM_ENABLE_SIM_PIN_STORAGE_KEY = + "config_allow_pin_storage_for_unattended_reboot"; + // This is a copy of the hidden field + // CarrierConfigManager#KEY_STORE_SIM_PIN_FOR_UNATTENDED_REBOOT_BOOL. Phonesky uses this key to + // read the boolean value in carrier configs specifying whether to enable/disable auto-sim-unlock. + private static final String CARRIER_ENABLE_SIM_PIN_STORAGE_KEY = + "store_sim_pin_for_unattended_reboot_bool"; + + private Context mContext; + + SimPinReplayManager(Context context) { + mContext = context; + } + + /** Returns true, if no SIM PIN present or prepared SIM PIN Replay. */ + public boolean prepareSimPinReplay() { + // Is SIM Pin present? + ImmutableList<Integer> pinLockedSubscriptionIds = getPinLockedSubscriptionIds(mContext); + if (pinLockedSubscriptionIds.isEmpty()) { + return true; + } + + if (!isSimPinStorageEnabled(mContext, pinLockedSubscriptionIds)) { + Log.w(TAG, "SIM PIN storage is disabled"); + return false; + } + + TelephonyManager telephonyManager = mContext.getSystemService(TelephonyManager.class); + if (telephonyManager == null) { + Log.e(TAG, "Failed to prepare SIM PIN Replay, TelephonyManager is null"); + return false; + } + + int prepareUnattendedRebootResult = telephonyManager.prepareForUnattendedReboot(); + if (prepareUnattendedRebootResult == TelephonyManager.PREPARE_UNATTENDED_REBOOT_SUCCESS) { + Log.i(TAG, "SIM PIN replay prepared"); + return true; + } + Log.w(TAG, "Failed to prepare SIM PIN Replay, " + prepareUnattendedRebootResult); + return false; + } + + /** Returns a list of telephony subscription IDs (SIM IDs) locked by PIN. */ + private static ImmutableList<Integer> getPinLockedSubscriptionIds(Context context) { + SubscriptionManager subscriptionManager = context.getSystemService(SubscriptionManager.class); + int[] subscriptionIds = subscriptionManager.getActiveSubscriptionIdList(); + if (subscriptionIds.length == 0) { + return ImmutableList.of(); + } + + TelephonyManager telephonyManager = context.getSystemService(TelephonyManager.class); + ImmutableList.Builder<Integer> pinLockedSubscriptionIdsBuilder = ImmutableList.builder(); + for (int subscriptionId : subscriptionIds) { + if (telephonyManager.createForSubscriptionId(subscriptionId).isIccLockEnabled()) { + pinLockedSubscriptionIdsBuilder.add(subscriptionId); + } + } + return pinLockedSubscriptionIdsBuilder.build(); + } + + /** + * Returns true, if SIM PIN storage is enabled. + * + * <p>The SIM PIN storage might be disabled by OEM or by carrier, subscription (SIM) Id is + * required when checking if the corresponding SIM PIN storage is disabled by the carrier. + * + * <p>Both the OEM and carrier enable SIM PIN storage by default. If fails to read the OEM/carrier + * configs, it assume SIM PIN storage is enabled. + */ + private static boolean isSimPinStorageEnabled( + Context context, ImmutableList<Integer> pinLockedSubscriptionIds) { + if (!isSystemEnableSimPin()) { + return false; + } + + // If the carrier enables SIM PIN. + CarrierConfigManager carrierConfigManager = + context.getSystemService(CarrierConfigManager.class); + if (carrierConfigManager == null) { + Log.w(TAG, "CarrierConfigManager is null"); + return true; + } + for (int pinLockedSubscriptionId : pinLockedSubscriptionIds) { + PersistableBundle subscriptionConfig = + carrierConfigManager.getConfigForSubId( + pinLockedSubscriptionId, CARRIER_ENABLE_SIM_PIN_STORAGE_KEY); + // Only disable if carrier explicitly disables sim pin storage. + if (!subscriptionConfig.isEmpty() + && !subscriptionConfig.getBoolean( + CARRIER_ENABLE_SIM_PIN_STORAGE_KEY, /* defaultValue= */ true)) { + Log.w( + TAG, + "The carrier disables SIM PIN storage on subscription ID " + pinLockedSubscriptionId); + return false; + } + } + Log.v(TAG, "SIM PIN Storage is enabled"); + return true; + } + + private static boolean isSystemEnableSimPin() { + try { + boolean value = + Resources.getSystem() + .getBoolean( + Resources.getSystem() + .getIdentifier( + SYSTEM_ENABLE_SIM_PIN_STORAGE_KEY, + /* defType= */ "bool", + /* defPackage= */ "android")); + Log.i(TAG, SYSTEM_ENABLE_SIM_PIN_STORAGE_KEY + " = " + value); + return value; + } catch (Resources.NotFoundException e) { + Log.e(TAG, "Could not read system resource value ," + SYSTEM_ENABLE_SIM_PIN_STORAGE_KEY); + // When not explicitly disabled, assume SIM PIN storage functions properly. + return true; + } + } +} diff --git a/service/java/com/android/server/deviceconfig/UnattendedRebootManager.java b/service/java/com/android/server/deviceconfig/UnattendedRebootManager.java new file mode 100644 index 0000000..30f2439 --- /dev/null +++ b/service/java/com/android/server/deviceconfig/UnattendedRebootManager.java @@ -0,0 +1,323 @@ +package com.android.server.deviceconfig; + +import static com.android.server.deviceconfig.Flags.enableSimPinReplay; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.AlarmManager; +import android.app.KeyguardManager; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.IntentSender; +import android.net.ConnectivityManager; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.NetworkRequest; +import android.os.PowerManager; +import android.os.RecoverySystem; +import android.os.SystemClock; +import android.util.Log; +import com.android.internal.annotations.VisibleForTesting; + +import java.io.IOException; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.concurrent.TimeUnit; + +/** + * Reboot scheduler for applying aconfig flags. + * + * <p>If device is password protected, uses <a + * href="https://source.android.com/docs/core/ota/resume-on-reboot">Resume on Reboot</a> to reboot + * the device, otherwise proceeds with regular reboot. + * + * @hide + */ +final class UnattendedRebootManager { + private static final int DEFAULT_REBOOT_WINDOW_START_TIME_HOUR = 3; + private static final int DEFAULT_REBOOT_WINDOW_END_TIME_HOUR = 5; + + private static final int DEFAULT_REBOOT_FREQUENCY_DAYS = 2; + + private static final String TAG = "UnattendedRebootManager"; + + static final String REBOOT_REASON = "unattended,flaginfra"; + + @VisibleForTesting + static final String ACTION_RESUME_ON_REBOOT_LSKF_CAPTURED = + "com.android.server.deviceconfig.RESUME_ON_REBOOOT_LSKF_CAPTURED"; + + @VisibleForTesting + static final String ACTION_TRIGGER_REBOOT = "com.android.server.deviceconfig.TRIGGER_REBOOT"; + + private final Context mContext; + + private boolean mLskfCaptured; + + private final UnattendedRebootManagerInjector mInjector; + + private final SimPinReplayManager mSimPinReplayManager; + + private static class InjectorImpl implements UnattendedRebootManagerInjector { + InjectorImpl() { + /*no op*/ + } + + public long now() { + return System.currentTimeMillis(); + } + + public ZoneId zoneId() { + return ZoneId.systemDefault(); + } + + @Override + public long elapsedRealtime() { + return SystemClock.elapsedRealtime(); + } + + public int getRebootStartTime() { + return DEFAULT_REBOOT_WINDOW_START_TIME_HOUR; + } + + public int getRebootEndTime() { + return DEFAULT_REBOOT_WINDOW_END_TIME_HOUR; + } + + public int getRebootFrequency() { + return DEFAULT_REBOOT_FREQUENCY_DAYS; + } + + public void setRebootAlarm(Context context, long rebootTimeMillis) { + AlarmManager alarmManager = context.getSystemService(AlarmManager.class); + alarmManager.setExact( + AlarmManager.RTC_WAKEUP, rebootTimeMillis, createTriggerRebootPendingIntent(context)); + } + + public void triggerRebootOnNetworkAvailable(Context context) { + final ConnectivityManager connectivityManager = + context.getSystemService(ConnectivityManager.class); + NetworkRequest request = + new NetworkRequest.Builder() + .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + .build(); + connectivityManager.requestNetwork(request, createTriggerRebootPendingIntent(context)); + } + + public int rebootAndApply(@NonNull Context context, @NonNull String reason, boolean slotSwitch) + throws IOException { + return RecoverySystem.rebootAndApply(context, reason, slotSwitch); + } + + public void prepareForUnattendedUpdate( + @NonNull Context context, @NonNull String updateToken, @Nullable IntentSender intentSender) + throws IOException { + RecoverySystem.prepareForUnattendedUpdate(context, updateToken, intentSender); + } + + public boolean isPreparedForUnattendedUpdate(@NonNull Context context) throws IOException { + return RecoverySystem.isPreparedForUnattendedUpdate(context); + } + + public void regularReboot(Context context) { + PowerManager powerManager = context.getSystemService(PowerManager.class); + powerManager.reboot(REBOOT_REASON); + } + + private static PendingIntent createTriggerRebootPendingIntent(Context context) { + return PendingIntent.getBroadcast( + context, + /* requestCode= */ 0, + new Intent(ACTION_TRIGGER_REBOOT), + PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_IMMUTABLE); + } + } + + @VisibleForTesting + UnattendedRebootManager( + Context context, + UnattendedRebootManagerInjector injector, + SimPinReplayManager simPinReplayManager) { + mContext = context; + mInjector = injector; + mSimPinReplayManager = simPinReplayManager; + + mContext.registerReceiver( + new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + mLskfCaptured = true; + } + }, + new IntentFilter(ACTION_RESUME_ON_REBOOT_LSKF_CAPTURED), + Context.RECEIVER_EXPORTED); + + // Do not export receiver so that tests don't trigger reboot. + mContext.registerReceiver( + new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + tryRebootOrSchedule(); + } + }, + new IntentFilter(ACTION_TRIGGER_REBOOT), + Context.RECEIVER_NOT_EXPORTED); + } + + UnattendedRebootManager(Context context) { + this(context, new InjectorImpl(), new SimPinReplayManager(context)); + } + + public void prepareUnattendedReboot() { + Log.i(TAG, "Preparing for Unattended Reboot"); + // RoR only supported on devices with screen lock. + if (!isDeviceSecure(mContext)) { + return; + } + PendingIntent pendingIntent = + PendingIntent.getBroadcast( + mContext, + /* requestCode= */ 0, + new Intent(ACTION_RESUME_ON_REBOOT_LSKF_CAPTURED), + PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_IMMUTABLE); + + try { + mInjector.prepareForUnattendedUpdate( + mContext, /* updateToken= */ "", pendingIntent.getIntentSender()); + } catch (IOException e) { + Log.i(TAG, "prepareForUnattendedReboot failed with exception" + e.getLocalizedMessage()); + } + } + + public void scheduleReboot() { + // Reboot the next day at the reboot start time. + LocalDateTime timeToReboot = + Instant.ofEpochMilli(mInjector.now()) + .atZone(mInjector.zoneId()) + .toLocalDate() + .plusDays(mInjector.getRebootFrequency()) + .atTime(mInjector.getRebootStartTime(), /* minute= */ 12); + long rebootTimeMillis = timeToReboot.atZone(mInjector.zoneId()).toInstant().toEpochMilli(); + Log.v(TAG, "Scheduling unattended reboot at time " + timeToReboot); + + if (timeToReboot.isBefore( + LocalDateTime.ofInstant(Instant.ofEpochMilli(mInjector.now()), mInjector.zoneId()))) { + Log.w(TAG, "Reboot time has already passed."); + return; + } + + mInjector.setRebootAlarm(mContext, rebootTimeMillis); + } + + @VisibleForTesting + void tryRebootOrSchedule() { + Log.v(TAG, "Attempting unattended reboot"); + + // Has enough time passed since reboot? + if (TimeUnit.MILLISECONDS.toDays(mInjector.elapsedRealtime()) + < mInjector.getRebootFrequency()) { + Log.v( + TAG, + "Device has already been rebooted in that last " + + mInjector.getRebootFrequency() + + " days."); + scheduleReboot(); + return; + } + // Is RoR is supported? + if (!isDeviceSecure(mContext)) { + Log.v(TAG, "Device is not secure. Proceed with regular reboot"); + mInjector.regularReboot(mContext); + return; + } + // Is RoR prepared? + if (!isPreparedForUnattendedReboot()) { + Log.v(TAG, "Lskf is not captured, reschedule reboot."); + prepareUnattendedReboot(); + scheduleReboot(); + return; + } + // Is network connected? + // TODO(b/305259443): Use after-boot network connectivity projection + if (!isNetworkConnected(mContext)) { + Log.i(TAG, "Network is not connected, reschedule reboot."); + mInjector.triggerRebootOnNetworkAvailable(mContext); + return; + } + // Is current time between reboot window? + int currentHour = + Instant.ofEpochMilli(mInjector.now()) + .atZone(mInjector.zoneId()) + .toLocalDateTime() + .getHour(); + if (currentHour < mInjector.getRebootStartTime() + || currentHour >= mInjector.getRebootEndTime()) { + Log.v(TAG, "Reboot requested outside of reboot window, reschedule reboot."); + prepareUnattendedReboot(); + scheduleReboot(); + return; + } + // Is preparing for SIM PIN replay successful? + if (enableSimPinReplay() && !mSimPinReplayManager.prepareSimPinReplay()) { + Log.w(TAG, "Sim Pin Replay failed, reschedule reboot"); + scheduleReboot(); + } + + // Proceed with RoR. + Log.v(TAG, "Rebooting device to apply device config flags."); + try { + int success = mInjector.rebootAndApply(mContext, REBOOT_REASON, /* slotSwitch= */ false); + if (success != 0) { + // If reboot is not successful, reschedule. + Log.w(TAG, "Unattended reboot failed, reschedule reboot."); + scheduleReboot(); + } + } catch (IOException e) { + Log.e(TAG, e.getLocalizedMessage()); + scheduleReboot(); + } + } + + private boolean isPreparedForUnattendedReboot() { + try { + boolean isPrepared = mInjector.isPreparedForUnattendedUpdate(mContext); + if (isPrepared != mLskfCaptured) { + Log.w(TAG, "isPrepared != mLskfCaptured. Received " + isPrepared); + } + return isPrepared; + } catch (IOException e) { + Log.w(TAG, e.getLocalizedMessage()); + return mLskfCaptured; + } + } + + /** Returns true if the device has screen lock. */ + private static boolean isDeviceSecure(Context context) { + KeyguardManager keyguardManager = context.getSystemService(KeyguardManager.class); + if (keyguardManager == null) { + // Unknown if device is locked, proceed with RoR anyway. + Log.w(TAG, "Keyguard manager is null, proceeding with RoR anyway."); + return true; + } + return keyguardManager.isDeviceSecure(); + } + + private static boolean isNetworkConnected(Context context) { + final ConnectivityManager connectivityManager = + context.getSystemService(ConnectivityManager.class); + if (connectivityManager == null) { + Log.w(TAG, "ConnectivityManager is null"); + return false; + } + Network activeNetwork = connectivityManager.getActiveNetwork(); + NetworkCapabilities networkCapabilities = + connectivityManager.getNetworkCapabilities(activeNetwork); + return networkCapabilities != null + && networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + && networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED); + } +} diff --git a/service/java/com/android/server/deviceconfig/UnattendedRebootManagerInjector.java b/service/java/com/android/server/deviceconfig/UnattendedRebootManagerInjector.java new file mode 100644 index 0000000..5ca3e1e --- /dev/null +++ b/service/java/com/android/server/deviceconfig/UnattendedRebootManagerInjector.java @@ -0,0 +1,50 @@ +package com.android.server.deviceconfig; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.content.IntentSender; +import android.os.RecoverySystem; + +import java.io.IOException; +import java.time.ZoneId; + +/** + * Dependency injectors for {@link com.android.server.deviceconfig.UnattendedRebootManager} to + * enable unit testing. + */ +interface UnattendedRebootManagerInjector { + + /** Time injectors. */ + long now(); + + ZoneId zoneId(); + + long elapsedRealtime(); + + /** Reboot time injectors. */ + int getRebootStartTime(); + + int getRebootEndTime(); + + int getRebootFrequency(); + + /** Reboot Alarm injector. */ + void setRebootAlarm(Context context, long rebootTimeMillis); + + /** Connectivity injector. */ + void triggerRebootOnNetworkAvailable(Context context); + + /** {@link RecoverySystem} methods injectors. */ + int rebootAndApply(@NonNull Context context, @NonNull String reason, boolean slotSwitch) + throws IOException; + + void prepareForUnattendedUpdate( + @NonNull Context context, @NonNull String updateToken, @Nullable IntentSender intentSender) + throws IOException; + + boolean isPreparedForUnattendedUpdate(@NonNull Context context) throws IOException; + + /** Regular reboot injector. */ + void regularReboot(Context context); +} diff --git a/service/java/com/android/server/deviceconfig/db/DeviceConfigDbAdapter.java b/service/java/com/android/server/deviceconfig/db/DeviceConfigDbAdapter.java new file mode 100644 index 0000000..52ee5bd --- /dev/null +++ b/service/java/com/android/server/deviceconfig/db/DeviceConfigDbAdapter.java @@ -0,0 +1,150 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.deviceconfig.db; + +import android.content.ContentValues; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.text.TextUtils; + +import com.android.server.deviceconfig.db.DeviceConfigDbHelper.Contract.DeviceConfigEntry; + +import java.util.HashMap; +import java.util.Map; + +/** + * @hide + */ +public class DeviceConfigDbAdapter { + + private final SQLiteDatabase mDb; + + public DeviceConfigDbAdapter(SQLiteDatabase db) { + mDb = db; + } + + public Map<String, String> getValuesForNamespace(String namespace, String... keys) { + + String[] projection = { + DeviceConfigEntry.COLUMN_NAME_KEY, + DeviceConfigEntry.COLUMN_NAME_VALUE + }; + + String selection; + String[] selectionArgs; + if (keys != null && keys.length > 0) { + selection = DeviceConfigEntry.COLUMN_NAME_NAMESPACE + " = ? " + + "and " + DeviceConfigEntry.COLUMN_NAME_KEY + " in ( ? ) "; + String keySelection = TextUtils.join(",", keys); + selectionArgs = new String[]{namespace, keySelection}; + } else { + selection = DeviceConfigEntry.COLUMN_NAME_NAMESPACE + " = ?"; + selectionArgs = new String[]{namespace}; + } + Cursor cursor = mDb.query( + DeviceConfigEntry.TABLE_NAME, + projection, + selection, + selectionArgs, + null, + null, + null + ); + + Map<String, String> map = new HashMap<>(cursor.getCount()); + while (cursor.moveToNext()) { + String key = cursor.getString( + cursor.getColumnIndexOrThrow(DeviceConfigEntry.COLUMN_NAME_KEY)); + String value = cursor.getString( + cursor.getColumnIndexOrThrow(DeviceConfigEntry.COLUMN_NAME_VALUE)); + map.put(key, value); + } + cursor.close(); + return map; + } + + /** + * + * @return true if the data was inserted or updated in the database + */ + private boolean insertOrUpdateValue_inTransaction(String namespace, String key, String value) { + // TODO(b/265948914): see if this is the most performant way to either insert or update a record + ContentValues values = new ContentValues(); + values.put(DeviceConfigEntry.COLUMN_NAME_NAMESPACE, namespace); + values.put(DeviceConfigEntry.COLUMN_NAME_KEY, key); + values.put(DeviceConfigEntry.COLUMN_NAME_VALUE, value); + + String where = DeviceConfigEntry.COLUMN_NAME_NAMESPACE + " = ? " + + "and " + DeviceConfigEntry.COLUMN_NAME_VALUE + " = ? "; + + String[] whereArgs = {namespace, key}; + int updatedRows = mDb.update(DeviceConfigEntry.TABLE_NAME, values, where, whereArgs); + if (updatedRows == 0) { + // this is a new row, we need to insert it + long id = mDb.insert(DeviceConfigEntry.TABLE_NAME, null, values); + return id != -1; + } + return updatedRows > 0; + } + + /** + * Set or update the values in the map into the namespace. + * + * @return true if all values were set. Returns true if the map is empty. + */ + public boolean setValues(String namespace, Map<String, String> map) { + if (map.size() == 0) { + return true; + } + boolean allSucceeded = true; + try { + mDb.beginTransaction(); + for (Map.Entry<String, String> entry : map.entrySet()) { + // TODO(b/265948914) probably should call yieldIfContendedSafely in this loop + allSucceeded &= insertOrUpdateValue_inTransaction(namespace, entry.getKey(), + entry.getValue()); + } + mDb.setTransactionSuccessful(); + } finally { + mDb.endTransaction(); + } + return allSucceeded; + } + + /** + * + * @return true if the value was set + */ + public boolean setValue(String namespace, String key, String value, boolean makeDefault) { + HashMap<String, String> map = new HashMap<>(); + map.put(key, value); + return setValues(namespace, map); + // TODO(b/265948914) implement make default! + } + + /** + * + * @return true if any value was deleted + */ + public boolean deleteValue(String namespace, String key) { + String where = DeviceConfigEntry.COLUMN_NAME_NAMESPACE + " = ? " + + "and " + DeviceConfigEntry.COLUMN_NAME_KEY + " = ? "; + String[] whereArgs = { namespace, key }; + int count = mDb.delete(DeviceConfigEntry.TABLE_NAME, where, whereArgs); + return count > 0; + } +} diff --git a/service/java/com/android/server/deviceconfig/db/DeviceConfigDbHelper.java b/service/java/com/android/server/deviceconfig/db/DeviceConfigDbHelper.java new file mode 100644 index 0000000..d7c90cc --- /dev/null +++ b/service/java/com/android/server/deviceconfig/db/DeviceConfigDbHelper.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.deviceconfig.db; + +import android.content.Context; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.provider.BaseColumns; + +/** + * @hide + */ +public class DeviceConfigDbHelper extends SQLiteOpenHelper { + public static final int DATABASE_VERSION = 1; + public static final String DATABASE_NAME = "config_infrastructure.db"; + + /** + * TODO(b/265948914) / to consider: + * + * - enforce uniqueness of (namespace, key) pairs + * - synchronize calls that modify the db (maybe reads too?) + * - probably use a read/write lock + * - per-process caching of results so we don't go to the db every time + * - test the sql commands to make sure they work well (e.g. where clauses are + * written properly) + * - check the performance of the sql commands and look for optimizations + * - write a test for adapter.setProperties that has some but not all + * preexisting properties + * - Settings.Config has a concept "makeDefault" which is not implemented here + * - ensure that any sql exceptions are not thrown to the callers (where methods + * can return + * false) + * - see what happens if a caller starts observing changes before the database + * is loaded/ready (early in the boot process) + * - I've seen strict mode alerts about doing I/O in the main thread after a + * device boots. Maybe we can't avoid it but double check. + * - finish API implementation in DatabaseDataStore + */ + + interface Contract { + class DeviceConfigEntry implements BaseColumns { + public static final String TABLE_NAME = "config"; + public static final String COLUMN_NAME_NAMESPACE = "namespace"; + public static final String COLUMN_NAME_KEY = "config_key"; + public static final String COLUMN_NAME_VALUE = "config_value"; + } + } + + private static final String SQL_CREATE_ENTRIES = + "CREATE TABLE " + Contract.DeviceConfigEntry.TABLE_NAME + " (" + + Contract.DeviceConfigEntry._ID + " INTEGER PRIMARY KEY," + + Contract.DeviceConfigEntry.COLUMN_NAME_NAMESPACE + " TEXT," + + Contract.DeviceConfigEntry.COLUMN_NAME_KEY + " TEXT," + + Contract.DeviceConfigEntry.COLUMN_NAME_VALUE + " TEXT)"; + + public DeviceConfigDbHelper(Context context) { + super(context, DATABASE_NAME, null, DATABASE_VERSION); + } + + @Override + public void onCreate(SQLiteDatabase db) { + db.execSQL(SQL_CREATE_ENTRIES); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + // no op for now + } + +} |