Improved Entitlement UI Launch for multiple admin users
This change addresses the launch of entitlement UI activities
in Headless System User Mode (HSUM) on multi-user devices.
HSUM is a configuration where the system user (user 0) does
not represent a human user and primarily runs system services.
In this mode, the first user set up on the device is not user 0.
Modified the EntitlementManager logic to handle the
launch of the entitlement UI in multi-user environments,
specifically addressing scenarios where HSUM is enabled, which
includes:
1. Always launch the UI to the foreground current user.
2. Check whether the current user is allowed to show the UI.
This ensures the UI is presented to the appropriate user.
Test: Manual
1. Mock requires entitlement
2. adb shell cmd user set-system-user-mode-emulation headless
3. enable hotspot
Test: atest TetheringTests:EntitlementManagerTest
Fix: 363972541
Change-Id: I950d678741a3f1714f347206fa161bcd7538b402
diff --git a/Tethering/AndroidManifest.xml b/Tethering/AndroidManifest.xml
index 6a363b0..2a6f6d5 100644
--- a/Tethering/AndroidManifest.xml
+++ b/Tethering/AndroidManifest.xml
@@ -32,8 +32,10 @@
<uses-permission android:name="android.permission.BLUETOOTH_PRIVILEGED" />
<uses-permission android:name="android.permission.BROADCAST_STICKY" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
+ <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS"/>
<uses-permission android:name="android.permission.MANAGE_USB" />
<uses-permission android:name="android.permission.MODIFY_PHONE_STATE" />
+ <uses-permission android:name="android.permission.QUERY_USERS"/>
<uses-permission android:name="android.permission.READ_DEVICE_CONFIG" />
<uses-permission android:name="android.permission.READ_NETWORK_USAGE_HISTORY" />
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
diff --git a/Tethering/apex/permissions/permissions.xml b/Tethering/apex/permissions/permissions.xml
index f26a961..fcb287e 100644
--- a/Tethering/apex/permissions/permissions.xml
+++ b/Tethering/apex/permissions/permissions.xml
@@ -18,8 +18,10 @@
<permissions>
<privapp-permissions package="com.android.networkstack.tethering">
<permission name="android.permission.BLUETOOTH_PRIVILEGED" />
+ <permission name="android.permission.INTERACT_ACROSS_USERS"/>
<permission name="android.permission.MANAGE_USB"/>
<permission name="android.permission.MODIFY_PHONE_STATE"/>
+ <permission name="android.permission.QUERY_USERS"/>
<permission name="android.permission.READ_NETWORK_USAGE_HISTORY"/>
<permission name="android.permission.TETHER_PRIVILEGED"/>
<permission name="android.permission.UPDATE_APP_OPS_STATS"/>
diff --git a/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java b/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java
index b88b13b..e42a516 100644
--- a/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java
+++ b/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java
@@ -36,6 +36,7 @@
import static com.android.networkstack.apishim.ConstantsShim.ACTION_TETHER_UNSUPPORTED_CARRIER_UI;
import static com.android.networkstack.apishim.ConstantsShim.RECEIVER_NOT_EXPORTED;
+import android.app.ActivityManager;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
@@ -50,9 +51,13 @@
import android.os.ResultReceiver;
import android.os.SystemClock;
import android.os.SystemProperties;
+import android.os.UserHandle;
+import android.os.UserManager;
import android.provider.Settings;
import android.util.SparseIntArray;
+import androidx.annotation.Nullable;
+
import com.android.internal.annotations.VisibleForTesting;
import com.android.modules.utils.build.SdkLevel;
import com.android.net.module.util.SharedLog;
@@ -406,12 +411,20 @@
return intent;
}
+ @VisibleForTesting
+ int getCurrentUser() {
+ return ActivityManager.getCurrentUser();
+ }
+
/**
* Run the UI-enabled tethering provisioning check.
* @param type tethering type from TetheringManager.TETHERING_{@code *}
- * @param subId default data subscription ID.
* @param receiver to receive entitlement check result.
+ *
+ * @return the broadcast intent, or null if the current user is not allowed to
+ * perform entitlement check.
*/
+ @Nullable
@VisibleForTesting
protected Intent runUiTetherProvisioning(int type, final TetheringConfiguration config,
ResultReceiver receiver) {
@@ -423,9 +436,28 @@
intent.putExtra(EXTRA_PROVISION_CALLBACK, receiver);
intent.putExtra(EXTRA_TETHER_SUBID, config.activeDataSubId);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- // Only launch entitlement UI for system user. Entitlement UI should not appear for other
- // user because only admin user is allowed to change tethering.
- mContext.startActivity(intent);
+
+ // Only launch entitlement UI for the current user if it is allowed to
+ // change tethering. This usually means the system user or the admin users in HSUM.
+ if (SdkLevel.isAtLeastT()) {
+ // Create a user context for the current foreground user as UserManager#isAdmin()
+ // operates on the context user.
+ final int currentUserId = getCurrentUser();
+ final UserHandle currentUser = UserHandle.of(currentUserId);
+ final Context userContext = mContext.createContextAsUser(currentUser, 0);
+ final UserManager userManager = userContext.getSystemService(UserManager.class);
+
+ if (userManager.isAdminUser()) {
+ mContext.startActivityAsUser(intent, currentUser);
+ } else {
+ mLog.e("Current user (" + currentUserId
+ + ") is not allowed to perform entitlement check.");
+ return null;
+ }
+ } else {
+ // For T- devices, there is no other admin user other than the system user.
+ mContext.startActivity(intent);
+ }
return intent;
}
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/EntitlementManagerTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/EntitlementManagerTest.java
index c2e1617..9fa7004 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/EntitlementManagerTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/EntitlementManagerTest.java
@@ -38,6 +38,7 @@
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
import static com.android.networkstack.apishim.ConstantsShim.KEY_CARRIER_SUPPORTS_TETHERING_BOOL;
+import static com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
import static com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
@@ -71,6 +72,7 @@
import android.os.ResultReceiver;
import android.os.SystemProperties;
import android.os.UserHandle;
+import android.os.UserManager;
import android.os.test.TestLooper;
import android.provider.DeviceConfig;
import android.provider.Settings;
@@ -114,6 +116,7 @@
@Mock private EntitlementManager
.OnTetherProvisioningFailedListener mTetherProvisioningFailedListener;
@Mock private AlarmManager mAlarmManager;
+ @Mock private UserManager mUserManager;
@Mock private PendingIntent mAlarmIntent;
@Rule
@@ -143,9 +146,21 @@
@Override
public Object getSystemService(String name) {
if (Context.ALARM_SERVICE.equals(name)) return mAlarmManager;
+ if (Context.USER_SERVICE.equals(name)) return mUserManager;
return super.getSystemService(name);
}
+
+ @Override
+ public String getSystemServiceName(Class<?> serviceClass) {
+ if (UserManager.class.equals(serviceClass)) return Context.USER_SERVICE;
+ return super.getSystemServiceName(serviceClass);
+ }
+
+ @Override
+ public Context createContextAsUser(UserHandle user, int flags) {
+ return mMockContext; // Return self for easier test injection.
+ }
}
public class WrappedEntitlementManager extends EntitlementManager {
@@ -168,8 +183,10 @@
protected Intent runUiTetherProvisioning(int type,
final TetheringConfiguration config, final ResultReceiver receiver) {
Intent intent = super.runUiTetherProvisioning(type, config, receiver);
- assertUiTetherProvisioningIntent(type, config, receiver, intent);
- uiProvisionCount++;
+ if (intent != null) {
+ assertUiTetherProvisioningIntent(type, config, receiver, intent);
+ uiProvisionCount++;
+ }
receiver.send(fakeEntitlementResult, null);
return intent;
}
@@ -217,6 +234,13 @@
assertEquals(TEST_PACKAGE_NAME, pkgName);
return mAlarmIntent;
}
+
+ @Override
+ int getCurrentUser() {
+ // The result is not used, just override to bypass the need of accessing
+ // the static method.
+ return 0;
+ }
}
@Before
@@ -253,6 +277,7 @@
false);
when(mResources.getString(R.string.config_wifi_tether_enable)).thenReturn("");
when(mLog.forSubComponent(anyString())).thenReturn(mLog);
+ doReturn(true).when(mUserManager).isAdminUser();
mMockContext = new MockContext(mContext);
mPermissionChangeCallback = spy(() -> { });
@@ -619,6 +644,33 @@
.onTetherProvisioningFailed(TETHERING_WIFI, FAILED_TETHERING_REASON);
}
+ @IgnoreUpTo(SC_V2)
+ @Test
+ public void testUiProvisioningMultiUser_aboveT() {
+ doTestUiProvisioningMultiUser(true, 1);
+ doTestUiProvisioningMultiUser(false, 0);
+ }
+
+ @IgnoreAfter(SC_V2)
+ @Test
+ public void testUiProvisioningMultiUser_belowT() {
+ doTestUiProvisioningMultiUser(true, 1);
+ doTestUiProvisioningMultiUser(false, 1);
+ }
+
+ private void doTestUiProvisioningMultiUser(boolean isAdminUser, int expectedUiProvisionCount) {
+ setupForRequiredProvisioning();
+ doReturn(isAdminUser).when(mUserManager).isAdminUser();
+
+ mEnMgr.reset();
+ mEnMgr.fakeEntitlementResult = TETHER_ERROR_NO_ERROR;
+ mEnMgr.notifyUpstream(true);
+ mLooper.dispatchAll();
+ mEnMgr.startProvisioningIfNeeded(TETHERING_USB, true);
+ mLooper.dispatchAll();
+ assertEquals(expectedUiProvisionCount, mEnMgr.uiProvisionCount);
+ }
+
@Test
public void testsetExemptedDownstreamType() throws Exception {
setupForRequiredProvisioning();