diff --git a/core/api/system-current.txt b/core/api/system-current.txt index bdef8c5f62101..fd8df8710da16 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -65,6 +65,7 @@ package android { field public static final String BIND_NOTIFICATION_ASSISTANT_SERVICE = "android.permission.BIND_NOTIFICATION_ASSISTANT_SERVICE"; field public static final String BIND_PHONE_ACCOUNT_SUGGESTION_SERVICE = "android.permission.BIND_PHONE_ACCOUNT_SUGGESTION_SERVICE"; field public static final String BIND_PRINT_RECOMMENDATION_SERVICE = "android.permission.BIND_PRINT_RECOMMENDATION_SERVICE"; + field public static final String BIND_REMOTE_LOCKSCREEN_VALIDATION_SERVICE = "android.permission.BIND_REMOTE_LOCKSCREEN_VALIDATION_SERVICE"; field public static final String BIND_RESOLVER_RANKER_SERVICE = "android.permission.BIND_RESOLVER_RANKER_SERVICE"; field public static final String BIND_RESUME_ON_REBOOT_SERVICE = "android.permission.BIND_RESUME_ON_REBOOT_SERVICE"; field public static final String BIND_ROTATION_RESOLVER_SERVICE = "android.permission.BIND_ROTATION_RESOLVER_SERVICE"; @@ -12565,6 +12566,17 @@ package android.service.quicksettings { } +package android.service.remotelockscreenvalidation { + + public abstract class RemoteLockscreenValidationService extends android.app.Service { + ctor public RemoteLockscreenValidationService(); + method @Nullable public final android.os.IBinder onBind(@NonNull android.content.Intent); + method public abstract void onValidateLockscreenGuess(@NonNull byte[], @NonNull android.os.OutcomeReceiver); + field public static final String SERVICE_INTERFACE = "android.service.remotelockscreenvalidation.RemoteLockscreenValidationService"; + } + +} + package android.service.resolver { public abstract class ResolverRankerService extends android.app.Service { diff --git a/core/java/android/service/remotelockscreenvalidation/IRemoteLockscreenValidationCallback.aidl b/core/java/android/service/remotelockscreenvalidation/IRemoteLockscreenValidationCallback.aidl new file mode 100644 index 0000000000000..fa4a75cce4b9e --- /dev/null +++ b/core/java/android/service/remotelockscreenvalidation/IRemoteLockscreenValidationCallback.aidl @@ -0,0 +1,12 @@ +package android.service.remotelockscreenvalidation; + +import android.app.RemoteLockscreenValidationResult; + +/** +* Callback interface for remote device lockscreen validation +* @hide +*/ +interface IRemoteLockscreenValidationCallback { + oneway void onSuccess(in RemoteLockscreenValidationResult result); + oneway void onFailure(in String message); +} diff --git a/core/java/android/service/remotelockscreenvalidation/IRemoteLockscreenValidationService.aidl b/core/java/android/service/remotelockscreenvalidation/IRemoteLockscreenValidationService.aidl new file mode 100644 index 0000000000000..530e5cef7899c --- /dev/null +++ b/core/java/android/service/remotelockscreenvalidation/IRemoteLockscreenValidationService.aidl @@ -0,0 +1,12 @@ +package android.service.remotelockscreenvalidation; + +import android.app.RemoteLockscreenValidationResult; +import android.service.remotelockscreenvalidation.IRemoteLockscreenValidationCallback; + +/** +* Interface used by the System to validate remote device lockscreen. +* {@hide} +*/ +interface IRemoteLockscreenValidationService { + void validateLockscreenGuess(in byte[] guess, in IRemoteLockscreenValidationCallback callback); +} diff --git a/core/java/android/service/remotelockscreenvalidation/OWNERS b/core/java/android/service/remotelockscreenvalidation/OWNERS new file mode 100644 index 0000000000000..cfa058538815c --- /dev/null +++ b/core/java/android/service/remotelockscreenvalidation/OWNERS @@ -0,0 +1,2 @@ +include /services/core/java/com/android/server/locksettings/recoverablekeystore/OWNERS +brnlee@google.com \ No newline at end of file diff --git a/core/java/android/service/remotelockscreenvalidation/RemoteLockscreenValidationClient.java b/core/java/android/service/remotelockscreenvalidation/RemoteLockscreenValidationClient.java new file mode 100644 index 0000000000000..e06b0cd4ba200 --- /dev/null +++ b/core/java/android/service/remotelockscreenvalidation/RemoteLockscreenValidationClient.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.service.remotelockscreenvalidation; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.ComponentName; +import android.content.Context; + +import java.util.concurrent.Executor; + +/** + * Client for {@link RemoteLockscreenValidationService} + * @hide + */ +public interface RemoteLockscreenValidationClient { + + /** + * Create a client for the {@link RemoteLockscreenValidationService} specified by the + * {@link ComponentName} + * @hide + */ + @NonNull + static RemoteLockscreenValidationClient create(@NonNull Context context, + @NonNull ComponentName serviceComponent) { + return new RemoteLockscreenValidationClientImpl( + context, + /* bgExecutor= */ null, + serviceComponent); + } + + /** + * Create a client for the {@link RemoteLockscreenValidationService} specified by the + * {@link ComponentName} + * @param context Context. + * @param bgExecutor A background {@link Executor} for service registration. + * @hide + */ + @NonNull + static RemoteLockscreenValidationClient create(@NonNull Context context, + @Nullable Executor bgExecutor, @NonNull ComponentName serviceComponent) { + return new RemoteLockscreenValidationClientImpl(context, bgExecutor, serviceComponent); + } + + /** + * Returns whether the {@link RemoteLockscreenValidationService} defined by the + * {@code ComponentName} provided in the constructor is available. + * + *

Calling API methods like {@link #validateLockscreenGuess} will fail if unavailable. + */ + boolean isServiceAvailable(); + + /** + * Unbinds from the {@link RemoteLockscreenValidationService} + */ + void disconnect(); + + /** + * Validates the lockscreen guess. + * + * @param guess lockscreen guess + * @param callback object used to relay the response of the guess validation + */ + void validateLockscreenGuess(byte[] guess, IRemoteLockscreenValidationCallback callback); +} diff --git a/core/java/android/service/remotelockscreenvalidation/RemoteLockscreenValidationClientImpl.java b/core/java/android/service/remotelockscreenvalidation/RemoteLockscreenValidationClientImpl.java new file mode 100644 index 0000000000000..140ef39f51c21 --- /dev/null +++ b/core/java/android/service/remotelockscreenvalidation/RemoteLockscreenValidationClientImpl.java @@ -0,0 +1,244 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.service.remotelockscreenvalidation; + +import static android.service.remotelockscreenvalidation.RemoteLockscreenValidationService.SERVICE_INTERFACE; + +import android.Manifest; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.content.pm.PackageManager; +import android.content.pm.ServiceInfo; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.RemoteException; +import android.text.TextUtils; +import android.util.Log; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Queue; +import java.util.concurrent.Executor; + +/** + * Implements {@link RemoteLockscreenValidationClient}. + * + * @hide + */ +public class RemoteLockscreenValidationClientImpl implements RemoteLockscreenValidationClient, + ServiceConnection { + + private static final String TAG = RemoteLockscreenValidationClientImpl.class.getSimpleName(); + private final Handler mHandler; + private final Context mContext; + private final Queue mRequestQueue; + private final Executor mLifecycleExecutor; + private final boolean mIsServiceAvailable; + private boolean mIsConnected; + + @Nullable + private IRemoteLockscreenValidationService mService; + + @Nullable + private ServiceInfo mServiceInfo; + + RemoteLockscreenValidationClientImpl( + @NonNull Context context, + @Nullable Executor bgExecutor, + @NonNull ComponentName serviceComponent) { + mContext = context.getApplicationContext(); + mIsServiceAvailable = isServiceAvailable(mContext, serviceComponent); + mHandler = new Handler(Looper.getMainLooper()); + mLifecycleExecutor = (bgExecutor == null) ? Runnable::run : bgExecutor; + mRequestQueue = new ArrayDeque<>(); + } + + @Override + public boolean isServiceAvailable() { + return mIsServiceAvailable; + } + + @Override + public void validateLockscreenGuess( + byte[] guess, IRemoteLockscreenValidationCallback callback) { + try { + if (!isServiceAvailable()) { + callback.onFailure("Service is not available"); + return; + } + } catch (RemoteException e) { + Log.e(TAG, "Error while failing for service unavailable", e); + } + + executeApiCall(new Call() { + @Override + public void exec(IRemoteLockscreenValidationService service) throws RemoteException { + service.validateLockscreenGuess(guess, callback); + } + + @Override + void onError(String msg) { + try { + callback.onFailure(msg); + } catch (RemoteException e) { + Log.e(TAG, "Error while failing validateLockscreenGuess", e); + } + } + }); + } + + @Override + public void disconnect() { + mHandler.post(this::disconnectInternal); + } + + private void disconnectInternal() { + if (!mIsConnected) { + Log.w(TAG, "already disconnected"); + return; + } + mIsConnected = false; + mLifecycleExecutor.execute(() -> mContext.unbindService(/* conn= */ this)); + mService = null; + mRequestQueue.clear(); + } + + private void connect() { + mHandler.post(this::connectInternal); + } + + private void connectInternal() { + if (mServiceInfo == null) { + Log.w(TAG, "RemoteLockscreenValidation service unavailable"); + return; + } + if (mIsConnected) { + return; + } + mIsConnected = true; + Intent intent = new Intent(SERVICE_INTERFACE); + intent.setComponent(mServiceInfo.getComponentName()); + int flags = Context.BIND_AUTO_CREATE | Context.BIND_WAIVE_PRIORITY; + mLifecycleExecutor.execute(() -> mContext.bindService(intent, this, flags)); + } + + private void onConnectedInternal(IRemoteLockscreenValidationService service) { + if (!mIsConnected) { + Log.w(TAG, "onConnectInternal but connection closed"); + mService = null; + return; + } + mService = service; + for (Call call : new ArrayList<>(mRequestQueue)) { + performApiCallInternal(call, mService); + mRequestQueue.remove(call); + } + } + + private boolean isServiceAvailable( + @NonNull Context context, + @NonNull ComponentName serviceComponent) { + mServiceInfo = getServiceInfo(context, serviceComponent); + if (mServiceInfo == null) { + return false; + } + + if (!Manifest.permission.BIND_REMOTE_LOCKSCREEN_VALIDATION_SERVICE.equals( + mServiceInfo.permission)) { + Log.w(TAG, TextUtils.formatSimple("%s/%s does not require permission %s", + mServiceInfo.packageName, mServiceInfo.name, + Manifest.permission.BIND_REMOTE_LOCKSCREEN_VALIDATION_SERVICE)); + return false; + } + return true; + } + + private ServiceInfo getServiceInfo( + @NonNull Context context, @NonNull ComponentName serviceComponent) { + try { + return context.getPackageManager().getServiceInfo(serviceComponent, + PackageManager.ComponentInfoFlags.of(PackageManager.GET_META_DATA)); + } catch (PackageManager.NameNotFoundException e) { + Log.w(TAG, TextUtils.formatSimple("Cannot resolve service %s", + serviceComponent.getClass().getName())); + return null; + } + } + + private void executeApiCall(Call call) { + mHandler.post(() -> executeInternal(call)); + } + + private void executeInternal(RemoteLockscreenValidationClientImpl.Call call) { + if (mIsConnected && mService != null) { + performApiCallInternal(call, mService); + } else { + mRequestQueue.add(call); + connect(); + } + } + + private void performApiCallInternal( + RemoteLockscreenValidationClientImpl.Call apiCaller, + IRemoteLockscreenValidationService service) { + if (service == null) { + apiCaller.onError("Service is null"); + return; + } + try { + apiCaller.exec(service); + } catch (RemoteException e) { + Log.w(TAG, "executeInternal error", e); + apiCaller.onError(e.getMessage()); + disconnect(); + } + } + + @Override // ServiceConnection + public void onServiceConnected(ComponentName name, IBinder binder) { + IRemoteLockscreenValidationService service = + IRemoteLockscreenValidationService.Stub.asInterface(binder); + mHandler.post(() -> onConnectedInternal(service)); + } + + @Override // ServiceConnection + public void onServiceDisconnected(ComponentName name) { + // Do not disconnect, as we may later be re-connected + } + + @Override // ServiceConnection + public void onBindingDied(ComponentName name) { + // This is a recoverable error but the client will need to reconnect. + disconnect(); + } + + @Override // ServiceConnection + public void onNullBinding(ComponentName name) { + disconnect(); + } + + private abstract static class Call { + abstract void exec(IRemoteLockscreenValidationService service) + throws RemoteException; + abstract void onError(String msg); + } +} diff --git a/core/java/android/service/remotelockscreenvalidation/RemoteLockscreenValidationService.java b/core/java/android/service/remotelockscreenvalidation/RemoteLockscreenValidationService.java new file mode 100644 index 0000000000000..9b588034cfdd9 --- /dev/null +++ b/core/java/android/service/remotelockscreenvalidation/RemoteLockscreenValidationService.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.service.remotelockscreenvalidation; + +import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SdkConstant; +import android.annotation.SystemApi; +import android.app.RemoteLockscreenValidationResult; +import android.app.Service; +import android.content.Intent; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.OutcomeReceiver; +import android.os.RemoteException; +import android.util.Log; + +/** + * Provides an interface to validate a remote device's lockscreen + * @hide + */ +@SystemApi +public abstract class RemoteLockscreenValidationService extends Service { + + /** + * The {@link Intent} that must be declared as handled by the service. To be supported, the + * service must also require the + * {@link android.Manifest.permission#BIND_REMOTE_LOCKSCREEN_VALIDATION_SERVICE} + * permission so that other applications can not abuse it. + */ + @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION) + public static final String SERVICE_INTERFACE = + "android.service.remotelockscreenvalidation.RemoteLockscreenValidationService"; + private static final String TAG = RemoteLockscreenValidationService.class.getSimpleName(); + + private final Handler mHandler = new Handler(Looper.getMainLooper()); + private final IRemoteLockscreenValidationService mInterface = + new IRemoteLockscreenValidationService.Stub() { + @Override + public void validateLockscreenGuess( + byte[] guess, IRemoteLockscreenValidationCallback callback) { + mHandler.sendMessage(obtainMessage( + RemoteLockscreenValidationService::onValidateLockscreenGuess, + RemoteLockscreenValidationService.this, guess, + new OutcomeReceiver() { + @Override + public void onResult(RemoteLockscreenValidationResult result) { + try { + callback.onSuccess(result); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + } + @Override + public void onError(Exception e) { + try { + callback.onFailure(e.getMessage()); + } catch (RemoteException ex) { + ex.rethrowFromSystemServer(); + } + } + } + )); + } + }; + + @Override + @Nullable + public final IBinder onBind(@NonNull Intent intent) { + if (!SERVICE_INTERFACE.equals(intent.getAction())) { + Log.w(TAG, "Wrong action"); + return null; + } + return mInterface.asBinder(); + } + + /** + * Validates the lockscreen guess. + * + *

Implementation should send guess to remote device and perform lockscreen validation + * using {@link android.app.KeyguardManager#validateRemoteLockScreen}. + * + * @param guess lockscreen guess + * @param callback object used to relay the response of the guess validation + */ + public abstract void onValidateLockscreenGuess(@NonNull byte[] guess, + @NonNull OutcomeReceiver callback); +} diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 64c048223b243..f93739005472d 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -5000,6 +5000,15 @@ + + +