Merge "[mdns] Skip conflict check for incoming mDNS answer records which are probed in the repository" into main
diff --git a/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt b/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt
index 933cde4..a679498 100644
--- a/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt
+++ b/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt
@@ -313,12 +313,14 @@
// - [GMS-VSR-5.3.12-004] MUST indicate at least 1024 bytes of usable memory from calls to
// the getApfPacketFilterCapabilities HAL method.
// TODO: check whether above text should be changed "34 or higher"
- // This should assert apfVersionSupported >= 4 as per the VSR requirements, but there are
- // currently no tests for APFv6 and there cannot be a valid implementation as the
- // interpreter has yet to be finalized.
- assertThat(caps.apfVersionSupported).isEqualTo(4)
+ assertThat(caps.apfVersionSupported).isAtLeast(4)
assertThat(caps.maximumApfProgramSize).isAtLeast(1024)
+ if (caps.apfVersionSupported > 4) {
+ assertThat(caps.maximumApfProgramSize).isAtLeast(2048)
+ assertThat(caps.apfVersionSupported).isEqualTo(6000) // v6.0000
+ }
+
// DEVICEs launching with Android 15 (AOSP experimental) or higher with CHIPSETs that set
// ro.board.first_api_level or ro.board.api_level to 202404 or higher:
// - [GMS-VSR-5.3.12-009] MUST indicate at least 2048 bytes of usable memory from calls to
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
index 0559499..61bc5c1 100644
--- a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
@@ -106,6 +106,7 @@
import android.os.Looper;
import android.os.RemoteException;
import android.os.UserManager;
+import android.provider.Settings;
import android.util.Log;
import android.util.SparseArray;
@@ -199,6 +200,7 @@
private final ThreadPersistentSettings mPersistentSettings;
private final UserManager mUserManager;
private boolean mUserRestricted;
+ private boolean mAirplaneModeOn;
private boolean mForceStopOtDaemonEnabled;
private BorderRouterConfigurationParcel mBorderRouterConfig;
@@ -282,7 +284,7 @@
}
private void maybeInitializeOtDaemon() {
- if (!isEnabled()) {
+ if (!shouldEnableThread()) {
return;
}
@@ -317,7 +319,7 @@
otDaemon.initialize(
mTunIfController.getTunFd(),
- isEnabled(),
+ shouldEnableThread(),
mNsdPublisher,
getMeshcopTxtAttributes(mResources.get()),
mOtDaemonCallbackProxy,
@@ -382,7 +384,7 @@
Log.d(
TAG,
"Initializing Thread system service: Thread is "
- + (isEnabled() ? "enabled" : "disabled"));
+ + (shouldEnableThread() ? "enabled" : "disabled"));
try {
mTunIfController.createTunInterface();
} catch (IOException e) {
@@ -394,6 +396,8 @@
requestThreadNetwork();
mUserRestricted = isThreadUserRestricted();
registerUserRestrictionsReceiver();
+ mAirplaneModeOn = isAirplaneModeOn();
+ registerAirplaneModeReceiver();
maybeInitializeOtDaemon();
});
}
@@ -474,6 +478,15 @@
// the otDaemon set enabled state operation succeeded or not, so that it can recover
// to the desired value after reboot.
mPersistentSettings.put(ThreadPersistentSettings.THREAD_ENABLED.key, isEnabled);
+
+ // Remember whether the user wanted to keep Thread enabled in airplane mode. If once
+ // the user disabled Thread again in airplane mode, the persistent settings state is
+ // reset (so that Thread will be auto-disabled again when airplane mode is turned on).
+ // This behavior is consistent with Wi-Fi and bluetooth.
+ if (mAirplaneModeOn) {
+ mPersistentSettings.put(
+ ThreadPersistentSettings.THREAD_ENABLED_IN_AIRPLANE_MODE.key, isEnabled);
+ }
}
try {
@@ -510,36 +523,30 @@
+ newUserRestrictedState);
mUserRestricted = newUserRestrictedState;
- final boolean isEnabled = isEnabled();
+ final boolean shouldEnableThread = shouldEnableThread();
final IOperationReceiver receiver =
new IOperationReceiver.Stub() {
@Override
public void onSuccess() {
Log.d(
TAG,
- (isEnabled ? "Enabled" : "Disabled")
+ (shouldEnableThread ? "Enabled" : "Disabled")
+ " Thread due to user restriction change");
}
@Override
- public void onError(int otError, String messages) {
+ public void onError(int errorCode, String errorMessage) {
Log.e(
TAG,
"Failed to "
- + (isEnabled ? "enable" : "disable")
+ + (shouldEnableThread ? "enable" : "disable")
+ " Thread for user restriction change");
}
};
// Do not save the user restriction state to persistent settings so that the user
// configuration won't be overwritten
- setEnabledInternal(isEnabled, false /* persist */, new OperationReceiverWrapper(receiver));
- }
-
- /** Returns {@code true} if Thread is set enabled. */
- private boolean isEnabled() {
- return !mForceStopOtDaemonEnabled
- && !mUserRestricted
- && mPersistentSettings.get(ThreadPersistentSettings.THREAD_ENABLED);
+ setEnabledInternal(
+ shouldEnableThread, false /* persist */, new OperationReceiverWrapper(receiver));
}
/** Returns {@code true} if Thread has been restricted for the user. */
@@ -547,6 +554,74 @@
return mUserManager.hasUserRestriction(DISALLOW_THREAD_NETWORK);
}
+ private void registerAirplaneModeReceiver() {
+ mContext.registerReceiver(
+ new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ onAirplaneModeChanged(isAirplaneModeOn());
+ }
+ },
+ new IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED),
+ null /* broadcastPermission */,
+ mHandler);
+ }
+
+ private void onAirplaneModeChanged(boolean newAirplaneModeOn) {
+ checkOnHandlerThread();
+ if (mAirplaneModeOn == newAirplaneModeOn) {
+ return;
+ }
+ Log.i(TAG, "Airplane mode changed: " + mAirplaneModeOn + " -> " + newAirplaneModeOn);
+ mAirplaneModeOn = newAirplaneModeOn;
+
+ final boolean shouldEnableThread = shouldEnableThread();
+ final IOperationReceiver receiver =
+ new IOperationReceiver.Stub() {
+ @Override
+ public void onSuccess() {
+ Log.d(
+ TAG,
+ (shouldEnableThread ? "Enabled" : "Disabled")
+ + " Thread due to airplane mode change");
+ }
+
+ @Override
+ public void onError(int errorCode, String errorMessage) {
+ Log.e(
+ TAG,
+ "Failed to "
+ + (shouldEnableThread ? "enable" : "disable")
+ + " Thread for airplane mode change");
+ }
+ };
+ // Do not save the user restriction state to persistent settings so that the user
+ // configuration won't be overwritten
+ setEnabledInternal(
+ shouldEnableThread, false /* persist */, new OperationReceiverWrapper(receiver));
+ }
+
+ /** Returns {@code true} if Airplane mode has been turned on. */
+ private boolean isAirplaneModeOn() {
+ return Settings.Global.getInt(
+ mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 0)
+ == 1;
+ }
+
+ /**
+ * Returns {@code true} if Thread should be enabled based on current settings, runtime user
+ * restriction and airplane mode state.
+ */
+ private boolean shouldEnableThread() {
+ final boolean enabledInAirplaneMode =
+ mPersistentSettings.get(ThreadPersistentSettings.THREAD_ENABLED_IN_AIRPLANE_MODE);
+
+ return !mForceStopOtDaemonEnabled
+ && !mUserRestricted
+ && (!mAirplaneModeOn || enabledInAirplaneMode)
+ && mPersistentSettings.get(ThreadPersistentSettings.THREAD_ENABLED);
+ }
+
private void requestUpstreamNetwork() {
if (mUpstreamNetworkCallback != null) {
throw new AssertionError("The upstream network request is already there.");
@@ -1018,7 +1093,7 @@
checkOnHandlerThread();
// Fails early to avoid waking up ot-daemon by the ThreadNetworkCountryCode class
- if (!isEnabled()) {
+ if (!shouldEnableThread()) {
receiver.onError(
ERROR_THREAD_DISABLED, "Can't set country code when Thread is disabled");
return;
diff --git a/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java b/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java
index 8aaff60..f18aac9 100644
--- a/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java
+++ b/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java
@@ -63,6 +63,14 @@
/** Stores the Thread feature toggle state, true for enabled and false for disabled. */
public static final Key<Boolean> THREAD_ENABLED = new Key<>("thread_enabled", true);
+ /**
+ * Indicates that Thread was enabled (i.e. via the setEnabled() API) when the airplane mode is
+ * turned on in settings. When this value is {@code true}, the current airplane mode state will
+ * be ignored when evaluating the Thread enabled state.
+ */
+ public static final Key<Boolean> THREAD_ENABLED_IN_AIRPLANE_MODE =
+ new Key<>("thread_enabled_in_airplane_mode", false);
+
/** Stores the Thread country code, null if no country code is stored. */
public static final Key<String> THREAD_COUNTRY_CODE = new Key<>("thread_country_code", null);
diff --git a/thread/tests/unit/AndroidManifest.xml b/thread/tests/unit/AndroidManifest.xml
index ace7c52..8442e80 100644
--- a/thread/tests/unit/AndroidManifest.xml
+++ b/thread/tests/unit/AndroidManifest.xml
@@ -19,6 +19,8 @@
xmlns:android="http://schemas.android.com/apk/res/android"
package="android.net.thread.unittests">
+ <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
+
<application android:debuggable="true">
<uses-library android:name="android.test.runner" />
</application>
diff --git a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
index 493058f..52a9dd9 100644
--- a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
+++ b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
@@ -26,6 +26,7 @@
import static android.net.thread.ThreadNetworkManager.PERMISSION_THREAD_NETWORK_PRIVILEGED;
import static com.android.server.thread.ThreadNetworkCountryCode.DEFAULT_COUNTRY_CODE;
+import static com.android.server.thread.ThreadPersistentSettings.THREAD_ENABLED_IN_AIRPLANE_MODE;
import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_INVALID_STATE;
import static com.google.common.io.BaseEncoding.base16;
@@ -35,6 +36,7 @@
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.doAnswer;
@@ -64,6 +66,8 @@
import android.os.RemoteException;
import android.os.UserManager;
import android.os.test.TestLooper;
+import android.provider.Settings;
+import android.util.AtomicFile;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -75,7 +79,9 @@
import com.android.server.thread.openthread.testing.FakeOtDaemon;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
@@ -133,7 +139,6 @@
@Mock private TunInterfaceController mMockTunIfController;
@Mock private ParcelFileDescriptor mMockTunFd;
@Mock private InfraInterfaceController mMockInfraIfController;
- @Mock private ThreadPersistentSettings mMockPersistentSettings;
@Mock private NsdPublisher mMockNsdPublisher;
@Mock private UserManager mMockUserManager;
@Mock private IBinder mIBinder;
@@ -143,11 +148,15 @@
private Context mContext;
private TestLooper mTestLooper;
private FakeOtDaemon mFakeOtDaemon;
+ private ThreadPersistentSettings mPersistentSettings;
private ThreadNetworkControllerService mService;
@Captor private ArgumentCaptor<ActiveOperationalDataset> mActiveDatasetCaptor;
+ @Rule(order = 1)
+ public final TemporaryFolder tempFolder = new TemporaryFolder();
+
@Before
- public void setUp() {
+ public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
mContext = spy(ApplicationProvider.getApplicationContext());
@@ -164,10 +173,12 @@
mFakeOtDaemon = new FakeOtDaemon(handler);
when(mMockTunIfController.getTunFd()).thenReturn(mMockTunFd);
- when(mMockPersistentSettings.get(any())).thenReturn(true);
when(mMockUserManager.hasUserRestriction(eq(DISALLOW_THREAD_NETWORK))).thenReturn(false);
+ Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 0);
+
when(mConnectivityResources.get()).thenReturn(mResources);
+ when(mResources.getBoolean(eq(R.bool.config_thread_default_enabled))).thenReturn(true);
when(mResources.getString(eq(R.string.config_thread_vendor_name)))
.thenReturn(TEST_VENDOR_NAME);
when(mResources.getString(eq(R.string.config_thread_vendor_oui)))
@@ -175,6 +186,10 @@
when(mResources.getString(eq(R.string.config_thread_model_name)))
.thenReturn(TEST_MODEL_NAME);
+ final AtomicFile storageFile = new AtomicFile(tempFolder.newFile("thread_settings.xml"));
+ mPersistentSettings = new ThreadPersistentSettings(storageFile, mConnectivityResources);
+ mPersistentSettings.initialize();
+
mService =
new ThreadNetworkControllerService(
mContext,
@@ -184,7 +199,7 @@
mMockConnectivityManager,
mMockTunIfController,
mMockInfraIfController,
- mMockPersistentSettings,
+ mPersistentSettings,
mMockNsdPublisher,
mMockUserManager,
mConnectivityResources,
@@ -343,15 +358,9 @@
@Test
public void userRestriction_userBecomesRestricted_stateIsDisabledButNotPersisted() {
- AtomicReference<BroadcastReceiver> receiverRef = new AtomicReference<>();
when(mMockUserManager.hasUserRestriction(eq(DISALLOW_THREAD_NETWORK))).thenReturn(false);
- doAnswer(
- invocation -> {
- receiverRef.set((BroadcastReceiver) invocation.getArguments()[0]);
- return null;
- })
- .when(mContext)
- .registerReceiver(any(BroadcastReceiver.class), any(), any(), any());
+ AtomicReference<BroadcastReceiver> receiverRef =
+ captureBroadcastReceiver(UserManager.ACTION_USER_RESTRICTIONS_CHANGED);
mService.initialize();
mTestLooper.dispatchAll();
@@ -360,21 +369,14 @@
mTestLooper.dispatchAll();
assertThat(mFakeOtDaemon.getEnabledState()).isEqualTo(STATE_DISABLED);
- verify(mMockPersistentSettings, never())
- .put(eq(ThreadPersistentSettings.THREAD_ENABLED.key), eq(false));
+ assertThat(mPersistentSettings.get(ThreadPersistentSettings.THREAD_ENABLED)).isTrue();
}
@Test
- public void userRestriction_userBecomesNotRestricted_stateIsEnabledButNotPersisted() {
- AtomicReference<BroadcastReceiver> receiverRef = new AtomicReference<>();
+ public void userRestriction_userBecomesNotRestricted_stateIsEnabled() {
when(mMockUserManager.hasUserRestriction(eq(DISALLOW_THREAD_NETWORK))).thenReturn(true);
- doAnswer(
- invocation -> {
- receiverRef.set((BroadcastReceiver) invocation.getArguments()[0]);
- return null;
- })
- .when(mContext)
- .registerReceiver(any(BroadcastReceiver.class), any(), any(), any());
+ AtomicReference<BroadcastReceiver> receiverRef =
+ captureBroadcastReceiver(UserManager.ACTION_USER_RESTRICTIONS_CHANGED);
mService.initialize();
mTestLooper.dispatchAll();
@@ -383,8 +385,6 @@
mTestLooper.dispatchAll();
assertThat(mFakeOtDaemon.getEnabledState()).isEqualTo(STATE_ENABLED);
- verify(mMockPersistentSettings, never())
- .put(eq(ThreadPersistentSettings.THREAD_ENABLED.key), eq(true));
}
@Test
@@ -401,6 +401,118 @@
assertThat(failure.getErrorCode()).isEqualTo(ERROR_FAILED_PRECONDITION);
}
+ @Test
+ public void airplaneMode_initWithAirplaneModeOn_otDaemonNotStarted() {
+ Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 1);
+
+ mService.initialize();
+ mTestLooper.dispatchAll();
+
+ assertThat(mFakeOtDaemon.isInitialized()).isFalse();
+ }
+
+ @Test
+ public void airplaneMode_initWithAirplaneModeOff_threadIsEnabled() {
+ Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 0);
+
+ mService.initialize();
+ mTestLooper.dispatchAll();
+
+ assertThat(mFakeOtDaemon.getEnabledState()).isEqualTo(STATE_ENABLED);
+ }
+
+ @Test
+ public void airplaneMode_changesFromOffToOn_stateIsDisabled() {
+ Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 0);
+ AtomicReference<BroadcastReceiver> receiverRef =
+ captureBroadcastReceiver(Intent.ACTION_AIRPLANE_MODE_CHANGED);
+ mService.initialize();
+ mTestLooper.dispatchAll();
+
+ Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 1);
+ receiverRef.get().onReceive(mContext, new Intent());
+ mTestLooper.dispatchAll();
+
+ assertThat(mFakeOtDaemon.getEnabledState()).isEqualTo(STATE_DISABLED);
+ }
+
+ @Test
+ public void airplaneMode_changesFromOnToOff_stateIsEnabled() {
+ Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 1);
+ AtomicReference<BroadcastReceiver> receiverRef =
+ captureBroadcastReceiver(Intent.ACTION_AIRPLANE_MODE_CHANGED);
+ mService.initialize();
+ mTestLooper.dispatchAll();
+
+ Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 0);
+ receiverRef.get().onReceive(mContext, new Intent());
+ mTestLooper.dispatchAll();
+
+ assertThat(mFakeOtDaemon.getEnabledState()).isEqualTo(STATE_ENABLED);
+ }
+
+ @Test
+ public void airplaneMode_setEnabledWhenAirplaneModeIsOn_WillNotAutoDisableSecondTime() {
+ Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 1);
+ AtomicReference<BroadcastReceiver> receiverRef =
+ captureBroadcastReceiver(Intent.ACTION_AIRPLANE_MODE_CHANGED);
+ CompletableFuture<Void> setEnabledFuture = new CompletableFuture<>();
+ mService.initialize();
+
+ mService.setEnabled(true, newOperationReceiver(setEnabledFuture));
+ mTestLooper.dispatchAll();
+ Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 0);
+ receiverRef.get().onReceive(mContext, new Intent());
+ mTestLooper.dispatchAll();
+ Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 1);
+ receiverRef.get().onReceive(mContext, new Intent());
+ mTestLooper.dispatchAll();
+
+ assertThat(mFakeOtDaemon.getEnabledState()).isEqualTo(STATE_ENABLED);
+ assertThat(mPersistentSettings.get(THREAD_ENABLED_IN_AIRPLANE_MODE)).isTrue();
+ }
+
+ @Test
+ public void airplaneMode_setDisabledWhenAirplaneModeIsOn_WillAutoDisableSecondTime() {
+ Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 1);
+ AtomicReference<BroadcastReceiver> receiverRef =
+ captureBroadcastReceiver(Intent.ACTION_AIRPLANE_MODE_CHANGED);
+ CompletableFuture<Void> setEnabledFuture = new CompletableFuture<>();
+ mService.initialize();
+ mService.setEnabled(true, newOperationReceiver(setEnabledFuture));
+ mTestLooper.dispatchAll();
+
+ mService.setEnabled(false, newOperationReceiver(setEnabledFuture));
+ mTestLooper.dispatchAll();
+ Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 0);
+ receiverRef.get().onReceive(mContext, new Intent());
+ mTestLooper.dispatchAll();
+ Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 1);
+ receiverRef.get().onReceive(mContext, new Intent());
+ mTestLooper.dispatchAll();
+
+ assertThat(mFakeOtDaemon.getEnabledState()).isEqualTo(STATE_DISABLED);
+ assertThat(mPersistentSettings.get(THREAD_ENABLED_IN_AIRPLANE_MODE)).isFalse();
+ }
+
+ private AtomicReference<BroadcastReceiver> captureBroadcastReceiver(String action) {
+ AtomicReference<BroadcastReceiver> receiverRef = new AtomicReference<>();
+
+ doAnswer(
+ invocation -> {
+ receiverRef.set((BroadcastReceiver) invocation.getArguments()[0]);
+ return null;
+ })
+ .when(mContext)
+ .registerReceiver(
+ any(BroadcastReceiver.class),
+ argThat(actualIntentFilter -> actualIntentFilter.hasAction(action)),
+ any(),
+ any());
+
+ return receiverRef;
+ }
+
private static IOperationReceiver newOperationReceiver(CompletableFuture<Void> future) {
return new IOperationReceiver.Stub() {
@Override
diff --git a/tools/aospify_device.sh b/tools/aospify_device.sh
new file mode 100755
index 0000000..f25ac9d
--- /dev/null
+++ b/tools/aospify_device.sh
@@ -0,0 +1,164 @@
+#!/bin/bash
+
+# Script to swap core networking modules in a GMS userdebug device to AOSP modules, by remounting
+# the system partition and replacing module prebuilts. This is only to be used for local testing,
+# and should only be used on userdebug devices that support "adb root" and remounting the system
+# partition using overlayfs.
+#
+# Usage: aospify_device.sh [device_serial]
+# Reset by wiping data (adb reboot bootloader && fastboot erase userdata && fastboot reboot).
+#
+# This applies to NetworkStack, CaptivePortalLogin, dnsresolver, tethering, cellbroadcast modules,
+# which generally need to be preloaded together (core networking modules + cellbroadcast which
+# shares its certificates with NetworkStack and CaptivePortalLogin)
+#
+# This allows device manufacturers to test their changes in AOSP modules, running them on their
+# own device builds, before contributing contributing the patches to AOSP. After running this script
+# once AOSP modules can be quickly built and updated on the prepared device with:
+# m NetworkStack
+# adb install --staged $ANDROID_PRODUCT_OUT/system/priv-app/NetworkStack/NetworkStack.apk \
+# adb reboot
+# or for APEX modules:
+# m com.android.tethering deapexer
+# $ANDROID_HOST_OUT/bin/deapexer decompress --input $ANDROID_PRODUCT_OUT/system/apex/com.android.tethering.capex --output /tmp/decompressed.apex
+# adb install /tmp/decompressed.apex && adb reboot
+#
+# This has been tested on Android T and Android U Pixel devices. On recent (U+) devices, it requires
+# setting a released target SDK (for example target_sdk_version: "34") in
+# packages/modules/Connectivity/service/ServiceConnectivityResources/Android.bp before building.
+set -e
+
+function push_apex {
+ local original_apex_name=$1
+ local aosp_apex_name=$2
+ if $ADB_CMD shell ls /system/apex/$original_apex_name.capex 1>/dev/null 2>/dev/null; then
+ $ADB_CMD shell rm /system/apex/$original_apex_name.capex
+ $ADB_CMD push $ANDROID_PRODUCT_OUT/system/apex/$aosp_apex_name.capex /system/apex/
+ else
+ rm -f /tmp/decompressed_$aosp_apex_name.apex
+ $ANDROID_HOST_OUT/bin/deapexer decompress --input $ANDROID_PRODUCT_OUT/system/apex/$aosp_apex_name.capex --output /tmp/decompressed_$aosp_apex_name.apex
+ $ADB_CMD shell rm /system/apex/$original_apex_name.apex
+ $ADB_CMD push /tmp/decompressed_$aosp_apex_name.apex /system/apex/$aosp_apex_name.apex
+ rm /tmp/decompressed_$aosp_apex_name.apex
+ fi
+}
+
+function push_apk {
+ local app_type=$1
+ local original_apk_name=$2
+ local aosp_apk_name=$3
+ $ADB_CMD shell rm /system/$app_type/$original_apk_name/$original_apk_name.apk
+ $ADB_CMD push $ANDROID_PRODUCT_OUT/system/$app_type/$aosp_apk_name/$aosp_apk_name.apk /system/$app_type/$original_apk_name/
+}
+
+NETWORKSTACK_AOSP_SEPOLICY_KEY="<signer signature=\"308205dc308203c4a003020102020900fc6cb0d8a6fdd16\
+8300d06092a864886f70d01010b0500308181310b30090603550406130255533113301106035504080c0a43616c69666f72\
+6e69613116301406035504070c0d4d6f756e7461696e20566965773110300e060355040a0c07416e64726f69643110300e0\
+60355040b0c07416e64726f69643121301f06035504030c18636f6d2e616e64726f69642e6e6574776f726b737461636b30\
+20170d3139303231323031343632305a180f34373537303130383031343632305a308181310b30090603550406130255533\
+113301106035504080c0a43616c69666f726e69613116301406035504070c0d4d6f756e7461696e20566965773110300e06\
+0355040a0c07416e64726f69643110300e060355040b0c07416e64726f69643121301f06035504030c18636f6d2e616e647\
+26f69642e6e6574776f726b737461636b30820222300d06092a864886f70d01010105000382020f003082020a0282020100\
+bb71f5137ff0b2d757acc2ca3d378e0f8de11090d5caf3d49e314d35c283b778b02d792d8eba440364ca970985441660f0b\
+c00afbc63dd611b1bf51ad28a1edd21e0048f548b80f8bd113e25682822f57dab8273afaf12c64d19a0c6be238f3e66ddc7\
+9b10fd926931e3ee60a7bf618644da3c2c4fc428139d45d27beda7fe45e30075b493ead6ec01cdd55d931c0a657e2e59742\
+ca632b6dc3842a2deb7d22443c809291d7a549203ae6ae356582a4ca23f30f0549c4ec8408a75278e95c69e8390ad5280bc\
+efaef6f1309a41bd9f3bfb5d12dca7e79ec6fd6848193fa9ab728224887b4f93e985ec7cbf6401b0e863a4b91c05d046f04\
+0fe954004b1645954fcb4114cee1e8b64b47d719a19ef4c001cb183f7f3e166e43f56d68047c3440da34fdf529d44274b8b\
+2f6afb345091ad8ad4b93bd5c55d52286a5d3c157465db8ddf62e7cdb6b10fb18888046afdd263ae6f2125d9065759c7e42\
+f8610a6746edbdc547d4301612eeec3c3cbd124dececc8d38b20e73b13f24ee7ca13a98c5f61f0c81b07d2b519749bc2bcb\
+9e0949aef6c118a3e8125e6ab57fce46bb091a66740e10b31c740b891900c0ecda9cc69ecb4f3369998b175106dd0a4ffd7\
+024eb7e75fedd1a5b131d0bb2b40c63491e3cf86b8957b21521b3a96ed1376a51a6ac697866b0256dee1bcd9ab9a188bf4c\
+ed80b59a5f24c2da9a55eb7b0e502116e30203010001a3533051301d0603551d0e041604149383c92cfbf099d5c47b0c365\
+7d8622a084b72e1301f0603551d230418301680149383c92cfbf099d5c47b0c3657d8622a084b72e1300f0603551d130101\
+ff040530030101ff300d06092a864886f70d01010b050003820201006a0501382fde2a6b8f70c60cd1b8ee4f788718c288b\
+170258ef3a96230b65005650d6a4c42a59a97b2ddec502413e7b438fbd060363d74b74a232382a7f77fd3da34e38f79fad0\
+35a8b472c5cff365818a0118d87fa1e31cc7ed4befd27628760c290980c3cc3b7ff0cfd01b75ff1fcc83e981b5b25a54d85\
+b68a80424ac26015fb3a4c754969a71174c0bc283f6c88191dced609e245f5938ffd0ad799198e2d0bf6342221c1b0a5d33\
+2ed2fffc668982cabbcb7d3b630ff8476e5c84ac0ad37adf9224035200039f95ec1fa95bf83796c0e8986135cee2dcaef19\
+0b249855a7e7397d4a0bf17ea63d978589c6b48118a381fffbd790c44d80233e2e35292a3b5533ca3f2cc173f85cf904adf\
+e2e4e2183dc1eba0ebae07b839a81ff1bc92e292550957c8599af21e9c0497b9234ce345f3f508b1cc872aa55ddb5e773c5\
+c7dd6577b9a8b6daed20ae1ff4b8206fd9f5c8f5a22ba1980bef01ae6fcb2659b97ad5b985fa81c019ffe008ddd9c8130c0\
+6fc6032b2149c2209fc438a7e8c3b20ce03650ad31c4ee48f169777a0ae182b72ca31b81540f61f167d8d7adf4f6bb2330f\
+f5c24037245000d8172c12ab5d5aa5890b8b12db0f0e7296264eb66e7f9714c31004649fb4b864005f9c43c80db3f6de52f\
+d44d6e2036bfe7f5807156ed5ab591d06fd6bb93ba4334ea2739af8b41ed2686454e60b666d10738bb7ba88001\">\
+<seinfo value=\"network_stack\"\/><\/signer>"
+
+DEVICE=$1
+ADB_CMD="adb -s $DEVICE"
+
+if [ -z "$DEVICE" ]; then
+ echo "Usage: aospify_device.sh [device_serial]"
+ exit 1
+fi
+
+if [ -z "$ANDROID_BUILD_TOP" ]; then
+ echo "Run build/envsetup.sh first to set ANDROID_BUILD_TOP"
+ exit 1
+fi
+
+if ! $ADB_CMD wait-for-device shell pm path com.google.android.networkstack; then
+ echo "This device is already not using GMS modules"
+ exit 1
+fi
+
+read -p "This script is only for test purposes and highly likely to make your device unusable. \
+Continue ? <y/N>" prompt
+if [[ $prompt != "y" ]]
+then
+ exit 0
+fi
+
+cd $ANDROID_BUILD_TOP
+source build/envsetup.sh
+lunch aosp_arm64-trunk_staging-userdebug
+m NetworkStack CaptivePortalLogin com.android.tethering com.android.cellbroadcast \
+ com.android.resolv deapexer \
+ out/target/product/generic_arm64/system/etc/selinux/plat_mac_permissions.xml \
+ out/target/product/generic_arm64/system/etc/permissions/com.android.networkstack.xml
+
+$ADB_CMD root
+$ADB_CMD remount
+$ADB_CMD reboot
+
+echo "Waiting for boot..."
+$ADB_CMD wait-for-device;
+until [[ $($ADB_CMD shell getprop sys.boot_completed) == 1 ]]; do
+ sleep 1;
+done
+
+$ADB_CMD root
+$ADB_CMD remount
+
+push_apk priv-app NetworkStackGoogle NetworkStack
+push_apk app CaptivePortalLoginGoogle CaptivePortalLogin
+push_apex com.google.android.tethering com.android.tethering
+push_apex com.google.android.cellbroadcast com.android.cellbroadcast
+push_apex com.google.android.resolv com.android.resolv
+
+# Replace the network_stack key used to set its sepolicy context
+rm -f /tmp/pulled_plat_mac_permissions.xml
+$ADB_CMD pull /system/etc/selinux/plat_mac_permissions.xml /tmp/pulled_plat_mac_permissions.xml
+sed_replace='s/<signer signature="[0-9a-fA-F]+"><seinfo value="network_stack"\/><\/signer>/'$NETWORKSTACK_AOSP_SEPOLICY_KEY'/'
+sed -E "$sed_replace" /tmp/pulled_plat_mac_permissions.xml |
+ $ADB_CMD shell 'cat > /system/etc/selinux/plat_mac_permissions.xml'
+rm /tmp/pulled_plat_mac_permissions.xml
+
+# Update the networkstack privapp-permissions allowlist
+rm -f /tmp/pulled_privapp-permissions.xml
+$ADB_CMD pull /system/etc/permissions/privapp-permissions-google.xml /tmp/pulled_privapp-permissions.xml
+
+# Remove last </permission> line, and the permissions for com.google.android.networkstack
+sed -nE '1,/<\/permissions>/p' /tmp/pulled_privapp-permissions.xml \
+ | sed -E '/com.google.android.networkstack/,/privapp-permissions/d' > /tmp/modified_privapp-permissions.xml
+# Add the AOSP permissions and re-add the </permissions> line
+sed -nE '/com.android.networkstack/,/privapp-permissions/p' $ANDROID_PRODUCT_OUT/system/etc/permissions/com.android.networkstack.xml \
+ >> /tmp/modified_privapp-permissions.xml
+echo '</permissions>' >> /tmp/modified_privapp-permissions.xml
+
+$ADB_CMD push /tmp/modified_privapp-permissions.xml /system/etc/permissions/privapp-permissions-google.xml
+
+rm /tmp/pulled_privapp-permissions.xml /tmp/modified_privapp-permissions.xml
+
+echo "Done modifying, rebooting"
+$ADB_CMD reboot
\ No newline at end of file