Merge "Basic create flow for credential manager up until pending intent is invoked Test: Built locally & deployed Bug: 253155340"

This commit is contained in:
Reema Bajwa
2022-11-14 17:36:20 +00:00
committed by Android (Google) Code Review
15 changed files with 1023 additions and 336 deletions

View File

@@ -173,7 +173,7 @@ public final class CredentialEntry implements Parcelable {
*/
public @NonNull Builder setPendingIntent(@Nullable PendingIntent pendingIntent) {
if (pendingIntent != null) {
Preconditions.checkState(mCredential != null,
Preconditions.checkState(mCredential == null,
"credential is already set. Cannot set both the pendingIntent "
+ "and the credential");
}
@@ -189,7 +189,7 @@ public final class CredentialEntry implements Parcelable {
*/
public @NonNull Builder setCredential(@Nullable Credential credential) {
if (credential != null) {
Preconditions.checkState(mPendingIntent != null,
Preconditions.checkState(mPendingIntent == null,
"pendingIntent is already set. Cannot set both the "
+ "pendingIntent and the credential");
}
@@ -215,10 +215,10 @@ public final class CredentialEntry implements Parcelable {
* is set, or if both are set.
*/
public @NonNull CredentialEntry build() {
Preconditions.checkState(mPendingIntent == null && mCredential == null,
"Either pendingIntent or credential must be set");
Preconditions.checkState(mPendingIntent != null && mCredential != null,
"Cannot set both the pendingIntent and credential");
Preconditions.checkState(((mPendingIntent != null && mCredential == null)
|| (mPendingIntent == null && mCredential != null)),
"Either pendingIntent or credential must be set, and both cannot"
+ "be set at the same time");
return new CredentialEntry(mType, mSlice, mPendingIntent,
mCredential, mAutoSelectAllowed);
}

View File

@@ -24,7 +24,6 @@ import android.app.AppGlobals;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageItemInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
@@ -96,6 +95,8 @@ public final class CredentialProviderInfo {
mLabel = mServiceInfo.loadSafeLabel(
mContext.getPackageManager(), 0 /* do not ellipsize */,
TextUtils.SAFE_STRING_FLAG_FIRST_LINE | TextUtils.SAFE_STRING_FLAG_TRIM);
Log.i(TAG, "mLabel is : " + mLabel + ", for: " + mServiceInfo.getComponentName()
.flattenToString());
populateProviderCapabilities(context, serviceInfo);
}

View File

@@ -41,6 +41,18 @@ import java.util.Objects;
* @hide
*/
public abstract class CredentialProviderService extends Service {
/** Extra to be used by provider to populate the credential when ending the activity started
* through the {@code pendingIntent} on the selected {@link SaveEntry}. **/
public static final String EXTRA_SAVE_CREDENTIAL =
"android.service.credentials.extra.SAVE_CREDENTIAL";
/**
* Provider must read the value against this extra to receive the complete create credential
* request parameters, when a pending intent is launched.
*/
public static final String EXTRA_CREATE_CREDENTIAL_REQUEST_PARAMS =
"android.service.credentials.extra.CREATE_CREDENTIAL_REQUEST_PARAMS";
private static final String TAG = "CredProviderService";
public static final String CAPABILITY_META_DATA_KEY = "android.credentials.capabilities";
@@ -64,7 +76,7 @@ public abstract class CredentialProviderService extends Service {
}
@Override
public final @NonNull IBinder onBind(@NonNull Intent intent) {
@NonNull public final IBinder onBind(@NonNull Intent intent) {
if (SERVICE_INTERFACE.equals(intent.getAction())) {
return mInterface.asBinder();
}

View File

@@ -119,9 +119,9 @@ public final class GetCredentialsRequest implements Parcelable {
*/
public @NonNull Builder setGetCredentialOptions(
@NonNull List<GetCredentialOption> getCredentialOptions) {
Preconditions.checkCollectionNotEmpty(mGetCredentialOptions,
Preconditions.checkCollectionNotEmpty(getCredentialOptions,
"getCredentialOptions");
Preconditions.checkCollectionElementsNotNull(mGetCredentialOptions,
Preconditions.checkCollectionElementsNotNull(getCredentialOptions,
"getCredentialOptions");
mGetCredentialOptions = getCredentialOptions;
return this;

View File

@@ -0,0 +1,75 @@
/*
* 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.content.Context;
import android.credentials.CreateCredentialRequest;
import android.credentials.CredentialManager;
import android.credentials.ICreateCredentialCallback;
import android.credentials.ui.ProviderData;
import android.credentials.ui.RequestInfo;
import android.service.credentials.CredentialProviderInfo;
import android.util.Log;
import java.util.ArrayList;
/**
* Central session for a single {@link CredentialManager#executeCreateCredential} request.
* This class listens to the responses from providers, and the UX app, and updates the
* provider(s) state maintained in {@link ProviderCreateSession}.
*/
public final class CreateRequestSession extends RequestSession<CreateCredentialRequest,
ICreateCredentialCallback> {
private static final String TAG = "CreateRequestSession";
CreateRequestSession(@NonNull Context context, int userId,
CreateCredentialRequest request,
ICreateCredentialCallback callback,
String callingPackage) {
super(context, userId, request, callback, RequestInfo.TYPE_CREATE, callingPackage);
}
/**
* Creates a new provider session, and adds it to list of providers that are contributing to
* this request session.
*
* @return the provider session that was started
*/
@Override
@Nullable
public ProviderSession initiateProviderSession(CredentialProviderInfo providerInfo,
RemoteCredentialService remoteCredentialService) {
ProviderCreateSession providerCreateSession = ProviderCreateSession
.createNewSession(mContext, mUserId, providerInfo,
this, remoteCredentialService);
if (providerCreateSession != null) {
Log.i(TAG, "In startProviderSession - provider session created and being added");
mProviders.put(providerCreateSession.getComponentName().flattenToString(),
providerCreateSession);
}
return providerCreateSession;
}
@Override
protected void launchUiWithProviderData(ArrayList<ProviderData> providerDataList) {
mHandler.post(() -> mCredentialManagerUi.show(RequestInfo.newCreateRequestInfo(
mRequestId, mClientRequest, mIsFirstUiTurn, mClientCallingPackage),
providerDataList));
}
}

View File

@@ -23,6 +23,7 @@ import android.annotation.UserIdInt;
import android.content.Context;
import android.content.pm.PackageManager;
import android.credentials.CreateCredentialRequest;
import android.credentials.GetCredentialOption;
import android.credentials.GetCredentialRequest;
import android.credentials.IClearCredentialSessionCallback;
import android.credentials.ICreateCredentialCallback;
@@ -33,6 +34,7 @@ import android.os.CancellationSignal;
import android.os.ICancellationSignal;
import android.os.UserHandle;
import android.provider.Settings;
import android.service.credentials.GetCredentialsRequest;
import android.text.TextUtils;
import android.util.Log;
import android.util.Slog;
@@ -43,6 +45,7 @@ import com.android.server.infra.SecureSettingsServiceNameResolver;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import java.util.stream.Collectors;
/**
* Entry point service for credential management.
@@ -91,17 +94,15 @@ public final class CredentialManagerService extends
return new ArrayList<>();
}
List<CredentialManagerServiceImpl> serviceList = new ArrayList<>(serviceNames.length);
for (int i = 0; i < serviceNames.length; i++) {
Log.i(TAG, "in newServiceListLocked, service: " + serviceNames[i]);
if (TextUtils.isEmpty(serviceNames[i])) {
for (String serviceName : serviceNames) {
Log.i(TAG, "in newServiceListLocked, service: " + serviceName);
if (TextUtils.isEmpty(serviceName)) {
continue;
}
try {
serviceList.add(new CredentialManagerServiceImpl(this, mLock, resolvedUserId,
serviceNames[i]));
} catch (PackageManager.NameNotFoundException e) {
Log.i(TAG, "Unable to add serviceInfo : " + e.getMessage());
} catch (SecurityException e) {
serviceName));
} catch (PackageManager.NameNotFoundException | SecurityException e) {
Log.i(TAG, "Unable to add serviceInfo : " + e.getMessage());
}
}
@@ -115,15 +116,31 @@ public final class CredentialManagerService extends
synchronized (mLock) {
final List<CredentialManagerServiceImpl> services =
getServiceListForUserLocked(userId);
services.forEach(s -> {
for (CredentialManagerServiceImpl s : services) {
c.accept(s);
});
}
}
} finally {
Binder.restoreCallingIdentity(origId);
}
}
private List<ProviderSession> initiateProviderSessions(RequestSession session,
List<String> requestOptions) {
List<ProviderSession> providerSessions = new ArrayList<>();
// Invoke all services of a user to initiate a provider session
runForUser((service) -> {
if (service.isServiceCapable(requestOptions)) {
ProviderSession providerSession = service
.initiateProviderSessionForRequest(session);
if (providerSession != null) {
providerSessions.add(providerSession);
}
}
});
return providerSessions;
}
final class CredentialManagerServiceStub extends ICredentialManager.Stub {
@Override
public ICancellationSignal executeGetCredential(
@@ -137,11 +154,22 @@ public final class CredentialManagerService extends
// New request session, scoped for this request only.
final GetRequestSession session = new GetRequestSession(getContext(),
UserHandle.getCallingUserId(),
callback);
callback,
request,
callingPackage);
// Invoke all services of a user
runForUser((service) -> {
service.getCredential(request, session, callingPackage);
// Initiate all provider sessions
List<ProviderSession> providerSessions =
initiateProviderSessions(session, request.getGetCredentialOptions()
.stream().map(GetCredentialOption::getType)
.collect(Collectors.toList()));
// TODO : Return error when no providers available
// Iterate over all provider sessions and invoke the request
providerSessions.forEach(providerGetSession -> {
providerGetSession.getRemoteCredentialService().onGetCredentials(
(GetCredentialsRequest) providerGetSession.getProviderRequest(),
/*callback=*/providerGetSession);
});
return cancelTransport;
}
@@ -151,9 +179,29 @@ public final class CredentialManagerService extends
CreateCredentialRequest request,
ICreateCredentialCallback callback,
String callingPackage) {
// TODO: implement.
Log.i(TAG, "executeCreateCredential");
Log.i(TAG, "starting executeCreateCredential with callingPackage: " + callingPackage);
// TODO : Implement cancellation
ICancellationSignal cancelTransport = CancellationSignal.createTransport();
// New request session, scoped for this request only.
final CreateRequestSession session = new CreateRequestSession(getContext(),
UserHandle.getCallingUserId(),
request,
callback,
callingPackage);
// Initiate all provider sessions
List<ProviderSession> providerSessions =
initiateProviderSessions(session, List.of(request.getType()));
// TODO : Return error when no providers available
// Iterate over all provider sessions and invoke the request
providerSessions.forEach(providerCreateSession -> {
providerCreateSession.getRemoteCredentialService().onCreateCredential(
(android.service.credentials.CreateCredentialRequest)
providerCreateSession.getProviderRequest(),
/*callback=*/providerCreateSession);
});
return cancelTransport;
}

View File

@@ -21,13 +21,13 @@ import android.annotation.Nullable;
import android.content.ComponentName;
import android.content.pm.PackageManager;
import android.content.pm.ServiceInfo;
import android.credentials.GetCredentialRequest;
import android.service.credentials.CredentialProviderInfo;
import android.service.credentials.GetCredentialsRequest;
import android.util.Slog;
import com.android.server.infra.AbstractPerUserSystemService;
import java.util.List;
/**
* Per-user, per remote service implementation of {@link CredentialManagerService}
@@ -61,50 +61,38 @@ public final class CredentialManagerServiceImpl extends
return mInfo.getServiceInfo();
}
public void getCredential(GetCredentialRequest request, GetRequestSession requestSession,
String callingPackage) {
Slog.i(TAG, "in getCredential in CredManServiceImpl");
/**
* Starts a provider session and associates it with the given request session. */
@Nullable
public ProviderSession initiateProviderSessionForRequest(
RequestSession requestSession) {
Slog.i(TAG, "in initiateProviderSessionForRequest in CredManServiceImpl");
if (mInfo == null) {
Slog.i(TAG, "in getCredential in CredManServiceImpl, but mInfo is null");
return;
Slog.i(TAG, "in initiateProviderSessionForRequest in CredManServiceImpl, "
+ "but mInfo is null. This shouldn't happen");
return null;
}
// TODO : Determine if remoteService instance can be reused across requests
final RemoteCredentialService remoteService = new RemoteCredentialService(
getContext(), mInfo.getServiceInfo().getComponentName(), mUserId);
ProviderGetSession providerSession = new ProviderGetSession(mInfo,
requestSession, mUserId, remoteService);
// Set the provider info to the session when the request is initiated. This happens here
// because there is one serviceImpl per remote provider, and so we can only retrieve
// the provider information in the scope of this instance, whereas the session is for the
// entire request.
requestSession.addProviderSession(providerSession);
GetCredentialsRequest filteredRequest = getRequestWithValidType(request, callingPackage);
if (filteredRequest != null) {
remoteService.onGetCredentials(getRequestWithValidType(request, callingPackage),
providerSession);
}
ProviderSession providerSession =
requestSession.initiateProviderSession(mInfo, remoteService);
return providerSession;
}
@Nullable
private GetCredentialsRequest getRequestWithValidType(GetCredentialRequest request,
String callingPackage) {
GetCredentialsRequest.Builder builder =
new GetCredentialsRequest.Builder(callingPackage);
request.getGetCredentialOptions().forEach( option -> {
if (mInfo.hasCapability(option.getType())) {
Slog.i(TAG, "Provider can handle: " + option.getType());
builder.addGetCredentialOption(option);
} else {
Slog.i(TAG, "Skipping request as provider cannot handle it");
}
});
try {
return builder.build();
} catch (IllegalArgumentException | NullPointerException e) {
Slog.i(TAG, "issue with request build: " + e.getMessage());
/** Return true if at least one capability found. */
boolean isServiceCapable(List<String> requestedOptions) {
if (mInfo == null) {
Slog.i(TAG, "in isServiceCapable, mInfo is null");
return false;
}
return null;
for (String capability : requestedOptions) {
if (mInfo.hasCapability(capability)) {
Slog.i(TAG, "Provider can handle: " + capability);
return true;
} else {
Slog.i(TAG, "Provider cannot handle: " + capability);
}
}
return false;
}
}

View File

@@ -37,6 +37,7 @@ public class CredentialManagerUi {
@NonNull
private final CredentialManagerUiCallback mCallbacks;
@NonNull private final Context mContext;
// TODO : Use for starting the activity for this user
private final int mUserId;
@NonNull private final ResultReceiver mResultReceiver = new ResultReceiver(
new Handler(Looper.getMainLooper())) {
@@ -56,7 +57,7 @@ public class CredentialManagerUi {
Slog.i(TAG, "No selection found in UI result");
}
} else if (resultCode == UserSelectionDialogResult.RESULT_CODE_DIALOG_CANCELED) {
mCallbacks.onUiCancelation();
mCallbacks.onUiCancellation();
}
}
@@ -67,7 +68,7 @@ public class CredentialManagerUi {
/** Called when the user makes a selection. */
void onUiSelection(UserSelectionDialogResult selection);
/** Called when the user cancels the UI. */
void onUiCancelation();
void onUiCancellation();
}
public CredentialManagerUi(Context context, int userId,
CredentialManagerUiCallback callbacks) {
@@ -83,9 +84,8 @@ public class CredentialManagerUi {
*/
public void show(RequestInfo requestInfo, ArrayList<ProviderData> providerDataList) {
Log.i(TAG, "In show");
Intent intent = IntentFactory.newIntent(
requestInfo, providerDataList,
new ArrayList<>(), mResultReceiver);
Intent intent = IntentFactory.newIntent(requestInfo, providerDataList, new ArrayList<>(),
mResultReceiver);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
mContext.startActivity(intent);
}

View File

@@ -16,9 +16,10 @@
package com.android.server.credentials;
import android.content.ComponentName;
import android.annotation.Nullable;
import android.content.Context;
import android.credentials.Credential;
import android.credentials.GetCredentialRequest;
import android.credentials.GetCredentialResponse;
import android.credentials.IGetCredentialCallback;
import android.credentials.ui.ProviderData;
@@ -26,62 +27,52 @@ import android.credentials.ui.RequestInfo;
import android.credentials.ui.UserSelectionDialogResult;
import android.os.RemoteException;
import android.service.credentials.CredentialEntry;
import android.service.credentials.CredentialProviderInfo;
import android.util.Log;
import android.util.Slog;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Central session for a single getCredentials request. This class listens to the
* responses from providers, and the UX app, and updates the provider(S) state.
*/
public final class GetRequestSession extends RequestSession {
public final class GetRequestSession extends RequestSession<GetCredentialRequest,
IGetCredentialCallback> {
private static final String TAG = "GetRequestSession";
private final IGetCredentialCallback mClientCallback;
private final Map<String, ProviderGetSession> mProviders;
public GetRequestSession(Context context, int userId,
IGetCredentialCallback callback) {
super(context, userId, RequestInfo.TYPE_GET);
mClientCallback = callback;
mProviders = new HashMap<>();
IGetCredentialCallback callback, GetCredentialRequest request,
String callingPackage) {
super(context, userId, request, callback, RequestInfo.TYPE_GET, callingPackage);
}
/**
* Adds a new provider to the list of providers that are contributing to this session.
* Creates a new provider session, and adds it list of providers that are contributing to
* this session.
* @return the provider session created within this request session, for the given provider
* info.
*/
public void addProviderSession(ProviderGetSession providerSession) {
mProviders.put(providerSession.getComponentName().flattenToString(),
providerSession);
}
@Override
public void onProviderStatusChanged(ProviderSession.Status status,
ComponentName componentName) {
Log.i(TAG, "in onStatusChanged");
if (ProviderSession.isTerminatingStatus(status)) {
Log.i(TAG, "in onStatusChanged terminating status");
ProviderGetSession session = mProviders.remove(componentName.flattenToString());
if (session != null) {
Slog.i(TAG, "Provider session removed.");
} else {
Slog.i(TAG, "Provider session null, did not exist.");
}
} else if (ProviderSession.isCompletionStatus(status)) {
Log.i(TAG, "in onStatusChanged isCompletionStatus status");
onProviderResponseComplete();
@Nullable
public ProviderSession initiateProviderSession(CredentialProviderInfo providerInfo,
RemoteCredentialService remoteCredentialService) {
ProviderGetSession providerGetSession = ProviderGetSession
.createNewSession(mContext, mUserId, providerInfo,
this, remoteCredentialService);
if (providerGetSession != null) {
Log.i(TAG, "In startProviderSession - provider session created and being added");
mProviders.put(providerGetSession.getComponentName().flattenToString(),
providerGetSession);
}
return providerGetSession;
}
// TODO: Override for this method not needed once get selection logic is
// moved to ProviderGetSession
@Override
public void onUiSelection(UserSelectionDialogResult selection) {
String providerId = selection.getProviderId();
ProviderGetSession providerSession = mProviders.get(providerId);
ProviderGetSession providerSession = (ProviderGetSession) mProviders.get(providerId);
if (providerSession != null) {
CredentialEntry credentialEntry = providerSession.getCredentialEntry(
selection.getEntrySubkey());
@@ -89,57 +80,17 @@ public final class GetRequestSession extends RequestSession {
respondToClientAndFinish(credentialEntry.getCredential());
}
// TODO : Handle action chips and authentication selection
return;
}
// TODO : finish session and respond to client if provider not found
}
@Override
public void onUiCancelation() {
// User canceled the activity
// TODO : Send error code to client
finishSession();
}
private void onProviderResponseComplete() {
Log.i(TAG, "in onProviderResponseComplete");
if (isResponseCompleteAcrossProviders()) {
Log.i(TAG, "in onProviderResponseComplete - isResponseCompleteAcrossProviders");
getProviderDataAndInitiateUi();
}
}
private void getProviderDataAndInitiateUi() {
ArrayList<ProviderData> providerDataList = new ArrayList<>();
for (ProviderGetSession session : mProviders.values()) {
Log.i(TAG, "preparing data for : " + session.getComponentName());
providerDataList.add(session.prepareUiData());
}
if (!providerDataList.isEmpty()) {
Log.i(TAG, "provider list not empty about to initiate ui");
initiateUi(providerDataList);
}
}
private void initiateUi(ArrayList<ProviderData> providerDataList) {
protected void launchUiWithProviderData(ArrayList<ProviderData> providerDataList) {
mHandler.post(() -> mCredentialManagerUi.show(RequestInfo.newGetRequestInfo(
mRequestId, null, mIsFirstUiTurn, ""),
providerDataList));
}
/**
* Iterates over all provider sessions and returns true if all have responded.
*/
private boolean isResponseCompleteAcrossProviders() {
AtomicBoolean isRequestComplete = new AtomicBoolean(true);
mProviders.forEach( (packageName, session) -> {
if (session.getStatus() != ProviderSession.Status.COMPLETE) {
isRequestComplete.set(false);
}
});
return isRequestComplete.get();
}
private void respondToClientAndFinish(Credential credential) {
try {
mClientCallback.onResponse(new GetCredentialResponse(credential));
@@ -148,17 +99,4 @@ public final class GetRequestSession extends RequestSession {
}
finishSession();
}
private void finishSession() {
clearProviderSessions();
}
private void clearProviderSessions() {
for (ProviderGetSession session : mProviders.values()) {
// TODO : Evaluate if we should unbind remote services here or wait for them
// to automatically unbind when idle. Re-binding frequently also has a cost.
//session.destroy();
}
mProviders.clear();
}
}

View File

@@ -0,0 +1,231 @@
/*
* 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.Context;
import android.credentials.Credential;
import android.credentials.ui.CreateCredentialProviderData;
import android.credentials.ui.Entry;
import android.os.Bundle;
import android.service.credentials.CreateCredentialRequest;
import android.service.credentials.CreateCredentialResponse;
import android.service.credentials.CredentialProviderInfo;
import android.service.credentials.CredentialProviderService;
import android.service.credentials.SaveEntry;
import android.util.Log;
import android.util.Slog;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Central provider session that listens for provider callbacks, and maintains provider state.
* Will likely split this into remote response state and UI state.
*/
public final class ProviderCreateSession extends ProviderSession<
CreateCredentialRequest, CreateCredentialResponse> {
private static final String TAG = "ProviderCreateSession";
// Key to be used as an entry key for a save entry
private static final String SAVE_ENTRY_KEY = "save_entry_key";
@NonNull
private final Map<String, SaveEntry> mUiSaveEntries = new HashMap<>();
/** The complete request to be used in the second round. */
private final CreateCredentialRequest mCompleteRequest;
/** Creates a new provider session to be used by the request session. */
@Nullable public static ProviderCreateSession createNewSession(
Context context,
@UserIdInt int userId,
CredentialProviderInfo providerInfo,
CreateRequestSession createRequestSession,
RemoteCredentialService remoteCredentialService) {
CreateCredentialRequest providerRequest =
createProviderRequest(providerInfo.getCapabilities(),
createRequestSession.mClientRequest,
createRequestSession.mClientCallingPackage);
if (providerRequest != null) {
return new ProviderCreateSession(context, providerInfo, createRequestSession, userId,
remoteCredentialService, providerRequest);
}
Log.i(TAG, "Unable to create provider session");
return null;
}
@Nullable
private static CreateCredentialRequest createProviderRequest(List<String> providerCapabilities,
android.credentials.CreateCredentialRequest clientRequest,
String clientCallingPackage) {
String capability = clientRequest.getType();
if (providerCapabilities.contains(capability)) {
return new CreateCredentialRequest(clientCallingPackage, capability,
clientRequest.getData());
}
Log.i(TAG, "Unable to create provider request - capabilities do not match");
return null;
}
private static CreateCredentialRequest getFirstRoundRequest(CreateCredentialRequest request) {
// TODO: Replace with first round bundle from request when ready
return new CreateCredentialRequest(
request.getCallingPackage(),
request.getType(),
new Bundle());
}
private ProviderCreateSession(
@NonNull Context context,
@NonNull CredentialProviderInfo info,
@NonNull ProviderInternalCallback callbacks,
@UserIdInt int userId,
@NonNull RemoteCredentialService remoteCredentialService,
@NonNull CreateCredentialRequest request) {
super(context, info, getFirstRoundRequest(request), callbacks, userId,
remoteCredentialService);
// TODO : Replace with proper splitting of request
mCompleteRequest = request;
setStatus(Status.PENDING);
}
/** Returns the save entry maintained in state by this provider session. */
public SaveEntry getUiSaveEntry(String entryId) {
return mUiSaveEntries.get(entryId);
}
@Override
public void onProviderResponseSuccess(
@Nullable CreateCredentialResponse response) {
Log.i(TAG, "in onProviderResponseSuccess");
onUpdateResponse(response);
}
/** Called when the provider response resulted in a failure. */
@Override
public void onProviderResponseFailure(int errorCode, @Nullable CharSequence message) {
updateStatusAndInvokeCallback(toStatus(errorCode));
}
/** Called when provider service dies. */
@Override
public void onProviderServiceDied(RemoteCredentialService service) {
if (service.getComponentName().equals(mProviderInfo.getServiceInfo().getComponentName())) {
updateStatusAndInvokeCallback(Status.SERVICE_DEAD);
} else {
Slog.i(TAG, "Component names different in onProviderServiceDied - "
+ "this should not happen");
}
}
private void onUpdateResponse(CreateCredentialResponse response) {
Log.i(TAG, "updateResponse with save entries");
mProviderResponse = response;
updateStatusAndInvokeCallback(Status.SAVE_ENTRIES_RECEIVED);
}
@Override
@Nullable protected CreateCredentialProviderData prepareUiData()
throws IllegalArgumentException {
Log.i(TAG, "In prepareUiData");
if (!ProviderSession.isUiInvokingStatus(getStatus())) {
Log.i(TAG, "In prepareUiData not in uiInvokingStatus");
return null;
}
final CreateCredentialResponse response = getProviderResponse();
if (response == null) {
Log.i(TAG, "In prepareUiData response null");
throw new IllegalStateException("Response must be in completion mode");
}
if (response.getSaveEntries() != null) {
Log.i(TAG, "In prepareUiData save entries not null");
return prepareUiProviderData(
prepareUiSaveEntries(response.getSaveEntries()),
null,
/*isDefaultProvider=*/false);
}
return null;
}
@Override
public void onProviderIntentResult(Bundle resultData) {
Credential credential = resultData.getParcelable(
CredentialProviderService.EXTRA_SAVE_CREDENTIAL,
Credential.class);
if (credential == null) {
Log.i(TAG, "Credential returned from intent is null");
return;
}
updateFinalCredentialResponse(credential);
}
@Override
public void onUiEntrySelected(String entryType, String entryKey) {
if (entryType.equals(SAVE_ENTRY_KEY)) {
SaveEntry saveEntry = mUiSaveEntries.get(entryKey);
if (saveEntry == null) {
Log.i(TAG, "Save entry not found");
return;
}
// TODO: Uncomment when pending intent works
// onSaveEntrySelected(saveEntry);
}
}
@Override
public void onProviderIntentCancelled() {
//TODO (Implement)
}
private List<Entry> prepareUiSaveEntries(@NonNull List<SaveEntry> saveEntries) {
Log.i(TAG, "in populateUiSaveEntries");
List<Entry> uiSaveEntries = new ArrayList<>();
// Populate the save entries
for (SaveEntry saveEntry : saveEntries) {
String entryId = generateEntryId();
mUiSaveEntries.put(entryId, saveEntry);
Log.i(TAG, "in prepareUiProviderData creating ui entry with id " + entryId);
uiSaveEntries.add(new Entry(SAVE_ENTRY_KEY, entryId, saveEntry.getSlice()));
}
return uiSaveEntries;
}
private void updateFinalCredentialResponse(@NonNull Credential credential) {
mFinalCredentialResponse = credential;
updateStatusAndInvokeCallback(Status.CREDENTIAL_RECEIVED_FROM_INTENT);
}
private CreateCredentialProviderData prepareUiProviderData(List<Entry> saveEntries,
Entry remoteEntry, boolean isDefaultProvider) {
return new CreateCredentialProviderData.Builder(
mComponentName.flattenToString())
.setSaveEntries(saveEntries)
.setIsDefaultProvider(isDefaultProvider)
.build();
}
private void onSaveEntrySelected(SaveEntry saveEntry) {
mProviderIntentController.setupAndInvokePendingIntent(saveEntry.getPendingIntent(),
mProviderRequest);
setStatus(Status.PENDING_INTENT_INVOKED);
}
}

View File

@@ -18,13 +18,16 @@ package com.android.server.credentials;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.slice.Slice;
import android.annotation.UserIdInt;
import android.content.Context;
import android.credentials.GetCredentialOption;
import android.credentials.ui.Entry;
import android.credentials.ui.GetCredentialProviderData;
import android.os.Bundle;
import android.service.credentials.Action;
import android.service.credentials.CredentialEntry;
import android.service.credentials.CredentialProviderInfo;
import android.service.credentials.CredentialsDisplayContent;
import android.service.credentials.GetCredentialsRequest;
import android.service.credentials.GetCredentialsResponse;
import android.util.Log;
import android.util.Slog;
@@ -38,75 +41,95 @@ import java.util.UUID;
/**
* 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<GetCredentialsResponse>
implements RemoteCredentialService.ProviderCallbacks<GetCredentialsResponse> {
public final class ProviderGetSession extends ProviderSession<GetCredentialsRequest,
GetCredentialsResponse>
implements
RemoteCredentialService.ProviderCallbacks<GetCredentialsResponse> {
private static final String TAG = "ProviderGetSession";
// Key to be used as an entry key for a credential entry
private static final String CREDENTIAL_ENTRY_KEY = "credential_key";
private GetCredentialsResponse mResponse;
@NonNull
private final Map<String, CredentialEntry> mUiCredentials = new HashMap<>();
private final Map<String, CredentialEntry> mUiCredentialEntries = new HashMap<>();
@NonNull
private final Map<String, Action> mUiActions = new HashMap<>();
private final Map<String, Action> mUiActionsEntries = new HashMap<>();
private Action mAuthenticationAction = null;
public ProviderGetSession(CredentialProviderInfo info,
ProviderInternalCallback callbacks,
int userId, RemoteCredentialService remoteCredentialService) {
super(info, callbacks, userId, remoteCredentialService);
setStatus(Status.PENDING);
}
/** Updates the response being maintained in state by this provider session. */
@Override
public void updateResponse(GetCredentialsResponse response) {
if (response.getAuthenticationAction() != null) {
// TODO : Implement authentication logic
} else if (response.getCredentialsDisplayContent() != null) {
Log.i(TAG , "updateResponse with credentialEntries");
mResponse = response;
updateStatusAndInvokeCallback(Status.COMPLETE);
/** 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) {
GetCredentialsRequest providerRequest =
createProviderRequest(providerInfo.getCapabilities(),
getRequestSession.mClientRequest,
getRequestSession.mClientCallingPackage);
if (providerRequest != null) {
return new ProviderGetSession(context, providerInfo, getRequestSession, userId,
remoteCredentialService, providerRequest);
}
Log.i(TAG, "Unable to create provider session");
return null;
}
/** Returns the response being maintained in this provider session. */
@Override
@Nullable
public GetCredentialsResponse getResponse() {
return mResponse;
private static GetCredentialsRequest createProviderRequest(List<String> providerCapabilities,
android.credentials.GetCredentialRequest clientRequest,
String clientCallingPackage) {
List<GetCredentialOption> filteredOptions = new ArrayList<>();
for (GetCredentialOption option : clientRequest.getGetCredentialOptions()) {
if (providerCapabilities.contains(option.getType())) {
Log.i(TAG, "In createProviderRequest - capability found : " + option.getType());
filteredOptions.add(option);
} else {
Log.i(TAG, "In createProviderRequest - capability not "
+ "found : " + option.getType());
}
}
if (!filteredOptions.isEmpty()) {
return new GetCredentialsRequest.Builder(clientCallingPackage).setGetCredentialOptions(
filteredOptions).build();
}
Log.i(TAG, "In createProviderRequest - returning null");
return null;
}
public ProviderGetSession(Context context,
CredentialProviderInfo info,
ProviderInternalCallback callbacks,
int userId, RemoteCredentialService remoteCredentialService,
GetCredentialsRequest request) {
super(context, info, request, callbacks, userId, remoteCredentialService);
setStatus(Status.PENDING);
}
/** Returns the credential entry maintained in state by this provider session. */
@Nullable
public CredentialEntry getCredentialEntry(@NonNull String entryId) {
return mUiCredentials.get(entryId);
}
/** Returns the action entry maintained in state by this provider session. */
@Nullable
public Action getAction(@NonNull String entryId) {
return mUiActions.get(entryId);
return mUiCredentialEntries.get(entryId);
}
/** Called when the provider response has been updated by an external source. */
@Override
@Override // Callback from the remote provider
public void onProviderResponseSuccess(@Nullable GetCredentialsResponse response) {
Log.i(TAG, "in onProviderResponseSuccess");
updateResponse(response);
onUpdateResponse(response);
}
/** Called when the provider response resulted in a failure. */
@Override
@Override // Callback from the remote provider
public void onProviderResponseFailure(int errorCode, @Nullable CharSequence message) {
updateStatusAndInvokeCallback(toStatus(errorCode));
}
/** Called when provider service dies. */
@Override
@Override // Callback from the remote provider
public void onProviderServiceDied(RemoteCredentialService service) {
if (service.getComponentName().equals(mProviderInfo.getServiceInfo().getComponentName())) {
updateStatusAndInvokeCallback(Status.SERVICE_DEAD);
@@ -116,77 +139,106 @@ public final class ProviderGetSession extends ProviderSession<GetCredentialsResp
}
}
@Override
protected GetCredentialProviderData prepareUiData() throws IllegalArgumentException {
Log.i(TAG, "In prepareUiData");
if (!ProviderSession.isCompletionStatus(getStatus())) {
Log.i(TAG, "In prepareUiData not complete");
@Override // Callback from the provider intent controller class
public void onProviderIntentResult(Bundle resultData) {
// TODO : Implement
}
throw new IllegalStateException("Status must be in completion mode");
@Override
public void onProviderIntentCancelled() {
// TODO : Implement
}
@Override // Selection call from the request provider
protected void onUiEntrySelected(String entryType, String entryId) {
// TODO: Implement
}
@Override // Call from request session to data to be shown on the UI
@Nullable protected GetCredentialProviderData prepareUiData() throws IllegalArgumentException {
Log.i(TAG, "In prepareUiData");
if (!ProviderSession.isUiInvokingStatus(getStatus())) {
Log.i(TAG, "In prepareUiData - provider does not want to show UI: "
+ mComponentName.flattenToString());
return null;
}
GetCredentialsResponse response = getResponse();
GetCredentialsResponse response = getProviderResponse();
if (response == null) {
Log.i(TAG, "In prepareUiData response null");
throw new IllegalStateException("Response must be in completion mode");
}
if (response.getAuthenticationAction() != null) {
Log.i(TAG, "In prepareUiData auth not null");
return prepareUiProviderDataWithAuthentication(response.getAuthenticationAction());
Log.i(TAG, "In prepareUiData - top level authentication mode");
return prepareUiProviderData(null, null,
prepareUiAuthenticationActionEntry(response.getAuthenticationAction()),
/*remoteEntry=*/null);
}
if (response.getCredentialsDisplayContent() != null){
Log.i(TAG, "In prepareUiData credentials not null");
return prepareUiProviderDataWithCredentials(response.getCredentialsDisplayContent());
Log.i(TAG, "In prepareUiData displayContent not null");
return prepareUiProviderData(populateUiActionEntries(
response.getCredentialsDisplayContent().getActions()),
prepareUiCredentialEntries(response.getCredentialsDisplayContent()
.getCredentialEntries()),
/*authenticationActionEntry=*/null, /*remoteEntry=*/null);
}
return null;
}
/**
* To be called by {@link ProviderGetSession} when the UI is to be invoked.
*/
@Nullable
private GetCredentialProviderData prepareUiProviderDataWithCredentials(@NonNull
CredentialsDisplayContent content) {
Log.i(TAG, "in prepareUiProviderData");
List<Entry> credentialEntries = new ArrayList<>();
List<Entry> actionChips = new ArrayList<>();
Entry authenticationEntry = null;
private Entry prepareUiAuthenticationActionEntry(@NonNull Action authenticationAction) {
String entryId = generateEntryId();
mUiActionsEntries.put(entryId, authenticationAction);
return new Entry(ACTION_ENTRY_KEY, entryId, authenticationAction.getSlice());
}
private List<Entry> prepareUiCredentialEntries(@NonNull
List<CredentialEntry> credentialEntries) {
Log.i(TAG, "in prepareUiProviderDataWithCredentials");
List<Entry> credentialUiEntries = new ArrayList<>();
// Populate the credential entries
for (CredentialEntry credentialEntry : content.getCredentialEntries()) {
String entryId = UUID.randomUUID().toString();
mUiCredentials.put(entryId, credentialEntry);
for (CredentialEntry credentialEntry : credentialEntries) {
String entryId = generateEntryId();
mUiCredentialEntries.put(entryId, credentialEntry);
Log.i(TAG, "in prepareUiProviderData creating ui entry with id " + entryId);
Slice slice = credentialEntry.getSlice();
// TODO : Remove conversion of string to int after change in Entry class
credentialEntries.add(new Entry(CREDENTIAL_ENTRY_KEY, entryId,
credentialUiEntries.add(new Entry(CREDENTIAL_ENTRY_KEY, entryId,
credentialEntry.getSlice()));
}
// populate the action chip
for (Action action : content.getActions()) {
String entryId = UUID.randomUUID().toString();
mUiActions.put(entryId, action);
// TODO : Remove conversion of string to int after change in Entry class
actionChips.add(new Entry(ACTION_ENTRY_KEY, entryId,
action.getSlice()));
}
return credentialUiEntries;
}
return new GetCredentialProviderData.Builder(mComponentName.flattenToString())
private List<Entry> populateUiActionEntries(@Nullable List<Action> actions) {
List<Entry> actionEntries = new ArrayList<>();
for (Action action : actions) {
String entryId = UUID.randomUUID().toString();
mUiActionsEntries.put(entryId, action);
// TODO : Remove conversion of string to int after change in Entry class
actionEntries.add(new Entry(ACTION_ENTRY_KEY, entryId, action.getSlice()));
}
return actionEntries;
}
private GetCredentialProviderData prepareUiProviderData(List<Entry> actionEntries,
List<Entry> credentialEntries, Entry authenticationActionEntry,
Entry remoteEntry) {
return new GetCredentialProviderData.Builder(
mComponentName.flattenToString()).setActionChips(actionEntries)
.setCredentialEntries(credentialEntries)
.setActionChips(actionChips)
.setAuthenticationEntry(authenticationEntry)
.setAuthenticationEntry(authenticationActionEntry)
.build();
}
/**
* To be called by {@link ProviderGetSession} when the UI is to be invoked.
*/
@Nullable
private GetCredentialProviderData prepareUiProviderDataWithAuthentication(@NonNull
Action authenticationEntry) {
// TODO : Implement authentication flow
return null;
/** Updates the response being maintained in state by this provider session. */
private void onUpdateResponse(GetCredentialsResponse response) {
mProviderResponse = response;
if (response.getAuthenticationAction() != null) {
Log.i(TAG , "updateResponse with authentication entry");
// TODO validate authentication action
mAuthenticationAction = response.getAuthenticationAction();
updateStatusAndInvokeCallback(Status.REQUIRES_AUTHENTICATION);
} else if (response.getCredentialsDisplayContent() != null) {
Log.i(TAG , "updateResponse with credentialEntries");
// TODO validate response
updateStatusAndInvokeCallback(Status.CREDENTIALS_RECEIVED);
}
}
}

View File

@@ -0,0 +1,117 @@
/*
* 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.SuppressLint;
import android.annotation.UserIdInt;
import android.app.Activity;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.IntentSender;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Parcel;
import android.os.ResultReceiver;
import android.service.credentials.CreateCredentialRequest;
import android.service.credentials.CredentialProviderService;
import android.util.Log;
/**
* Class that invokes providers' pending intents and listens to the responses.
*/
@SuppressLint("LongLogTag")
public class ProviderIntentController {
private static final String TAG = "ProviderIntentController";
/**
* Interface to be implemented by any class that wishes to get callbacks from the UI.
*/
public interface ProviderIntentControllerCallback {
/** Called when the user makes a selection. */
void onProviderIntentResult(Bundle resultData);
/** Called when the user cancels the UI. */
void onProviderIntentCancelled();
}
private final int mUserId;
private final Context mContext;
private final ProviderIntentControllerCallback mCallback;
private final ResultReceiver mResultReceiver = new ResultReceiver(
new Handler(Looper.getMainLooper())) {
@Override
protected void onReceiveResult(int resultCode, Bundle resultData) {
Log.i(TAG, "onReceiveResult in providerIntentController");
if (resultCode == Activity.RESULT_OK) {
Log.i(TAG, "onReceiveResult - ACTIVITYOK");
mCallback.onProviderIntentResult(resultData);
} else if (resultCode == Activity.RESULT_CANCELED) {
Log.i(TAG, "onReceiveResult - RESULTCANCELED");
mCallback.onProviderIntentCancelled();
}
// Drop unknown result
}
};
public ProviderIntentController(@UserIdInt int userId,
Context context,
ProviderIntentControllerCallback callback) {
mUserId = userId;
mContext = context;
mCallback = callback;
}
/** Sets up the request data and invokes the given pending intent. */
public void setupAndInvokePendingIntent(@NonNull PendingIntent pendingIntent,
CreateCredentialRequest request) {
Log.i(TAG, "in invokePendingIntent");
setupIntent(pendingIntent, request);
Log.i(TAG, "in invokePendingIntent receiver set up");
Log.i(TAG, "creator package: " + pendingIntent.getIntentSender()
.getCreatorPackage());
try {
mContext.startIntentSender(pendingIntent.getIntentSender(),
null, 0, 0, 0);
} catch (IntentSender.SendIntentException e) {
Log.i(TAG, "Error while invoking pending intent");
}
}
private void setupIntent(PendingIntent pendingIntent, CreateCredentialRequest request) {
pendingIntent.getIntent().putExtra(Intent.EXTRA_RESULT_RECEIVER,
toIpcFriendlyResultReceiver(mResultReceiver));
pendingIntent.getIntent().putExtra(
CredentialProviderService.EXTRA_CREATE_CREDENTIAL_REQUEST_PARAMS,
request.getData());
}
private <T extends ResultReceiver> ResultReceiver toIpcFriendlyResultReceiver(
T resultReceiver) {
final Parcel parcel = Parcel.obtain();
resultReceiver.writeToParcel(parcel, 0);
parcel.setDataPosition(0);
final ResultReceiver ipcFriendly = ResultReceiver.CREATOR.createFromParcel(parcel);
parcel.recycle();
return ipcFriendly;
}
}

View File

@@ -17,25 +17,70 @@
package com.android.server.credentials;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.ComponentName;
import android.content.Context;
import android.credentials.Credential;
import android.credentials.ui.ProviderData;
import android.os.Bundle;
import android.service.credentials.CredentialProviderException;
import android.service.credentials.CredentialProviderInfo;
import java.util.UUID;
/**
* Provider session storing the state of provider response and ui entries.
* @param <T> The request type expected from the remote provider, for a given request session.
* @param <T> The request to be sent to the provider
* @param <R> The response to be expected from the provider
*/
public abstract class ProviderSession<T> implements RemoteCredentialService.ProviderCallbacks<T> {
public abstract class ProviderSession<T, R> implements RemoteCredentialService.ProviderCallbacks<R>,
ProviderIntentController.ProviderIntentControllerCallback {
// Key to be used as the entry key for an action entry
protected static final String ACTION_ENTRY_KEY = "action_key";
@NonNull protected final Context mContext;
@NonNull protected final ComponentName mComponentName;
@NonNull protected final CredentialProviderInfo mProviderInfo;
@NonNull protected final RemoteCredentialService mRemoteCredentialService;
@NonNull protected final int mUserId;
@NonNull protected Status mStatus = Status.NOT_STARTED;
@NonNull protected final ProviderInternalCallback mCallbacks;
@NonNull protected final ProviderIntentController mProviderIntentController;
@Nullable protected Credential mFinalCredentialResponse;
@NonNull protected final T mProviderRequest;
@Nullable protected R mProviderResponse;
/**
* Returns true if the given status reflects that the provider state is ready to be shown
* on the credMan UI.
*/
public static boolean isUiInvokingStatus(Status status) {
return status == Status.CREDENTIALS_RECEIVED || status == Status.SAVE_ENTRIES_RECEIVED;
}
/**
* Returns true if the given status reflects that the provider is waiting for a remote
* response.
*/
public static boolean isStatusWaitingForRemoteResponse(Status status) {
return status == Status.PENDING;
}
/**
* Returns true if the given status means that the provider session must be terminated.
*/
public static boolean isTerminatingStatus(Status status) {
return status == Status.CANCELED || status == Status.SERVICE_DEAD;
}
/**
* Returns true if the given status reflects that the provider is done getting the response,
* and is ready to return the final credential back to the user.
*/
public static boolean isCompletionStatus(Status status) {
return status == Status.CREDENTIAL_RECEIVED_FROM_INTENT
|| status == Status.CREDENTIAL_RECEIVED_FROM_SELECTION;
}
/**
* Interface to be implemented by any class that wishes to get a callback when a particular
@@ -49,35 +94,49 @@ public abstract class ProviderSession<T> implements RemoteCredentialService.Prov
void onProviderStatusChanged(Status status, ComponentName componentName);
}
protected ProviderSession(@NonNull CredentialProviderInfo info,
protected ProviderSession(@NonNull Context context, @NonNull CredentialProviderInfo info,
@NonNull T providerRequest,
@NonNull ProviderInternalCallback callbacks,
@NonNull int userId,
@NonNull RemoteCredentialService remoteCredentialService) {
mContext = context;
mProviderInfo = info;
mProviderRequest = providerRequest;
mCallbacks = callbacks;
mUserId = userId;
mComponentName = info.getServiceInfo().getComponentName();
mRemoteCredentialService = remoteCredentialService;
mProviderIntentController = new ProviderIntentController(userId, context, this);
}
/** Update the response state stored with the provider session. */
protected abstract void updateResponse (T response);
/** Update the response state stored with the provider session. */
protected abstract T getResponse ();
/** Should be overridden to prepare, and stores state for {@link ProviderData} to be
* shown on the UI. */
protected abstract ProviderData prepareUiData();
/** Provider status at various states of the request session. */
// TODO: Review status values, and adjust where needed
enum Status {
NOT_STARTED,
PENDING,
REQUIRES_AUTHENTICATION,
COMPLETE,
CREDENTIALS_RECEIVED,
SERVICE_DEAD,
CANCELED
CREDENTIAL_RECEIVED_FROM_INTENT,
PENDING_INTENT_INVOKED,
CREDENTIAL_RECEIVED_FROM_SELECTION,
SAVE_ENTRIES_RECEIVED, CANCELED
}
/** Converts exception to a provider session status. */
@NonNull
public static Status toStatus(
@CredentialProviderException.CredentialProviderError int errorCode) {
// TODO : Add more mappings as more flows are supported
return Status.CANCELED;
}
protected String generateEntryId() {
return UUID.randomUUID().toString();
}
public Credential getFinalCredentialResponse() {
return mFinalCredentialResponse;
}
protected void setStatus(@NonNull Status status) {
@@ -94,31 +153,38 @@ public abstract class ProviderSession<T> implements RemoteCredentialService.Prov
return mComponentName;
}
@NonNull
protected RemoteCredentialService getRemoteCredentialService() {
return mRemoteCredentialService;
}
/** Updates the status .*/
protected void updateStatusAndInvokeCallback(@NonNull Status status) {
setStatus(status);
mCallbacks.onProviderStatusChanged(status, mComponentName);
}
@NonNull
public static Status toStatus(
@CredentialProviderException.CredentialProviderError int errorCode) {
// TODO : Add more mappings as more flows are supported
return Status.CANCELED;
/** Get the request to be sent to the provider. */
protected T getProviderRequest() {
return mProviderRequest;
}
/**
* Returns true if the given status means that the provider session must be terminated.
*/
public static boolean isTerminatingStatus(Status status) {
return status == Status.CANCELED || status == Status.SERVICE_DEAD;
/** Update the response state stored with the provider session. */
@Nullable protected R getProviderResponse() {
return mProviderResponse;
}
/**
* Returns true if the given status means that the provider is done getting the response,
* and is ready for user interaction.
*/
public static boolean isCompletionStatus(Status status) {
return status == Status.COMPLETE || status == Status.REQUIRES_AUTHENTICATION;
}
/** Should be overridden to prepare, and stores state for {@link ProviderData} to be
* shown on the UI. */
@Nullable protected abstract ProviderData prepareUiData();
/** Should be overridden to handle the selected entry from the UI. */
protected abstract void onUiEntrySelected(String entryType, String entryId);
@Override
public abstract void onProviderIntentResult(Bundle resultData);
@Override
public abstract void onProviderIntentCancelled();
}

View File

@@ -24,11 +24,14 @@ import android.content.Intent;
import android.os.Handler;
import android.os.ICancellationSignal;
import android.os.RemoteException;
import android.service.credentials.CreateCredentialRequest;
import android.service.credentials.CreateCredentialResponse;
import android.service.credentials.CredentialProviderException;
import android.service.credentials.CredentialProviderException.CredentialProviderError;
import android.service.credentials.CredentialProviderService;
import android.service.credentials.GetCredentialsRequest;
import android.service.credentials.GetCredentialsResponse;
import android.service.credentials.ICreateCredentialCallback;
import android.service.credentials.ICredentialProviderService;
import android.service.credentials.IGetCredentialsCallback;
import android.text.format.DateUtils;
@@ -76,7 +79,7 @@ public class RemoteCredentialService extends ServiceConnector.Impl<ICredentialPr
public RemoteCredentialService(@NonNull Context context,
@NonNull ComponentName componentName, int userId) {
super(context, new Intent(CredentialProviderService.SERVICE_INTERFACE)
.setComponent(componentName), Context.BIND_ALLOW_BACKGROUND_ACTIVITY_STARTS,
.setComponent(componentName), /*bindingFlags=*/0,
userId, ICredentialProviderService.Stub::asInterface);
mComponentName = componentName;
}
@@ -101,7 +104,7 @@ public class RemoteCredentialService extends ServiceConnector.Impl<ICredentialPr
* provider service.
* @param request the request to be sent to the provider
* @param callback the callback to be used to send back the provider response to the
* {@link ProviderSession} class that maintains provider state
* {@link ProviderGetSession} class that maintains provider state
*/
public void onGetCredentials(@NonNull GetCredentialsRequest request,
ProviderCallbacks<GetCredentialsResponse> callback) {
@@ -114,21 +117,21 @@ public class RemoteCredentialService extends ServiceConnector.Impl<ICredentialPr
CompletableFuture<GetCredentialsResponse> getCredentials = new CompletableFuture<>();
ICancellationSignal cancellationSignal =
service.onGetCredentials(request, new IGetCredentialsCallback.Stub() {
@Override
public void onSuccess(GetCredentialsResponse response) {
Log.i(TAG, "In onSuccess in RemoteCredentialService");
getCredentials.complete(response);
}
@Override
public void onSuccess(GetCredentialsResponse response) {
Log.i(TAG, "In onSuccess in RemoteCredentialService");
getCredentials.complete(response);
}
@Override
public void onFailure(@CredentialProviderError int errorCode,
CharSequence message) {
Log.i(TAG, "In onFailure in RemoteCredentialService");
String errorMsg = message == null ? "" : String.valueOf(message);
getCredentials.completeExceptionally(new CredentialProviderException(
errorCode, errorMsg));
}
});
@Override
public void onFailure(@CredentialProviderError int errorCode,
CharSequence message) {
Log.i(TAG, "In onFailure in RemoteCredentialService");
String errorMsg = message == null ? "" : String.valueOf(message);
getCredentials.completeExceptionally(new CredentialProviderException(
errorCode, errorMsg));
}
});
CompletableFuture<GetCredentialsResponse> future = futureRef.get();
if (future != null && future.isCancelled()) {
dispatchCancellationSignal(cancellationSignal);
@@ -137,38 +140,91 @@ public class RemoteCredentialService extends ServiceConnector.Impl<ICredentialPr
}
return getCredentials;
}).orTimeout(TIMEOUT_REQUEST_MILLIS, TimeUnit.MILLISECONDS);
futureRef.set(connectThenExecute);
connectThenExecute.whenComplete((result, error) -> Handler.getMain().post(() -> {
if (error == null) {
Log.i(TAG, "In RemoteCredentialService execute error is null");
callback.onProviderResponseSuccess(result);
futureRef.set(connectThenExecute);
connectThenExecute.whenComplete((result, error) -> Handler.getMain().post(() ->
handleExecutionResponse(result, error, cancellationSink, callback)));
}
/** Main entry point to be called for executing a createCredential call on the remote
* provider service.
* @param request the request to be sent to the provider
* @param callback the callback to be used to send back the provider response to the
* {@link ProviderCreateSession} class that maintains provider state
*/
public void onCreateCredential(@NonNull CreateCredentialRequest request,
ProviderCallbacks<CreateCredentialResponse> callback) {
Log.i(TAG, "In onCreateCredential in RemoteCredentialService");
AtomicReference<ICancellationSignal> cancellationSink = new AtomicReference<>();
AtomicReference<CompletableFuture<CreateCredentialResponse>> futureRef =
new AtomicReference<>();
CompletableFuture<CreateCredentialResponse> connectThenExecute = postAsync(service -> {
CompletableFuture<CreateCredentialResponse> createCredentialFuture =
new CompletableFuture<>();
ICancellationSignal cancellationSignal = service.onCreateCredential(
request, new ICreateCredentialCallback.Stub() {
@Override
public void onSuccess(CreateCredentialResponse response) {
Log.i(TAG, "In onSuccess onCreateCredential "
+ "in RemoteCredentialService");
createCredentialFuture.complete(response);
}
@Override
public void onFailure(@CredentialProviderError int errorCode,
CharSequence message) {
Log.i(TAG, "In onFailure in RemoteCredentialService");
String errorMsg = message == null ? "" : String.valueOf(message);
createCredentialFuture.completeExceptionally(
new CredentialProviderException(errorCode, errorMsg));
}});
CompletableFuture<CreateCredentialResponse> future = futureRef.get();
if (future != null && future.isCancelled()) {
dispatchCancellationSignal(cancellationSignal);
} else {
if (error instanceof TimeoutException) {
Log.i(TAG, "In RemoteCredentialService execute error is timeout");
dispatchCancellationSignal(cancellationSink.get());
callback.onProviderResponseFailure(
CredentialProviderException.ERROR_TIMEOUT,
error.getMessage());
} else if (error instanceof CancellationException) {
Log.i(TAG, "In RemoteCredentialService execute error is cancellation");
dispatchCancellationSignal(cancellationSink.get());
callback.onProviderResponseFailure(
CredentialProviderException.ERROR_TASK_CANCELED,
error.getMessage());
} else if (error instanceof CredentialProviderException) {
Log.i(TAG, "In RemoteCredentialService execute error is provider error");
callback.onProviderResponseFailure(((CredentialProviderException) error)
.getErrorCode(),
error.getMessage());
} else {
Log.i(TAG, "In RemoteCredentialService execute error is unknown");
callback.onProviderResponseFailure(
CredentialProviderException.ERROR_UNKNOWN,
error.getMessage());
}
cancellationSink.set(cancellationSignal);
}
}));
return createCredentialFuture;
}).orTimeout(TIMEOUT_REQUEST_MILLIS, TimeUnit.MILLISECONDS);
futureRef.set(connectThenExecute);
connectThenExecute.whenComplete((result, error) -> Handler.getMain().post(() ->
handleExecutionResponse(result, error, cancellationSink, callback)));
}
private <T> void handleExecutionResponse(T result,
Throwable error,
AtomicReference<ICancellationSignal> cancellationSink,
ProviderCallbacks<T> callback) {
if (error == null) {
Log.i(TAG, "In RemoteCredentialService execute error is null");
callback.onProviderResponseSuccess(result);
} else {
if (error instanceof TimeoutException) {
Log.i(TAG, "In RemoteCredentialService execute error is timeout");
dispatchCancellationSignal(cancellationSink.get());
callback.onProviderResponseFailure(
CredentialProviderException.ERROR_TIMEOUT,
error.getMessage());
} else if (error instanceof CancellationException) {
Log.i(TAG, "In RemoteCredentialService execute error is cancellation");
dispatchCancellationSignal(cancellationSink.get());
callback.onProviderResponseFailure(
CredentialProviderException.ERROR_TASK_CANCELED,
error.getMessage());
} else if (error instanceof CredentialProviderException) {
Log.i(TAG, "In RemoteCredentialService execute error is provider error");
callback.onProviderResponseFailure(((CredentialProviderException) error)
.getErrorCode(),
error.getMessage());
} else {
Log.i(TAG, "In RemoteCredentialService execute error is unknown");
callback.onProviderResponseFailure(
CredentialProviderException.ERROR_UNKNOWN,
error.getMessage());
}
}
}
private void dispatchCancellationSignal(@Nullable ICancellationSignal signal) {

View File

@@ -20,18 +20,30 @@ import android.annotation.NonNull;
import android.annotation.UserIdInt;
import android.content.ComponentName;
import android.content.Context;
import android.credentials.ui.ProviderData;
import android.credentials.ui.UserSelectionDialogResult;
import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.service.credentials.CredentialProviderInfo;
import android.util.Log;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
/**
* Base class of a request session, that listens to UI events. This class must be extended
* every time a new response type is expected from the providers.
*/
abstract class RequestSession implements CredentialManagerUi.CredentialManagerUiCallback,
abstract class RequestSession<T, U> implements CredentialManagerUi.CredentialManagerUiCallback,
ProviderSession.ProviderInternalCallback {
private static final String TAG = "RequestSession";
// TODO: Revise access levels of attributes
@NonNull protected final T mClientRequest;
@NonNull protected final U mClientCallback;
@NonNull protected final IBinder mRequestId;
@NonNull protected final Context mContext;
@NonNull protected final CredentialManagerUi mCredentialManagerUi;
@@ -39,29 +51,120 @@ abstract class RequestSession implements CredentialManagerUi.CredentialManagerUi
@NonNull protected final Handler mHandler;
@NonNull protected boolean mIsFirstUiTurn = true;
@UserIdInt protected final int mUserId;
@NonNull protected final String mClientCallingPackage;
protected final Map<String, ProviderSession> mProviders = new HashMap<>();
protected RequestSession(@NonNull Context context,
@UserIdInt int userId, @NonNull String requestType) {
@UserIdInt int userId, @NonNull T clientRequest, U clientCallback,
@NonNull String requestType,
String clientCallingPackage) {
mContext = context;
mUserId = userId;
mClientRequest = clientRequest;
mClientCallback = clientCallback;
mRequestType = requestType;
mClientCallingPackage = clientCallingPackage;
mHandler = new Handler(Looper.getMainLooper(), null, true);
mRequestId = new Binder();
mCredentialManagerUi = new CredentialManagerUi(mContext,
mUserId, this);
}
/** Returns the unique identifier of this request session. */
public IBinder getRequestId() {
return mRequestId;
public abstract ProviderSession initiateProviderSession(CredentialProviderInfo providerInfo,
RemoteCredentialService remoteCredentialService);
protected abstract void launchUiWithProviderData(ArrayList<ProviderData> providerDataList);
// UI callbacks
@Override // from CredentialManagerUiCallbacks
public void onUiSelection(UserSelectionDialogResult selection) {
String providerId = selection.getProviderId();
Log.i(TAG, "onUiSelection, providerId: " + providerId);
ProviderSession providerSession = mProviders.get(providerId);
if (providerSession == null) {
Log.i(TAG, "providerSession not found in onUiSelection");
return;
}
Log.i(TAG, "Provider session found");
providerSession.onUiEntrySelected(selection.getEntryKey(),
selection.getEntrySubkey());
}
@Override // from CredentialManagerUiCallback
public abstract void onUiSelection(UserSelectionDialogResult selection);
@Override // from CredentialManagerUiCallbacks
public void onUiCancellation() {
// User canceled the activity
finishSession();
}
@Override // from CredentialManagerUiCallback
public abstract void onUiCancelation();
@Override // from provider session
public void onProviderStatusChanged(ProviderSession.Status status,
ComponentName componentName) {
Log.i(TAG, "in onStatusChanged with status: " + status);
if (ProviderSession.isTerminatingStatus(status)) {
Log.i(TAG, "in onStatusChanged terminating status");
onProviderTerminated(componentName);
//TODO: Check if this was the provider we were waiting for and can invoke the UI now
} else if (ProviderSession.isCompletionStatus(status)) {
Log.i(TAG, "in onStatusChanged isCompletionStatus status");
onProviderResponseComplete(componentName);
} else if (ProviderSession.isUiInvokingStatus(status)) {
Log.i(TAG, "in onStatusChanged isUiInvokingStatus status");
onProviderResponseRequiresUi();
}
}
@Override // from ProviderInternalCallback
public abstract void onProviderStatusChanged(ProviderSession.Status status, ComponentName componentName);
protected void onProviderTerminated(ComponentName componentName) {
//TODO: Implement
}
protected void onProviderResponseComplete(ComponentName componentName) {
//TODO: Implement
}
protected void onProviderResponseRequiresUi() {
Log.i(TAG, "in onProviderResponseComplete");
// TODO: Determine whether UI has already been invoked, and deal accordingly
if (!isAnyProviderPending()) {
Log.i(TAG, "in onProviderResponseComplete - isResponseCompleteAcrossProviders");
getProviderDataAndInitiateUi();
} else {
Log.i(TAG, "Can't invoke UI - waiting on some providers");
}
}
protected void finishSession() {
clearProviderSessions();
}
protected void clearProviderSessions() {
//TODO: Implement
mProviders.clear();
}
private boolean isAnyProviderPending() {
for (ProviderSession session : mProviders.values()) {
if (ProviderSession.isStatusWaitingForRemoteResponse(session.getStatus())) {
return true;
}
}
return false;
}
private void getProviderDataAndInitiateUi() {
ArrayList<ProviderData> providerDataList = new ArrayList<>();
for (ProviderSession session : mProviders.values()) {
Log.i(TAG, "preparing data for : " + session.getComponentName());
ProviderData providerData = session.prepareUiData();
if (providerData != null) {
Log.i(TAG, "Provider data is not null");
providerDataList.add(providerData);
}
}
if (!providerDataList.isEmpty()) {
Log.i(TAG, "provider list not empty about to initiate ui");
launchUiWithProviderData(providerDataList);
}
}
}