summaryrefslogtreecommitdiff
path: root/service/java/com/android
diff options
context:
space:
mode:
Diffstat (limited to 'service/java/com/android')
-rw-r--r--service/java/com/android/server/deviceconfig/BootNotificationCreator.java231
-rw-r--r--service/java/com/android/server/deviceconfig/DeviceConfigBootstrapValues.java126
-rw-r--r--service/java/com/android/server/deviceconfig/DeviceConfigInit.java105
-rw-r--r--service/java/com/android/server/deviceconfig/DeviceConfigServiceImpl.java99
-rw-r--r--service/java/com/android/server/deviceconfig/DeviceConfigShellService.java40
-rw-r--r--service/java/com/android/server/deviceconfig/SimPinReplayManager.java144
-rw-r--r--service/java/com/android/server/deviceconfig/UnattendedRebootManager.java323
-rw-r--r--service/java/com/android/server/deviceconfig/UnattendedRebootManagerInjector.java50
-rw-r--r--service/java/com/android/server/deviceconfig/db/DeviceConfigDbAdapter.java150
-rw-r--r--service/java/com/android/server/deviceconfig/db/DeviceConfigDbHelper.java84
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
+ }
+
+}