blob: fc179e4526f5714d0b103a0d4445e7b3500f17e0 [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 static com.android.server.credentials.MetricUtilities.METRICS_PROVIDER_STATUS_QUERY_FAILURE;
import static com.android.server.credentials.MetricUtilities.METRICS_PROVIDER_STATUS_QUERY_SUCCESS;
import android.Manifest;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.credentials.Credential;
import android.credentials.CredentialProviderInfo;
import android.credentials.ui.ProviderData;
import android.credentials.ui.ProviderPendingIntentResponse;
import android.os.ICancellationSignal;
import android.os.RemoteException;
import android.util.Log;
import com.android.server.credentials.metrics.CandidateProviderMetric;
import java.util.UUID;
/**
* Provider session storing the state of provider response and ui entries.
* @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, R>
implements RemoteCredentialService.ProviderCallbacks<R> {
private static final String TAG = "ProviderSession";
@NonNull protected final Context mContext;
@NonNull protected final ComponentName mComponentName;
@Nullable protected final CredentialProviderInfo mProviderInfo;
@Nullable protected final RemoteCredentialService mRemoteCredentialService;
@NonNull protected final int mUserId;
@NonNull protected Status mStatus = Status.NOT_STARTED;
@Nullable protected final ProviderInternalCallback mCallbacks;
@Nullable protected Credential mFinalCredentialResponse;
@Nullable protected ICancellationSignal mProviderCancellationSignal;
@NonNull protected final T mProviderRequest;
@Nullable protected R mProviderResponse;
@NonNull protected Boolean mProviderResponseSet = false;
// Specific candidate provider metric for the provider this session handles
@Nullable protected CandidateProviderMetric mCandidateProviderMetric;
@NonNull private int mProviderSessionUid;
/**
* 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
|| status == Status.NO_CREDENTIALS_FROM_AUTH_ENTRY;
}
/**
* 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
|| status == Status.COMPLETE;
}
/**
* Interface to be implemented by any class that wishes to get a callback when a particular
* provider session's status changes. Typically, implemented by the {@link RequestSession}
* class.
* @param <V> the type of the final response expected
*/
public interface ProviderInternalCallback<V> {
/** Called when status changes. */
void onProviderStatusChanged(Status status, ComponentName componentName);
/** Called when the final credential is received through an entry selection. */
void onFinalResponseReceived(ComponentName componentName, V response);
/** Called when an error is received through an entry selection. */
void onFinalErrorReceived(ComponentName componentName, String errorType,
@Nullable String message);
}
protected ProviderSession(@NonNull Context context, @NonNull CredentialProviderInfo info,
@NonNull T providerRequest,
@Nullable ProviderInternalCallback callbacks,
@NonNull int userId,
@Nullable RemoteCredentialService remoteCredentialService) {
mContext = context;
mProviderInfo = info;
mProviderRequest = providerRequest;
mCallbacks = callbacks;
mUserId = userId;
mComponentName = info.getServiceInfo().getComponentName();
mRemoteCredentialService = remoteCredentialService;
mCandidateProviderMetric = new CandidateProviderMetric();
mProviderSessionUid = MetricUtilities.getPackageUid(mContext, mComponentName);
}
/** Provider status at various states of the request session. */
// TODO: Review status values, and adjust where needed
enum Status {
NOT_STARTED,
PENDING,
REQUIRES_AUTHENTICATION,
CREDENTIALS_RECEIVED,
SERVICE_DEAD,
CREDENTIAL_RECEIVED_FROM_INTENT,
PENDING_INTENT_INVOKED,
CREDENTIAL_RECEIVED_FROM_SELECTION,
SAVE_ENTRIES_RECEIVED, CANCELED,
NO_CREDENTIALS, EMPTY_RESPONSE, NO_CREDENTIALS_FROM_AUTH_ENTRY, COMPLETE
}
/** Converts exception to a provider session status. */
@NonNull
public static Status toStatus(int errorCode) {
// TODO : Add more mappings as more flows are supported
return Status.CANCELED;
}
protected static String generateUniqueId() {
return UUID.randomUUID().toString();
}
public Credential getFinalCredentialResponse() {
return mFinalCredentialResponse;
}
/** Propagates cancellation signal to the remote provider service. */
public void cancelProviderRemoteSession() {
try {
if (mProviderCancellationSignal != null) {
mProviderCancellationSignal.cancel();
}
setStatus(Status.CANCELED);
} catch (RemoteException e) {
Log.i(TAG, "Issue while cancelling provider session: " + e.getMessage());
}
}
protected void setStatus(@NonNull Status status) {
mStatus = status;
}
@NonNull
protected Status getStatus() {
return mStatus;
}
@NonNull
protected ComponentName getComponentName() {
return mComponentName;
}
@Nullable
protected RemoteCredentialService getRemoteCredentialService() {
return mRemoteCredentialService;
}
/** Updates the status .*/
protected void updateStatusAndInvokeCallback(@NonNull Status status) {
setStatus(status);
updateCandidateMetric(status);
mCallbacks.onProviderStatusChanged(status, mComponentName);
}
private void updateCandidateMetric(Status status) {
mCandidateProviderMetric.setCandidateUid(mProviderSessionUid);
mCandidateProviderMetric
.setQueryFinishTimeNanoseconds(System.nanoTime());
if (isTerminatingStatus(status)) {
mCandidateProviderMetric.setProviderQueryStatus(METRICS_PROVIDER_STATUS_QUERY_FAILURE);
} else if (isCompletionStatus(status)) {
mCandidateProviderMetric.setProviderQueryStatus(METRICS_PROVIDER_STATUS_QUERY_SUCCESS);
}
}
/** Get the request to be sent to the provider. */
protected T getProviderRequest() {
return mProviderRequest;
}
/** Returns whether the provider response is set. */
protected Boolean isProviderResponseSet() {
return mProviderResponse != null || mProviderResponseSet;
}
protected void invokeCallbackWithError(String errorType, @Nullable String errorMessage) {
// TODO: Determine what the error message should be
mCallbacks.onFinalErrorReceived(mComponentName, errorType, errorMessage);
}
/** Update the response state stored with the provider session. */
@Nullable protected R getProviderResponse() {
return mProviderResponse;
}
protected boolean enforceRemoteEntryRestrictions(
@Nullable ComponentName expectedRemoteEntryProviderService) {
// Check if the service is the one set by the OEM. If not silently reject this entry
if (!mComponentName.equals(expectedRemoteEntryProviderService)) {
Log.i(TAG, "Remote entry being dropped as it is not from the service "
+ "configured by the OEM.");
return false;
}
// Check if the service has the hybrid permission .If not, silently reject this entry.
// This check is in addition to the permission check happening in the provider's process.
try {
ApplicationInfo appInfo = mContext.getPackageManager().getApplicationInfo(
mComponentName.getPackageName(),
PackageManager.ApplicationInfoFlags.of(PackageManager.MATCH_SYSTEM_ONLY));
if (appInfo != null
&& mContext.checkPermission(
Manifest.permission.PROVIDE_REMOTE_CREDENTIALS,
/*pId=*/-1, appInfo.uid) == PackageManager.PERMISSION_GRANTED) {
return true;
}
} catch (SecurityException e) {
Log.i(TAG, "Error getting info for "
+ mComponentName.flattenToString() + ": " + e.getMessage());
return false;
} catch (PackageManager.NameNotFoundException e) {
Log.i(TAG, "Error getting info for "
+ mComponentName.flattenToString() + ": " + e.getMessage());
return false;
}
Log.i(TAG, "In enforceRemoteEntryRestrictions - remote entry checks fail");
return false;
}
/** 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,
ProviderPendingIntentResponse providerPendingIntentResponse);
/** Should be overridden to invoke the provider at a defined location. Helpful for
* situations such as metric generation. */
protected abstract void invokeSession();
}