[Ravenwood] Support DeviceConfig

Flag: EXEMPT host test change only
Bug: 368591527
Test: atest CtsDeviceConfigTestCasesRavenwood
Test: $ANDROID_BUILD_TOP/frameworks/base/ravenwood/scripts/run-ravenwood-tests.sh
Change-Id: I696b857f7f724517314444c35effdccadbc7adac
Merged-In: I920c65840776669acee4783e0f2ca23aecc0ea1b
diff --git a/ravenwood/Android.bp b/ravenwood/Android.bp
index bfa801f..c6f68a6 100644
--- a/ravenwood/Android.bp
+++ b/ravenwood/Android.bp
@@ -124,6 +124,7 @@
     ],
     libs: [
         "framework-minus-apex.ravenwood",
+        "framework-configinfrastructure.ravenwood",
         "ravenwood-helper-libcore-runtime",
     ],
     sdk_version: "core_current",
@@ -394,6 +395,9 @@
         "icu4j-icudata-jarjar",
         "icu4j-icutzdata-jarjar",
 
+        // DeviceConfig
+        "framework-configinfrastructure.ravenwood",
+
         // Provide runtime versions of utils linked in below
         "junit",
         "truth",
diff --git a/ravenwood/Framework.bp b/ravenwood/Framework.bp
index 5cb1479..1bea434 100644
--- a/ravenwood/Framework.bp
+++ b/ravenwood/Framework.bp
@@ -290,3 +290,57 @@
         "core-icu4j-for-host.ravenwood.jar",
     ],
 }
+
+///////////////////////////////////
+// framework-configinfrastructure
+///////////////////////////////////
+
+java_genrule {
+    name: "framework-configinfrastructure.ravenwood-base",
+    tools: ["hoststubgen"],
+    cmd: "$(location hoststubgen) " +
+        "@$(location :ravenwood-standard-options) " +
+
+        "--debug-log $(location framework-configinfrastructure.log) " +
+        "--stats-file $(location framework-configinfrastructure_stats.csv) " +
+        "--supported-api-list-file $(location framework-configinfrastructure_apis.csv) " +
+        "--gen-keep-all-file $(location framework-configinfrastructure_keep_all.txt) " +
+        "--gen-input-dump-file $(location framework-configinfrastructure_dump.txt) " +
+
+        "--out-impl-jar $(location ravenwood.jar) " +
+        "--in-jar $(location :framework-configinfrastructure.impl{.jar}) " +
+
+        "--policy-override-file $(location :ravenwood-common-policies) " +
+        "--policy-override-file $(location :framework-configinfrastructure-ravenwood-policies) ",
+    srcs: [
+        ":framework-configinfrastructure.impl{.jar}",
+
+        ":ravenwood-common-policies",
+        ":framework-configinfrastructure-ravenwood-policies",
+        ":ravenwood-standard-options",
+    ],
+    out: [
+        "ravenwood.jar",
+
+        // Following files are created just as FYI.
+        "framework-configinfrastructure_keep_all.txt",
+        "framework-configinfrastructure_dump.txt",
+
+        "framework-configinfrastructure.log",
+        "framework-configinfrastructure_stats.csv",
+        "framework-configinfrastructure_apis.csv",
+    ],
+    visibility: ["//visibility:private"],
+}
+
+java_genrule {
+    name: "framework-configinfrastructure.ravenwood",
+    defaults: ["ravenwood-internal-only-visibility-genrule"],
+    cmd: "cp $(in) $(out)",
+    srcs: [
+        ":framework-configinfrastructure.ravenwood-base{ravenwood.jar}",
+    ],
+    out: [
+        "framework-configinfrastructure.ravenwood.jar",
+    ],
+}
diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java
index 5894476..5237c56 100644
--- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java
+++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java
@@ -38,6 +38,7 @@
 import android.os.Looper;
 import android.os.ServiceManager;
 import android.os.SystemProperties;
+import android.provider.DeviceConfig_host;
 import android.system.ErrnoException;
 import android.system.Os;
 import android.util.Log;
@@ -335,6 +336,8 @@
         }
         android.os.Process.reset$ravenwood();
 
+        DeviceConfig_host.reset();
+
         try {
             ResourcesManager.setInstance(null); // Better structure needed.
         } catch (Exception e) {
diff --git a/ravenwood/runtime-helper-src/framework/android/provider/DeviceConfig_host.java b/ravenwood/runtime-helper-src/framework/android/provider/DeviceConfig_host.java
new file mode 100644
index 0000000..9c2188f
--- /dev/null
+++ b/ravenwood/runtime-helper-src/framework/android/provider/DeviceConfig_host.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2024 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 android.provider;
+
+public class DeviceConfig_host {
+
+    /**
+     * Called by Ravenwood runtime to reset all local changes.
+     */
+    public static void reset() {
+        RavenwoodConfigDataStore.getInstance().clearAll();
+    }
+
+    /**
+     * Called by {@link DeviceConfig#newDataStore()}
+     */
+    public static DeviceConfigDataStore newDataStore() {
+        return RavenwoodConfigDataStore.getInstance();
+    }
+}
diff --git a/ravenwood/runtime-helper-src/framework/android/provider/RavenwoodConfigDataStore.java b/ravenwood/runtime-helper-src/framework/android/provider/RavenwoodConfigDataStore.java
new file mode 100644
index 0000000..4bc3de7
--- /dev/null
+++ b/ravenwood/runtime-helper-src/framework/android/provider/RavenwoodConfigDataStore.java
@@ -0,0 +1,253 @@
+/*
+ * Copyright (C) 2024 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 android.provider;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.ContentResolver;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Looper;
+import android.provider.DeviceConfig.BadConfigException;
+import android.provider.DeviceConfig.MonitorCallback;
+import android.provider.DeviceConfig.Properties;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.Executor;
+
+/**
+ * {@link DeviceConfigDataStore} used only on Ravenwood.
+ *
+ * TODO(b/368591527) Support monitor callback related features
+ * TODO(b/368591527) Support "default" related features
+ */
+final class RavenwoodConfigDataStore implements DeviceConfigDataStore {
+    private static final RavenwoodConfigDataStore sInstance = new RavenwoodConfigDataStore();
+
+    private final Object mLock = new Object();
+
+    @GuardedBy("mLock")
+    private int mSyncDisabledMode = DeviceConfig.SYNC_DISABLED_MODE_NONE;
+
+    @GuardedBy("mLock")
+    private final HashMap<String, HashMap<String, String>> mStore = new HashMap<>();
+
+    private record ObserverInfo(String namespace, ContentObserver observer) {
+    }
+
+    @GuardedBy("mLock")
+    private final ArrayList<ObserverInfo> mObservers = new ArrayList<>();
+
+    static RavenwoodConfigDataStore getInstance() {
+        return sInstance;
+    }
+
+    private static void shouldNotBeCalled() {
+        throw new RuntimeException("shouldNotBeCalled");
+    }
+
+    void clearAll() {
+        synchronized (mLock) {
+            mSyncDisabledMode = DeviceConfig.SYNC_DISABLED_MODE_NONE;
+            mStore.clear();
+        }
+    }
+
+    @GuardedBy("mLock")
+    private HashMap<String, String> getNamespaceLocked(@NonNull String namespace) {
+        Objects.requireNonNull(namespace);
+        return mStore.computeIfAbsent(namespace, k -> new HashMap<>());
+    }
+
+    /** {@inheritDoc} */
+    @NonNull
+    @Override
+    public Map<String, String> getAllProperties() {
+        synchronized (mLock) {
+            var ret = new HashMap<String, String>();
+
+            for (var namespaceAndMap : mStore.entrySet()) {
+                for (var nameAndValue : namespaceAndMap.getValue().entrySet()) {
+                    ret.put(namespaceAndMap.getKey() + "/" + nameAndValue.getKey(),
+                            nameAndValue.getValue());
+                }
+            }
+            return ret;
+        }
+    }
+
+    /** {@inheritDoc} */
+    @NonNull
+    @Override
+    public Properties getProperties(@NonNull String namespace, @NonNull String... names) {
+        Objects.requireNonNull(namespace);
+
+        synchronized (mLock) {
+            var namespaceMap = getNamespaceLocked(namespace);
+
+            if (names.length == 0) {
+                return new Properties(namespace, Map.copyOf(namespaceMap));
+            } else {
+                var map = new HashMap<String, String>();
+                for (var name : names) {
+                    Objects.requireNonNull(name);
+                    map.put(name, namespaceMap.get(name));
+                }
+                return new Properties(namespace, map);
+            }
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean setProperties(@NonNull Properties properties) throws BadConfigException {
+        Objects.requireNonNull(properties);
+
+        synchronized (mLock) {
+            var namespaceMap = getNamespaceLocked(properties.getNamespace());
+            for (var kv : properties.getPropertyValues().entrySet()) {
+                namespaceMap.put(
+                        Objects.requireNonNull(kv.getKey()),
+                        Objects.requireNonNull(kv.getValue())
+                );
+            }
+            notifyObserversLock(properties.getNamespace(),
+                    properties.getKeyset().toArray(new String[0]));
+        }
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean setProperty(@NonNull String namespace, @NonNull String name,
+            @Nullable String value, boolean makeDefault) {
+        Objects.requireNonNull(namespace);
+        Objects.requireNonNull(name);
+
+        synchronized (mLock) {
+            var namespaceMap = getNamespaceLocked(namespace);
+            namespaceMap.put(name, value);
+
+            // makeDefault not supported.
+            notifyObserversLock(namespace, new String[]{name});
+        }
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean deleteProperty(@NonNull String namespace, @NonNull String name) {
+        Objects.requireNonNull(namespace);
+        Objects.requireNonNull(name);
+
+        synchronized (mLock) {
+            var namespaceMap = getNamespaceLocked(namespace);
+            if (namespaceMap.remove(name) != null) {
+                notifyObserversLock(namespace, new String[]{name});
+            }
+        }
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void resetToDefaults(int resetMode, @Nullable String namespace) {
+        // not supported in DeviceConfig.java
+        shouldNotBeCalled();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void setSyncDisabledMode(int syncDisabledMode) {
+        synchronized (mLock) {
+            mSyncDisabledMode = syncDisabledMode;
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public int getSyncDisabledMode() {
+        synchronized (mLock) {
+            return mSyncDisabledMode;
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void setMonitorCallback(@NonNull ContentResolver resolver, @NonNull Executor executor,
+            @NonNull MonitorCallback callback) {
+        // not supported in DeviceConfig.java
+        shouldNotBeCalled();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void clearMonitorCallback(@NonNull ContentResolver resolver) {
+        // not supported in DeviceConfig.java
+        shouldNotBeCalled();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void registerContentObserver(@NonNull String namespace, boolean notifyForescendants,
+            ContentObserver contentObserver) {
+        synchronized (mLock) {
+            mObservers.add(new ObserverInfo(
+                    Objects.requireNonNull(namespace),
+                    Objects.requireNonNull(contentObserver)
+            ));
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void unregisterContentObserver(@NonNull ContentObserver contentObserver) {
+        synchronized (mLock) {
+            for (int i = mObservers.size() - 1; i >= 0; i--) {
+                if (mObservers.get(i).observer == contentObserver) {
+                    mObservers.remove(i);
+                }
+            }
+        }
+    }
+
+    private static final Uri CONTENT_URI = Uri.parse("content://settings/config");
+
+    @GuardedBy("mLock")
+    private void notifyObserversLock(@NonNull String namespace, String[] keys) {
+        var urib = CONTENT_URI.buildUpon().appendPath(namespace);
+        for (var key : keys) {
+            urib.appendEncodedPath(key);
+        }
+        var uri = urib.build();
+
+        final var copy = List.copyOf(mObservers);
+        new Handler(Looper.getMainLooper()).post(() -> {
+            for (int i = copy.size() - 1; i >= 0; i--) {
+                if (copy.get(i).namespace.equals(namespace)) {
+                    copy.get(i).observer.dispatchChange(false, uri);
+                }
+            }
+        });
+    }
+}