Warn the user when a streamed app requests authentication

When a user is streaming an app running on a VirtualDisplay owned by a
VirtualDevice, and that app requests authentication via the
BiometricPrompt api, the streamed view of the app appears frozen and
the user may not understand that they need to go to their phone to
complete the authentication. This CL adds a toast we show in this
situation to help the user know what to do.

Bug: 246425400
Test: atest AuthServiceTest VirtualDeviceManagerServiceMockingTest
Change-Id: I5c88832c948545e9aab1eb23cca97a3165008dae
This commit is contained in:
Antony Sargent
2023-05-03 15:23:58 -07:00
parent 29fd4cf2d8
commit 734a40cf8f
8 changed files with 222 additions and 4 deletions

View File

@@ -5403,11 +5403,11 @@
<string name="app_streaming_blocked_message" product="tablet">This can’t be accessed on your <xliff:g id="device" example="Chromebook">%1$s</xliff:g> at this time. Try on your tablet instead.</string>
<!-- Message shown when an app is blocked from being streamed to a remote device. [CHAR LIMIT=NONE] -->
<string name="app_streaming_blocked_message" product="default">This can’t be accessed on your <xliff:g id="device" example="Chromebook">%1$s</xliff:g> at this time. Try on your phone instead.</string>
<!-- Message shown when the fingerprint permission is blocked from being streamed to a remote device. [CHAR LIMIT=NONE] -->
<!-- Message shown when an app being streamed to another device requests authentication such as via the biometrics API, and the user needs to complete the on their device. [CHAR LIMIT=NONE] -->
<string name="app_streaming_blocked_message_for_fingerprint_dialog" product="tv">This app is requesting additional security. Try on your Android TV device instead.</string>
<!-- Message shown when the fingerprint permission is blocked from being streamed to a remote device. [CHAR LIMIT=NONE] -->
<!-- Message shown when an app being streamed to another device requests authentication such as via the biometrics API, and the user needs to complete the on their device. [CHAR LIMIT=NONE] -->
<string name="app_streaming_blocked_message_for_fingerprint_dialog" product="tablet">This app is requesting additional security. Try on your tablet instead.</string>
<!-- Message shown when the fingerprint permission is blocked from being streamed to a remote device. [CHAR LIMIT=NONE] -->
<!-- Message shown when an app being streamed to another device requests authentication such as via the biometrics API, and the user needs to complete the on their device. [CHAR LIMIT=NONE] -->
<string name="app_streaming_blocked_message_for_fingerprint_dialog" product="default">This app is requesting additional security. Try on your phone instead.</string>
<!-- Message shown when the settings is blocked from being streamed to a remote device. [CHAR LIMIT=NONE] -->
<string name="app_streaming_blocked_message_for_settings_dialog" product="tv">This can’t be accessed on your <xliff:g id="device" example="Chromebook">%1$s</xliff:g>. Try on your Android TV device instead.</string>

View File

@@ -55,6 +55,7 @@ import android.util.SparseArray;
import android.view.Display;
import android.widget.Toast;
import com.android.internal.R;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.DumpUtils;
@@ -156,7 +157,7 @@ public class VirtualDeviceManagerService extends SystemService {
VirtualDeviceImpl virtualDevice = virtualDevicesSnapshot.get(i);
virtualDevice.showToastWhereUidIsRunning(appUid,
getContext().getString(
com.android.internal.R.string.vdm_camera_access_denied,
R.string.vdm_camera_access_denied,
virtualDevice.getDisplayName()),
Toast.LENGTH_LONG, Looper.myLooper());
}
@@ -622,6 +623,18 @@ public class VirtualDeviceManagerService extends SystemService {
}
}
@Override
public void onAuthenticationPrompt(int uid) {
synchronized (mVirtualDeviceManagerLock) {
for (int i = 0; i < mVirtualDevices.size(); i++) {
VirtualDeviceImpl device = mVirtualDevices.valueAt(i);
device.showToastWhereUidIsRunning(uid,
R.string.app_streaming_blocked_message_for_fingerprint_dialog,
Toast.LENGTH_LONG, Looper.getMainLooper());
}
}
}
@Override
public int getBaseVirtualDisplayFlags(IVirtualDevice virtualDevice) {
return ((VirtualDeviceImpl) virtualDevice).getBaseVirtualDisplayFlags();

View File

@@ -70,6 +70,7 @@ import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.ArrayUtils;
import com.android.server.SystemService;
import com.android.server.companion.virtual.VirtualDeviceManagerInternal;
import java.util.ArrayList;
import java.util.Arrays;
@@ -262,6 +263,11 @@ public class AuthService extends SystemService {
final long identity = Binder.clearCallingIdentity();
try {
VirtualDeviceManagerInternal vdm = getLocalService(
VirtualDeviceManagerInternal.class);
if (vdm != null) {
vdm.onAuthenticationPrompt(callingUid);
}
return mBiometricService.authenticate(
token, sessionId, userId, receiver, opPackageName, promptInfo);
} finally {

View File

@@ -91,6 +91,7 @@ import com.android.server.biometrics.sensors.LockoutTracker;
import com.android.server.biometrics.sensors.fingerprint.aidl.FingerprintProvider;
import com.android.server.biometrics.sensors.fingerprint.hidl.Fingerprint21;
import com.android.server.biometrics.sensors.fingerprint.hidl.Fingerprint21UdfpsMock;
import com.android.server.companion.virtual.VirtualDeviceManagerInternal;
import com.google.android.collect.Lists;
@@ -329,6 +330,16 @@ public class FingerprintService extends SystemService {
return -1;
}
}
final long identity2 = Binder.clearCallingIdentity();
try {
VirtualDeviceManagerInternal vdm = getLocalService(
VirtualDeviceManagerInternal.class);
if (vdm != null) {
vdm.onAuthenticationPrompt(callingUid);
}
} finally {
Binder.restoreCallingIdentity(identity2);
}
return provider.second.scheduleAuthenticate(token, operationId,
0 /* cookie */, new ClientMonitorCallbackConverter(receiver), options,
restricted, statsClient, isKeyguard);

View File

@@ -69,6 +69,11 @@ public abstract class VirtualDeviceManagerInternal {
*/
public abstract void onAppsOnVirtualDeviceChanged();
/**
* Notifies that an authentication prompt is about to be shown for an app with the given uid.
*/
public abstract void onAuthenticationPrompt(int uid);
/**
* Gets the owner uid for a deviceId.
*

View File

@@ -0,0 +1,96 @@
/*
* Copyright (C) 2023 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.companion.virtual;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.os.Looper;
import android.platform.test.annotations.Presubmit;
import android.testing.TestableContext;
import androidx.test.InstrumentationRegistry;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.android.internal.R;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@Presubmit
@RunWith(AndroidJUnit4.class)
public class VirtualDeviceManagerServiceMockingTest {
private static final int UID_1 = 0;
private static final int DEVICE_ID_1 = 42;
private static final int DEVICE_ID_2 = 43;
@Rule
public final TestableContext mContext = new TestableContext(
InstrumentationRegistry.getContext());
private VirtualDeviceManagerService mVdms;
private VirtualDeviceManagerInternal mLocalService;
@Before
public void setUp() {
mVdms = new VirtualDeviceManagerService(mContext);
mLocalService = mVdms.getLocalServiceInstance();
}
@Test
public void onAuthenticationPrompt_noDevices_noCrash() {
// This should not crash
mLocalService.onAuthenticationPrompt(UID_1);
}
@Test
public void onAuthenticationPrompt_oneDevice_showToastWhereUidIsRunningIsCalled() {
VirtualDeviceImpl device = mock(VirtualDeviceImpl.class);
mVdms.addVirtualDevice(device);
mLocalService.onAuthenticationPrompt(UID_1);
verify(device).showToastWhereUidIsRunning(eq(UID_1),
eq(R.string.app_streaming_blocked_message_for_fingerprint_dialog), anyInt(),
any(Looper.class));
}
@Test
public void onAuthenticationPrompt_twoDevices_showToastWhereUidIsRunningIsCalledOnBoth() {
VirtualDeviceImpl device1 = mock(VirtualDeviceImpl.class);
VirtualDeviceImpl device2 = mock(VirtualDeviceImpl.class);
when(device1.getDeviceId()).thenReturn(DEVICE_ID_1);
when(device2.getDeviceId()).thenReturn(DEVICE_ID_2);
mVdms.addVirtualDevice(device1);
mVdms.addVirtualDevice(device2);
mLocalService.onAuthenticationPrompt(UID_1);
verify(device1).showToastWhereUidIsRunning(eq(UID_1),
eq(R.string.app_streaming_blocked_message_for_fingerprint_dialog), anyInt(),
any(Looper.class));
verify(device2).showToastWhereUidIsRunning(eq(UID_1),
eq(R.string.app_streaming_blocked_message_for_fingerprint_dialog), anyInt(),
any(Looper.class));
}
}

View File

@@ -58,6 +58,8 @@ import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;
import com.android.internal.R;
import com.android.server.LocalServices;
import com.android.server.companion.virtual.VirtualDeviceManagerInternal;
import org.junit.Before;
import org.junit.Rule;
@@ -102,6 +104,8 @@ public class AuthServiceTest {
IFaceService mFaceService;
@Mock
AppOpsManager mAppOpsManager;
@Mock
private VirtualDeviceManagerInternal mVdmInternal;
@Captor
private ArgumentCaptor<List<FingerprintSensorPropertiesInternal>> mFingerprintPropsCaptor;
@Captor
@@ -115,6 +119,8 @@ public class AuthServiceTest {
"1:4:15", // ID1:Iris:Strong
"2:8:15", // ID2:Face:Strong
};
LocalServices.removeServiceForTest(VirtualDeviceManagerInternal.class);
LocalServices.addService(VirtualDeviceManagerInternal.class, mVdmInternal);
when(mResources.getIntArray(eq(R.array.config_udfps_sensor_props))).thenReturn(new int[0]);
when(mResources.getBoolean(eq(R.bool.config_is_powerbutton_fps))).thenReturn(false);
@@ -271,6 +277,47 @@ public class AuthServiceTest {
verify(mReceiver).onError(eq(TYPE_NONE), eq(BIOMETRIC_ERROR_CANCELED), anyInt());
}
@Test
public void testAuthenticate_noVdmInternalService_noCrash() throws Exception {
LocalServices.removeServiceForTest(VirtualDeviceManagerInternal.class);
mAuthService = new AuthService(mContext, mInjector);
mAuthService.onStart();
final Binder token = new Binder();
// This should not crash
mAuthService.mImpl.authenticate(
token,
0, /* sessionId */
0, /* userId */
mReceiver,
TEST_OP_PACKAGE_NAME,
new PromptInfo());
waitForIdle();
}
@Test
public void testAuthenticate_callsVirtualDeviceManagerOnAuthenticationPrompt()
throws Exception {
mAuthService = new AuthService(mContext, mInjector);
mAuthService.onStart();
final Binder token = new Binder();
mAuthService.mImpl.authenticate(
token,
0, /* sessionId */
0, /* userId */
mReceiver,
TEST_OP_PACKAGE_NAME,
new PromptInfo());
waitForIdle();
ArgumentCaptor<Integer> uidCaptor = ArgumentCaptor.forClass(Integer.class);
verify(mVdmInternal).onAuthenticationPrompt(uidCaptor.capture());
assertEquals((int) (uidCaptor.getValue()), Binder.getCallingUid());
}
@Test
public void testAuthenticate_throwsWhenUsingTestConfigurations() {
final PromptInfo promptInfo = mock(PromptInfo.class);

View File

@@ -27,6 +27,8 @@ import static android.hardware.fingerprint.FingerprintSensorProperties.TYPE_UDFP
import static com.google.common.truth.Truth.assertThat;
import static junit.framework.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
@@ -44,6 +46,7 @@ import android.hardware.fingerprint.FingerprintAuthenticateOptions;
import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
import android.hardware.fingerprint.IFingerprintAuthenticatorsRegisteredCallback;
import android.hardware.fingerprint.IFingerprintServiceReceiver;
import android.os.Binder;
import android.os.IBinder;
import android.platform.test.annotations.Presubmit;
import android.provider.Settings;
@@ -54,8 +57,10 @@ import androidx.test.platform.app.InstrumentationRegistry;
import com.android.internal.util.test.FakeSettingsProvider;
import com.android.internal.util.test.FakeSettingsProviderRule;
import com.android.server.LocalServices;
import com.android.server.biometrics.log.BiometricContext;
import com.android.server.biometrics.sensors.fingerprint.aidl.FingerprintProvider;
import com.android.server.companion.virtual.VirtualDeviceManagerInternal;
import org.junit.Before;
import org.junit.Rule;
@@ -103,6 +108,8 @@ public class FingerprintServiceTest {
private IFingerprintServiceReceiver mServiceReceiver;
@Mock
private IBinder mToken;
@Mock
private VirtualDeviceManagerInternal mVdmInternal;
@Captor
private ArgumentCaptor<FingerprintAuthenticateOptions> mAuthenticateOptionsCaptor;
@@ -125,6 +132,9 @@ public class FingerprintServiceTest {
@Before
public void setup() throws Exception {
LocalServices.removeServiceForTest(VirtualDeviceManagerInternal.class);
LocalServices.addService(VirtualDeviceManagerInternal.class, mVdmInternal);
when(mFingerprintDefault.getSensorProperties()).thenReturn(List.of(mSensorPropsDefault));
when(mFingerprintVirtual.getSensorProperties()).thenReturn(List.of(mSensorPropsVirtual));
when(mFingerprintDefault.containsSensor(anyInt()))
@@ -225,6 +235,36 @@ public class FingerprintServiceTest {
verifyNoAuthenticate(mFingerprintVirtual);
}
@Test
public void testAuthenticate_noVdmInternalService_noCrash() throws Exception {
initServiceWithAndWait(NAME_DEFAULT, NAME_VIRTUAL);
LocalServices.removeServiceForTest(VirtualDeviceManagerInternal.class);
final long operationId = 2;
// This should not crash
mService.mServiceWrapper.authenticate(mToken, operationId, mServiceReceiver,
new FingerprintAuthenticateOptions.Builder()
.setSensorId(SENSOR_ID_ANY)
.build());
}
@Test
public void testAuthenticate_callsVirtualDeviceManagerOnAuthenticationPrompt()
throws Exception {
initServiceWithAndWait(NAME_DEFAULT, NAME_VIRTUAL);
final long operationId = 2;
mService.mServiceWrapper.authenticate(mToken, operationId, mServiceReceiver,
new FingerprintAuthenticateOptions.Builder()
.setSensorId(SENSOR_ID_ANY)
.build());
ArgumentCaptor<Integer> uidCaptor = ArgumentCaptor.forClass(Integer.class);
verify(mVdmInternal).onAuthenticationPrompt(uidCaptor.capture());
assertEquals((int) (uidCaptor.getValue()), Binder.getCallingUid());
}
private FingerprintAuthenticateOptions verifyAuthenticateWithNewRequestId(
FingerprintProvider provider, long operationId) {