diff --git a/service/Android.bp b/service/Android.bp index 1af9070fde..403ba7d33a 100644 --- a/service/Android.bp +++ b/service/Android.bp @@ -179,6 +179,8 @@ java_library { "unsupportedappusage", "ServiceConnectivityResources", "framework-statsd", + "framework-permission", + "framework-permission-s", ], static_libs: [ // Do not add libs here if they are already included @@ -265,6 +267,8 @@ java_defaults { "framework-tethering.impl", "framework-wifi", "libprotobuf-java-nano", + "framework-permission", + "framework-permission-s", ], jarjar_rules: ":connectivity-jarjar-rules", apex_available: [ diff --git a/service/src/com/android/server/connectivity/SatelliteAccessController.java b/service/src/com/android/server/connectivity/SatelliteAccessController.java new file mode 100644 index 0000000000..0968afffbf --- /dev/null +++ b/service/src/com/android/server/connectivity/SatelliteAccessController.java @@ -0,0 +1,194 @@ +/* + * 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.server.connectivity; + +import android.Manifest; +import android.annotation.NonNull; +import android.app.role.OnRoleHoldersChangedListener; +import android.app.role.RoleManager; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.os.Handler; +import android.os.Process; +import android.os.UserHandle; +import android.util.ArraySet; +import android.util.Log; + +import com.android.internal.annotations.VisibleForTesting; + +import java.util.List; +import java.util.Set; +import java.util.concurrent.Executor; +import java.util.function.Consumer; + +/** + * Tracks the uid of all the default messaging application which are role_sms role and + * satellite_communication permission complaint and requests ConnectivityService to create multi + * layer request with satellite internet access support for the default message application. + * @hide + */ +public class SatelliteAccessController { + private static final String TAG = SatelliteAccessController.class.getSimpleName(); + private final PackageManager mPackageManager; + private final Dependencies mDeps; + private final DefaultMessageRoleListener mDefaultMessageRoleListener; + private final Consumer> mCallback; + private final Set mSatelliteNetworkPreferredUidCache = new ArraySet<>(); + private final Handler mConnectivityServiceHandler; + + /** + * Monitor {@link android.app.role.OnRoleHoldersChangedListener#onRoleHoldersChanged(String, + * UserHandle)}, + * + */ + private final class DefaultMessageRoleListener + implements OnRoleHoldersChangedListener { + @Override + public void onRoleHoldersChanged(String role, UserHandle user) { + if (RoleManager.ROLE_SMS.equals(role)) { + Log.i(TAG, "ROLE_SMS Change detected "); + onRoleSmsChanged(); + } + } + + public void register() { + try { + mDeps.addOnRoleHoldersChangedListenerAsUser( + mConnectivityServiceHandler::post, this, UserHandle.ALL); + } catch (RuntimeException e) { + Log.e(TAG, "Could not register satellite controller listener due to " + e); + } + } + } + + public SatelliteAccessController(@NonNull final Context c, + Consumer> callback, + @NonNull final Handler connectivityServiceInternalHandler) { + this(c, new Dependencies(c), callback, connectivityServiceInternalHandler); + } + + public static class Dependencies { + private final RoleManager mRoleManager; + + private Dependencies(Context context) { + mRoleManager = context.getSystemService(RoleManager.class); + } + + /** See {@link RoleManager#getRoleHolders(String)} */ + public List getRoleHolders(String roleName) { + return mRoleManager.getRoleHolders(roleName); + } + + /** See {@link RoleManager#addOnRoleHoldersChangedListenerAsUser} */ + public void addOnRoleHoldersChangedListenerAsUser(@NonNull Executor executor, + @NonNull OnRoleHoldersChangedListener listener, UserHandle user) { + mRoleManager.addOnRoleHoldersChangedListenerAsUser(executor, listener, user); + } + } + + @VisibleForTesting + SatelliteAccessController(@NonNull final Context c, @NonNull final Dependencies deps, + Consumer> callback, + @NonNull final Handler connectivityServiceInternalHandler) { + mDeps = deps; + mPackageManager = c.getPackageManager(); + mDefaultMessageRoleListener = new DefaultMessageRoleListener(); + mCallback = callback; + mConnectivityServiceHandler = connectivityServiceInternalHandler; + } + + private void updateSatelliteNetworkPreferredUidListCache(List packageNames) { + for (String packageName : packageNames) { + // Check if SATELLITE_COMMUNICATION permission is enabled for default sms application + // package before adding it part of satellite network preferred uid cache list. + if (isSatellitePermissionEnabled(packageName)) { + mSatelliteNetworkPreferredUidCache.add(getUidForPackage(packageName)); + } + } + } + + //Check if satellite communication is enabled for the package + private boolean isSatellitePermissionEnabled(String packageName) { + if (mPackageManager != null) { + return mPackageManager.checkPermission( + Manifest.permission.SATELLITE_COMMUNICATION, packageName) + == PackageManager.PERMISSION_GRANTED; + } + return false; + } + + private int getUidForPackage(String pkgName) { + if (pkgName == null) { + return Process.INVALID_UID; + } + try { + if (mPackageManager != null) { + ApplicationInfo applicationInfo = mPackageManager.getApplicationInfo(pkgName, 0); + if (applicationInfo != null) { + return applicationInfo.uid; + } + } + } catch (PackageManager.NameNotFoundException exception) { + Log.e(TAG, "Unable to find uid for package: " + pkgName); + } + return Process.INVALID_UID; + } + + //on Role sms change triggered by OnRoleHoldersChangedListener() + private void onRoleSmsChanged() { + final List packageNames = getRoleSmsChangedPackageName(); + + // Create a new Set + Set previousSatellitePreferredUid = new ArraySet<>( + mSatelliteNetworkPreferredUidCache); + + mSatelliteNetworkPreferredUidCache.clear(); + + if (packageNames != null) { + Log.i(TAG, "role_sms_packages: " + packageNames); + // On Role change listener, update the satellite network preferred uid cache list + updateSatelliteNetworkPreferredUidListCache(packageNames); + Log.i(TAG, "satellite_preferred_uid: " + mSatelliteNetworkPreferredUidCache); + } else { + Log.wtf(TAG, "package name was found null"); + } + + // on Role change, update the multilayer request at ConnectivityService with updated + // satellite network preferred uid cache list if changed or to revoke for previous default + // sms app + if (!mSatelliteNetworkPreferredUidCache.equals(previousSatellitePreferredUid)) { + Log.i(TAG, "update multi layer request"); + mCallback.accept(mSatelliteNetworkPreferredUidCache); + } + } + + private List getRoleSmsChangedPackageName() { + try { + return mDeps.getRoleHolders(RoleManager.ROLE_SMS); + } catch (RuntimeException e) { + Log.wtf(TAG, "Could not get package name at role sms change update due to: " + e); + return null; + } + } + + /** Register OnRoleHoldersChangedListener */ + public void start() { + mConnectivityServiceHandler.post(this::onRoleSmsChanged); + mDefaultMessageRoleListener.register(); + } +} diff --git a/tests/unit/java/com/android/server/connectivity/SatelliteAccessControllerTest.kt b/tests/unit/java/com/android/server/connectivity/SatelliteAccessControllerTest.kt new file mode 100644 index 0000000000..64a515a781 --- /dev/null +++ b/tests/unit/java/com/android/server/connectivity/SatelliteAccessControllerTest.kt @@ -0,0 +1,176 @@ +/* + * 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.server.connectivity + +import android.Manifest +import android.app.role.OnRoleHoldersChangedListener +import android.app.role.RoleManager +import android.content.Context +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager +import android.os.Build +import android.os.Handler +import android.os.UserHandle +import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo +import com.android.testutils.DevSdkIgnoreRunner +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.ArgumentMatchers.eq +import org.mockito.Mockito.doReturn +import org.mockito.Mockito.mock +import org.mockito.Mockito.never +import org.mockito.Mockito.times +import org.mockito.Mockito.verify +import java.util.concurrent.Executor +import java.util.function.Consumer +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +private const val DEFAULT_MESSAGING_APP1 = "default_messaging_app_1" +private const val DEFAULT_MESSAGING_APP2 = "default_messaging_app_2" +private const val DEFAULT_MESSAGING_APP1_UID = 1234 +private const val DEFAULT_MESSAGING_APP2_UID = 5678 + +@RunWith(DevSdkIgnoreRunner::class) +@IgnoreUpTo(Build.VERSION_CODES.TIRAMISU) +class SatelliteAccessControllerTest { + private val context = mock(Context::class.java) + private val mPackageManager = mock(PackageManager::class.java) + private val mHandler = mock(Handler::class.java) + private val mRoleManager = + mock(SatelliteAccessController.Dependencies::class.java) + private val mCallback = mock(Consumer::class.java) as Consumer> + private val mSatelliteAccessController by lazy { + SatelliteAccessController(context, mRoleManager, mCallback, mHandler)} + private var mRoleHolderChangedListener: OnRoleHoldersChangedListener? = null + @Before + @Throws(PackageManager.NameNotFoundException::class) + fun setup() { + doReturn(mPackageManager).`when`(context).packageManager + doReturn(PackageManager.PERMISSION_GRANTED) + .`when`(mPackageManager) + .checkPermission(Manifest.permission.SATELLITE_COMMUNICATION, DEFAULT_MESSAGING_APP1) + doReturn(PackageManager.PERMISSION_GRANTED) + .`when`(mPackageManager) + .checkPermission(Manifest.permission.SATELLITE_COMMUNICATION, DEFAULT_MESSAGING_APP2) + + // Initialise default message application package1 + val applicationInfo1 = ApplicationInfo() + applicationInfo1.uid = DEFAULT_MESSAGING_APP1_UID + doReturn(applicationInfo1) + .`when`(mPackageManager) + .getApplicationInfo(eq(DEFAULT_MESSAGING_APP1), anyInt()) + + // Initialise default message application package2 + val applicationInfo2 = ApplicationInfo() + applicationInfo2.uid = DEFAULT_MESSAGING_APP2_UID + doReturn(applicationInfo2) + .`when`(mPackageManager) + .getApplicationInfo(eq(DEFAULT_MESSAGING_APP2), anyInt()) + + // Get registered listener using captor + val listenerCaptor = ArgumentCaptor.forClass( + OnRoleHoldersChangedListener::class.java + ) + mSatelliteAccessController.start() + verify(mRoleManager).addOnRoleHoldersChangedListenerAsUser( + any(Executor::class.java), listenerCaptor.capture(), any(UserHandle::class.java)) + mRoleHolderChangedListener = listenerCaptor.value + } + + @Test + fun test_onRoleHoldersChanged_SatellitePreferredUid_Changed() { + doReturn(listOf()).`when`(mRoleManager).getRoleHolders(RoleManager.ROLE_SMS) + val satelliteNetworkPreferredSet = + ArgumentCaptor.forClass(Set::class.java) as ArgumentCaptor> + mRoleHolderChangedListener?.onRoleHoldersChanged(RoleManager.ROLE_SMS, UserHandle.ALL) + verify(mCallback, never()).accept(satelliteNetworkPreferredSet.capture()) + + // check DEFAULT_MESSAGING_APP1 is available as satellite network preferred uid + doReturn(listOf(DEFAULT_MESSAGING_APP1)) + .`when`(mRoleManager).getRoleHolders(RoleManager.ROLE_SMS) + mRoleHolderChangedListener?.onRoleHoldersChanged(RoleManager.ROLE_SMS, UserHandle.ALL) + verify(mCallback).accept(satelliteNetworkPreferredSet.capture()) + var satelliteNetworkPreferredUids = satelliteNetworkPreferredSet.value + assertEquals(1, satelliteNetworkPreferredUids.size) + assertTrue(satelliteNetworkPreferredUids.contains(DEFAULT_MESSAGING_APP1_UID)) + assertFalse(satelliteNetworkPreferredUids.contains(DEFAULT_MESSAGING_APP2_UID)) + + // check DEFAULT_MESSAGING_APP1 and DEFAULT_MESSAGING_APP2 is available + // as satellite network preferred uid + val dmas: MutableList = ArrayList() + dmas.add(DEFAULT_MESSAGING_APP1) + dmas.add(DEFAULT_MESSAGING_APP2) + doReturn(dmas).`when`(mRoleManager).getRoleHolders(RoleManager.ROLE_SMS) + mRoleHolderChangedListener?.onRoleHoldersChanged(RoleManager.ROLE_SMS, UserHandle.ALL) + verify(mCallback, times(2)) + .accept(satelliteNetworkPreferredSet.capture()) + satelliteNetworkPreferredUids = satelliteNetworkPreferredSet.value + assertEquals(2, satelliteNetworkPreferredUids.size) + assertTrue(satelliteNetworkPreferredUids.contains(DEFAULT_MESSAGING_APP1_UID)) + assertTrue(satelliteNetworkPreferredUids.contains(DEFAULT_MESSAGING_APP2_UID)) + + // check no uid is available as satellite network preferred uid + doReturn(listOf()).`when`(mRoleManager).getRoleHolders(RoleManager.ROLE_SMS) + mRoleHolderChangedListener?.onRoleHoldersChanged(RoleManager.ROLE_SMS, UserHandle.ALL) + verify(mCallback, times(3)) + .accept(satelliteNetworkPreferredSet.capture()) + satelliteNetworkPreferredUids = satelliteNetworkPreferredSet.value + assertEquals(0, satelliteNetworkPreferredUids.size) + assertFalse(satelliteNetworkPreferredUids.contains(DEFAULT_MESSAGING_APP1_UID)) + assertFalse(satelliteNetworkPreferredUids.contains(DEFAULT_MESSAGING_APP2_UID)) + + // No Change received at OnRoleSmsChanged, check callback not triggered + doReturn(listOf()).`when`(mRoleManager).getRoleHolders(RoleManager.ROLE_SMS) + mRoleHolderChangedListener?.onRoleHoldersChanged(RoleManager.ROLE_SMS, UserHandle.ALL) + verify(mCallback, times(3)) + .accept(satelliteNetworkPreferredSet.capture()) + } + + @Test + fun test_onRoleHoldersChanged_NoSatelliteCommunicationPermission() { + doReturn(listOf()).`when`(mRoleManager).getRoleHolders(RoleManager.ROLE_SMS) + val satelliteNetworkPreferredSet = + ArgumentCaptor.forClass(Set::class.java) as ArgumentCaptor> + mRoleHolderChangedListener?.onRoleHoldersChanged(RoleManager.ROLE_SMS, UserHandle.ALL) + verify(mCallback, never()).accept(satelliteNetworkPreferredSet.capture()) + + // check DEFAULT_MESSAGING_APP1 is not available as satellite network preferred uid + // since satellite communication permission not available. + doReturn(PackageManager.PERMISSION_DENIED) + .`when`(mPackageManager) + .checkPermission(Manifest.permission.SATELLITE_COMMUNICATION, DEFAULT_MESSAGING_APP1) + doReturn(listOf(DEFAULT_MESSAGING_APP1)) + .`when`(mRoleManager).getRoleHolders(RoleManager.ROLE_SMS) + mRoleHolderChangedListener?.onRoleHoldersChanged(RoleManager.ROLE_SMS, UserHandle.ALL) + verify(mCallback, never()).accept(satelliteNetworkPreferredSet.capture()) + } + + @Test + fun test_onRoleHoldersChanged_RoleSms_NotAvailable() { + doReturn(listOf(DEFAULT_MESSAGING_APP1)) + .`when`(mRoleManager).getRoleHolders(RoleManager.ROLE_SMS) + val satelliteNetworkPreferredSet = + ArgumentCaptor.forClass(Set::class.java) as ArgumentCaptor> + mRoleHolderChangedListener?.onRoleHoldersChanged(RoleManager.ROLE_BROWSER, UserHandle.ALL) + verify(mCallback, never()).accept(satelliteNetworkPreferredSet.capture()) + } +}