diff options
12 files changed, 799 insertions, 487 deletions
diff --git a/core/java/android/credentials/CredentialManager.java b/core/java/android/credentials/CredentialManager.java index 9140d02e223d..c2a0062b43e8 100644 --- a/core/java/android/credentials/CredentialManager.java +++ b/core/java/android/credentials/CredentialManager.java @@ -355,7 +355,11 @@ public final class CredentialManager { * Sets a list of all user configurable credential providers registered on the system. This API * is intended for settings apps. * - * @param providers the list of enabled providers + * @param primaryProviders the primary providers that user selected for saving credentials. In + * the most case, there should be only one primary provider, However, + * if there are more than one CredentialProviderService in the same APK, + * they should be passed in altogether. + * @param providers the list of enabled providers. * @param userId the user ID to configure credential manager for * @param executor the callback will take place on this {@link Executor} * @param callback the callback invoked when the request succeeds or fails @@ -363,6 +367,7 @@ public final class CredentialManager { */ @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public void setEnabledProviders( + @NonNull List<String> primaryProviders, @NonNull List<String> providers, int userId, @CallbackExecutor @NonNull Executor executor, @@ -370,9 +375,11 @@ public final class CredentialManager { requireNonNull(executor, "executor must not be null"); requireNonNull(callback, "callback must not be null"); requireNonNull(providers, "providers must not be null"); + requireNonNull(primaryProviders, "primaryProviders must not be null"); try { mService.setEnabledProviders( + primaryProviders, providers, userId, new SetEnabledProvidersTransport(executor, callback)); } catch (RemoteException e) { e.rethrowFromSystemServer(); diff --git a/core/java/android/credentials/CredentialProviderInfo.java b/core/java/android/credentials/CredentialProviderInfo.java index c224f01055e1..d66b8f00a2e9 100644 --- a/core/java/android/credentials/CredentialProviderInfo.java +++ b/core/java/android/credentials/CredentialProviderInfo.java @@ -46,6 +46,7 @@ public final class CredentialProviderInfo implements Parcelable { @Nullable private CharSequence mSettingsSubtitle = null; private final boolean mIsSystemProvider; private final boolean mIsEnabled; + private final boolean mIsPrimary; /** * Constructs an information instance of the credential provider. @@ -58,6 +59,7 @@ public final class CredentialProviderInfo implements Parcelable { mIsSystemProvider = builder.mIsSystemProvider; mSettingsSubtitle = builder.mSettingsSubtitle; mIsEnabled = builder.mIsEnabled; + mIsPrimary = builder.mIsPrimary; mOverrideLabel = builder.mOverrideLabel; } @@ -108,6 +110,15 @@ public final class CredentialProviderInfo implements Parcelable { return mIsEnabled; } + /** + * Returns whether the provider is set as primary by the user. + * + * @hide + */ + public boolean isPrimary() { + return mIsPrimary; + } + /** Returns the settings subtitle. */ @Nullable public CharSequence getSettingsSubtitle() { @@ -125,6 +136,7 @@ public final class CredentialProviderInfo implements Parcelable { dest.writeTypedObject(mServiceInfo, flags); dest.writeBoolean(mIsSystemProvider); dest.writeBoolean(mIsEnabled); + dest.writeBoolean(mIsPrimary); TextUtils.writeToParcel(mOverrideLabel, dest, flags); TextUtils.writeToParcel(mSettingsSubtitle, dest, flags); @@ -149,6 +161,9 @@ public final class CredentialProviderInfo implements Parcelable { + "isEnabled=" + mIsEnabled + ", " + + "isPrimary=" + + mIsPrimary + + ", " + "overrideLabel=" + mOverrideLabel + ", " @@ -164,6 +179,7 @@ public final class CredentialProviderInfo implements Parcelable { mServiceInfo = in.readTypedObject(ServiceInfo.CREATOR); mIsSystemProvider = in.readBoolean(); mIsEnabled = in.readBoolean(); + mIsPrimary = in.readBoolean(); mOverrideLabel = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); mSettingsSubtitle = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); @@ -193,6 +209,7 @@ public final class CredentialProviderInfo implements Parcelable { private boolean mIsSystemProvider = false; @Nullable private CharSequence mSettingsSubtitle = null; private boolean mIsEnabled = false; + private boolean mIsPrimary = false; @Nullable private CharSequence mOverrideLabel = null; /** @@ -248,6 +265,20 @@ public final class CredentialProviderInfo implements Parcelable { return this; } + /** + * Sets whether it is set as primary by the user. + * + * <p>Primary provider will be used for saving credentials by default. In most cases, there + * should only one primary provider exist. However, if there are multiple credential + * providers exist in the same package, all of them will be marked as primary. + * + * @hide + */ + public @NonNull Builder setPrimary(boolean isPrimary) { + mIsPrimary = isPrimary; + return this; + } + /** Builds a new {@link CredentialProviderInfo} instance. */ public @NonNull CredentialProviderInfo build() { return new CredentialProviderInfo(this); diff --git a/core/java/android/credentials/ICredentialManager.aidl b/core/java/android/credentials/ICredentialManager.aidl index b779c56035d3..dec729f4a19f 100644 --- a/core/java/android/credentials/ICredentialManager.aidl +++ b/core/java/android/credentials/ICredentialManager.aidl @@ -47,7 +47,7 @@ interface ICredentialManager { @nullable ICancellationSignal clearCredentialState(in ClearCredentialStateRequest request, in IClearCredentialStateCallback callback, String callingPackage); - void setEnabledProviders(in List<String> providers, in int userId, in ISetEnabledProvidersCallback callback); + void setEnabledProviders(in List<String> primaryProviders, in List<String> providers, in int userId, in ISetEnabledProvidersCallback callback); void registerCredentialDescription(in RegisterCredentialDescriptionRequest request, String callingPackage); diff --git a/core/java/android/service/credentials/CredentialProviderInfoFactory.java b/core/java/android/service/credentials/CredentialProviderInfoFactory.java index 751c675b28f4..b196b064a670 100644 --- a/core/java/android/service/credentials/CredentialProviderInfoFactory.java +++ b/core/java/android/service/credentials/CredentialProviderInfoFactory.java @@ -75,13 +75,12 @@ public final class CredentialProviderInfoFactory { /** * Constructs an information instance of the credential provider. * - * @param context the context object + * @param context the context object * @param serviceComponent the serviceComponent of the provider service - * @param userId the android userId for which the current process is running + * @param userId the android userId for which the current process is running * @param isSystemProvider whether this provider is a system provider * @throws PackageManager.NameNotFoundException If provider service is not found - * @throws SecurityException If provider does not require the relevant - * permission + * @throws SecurityException If provider does not require the relevant permission */ public static CredentialProviderInfo create( @NonNull Context context, @@ -94,21 +93,20 @@ public final class CredentialProviderInfoFactory { getServiceInfoOrThrow(serviceComponent, userId), isSystemProvider, /* disableSystemAppVerificationForTests= */ false, - /* isEnabled= */ false); + /* isEnabled= */ false, + /* isPrimary= */ false); } /** * Constructs an information instance of the credential provider. * - * @param context the context object - * @param serviceInfo the service info for the provider app. This must - * be retrieved from the - * {@code PackageManager} - * @param isSystemProvider whether the provider app is a system provider + * @param context the context object + * @param serviceInfo the service info for the provider app. This must be retrieved from the + * {@code PackageManager} + * @param isSystemProvider whether the provider app is a system provider * @param disableSystemAppVerificationForTests whether to disable system app permission - * verification so that tests can install system - * providers - * @param isEnabled whether the user enabled this provider + * verification so that tests can install system providers + * @param isEnabled whether the user enabled this provider * @throws SecurityException If provider does not require the relevant permission */ public static CredentialProviderInfo create( @@ -116,7 +114,8 @@ public final class CredentialProviderInfoFactory { @NonNull ServiceInfo serviceInfo, boolean isSystemProvider, boolean disableSystemAppVerificationForTests, - boolean isEnabled) + boolean isEnabled, + boolean isPrimary) throws SecurityException { verifyProviderPermission(serviceInfo); if (isSystemProvider) { @@ -131,6 +130,7 @@ public final class CredentialProviderInfoFactory { return populateMetadata(context, serviceInfo) .setSystemProvider(isSystemProvider) .setEnabled(isEnabled) + .setPrimary(isPrimary) .build(); } @@ -168,7 +168,9 @@ public final class CredentialProviderInfoFactory { Slog.w(TAG, "Context is null in isSystemProviderWithValidPermission"); return false; } - return PermissionUtils.hasPermission(context, serviceInfo.packageName, + return PermissionUtils.hasPermission( + context, + serviceInfo.packageName, Manifest.permission.PROVIDE_DEFAULT_ENABLED_CREDENTIAL_SERVICE); } @@ -181,8 +183,11 @@ public final class CredentialProviderInfoFactory { if (disableSystemAppVerificationForTests) { Bundle metadata = serviceInfo.metaData; if (metadata == null) { - Slog.w(TAG, "metadata is null while reading " - + "TEST_SYSTEM_PROVIDER_META_DATA_KEY: " + serviceInfo); + Slog.w( + TAG, + "metadata is null while reading " + + "TEST_SYSTEM_PROVIDER_META_DATA_KEY: " + + serviceInfo); return false; } return metadata.getBoolean( @@ -215,8 +220,10 @@ public final class CredentialProviderInfoFactory { // 3. Stop if we are missing data. if (resources == null) { - Slog.w(TAG, "Resources are null for the serviceInfo being processed: " - + serviceInfo.getComponentName()); + Slog.w( + TAG, + "Resources are null for the serviceInfo being processed: " + + serviceInfo.getComponentName()); return builder; } @@ -408,7 +415,7 @@ public final class CredentialProviderInfoFactory { si, /* isSystemProvider= */ true, disableSystemAppVerificationForTests, - enabledServices.contains(si.getComponentName())); + enabledServices.contains(si.getComponentName()), false); if (cpi.isSystemProvider()) { providerInfos.add(cpi); } else { @@ -446,7 +453,8 @@ public final class CredentialProviderInfoFactory { @NonNull Context context, int userId, int providerFilter, - Set<ComponentName> enabledServices) { + Set<ComponentName> enabledServices, + Set<String> primaryServices) { requireNonNull(context, "context must not be null"); // Get the device policy. @@ -459,7 +467,11 @@ public final class CredentialProviderInfoFactory { context, pp, disableSystemAppVerificationForTests, providerFilter); generator.addUserProviders( getUserProviders( - context, userId, disableSystemAppVerificationForTests, enabledServices)); + context, + userId, + disableSystemAppVerificationForTests, + enabledServices, + primaryServices)); generator.addSystemProviders( getAvailableSystemServices( context, userId, disableSystemAppVerificationForTests, enabledServices)); @@ -475,7 +487,8 @@ public final class CredentialProviderInfoFactory { @NonNull Context context, int userId, int providerFilter, - Set<ComponentName> enabledServices) { + Set<ComponentName> enabledServices, + Set<String> primaryServices) { requireNonNull(context, "context must not be null"); // Get the device policy. @@ -488,7 +501,11 @@ public final class CredentialProviderInfoFactory { context, pp, disableSystemAppVerificationForTests, providerFilter); generator.addUserProviders( getUserProviders( - context, userId, disableSystemAppVerificationForTests, enabledServices)); + context, + userId, + disableSystemAppVerificationForTests, + enabledServices, + primaryServices)); generator.addSystemProviders( getAvailableSystemServices( context, userId, disableSystemAppVerificationForTests, enabledServices)); @@ -581,7 +598,8 @@ public final class CredentialProviderInfoFactory { @NonNull Context context, @UserIdInt int userId, boolean disableSystemAppVerificationForTests, - Set<ComponentName> enabledServices) { + Set<ComponentName> enabledServices, + Set<String> primaryServices) { final List<CredentialProviderInfo> services = new ArrayList<>(); final List<ResolveInfo> resolveInfos = context.getPackageManager() @@ -603,7 +621,9 @@ public final class CredentialProviderInfoFactory { serviceInfo, /* isSystemProvider= */ false, disableSystemAppVerificationForTests, - enabledServices.contains(serviceInfo.getComponentName())); + enabledServices.contains(serviceInfo.getComponentName()), + primaryServices.contains( + serviceInfo.getComponentName().flattenToString())); if (!cpi.isSystemProvider()) { services.add(cpi); } diff --git a/core/tests/coretests/src/android/credentials/CredentialManagerTest.java b/core/tests/coretests/src/android/credentials/CredentialManagerTest.java index b0d5240c61ba..dc4c2527cc03 100644 --- a/core/tests/coretests/src/android/credentials/CredentialManagerTest.java +++ b/core/tests/coretests/src/android/credentials/CredentialManagerTest.java @@ -56,22 +56,20 @@ import java.util.concurrent.Executor; @RunWith(MockitoJUnitRunner.class) public class CredentialManagerTest { - @Mock - private ICredentialManager mMockCredentialManagerService; + @Mock private ICredentialManager mMockCredentialManagerService; - @Mock - private Activity mMockActivity; + @Mock private Activity mMockActivity; private static final int TEST_USER_ID = 1; private static final CredentialProviderInfo TEST_CREDENTIAL_PROVIDER_INFO = - new CredentialProviderInfo.Builder(new ServiceInfo()) - .setSystemProvider(true) - .setOverrideLabel("test") - .addCapabilities(Arrays.asList("passkey")) - .setEnabled(true) - .build(); + new CredentialProviderInfo.Builder(new ServiceInfo()) + .setSystemProvider(true) + .setOverrideLabel("test") + .addCapabilities(Arrays.asList("passkey")) + .setEnabled(true) + .build(); private static final List<CredentialProviderInfo> TEST_CREDENTIAL_PROVIDER_INFO_LIST = - Arrays.asList(TEST_CREDENTIAL_PROVIDER_INFO); + Arrays.asList(TEST_CREDENTIAL_PROVIDER_INFO); private GetCredentialRequest mGetRequest; private CreateCredentialRequest mCreateRequest; @@ -112,27 +110,43 @@ public class CredentialManagerTest { @Before public void setup() { - mGetRequest = new GetCredentialRequest.Builder(Bundle.EMPTY).addCredentialOption( - new CredentialOption(Credential.TYPE_PASSWORD_CREDENTIAL, Bundle.EMPTY, - Bundle.EMPTY, false)).build(); - mCreateRequest = new CreateCredentialRequest.Builder( - Credential.TYPE_PASSWORD_CREDENTIAL, - Bundle.EMPTY, Bundle.EMPTY) - .setIsSystemProviderRequired(false) - .setAlwaysSendAppInfoToProvider(false) - .build(); + mGetRequest = + new GetCredentialRequest.Builder(Bundle.EMPTY) + .addCredentialOption( + new CredentialOption( + Credential.TYPE_PASSWORD_CREDENTIAL, + Bundle.EMPTY, + Bundle.EMPTY, + false)) + .build(); + mCreateRequest = + new CreateCredentialRequest.Builder( + Credential.TYPE_PASSWORD_CREDENTIAL, Bundle.EMPTY, Bundle.EMPTY) + .setIsSystemProviderRequired(false) + .setAlwaysSendAppInfoToProvider(false) + .build(); mClearRequest = new ClearCredentialStateRequest(Bundle.EMPTY); - final Slice slice = new Slice.Builder(Uri.parse("foo://bar"), null).addText("some text", - null, List.of(Slice.HINT_TITLE)).build(); - mRegisterRequest = new RegisterCredentialDescriptionRequest( - new CredentialDescription(Credential.TYPE_PASSWORD_CREDENTIAL, - new HashSet<>(List.of("{ \"foo\": \"bar\" }")), - List.of(new CredentialEntry(Credential.TYPE_PASSWORD_CREDENTIAL, slice)))); - mUnregisterRequest = new UnregisterCredentialDescriptionRequest( - new CredentialDescription(Credential.TYPE_PASSWORD_CREDENTIAL, - new HashSet<>(List.of("{ \"foo\": \"bar\" }")), - List.of(new CredentialEntry(Credential.TYPE_PASSWORD_CREDENTIAL, slice)))); + final Slice slice = + new Slice.Builder(Uri.parse("foo://bar"), null) + .addText("some text", null, List.of(Slice.HINT_TITLE)) + .build(); + mRegisterRequest = + new RegisterCredentialDescriptionRequest( + new CredentialDescription( + Credential.TYPE_PASSWORD_CREDENTIAL, + new HashSet<>(List.of("{ \"foo\": \"bar\" }")), + List.of( + new CredentialEntry( + Credential.TYPE_PASSWORD_CREDENTIAL, slice)))); + mUnregisterRequest = + new UnregisterCredentialDescriptionRequest( + new CredentialDescription( + Credential.TYPE_PASSWORD_CREDENTIAL, + new HashSet<>(List.of("{ \"foo\": \"bar\" }")), + List.of( + new CredentialEntry( + Credential.TYPE_PASSWORD_CREDENTIAL, slice)))); final Context context = InstrumentationRegistry.getInstrumentation().getContext(); mCredentialManager = new CredentialManager(context, mMockCredentialManagerService); @@ -143,56 +157,63 @@ public class CredentialManagerTest { @Test public void testGetCredential_nullRequest() { GetCredentialRequest nullRequest = null; - assertThrows(NullPointerException.class, - () -> mCredentialManager.getCredential(mMockActivity, nullRequest, null, mExecutor, - result -> { - })); + assertThrows( + NullPointerException.class, + () -> + mCredentialManager.getCredential( + mMockActivity, nullRequest, null, mExecutor, result -> {})); } @Test public void testGetCredential_nullActivity() { - assertThrows(NullPointerException.class, - () -> mCredentialManager.getCredential(null, mGetRequest, null, mExecutor, - result -> { - })); + assertThrows( + NullPointerException.class, + () -> + mCredentialManager.getCredential( + null, mGetRequest, null, mExecutor, result -> {})); } @Test public void testGetCredential_nullExecutor() { - assertThrows(NullPointerException.class, - () -> mCredentialManager.getCredential(mMockActivity, mGetRequest, null, null, - result -> { - })); + assertThrows( + NullPointerException.class, + () -> + mCredentialManager.getCredential( + mMockActivity, mGetRequest, null, null, result -> {})); } @Test public void testGetCredential_nullCallback() { - assertThrows(NullPointerException.class, - () -> mCredentialManager.getCredential(mMockActivity, mGetRequest, null, null, - null)); + assertThrows( + NullPointerException.class, + () -> + mCredentialManager.getCredential( + mMockActivity, mGetRequest, null, null, null)); } @Test public void testGetCredential_noCredential() throws RemoteException { - ArgumentCaptor<IGetCredentialCallback> callbackCaptor = ArgumentCaptor.forClass( - IGetCredentialCallback.class); - ArgumentCaptor<GetCredentialException> errorCaptor = ArgumentCaptor.forClass( - GetCredentialException.class); + ArgumentCaptor<IGetCredentialCallback> callbackCaptor = + ArgumentCaptor.forClass(IGetCredentialCallback.class); + ArgumentCaptor<GetCredentialException> errorCaptor = + ArgumentCaptor.forClass(GetCredentialException.class); - OutcomeReceiver<GetCredentialResponse, GetCredentialException> callback = mock( - OutcomeReceiver.class); + OutcomeReceiver<GetCredentialResponse, GetCredentialException> callback = + mock(OutcomeReceiver.class); - when(mMockCredentialManagerService.executeGetCredential(any(), callbackCaptor.capture(), - any())).thenReturn(mock(ICancellationSignal.class)); + when(mMockCredentialManagerService.executeGetCredential( + any(), callbackCaptor.capture(), any())) + .thenReturn(mock(ICancellationSignal.class)); mCredentialManager.getCredential(mMockActivity, mGetRequest, null, mExecutor, callback); verify(mMockCredentialManagerService).executeGetCredential(any(), any(), eq(mPackageName)); - callbackCaptor.getValue().onError(GetCredentialException.TYPE_NO_CREDENTIAL, - "no credential found"); + callbackCaptor + .getValue() + .onError(GetCredentialException.TYPE_NO_CREDENTIAL, "no credential found"); verify(callback).onError(errorCaptor.capture()); - assertThat(errorCaptor.getValue().getType()).isEqualTo( - GetCredentialException.TYPE_NO_CREDENTIAL); + assertThat(errorCaptor.getValue().getType()) + .isEqualTo(GetCredentialException.TYPE_NO_CREDENTIAL); } @Test @@ -200,9 +221,8 @@ public class CredentialManagerTest { final CancellationSignal cancellation = new CancellationSignal(); cancellation.cancel(); - mCredentialManager.getCredential(mMockActivity, mGetRequest, cancellation, mExecutor, - result -> { - }); + mCredentialManager.getCredential( + mMockActivity, mGetRequest, cancellation, mExecutor, result -> {}); verify(mMockCredentialManagerService, never()).executeGetCredential(any(), any(), any()); } @@ -212,14 +232,14 @@ public class CredentialManagerTest { final ICancellationSignal serviceSignal = mock(ICancellationSignal.class); final CancellationSignal cancellation = new CancellationSignal(); - OutcomeReceiver<GetCredentialResponse, GetCredentialException> callback = mock( - OutcomeReceiver.class); + OutcomeReceiver<GetCredentialResponse, GetCredentialException> callback = + mock(OutcomeReceiver.class); - when(mMockCredentialManagerService.executeGetCredential(any(), any(), any())).thenReturn( - serviceSignal); + when(mMockCredentialManagerService.executeGetCredential(any(), any(), any())) + .thenReturn(serviceSignal); - mCredentialManager.getCredential(mMockActivity, mGetRequest, cancellation, mExecutor, - callback); + mCredentialManager.getCredential( + mMockActivity, mGetRequest, cancellation, mExecutor, callback); verify(mMockCredentialManagerService).executeGetCredential(any(), any(), eq(mPackageName)); @@ -231,16 +251,17 @@ public class CredentialManagerTest { public void testGetCredential_success() throws RemoteException { final Credential cred = new Credential(Credential.TYPE_PASSWORD_CREDENTIAL, Bundle.EMPTY); - ArgumentCaptor<IGetCredentialCallback> callbackCaptor = ArgumentCaptor.forClass( - IGetCredentialCallback.class); - ArgumentCaptor<GetCredentialResponse> responseCaptor = ArgumentCaptor.forClass( - GetCredentialResponse.class); + ArgumentCaptor<IGetCredentialCallback> callbackCaptor = + ArgumentCaptor.forClass(IGetCredentialCallback.class); + ArgumentCaptor<GetCredentialResponse> responseCaptor = + ArgumentCaptor.forClass(GetCredentialResponse.class); - OutcomeReceiver<GetCredentialResponse, GetCredentialException> callback = mock( - OutcomeReceiver.class); + OutcomeReceiver<GetCredentialResponse, GetCredentialException> callback = + mock(OutcomeReceiver.class); - when(mMockCredentialManagerService.executeGetCredential(any(), callbackCaptor.capture(), - any())).thenReturn(mock(ICancellationSignal.class)); + when(mMockCredentialManagerService.executeGetCredential( + any(), callbackCaptor.capture(), any())) + .thenReturn(mock(ICancellationSignal.class)); mCredentialManager.getCredential(mMockActivity, mGetRequest, null, mExecutor, callback); verify(mMockCredentialManagerService).executeGetCredential(any(), any(), eq(mPackageName)); @@ -252,33 +273,38 @@ public class CredentialManagerTest { @Test public void testCreateCredential_nullRequest() { - assertThrows(NullPointerException.class, - () -> mCredentialManager.createCredential(mMockActivity, null, null, mExecutor, - result -> { - })); + assertThrows( + NullPointerException.class, + () -> + mCredentialManager.createCredential( + mMockActivity, null, null, mExecutor, result -> {})); } @Test public void testCreateCredential_nullActivity() { - assertThrows(NullPointerException.class, - () -> mCredentialManager.createCredential(null, mCreateRequest, null, mExecutor, - result -> { - })); + assertThrows( + NullPointerException.class, + () -> + mCredentialManager.createCredential( + null, mCreateRequest, null, mExecutor, result -> {})); } @Test public void testCreateCredential_nullExecutor() { - assertThrows(NullPointerException.class, - () -> mCredentialManager.createCredential(mMockActivity, mCreateRequest, null, null, - result -> { - })); + assertThrows( + NullPointerException.class, + () -> + mCredentialManager.createCredential( + mMockActivity, mCreateRequest, null, null, result -> {})); } @Test public void testCreateCredential_nullCallback() { - assertThrows(NullPointerException.class, - () -> mCredentialManager.createCredential(mMockActivity, mCreateRequest, null, - mExecutor, null)); + assertThrows( + NullPointerException.class, + () -> + mCredentialManager.createCredential( + mMockActivity, mCreateRequest, null, mExecutor, null)); } @Test @@ -286,9 +312,8 @@ public class CredentialManagerTest { final CancellationSignal cancellation = new CancellationSignal(); cancellation.cancel(); - mCredentialManager.createCredential(mMockActivity, mCreateRequest, cancellation, mExecutor, - result -> { - }); + mCredentialManager.createCredential( + mMockActivity, mCreateRequest, cancellation, mExecutor, result -> {}); verify(mMockCredentialManagerService, never()).executeCreateCredential(any(), any(), any()); } @@ -298,17 +323,17 @@ public class CredentialManagerTest { final ICancellationSignal serviceSignal = mock(ICancellationSignal.class); final CancellationSignal cancellation = new CancellationSignal(); - OutcomeReceiver<CreateCredentialResponse, CreateCredentialException> callback = mock( - OutcomeReceiver.class); + OutcomeReceiver<CreateCredentialResponse, CreateCredentialException> callback = + mock(OutcomeReceiver.class); - when(mMockCredentialManagerService.executeCreateCredential(any(), any(), any())).thenReturn( - serviceSignal); + when(mMockCredentialManagerService.executeCreateCredential(any(), any(), any())) + .thenReturn(serviceSignal); - mCredentialManager.createCredential(mMockActivity, mCreateRequest, cancellation, mExecutor, - callback); + mCredentialManager.createCredential( + mMockActivity, mCreateRequest, cancellation, mExecutor, callback); - verify(mMockCredentialManagerService).executeCreateCredential(any(), any(), - eq(mPackageName)); + verify(mMockCredentialManagerService) + .executeCreateCredential(any(), any(), eq(mPackageName)); cancellation.cancel(); verify(serviceSignal).cancel(); @@ -316,26 +341,27 @@ public class CredentialManagerTest { @Test public void testCreateCredential_failed() throws RemoteException { - ArgumentCaptor<ICreateCredentialCallback> callbackCaptor = ArgumentCaptor.forClass( - ICreateCredentialCallback.class); - ArgumentCaptor<CreateCredentialException> errorCaptor = ArgumentCaptor.forClass( - CreateCredentialException.class); - - OutcomeReceiver<CreateCredentialResponse, CreateCredentialException> callback = mock( - OutcomeReceiver.class); - - when(mMockCredentialManagerService.executeCreateCredential(any(), callbackCaptor.capture(), - any())).thenReturn(mock(ICancellationSignal.class)); - mCredentialManager.createCredential(mMockActivity, mCreateRequest, null, mExecutor, - callback); - verify(mMockCredentialManagerService).executeCreateCredential(any(), any(), - eq(mPackageName)); + ArgumentCaptor<ICreateCredentialCallback> callbackCaptor = + ArgumentCaptor.forClass(ICreateCredentialCallback.class); + ArgumentCaptor<CreateCredentialException> errorCaptor = + ArgumentCaptor.forClass(CreateCredentialException.class); + + OutcomeReceiver<CreateCredentialResponse, CreateCredentialException> callback = + mock(OutcomeReceiver.class); + + when(mMockCredentialManagerService.executeCreateCredential( + any(), callbackCaptor.capture(), any())) + .thenReturn(mock(ICancellationSignal.class)); + mCredentialManager.createCredential( + mMockActivity, mCreateRequest, null, mExecutor, callback); + verify(mMockCredentialManagerService) + .executeCreateCredential(any(), any(), eq(mPackageName)); callbackCaptor.getValue().onError(CreateCredentialException.TYPE_UNKNOWN, "unknown error"); verify(callback).onError(errorCaptor.capture()); - assertThat(errorCaptor.getValue().getType()).isEqualTo( - CreateCredentialException.TYPE_UNKNOWN); + assertThat(errorCaptor.getValue().getType()) + .isEqualTo(CreateCredentialException.TYPE_UNKNOWN); } @Test @@ -343,20 +369,21 @@ public class CredentialManagerTest { final Bundle responseData = new Bundle(); responseData.putString("foo", "bar"); - ArgumentCaptor<ICreateCredentialCallback> callbackCaptor = ArgumentCaptor.forClass( - ICreateCredentialCallback.class); - ArgumentCaptor<CreateCredentialResponse> responseCaptor = ArgumentCaptor.forClass( - CreateCredentialResponse.class); + ArgumentCaptor<ICreateCredentialCallback> callbackCaptor = + ArgumentCaptor.forClass(ICreateCredentialCallback.class); + ArgumentCaptor<CreateCredentialResponse> responseCaptor = + ArgumentCaptor.forClass(CreateCredentialResponse.class); - OutcomeReceiver<CreateCredentialResponse, CreateCredentialException> callback = mock( - OutcomeReceiver.class); + OutcomeReceiver<CreateCredentialResponse, CreateCredentialException> callback = + mock(OutcomeReceiver.class); - when(mMockCredentialManagerService.executeCreateCredential(any(), callbackCaptor.capture(), - any())).thenReturn(mock(ICancellationSignal.class)); - mCredentialManager.createCredential(mMockActivity, mCreateRequest, null, mExecutor, - callback); - verify(mMockCredentialManagerService).executeCreateCredential(any(), any(), - eq(mPackageName)); + when(mMockCredentialManagerService.executeCreateCredential( + any(), callbackCaptor.capture(), any())) + .thenReturn(mock(ICancellationSignal.class)); + mCredentialManager.createCredential( + mMockActivity, mCreateRequest, null, mExecutor, callback); + verify(mMockCredentialManagerService) + .executeCreateCredential(any(), any(), eq(mPackageName)); callbackCaptor.getValue().onResponse(new CreateCredentialResponse(responseData)); verify(callback).onResult(responseCaptor.capture()); @@ -366,23 +393,27 @@ public class CredentialManagerTest { @Test public void testClearCredentialState_nullRequest() { - assertThrows(NullPointerException.class, - () -> mCredentialManager.clearCredentialState(null, null, mExecutor, result -> { - })); + assertThrows( + NullPointerException.class, + () -> mCredentialManager.clearCredentialState(null, null, mExecutor, result -> {})); } @Test public void testClearCredentialState_nullExecutor() { - assertThrows(NullPointerException.class, - () -> mCredentialManager.clearCredentialState(mClearRequest, null, null, result -> { - })); + assertThrows( + NullPointerException.class, + () -> + mCredentialManager.clearCredentialState( + mClearRequest, null, null, result -> {})); } @Test public void testClearCredentialState_nullCallback() { - assertThrows(NullPointerException.class, - () -> mCredentialManager.clearCredentialState(mClearRequest, null, mExecutor, - null)); + assertThrows( + NullPointerException.class, + () -> + mCredentialManager.clearCredentialState( + mClearRequest, null, mExecutor, null)); } @Test @@ -390,8 +421,8 @@ public class CredentialManagerTest { final CancellationSignal cancellation = new CancellationSignal(); cancellation.cancel(); - mCredentialManager.clearCredentialState(mClearRequest, cancellation, mExecutor, result -> { - }); + mCredentialManager.clearCredentialState( + mClearRequest, cancellation, mExecutor, result -> {}); verify(mMockCredentialManagerService, never()).clearCredentialState(any(), any(), any()); } @@ -403,8 +434,8 @@ public class CredentialManagerTest { OutcomeReceiver<Void, ClearCredentialStateException> callback = mock(OutcomeReceiver.class); - when(mMockCredentialManagerService.clearCredentialState(any(), any(), any())).thenReturn( - serviceSignal); + when(mMockCredentialManagerService.clearCredentialState(any(), any(), any())) + .thenReturn(serviceSignal); mCredentialManager.clearCredentialState(mClearRequest, cancellation, mExecutor, callback); @@ -416,35 +447,38 @@ public class CredentialManagerTest { @Test public void testClearCredential_failed() throws RemoteException { - ArgumentCaptor<IClearCredentialStateCallback> callbackCaptor = ArgumentCaptor.forClass( - IClearCredentialStateCallback.class); - ArgumentCaptor<ClearCredentialStateException> errorCaptor = ArgumentCaptor.forClass( - ClearCredentialStateException.class); + ArgumentCaptor<IClearCredentialStateCallback> callbackCaptor = + ArgumentCaptor.forClass(IClearCredentialStateCallback.class); + ArgumentCaptor<ClearCredentialStateException> errorCaptor = + ArgumentCaptor.forClass(ClearCredentialStateException.class); OutcomeReceiver<Void, ClearCredentialStateException> callback = mock(OutcomeReceiver.class); - when(mMockCredentialManagerService.clearCredentialState(any(), callbackCaptor.capture(), - any())).thenReturn(mock(ICancellationSignal.class)); + when(mMockCredentialManagerService.clearCredentialState( + any(), callbackCaptor.capture(), any())) + .thenReturn(mock(ICancellationSignal.class)); mCredentialManager.clearCredentialState(mClearRequest, null, mExecutor, callback); verify(mMockCredentialManagerService).clearCredentialState(any(), any(), eq(mPackageName)); - callbackCaptor.getValue().onError(ClearCredentialStateException.TYPE_UNKNOWN, - "unknown error"); + callbackCaptor + .getValue() + .onError(ClearCredentialStateException.TYPE_UNKNOWN, "unknown error"); verify(callback).onError(errorCaptor.capture()); - assertThat(errorCaptor.getValue().getType()).isEqualTo( - ClearCredentialStateException.TYPE_UNKNOWN); + assertThat(errorCaptor.getValue().getType()) + .isEqualTo(ClearCredentialStateException.TYPE_UNKNOWN); } @Test public void testClearCredential_success() throws RemoteException { - ArgumentCaptor<IClearCredentialStateCallback> callbackCaptor = ArgumentCaptor.forClass( - IClearCredentialStateCallback.class); + ArgumentCaptor<IClearCredentialStateCallback> callbackCaptor = + ArgumentCaptor.forClass(IClearCredentialStateCallback.class); OutcomeReceiver<Void, ClearCredentialStateException> callback = mock(OutcomeReceiver.class); - when(mMockCredentialManagerService.clearCredentialState(any(), callbackCaptor.capture(), - any())).thenReturn(mock(ICancellationSignal.class)); + when(mMockCredentialManagerService.clearCredentialState( + any(), callbackCaptor.capture(), any())) + .thenReturn(mock(ICancellationSignal.class)); mCredentialManager.clearCredentialState(mClearRequest, null, mExecutor, callback); verify(mMockCredentialManagerService).clearCredentialState(any(), any(), eq(mPackageName)); @@ -464,27 +498,32 @@ public class CredentialManagerTest { @Test public void testGetCredentialProviderServices_systemProviders() throws RemoteException { - verifyGetCredentialProviderServices(CredentialManager.PROVIDER_FILTER_SYSTEM_PROVIDERS_ONLY); + verifyGetCredentialProviderServices( + CredentialManager.PROVIDER_FILTER_SYSTEM_PROVIDERS_ONLY); } @Test public void testGetCredentialProviderServicesForTesting_allProviders() throws RemoteException { - verifyGetCredentialProviderServicesForTesting(CredentialManager.PROVIDER_FILTER_ALL_PROVIDERS); + verifyGetCredentialProviderServicesForTesting( + CredentialManager.PROVIDER_FILTER_ALL_PROVIDERS); } @Test public void testGetCredentialProviderServicesForTesting_userProviders() throws RemoteException { - verifyGetCredentialProviderServicesForTesting(CredentialManager.PROVIDER_FILTER_USER_PROVIDERS_ONLY); + verifyGetCredentialProviderServicesForTesting( + CredentialManager.PROVIDER_FILTER_USER_PROVIDERS_ONLY); } @Test - public void testGetCredentialProviderServicesForTesting_systemProviders() throws RemoteException { - verifyGetCredentialProviderServicesForTesting(CredentialManager.PROVIDER_FILTER_SYSTEM_PROVIDERS_ONLY); + public void testGetCredentialProviderServicesForTesting_systemProviders() + throws RemoteException { + verifyGetCredentialProviderServicesForTesting( + CredentialManager.PROVIDER_FILTER_SYSTEM_PROVIDERS_ONLY); } private void verifyGetCredentialProviderServices(int testFilter) throws RemoteException { - when(mMockCredentialManagerService.getCredentialProviderServices( - TEST_USER_ID, testFilter)).thenReturn(TEST_CREDENTIAL_PROVIDER_INFO_LIST); + when(mMockCredentialManagerService.getCredentialProviderServices(TEST_USER_ID, testFilter)) + .thenReturn(TEST_CREDENTIAL_PROVIDER_INFO_LIST); List<CredentialProviderInfo> output = mCredentialManager.getCredentialProviderServices(TEST_USER_ID, testFilter); @@ -492,9 +531,10 @@ public class CredentialManagerTest { assertThat(output).containsExactlyElementsIn(TEST_CREDENTIAL_PROVIDER_INFO_LIST); } - private void verifyGetCredentialProviderServicesForTesting(int testFilter) throws RemoteException { - when(mMockCredentialManagerService.getCredentialProviderServicesForTesting( - testFilter)).thenReturn(TEST_CREDENTIAL_PROVIDER_INFO_LIST); + private void verifyGetCredentialProviderServicesForTesting(int testFilter) + throws RemoteException { + when(mMockCredentialManagerService.getCredentialProviderServicesForTesting(testFilter)) + .thenReturn(TEST_CREDENTIAL_PROVIDER_INFO_LIST); List<CredentialProviderInfo> output = mCredentialManager.getCredentialProviderServicesForTesting(testFilter); @@ -504,41 +544,45 @@ public class CredentialManagerTest { @Test public void testSetEnabledProviders_nullProviders() { - assertThrows(NullPointerException.class, - () -> mCredentialManager.setEnabledProviders(null, 0, mExecutor, response -> { - })); - + assertThrows( + NullPointerException.class, + () -> + mCredentialManager.setEnabledProviders( + null, null, 0, mExecutor, response -> {})); } @Test public void testSetEnabledProviders_nullExecutor() { - assertThrows(NullPointerException.class, - () -> mCredentialManager.setEnabledProviders(List.of("foo"), 0, null, response -> { - })); - + assertThrows( + NullPointerException.class, + () -> + mCredentialManager.setEnabledProviders( + List.of("foo"), List.of("foo"), 0, null, response -> {})); } @Test public void testSetEnabledProviders_nullCallback() { - assertThrows(NullPointerException.class, - () -> mCredentialManager.setEnabledProviders(List.of("foo"), 0, mExecutor, null)); - + assertThrows( + NullPointerException.class, + () -> + mCredentialManager.setEnabledProviders( + List.of("foo"), List.of("foo"), 0, mExecutor, null)); } @Test public void testSetEnabledProviders_failed() throws RemoteException { OutcomeReceiver<Void, SetEnabledProvidersException> callback = mock(OutcomeReceiver.class); - ArgumentCaptor<ISetEnabledProvidersCallback> callbackCaptor = ArgumentCaptor.forClass( - ISetEnabledProvidersCallback.class); - ArgumentCaptor<SetEnabledProvidersException> errorCaptor = ArgumentCaptor.forClass( - SetEnabledProvidersException.class); + ArgumentCaptor<ISetEnabledProvidersCallback> callbackCaptor = + ArgumentCaptor.forClass(ISetEnabledProvidersCallback.class); + ArgumentCaptor<SetEnabledProvidersException> errorCaptor = + ArgumentCaptor.forClass(SetEnabledProvidersException.class); final List<String> providers = List.of("foo", "bar"); final int userId = 0; - mCredentialManager.setEnabledProviders(providers, userId, mExecutor, callback); - verify(mMockCredentialManagerService).setEnabledProviders(eq(providers), eq(0), - callbackCaptor.capture()); + mCredentialManager.setEnabledProviders(providers, providers, userId, mExecutor, callback); + verify(mMockCredentialManagerService) + .setEnabledProviders(eq(providers), eq(providers), eq(0), callbackCaptor.capture()); final String errorType = "unknown"; final String errorMessage = "Unknown error"; @@ -553,15 +597,18 @@ public class CredentialManagerTest { public void testSetEnabledProviders_success() throws RemoteException { OutcomeReceiver<Void, SetEnabledProvidersException> callback = mock(OutcomeReceiver.class); - ArgumentCaptor<ISetEnabledProvidersCallback> callbackCaptor = ArgumentCaptor.forClass( - ISetEnabledProvidersCallback.class); + ArgumentCaptor<ISetEnabledProvidersCallback> callbackCaptor = + ArgumentCaptor.forClass(ISetEnabledProvidersCallback.class); final List<String> providers = List.of("foo", "bar"); + final List<String> primaryProviders = List.of("foo"); final int userId = 0; - mCredentialManager.setEnabledProviders(providers, userId, mExecutor, callback); + mCredentialManager.setEnabledProviders( + primaryProviders, providers, userId, mExecutor, callback); - verify(mMockCredentialManagerService).setEnabledProviders(eq(providers), eq(0), - callbackCaptor.capture()); + verify(mMockCredentialManagerService) + .setEnabledProviders( + eq(primaryProviders), eq(providers), eq(0), callbackCaptor.capture()); callbackCaptor.getValue().onResponse(); verify(callback).onResult(any()); @@ -569,27 +616,29 @@ public class CredentialManagerTest { @Test public void testRegisterCredentialDescription_nullRequest() { - assertThrows(NullPointerException.class, + assertThrows( + NullPointerException.class, () -> mCredentialManager.registerCredentialDescription(null)); } @Test public void testRegisterCredentialDescription_success() throws RemoteException { mCredentialManager.registerCredentialDescription(mRegisterRequest); - verify(mMockCredentialManagerService).registerCredentialDescription(same(mRegisterRequest), - eq(mPackageName)); + verify(mMockCredentialManagerService) + .registerCredentialDescription(same(mRegisterRequest), eq(mPackageName)); } @Test public void testUnregisterCredentialDescription_nullRequest() { - assertThrows(NullPointerException.class, + assertThrows( + NullPointerException.class, () -> mCredentialManager.unregisterCredentialDescription(null)); } @Test public void testUnregisterCredentialDescription_success() throws RemoteException { mCredentialManager.unregisterCredentialDescription(mUnregisterRequest); - verify(mMockCredentialManagerService).unregisterCredentialDescription( - same(mUnregisterRequest), eq(mPackageName)); + verify(mMockCredentialManagerService) + .unregisterCredentialDescription(same(mUnregisterRequest), eq(mPackageName)); } } diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt index ccbd46d79437..31af7aa78c69 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt @@ -217,13 +217,27 @@ fun PrimarySelectionCard( text = stringResource( if (hasSingleEntry) { val singleEntryType = sortedUserNameToCredentialEntryList.firstOrNull() - ?.sortedCredentialEntryList?.first()?.credentialType + ?.sortedCredentialEntryList?.firstOrNull()?.credentialType if (singleEntryType == CredentialType.PASSKEY) R.string.get_dialog_title_use_passkey_for else if (singleEntryType == CredentialType.PASSWORD) R.string.get_dialog_title_use_password_for + else if (authenticationEntryList.isNotEmpty()) + R.string.get_dialog_title_unlock_options_for else R.string.get_dialog_title_use_sign_in_for - } else R.string.get_dialog_title_choose_sign_in_for, + } else { + if (authenticationEntryList.isNotEmpty() || + sortedUserNameToCredentialEntryList.any { perNameEntryList -> + perNameEntryList.sortedCredentialEntryList.any { entry -> + entry.credentialType != CredentialType.PASSWORD && + entry.credentialType != CredentialType.PASSKEY + } + } + ) + R.string.get_dialog_title_choose_sign_in_for + else + R.string.get_dialog_title_choose_saved_sign_in_for + }, requestDisplayInfo.appName ), ) diff --git a/services/core/java/com/android/server/input/InputFeatureFlagProvider.java b/services/core/java/com/android/server/input/InputFeatureFlagProvider.java new file mode 100644 index 000000000000..d7efc05f8052 --- /dev/null +++ b/services/core/java/com/android/server/input/InputFeatureFlagProvider.java @@ -0,0 +1,67 @@ +/* + * Copyright 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.input; + +import android.os.SystemProperties; + +import java.util.Optional; + +/** + * A component of {@link InputManagerService} responsible for managing the input sysprop flags + * + * @hide + */ +@SuppressWarnings("OptionalUsedAsFieldOrParameterType") +public final class InputFeatureFlagProvider { + + // To disable Keyboard backlight control via Framework, run: + // 'adb shell setprop persist.input.keyboard_backlight_control.enabled false' (requires restart) + private static final boolean KEYBOARD_BACKLIGHT_CONTROL_ENABLED = SystemProperties.getBoolean( + "persist.input.keyboard.backlight_control.enabled", true); + + // To disable Framework controlled keyboard backlight animation run: + // adb shell setprop persist.input.keyboard_backlight_animation.enabled false (requires restart) + private static final boolean KEYBOARD_BACKLIGHT_ANIMATION_ENABLED = SystemProperties.getBoolean( + "persist.input.keyboard.keyboard_backlight_animation.enabled", false); + + private static Optional<Boolean> sKeyboardBacklightControlOverride = Optional.empty(); + private static Optional<Boolean> sKeyboardBacklightAnimationOverride = Optional.empty(); + + public static boolean isKeyboardBacklightControlEnabled() { + return sKeyboardBacklightControlOverride.orElse(KEYBOARD_BACKLIGHT_CONTROL_ENABLED); + } + + public static boolean isKeyboardBacklightAnimationEnabled() { + return sKeyboardBacklightAnimationOverride.orElse(KEYBOARD_BACKLIGHT_ANIMATION_ENABLED); + } + + public static void setKeyboardBacklightControlEnabled(boolean enabled) { + sKeyboardBacklightControlOverride = Optional.of(enabled); + } + + public static void setKeyboardBacklightAnimationEnabled(boolean enabled) { + sKeyboardBacklightAnimationOverride = Optional.of(enabled); + } + + /** + * Clears all input feature flag overrides. + */ + public static void clearOverrides() { + sKeyboardBacklightControlOverride = Optional.empty(); + sKeyboardBacklightAnimationOverride = Optional.empty(); + } +} diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java index 5f45f912a87a..23beb44f497c 100644 --- a/services/core/java/com/android/server/input/InputManagerService.java +++ b/services/core/java/com/android/server/input/InputManagerService.java @@ -72,7 +72,6 @@ import android.os.RemoteCallbackList; import android.os.RemoteException; import android.os.ResultReceiver; import android.os.ShellCallback; -import android.os.SystemProperties; import android.os.UserHandle; import android.os.VibrationEffect; import android.os.vibrator.StepSegment; @@ -157,11 +156,6 @@ public class InputManagerService extends IInputManager.Stub private static final AdditionalDisplayInputProperties DEFAULT_ADDITIONAL_DISPLAY_INPUT_PROPERTIES = new AdditionalDisplayInputProperties(); - // To disable Keyboard backlight control via Framework, run: - // 'adb shell setprop persist.input.keyboard_backlight_control.enabled false' (requires restart) - private static final boolean KEYBOARD_BACKLIGHT_CONTROL_ENABLED = SystemProperties.getBoolean( - "persist.input.keyboard.backlight_control.enabled", true); - private final NativeInputManagerService mNative; private final Context mContext; @@ -431,10 +425,9 @@ public class InputManagerService extends IInputManager.Stub mKeyboardLayoutManager = new KeyboardLayoutManager(mContext, mNative, mDataStore, injector.getLooper()); mBatteryController = new BatteryController(mContext, mNative, injector.getLooper()); - mKeyboardBacklightController = - KEYBOARD_BACKLIGHT_CONTROL_ENABLED ? new KeyboardBacklightController(mContext, - mNative, mDataStore, injector.getLooper()) - : new KeyboardBacklightControllerInterface() {}; + mKeyboardBacklightController = InputFeatureFlagProvider.isKeyboardBacklightControlEnabled() + ? new KeyboardBacklightController(mContext, mNative, mDataStore, + injector.getLooper()) : new KeyboardBacklightControllerInterface() {}; mKeyRemapper = new KeyRemapper(mContext, mNative, mDataStore, injector.getLooper()); mUseDevInputEventForAudioJack = diff --git a/services/core/java/com/android/server/input/KeyboardBacklightController.java b/services/core/java/com/android/server/input/KeyboardBacklightController.java index 48c346a2fe22..3ea19cb4063a 100644 --- a/services/core/java/com/android/server/input/KeyboardBacklightController.java +++ b/services/core/java/com/android/server/input/KeyboardBacklightController.java @@ -16,6 +16,7 @@ package com.android.server.input; +import android.animation.ValueAnimator; import android.annotation.BinderThread; import android.content.Context; import android.graphics.Color; @@ -70,6 +71,8 @@ final class KeyboardBacklightController implements private static final int MSG_INTERACTIVE_STATE_CHANGED = 6; private static final int MAX_BRIGHTNESS = 255; private static final int NUM_BRIGHTNESS_CHANGE_STEPS = 10; + private static final long TRANSITION_ANIMATION_DURATION_MILLIS = Duration.ofSeconds( + 1).toMillis(); private static final String UEVENT_KEYBOARD_BACKLIGHT_TAG = "kbd_backlight"; @@ -85,6 +88,7 @@ final class KeyboardBacklightController implements @GuardedBy("mDataStore") private final PersistentDataStore mDataStore; private final Handler mHandler; + private final AnimatorFactory mAnimatorFactory; // Always access on handler thread or need to lock this for synchronization. private final SparseArray<KeyboardBacklightState> mKeyboardBacklights = new SparseArray<>(1); // Maintains state if all backlights should be on or turned off @@ -109,10 +113,17 @@ final class KeyboardBacklightController implements KeyboardBacklightController(Context context, NativeInputManagerService nativeService, PersistentDataStore dataStore, Looper looper) { + this(context, nativeService, dataStore, looper, ValueAnimator::ofInt); + } + + @VisibleForTesting + KeyboardBacklightController(Context context, NativeInputManagerService nativeService, + PersistentDataStore dataStore, Looper looper, AnimatorFactory animatorFactory) { mContext = context; mNative = nativeService; mDataStore = dataStore; mHandler = new Handler(looper, this::handleMessage); + mAnimatorFactory = animatorFactory; } @Override @@ -177,8 +188,7 @@ final class KeyboardBacklightController implements } else { newBrightnessLevel = Math.max(currBrightnessLevel - 1, 0); } - updateBacklightState(deviceId, keyboardBacklight, newBrightnessLevel, - true /* isTriggeredByKeyPress */); + updateBacklightState(deviceId, newBrightnessLevel, true /* isTriggeredByKeyPress */); synchronized (mDataStore) { try { @@ -203,8 +213,7 @@ final class KeyboardBacklightController implements if (index < 0) { index = Math.min(NUM_BRIGHTNESS_CHANGE_STEPS, -(index + 1)); } - updateBacklightState(inputDevice.getId(), keyboardBacklight, index, - false /* isTriggeredByKeyPress */); + updateBacklightState(inputDevice.getId(), index, false /* isTriggeredByKeyPress */); if (DEBUG) { Slog.d(TAG, "Restoring brightness level " + brightness.getAsInt()); } @@ -217,14 +226,10 @@ final class KeyboardBacklightController implements if (!mIsInteractive) { return; } - if (!mIsBacklightOn) { - mIsBacklightOn = true; - for (int i = 0; i < mKeyboardBacklights.size(); i++) { - int deviceId = mKeyboardBacklights.keyAt(i); - KeyboardBacklightState state = mKeyboardBacklights.valueAt(i); - updateBacklightState(deviceId, state.mLight, state.mBrightnessLevel, - false /* isTriggeredByKeyPress */); - } + mIsBacklightOn = true; + for (int i = 0; i < mKeyboardBacklights.size(); i++) { + KeyboardBacklightState state = mKeyboardBacklights.valueAt(i); + state.onBacklightStateChanged(); } mHandler.removeMessages(MSG_NOTIFY_USER_INACTIVITY); mHandler.sendEmptyMessageAtTime(MSG_NOTIFY_USER_INACTIVITY, @@ -232,14 +237,10 @@ final class KeyboardBacklightController implements } private void handleUserInactivity() { - if (mIsBacklightOn) { - mIsBacklightOn = false; - for (int i = 0; i < mKeyboardBacklights.size(); i++) { - int deviceId = mKeyboardBacklights.keyAt(i); - KeyboardBacklightState state = mKeyboardBacklights.valueAt(i); - updateBacklightState(deviceId, state.mLight, state.mBrightnessLevel, - false /* isTriggeredByKeyPress */); - } + mIsBacklightOn = false; + for (int i = 0; i < mKeyboardBacklights.size(); i++) { + KeyboardBacklightState state = mKeyboardBacklights.valueAt(i); + state.onBacklightStateChanged(); } } @@ -310,7 +311,7 @@ final class KeyboardBacklightController implements return; } // The keyboard backlight was added or changed. - mKeyboardBacklights.put(deviceId, new KeyboardBacklightState(keyboardBacklight)); + mKeyboardBacklights.put(deviceId, new KeyboardBacklightState(deviceId, keyboardBacklight)); restoreBacklightBrightness(inputDevice, keyboardBacklight); } @@ -372,21 +373,14 @@ final class KeyboardBacklightController implements } } - private void updateBacklightState(int deviceId, Light light, int brightnessLevel, + private void updateBacklightState(int deviceId, int brightnessLevel, boolean isTriggeredByKeyPress) { KeyboardBacklightState state = mKeyboardBacklights.get(deviceId); if (state == null) { return; } - mNative.setLightColor(deviceId, light.getId(), - mIsBacklightOn ? Color.argb(BRIGHTNESS_VALUE_FOR_LEVEL[brightnessLevel], 0, 0, 0) - : 0); - if (DEBUG) { - Slog.d(TAG, "Changing state from " + state.mBrightnessLevel + " to " + brightnessLevel - + "(isBacklightOn = " + mIsBacklightOn + ")"); - } - state.mBrightnessLevel = brightnessLevel; + state.setBrightnessLevel(brightnessLevel); synchronized (mKeyboardBacklightListenerRecords) { for (int i = 0; i < mKeyboardBacklightListenerRecords.size(); i++) { @@ -397,6 +391,10 @@ final class KeyboardBacklightController implements deviceId, callbackState, isTriggeredByKeyPress); } } + + if (DEBUG) { + Slog.d(TAG, "Changing state from " + state.mBrightnessLevel + " to " + brightnessLevel); + } } private void onKeyboardBacklightListenerDied(int pid) { @@ -436,10 +434,7 @@ final class KeyboardBacklightController implements @Override public void dump(PrintWriter pw) { IndentingPrintWriter ipw = new IndentingPrintWriter(pw); - ipw.println( - TAG + ": " + mKeyboardBacklights.size() + " keyboard backlights, isBacklightOn = " - + mIsBacklightOn); - + ipw.println(TAG + ": " + mKeyboardBacklights.size() + " keyboard backlights"); ipw.increaseIndent(); for (int i = 0; i < mKeyboardBacklights.size(); i++) { KeyboardBacklightState state = mKeyboardBacklights.valueAt(i); @@ -448,6 +443,10 @@ final class KeyboardBacklightController implements ipw.decreaseIndent(); } + private static boolean isAnimationEnabled() { + return InputFeatureFlagProvider.isKeyboardBacklightAnimationEnabled(); + } + // A record of a registered Keyboard backlight listener from one process. private class KeyboardBacklightListenerRecord implements IBinder.DeathRecipient { public final int mPid; @@ -478,14 +477,55 @@ final class KeyboardBacklightController implements } } - private static class KeyboardBacklightState { + private class KeyboardBacklightState { + private final int mDeviceId; private final Light mLight; private int mBrightnessLevel; + private ValueAnimator mAnimator; - KeyboardBacklightState(Light light) { + KeyboardBacklightState(int deviceId, Light light) { + mDeviceId = deviceId; mLight = light; } + private void onBacklightStateChanged() { + setBacklightValue(mIsBacklightOn ? BRIGHTNESS_VALUE_FOR_LEVEL[mBrightnessLevel] : 0); + } + private void setBrightnessLevel(int brightnessLevel) { + if (mIsBacklightOn) { + setBacklightValue(BRIGHTNESS_VALUE_FOR_LEVEL[brightnessLevel]); + } + mBrightnessLevel = brightnessLevel; + } + + private void cancelAnimation() { + if (mAnimator != null && mAnimator.isRunning()) { + mAnimator.cancel(); + } + } + + private void setBacklightValue(int toValue) { + int fromValue = Color.alpha(mNative.getLightColor(mDeviceId, mLight.getId())); + if (fromValue == toValue) { + return; + } + if (isAnimationEnabled()) { + startAnimation(fromValue, toValue); + } else { + mNative.setLightColor(mDeviceId, mLight.getId(), Color.argb(toValue, 0, 0, 0)); + } + } + + private void startAnimation(int fromValue, int toValue) { + // Cancel any ongoing animation before starting a new one + cancelAnimation(); + mAnimator = mAnimatorFactory.makeIntAnimator(fromValue, toValue); + mAnimator.addUpdateListener( + (animation) -> mNative.setLightColor(mDeviceId, mLight.getId(), + Color.argb((int) animation.getAnimatedValue(), 0, 0, 0))); + mAnimator.setDuration(TRANSITION_ANIMATION_DURATION_MILLIS).start(); + } + @Override public String toString() { return "KeyboardBacklightState{Light=" + mLight.getId() @@ -493,4 +533,9 @@ final class KeyboardBacklightController implements + "}"; } } + + @VisibleForTesting + interface AnimatorFactory { + ValueAnimator makeIntAnimator(int from, int to); + } } diff --git a/services/credentials/java/com/android/server/credentials/CredentialManagerService.java b/services/credentials/java/com/android/server/credentials/CredentialManagerService.java index 6793800364de..08d7d5b872ac 100644 --- a/services/credentials/java/com/android/server/credentials/CredentialManagerService.java +++ b/services/credentials/java/com/android/server/credentials/CredentialManagerService.java @@ -18,7 +18,6 @@ package com.android.server.credentials; import static android.Manifest.permission.CREDENTIAL_MANAGER_SET_ALLOWED_PROVIDERS; import static android.Manifest.permission.CREDENTIAL_MANAGER_SET_ORIGIN; -import static android.Manifest.permission.LAUNCH_CREDENTIAL_SELECTOR; import static android.content.Context.CREDENTIAL_SERVICE; import static android.content.pm.PackageManager.PERMISSION_GRANTED; @@ -46,7 +45,6 @@ import android.credentials.ISetEnabledProvidersCallback; import android.credentials.PrepareGetCredentialResponseInternal; import android.credentials.RegisterCredentialDescriptionRequest; import android.credentials.UnregisterCredentialDescriptionRequest; -import android.credentials.ui.IntentFactory; import android.os.Binder; import android.os.CancellationSignal; import android.os.IBinder; @@ -69,6 +67,7 @@ import com.android.server.infra.AbstractMasterSystemService; import com.android.server.infra.SecureSettingsServiceNameResolver; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; @@ -282,6 +281,18 @@ public final class CredentialManagerService } } + private Set<String> getPrimaryProvidersForUserId(int userId) { + final int resolvedUserId = ActivityManager.handleIncomingUser( + Binder.getCallingPid(), Binder.getCallingUid(), + userId, false, false, + "getPrimaryProvidersForUserId", null); + SecureSettingsServiceNameResolver resolver = new SecureSettingsServiceNameResolver( + mContext, Settings.Secure.CREDENTIAL_SERVICE_PRIMARY, + /* isMultipleMode= */ true); + String[] serviceNames = resolver.readServiceNameList(resolvedUserId); + return new HashSet<String>(Arrays.asList(serviceNames)); + } + @GuardedBy("mLock") private List<CredentialManagerServiceImpl> getCredentialProviderServicesLocked(int userId) { List<CredentialManagerServiceImpl> concatenatedServices = new ArrayList<>(); @@ -689,7 +700,8 @@ public final class CredentialManagerService @Override public void setEnabledProviders( - List<String> providers, int userId, ISetEnabledProvidersCallback callback) { + List<String> primaryProviders, List<String> providers, int userId, + ISetEnabledProvidersCallback callback) { if (!hasWriteSecureSettingsPermission()) { try { callback.onError( @@ -710,17 +722,24 @@ public final class CredentialManagerService "setEnabledProviders", null); - String storedValue = String.join(":", providers); - if (!Settings.Secure.putStringForUser( - getContext().getContentResolver(), - Settings.Secure.CREDENTIAL_SERVICE, - storedValue, - userId)) { - Slog.e(TAG, "Failed to store setting containing enabled providers"); + boolean writeEnabledStatus = + Settings.Secure.putStringForUser(getContext().getContentResolver(), + Settings.Secure.CREDENTIAL_SERVICE, + String.join(":", providers), + userId); + + boolean writePrimaryStatus = + Settings.Secure.putStringForUser(getContext().getContentResolver(), + Settings.Secure.CREDENTIAL_SERVICE_PRIMARY, + String.join(":", primaryProviders), + userId); + + if (!writeEnabledStatus || !writePrimaryStatus) { + Slog.e(TAG, "Failed to store setting containing enabled or primary providers"); try { callback.onError( "failed_setting_store", - "Failed to store setting containing enabled providers"); + "Failed to store setting containing enabled or primary providers"); } catch (RemoteException e) { Slog.e(TAG, "Issue with invoking error response: ", e); return; @@ -734,10 +753,6 @@ public final class CredentialManagerService Slog.e(TAG, "Issue with invoking response: ", e); // TODO: Propagate failure } - - // Send an intent to the UI that we have new enabled providers. - getContext().sendBroadcast(IntentFactory.createProviderUpdateIntent(), - LAUNCH_CREDENTIAL_SELECTOR); } @Override @@ -785,7 +800,8 @@ public final class CredentialManagerService verifyGetProvidersPermission(); return CredentialProviderInfoFactory.getCredentialProviderServices( - mContext, userId, providerFilter, getEnabledProviders()); + mContext, userId, providerFilter, getEnabledProviders(), + getPrimaryProvidersForUserId(userId)); } @Override @@ -795,7 +811,8 @@ public final class CredentialManagerService final int userId = UserHandle.getCallingUserId(); return CredentialProviderInfoFactory.getCredentialProviderServicesForTesting( - mContext, userId, providerFilter, getEnabledProviders()); + mContext, userId, providerFilter, getEnabledProviders(), + getPrimaryProvidersForUserId(userId)); } @Override diff --git a/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java b/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java index c2b51029778e..b3812c9138de 100644 --- a/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java +++ b/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java @@ -36,6 +36,7 @@ import android.service.credentials.CredentialProviderInfoFactory; import android.util.Slog; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.UUID; @@ -154,7 +155,9 @@ public class CredentialManagerUi { mContext, mUserId, CredentialManager.PROVIDER_FILTER_USER_PROVIDERS_ONLY, - mEnabledProviders); + mEnabledProviders, + // Don't need primary providers here. + new HashSet<String>()); List<DisabledProviderData> disabledProviderDataList = allProviders.stream() .filter(provider -> !provider.isEnabled()) diff --git a/services/tests/servicestests/src/com/android/server/input/KeyboardBacklightControllerTests.kt b/services/tests/servicestests/src/com/android/server/input/KeyboardBacklightControllerTests.kt index 64c05dc8ab84..272679280a62 100644 --- a/services/tests/servicestests/src/com/android/server/input/KeyboardBacklightControllerTests.kt +++ b/services/tests/servicestests/src/com/android/server/input/KeyboardBacklightControllerTests.kt @@ -16,6 +16,7 @@ package com.android.server.input +import android.animation.ValueAnimator import android.content.Context import android.content.ContextWrapper import android.graphics.Color @@ -29,6 +30,7 @@ import android.os.UEventObserver import android.os.test.TestLooper import android.platform.test.annotations.Presubmit import android.view.InputDevice +import androidx.test.annotation.UiThreadTest import androidx.test.core.app.ApplicationProvider import com.android.server.input.KeyboardBacklightController.BRIGHTNESS_VALUE_FOR_LEVEL import com.android.server.input.KeyboardBacklightController.USER_INACTIVITY_THRESHOLD_MILLIS @@ -96,9 +98,11 @@ class KeyboardBacklightControllerTests { private lateinit var context: Context private lateinit var dataStore: PersistentDataStore private lateinit var testLooper: TestLooper + private val totalLevels = BRIGHTNESS_VALUE_FOR_LEVEL.size private var lightColorMap: HashMap<Int, Int> = HashMap() private var lastBacklightState: KeyboardBacklightState? = null private var sysfsNodeChanges = 0 + private var lastAnimationValues = IntArray(2) @Before fun setup() { @@ -115,8 +119,8 @@ class KeyboardBacklightControllerTests { override fun finishWrite(fos: FileOutputStream?, success: Boolean) {} }) testLooper = TestLooper() - keyboardBacklightController = - KeyboardBacklightController(context, native, dataStore, testLooper.looper) + keyboardBacklightController = KeyboardBacklightController(context, native, dataStore, + testLooper.looper, FakeAnimatorFactory()) InputManagerGlobal.resetInstance(iInputManager) val inputManager = InputManager(context) `when`(context.getSystemService(eq(Context.INPUT_SERVICE))).thenReturn(inputManager) @@ -125,6 +129,10 @@ class KeyboardBacklightControllerTests { val args = it.arguments lightColorMap.put(args[1] as Int, args[2] as Int) } + `when`(native.getLightColor(anyInt(), anyInt())).thenAnswer { + val args = it.arguments + lightColorMap.getOrDefault(args[1] as Int, 0) + } lightColorMap.clear() `when`(native.sysfsNodeChanged(any())).then { sysfsNodeChanges++ @@ -138,271 +146,287 @@ class KeyboardBacklightControllerTests { @Test fun testKeyboardBacklightIncrementDecrement() { - val keyboardWithBacklight = createKeyboard(DEVICE_ID) - val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) - `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) - `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) - keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) + BacklightAnimationFlag(false).use { + val keyboardWithBacklight = createKeyboard(DEVICE_ID) + val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) + `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) + `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) + keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) - for (level in 1 until BRIGHTNESS_VALUE_FOR_LEVEL.size) { + for (level in 1 until totalLevels) { + incrementKeyboardBacklight(DEVICE_ID) + assertEquals( + "Light value for level $level mismatched", + Color.argb(BRIGHTNESS_VALUE_FOR_LEVEL[level], 0, 0, 0), + lightColorMap[LIGHT_ID] + ) + assertEquals( + "Light value for level $level must be correctly stored in the datastore", + BRIGHTNESS_VALUE_FOR_LEVEL[level], + dataStore.getKeyboardBacklightBrightness( + keyboardWithBacklight.descriptor, + LIGHT_ID + ).asInt + ) + } + + // Increment above max level incrementKeyboardBacklight(DEVICE_ID) assertEquals( - "Light value for level $level mismatched", - Color.argb(BRIGHTNESS_VALUE_FOR_LEVEL[level], 0, 0, 0), + "Light value for max level mismatched", + Color.argb(MAX_BRIGHTNESS, 0, 0, 0), lightColorMap[LIGHT_ID] ) assertEquals( - "Light value for level $level must be correctly stored in the datastore", - BRIGHTNESS_VALUE_FOR_LEVEL[level], + "Light value for max level must be correctly stored in the datastore", + MAX_BRIGHTNESS, dataStore.getKeyboardBacklightBrightness( - keyboardWithBacklight.descriptor, - LIGHT_ID + keyboardWithBacklight.descriptor, + LIGHT_ID ).asInt ) - } - // Increment above max level - incrementKeyboardBacklight(DEVICE_ID) - assertEquals( - "Light value for max level mismatched", - Color.argb(MAX_BRIGHTNESS, 0, 0, 0), - lightColorMap[LIGHT_ID] - ) - assertEquals( - "Light value for max level must be correctly stored in the datastore", - MAX_BRIGHTNESS, - dataStore.getKeyboardBacklightBrightness( - keyboardWithBacklight.descriptor, - LIGHT_ID - ).asInt - ) + for (level in totalLevels - 2 downTo 0) { + decrementKeyboardBacklight(DEVICE_ID) + assertEquals( + "Light value for level $level mismatched", + Color.argb(BRIGHTNESS_VALUE_FOR_LEVEL[level], 0, 0, 0), + lightColorMap[LIGHT_ID] + ) + assertEquals( + "Light value for level $level must be correctly stored in the datastore", + BRIGHTNESS_VALUE_FOR_LEVEL[level], + dataStore.getKeyboardBacklightBrightness( + keyboardWithBacklight.descriptor, + LIGHT_ID + ).asInt + ) + } - for (level in BRIGHTNESS_VALUE_FOR_LEVEL.size - 2 downTo 0) { + // Decrement below min level decrementKeyboardBacklight(DEVICE_ID) assertEquals( - "Light value for level $level mismatched", - Color.argb(BRIGHTNESS_VALUE_FOR_LEVEL[level], 0, 0, 0), + "Light value for min level mismatched", + Color.argb(0, 0, 0, 0), lightColorMap[LIGHT_ID] ) assertEquals( - "Light value for level $level must be correctly stored in the datastore", - BRIGHTNESS_VALUE_FOR_LEVEL[level], + "Light value for min level must be correctly stored in the datastore", + 0, dataStore.getKeyboardBacklightBrightness( - keyboardWithBacklight.descriptor, - LIGHT_ID + keyboardWithBacklight.descriptor, + LIGHT_ID ).asInt ) } - - // Decrement below min level - decrementKeyboardBacklight(DEVICE_ID) - assertEquals( - "Light value for min level mismatched", - Color.argb(0, 0, 0, 0), - lightColorMap[LIGHT_ID] - ) - assertEquals( - "Light value for min level must be correctly stored in the datastore", - 0, - dataStore.getKeyboardBacklightBrightness( - keyboardWithBacklight.descriptor, - LIGHT_ID - ).asInt - ) } @Test fun testKeyboardWithoutBacklight() { - val keyboardWithoutBacklight = createKeyboard(DEVICE_ID) - val keyboardInputLight = createLight(LIGHT_ID, Light.LIGHT_TYPE_INPUT) - `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithoutBacklight) - `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardInputLight)) - keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) - - incrementKeyboardBacklight(DEVICE_ID) - assertTrue("Non Keyboard backlights should not change", lightColorMap.isEmpty()) + BacklightAnimationFlag(false).use { + val keyboardWithoutBacklight = createKeyboard(DEVICE_ID) + val keyboardInputLight = createLight(LIGHT_ID, Light.LIGHT_TYPE_INPUT) + `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithoutBacklight) + `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardInputLight)) + keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) + + incrementKeyboardBacklight(DEVICE_ID) + assertTrue("Non Keyboard backlights should not change", lightColorMap.isEmpty()) + } } @Test fun testKeyboardWithMultipleLight() { - val keyboardWithBacklight = createKeyboard(DEVICE_ID) - val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) - val keyboardInputLight = createLight(SECOND_LIGHT_ID, Light.LIGHT_TYPE_INPUT) - `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) - `when`(iInputManager.getLights(DEVICE_ID)).thenReturn( - listOf( - keyboardBacklight, - keyboardInputLight + BacklightAnimationFlag(false).use { + val keyboardWithBacklight = createKeyboard(DEVICE_ID) + val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) + val keyboardInputLight = createLight(SECOND_LIGHT_ID, Light.LIGHT_TYPE_INPUT) + `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) + `when`(iInputManager.getLights(DEVICE_ID)).thenReturn( + listOf( + keyboardBacklight, + keyboardInputLight + ) ) - ) - keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) + keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) - incrementKeyboardBacklight(DEVICE_ID) - assertEquals("Only keyboard backlights should change", 1, lightColorMap.size) - assertNotNull("Keyboard backlight should change", lightColorMap[LIGHT_ID]) - assertNull("Input lights should not change", lightColorMap[SECOND_LIGHT_ID]) + incrementKeyboardBacklight(DEVICE_ID) + assertEquals("Only keyboard backlights should change", 1, lightColorMap.size) + assertNotNull("Keyboard backlight should change", lightColorMap[LIGHT_ID]) + assertNull("Input lights should not change", lightColorMap[SECOND_LIGHT_ID]) + } } @Test fun testRestoreBacklightOnInputDeviceAdded() { - val keyboardWithBacklight = createKeyboard(DEVICE_ID) - val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) - `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) - `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) - - for (level in 1 until BRIGHTNESS_VALUE_FOR_LEVEL.size) { - dataStore.setKeyboardBacklightBrightness( + BacklightAnimationFlag(false).use { + val keyboardWithBacklight = createKeyboard(DEVICE_ID) + val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) + `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) + `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) + + for (level in 1 until totalLevels) { + dataStore.setKeyboardBacklightBrightness( keyboardWithBacklight.descriptor, LIGHT_ID, BRIGHTNESS_VALUE_FOR_LEVEL[level] - 1 - ) - - keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) - keyboardBacklightController.notifyUserActivity() - testLooper.dispatchNext() - assertEquals( - "Keyboard backlight level should be restored to the level saved in the data " + - "store", + ) + + keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) + keyboardBacklightController.notifyUserActivity() + testLooper.dispatchNext() + assertEquals( + "Keyboard backlight level should be restored to the level saved in the " + + "data store", Color.argb(BRIGHTNESS_VALUE_FOR_LEVEL[level], 0, 0, 0), lightColorMap[LIGHT_ID] - ) - keyboardBacklightController.onInputDeviceRemoved(DEVICE_ID) + ) + keyboardBacklightController.onInputDeviceRemoved(DEVICE_ID) + } } } @Test fun testRestoreBacklightOnInputDeviceChanged() { - val keyboardWithBacklight = createKeyboard(DEVICE_ID) - val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) - `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) - dataStore.setKeyboardBacklightBrightness( - keyboardWithBacklight.descriptor, - LIGHT_ID, - MAX_BRIGHTNESS - ) + BacklightAnimationFlag(false).use { + val keyboardWithBacklight = createKeyboard(DEVICE_ID) + val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) + `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) + dataStore.setKeyboardBacklightBrightness( + keyboardWithBacklight.descriptor, + LIGHT_ID, + MAX_BRIGHTNESS + ) - keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) - keyboardBacklightController.notifyUserActivity() - testLooper.dispatchNext() - assertTrue( - "Keyboard backlight should not be changed until its added", - lightColorMap.isEmpty() - ) + keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) + keyboardBacklightController.notifyUserActivity() + testLooper.dispatchNext() + assertTrue( + "Keyboard backlight should not be changed until its added", + lightColorMap.isEmpty() + ) - `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) - keyboardBacklightController.onInputDeviceChanged(DEVICE_ID) - keyboardBacklightController.notifyUserActivity() - testLooper.dispatchNext() - assertEquals( - "Keyboard backlight level should be restored to the level saved in the data store", - Color.argb(MAX_BRIGHTNESS, 0, 0, 0), - lightColorMap[LIGHT_ID] - ) + `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) + keyboardBacklightController.onInputDeviceChanged(DEVICE_ID) + keyboardBacklightController.notifyUserActivity() + testLooper.dispatchNext() + assertEquals( + "Keyboard backlight level should be restored to the level saved in the data store", + Color.argb(MAX_BRIGHTNESS, 0, 0, 0), + lightColorMap[LIGHT_ID] + ) + } } @Test fun testKeyboardBacklight_registerUnregisterListener() { - val keyboardWithBacklight = createKeyboard(DEVICE_ID) - val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) - `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) - `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) - keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) + BacklightAnimationFlag(false).use { + val keyboardWithBacklight = createKeyboard(DEVICE_ID) + val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) + `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) + `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) + keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) - // Register backlight listener - val listener = KeyboardBacklightListener() - keyboardBacklightController.registerKeyboardBacklightListener(listener, 0) + // Register backlight listener + val listener = KeyboardBacklightListener() + keyboardBacklightController.registerKeyboardBacklightListener(listener, 0) - lastBacklightState = null - keyboardBacklightController.incrementKeyboardBacklight(DEVICE_ID) - testLooper.dispatchNext() + lastBacklightState = null + keyboardBacklightController.incrementKeyboardBacklight(DEVICE_ID) + testLooper.dispatchNext() - assertEquals( - "Backlight state device Id should be $DEVICE_ID", - DEVICE_ID, - lastBacklightState!!.deviceId - ) - assertEquals( - "Backlight state brightnessLevel should be " + 1, - 1, - lastBacklightState!!.brightnessLevel - ) - assertEquals( - "Backlight state maxBrightnessLevel should be " + (BRIGHTNESS_VALUE_FOR_LEVEL.size - 1), - (BRIGHTNESS_VALUE_FOR_LEVEL.size - 1), - lastBacklightState!!.maxBrightnessLevel - ) - assertEquals( - "Backlight state isTriggeredByKeyPress should be true", - true, - lastBacklightState!!.isTriggeredByKeyPress - ) + assertEquals( + "Backlight state device Id should be $DEVICE_ID", + DEVICE_ID, + lastBacklightState!!.deviceId + ) + assertEquals( + "Backlight state brightnessLevel should be " + 1, + 1, + lastBacklightState!!.brightnessLevel + ) + assertEquals( + "Backlight state maxBrightnessLevel should be " + (totalLevels - 1), + (totalLevels - 1), + lastBacklightState!!.maxBrightnessLevel + ) + assertEquals( + "Backlight state isTriggeredByKeyPress should be true", + true, + lastBacklightState!!.isTriggeredByKeyPress + ) - // Unregister listener - keyboardBacklightController.unregisterKeyboardBacklightListener(listener, 0) + // Unregister listener + keyboardBacklightController.unregisterKeyboardBacklightListener(listener, 0) - lastBacklightState = null - incrementKeyboardBacklight(DEVICE_ID) + lastBacklightState = null + incrementKeyboardBacklight(DEVICE_ID) - assertNull("Listener should not receive any updates", lastBacklightState) + assertNull("Listener should not receive any updates", lastBacklightState) + } } @Test fun testKeyboardBacklight_userActivity() { - val keyboardWithBacklight = createKeyboard(DEVICE_ID) - val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) - `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) - `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) - dataStore.setKeyboardBacklightBrightness( - keyboardWithBacklight.descriptor, - LIGHT_ID, - MAX_BRIGHTNESS - ) + BacklightAnimationFlag(false).use { + val keyboardWithBacklight = createKeyboard(DEVICE_ID) + val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) + `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) + `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) + dataStore.setKeyboardBacklightBrightness( + keyboardWithBacklight.descriptor, + LIGHT_ID, + MAX_BRIGHTNESS + ) - keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) - keyboardBacklightController.notifyUserActivity() - testLooper.dispatchNext() - assertEquals( - "Keyboard backlight level should be restored to the level saved in the data store", - Color.argb(MAX_BRIGHTNESS, 0, 0, 0), - lightColorMap[LIGHT_ID] - ) + keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) + keyboardBacklightController.notifyUserActivity() + testLooper.dispatchNext() + assertEquals( + "Keyboard backlight level should be restored to the level saved in the data store", + Color.argb(MAX_BRIGHTNESS, 0, 0, 0), + lightColorMap[LIGHT_ID] + ) - testLooper.moveTimeForward(USER_INACTIVITY_THRESHOLD_MILLIS + 1000) - testLooper.dispatchNext() - assertEquals( - "Keyboard backlight level should be turned off after inactivity", - 0, - lightColorMap[LIGHT_ID] - ) + testLooper.moveTimeForward(USER_INACTIVITY_THRESHOLD_MILLIS + 1000) + testLooper.dispatchNext() + assertEquals( + "Keyboard backlight level should be turned off after inactivity", + 0, + lightColorMap[LIGHT_ID] + ) + } } @Test fun testKeyboardBacklight_displayOnOff() { - val keyboardWithBacklight = createKeyboard(DEVICE_ID) - val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) - `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) - `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) - dataStore.setKeyboardBacklightBrightness( - keyboardWithBacklight.descriptor, - LIGHT_ID, - MAX_BRIGHTNESS - ) + BacklightAnimationFlag(false).use { + val keyboardWithBacklight = createKeyboard(DEVICE_ID) + val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) + `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) + `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) + dataStore.setKeyboardBacklightBrightness( + keyboardWithBacklight.descriptor, + LIGHT_ID, + MAX_BRIGHTNESS + ) - keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) - keyboardBacklightController.handleInteractiveStateChange(true /* isDisplayOn */) - assertEquals( - "Keyboard backlight level should be restored to the level saved in the data " + - "store when display turned on", - Color.argb(MAX_BRIGHTNESS, 0, 0, 0), - lightColorMap[LIGHT_ID] - ) + keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) + keyboardBacklightController.handleInteractiveStateChange(true /* isDisplayOn */) + assertEquals( + "Keyboard backlight level should be restored to the level saved in the data " + + "store when display turned on", + Color.argb(MAX_BRIGHTNESS, 0, 0, 0), + lightColorMap[LIGHT_ID] + ) - keyboardBacklightController.handleInteractiveStateChange(false /* isDisplayOn */) - assertEquals( - "Keyboard backlight level should be turned off after display is turned off", - 0, - lightColorMap[LIGHT_ID] - ) + keyboardBacklightController.handleInteractiveStateChange(false /* isDisplayOn */) + assertEquals( + "Keyboard backlight level should be turned off after display is turned off", + 0, + lightColorMap[LIGHT_ID] + ) + } } @Test @@ -463,6 +487,30 @@ class KeyboardBacklightControllerTests { ) } + @Test + @UiThreadTest + fun testKeyboardBacklightAnimation_onChangeLevels() { + BacklightAnimationFlag(true).use { + val keyboardWithBacklight = createKeyboard(DEVICE_ID) + val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) + `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) + `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) + keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) + + incrementKeyboardBacklight(DEVICE_ID) + assertEquals( + "Should start animation from level 0", + BRIGHTNESS_VALUE_FOR_LEVEL[0], + lastAnimationValues[0] + ) + assertEquals( + "Should start animation to level 1", + BRIGHTNESS_VALUE_FOR_LEVEL[1], + lastAnimationValues[1] + ) + } + } + inner class KeyboardBacklightListener : IKeyboardBacklightListener.Stub() { override fun onBrightnessChanged( deviceId: Int, @@ -496,4 +544,22 @@ class KeyboardBacklightControllerTests { val maxBrightnessLevel: Int, val isTriggeredByKeyPress: Boolean ) + + private inner class BacklightAnimationFlag constructor(enabled: Boolean) : AutoCloseable { + init { + InputFeatureFlagProvider.setKeyboardBacklightAnimationEnabled(enabled) + } + + override fun close() { + InputFeatureFlagProvider.clearOverrides() + } + } + + private inner class FakeAnimatorFactory : KeyboardBacklightController.AnimatorFactory { + override fun makeIntAnimator(from: Int, to: Int): ValueAnimator { + lastAnimationValues[0] = from + lastAnimationValues[1] = to + return ValueAnimator.ofInt(from, to) + } + } } |