Create shared memory region for framework APIs

System server owns a read-write mapping to this region.
App processes are initialized with a read-only mapping.

Shared memory can be used to quickly read information that's
maintained by system server from any app process, without the
overhead of Binder, where the additional features offered by
Binder IPC are not required.

Demonstrate the use of shared memory to implement network clock.

go/ApplicationSharedMemory-bluedoc

Bug: 361329788
Bug: 365575551
Change-Id: Ic7e8231355392880b34a659e8531b3bba0614b27
Flag: com.android.internal.os.application_shared_memory_enabled
Flag: android.os.network_time_uses_shared_memory
Tested: ApplicationSharedMemoryTest
Tested: SystemClockNetworkTimeTest
NO_IFTTT=adding IFTTT
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index 3bc3a93..5b556cc 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -230,6 +230,7 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.app.IVoiceInteractor;
 import com.android.internal.content.ReferrerIntent;
+import com.android.internal.os.ApplicationSharedMemory;
 import com.android.internal.os.BinderCallsStats;
 import com.android.internal.os.BinderInternal;
 import com.android.internal.os.DebugStore;
@@ -1301,6 +1302,7 @@
                 long[] disabledCompatChanges,
                 long[] loggableCompatChanges,
                 SharedMemory serializedSystemFontMap,
+                FileDescriptor applicationSharedMemoryFd,
                 long startRequestedElapsedTime,
                 long startRequestedUptime) {
             if (services != null) {
@@ -1329,6 +1331,16 @@
                 ServiceManager.initServiceCache(services);
             }
 
+            // This must be initialized as early as possible to ensure availability for any
+            // downstream callers.
+            if (com.android.internal.os.Flags.applicationSharedMemoryEnabled()) {
+                ApplicationSharedMemory instance =
+                        ApplicationSharedMemory.fromFileDescriptor(
+                                applicationSharedMemoryFd, /* mutable= */ false);
+                instance.closeFileDescriptor();
+                ApplicationSharedMemory.setInstance(instance);
+            }
+
             setCoreSettings(coreSettings);
 
             AppBindData data = new AppBindData();
diff --git a/core/java/android/app/IApplicationThread.aidl b/core/java/android/app/IApplicationThread.aidl
index 9f3829e..06d01ec 100644
--- a/core/java/android/app/IApplicationThread.aidl
+++ b/core/java/android/app/IApplicationThread.aidl
@@ -92,6 +92,7 @@
             in Bundle coreSettings, in String buildSerial, in AutofillOptions autofillOptions,
             in ContentCaptureOptions contentCaptureOptions, in long[] disabledCompatChanges,
             in long[] loggableCompatChanges, in SharedMemory serializedSystemFontMap,
+            in FileDescriptor applicationSharedMemoryFd,
             long startRequestedElapsedTime, long startRequestedUptime);
     void runIsolatedEntryPoint(in String entryPoint, in String[] entryPointArgs);
     void scheduleExit();
diff --git a/core/java/android/os/SystemClock.java b/core/java/android/os/SystemClock.java
index 4c9a02c..dfc591b 100644
--- a/core/java/android/os/SystemClock.java
+++ b/core/java/android/os/SystemClock.java
@@ -28,6 +28,8 @@
 import android.text.format.DateUtils;
 import android.util.Slog;
 
+import com.android.internal.os.ApplicationSharedMemory;
+
 import dalvik.annotation.optimization.CriticalNative;
 
 import java.time.Clock;
@@ -323,74 +325,74 @@
     }
 
     /**
-     * Returns milliseconds since January 1, 1970 00:00:00.0 UTC, synchronized
-     * using a remote network source outside the device.
-     * <p>
-     * While the time returned by {@link System#currentTimeMillis()} can be
-     * adjusted by the user, the time returned by this method cannot be adjusted
-     * by the user.
-     * <p>
-     * This performs no blocking network operations and returns values based on
-     * a recent successful synchronization event; it will either return a valid
-     * time or throw.
-     * <p>
-     * Note that synchronization may occur using an insecure network protocol,
-     * so the returned time should not be used for security purposes.
-     * The device may resynchronize with the same or different network source
-     * at any time. Due to network delays, variations between servers, or local
-     * (client side) clock drift, the accuracy of the returned times cannot be
-     * guaranteed. In extreme cases, consecutive calls to {@link
-     * #currentNetworkTimeMillis(ITimeDetectorService)} could return times that
-     * are out of order.
+     * Returns milliseconds since January 1, 1970 00:00:00.0 UTC, synchronized using a remote
+     * network source outside the device.
+     *
+     * <p>While the time returned by {@link System#currentTimeMillis()} can be adjusted by the user,
+     * the time returned by this method cannot be adjusted by the user.
+     *
+     * <p>This performs no blocking network operations and returns values based on a recent
+     * successful synchronization event; it will either return a valid time or throw.
+     *
+     * <p>Note that synchronization may occur using an insecure network protocol, so the returned
+     * time should not be used for security purposes. The device may resynchronize with the same or
+     * different network source at any time. Due to network delays, variations between servers, or
+     * local (client side) clock drift, the accuracy of the returned times cannot be guaranteed. In
+     * extreme cases, consecutive calls to {@link #currentNetworkTimeMillis()} could return times
+     * that are out of order.
      *
      * @throws DateTimeException when no network time can be provided.
      * @hide
      */
     public static long currentNetworkTimeMillis() {
-        ITimeDetectorService timeDetectorService = getITimeDetectorService();
-        if (timeDetectorService == null) {
-            throw new RuntimeException(new DeadSystemException());
-        }
+        if (com.android.internal.os.Flags.applicationSharedMemoryEnabled()
+                && Flags.networkTimeUsesSharedMemory()) {
+            final long latestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis =
+                    ApplicationSharedMemory.getInstance()
+                            .getLatestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis();
+            return latestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis + elapsedRealtime();
+        } else {
+            ITimeDetectorService timeDetectorService = getITimeDetectorService();
+            if (timeDetectorService == null) {
+                throw new RuntimeException(new DeadSystemException());
+            }
 
-        UnixEpochTime time;
-        try {
-            time = timeDetectorService.latestNetworkTime();
-        } catch (ParcelableException e) {
-            e.maybeRethrow(DateTimeException.class);
-            throw new RuntimeException(e);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
-        if (time == null) {
-            // This is not expected.
-            throw new DateTimeException("Network based time is not available.");
-        }
+            UnixEpochTime time;
+            try {
+                time = timeDetectorService.latestNetworkTime();
+            } catch (ParcelableException e) {
+                e.maybeRethrow(DateTimeException.class);
+                throw new RuntimeException(e);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+            if (time == null) {
+                // This is not expected.
+                throw new DateTimeException("Network based time is not available.");
+            }
 
-        long currentMillis = elapsedRealtime();
-        long deltaMs = currentMillis - time.getElapsedRealtimeMillis();
-        return time.getUnixEpochTimeMillis() + deltaMs;
+            long currentMillis = elapsedRealtime();
+            long deltaMs = currentMillis - time.getElapsedRealtimeMillis();
+            return time.getUnixEpochTimeMillis() + deltaMs;
+        }
     }
 
-   /**
-     * Returns a {@link Clock} that starts at January 1, 1970 00:00:00.0 UTC,
-     * synchronized using a remote network source outside the device.
-     * <p>
-     * While the time returned by {@link System#currentTimeMillis()} can be
-     * adjusted by the user, the time returned by this method cannot be adjusted
-     * by the user.
-     * <p>
-     * This performs no blocking network operations and returns values based on
-     * a recent successful synchronization event; it will either return a valid
-     * time or throw.
-     * <p>
-     * Note that synchronization may occur using an insecure network protocol,
-     * so the returned time should not be used for security purposes.
-     * The device may resynchronize with the same or different network source
-     * at any time. Due to network delays, variations between servers, or local
-     * (client side) clock drift, the accuracy of the returned times cannot be
-     * guaranteed. In extreme cases, consecutive calls to {@link
-     * Clock#millis()} on the returned {@link Clock} could return times that are
-     * out of order.
+    /**
+     * Returns a {@link Clock} that starts at January 1, 1970 00:00:00.0 UTC, synchronized using a
+     * remote network source outside the device.
+     *
+     * <p>While the time returned by {@link System#currentTimeMillis()} can be adjusted by the user,
+     * the time returned by this method cannot be adjusted by the user.
+     *
+     * <p>This performs no blocking network operations and returns values based on a recent
+     * successful synchronization event; it will either return a valid time or throw.
+     *
+     * <p>Note that synchronization may occur using an insecure network protocol, so the returned
+     * time should not be used for security purposes. The device may resynchronize with the same or
+     * different network source at any time. Due to network delays, variations between servers, or
+     * local (client side) clock drift, the accuracy of the returned times cannot be guaranteed. In
+     * extreme cases, consecutive calls to {@link Clock#millis()} on the returned {@link Clock}
+     * could return times that are out of order.
      *
      * @throws DateTimeException when no network time can be provided.
      */
diff --git a/core/java/android/os/flags.aconfig b/core/java/android/os/flags.aconfig
index f670601..a1bfe39 100644
--- a/core/java/android/os/flags.aconfig
+++ b/core/java/android/os/flags.aconfig
@@ -224,3 +224,11 @@
      is_exported: true
      bug: "366598445"
 }
+
+flag {
+    name: "network_time_uses_shared_memory"
+    namespace: "system_performance"
+    description: "SystemClock.currentNetworkTimeMillis() reads network time offset from shared memory"
+    bug: "361329788"
+    is_exported: true
+}
diff --git a/core/java/com/android/internal/os/ApplicationSharedMemory.java b/core/java/com/android/internal/os/ApplicationSharedMemory.java
new file mode 100644
index 0000000..84f713e
--- /dev/null
+++ b/core/java/com/android/internal/os/ApplicationSharedMemory.java
@@ -0,0 +1,296 @@
+/*
+ * 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 com.android.internal.os;
+
+import android.annotation.NonNull;
+import android.util.Log;
+import com.android.internal.annotations.VisibleForTesting;
+
+import dalvik.annotation.optimization.CriticalNative;
+
+import libcore.io.IoUtils;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.time.DateTimeException;
+
+/**
+ * This class is used to create and access a shared memory region.
+ *
+ * <p>The intended use case is that memory is shared between system processes and application
+ * processes such that it's readable to all apps and writable only to system processes.
+ *
+ * <p>This shared memory region can be used as an alternative to Binder IPC for driving
+ * communication between system processes and application processes at a lower latency and higher
+ * throughput than Binder IPC can provide, under circumstances where the additional features of
+ * Binder IPC are not required.
+ *
+ * <p>Unlike Binder IPC, shared memory doesn't support synchronous transactions and associated
+ * ordering guarantees, client identity (and therefore caller permission checking), and access
+ * auditing. Therefore it's not a suitable alternative to Binder IPC for most use cases.
+ *
+ * <p>Additionally, because the intended use case is to make this shared memory region readable to
+ * all apps, it's not suitable for sharing sensitive data.
+ *
+ * @see {@link ApplicationSharedMemoryTestRule} for unit testing support.
+ * @hide
+ */
+public class ApplicationSharedMemory implements AutoCloseable {
+
+    // LINT.IfChange(invalid_network_time)
+    public static final long INVALID_NETWORK_TIME = -1;
+    // LINT.ThenChange(frameworks/base/core/jni/com_android_internal_os_ApplicationSharedMemory.cpp:invalid_network_time)
+
+    private static final boolean DEBUG = false;
+    private static final String LOG_TAG = "ApplicationSharedMemory";
+
+    @VisibleForTesting public static ApplicationSharedMemory sInstance;
+
+    /** Get the process-global instance. */
+    public static ApplicationSharedMemory getInstance() {
+        ApplicationSharedMemory instance = sInstance;
+        if (instance == null) {
+            throw new IllegalStateException("ApplicationSharedMemory not initialized");
+        }
+        return instance;
+    }
+
+    /** Set the process-global instance. */
+    public static void setInstance(ApplicationSharedMemory instance) {
+        if (DEBUG) {
+            Log.d(LOG_TAG, "setInstance: " + instance);
+        }
+
+        if (sInstance != null) {
+            throw new IllegalStateException("ApplicationSharedMemory already initialized");
+        }
+        sInstance = instance;
+    }
+
+    /** Allocate mutable shared memory region. */
+    public static ApplicationSharedMemory create() {
+        if (DEBUG) {
+            Log.d(LOG_TAG, "create");
+        }
+
+        int fd = nativeCreate();
+        FileDescriptor fileDescriptor = new FileDescriptor();
+        fileDescriptor.setInt$(fd);
+
+        final boolean mutable = true;
+        long ptr = nativeMap(fd, mutable);
+        nativeInit(ptr);
+
+        return new ApplicationSharedMemory(fileDescriptor, mutable, ptr);
+    }
+
+    /**
+     * Open shared memory region from a given {@link FileDescriptor}.
+     *
+     * @param fileDescriptor Handle to shared memory region.
+     * @param mutable Whether the shared memory region is mutable. If true, will be mapped as
+     *     read-write memory. If false, will be mapped as read-only memory. Passing true (mutable)
+     *     if |pfd| is a handle to read-only memory will result in undefined behavior.
+     */
+    public static ApplicationSharedMemory fromFileDescriptor(
+            @NonNull FileDescriptor fileDescriptor, boolean mutable) {
+        if (DEBUG) {
+            Log.d(LOG_TAG, "fromFileDescriptor: " + fileDescriptor + " mutable: " + mutable);
+        }
+
+        long ptr = nativeMap(fileDescriptor.getInt$(), mutable);
+        return new ApplicationSharedMemory(fileDescriptor, mutable, ptr);
+    }
+
+    /**
+     * Allocate read-write shared memory region.
+     *
+     * @return File descriptor of the shared memory region.
+     */
+    private static native int nativeCreate();
+
+    /**
+     * Map the shared memory region.
+     *
+     * @param fd File descriptor of the shared memory region.
+     * @param isMutable Whether the shared memory region is mutable. If true, will be mapped as
+     *     read-write memory. If false, will be mapped as read-only memory.
+     * @return Pointer to the mapped shared memory region.
+     */
+    private static native long nativeMap(int fd, boolean isMutable);
+
+    /**
+     * Initialize read-write shared memory region.
+     *
+     * @param Pointer to the mapped shared memory region.
+     */
+    private static native void nativeInit(long ptr);
+
+    /**
+     * Unmap the shared memory region.
+     *
+     * @param ptr Pointer to the mapped shared memory region.
+     */
+    private static native void nativeUnmap(long ptr);
+
+    /**
+     * If true, this object owns the read-write instance of the shared memory region. If false, this
+     * object can only read.
+     */
+    private final boolean mMutable;
+
+    /**
+     * Handle to the shared memory region. This can be send to other processes over Binder calls or
+     * Intent extras. Recipients can use this handle to obtain read-only access to the shared memory
+     * region.
+     */
+    private FileDescriptor mFileDescriptor;
+
+    /** Native pointer to the mapped shared memory region. */
+    private volatile long mPtr;
+
+    ApplicationSharedMemory(@NonNull FileDescriptor fileDescriptor, boolean mutable, long ptr) {
+        mFileDescriptor = fileDescriptor;
+        mMutable = mutable;
+        mPtr = ptr;
+    }
+
+    /**
+     * Returns the file descriptor of the shared memory region.
+     *
+     * <p>This file descriptor retains the mutability properties of this object instance, and can be
+     * sent over Binder IPC or Intent extras to another process to allow the remote process to map
+     * the same shared memory region with the same access rights.
+     *
+     * @throws IllegalStateException if the file descriptor is closed.
+     */
+    public FileDescriptor getFileDescriptor() {
+        checkFileOpen();
+        return mFileDescriptor;
+    }
+
+    /**
+     * Returns a read-only file descriptor of the shared memory region. This object can be sent over
+     * Binder IPC or Intent extras to another process to allow the remote process to map the same
+     * shared memory region with read-only access.
+     *
+     * @return a read-only handle to the shared memory region.
+     * @throws IllegalStateException if the file descriptor is closed.
+     */
+    public FileDescriptor getReadOnlyFileDescriptor() throws IOException {
+        checkFileOpen();
+        FileDescriptor readOnlyFileDescriptor = new FileDescriptor();
+        int readOnlyFd = nativeDupAsReadOnly(mFileDescriptor.getInt$());
+        readOnlyFileDescriptor.setInt$(readOnlyFd);
+        return readOnlyFileDescriptor;
+    }
+
+    /** Return a read-only duplicate of the file descriptor. */
+    private static native int nativeDupAsReadOnly(int fd);
+
+    /** Set the latest network Unix Epoch minus realtime millis. */
+    public void setLatestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis(long offset) {
+        checkMutable();
+        nativeSetLatestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis(mPtr, offset);
+    }
+
+    /** Clear the latest network Unix Epoch minus realtime millis. */
+    public void clearLatestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis() {
+        checkMutable();
+        nativeSetLatestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis(
+                mPtr, INVALID_NETWORK_TIME);
+    }
+
+    @CriticalNative
+    private static native void nativeSetLatestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis(
+            long ptr, long offset);
+
+    /**
+     * Get the latest network Unix Epoch minus realtime millis.
+     *
+     * @throws DateTimeException when no network time can be provided.
+     */
+    public long getLatestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis()
+            throws DateTimeException {
+        checkMapped();
+        long offset = nativeGetLatestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis(mPtr);
+        if (offset == INVALID_NETWORK_TIME) {
+            throw new DateTimeException("No network time available");
+        }
+        return offset;
+    }
+
+    @CriticalNative
+    public static native long nativeGetLatestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis(
+            long ptr);
+
+    /**
+     * Close the associated file descriptor.
+     *
+     * <p>This method is safe to call if you never intend to pass the file descriptor to another
+     * process, whether via {@link #getFileDescriptor()} or {@link #getReadOnlyFileDescriptor()}.
+     * After calling this method, subsequent calls to {@link #getFileDescriptor()} or {@link
+     * #getReadOnlyFileDescriptor()} will throw an {@link IllegalStateException}.
+     */
+    public void closeFileDescriptor() {
+        if (mFileDescriptor != null) {
+            IoUtils.closeQuietly(mFileDescriptor);
+            mFileDescriptor = null;
+        }
+    }
+
+    public void close() {
+        if (mPtr != 0) {
+            nativeUnmap(mPtr);
+            mPtr = 0;
+        }
+
+        if (mFileDescriptor != null) {
+            IoUtils.closeQuietly(mFileDescriptor);
+            mFileDescriptor = null;
+        }
+    }
+
+    private void checkFileOpen() {
+        if (mFileDescriptor == null) {
+            throw new IllegalStateException("File descriptor is closed");
+        }
+    }
+
+    /**
+     * Check that the shared memory region is mapped.
+     *
+     * @throws IllegalStateException if the shared memory region is not mapped.
+     */
+    private void checkMapped() {
+        if (mPtr == 0) {
+            throw new IllegalStateException("Instance is closed");
+        }
+    }
+
+    /**
+     * Check that the shared memory region is mapped and mutable.
+     *
+     * @throws IllegalStateException if the shared memory region is not mapped or not mutable.
+     */
+    private void checkMutable() {
+        checkMapped();
+        if (!mMutable) {
+            throw new IllegalStateException("Not mutable");
+        }
+    }
+}
diff --git a/core/java/com/android/internal/os/OWNERS b/core/java/com/android/internal/os/OWNERS
index 391d257..ffd4499 100644
--- a/core/java/com/android/internal/os/OWNERS
+++ b/core/java/com/android/internal/os/OWNERS
@@ -15,3 +15,6 @@
 # ANRs
 # Bug component : 158088 = per-file TimeoutRecord.java
 per-file TimeoutRecord.java = file:/PERFORMANCE_OWNERS
+
+# ApplicationSharedMemory
+per-file *ApplicationSharedMemory* = file:/PERFORMANCE_OWNERS
diff --git a/core/java/com/android/internal/os/flags.aconfig b/core/java/com/android/internal/os/flags.aconfig
index c7117e9..07df248 100644
--- a/core/java/com/android/internal/os/flags.aconfig
+++ b/core/java/com/android/internal/os/flags.aconfig
@@ -27,4 +27,12 @@
     description: "If the debug store is enabled."
     bug: "314735374"
     is_fixed_read_only: true
+}
+
+flag {
+    name: "application_shared_memory_enabled"
+    namespace: "system_performance"
+    description: "Whether ApplicationSharedMemory is enabled."
+    bug: "365575551"
+    is_fixed_read_only: true
 }
\ No newline at end of file
diff --git a/core/jni/Android.bp b/core/jni/Android.bp
index 9a4ff8f..9797d96 100644
--- a/core/jni/Android.bp
+++ b/core/jni/Android.bp
@@ -258,6 +258,7 @@
                 "com_android_internal_content_om_OverlayConfig.cpp",
                 "com_android_internal_content_om_OverlayManagerImpl.cpp",
                 "com_android_internal_net_NetworkUtilsInternal.cpp",
+                "com_android_internal_os_ApplicationSharedMemory.cpp",
                 "com_android_internal_os_ClassLoaderFactory.cpp",
                 "com_android_internal_os_DebugStore.cpp",
                 "com_android_internal_os_FuseAppLoop.cpp",
diff --git a/core/jni/AndroidRuntime.cpp b/core/jni/AndroidRuntime.cpp
index 03b5143a..70a80b2 100644
--- a/core/jni/AndroidRuntime.cpp
+++ b/core/jni/AndroidRuntime.cpp
@@ -201,6 +201,7 @@
 extern int register_com_android_internal_content_om_OverlayConfig(JNIEnv *env);
 extern int register_com_android_internal_content_om_OverlayManagerImpl(JNIEnv* env);
 extern int register_com_android_internal_net_NetworkUtilsInternal(JNIEnv* env);
+extern int register_com_android_internal_os_ApplicationSharedMemory(JNIEnv *env);
 extern int register_com_android_internal_os_ClassLoaderFactory(JNIEnv* env);
 extern int register_com_android_internal_os_DebugStore(JNIEnv* env);
 extern int register_com_android_internal_os_FuseAppLoop(JNIEnv* env);
@@ -1516,6 +1517,7 @@
 }
 
 static const RegJNIRec gRegJNI[] = {
+        REG_JNI(register_com_android_internal_os_ApplicationSharedMemory),
         REG_JNI(register_com_android_internal_os_RuntimeInit),
         REG_JNI(register_com_android_internal_os_ZygoteInit_nativeZygoteInit),
         REG_JNI(register_android_os_SystemClock),
diff --git a/core/jni/OWNERS b/core/jni/OWNERS
index c0fe098..af10623 100644
--- a/core/jni/OWNERS
+++ b/core/jni/OWNERS
@@ -116,3 +116,6 @@
 
 # IF Tools
 per-file android_tracing_Perfetto* = file:platform/development:/tools/winscope/OWNERS
+
+# ApplicationSharedMemory
+per-file *ApplicationSharedMemory* = file:/PERFORMANCE_OWNERS
diff --git a/core/jni/com_android_internal_os_ApplicationSharedMemory.cpp b/core/jni/com_android_internal_os_ApplicationSharedMemory.cpp
new file mode 100644
index 0000000..453e539
--- /dev/null
+++ b/core/jni/com_android_internal_os_ApplicationSharedMemory.cpp
@@ -0,0 +1,164 @@
+/*
+ * 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.
+ */
+
+// See: ApplicationSharedMemory.md
+
+#include <cutils/ashmem.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <nativehelper/JNIHelp.h>
+#include <string.h>
+#include <sys/mman.h>
+
+#include <atomic>
+#include <cstddef>
+#include <new>
+
+#include "core_jni_helpers.h"
+
+namespace {
+
+// Atomics should be safe to use across processes if they are lock free.
+static_assert(std::atomic<int64_t>::is_always_lock_free == true,
+              "atomic<int64_t> is not always lock free");
+
+// This is the data structure that is shared between processes.
+//
+// Tips for extending:
+// - Atomics are safe for cross-process use as they are lock free, if they are accessed as
+//   individual values.
+// - Consider multi-ABI systems, e.g. devices that support launching both 64-bit and 32-bit
+//   app processes. Use fixed-size types (e.g. `int64_t`) to ensure that the data structure is
+//   the same size across all ABIs. Avoid implicit assumptions about struct packing/padding.
+class alignas(8) SharedMemory { // Ensure that `sizeof(SharedMemory)` is the same across 32-bit and
+                                // 64-bit systems.
+private:
+    volatile std::atomic<int64_t> latestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis;
+
+    // LINT.IfChange(invalid_network_time)
+    static constexpr int64_t INVALID_NETWORK_TIME = -1;
+    // LINT.ThenChange(frameworks/base/core/java/com/android/internal/os/ApplicationSharedMemory.java:invalid_network_time)
+
+public:
+    // Default constructor sets initial values
+    SharedMemory()
+          : latestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis(INVALID_NETWORK_TIME) {}
+
+    int64_t getLatestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis() const {
+        return latestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis;
+    }
+
+    void setLatestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis(int64_t offset) {
+        latestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis = offset;
+    }
+};
+
+// Update the expected value when modifying the members of SharedMemory.
+// The goal of this assertion is to ensure that the data structure is the same size across 32-bit
+// and 64-bit systems.
+static_assert(sizeof(SharedMemory) == 8, "Unexpected SharedMemory size");
+
+static jint nativeCreate(JNIEnv* env, jclass) {
+    // Create anonymous shared memory region
+    int fd = ashmem_create_region("ApplicationSharedMemory", sizeof(SharedMemory));
+    if (fd < 0) {
+        jniThrowExceptionFmt(env, "java/lang/RuntimeException", "Failed to create ashmem: %s",
+                             strerror(errno));
+    }
+    return fd;
+}
+
+static jlong nativeMap(JNIEnv* env, jclass, jint fd, jboolean isMutable) {
+    void* ptr = mmap(nullptr, sizeof(SharedMemory), isMutable ? PROT_READ | PROT_WRITE : PROT_READ,
+                     MAP_SHARED, fd, 0);
+    if (ptr == MAP_FAILED) {
+        close(fd);
+        jniThrowExceptionFmt(env, "java/lang/RuntimeException", "Failed to mmap shared memory: %s",
+                             strerror(errno));
+    }
+
+    return reinterpret_cast<jlong>(ptr);
+}
+
+static void nativeInit(JNIEnv* env, jclass, jlong ptr) {
+    new (reinterpret_cast<SharedMemory*>(ptr)) SharedMemory();
+}
+
+static void nativeUnmap(JNIEnv* env, jclass, jlong ptr) {
+    if (munmap(reinterpret_cast<void*>(ptr), sizeof(SharedMemory)) == -1) {
+        jniThrowExceptionFmt(env, "java/lang/RuntimeException",
+                             "Failed to munmap shared memory: %s", strerror(errno));
+    }
+}
+
+static jint nativeDupAsReadOnly(JNIEnv* env, jclass, jint fd) {
+    // Duplicate file descriptor
+    fd = fcntl(fd, F_DUPFD_CLOEXEC, 0);
+    if (fd < 0) {
+        jniThrowExceptionFmt(env, "java/lang/RuntimeException", "Failed to dup fd: %s",
+                             strerror(errno));
+    }
+
+    // Set new file descriptor to read-only
+    if (ashmem_set_prot_region(fd, PROT_READ)) {
+        close(fd);
+        jniThrowExceptionFmt(env, "java/lang/RuntimeException",
+                             "Failed to ashmem_set_prot_region: %s", strerror(errno));
+    }
+
+    return fd;
+}
+
+static void nativeSetLatestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis(jlong ptr,
+                                                                                 jlong offset) {
+    SharedMemory* sharedMemory = reinterpret_cast<SharedMemory*>(ptr);
+    sharedMemory->setLatestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis(offset);
+}
+
+static jlong nativeGetLatestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis(jlong ptr) {
+    SharedMemory* sharedMemory = reinterpret_cast<SharedMemory*>(ptr);
+    return sharedMemory->getLatestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis();
+}
+
+static const JNINativeMethod gMethods[] = {
+        {"nativeCreate", "()I", (void*)nativeCreate},
+        {"nativeMap", "(IZ)J", (void*)nativeMap},
+        {"nativeInit", "(J)V", (void*)nativeInit},
+        {"nativeUnmap", "(J)V", (void*)nativeUnmap},
+        {"nativeDupAsReadOnly", "(I)I", (void*)nativeDupAsReadOnly},
+        {"nativeSetLatestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis", "(JJ)V",
+         (void*)nativeSetLatestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis},
+        {"nativeGetLatestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis", "(J)J",
+         (void*)nativeGetLatestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis},
+};
+
+} // anonymous namespace
+
+namespace android {
+
+static const char kApplicationSharedMemoryClassName[] =
+        "com/android/internal/os/ApplicationSharedMemory";
+static jclass gApplicationSharedMemoryClass;
+
+int register_com_android_internal_os_ApplicationSharedMemory(JNIEnv* env) {
+    gApplicationSharedMemoryClass =
+            MakeGlobalRefOrDie(env, FindClassOrDie(env, kApplicationSharedMemoryClassName));
+    RegisterMethodsOrDie(env, "com/android/internal/os/ApplicationSharedMemory", gMethods,
+                         NELEM(gMethods));
+    return JNI_OK;
+}
+
+} // namespace android
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 414a4e6..979f3d6 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -397,6 +397,7 @@
 import com.android.internal.content.InstallLocationUtils;
 import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
 import com.android.internal.notification.SystemNotificationChannels;
+import com.android.internal.os.ApplicationSharedMemory;
 import com.android.internal.os.BackgroundThread;
 import com.android.internal.os.BinderCallHeavyHitterWatcher.BinderCallHeavyHitterListener;
 import com.android.internal.os.BinderCallHeavyHitterWatcher.HeavyHitterContainer;
@@ -836,6 +837,8 @@
     @GuardedBy("this")
     final ComponentAliasResolver mComponentAliasResolver;
 
+    final FileDescriptor mApplicationSharedMemoryReadOnlyFd;
+
     private static final long HOME_LAUNCH_TIMEOUT_MS = 15000;
     private final AtomicBoolean mHasHomeDelay = new AtomicBoolean(false);
 
@@ -2412,6 +2415,7 @@
         mBroadcastQueue = injector.getBroadcastQueue(this);
         mBroadcastController = new BroadcastController(mContext, this, mBroadcastQueue);
         mComponentAliasResolver = new ComponentAliasResolver(this);
+        mApplicationSharedMemoryReadOnlyFd = null;
     }
 
     // Note: This method is invoked on the main thread but may need to attach various
@@ -2518,6 +2522,13 @@
         mPendingStartActivityUids = new PendingStartActivityUids();
         mTraceErrorLogger = new TraceErrorLogger();
         mComponentAliasResolver = new ComponentAliasResolver(this);
+        try {
+            mApplicationSharedMemoryReadOnlyFd =
+                    ApplicationSharedMemory.getInstance().getReadOnlyFileDescriptor();
+        } catch (IOException e) {
+            Slog.e(TAG, "Failed to get read only fd for shared memory", e);
+            throw new RuntimeException(e);
+        }
     }
 
     void setBroadcastQueueForTest(BroadcastQueue broadcastQueue) {
@@ -4724,6 +4735,7 @@
                         app.getDisabledCompatChanges(),
                         app.getLoggableCompatChanges(),
                         serializedSystemFontMap,
+                        mApplicationSharedMemoryReadOnlyFd,
                         app.getStartElapsedTime(),
                         app.getStartUptime());
             }
diff --git a/services/core/java/com/android/server/timedetector/TimeDetectorStrategyImpl.java b/services/core/java/com/android/server/timedetector/TimeDetectorStrategyImpl.java
index 374dd89..6405353 100644
--- a/services/core/java/com/android/server/timedetector/TimeDetectorStrategyImpl.java
+++ b/services/core/java/com/android/server/timedetector/TimeDetectorStrategyImpl.java
@@ -37,12 +37,14 @@
 import android.app.timedetector.TelephonyTimeSuggestion;
 import android.content.Context;
 import android.os.Handler;
+import android.os.SystemClock;
 import android.util.ArraySet;
 import android.util.IndentingPrintWriter;
 import android.util.Slog;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.os.ApplicationSharedMemory;
 import com.android.server.SystemClockTime;
 import com.android.server.SystemClockTime.TimeConfidence;
 import com.android.server.timezonedetector.ArrayMapWithHistory;
@@ -315,6 +317,17 @@
         // detected time if, for example, the age of all suggestions are considered.
         NetworkTimeSuggestion lastNetworkSuggestion = mLastNetworkSuggestion.get();
         if (lastNetworkSuggestion == null || !lastNetworkSuggestion.equals(suggestion)) {
+            if (com.android.internal.os.Flags.applicationSharedMemoryEnabled()
+                    && android.os.Flags.networkTimeUsesSharedMemory()) {
+                UnixEpochTime networkUnixEpochTime = suggestion.getUnixEpochTime();
+                long lastNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis =
+                        networkUnixEpochTime.getUnixEpochTimeMillis()
+                                - networkUnixEpochTime.getElapsedRealtimeMillis();
+                ApplicationSharedMemory.getInstance()
+                        .setLatestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis(
+                                lastNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis);
+            }
+
             mLastNetworkSuggestion.set(suggestion);
             notifyNetworkTimeUpdateListenersAsynchronously();
         }
@@ -347,8 +360,12 @@
 
     @Override
     public synchronized void clearLatestNetworkSuggestion() {
+        if (com.android.internal.os.Flags.applicationSharedMemoryEnabled()
+                && android.os.Flags.networkTimeUsesSharedMemory()) {
+            ApplicationSharedMemory.getInstance()
+                    .clearLatestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis();
+        }
         mLastNetworkSuggestion.set(null);
-
         notifyNetworkTimeUpdateListenersAsynchronously();
 
         // The loss of network time may change the time signal to use to set the system clock.
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 3b334ec..8863f94 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -103,6 +103,7 @@
 import com.android.internal.R;
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.notification.SystemNotificationChannels;
+import com.android.internal.os.ApplicationSharedMemory;
 import com.android.internal.os.BinderInternal;
 import com.android.internal.os.RuntimeInit;
 import com.android.internal.policy.AttributeCache;
@@ -940,6 +941,12 @@
         // Setup the default WTF handler
         RuntimeInit.setDefaultApplicationWtfHandler(SystemServer::handleEarlySystemWtf);
 
+        // Initialize the application shared memory region.
+        // This needs to happen before any system services are started,
+        // as they may rely on the shared memory region having been initialized.
+        ApplicationSharedMemory instance = ApplicationSharedMemory.create();
+        ApplicationSharedMemory.setInstance(instance);
+
         // Start services.
         try {
             t.traceBegin("StartServices");
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/AsyncProcessStartTest.java b/services/tests/mockingservicestests/src/com/android/server/am/AsyncProcessStartTest.java
index 93066d8..67475335 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/AsyncProcessStartTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/AsyncProcessStartTest.java
@@ -213,7 +213,7 @@
                 any(), any(), any(),
                 any(), any(),
                 any(), any(),
-                any(), any(),
+                any(), any(), any(),
                 anyLong(), anyLong());
 
         final ProcessRecord r = spy(new ProcessRecord(mAms, ai, ai.processName, ai.uid));
@@ -277,7 +277,7 @@
                 null, null,
                 null,
                 null, null, null,
-                null, null, null,
+                null, null, null, null,
                 0, 0);
 
         // Sleep until timeout should have triggered
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/ProcessObserverTest.java b/services/tests/mockingservicestests/src/com/android/server/am/ProcessObserverTest.java
index 014b98c..43becc5 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/ProcessObserverTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/ProcessObserverTest.java
@@ -216,7 +216,7 @@
                 any(), any(), any(),
                 any(), any(),
                 any(), any(),
-                any(), any(),
+                any(), any(), any(),
                 anyLong(), anyLong());
         final ProcessRecord r = spy(new ProcessRecord(mAms, ai, ai.processName, ai.uid));
         r.setPid(myPid());
@@ -265,7 +265,7 @@
                 null, null,
                 null,
                 null, null, null,
-                null, null, null,
+                null, null, null, null,
                 0, 0);
         return app;
     }
diff --git a/services/tests/timetests/Android.bp b/services/tests/timetests/Android.bp
index aae6acc..65a694e 100644
--- a/services/tests/timetests/Android.bp
+++ b/services/tests/timetests/Android.bp
@@ -19,6 +19,7 @@
         "platform-test-annotations",
         "services.core",
         "truth",
+        "ApplicationSharedMemoryTestRule",
     ],
     libs: ["android.test.runner.stubs.system"],
     platform_apis: true,
diff --git a/services/tests/timetests/src/com/android/server/timedetector/TimeDetectorStrategyImplTest.java b/services/tests/timetests/src/com/android/server/timedetector/TimeDetectorStrategyImplTest.java
index c64ec72..3836063 100644
--- a/services/tests/timetests/src/com/android/server/timedetector/TimeDetectorStrategyImplTest.java
+++ b/services/tests/timetests/src/com/android/server/timedetector/TimeDetectorStrategyImplTest.java
@@ -46,6 +46,7 @@
 import android.os.TimestampedValue;
 import android.util.IndentingPrintWriter;
 
+import com.android.internal.os.ApplicationSharedMemoryTestRule;
 import com.android.server.SystemClockTime.TimeConfidence;
 import com.android.server.timedetector.TimeDetectorStrategy.Origin;
 import com.android.server.timezonedetector.StateChangeListener;
@@ -55,6 +56,7 @@
 import junitparams.Parameters;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -125,6 +127,10 @@
                     .setAutoDetectionEnabledSetting(true)
                     .build();
 
+    @Rule
+    public final ApplicationSharedMemoryTestRule mApplicationSharedMemoryTestRule =
+            new ApplicationSharedMemoryTestRule();
+
     private FakeEnvironment mFakeEnvironment;
     private FakeServiceConfigAccessor mFakeServiceConfigAccessorSpy;
     private TimeDetectorStrategyImpl mTimeDetectorStrategy;
diff --git a/tests/Internal/Android.bp b/tests/Internal/Android.bp
index 3e58517..9f35c7b 100644
--- a/tests/Internal/Android.bp
+++ b/tests/Internal/Android.bp
@@ -32,6 +32,27 @@
     test_suites: ["device-tests"],
 }
 
+// Run just ApplicationSharedMemoryTest with ABI override for 32 bits.
+// This is to test that on systems that support multi-ABI,
+// ApplicationSharedMemory works in app processes launched with a different ABI
+// than that of the system processes.
+android_test {
+    name: "ApplicationSharedMemoryTest32",
+    team: "trendy_team_system_performance",
+    srcs: ["src/com/android/internal/os/ApplicationSharedMemoryTest.java"],
+    libs: ["android.test.runner.stubs.system"],
+    static_libs: [
+        "junit",
+        "androidx.test.rules",
+        "platform-test-annotations",
+    ],
+    manifest: "ApplicationSharedMemoryTest32/AndroidManifest.xml",
+    test_config: "ApplicationSharedMemoryTest32/AndroidTest.xml",
+    certificate: "platform",
+    platform_apis: true,
+    test_suites: ["device-tests"],
+}
+
 android_ravenwood_test {
     name: "InternalTestsRavenwood",
     static_libs: [
@@ -45,3 +66,9 @@
     ],
     auto_gen_config: true,
 }
+
+java_test_helper_library {
+    name: "ApplicationSharedMemoryTestRule",
+    srcs: ["src/com/android/internal/os/ApplicationSharedMemoryTestRule.java"],
+    static_libs: ["junit"],
+}
diff --git a/tests/Internal/ApplicationSharedMemoryTest32/AndroidManifest.xml b/tests/Internal/ApplicationSharedMemoryTest32/AndroidManifest.xml
new file mode 100644
index 0000000..4e1058e
--- /dev/null
+++ b/tests/Internal/ApplicationSharedMemoryTest32/AndroidManifest.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2017 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
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+     package="com.android.internal.tests">
+    <application
+        android:use32bitAbi="true"
+        android:multiArch="true">
+        <uses-library android:name="android.test.runner"/>
+    </application>
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+         android:targetPackage="com.android.internal.tests"
+         android:label="Internal Tests"/>
+</manifest>
diff --git a/tests/Internal/ApplicationSharedMemoryTest32/AndroidTest.xml b/tests/Internal/ApplicationSharedMemoryTest32/AndroidTest.xml
new file mode 100644
index 0000000..9bde8b7
--- /dev/null
+++ b/tests/Internal/ApplicationSharedMemoryTest32/AndroidTest.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2017 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
+  -->
+<configuration description="Runs tests for internal classes/utilities.">
+    <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
+        <option name="test-file-name" value="ApplicationSharedMemoryTest32.apk" />
+    </target_preparer>
+
+    <option name="test-suite-tag" value="apct" />
+    <option name="test-suite-tag" value="framework-base-presubmit" />
+    <option name="test-tag" value="InternalTests" />
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="com.android.internal.tests" />
+        <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+    </test>
+
+    <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector">
+        <option name="pull-pattern-keys" value="perfetto_file_path"/>
+        <option name="directory-keys"
+            value="/data/user/0/com.android.internal.tests/files"/>
+        <option name="collect-on-run-ended-only" value="true"/>
+        <option name="clean-up" value="true"/>
+    </metrics_collector>
+</configuration>
\ No newline at end of file
diff --git a/tests/Internal/ApplicationSharedMemoryTest32/OWNERS b/tests/Internal/ApplicationSharedMemoryTest32/OWNERS
new file mode 100644
index 0000000..1ff3fac
--- /dev/null
+++ b/tests/Internal/ApplicationSharedMemoryTest32/OWNERS
@@ -0,0 +1 @@
+include platform/frameworks/base:/PERFORMANCE_OWNERS
\ No newline at end of file
diff --git a/tests/Internal/src/com/android/internal/os/ApplicationSharedMemoryTest.java b/tests/Internal/src/com/android/internal/os/ApplicationSharedMemoryTest.java
new file mode 100644
index 0000000..e3a129f
--- /dev/null
+++ b/tests/Internal/src/com/android/internal/os/ApplicationSharedMemoryTest.java
@@ -0,0 +1,119 @@
+/*
+ * 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 com.android.internal.os;
+
+import java.io.IOException;
+
+import static org.junit.Assert.fail;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assume.assumeTrue;
+
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.junit.Before;
+
+import java.io.FileDescriptor;
+
+/** Tests for {@link TimeoutRecord}. */
+@SmallTest
+@Presubmit
+@RunWith(JUnit4.class)
+public class ApplicationSharedMemoryTest {
+
+    @Before
+    public void setUp() {
+        // Skip tests if the feature under test is disabled.
+        assumeTrue(Flags.applicationSharedMemoryEnabled());
+    }
+
+    /**
+     * Every application process, including ours, should have had an instance installed at this
+     * point.
+     */
+    @Test
+    public void hasInstance() {
+        // This shouldn't throw and shouldn't return null.
+        assertNotNull(ApplicationSharedMemory.getInstance());
+    }
+
+    /** Any app process should be able to read shared memory values. */
+    @Test
+    public void canRead() {
+        ApplicationSharedMemory instance = ApplicationSharedMemory.getInstance();
+        instance.getLatestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis();
+        // Don't actually care about the value of the above.
+    }
+
+    /** Application processes should not have mutable access. */
+    @Test
+    public void appInstanceNotMutable() {
+        ApplicationSharedMemory instance = ApplicationSharedMemory.getInstance();
+        try {
+            instance.setLatestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis(17);
+            fail("Attempted mutation in an app process should throw");
+        } catch (Exception expected) {
+        }
+    }
+
+    /** Instances share memory if they share the underlying memory region. */
+    @Test
+    public void instancesShareMemory() throws IOException {
+        ApplicationSharedMemory instance1 = ApplicationSharedMemory.create();
+        ApplicationSharedMemory instance2 =
+                ApplicationSharedMemory.fromFileDescriptor(
+                        instance1.getFileDescriptor(), /* mutable= */ true);
+        ApplicationSharedMemory instance3 =
+                ApplicationSharedMemory.fromFileDescriptor(
+                        instance2.getReadOnlyFileDescriptor(), /* mutable= */ false);
+
+        instance1.setLatestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis(17);
+        assertEquals(
+                17, instance1.getLatestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis());
+        assertEquals(
+                17, instance2.getLatestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis());
+        assertEquals(
+                17, instance3.getLatestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis());
+
+        instance2.setLatestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis(24);
+        assertEquals(
+                24, instance1.getLatestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis());
+        assertEquals(
+                24, instance2.getLatestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis());
+        assertEquals(
+                24, instance3.getLatestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis());
+    }
+
+    /** Can't map read-only memory as mutable. */
+    @Test
+    public void readOnlyCantBeMutable() throws IOException {
+        ApplicationSharedMemory readWriteInstance = ApplicationSharedMemory.create();
+        FileDescriptor readOnlyFileDescriptor = readWriteInstance.getReadOnlyFileDescriptor();
+
+        try {
+            ApplicationSharedMemory.fromFileDescriptor(readOnlyFileDescriptor, /* mutable= */ true);
+            fail("Shouldn't be able to map read-only memory as mutable");
+        } catch (Exception expected) {
+        }
+    }
+}
diff --git a/tests/Internal/src/com/android/internal/os/ApplicationSharedMemoryTestRule.java b/tests/Internal/src/com/android/internal/os/ApplicationSharedMemoryTestRule.java
new file mode 100644
index 0000000..ff2a461
--- /dev/null
+++ b/tests/Internal/src/com/android/internal/os/ApplicationSharedMemoryTestRule.java
@@ -0,0 +1,56 @@
+/*
+ * 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 com.android.internal.os;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+import com.android.internal.os.ApplicationSharedMemory;
+
+/** Test rule that sets up and tears down ApplicationSharedMemory for test. */
+public class ApplicationSharedMemoryTestRule implements TestRule {
+
+    private ApplicationSharedMemory mSavedInstance;
+
+    @Override
+    public Statement apply(final Statement base, final Description description) {
+        return new Statement() {
+            @Override
+            public void evaluate() throws Throwable {
+                setup();
+                try {
+                    base.evaluate(); // Run the test
+                } finally {
+                    teardown();
+                }
+            }
+        };
+    }
+
+    private void setup() {
+        mSavedInstance = ApplicationSharedMemory.sInstance;
+        ApplicationSharedMemory.sInstance = ApplicationSharedMemory.create();
+    }
+
+    private void teardown() {
+        ApplicationSharedMemory.sInstance.close();
+        ApplicationSharedMemory.sInstance = mSavedInstance;
+        mSavedInstance = null;
+    }
+}