diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java index 647455611c..218e8f9b6e 100644 --- a/service/src/com/android/server/ConnectivityService.java +++ b/service/src/com/android/server/ConnectivityService.java @@ -362,6 +362,7 @@ import com.android.server.connectivity.ApplicationSelfCertifiedNetworkCapabiliti import com.android.server.connectivity.AutodestructReference; import com.android.server.connectivity.AutomaticOnOffKeepaliveTracker; import com.android.server.connectivity.AutomaticOnOffKeepaliveTracker.AutomaticOnOffKeepalive; +import com.android.server.connectivity.BroadcastReceiveHelper; import com.android.server.connectivity.CarrierPrivilegeAuthenticator; import com.android.server.connectivity.ClatCoordinator; import com.android.server.connectivity.ConnectivityFlags; @@ -439,7 +440,8 @@ import java.util.function.Predicate; * @hide */ @TargetApi(Build.VERSION_CODES.S) -public class ConnectivityService extends IConnectivityManager.Stub { +public class ConnectivityService extends IConnectivityManager.Stub + implements BroadcastReceiveHelper.Delegate { private static final String TAG = ConnectivityService.class.getSimpleName(); private static final String DIAG_ARG = "--diag"; @@ -528,6 +530,8 @@ public class ConnectivityService extends IConnectivityManager.Stub { private final PermissionMonitor mPermissionMonitor; + private final BroadcastReceiveHelper mBroadcastReceiveHelper; + @VisibleForTesting final RequestInfoPerUidCounter mNetworkRequestCounter; @VisibleForTesting @@ -1942,6 +1946,7 @@ public class ConnectivityService extends IConnectivityManager.Stub { mTrackerHandler = new NetworkStateTrackerHandler(mHandlerThread.getLooper()); mConnectivityDiagnosticsHandler = new ConnectivityDiagnosticsHandler(mHandlerThread.getLooper()); + mBroadcastReceiveHelper = new BroadcastReceiveHelper(mContext, mHandler, this); mReleasePendingIntentDelayMs = Settings.Secure.getInt(context.getContentResolver(), ConnectivitySettingsManager.CONNECTIVITY_RELEASE_PENDING_INTENT_DELAY_MS, 5_000); @@ -2009,22 +2014,8 @@ public class ConnectivityService extends IConnectivityManager.Stub { mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE); mUserAllContext = mContext.createContextAsUser(UserHandle.ALL, 0 /* flags */); - // Listen for user add/removes to inform PermissionMonitor. - // Should run on mHandler to avoid any races. - final IntentFilter userIntentFilter = new IntentFilter(); - userIntentFilter.addAction(Intent.ACTION_USER_ADDED); - userIntentFilter.addAction(Intent.ACTION_USER_REMOVED); - mUserAllContext.registerReceiver(mUserIntentReceiver, userIntentFilter, - null /* broadcastPermission */, mHandler); - - // Listen to package add/removes for netd - final IntentFilter packageIntentFilter = new IntentFilter(); - packageIntentFilter.addAction(Intent.ACTION_PACKAGE_ADDED); - packageIntentFilter.addAction(Intent.ACTION_PACKAGE_REMOVED); - packageIntentFilter.addAction(Intent.ACTION_PACKAGE_REPLACED); - packageIntentFilter.addDataScheme("package"); - mUserAllContext.registerReceiver(mPackageIntentReceiver, packageIntentFilter, - null /* broadcastPermission */, mHandler); + // TODO: Move all intent receivers to the helper class. + mBroadcastReceiveHelper.registerReceivers(); // This is needed for pre-V devices to propagate the data saver status // to the BPF map. This isn't supported before Android T because BPF maps are @@ -7720,14 +7711,17 @@ public class ConnectivityService extends IConnectivityManager.Stub { } } - private void onUserAdded(@NonNull final UserHandle user) { + @Override + public void onUserAdded(@NonNull final UserHandle user) { if (mOemNetworkPreferences.getNetworkPreferences().size() > 0) { handleSetOemNetworkPreference(mOemNetworkPreferences, null); } updateProfileAllowedNetworks(); + mPermissionMonitor.onUserAdded(user); } - private void onUserRemoved(@NonNull final UserHandle user) { + @Override + public void onUserRemoved(@NonNull final UserHandle user) { // If there was a network preference for this user, remove it. handleSetProfileNetworkPreference( List.of(new ProfileNetworkPreferenceInfo(user, null, true, @@ -7736,9 +7730,32 @@ public class ConnectivityService extends IConnectivityManager.Stub { if (mOemNetworkPreferences.getNetworkPreferences().size() > 0) { handleSetOemNetworkPreference(mOemNetworkPreferences, null); } + mPermissionMonitor.onUserRemoved(user); } - private void onPackageChanged(@NonNull final String packageName) { + @Override + public void onPackageAdded(@NonNull final String packageName, final int uid) { + handlePackageChanged(packageName); + mPermissionMonitor.onPackageAdded(packageName, uid); + } + + @Override + public void onPackageRemoved(@NonNull final String packageName, final int uid) { + handlePackageChanged(packageName); + mPermissionMonitor.onPackageRemoved(packageName, uid); + } + + @Override + public void onPackageReplaced(@NonNull final String packageName, final int uid) { + handlePackageChanged(packageName); + } + + @Override + public void onExternalApplicationsAvailable(@Nullable String[] pkgList) { + mPermissionMonitor.onExternalApplicationsAvailable(pkgList); + } + + private void handlePackageChanged(@NonNull final String packageName) { // This is necessary in case a package is added or removed, but also when it's replaced to // run as a new UID by its manifest rules. Also, if a separate package shares the same UID // as one in the preferences, then it should follow the same routing as that other package, @@ -7754,45 +7771,6 @@ public class ConnectivityService extends IConnectivityManager.Stub { } } - private final BroadcastReceiver mUserIntentReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - ensureRunningOnConnectivityServiceThread(); - final String action = intent.getAction(); - final UserHandle user = intent.getParcelableExtra(Intent.EXTRA_USER); - - // User should be filled for below intents, check the existence. - if (user == null) { - Log.wtf(TAG, intent.getAction() + " broadcast without EXTRA_USER"); - return; - } - - if (Intent.ACTION_USER_ADDED.equals(action)) { - onUserAdded(user); - } else if (Intent.ACTION_USER_REMOVED.equals(action)) { - onUserRemoved(user); - } else { - Log.wtf(TAG, "received unexpected intent: " + action); - } - } - }; - - private final BroadcastReceiver mPackageIntentReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - ensureRunningOnConnectivityServiceThread(); - switch (intent.getAction()) { - case Intent.ACTION_PACKAGE_ADDED: - case Intent.ACTION_PACKAGE_REMOVED: - case Intent.ACTION_PACKAGE_REPLACED: - onPackageChanged(intent.getData().getSchemeSpecificPart()); - break; - default: - Log.wtf(TAG, "received unexpected intent: " + intent.getAction()); - } - } - }; - @RequiresApi(Build.VERSION_CODES.TIRAMISU) private final BroadcastReceiver mDataSaverReceiver = new BroadcastReceiver() { @Override diff --git a/service/src/com/android/server/connectivity/BroadcastReceiveHelper.java b/service/src/com/android/server/connectivity/BroadcastReceiveHelper.java new file mode 100644 index 0000000000..fe430437f1 --- /dev/null +++ b/service/src/com/android/server/connectivity/BroadcastReceiveHelper.java @@ -0,0 +1,230 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.connectivity; + +import android.annotation.NonNull; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.Uri; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.UserHandle; +import android.util.Log; + +import com.android.net.module.util.HandlerUtils; + +/** + * Helper class for ConnectivityService to listen for broadcast intents and + * provide callbacks on a given {@link HandlerThread}. + */ +public class BroadcastReceiveHelper { + private static final String TAG = BroadcastReceiveHelper.class.getSimpleName(); + private final Context mContext; + private final Handler mHandler; + private final Delegate mCallback; + + /** + * Interface defining the callback methods for package and user related events. + */ + public interface Delegate { + /** + * Called when a new package has been installed. + * + * @param packageName The name of the package that was added. + * @param uid The user ID of the application that was added. + */ + void onPackageAdded(String packageName, int uid); + + /** + * Called when a package has been uninstalled. + * + * @param packageName The name of the package that was removed. + * @param uid The user ID of the application that was removed. + */ + void onPackageRemoved(String packageName, int uid); + + /** + * Called when an existing package has been replaced with a new version. + * + * @param packageName The name of the package that was replaced. + * @param uid The user ID of the application that was replaced. + */ + void onPackageReplaced(String packageName, int uid); + + /** + * Called when the availability of external applications changes. + * + * @param pkgList An array of package names that have become available. + */ + void onExternalApplicationsAvailable(String[] pkgList); + + /** + * Called when a new user has been added to the device. + * + * @param userHandle The {@link UserHandle} of the user that was added. + */ + void onUserAdded(UserHandle userHandle); + + /** + * Called when a user has been removed from the device. + * + * @param userHandle The {@link UserHandle} of the user that was removed. + */ + void onUserRemoved(UserHandle userHandle); + } + + /** + * Constructs a new {@code ConnectivityBroadcastReceiveHelper}. + * + * @param context The {@link Context} to register the broadcast receiver. + * @param handler The handler where the callback methods will be executed. + * @param callback The {@link Delegate} implementation that will receive the broadcast events. + */ + public BroadcastReceiveHelper(@NonNull Context context, + @NonNull final Handler handler, @NonNull Delegate callback) { + this.mContext = context; + this.mHandler = handler; + this.mCallback = callback; + } + + /** + * Registers the broadcast receivers for package and user related intents. + * The callbacks for these events will be executed on the {@link HandlerThread} + * provided in the constructor. + */ + public void registerReceivers() { + final Context userAllContext = mContext.createContextAsUser(UserHandle.ALL, 0 /* flags */); + + // Listen to user events. + // TODO: Receive intents inline and post them to the handler. + final IntentFilter userIntentFilter = new IntentFilter(); + userIntentFilter.addAction(Intent.ACTION_USER_ADDED); + userIntentFilter.addAction(Intent.ACTION_USER_REMOVED); + userAllContext.registerReceiver(mUserIntentReceiver, userIntentFilter, + null /* broadcastPermission */, mHandler); + + // Listen to package change events. + final IntentFilter packageIntentFilter = new IntentFilter(); + packageIntentFilter.addAction(Intent.ACTION_PACKAGE_ADDED); + packageIntentFilter.addAction(Intent.ACTION_PACKAGE_REMOVED); + packageIntentFilter.addAction(Intent.ACTION_PACKAGE_REPLACED); + packageIntentFilter.addDataScheme("package"); + userAllContext.registerReceiver(mPackageIntentReceiver, packageIntentFilter, + null /* broadcastPermission */, mHandler); + + // For PermissionMonitor, listen to EXTERNAL_APPLICATIONS_AVAILABLE is that an app + // becoming available means it may need to gain a permission. But an app that becomes + // unavailable can neither gain nor lose permissions on that account, it just can no + // longer run. Thus, doesn't need to listen to EXTERNAL_APPLICATIONS_UNAVAILABLE. + final IntentFilter externalIntentFilter = + new IntentFilter(Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE); + userAllContext.registerReceiver(mExternalAppIntentReceiver, externalIntentFilter, + null /* broadcastPermission */, mHandler); + } + + /** + * Unregisters the broadcast receiver, stopping the listener from receiving further events. + * It's important to call this method when the listener is no longer needed to avoid + * potential resource leaks. + */ + public void unregisterReceivers() { + try { + mContext.unregisterReceiver(mPackageIntentReceiver); + mContext.unregisterReceiver(mUserIntentReceiver); + mContext.unregisterReceiver(mExternalAppIntentReceiver); + } catch (IllegalArgumentException e) { + // Receiver was not registered + } + } + + private final BroadcastReceiver mPackageIntentReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + HandlerUtils.ensureRunningOnHandlerThread(mHandler); + + final int uid = intent.getIntExtra(Intent.EXTRA_UID, -1); + final Uri packageData = intent.getData(); + // AOSP never sends null data in the intent (see + // BroadcastHelper#sendPostInstallBroadcasts) + // but OEMs may have modified this code. + if (packageData == null) { + Log.wtf(TAG, "Intent received with null data: " + intent); + return; + } + final String packageName = packageData.getSchemeSpecificPart(); + + switch (intent.getAction()) { + case Intent.ACTION_PACKAGE_ADDED: { + mCallback.onPackageAdded(packageName, uid); + break; + } + case Intent.ACTION_PACKAGE_REMOVED: { + mCallback.onPackageRemoved(packageName, uid); + break; + } + case Intent.ACTION_PACKAGE_REPLACED: { + mCallback.onPackageReplaced(packageName, uid); + break; + } + default: + Log.wtf(TAG, "received unexpected intent: " + intent.getAction()); + } + } + }; + + private final BroadcastReceiver mExternalAppIntentReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + HandlerUtils.ensureRunningOnHandlerThread(mHandler); + switch (intent.getAction()) { + case Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE: { + final String[] pkgList = + intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST); + mCallback.onExternalApplicationsAvailable(pkgList); + break; + } + default: + Log.wtf(TAG, "received unexpected intent: " + intent.getAction()); + } + } + }; + + private final BroadcastReceiver mUserIntentReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + HandlerUtils.ensureRunningOnHandlerThread(mHandler); + final String action = intent.getAction(); + final UserHandle user = intent.getParcelableExtra(Intent.EXTRA_USER); + + // User should be filled for below intents, check the existence. + if (user == null) { + Log.wtf(TAG, intent.getAction() + " broadcast without EXTRA_USER"); + return; + } + + if (Intent.ACTION_USER_ADDED.equals(action)) { + mCallback.onUserAdded(user); + } else if (Intent.ACTION_USER_REMOVED.equals(action)) { + mCallback.onUserRemoved(user); + } else { + Log.wtf(TAG, "received unexpected intent: " + action); + } + } + }; +} diff --git a/service/src/com/android/server/connectivity/PermissionMonitor.java b/service/src/com/android/server/connectivity/PermissionMonitor.java index bb0ee83076..f055c5df08 100755 --- a/service/src/com/android/server/connectivity/PermissionMonitor.java +++ b/service/src/com/android/server/connectivity/PermissionMonitor.java @@ -42,10 +42,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.app.compat.CompatChanges; import android.content.AttributionSource; -import android.content.BroadcastReceiver; import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; @@ -164,49 +161,6 @@ public class PermissionMonitor { private static final int MAX_PERMISSION_UPDATE_LOGS = 40; private final SharedLog mPermissionUpdateLogs = new SharedLog(MAX_PERMISSION_UPDATE_LOGS, TAG); - private BroadcastReceiver mIntentReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - final String action = intent.getAction(); - - if (Intent.ACTION_PACKAGE_ADDED.equals(action)) { - final int uid = intent.getIntExtra(Intent.EXTRA_UID, -1); - final Uri packageData = intent.getData(); - final String packageName = - packageData != null ? packageData.getSchemeSpecificPart() : null; - onPackageAdded(packageName, uid); - } else if (Intent.ACTION_PACKAGE_REMOVED.equals(action)) { - final int uid = intent.getIntExtra(Intent.EXTRA_UID, -1); - final Uri packageData = intent.getData(); - final String packageName = - packageData != null ? packageData.getSchemeSpecificPart() : null; - onPackageRemoved(packageName, uid); - } else if (Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE.equals(action)) { - final String[] pkgList = - intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST); - onExternalApplicationsAvailable(pkgList); - } else if (Intent.ACTION_USER_ADDED.equals(action)) { - final UserHandle user = intent.getParcelableExtra(Intent.EXTRA_USER); - // User should be filled for below intents, check the existence. - if (user == null) { - Log.wtf(TAG, action + " broadcast without EXTRA_USER"); - return; - } - onUserAdded(user); - } else if (Intent.ACTION_USER_REMOVED.equals(action)) { - final UserHandle user = intent.getParcelableExtra(Intent.EXTRA_USER); - // User should be filled for below intents, check the existence. - if (user == null) { - Log.wtf(TAG, action + " broadcast without EXTRA_USER"); - return; - } - onUserRemoved(user); - } else { - Log.wtf(TAG, "received unexpected intent: " + action); - } - } - }; - /** * Dependencies of PermissionMonitor, for injection in tests. */ @@ -462,28 +416,6 @@ public class PermissionMonitor { final Handler handler = new Handler(mThread.getLooper()); final Context userAllContext = mContext.createContextAsUser(UserHandle.ALL, 0 /* flags */); - final IntentFilter intentFilter = new IntentFilter(); - intentFilter.addAction(Intent.ACTION_PACKAGE_ADDED); - intentFilter.addAction(Intent.ACTION_PACKAGE_REMOVED); - intentFilter.addDataScheme("package"); - userAllContext.registerReceiver( - mIntentReceiver, intentFilter, null /* broadcastPermission */, handler); - - // Listen to EXTERNAL_APPLICATIONS_AVAILABLE is that an app becoming available means it may - // need to gain a permission. But an app that becomes unavailable can neither gain nor lose - // permissions on that account, it just can no longer run. Thus, doesn't need to listen to - // EXTERNAL_APPLICATIONS_UNAVAILABLE. - final IntentFilter externalIntentFilter = - new IntentFilter(Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE); - userAllContext.registerReceiver( - mIntentReceiver, externalIntentFilter, null /* broadcastPermission */, handler); - - // Listen for user add/remove. - final IntentFilter userIntentFilter = new IntentFilter(); - userIntentFilter.addAction(Intent.ACTION_USER_ADDED); - userIntentFilter.addAction(Intent.ACTION_USER_REMOVED); - userAllContext.registerReceiver( - mIntentReceiver, userIntentFilter, null /* broadcastPermission */, handler); // Register UIDS_ALLOWED_ON_RESTRICTED_NETWORKS setting observer mDeps.registerContentObserver( @@ -625,8 +557,8 @@ public class PermissionMonitor { * * @hide */ - @VisibleForTesting - synchronized void onUserAdded(@NonNull UserHandle user) { + public synchronized void onUserAdded(@NonNull UserHandle user) { + ensureRunningOnHandlerThread(); mUsers.add(user); final List apps = getInstalledPackagesAsUser(user); @@ -657,8 +589,8 @@ public class PermissionMonitor { * * @hide */ - @VisibleForTesting - synchronized void onUserRemoved(@NonNull UserHandle user) { + public synchronized void onUserRemoved(@NonNull UserHandle user) { + ensureRunningOnHandlerThread(); mUsers.remove(user); // Remove uids network permissions that belongs to the user. @@ -843,8 +775,8 @@ public class PermissionMonitor { * * @hide */ - @VisibleForTesting - synchronized void onPackageAdded(@NonNull final String packageName, final int uid) { + public synchronized void onPackageAdded(@NonNull final String packageName, final int uid) { + ensureRunningOnHandlerThread(); // Update uid permission. updateAppIdTrafficPermission(uid); // Get the appId permission from all users then send the latest permission to netd. @@ -908,8 +840,8 @@ public class PermissionMonitor { * * @hide */ - @VisibleForTesting - synchronized void onPackageRemoved(@NonNull final String packageName, final int uid) { + public synchronized void onPackageRemoved(@NonNull final String packageName, final int uid) { + ensureRunningOnHandlerThread(); // Update uid permission. updateAppIdTrafficPermission(uid); if (BpfNetMaps.isAtLeast25Q2()) { @@ -1307,7 +1239,13 @@ public class PermissionMonitor { + ", remove=" + removedUids); } - private synchronized void onExternalApplicationsAvailable(String[] pkgList) { + /** + * Called when external applications are available. + * + * @param pkgList The package names of the external applications. + */ + public synchronized void onExternalApplicationsAvailable(String[] pkgList) { + ensureRunningOnHandlerThread(); if (CollectionUtils.isEmpty(pkgList)) { Log.e(TAG, "No available external application."); return; diff --git a/tests/unit/java/com/android/server/connectivity/BroadcastReceiveHelperTest.kt b/tests/unit/java/com/android/server/connectivity/BroadcastReceiveHelperTest.kt new file mode 100644 index 0000000000..88370ed307 --- /dev/null +++ b/tests/unit/java/com/android/server/connectivity/BroadcastReceiveHelperTest.kt @@ -0,0 +1,284 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.connectivity + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE +import android.content.Intent.ACTION_PACKAGE_ADDED +import android.content.Intent.ACTION_PACKAGE_REMOVED +import android.content.Intent.ACTION_PACKAGE_REPLACED +import android.content.Intent.ACTION_USER_ADDED +import android.content.Intent.ACTION_USER_REMOVED +import android.content.Intent.EXTRA_CHANGED_PACKAGE_LIST +import android.content.Intent.EXTRA_UID +import android.content.Intent.EXTRA_USER +import android.net.Uri +import android.os.Handler +import android.os.HandlerThread +import android.os.UserHandle +import com.android.compatibility.common.util.MoreMatchers.anyOrNull +import com.android.server.connectivity.BroadcastReceiveHelper.Delegate +import com.android.testutils.DevSdkIgnoreRunner +import com.android.testutils.waitForIdle +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.ArgumentMatchers.argThat +import org.mockito.ArgumentMatchers.eq +import org.mockito.Mockito.doReturn +import org.mockito.Mockito.mock +import org.mockito.Mockito.times +import org.mockito.Mockito.verify + +private const val TEST_PACKAGE_NAME = "com.example.app" +private const val TEST_UID = 1000 +private const val TIMEOUT_MS = 2000 + +private inline fun any() = org.mockito.Mockito.any(T::class.java) + +@DevSdkIgnoreRunner.MonitorThreadLeak +@RunWith(DevSdkIgnoreRunner::class) +class BroadcastReceiveHelperTest { + + private val mockContext = mock(Context::class.java) + private val mockDelegate = mock(Delegate::class.java) + + // Create an InOrder object to verify the sequence of delegate calls. + private val delegateInOrderVerifier = org.mockito.Mockito.inOrder(mockDelegate) + + // lateinit is used here because thread and handler need to be initialized in the + // @Before setUp method. + private lateinit var handlerThread: HandlerThread + private lateinit var handler: Handler + private lateinit var broadcastReceiveHelper: BroadcastReceiveHelper + private lateinit var packageReceiver: BroadcastReceiver + private lateinit var externalAppReceiver: BroadcastReceiver + private lateinit var userReceiver: BroadcastReceiver + + @Before + fun setUp() { + handlerThread = HandlerThread("TestThread") + handlerThread.start() + handler = Handler(handlerThread.looper) + broadcastReceiveHelper = BroadcastReceiveHelper(mockContext, handler, mockDelegate) + + // Capture intent receivers. + doReturn(mockContext).`when`(mockContext).createContextAsUser(UserHandle.ALL, 0) + broadcastReceiveHelper.registerReceivers() + userReceiver = captureIntentReceiver(ACTION_USER_ADDED) + packageReceiver = captureIntentReceiver(ACTION_PACKAGE_ADDED) + externalAppReceiver = captureIntentReceiver(ACTION_EXTERNAL_APPLICATIONS_AVAILABLE) + } + + private fun captureIntentReceiver(action: String): BroadcastReceiver { + val receiverCaptor = ArgumentCaptor.forClass(BroadcastReceiver::class.java) + verify(mockContext, times(1)).registerReceiver( + receiverCaptor.capture(), + argThat { it.hasAction(action) }, + anyOrNull(String::class.java), + eq(handler) + ) + // Return the first match. + return receiverCaptor.value + } + + @After + fun tearDown() { + broadcastReceiveHelper.unregisterReceivers() + handlerThread.quitSafely() + handlerThread.join() + } + + @Test + fun testPackageAdded() { + val intent = Intent(ACTION_PACKAGE_ADDED).apply { + data = Uri.fromParts("package", TEST_PACKAGE_NAME, null) + putExtra(EXTRA_UID, TEST_UID) + } + processOnHandlerThread { packageReceiver.onReceive(mockContext, intent) } + delegateInOrderVerifier.verify(mockDelegate).onPackageAdded(TEST_PACKAGE_NAME, TEST_UID) + delegateInOrderVerifier.verifyNoMoreInteractions() + } + + @Test + fun testPackageRemoved() { + val intent = Intent(ACTION_PACKAGE_REMOVED).apply { + data = Uri.fromParts("package", TEST_PACKAGE_NAME, null) + putExtra(EXTRA_UID, TEST_UID) + } + processOnHandlerThread { packageReceiver.onReceive(mockContext, intent) } + delegateInOrderVerifier.verify(mockDelegate).onPackageRemoved(TEST_PACKAGE_NAME, TEST_UID) + delegateInOrderVerifier.verifyNoMoreInteractions() + } + + @Test + fun testPackageReplaced() { + val intent = Intent(ACTION_PACKAGE_REPLACED).apply { + data = Uri.fromParts("package", TEST_PACKAGE_NAME, null) + putExtra(EXTRA_UID, TEST_UID) + } + processOnHandlerThread { packageReceiver.onReceive(mockContext, intent) } + delegateInOrderVerifier.verify(mockDelegate).onPackageReplaced(TEST_PACKAGE_NAME, TEST_UID) + delegateInOrderVerifier.verifyNoMoreInteractions() + } + + @Test + fun testExternalAppsAvailable() { + val packageList = arrayOf("com.example.app1", "com.example.app2") + val intent = Intent(ACTION_EXTERNAL_APPLICATIONS_AVAILABLE).apply { + putExtra(EXTRA_CHANGED_PACKAGE_LIST, packageList) + } + processOnHandlerThread { externalAppReceiver.onReceive(mockContext, intent) } + delegateInOrderVerifier.verify(mockDelegate).onExternalApplicationsAvailable(packageList) + delegateInOrderVerifier.verifyNoMoreInteractions() + } + + @Test + fun testUserAdded() { + val userHandle = mock(UserHandle::class.java) + val intent = Intent(ACTION_USER_ADDED).apply { + putExtra(EXTRA_USER, userHandle) + } + processOnHandlerThread { userReceiver.onReceive(mockContext, intent) } + delegateInOrderVerifier.verify(mockDelegate).onUserAdded(userHandle) + delegateInOrderVerifier.verifyNoMoreInteractions() + } + + @Test + fun testUserRemoved() { + val userHandle = mock(UserHandle::class.java) + val intent = Intent(ACTION_USER_REMOVED).apply { + putExtra(EXTRA_USER, userHandle) + } + processOnHandlerThread { userReceiver.onReceive(mockContext, intent) } + delegateInOrderVerifier.verify(mockDelegate).onUserRemoved(userHandle) + delegateInOrderVerifier.verifyNoMoreInteractions() + } + + @Test + fun testUnregisterReceivers() { + broadcastReceiveHelper.unregisterReceivers() + verify(mockContext).unregisterReceiver(userReceiver) + verify(mockContext).unregisterReceiver(packageReceiver) + verify(mockContext).unregisterReceiver(externalAppReceiver) + } + + @Test + fun testBroadcastOrder() { + val numEvents = 50 + val random = java.util.Random() + val eventList = mutableListOf() + + // Function to append a test package name extra. + fun Intent.withPackageNameForIndex(index: Int): Intent { + data = Uri.fromParts("package", "com.example.app$index", null) + putExtra(EXTRA_UID, index) + return this + } + + // Function to append a mock UserHandle extra. + fun Intent.withUserHandleForIndex(index: Int): Intent { + putExtra(EXTRA_USER, UserHandle.of(index)) + return this + } + + // Generate a list of 50 random actions and their corresponding intents. + repeat(numEvents) { index -> + val intent = when (random.nextInt(6)) { + 0 -> Intent(ACTION_PACKAGE_ADDED).withPackageNameForIndex(index) + 1 -> Intent(ACTION_PACKAGE_REMOVED).withPackageNameForIndex(index) + 2 -> Intent(ACTION_PACKAGE_REPLACED).withPackageNameForIndex(index) + 3 -> Intent(ACTION_EXTERNAL_APPLICATIONS_AVAILABLE).apply { + val packageList = arrayOf("com.example.app$index") + putExtra(EXTRA_CHANGED_PACKAGE_LIST, packageList) + } + + 4 -> Intent(ACTION_USER_ADDED).withUserHandleForIndex(index) + 5 -> Intent(ACTION_USER_REMOVED).withUserHandleForIndex(index) + + else -> throw IllegalStateException("Unexpected random number") + } + eventList.add(intent) + } + + // Post all intents to the handler thread at once. + eventList.forEach { + handler.post { + when (it.action) { + ACTION_PACKAGE_ADDED, ACTION_PACKAGE_REMOVED, ACTION_PACKAGE_REPLACED -> + packageReceiver.onReceive(mockContext, it) + + ACTION_EXTERNAL_APPLICATIONS_AVAILABLE -> + externalAppReceiver.onReceive(mockContext, it) + + ACTION_USER_ADDED, ACTION_USER_REMOVED -> + userReceiver.onReceive(mockContext, it) + } + } + } + + // Wait for all intents to be processed at once. + handler.waitForIdle(TIMEOUT_MS) + + // Verify the order of delegate calls. + eventList.forEach { + when (it.action) { + ACTION_PACKAGE_ADDED -> delegateInOrderVerifier.verify(mockDelegate) + .onPackageAdded( + it.data?.schemeSpecificPart!!, + it.getIntExtra(EXTRA_UID, -1) + ) + + ACTION_PACKAGE_REMOVED -> delegateInOrderVerifier.verify(mockDelegate) + .onPackageRemoved( + it.data?.schemeSpecificPart!!, + it.getIntExtra(EXTRA_UID, -1) + ) + + ACTION_PACKAGE_REPLACED -> delegateInOrderVerifier.verify(mockDelegate) + .onPackageReplaced( + it.data?.schemeSpecificPart!!, + it.getIntExtra(EXTRA_UID, -1) + ) + + ACTION_EXTERNAL_APPLICATIONS_AVAILABLE -> + delegateInOrderVerifier.verify(mockDelegate) + .onExternalApplicationsAvailable(it.getStringArrayExtra( + EXTRA_CHANGED_PACKAGE_LIST + )!!) + + ACTION_USER_ADDED -> delegateInOrderVerifier.verify(mockDelegate) + .onUserAdded(it.getParcelableExtra(EXTRA_USER)!!) + + ACTION_USER_REMOVED -> delegateInOrderVerifier.verify(mockDelegate) + .onUserRemoved(it.getParcelableExtra(EXTRA_USER)!!) + } + } + + // Ensure no other calls were made to the delegate. + delegateInOrderVerifier.verifyNoMoreInteractions() + } + + private fun processOnHandlerThread(function: Runnable) { + handler.post { function.run() } + handler.waitForIdle(HANDLER_TIMEOUT_MS) + } +} diff --git a/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java b/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java index ec9c6b0e48..3d474dfd82 100644 --- a/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java +++ b/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java @@ -47,7 +47,6 @@ import static android.permission.PermissionManager.PERMISSION_GRANTED; import static com.android.server.connectivity.PermissionMonitor.isHigherNetworkPermission; import static com.android.testutils.TestPermissionUtil.runAsShell; -import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo; import static junit.framework.Assert.fail; @@ -76,9 +75,7 @@ import static org.mockito.Mockito.when; import android.compat.testing.PlatformCompatChangeRule; import android.content.AttributionSource; -import android.content.BroadcastReceiver; import android.content.Context; -import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; @@ -109,6 +106,7 @@ import com.android.networkstack.apishim.ProcessShimImpl; import com.android.networkstack.apishim.common.ProcessShim; import com.android.server.BpfNetMaps; import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo; import com.android.testutils.DevSdkIgnoreRunner; import com.android.testutils.HandlerUtils; @@ -361,6 +359,10 @@ public class PermissionMonitorTest { processOnHandlerThread(() -> mPermissionMonitor.onPackageRemoved(packageName, uid)); } + private void onExternalApplicationsAvailable(String [] pkgList) { + processOnHandlerThread(() -> mPermissionMonitor.onExternalApplicationsAvailable(pkgList)); + } + private void sendAppIdsTrafficPermission(SparseIntArray netdPermissionsAppIds) { processOnHandlerThread(() -> mPermissionMonitor.sendAppIdsTrafficPermission(netdPermissionsAppIds)); @@ -1433,27 +1435,6 @@ public class PermissionMonitorTest { mBpfMapMonitor.expectTrafficPerm(PERMISSION_TRAFFIC_ALL, MOCK_APPID2); } - private BroadcastReceiver expectBroadcastReceiver(String... actions) { - final ArgumentCaptor receiverCaptor = - ArgumentCaptor.forClass(BroadcastReceiver.class); - verify(mContext, times(1)).registerReceiver(receiverCaptor.capture(), - argThat(filter -> { - for (String action : actions) { - if (!filter.hasAction(action)) { - return false; - } - } - return true; - }), any(), any()); - final BroadcastReceiver originalReceiver = receiverCaptor.getValue(); - return new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - processOnHandlerThread(() -> originalReceiver.onReceive(context, intent)); - } - }; - } - private void processOnHandlerThread(Runnable function) { final Handler handler = mHandlerThread.getThreadHandler(); handler.post(() -> function.run()); @@ -1462,26 +1443,13 @@ public class PermissionMonitorTest { @Test @EnableCompatChanges(RESTRICT_LOCAL_NETWORK) - public void testIntentReceiver() throws Exception { + public void testUidPermissionWhenPackageAddedRemoved() throws Exception { startMonitoring(); - final BroadcastReceiver receiver = expectBroadcastReceiver( - Intent.ACTION_PACKAGE_ADDED, Intent.ACTION_PACKAGE_REMOVED); - // Verify receiving PACKAGE_ADDED intent. - final Intent addedIntent = new Intent(Intent.ACTION_PACKAGE_ADDED, - Uri.fromParts("package", MOCK_PACKAGE1, null /* fragment */)); - addedIntent.putExtra(Intent.EXTRA_UID, MOCK_UID11); - buildAndMockPackageInfoWithPermissions(MOCK_PACKAGE1, MOCK_UID11, INTERNET, - UPDATE_DEVICE_STATS); - receiver.onReceive(mContext, addedIntent); + // Add/Remove package and verify uid permissions. + addPackage(MOCK_PACKAGE1, MOCK_UID11, INTERNET, UPDATE_DEVICE_STATS); mBpfMapMonitor.expectTrafficPerm(PERMISSION_TRAFFIC_ALL, MOCK_APPID1); - - // Verify receiving PACKAGE_REMOVED intent. - when(mPackageManager.getPackagesForUid(MOCK_UID11)).thenReturn(new String[]{}); - final Intent removedIntent = new Intent(Intent.ACTION_PACKAGE_REMOVED, - Uri.fromParts("package", MOCK_PACKAGE1, null /* fragment */)); - removedIntent.putExtra(Intent.EXTRA_UID, MOCK_UID11); - receiver.onReceive(mContext, removedIntent); + removePackage(MOCK_PACKAGE1, MOCK_UID11); mBpfMapMonitor.expectTrafficPerm(PERMISSION_UNINSTALLED, MOCK_APPID1); } @@ -1634,17 +1602,12 @@ public class PermissionMonitorTest { mNetdMonitor.expectNoNetworkPerm(new UserHandle[]{MOCK_USER1}, MOCK_APPID1, MOCK_APPID2); mBpfMapMonitor.expectTrafficPerm(PERMISSION_NONE, MOCK_APPID1, MOCK_APPID2); - final BroadcastReceiver receiver = expectBroadcastReceiver( - Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE); - // Verify receiving EXTERNAL_APPLICATIONS_AVAILABLE intent and update permission to netd. - final Intent externalIntent = new Intent(Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE); - externalIntent.putExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST, - new String[] { MOCK_PACKAGE1 , MOCK_PACKAGE2}); + // Call onExternalApplicationsAvailable and verify update permission to netd. buildAndMockPackageInfoWithPermissions(MOCK_PACKAGE1, MOCK_UID11, CONNECTIVITY_USE_RESTRICTED_NETWORKS, INTERNET); buildAndMockPackageInfoWithPermissions(MOCK_PACKAGE2, MOCK_UID12, CHANGE_NETWORK_STATE, UPDATE_DEVICE_STATS); - receiver.onReceive(mContext, externalIntent); + onExternalApplicationsAvailable(new String[] { MOCK_PACKAGE1 , MOCK_PACKAGE2}); mNetdMonitor.expectNetworkPerm(PERMISSION_SYSTEM, new UserHandle[]{MOCK_USER1}, MOCK_APPID1); mNetdMonitor.expectNetworkPerm(PERMISSION_NETWORK, new UserHandle[]{MOCK_USER1}, @@ -1658,8 +1621,6 @@ public class PermissionMonitorTest { public void testOnExternalApplicationsAvailable_AppsNotRegisteredOnStartMonitoring() throws Exception { startMonitoring(); - final BroadcastReceiver receiver = expectBroadcastReceiver( - Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE); // Initial the permission state. MOCK_PACKAGE1 and MOCK_PACKAGE2 are installed on external // and have different uids. There has no permission for both uids. @@ -1668,11 +1629,8 @@ public class PermissionMonitorTest { buildAndMockPackageInfoWithPermissions(MOCK_PACKAGE2, MOCK_UID12, CHANGE_NETWORK_STATE, UPDATE_DEVICE_STATS); - // Verify receiving EXTERNAL_APPLICATIONS_AVAILABLE intent and update permission to netd. - final Intent externalIntent = new Intent(Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE); - externalIntent.putExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST, - new String[] { MOCK_PACKAGE1 , MOCK_PACKAGE2}); - receiver.onReceive(mContext, externalIntent); + // Call onExternalApplicationsAvailable and verify update permission to netd. + onExternalApplicationsAvailable(new String[] { MOCK_PACKAGE1 , MOCK_PACKAGE2}); mNetdMonitor.expectNetworkPerm(PERMISSION_SYSTEM, new UserHandle[]{MOCK_USER1}, MOCK_APPID1); mNetdMonitor.expectNetworkPerm(PERMISSION_NETWORK, new UserHandle[]{MOCK_USER1}, @@ -1695,14 +1653,10 @@ public class PermissionMonitorTest { mNetdMonitor.expectNoNetworkPerm(new UserHandle[]{MOCK_USER1}, MOCK_APPID1); mBpfMapMonitor.expectTrafficPerm(PERMISSION_NONE, MOCK_APPID1); - final BroadcastReceiver receiver = expectBroadcastReceiver( - Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE); - // Verify receiving EXTERNAL_APPLICATIONS_AVAILABLE intent and update permission to netd. - final Intent externalIntent = new Intent(Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE); - externalIntent.putExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST, new String[] {MOCK_PACKAGE1}); + // Call onExternalApplicationsAvailable and verify update permission to netd. buildAndMockPackageInfoWithPermissions(MOCK_PACKAGE1, MOCK_UID11, CHANGE_NETWORK_STATE); buildAndMockPackageInfoWithPermissions(MOCK_PACKAGE2, MOCK_UID11, UPDATE_DEVICE_STATS); - receiver.onReceive(mContext, externalIntent); + onExternalApplicationsAvailable(new String[] {MOCK_PACKAGE1}); mNetdMonitor.expectNetworkPerm(PERMISSION_NETWORK, new UserHandle[]{MOCK_USER1}, MOCK_APPID1); mBpfMapMonitor.expectTrafficPerm(PERMISSION_UPDATE_DEVICE_STATS, MOCK_APPID1); @@ -1724,16 +1678,12 @@ public class PermissionMonitorTest { MOCK_APPID1); mBpfMapMonitor.expectTrafficPerm(PERMISSION_INTERNET, MOCK_APPID1); - final BroadcastReceiver receiver = expectBroadcastReceiver( - Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE); - // Verify receiving EXTERNAL_APPLICATIONS_AVAILABLE intent and update permission to netd. - final Intent externalIntent = new Intent(Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE); - externalIntent.putExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST, new String[] {MOCK_PACKAGE1}); + // Call onExternalApplicationsAvailable and verify update permission to netd. buildAndMockPackageInfoWithPermissions(MOCK_PACKAGE1, MOCK_UID11, CONNECTIVITY_USE_RESTRICTED_NETWORKS, UPDATE_DEVICE_STATS); buildAndMockPackageInfoWithPermissions(MOCK_PACKAGE2, MOCK_UID11, CHANGE_NETWORK_STATE, INTERNET); - receiver.onReceive(mContext, externalIntent); + onExternalApplicationsAvailable(new String[] {MOCK_PACKAGE1}); mNetdMonitor.expectNetworkPerm(PERMISSION_SYSTEM, new UserHandle[]{MOCK_USER1}, MOCK_APPID1); mBpfMapMonitor.expectTrafficPerm(PERMISSION_TRAFFIC_ALL, MOCK_APPID1);