blob: 4bcf8be0d21e7bba55340fb6efa40bcc65efc7e3 [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.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.credentials.ClearCredentialStateException;
import android.credentials.CreateCredentialException;
import android.credentials.GetCredentialException;
import android.os.Binder;
import android.os.Handler;
import android.os.ICancellationSignal;
import android.os.RemoteException;
import android.service.credentials.BeginCreateCredentialRequest;
import android.service.credentials.BeginCreateCredentialResponse;
import android.service.credentials.BeginGetCredentialRequest;
import android.service.credentials.BeginGetCredentialResponse;
import android.service.credentials.ClearCredentialStateRequest;
import android.service.credentials.CredentialProviderErrors;
import android.service.credentials.CredentialProviderService;
import android.service.credentials.IBeginCreateCredentialCallback;
import android.service.credentials.IBeginGetCredentialCallback;
import android.service.credentials.IClearCredentialStateCallback;
import android.service.credentials.ICredentialProviderService;
import android.text.format.DateUtils;
import android.util.Slog;
import com.android.internal.infra.ServiceConnector;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
/**
* Handles connections with the remote credential provider
*
* @hide
*/
public class RemoteCredentialService extends ServiceConnector.Impl<ICredentialProviderService> {
private static final String TAG = "RemoteCredentialService";
/** Timeout for a single request. */
private static final long TIMEOUT_REQUEST_MILLIS = 3 * DateUtils.SECOND_IN_MILLIS;
/** Timeout to unbind after the task queue is empty. */
private static final long TIMEOUT_IDLE_SERVICE_CONNECTION_MILLIS =
5 * DateUtils.SECOND_IN_MILLIS;
private final ComponentName mComponentName;
private AtomicBoolean mOngoingRequest = new AtomicBoolean(false);
@Nullable private ProviderCallbacks mCallback;
/**
* Callbacks to be invoked when the provider remote service responds with a
* success or failure.
*
* @param <T> the type of response expected from the provider
*/
public interface ProviderCallbacks<T> {
/** Called when a successful response is received from the remote provider. */
void onProviderResponseSuccess(@Nullable T response);
/** Called when a failure response is received from the remote provider. */
void onProviderResponseFailure(int internalErrorCode, @Nullable Exception e);
/** Called when the remote provider service dies. */
void onProviderServiceDied(RemoteCredentialService service);
/** Called to set the cancellation transport from the remote provider service. */
void onProviderCancellable(ICancellationSignal cancellation);
}
public RemoteCredentialService(@NonNull Context context,
@NonNull ComponentName componentName, int userId) {
super(context, new Intent(CredentialProviderService.SERVICE_INTERFACE)
.setComponent(componentName), /*bindingFlags=*/0,
userId, ICredentialProviderService.Stub::asInterface);
mComponentName = componentName;
}
public void setCallback(ProviderCallbacks callback) {
mCallback = callback;
}
/** Unbinds automatically after this amount of time. */
@Override
protected long getAutoDisconnectTimeoutMs() {
return TIMEOUT_IDLE_SERVICE_CONNECTION_MILLIS;
}
@Override
public void onBindingDied(ComponentName name) {
super.onBindingDied(name);
Slog.w(TAG, "binding died for: " + name);
}
@Override
public void binderDied() {
super.binderDied();
Slog.w(TAG, "binderDied");
if (mCallback != null) {
mOngoingRequest.set(false);
mCallback.onProviderServiceDied(this);
}
}
/** Return the componentName of the service to be connected. */
@NonNull
public ComponentName getComponentName() {
return mComponentName;
}
/** Destroys this remote service by unbinding the connection. */
public void destroy() {
unbind();
}
/**
* Main entry point to be called for executing a getCredential call on the remote
* provider service.
*
* @param request the request to be sent to the provider
*/
public void onBeginGetCredential(@NonNull BeginGetCredentialRequest request) {
if (mCallback == null) {
Slog.w(TAG, "Callback is not set");
return;
}
mOngoingRequest.set(true);
AtomicReference<ICancellationSignal> cancellationSink = new AtomicReference<>();
AtomicReference<CompletableFuture<BeginGetCredentialResponse>> futureRef =
new AtomicReference<>();
CompletableFuture<BeginGetCredentialResponse> connectThenExecute = postAsync(service -> {
CompletableFuture<BeginGetCredentialResponse> getCredentials =
new CompletableFuture<>();
final long originalCallingUidToken = Binder.clearCallingIdentity();
try {
service.onBeginGetCredential(request,
new IBeginGetCredentialCallback.Stub() {
@Override
public void onSuccess(BeginGetCredentialResponse response) {
getCredentials.complete(response);
}
@Override
public void onFailure(String errorType, CharSequence message) {
String errorMsg = message == null ? "" : String.valueOf(
message);
getCredentials.completeExceptionally(
new GetCredentialException(errorType, errorMsg));
}
@Override
public void onCancellable(ICancellationSignal cancellation) {
CompletableFuture<BeginGetCredentialResponse> future =
futureRef.get();
if (future != null && future.isCancelled()) {
dispatchCancellationSignal(cancellation);
} else {
cancellationSink.set(cancellation);
if (mCallback != null) {
mCallback.onProviderCancellable(cancellation);
}
}
}
});
return getCredentials;
} finally {
Binder.restoreCallingIdentity(originalCallingUidToken);
}
}).orTimeout(TIMEOUT_REQUEST_MILLIS, TimeUnit.MILLISECONDS);
futureRef.set(connectThenExecute);
connectThenExecute.whenComplete((result, error) -> Handler.getMain().post(() ->
handleExecutionResponse(result, error, cancellationSink)));
}
/**
* Main entry point to be called for executing a beginCreateCredential call on the remote
* provider service.
*
* @param request the request to be sent to the provider
*/
public void onBeginCreateCredential(@NonNull BeginCreateCredentialRequest request) {
if (mCallback == null) {
Slog.w(TAG, "Callback is not set");
return;
}
mOngoingRequest.set(true);
AtomicReference<ICancellationSignal> cancellationSink = new AtomicReference<>();
AtomicReference<CompletableFuture<BeginCreateCredentialResponse>> futureRef =
new AtomicReference<>();
CompletableFuture<BeginCreateCredentialResponse> connectThenExecute =
postAsync(service -> {
CompletableFuture<BeginCreateCredentialResponse> createCredentialFuture =
new CompletableFuture<>();
final long originalCallingUidToken = Binder.clearCallingIdentity();
try {
service.onBeginCreateCredential(
request, new IBeginCreateCredentialCallback.Stub() {
@Override
public void onSuccess(BeginCreateCredentialResponse response) {
createCredentialFuture.complete(response);
}
@Override
public void onFailure(String errorType, CharSequence message) {
String errorMsg = message == null ? "" : String.valueOf(
message);
createCredentialFuture.completeExceptionally(
new CreateCredentialException(errorType, errorMsg));
}
@Override
public void onCancellable(ICancellationSignal cancellation) {
CompletableFuture<BeginCreateCredentialResponse> future =
futureRef.get();
if (future != null && future.isCancelled()) {
dispatchCancellationSignal(cancellation);
} else {
cancellationSink.set(cancellation);
if (mCallback != null) {
mCallback.onProviderCancellable(cancellation);
}
}
}
});
return createCredentialFuture;
} finally {
Binder.restoreCallingIdentity(originalCallingUidToken);
}
}).orTimeout(TIMEOUT_REQUEST_MILLIS, TimeUnit.MILLISECONDS);
futureRef.set(connectThenExecute);
connectThenExecute.whenComplete((result, error) -> Handler.getMain().post(() ->
handleExecutionResponse(result, error, cancellationSink)));
}
/**
* Main entry point to be called for executing a clearCredentialState call on the remote
* provider service.
*
* @param request the request to be sent to the provider
*/
public void onClearCredentialState(@NonNull ClearCredentialStateRequest request) {
if (mCallback == null) {
Slog.w(TAG, "Callback is not set");
return;
}
mOngoingRequest.set(true);
AtomicReference<ICancellationSignal> cancellationSink = new AtomicReference<>();
AtomicReference<CompletableFuture<Void>> futureRef = new AtomicReference<>();
CompletableFuture<Void> connectThenExecute =
postAsync(service -> {
CompletableFuture<Void> clearCredentialFuture =
new CompletableFuture<>();
final long originalCallingUidToken = Binder.clearCallingIdentity();
try {
service.onClearCredentialState(
request, new IClearCredentialStateCallback.Stub() {
@Override
public void onSuccess() {
clearCredentialFuture.complete(null);
}
@Override
public void onFailure(String errorType, CharSequence message) {
String errorMsg = message == null ? "" :
String.valueOf(message);
clearCredentialFuture.completeExceptionally(
new ClearCredentialStateException(errorType,
errorMsg));
}
@Override
public void onCancellable(ICancellationSignal cancellation) {
CompletableFuture<Void> future = futureRef.get();
if (future != null && future.isCancelled()) {
dispatchCancellationSignal(cancellation);
} else {
cancellationSink.set(cancellation);
if (mCallback != null) {
mCallback.onProviderCancellable(cancellation);
}
}
}
});
return clearCredentialFuture;
} finally {
Binder.restoreCallingIdentity(originalCallingUidToken);
}
}).orTimeout(TIMEOUT_REQUEST_MILLIS, TimeUnit.MILLISECONDS);
futureRef.set(connectThenExecute);
connectThenExecute.whenComplete((result, error) -> Handler.getMain().post(() ->
handleExecutionResponse(result, error, cancellationSink)));
}
private <T> void handleExecutionResponse(T result,
Throwable error,
AtomicReference<ICancellationSignal> cancellationSink) {
if (error == null) {
if (mCallback != null) {
mCallback.onProviderResponseSuccess(result);
}
} else {
if (error instanceof TimeoutException) {
Slog.i(TAG, "Remote provider response timed tuo for: " + mComponentName);
if (!mOngoingRequest.get()) {
return;
}
dispatchCancellationSignal(cancellationSink.get());
if (mCallback != null) {
mOngoingRequest.set(false);
mCallback.onProviderResponseFailure(
CredentialProviderErrors.ERROR_TIMEOUT, null);
}
} else if (error instanceof CancellationException) {
Slog.i(TAG, "Cancellation exception for remote provider: " + mComponentName);
if (!mOngoingRequest.get()) {
return;
}
dispatchCancellationSignal(cancellationSink.get());
if (mCallback != null) {
mOngoingRequest.set(false);
mCallback.onProviderResponseFailure(
CredentialProviderErrors.ERROR_TASK_CANCELED,
null);
}
} else if (error instanceof GetCredentialException) {
if (mCallback != null) {
mCallback.onProviderResponseFailure(
CredentialProviderErrors.ERROR_PROVIDER_FAILURE,
(GetCredentialException) error);
}
} else if (error instanceof CreateCredentialException) {
if (mCallback != null) {
mCallback.onProviderResponseFailure(
CredentialProviderErrors.ERROR_PROVIDER_FAILURE,
(CreateCredentialException) error);
}
} else {
if (mCallback != null) {
mCallback.onProviderResponseFailure(
CredentialProviderErrors.ERROR_UNKNOWN,
(Exception) error);
}
}
}
}
private void dispatchCancellationSignal(@Nullable ICancellationSignal signal) {
if (signal == null) {
Slog.e(TAG, "Error dispatching a cancellation - Signal is null");
return;
}
try {
signal.cancel();
} catch (RemoteException e) {
Slog.e(TAG, "Error dispatching a cancellation", e);
}
}
}