blob: d490cda1e8eec6b2656252309c97455400f90b57 [file] [log] [blame]
/*
* Copyright (C) 2022 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.credentials;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.credentials.CredentialOption;
import android.credentials.CredentialProviderInfo;
import android.credentials.GetCredentialException;
import android.credentials.GetCredentialResponse;
import android.credentials.ui.AuthenticationEntry;
import android.credentials.ui.Entry;
import android.credentials.ui.GetCredentialProviderData;
import android.credentials.ui.ProviderPendingIntentResponse;
import android.os.ICancellationSignal;
import android.service.credentials.Action;
import android.service.credentials.BeginGetCredentialOption;
import android.service.credentials.BeginGetCredentialRequest;
import android.service.credentials.BeginGetCredentialResponse;
import android.service.credentials.CallingAppInfo;
import android.service.credentials.CredentialEntry;
import android.service.credentials.CredentialProviderService;
import android.service.credentials.GetCredentialRequest;
import android.service.credentials.RemoteEntry;
import android.util.Pair;
import android.util.Slog;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
/**
* Central provider session that listens for provider callbacks, and maintains provider state.
* Will likely split this into remote response state and UI state.
*
* @hide
*/
public final class ProviderGetSession extends ProviderSession<BeginGetCredentialRequest,
BeginGetCredentialResponse>
implements
RemoteCredentialService.ProviderCallbacks<BeginGetCredentialResponse> {
private static final String TAG = "ProviderGetSession";
// Key to be used as the entry key for an action entry
public static final String ACTION_ENTRY_KEY = "action_key";
// Key to be used as the entry key for the authentication entry
public static final String AUTHENTICATION_ACTION_ENTRY_KEY = "authentication_action_key";
// Key to be used as an entry key for a remote entry
public static final String REMOTE_ENTRY_KEY = "remote_entry_key";
// Key to be used as an entry key for a credential entry
public static final String CREDENTIAL_ENTRY_KEY = "credential_key";
@NonNull
private final Map<String, CredentialOption> mBeginGetOptionToCredentialOptionMap;
/** The complete request to be used in the second round. */
private final android.credentials.GetCredentialRequest mCompleteRequest;
private final CallingAppInfo mCallingAppInfo;
private GetCredentialException mProviderException;
private final ProviderResponseDataHandler mProviderResponseDataHandler;
/** Creates a new provider session to be used by the request session. */
@Nullable
public static ProviderGetSession createNewSession(
Context context,
@UserIdInt int userId,
CredentialProviderInfo providerInfo,
GetRequestSession getRequestSession,
RemoteCredentialService remoteCredentialService) {
android.credentials.GetCredentialRequest filteredRequest =
filterOptions(providerInfo.getCapabilities(),
getRequestSession.mClientRequest,
providerInfo);
if (filteredRequest != null) {
Map<String, CredentialOption> beginGetOptionToCredentialOptionMap =
new HashMap<>();
return new ProviderGetSession(
context,
providerInfo,
getRequestSession,
userId,
remoteCredentialService,
constructQueryPhaseRequest(
filteredRequest, getRequestSession.mClientAppInfo,
getRequestSession.mClientRequest.alwaysSendAppInfoToProvider(),
beginGetOptionToCredentialOptionMap),
filteredRequest,
getRequestSession.mClientAppInfo,
beginGetOptionToCredentialOptionMap,
getRequestSession.mHybridService
);
}
Slog.i(TAG, "Unable to create provider session for: "
+ providerInfo.getComponentName());
return null;
}
private static BeginGetCredentialRequest constructQueryPhaseRequest(
android.credentials.GetCredentialRequest filteredRequest,
CallingAppInfo callingAppInfo,
boolean propagateToProvider,
Map<String, CredentialOption> beginGetOptionToCredentialOptionMap
) {
BeginGetCredentialRequest.Builder builder = new BeginGetCredentialRequest.Builder();
filteredRequest.getCredentialOptions().forEach(option -> {
String id = generateUniqueId();
builder.addBeginGetCredentialOption(
new BeginGetCredentialOption(
id, option.getType(), option.getCandidateQueryData())
);
beginGetOptionToCredentialOptionMap.put(id, option);
});
if (propagateToProvider) {
builder.setCallingAppInfo(callingAppInfo);
}
return builder.build();
}
@Nullable
private static android.credentials.GetCredentialRequest filterOptions(
List<String> providerCapabilities,
android.credentials.GetCredentialRequest clientRequest,
CredentialProviderInfo info
) {
Slog.i(TAG, "Filtering request options for: " + info.getComponentName());
List<CredentialOption> filteredOptions = new ArrayList<>();
for (CredentialOption option : clientRequest.getCredentialOptions()) {
if (providerCapabilities.contains(option.getType())
&& isProviderAllowed(option, info)
&& checkSystemProviderRequirement(option, info.isSystemProvider())) {
Slog.i(TAG, "Option of type: " + option.getType() + " meets all filtering"
+ "conditions");
filteredOptions.add(option);
}
}
if (!filteredOptions.isEmpty()) {
return new android.credentials.GetCredentialRequest
.Builder(clientRequest.getData())
.setCredentialOptions(
filteredOptions).build();
}
Slog.i(TAG, "No options filtered");
return null;
}
private static boolean isProviderAllowed(CredentialOption option,
CredentialProviderInfo providerInfo) {
if (providerInfo.isSystemProvider()) {
// Always allow system providers , including the remote provider
return true;
}
if (!option.getAllowedProviders().isEmpty() && !option.getAllowedProviders().contains(
providerInfo.getComponentName())) {
Slog.i(TAG, "Provider allow list specified but does not contain this provider");
return false;
}
return true;
}
private static boolean checkSystemProviderRequirement(CredentialOption option,
boolean isSystemProvider) {
if (option.isSystemProviderRequired() && !isSystemProvider) {
Slog.i(TAG, "System provider required, but this service is not a system provider");
return false;
}
return true;
}
public ProviderGetSession(Context context,
CredentialProviderInfo info,
ProviderInternalCallback<GetCredentialResponse> callbacks,
int userId, RemoteCredentialService remoteCredentialService,
BeginGetCredentialRequest beginGetRequest,
android.credentials.GetCredentialRequest completeGetRequest,
CallingAppInfo callingAppInfo,
Map<String, CredentialOption> beginGetOptionToCredentialOptionMap,
String hybridService) {
super(context, beginGetRequest, callbacks, info.getComponentName(),
userId, remoteCredentialService);
mCompleteRequest = completeGetRequest;
mCallingAppInfo = callingAppInfo;
setStatus(Status.PENDING);
mBeginGetOptionToCredentialOptionMap = new HashMap<>(beginGetOptionToCredentialOptionMap);
mProviderResponseDataHandler = new ProviderResponseDataHandler(
ComponentName.unflattenFromString(hybridService));
}
/** Called when the provider response has been updated by an external source. */
@Override // Callback from the remote provider
public void onProviderResponseSuccess(@Nullable BeginGetCredentialResponse response) {
Slog.i(TAG, "Remote provider responded with a valid response: " + mComponentName);
onSetInitialRemoteResponse(response);
}
/** Called when the provider response resulted in a failure. */
@Override // Callback from the remote provider
public void onProviderResponseFailure(int errorCode, Exception exception) {
if (exception instanceof GetCredentialException) {
mProviderException = (GetCredentialException) exception;
// TODO(b/271135048) : Decide on exception type length
mProviderSessionMetric.collectCandidateFrameworkException(mProviderException.getType());
}
mProviderSessionMetric.collectCandidateExceptionStatus(/*hasException=*/true);
updateStatusAndInvokeCallback(Status.CANCELED,
/*source=*/ CredentialsSource.REMOTE_PROVIDER);
}
/** Called when provider service dies. */
@Override // Callback from the remote provider
public void onProviderServiceDied(RemoteCredentialService service) {
if (service.getComponentName().equals(mComponentName)) {
updateStatusAndInvokeCallback(Status.SERVICE_DEAD,
/*source=*/ CredentialsSource.REMOTE_PROVIDER);
} else {
Slog.w(TAG, "Component names different in onProviderServiceDied - "
+ "this should not happen");
}
}
@Override
public void onProviderCancellable(ICancellationSignal cancellation) {
mProviderCancellationSignal = cancellation;
}
@Override // Selection call from the request provider
protected void onUiEntrySelected(String entryType, String entryKey,
ProviderPendingIntentResponse providerPendingIntentResponse) {
Slog.i(TAG, "onUiEntrySelected with entryType: " + entryType + ", and entryKey: "
+ entryKey);
switch (entryType) {
case CREDENTIAL_ENTRY_KEY:
CredentialEntry credentialEntry = mProviderResponseDataHandler
.getCredentialEntry(entryKey);
if (credentialEntry == null) {
Slog.i(TAG, "Unexpected credential entry key");
invokeCallbackOnInternalInvalidState();
return;
}
onCredentialEntrySelected(providerPendingIntentResponse);
break;
case ACTION_ENTRY_KEY:
Action actionEntry = mProviderResponseDataHandler.getActionEntry(entryKey);
if (actionEntry == null) {
Slog.i(TAG, "Unexpected action entry key");
invokeCallbackOnInternalInvalidState();
return;
}
onActionEntrySelected(providerPendingIntentResponse);
break;
case AUTHENTICATION_ACTION_ENTRY_KEY:
Action authenticationEntry = mProviderResponseDataHandler
.getAuthenticationAction(entryKey);
mProviderSessionMetric.createAuthenticationBrowsingMetric();
if (authenticationEntry == null) {
Slog.i(TAG, "Unexpected authenticationEntry key");
invokeCallbackOnInternalInvalidState();
return;
}
boolean additionalContentReceived =
onAuthenticationEntrySelected(providerPendingIntentResponse);
if (additionalContentReceived) {
Slog.i(TAG, "Additional content received - removing authentication entry");
mProviderResponseDataHandler.removeAuthenticationAction(entryKey);
if (!mProviderResponseDataHandler.isEmptyResponse()) {
updateStatusAndInvokeCallback(Status.CREDENTIALS_RECEIVED,
/*source=*/ CredentialsSource.AUTH_ENTRY);
}
} else {
Slog.i(TAG, "Additional content not received from authentication entry");
mProviderResponseDataHandler
.updateAuthEntryWithNoCredentialsReceived(entryKey);
updateStatusAndInvokeCallback(Status.NO_CREDENTIALS_FROM_AUTH_ENTRY,
/*source=*/ CredentialsSource.AUTH_ENTRY);
}
break;
case REMOTE_ENTRY_KEY:
if (mProviderResponseDataHandler.getRemoteEntry(entryKey) != null) {
onRemoteEntrySelected(providerPendingIntentResponse);
} else {
Slog.i(TAG, "Unexpected remote entry key");
invokeCallbackOnInternalInvalidState();
}
break;
default:
Slog.i(TAG, "Unsupported entry type selected");
invokeCallbackOnInternalInvalidState();
}
}
@Override
protected void invokeSession() {
if (mRemoteCredentialService != null) {
startCandidateMetrics();
mRemoteCredentialService.setCallback(this);
mRemoteCredentialService.onBeginGetCredential(mProviderRequest);
}
}
@NonNull
protected Set<String> getCredentialEntryTypes() {
return mProviderResponseDataHandler.getCredentialEntryTypes();
}
@Override // Call from request session to data to be shown on the UI
@Nullable
protected GetCredentialProviderData prepareUiData() throws IllegalArgumentException {
if (!ProviderSession.isUiInvokingStatus(getStatus())) {
Slog.i(TAG, "No data for UI from: " + mComponentName.flattenToString());
return null;
}
if (mProviderResponse != null && !mProviderResponseDataHandler.isEmptyResponse()) {
return mProviderResponseDataHandler.toGetCredentialProviderData();
}
return null;
}
private Intent setUpFillInIntentWithFinalRequest(@NonNull String id) {
// TODO: Determine if we should skip this entry if entry id is not set, or is set
// but does not resolve to a valid option. For now, not skipping it because
// it may be possible that the provider adds their own extras and expects to receive
// those and complete the flow.
if (mBeginGetOptionToCredentialOptionMap.get(id) == null) {
Slog.w(TAG, "Id from Credential Entry does not resolve to a valid option");
return new Intent();
}
return new Intent().putExtra(CredentialProviderService.EXTRA_GET_CREDENTIAL_REQUEST,
new GetCredentialRequest(
mCallingAppInfo, List.of(mBeginGetOptionToCredentialOptionMap.get(id))));
}
private Intent setUpFillInIntentWithQueryRequest() {
Intent intent = new Intent();
intent.putExtra(CredentialProviderService.EXTRA_BEGIN_GET_CREDENTIAL_REQUEST,
mProviderRequest);
return intent;
}
private void onRemoteEntrySelected(
ProviderPendingIntentResponse providerPendingIntentResponse) {
onCredentialEntrySelected(providerPendingIntentResponse);
}
private void onCredentialEntrySelected(
ProviderPendingIntentResponse providerPendingIntentResponse) {
if (providerPendingIntentResponse == null) {
invokeCallbackOnInternalInvalidState();
return;
}
// Check if pending intent has an error
GetCredentialException exception = maybeGetPendingIntentException(
providerPendingIntentResponse);
if (exception != null) {
invokeCallbackWithError(exception.getType(), exception.getMessage());
return;
}
// Check if pending intent has a credential response
GetCredentialResponse getCredentialResponse = PendingIntentResultHandler
.extractGetCredentialResponse(
providerPendingIntentResponse.getResultData());
if (getCredentialResponse != null) {
mCallbacks.onFinalResponseReceived(mComponentName,
getCredentialResponse);
return;
}
Slog.i(TAG, "Pending intent response contains no credential, or error "
+ "for a credential entry");
invokeCallbackOnInternalInvalidState();
}
@Nullable
private GetCredentialException maybeGetPendingIntentException(
ProviderPendingIntentResponse pendingIntentResponse) {
if (pendingIntentResponse == null) {
return null;
}
if (PendingIntentResultHandler.isValidResponse(pendingIntentResponse)) {
GetCredentialException exception = PendingIntentResultHandler
.extractGetCredentialException(pendingIntentResponse.getResultData());
if (exception != null) {
return exception;
}
} else if (PendingIntentResultHandler.isCancelledResponse(pendingIntentResponse)) {
return new GetCredentialException(GetCredentialException.TYPE_USER_CANCELED);
} else {
return new GetCredentialException(GetCredentialException.TYPE_NO_CREDENTIAL);
}
return null;
}
/**
* Returns true if either an exception or a response is retrieved from the result.
* Returns false if the response is not set at all, or set to null, or empty.
*/
private boolean onAuthenticationEntrySelected(
@Nullable ProviderPendingIntentResponse providerPendingIntentResponse) {
// Authentication entry is expected to have a BeginGetCredentialResponse instance. If it
// does not have it, we remove the authentication entry and do not add any more content.
if (providerPendingIntentResponse == null) {
// Nothing received. This is equivalent to no content received.
return false;
}
GetCredentialException exception = maybeGetPendingIntentException(
providerPendingIntentResponse);
if (exception != null) {
// TODO (b/271135048), for AuthenticationEntry callback selection, set error
mProviderSessionMetric.collectAuthenticationExceptionStatus(/*hasException*/true);
invokeCallbackWithError(exception.getType(),
exception.getMessage());
// Additional content received is in the form of an exception which ends the flow.
return true;
}
// Check if pending intent has the response. If yes, remove this auth entry and
// replace it with the response content received.
BeginGetCredentialResponse response = PendingIntentResultHandler
.extractResponseContent(providerPendingIntentResponse
.getResultData());
mProviderSessionMetric.collectCandidateEntryMetrics(response, /*isAuthEntry*/true);
if (response != null && !mProviderResponseDataHandler.isEmptyResponse(response)) {
addToInitialRemoteResponse(response, /*isInitialResponse=*/ false);
// Additional content received is in the form of new response content.
return true;
}
// No response or exception found.
return false;
}
private void addToInitialRemoteResponse(BeginGetCredentialResponse content,
boolean isInitialResponse) {
if (content == null) {
return;
}
mProviderResponseDataHandler.addResponseContent(
content.getCredentialEntries(),
content.getActions(),
content.getAuthenticationActions(),
content.getRemoteCredentialEntry(),
isInitialResponse
);
}
/** Returns true if either an exception or a response is found. */
private void onActionEntrySelected(ProviderPendingIntentResponse
providerPendingIntentResponse) {
Slog.i(TAG, "onActionEntrySelected");
onCredentialEntrySelected(providerPendingIntentResponse);
}
/** Updates the response being maintained in state by this provider session. */
private void onSetInitialRemoteResponse(BeginGetCredentialResponse response) {
mProviderResponse = response;
addToInitialRemoteResponse(response, /*isInitialResponse=*/true);
// Log the data.
if (mProviderResponseDataHandler.isEmptyResponse(response)) {
mProviderSessionMetric.collectCandidateEntryMetrics(response, /*isAuthEntry*/false);
updateStatusAndInvokeCallback(Status.EMPTY_RESPONSE,
/*source=*/ CredentialsSource.REMOTE_PROVIDER);
return;
}
mProviderSessionMetric.collectCandidateEntryMetrics(response, /*isAuthEntry*/false);
updateStatusAndInvokeCallback(Status.CREDENTIALS_RECEIVED,
/*source=*/ CredentialsSource.REMOTE_PROVIDER);
}
/**
* When an invalid state occurs, e.g. entry mismatch or no response from provider,
* we send back a TYPE_NO_CREDENTIAL error as to the developer.
*/
private void invokeCallbackOnInternalInvalidState() {
mCallbacks.onFinalErrorReceived(mComponentName,
GetCredentialException.TYPE_NO_CREDENTIAL, null);
}
/** Update auth entries status based on an auth entry selected from a different session. */
public void updateAuthEntriesStatusFromAnotherSession() {
// Pass null for entryKey if the auth entry selected belongs to a different session
mProviderResponseDataHandler.updateAuthEntryWithNoCredentialsReceived(/*entryKey=*/null);
}
/** Returns true if the provider response contains empty auth entries only, false otherwise. **/
public boolean containsEmptyAuthEntriesOnly() {
// We do not consider action entries here because if actions are the only entries,
// we don't show the UI
return mProviderResponseDataHandler.mUiCredentialEntries.isEmpty()
&& mProviderResponseDataHandler.mUiRemoteEntry == null
&& mProviderResponseDataHandler.mUiAuthenticationEntries
.values().stream().allMatch(
e -> e.second.getStatus() == AuthenticationEntry
.STATUS_UNLOCKED_BUT_EMPTY_LESS_RECENT
|| e.second.getStatus()
== AuthenticationEntry.STATUS_UNLOCKED_BUT_EMPTY_MOST_RECENT
);
}
private class ProviderResponseDataHandler {
@Nullable
private final ComponentName mExpectedRemoteEntryProviderService;
@NonNull
private final Map<String, Pair<CredentialEntry, Entry>> mUiCredentialEntries =
new HashMap<>();
@NonNull
private final Map<String, Pair<Action, Entry>> mUiActionsEntries = new HashMap<>();
@Nullable
private final Map<String, Pair<Action, AuthenticationEntry>> mUiAuthenticationEntries =
new HashMap<>();
@NonNull
private final Set<String> mCredentialEntryTypes = new HashSet<>();
@Nullable
private Pair<String, Pair<RemoteEntry, Entry>> mUiRemoteEntry = null;
ProviderResponseDataHandler(@Nullable ComponentName expectedRemoteEntryProviderService) {
mExpectedRemoteEntryProviderService = expectedRemoteEntryProviderService;
}
public void addResponseContent(List<CredentialEntry> credentialEntries,
List<Action> actions, List<Action> authenticationActions,
RemoteEntry remoteEntry, boolean isInitialResponse) {
credentialEntries.forEach(this::addCredentialEntry);
actions.forEach(this::addAction);
authenticationActions.forEach(
authenticationAction -> addAuthenticationAction(authenticationAction,
AuthenticationEntry.STATUS_LOCKED));
// In the query phase, it is likely most providers will return a null remote entry
// so no need to invoke the setter since it adds the overhead of checking for the
// hybrid permission, and then sets an already null value to null.
// If this is not the query phase, e.g. response after a locked entry is unlocked
// then it is valid for the provider to remove the remote entry, and so we allow
// them to set it to null.
if (remoteEntry != null || !isInitialResponse) {
setRemoteEntry(remoteEntry);
}
}
public void addCredentialEntry(CredentialEntry credentialEntry) {
String id = generateUniqueId();
Entry entry = new Entry(CREDENTIAL_ENTRY_KEY,
id, credentialEntry.getSlice(),
setUpFillInIntentWithFinalRequest(credentialEntry
.getBeginGetCredentialOptionId()));
mUiCredentialEntries.put(id, new Pair<>(credentialEntry, entry));
mCredentialEntryTypes.add(credentialEntry.getType());
}
public void addAction(Action action) {
String id = generateUniqueId();
Entry entry = new Entry(ACTION_ENTRY_KEY,
id, action.getSlice(),
setUpFillInIntentWithQueryRequest());
mUiActionsEntries.put(id, new Pair<>(action, entry));
}
public void addAuthenticationAction(Action authenticationAction,
@AuthenticationEntry.Status int status) {
String id = generateUniqueId();
AuthenticationEntry entry = new AuthenticationEntry(
AUTHENTICATION_ACTION_ENTRY_KEY,
id, authenticationAction.getSlice(),
status,
setUpFillInIntentWithQueryRequest());
mUiAuthenticationEntries.put(id, new Pair<>(authenticationAction, entry));
}
public void removeAuthenticationAction(String id) {
mUiAuthenticationEntries.remove(id);
}
public void setRemoteEntry(@Nullable RemoteEntry remoteEntry) {
if (!enforceRemoteEntryRestrictions(mExpectedRemoteEntryProviderService)) {
Slog.w(TAG, "Remote entry being dropped as it does not meet the restriction"
+ " checks.");
return;
}
if (remoteEntry == null) {
mUiRemoteEntry = null;
return;
}
String id = generateUniqueId();
Entry entry = new Entry(REMOTE_ENTRY_KEY,
id, remoteEntry.getSlice(), setUpFillInIntentForRemoteEntry());
mUiRemoteEntry = new Pair<>(id, new Pair<>(remoteEntry, entry));
}
public GetCredentialProviderData toGetCredentialProviderData() {
return new GetCredentialProviderData.Builder(
mComponentName.flattenToString()).setActionChips(prepareActionEntries())
.setCredentialEntries(prepareCredentialEntries())
.setAuthenticationEntries(prepareAuthenticationEntries())
.setRemoteEntry(prepareRemoteEntry())
.build();
}
private List<Entry> prepareActionEntries() {
List<Entry> actionEntries = new ArrayList<>();
for (String key : mUiActionsEntries.keySet()) {
actionEntries.add(mUiActionsEntries.get(key).second);
}
return actionEntries;
}
private List<AuthenticationEntry> prepareAuthenticationEntries() {
List<AuthenticationEntry> authEntries = new ArrayList<>();
for (String key : mUiAuthenticationEntries.keySet()) {
authEntries.add(mUiAuthenticationEntries.get(key).second);
}
return authEntries;
}
private List<Entry> prepareCredentialEntries() {
List<Entry> credEntries = new ArrayList<>();
for (String key : mUiCredentialEntries.keySet()) {
credEntries.add(mUiCredentialEntries.get(key).second);
}
return credEntries;
}
private Entry prepareRemoteEntry() {
if (mUiRemoteEntry == null || mUiRemoteEntry.first == null
|| mUiRemoteEntry.second == null) {
return null;
}
return mUiRemoteEntry.second.second;
}
private boolean isEmptyResponse() {
return mUiCredentialEntries.isEmpty() && mUiActionsEntries.isEmpty()
&& mUiAuthenticationEntries.isEmpty() && mUiRemoteEntry == null;
}
private boolean isEmptyResponse(BeginGetCredentialResponse response) {
return response.getCredentialEntries().isEmpty() && response.getActions().isEmpty()
&& response.getAuthenticationActions().isEmpty()
&& response.getRemoteCredentialEntry() == null;
}
@NonNull
public Set<String> getCredentialEntryTypes() {
return mCredentialEntryTypes;
}
@Nullable
public Action getAuthenticationAction(String entryKey) {
return mUiAuthenticationEntries.get(entryKey) == null ? null :
mUiAuthenticationEntries.get(entryKey).first;
}
@Nullable
public Action getActionEntry(String entryKey) {
return mUiActionsEntries.get(entryKey) == null
? null : mUiActionsEntries.get(entryKey).first;
}
@Nullable
public RemoteEntry getRemoteEntry(String entryKey) {
return mUiRemoteEntry.first.equals(entryKey) && mUiRemoteEntry.second != null
? mUiRemoteEntry.second.first : null;
}
@Nullable
public CredentialEntry getCredentialEntry(String entryKey) {
return mUiCredentialEntries.get(entryKey) == null
? null : mUiCredentialEntries.get(entryKey).first;
}
public void updateAuthEntryWithNoCredentialsReceived(@Nullable String entryKey) {
if (entryKey == null) {
// Auth entry from a different provider was selected by the user.
updatePreviousMostRecentAuthEntry();
return;
}
updatePreviousMostRecentAuthEntry();
updateMostRecentAuthEntry(entryKey);
}
private void updateMostRecentAuthEntry(String entryKey) {
AuthenticationEntry previousAuthenticationEntry =
mUiAuthenticationEntries.get(entryKey).second;
Action previousAuthenticationAction = mUiAuthenticationEntries.get(entryKey).first;
mUiAuthenticationEntries.put(entryKey, new Pair<>(
previousAuthenticationAction,
copyAuthEntryAndChangeStatus(
previousAuthenticationEntry,
AuthenticationEntry.STATUS_UNLOCKED_BUT_EMPTY_MOST_RECENT)));
}
private void updatePreviousMostRecentAuthEntry() {
Optional<Map.Entry<String, Pair<Action, AuthenticationEntry>>>
previousMostRecentAuthEntry = mUiAuthenticationEntries
.entrySet().stream().filter(e -> e.getValue().second.getStatus()
== AuthenticationEntry.STATUS_UNLOCKED_BUT_EMPTY_MOST_RECENT)
.findFirst();
if (previousMostRecentAuthEntry.isEmpty()) {
return;
}
String id = previousMostRecentAuthEntry.get().getKey();
mUiAuthenticationEntries.remove(id);
mUiAuthenticationEntries.put(id, new Pair<>(
previousMostRecentAuthEntry.get().getValue().first,
copyAuthEntryAndChangeStatus(
previousMostRecentAuthEntry.get().getValue().second,
AuthenticationEntry.STATUS_UNLOCKED_BUT_EMPTY_LESS_RECENT)));
}
private AuthenticationEntry copyAuthEntryAndChangeStatus(
AuthenticationEntry from, Integer toStatus) {
return new AuthenticationEntry(AUTHENTICATION_ACTION_ENTRY_KEY, from.getSubkey(),
from.getSlice(), toStatus,
from.getFrameworkExtrasIntent());
}
}
private Intent setUpFillInIntentForRemoteEntry() {
return new Intent().putExtra(CredentialProviderService.EXTRA_GET_CREDENTIAL_REQUEST,
new GetCredentialRequest(
mCallingAppInfo, mCompleteRequest.getCredentialOptions()));
}
}