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:
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user