1 /*
2  * Copyright 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.remoteauth;
18 
19 import static android.remoteauth.DeviceDiscoveryCallback.STATE_LOST;
20 import static android.remoteauth.DeviceDiscoveryCallback.STATE_SEEN;
21 
22 import android.annotation.CallbackExecutor;
23 import android.annotation.NonNull;
24 import android.annotation.SuppressLint;
25 import android.annotation.SystemService;
26 import android.annotation.UserIdInt;
27 import android.content.Context;
28 import android.os.RemoteException;
29 import android.util.Log;
30 
31 import com.android.internal.annotations.GuardedBy;
32 import com.android.internal.util.Preconditions;
33 
34 import java.lang.ref.WeakReference;
35 import java.util.Objects;
36 import java.util.WeakHashMap;
37 import java.util.concurrent.Executor;
38 
39 /**
40  * A system service providing a way to perform remote authentication-related operations such as
41  * discovering, registering and authenticating via remote authenticator.
42  *
43  * <p>To get a {@link RemoteAuthManager} instance, call the <code>
44  * Context.getSystemService(Context.REMOTE_AUTH_SERVICE)</code>.
45  *
46  * @hide
47  */
48 // TODO(b/290092977): Add back after M-2023-11 release - @SystemApi(client = MODULE_LIBRARIES)
49 // TODO(b/290092977): Change to Context.REMOTE_AUTH_SERVICE after aosp/2681375
50 // is automerges from aosp-main to udc-mainline-prod
51 @SystemService(RemoteAuthManager.REMOTE_AUTH_SERVICE)
52 public class RemoteAuthManager {
53     private static final String TAG = "RemoteAuthManager";
54 
55     /** @hide */
56     public static final String REMOTE_AUTH_SERVICE = "remote_auth";
57 
58     private final Context mContext;
59     private final IRemoteAuthService mService;
60 
61     @GuardedBy("mDiscoveryListeners")
62     private final WeakHashMap<
63                     DeviceDiscoveryCallback, WeakReference<DeviceDiscoveryListenerTransport>>
64             mDiscoveryListeners = new WeakHashMap<>();
65 
66     /** @hide */
RemoteAuthManager(@onNull Context context, @NonNull IRemoteAuthService service)67     public RemoteAuthManager(@NonNull Context context, @NonNull IRemoteAuthService service) {
68         Objects.requireNonNull(context);
69         Objects.requireNonNull(service);
70         mContext = context;
71         mService = service;
72     }
73 
74     /**
75      * Returns if this device can be enrolled in the feature.
76      *
77      * @return true if this device can be enrolled
78      * @hide
79      */
80     // TODO(b/290092977): Add back after M-2023-11 release - @SystemApi(client = MODULE_LIBRARIES)
81     // TODO(b/297301535): @RequiresPermission(MANAGE_REMOTE_AUTH)
isRemoteAuthSupported()82     public boolean isRemoteAuthSupported() {
83         try {
84             return mService.isRemoteAuthSupported();
85         } catch (RemoteException e) {
86             throw e.rethrowFromSystemServer();
87         }
88     }
89 
90     /**
91      * Starts remote authenticator discovery process with timeout. Devices that are capable to
92      * operate as remote authenticators are reported via callback. The discovery stops by calling
93      * stopDiscovery or after a timeout.
94      *
95      * @param timeoutMs the duration in milliseconds after which discovery will stop automatically
96      * @param executor the callback will be executed in the executor thread
97      * @param callback to be used by the caller to get notifications about remote devices
98      * @return {@code true} if discovery began successfully, {@code false} otherwise
99      * @hide
100      */
101     // TODO(b/290092977): Add back after M-2023-11 release - @SystemApi(client = MODULE_LIBRARIES)
102     // TODO(b/297301535): @RequiresPermission(MANAGE_REMOTE_AUTH)
startDiscovery( int timeoutMs, @CallbackExecutor @NonNull Executor executor, @NonNull DeviceDiscoveryCallback callback)103     public boolean startDiscovery(
104             int timeoutMs,
105             @CallbackExecutor @NonNull Executor executor,
106             @NonNull DeviceDiscoveryCallback callback) {
107         try {
108             Preconditions.checkNotNull(callback, "invalid null callback");
109             Preconditions.checkArgument(timeoutMs > 0, "invalid timeoutMs, must be > 0");
110             Preconditions.checkNotNull(executor, "invalid null executor");
111             DeviceDiscoveryListenerTransport transport;
112             synchronized (mDiscoveryListeners) {
113                 WeakReference<DeviceDiscoveryListenerTransport> reference =
114                         mDiscoveryListeners.get(callback);
115                 transport = (reference != null) ? reference.get() : null;
116                 if (transport == null) {
117                     transport =
118                             new DeviceDiscoveryListenerTransport(
119                                     callback, mContext.getUser().getIdentifier(), executor);
120                 }
121 
122                 boolean result =
123                         mService.registerDiscoveryListener(
124                                 transport,
125                                 mContext.getUser().getIdentifier(),
126                                 timeoutMs,
127                                 mContext.getPackageName(),
128                                 mContext.getAttributionTag());
129                 if (result) {
130                     mDiscoveryListeners.put(callback, new WeakReference<>(transport));
131                     return true;
132                 }
133             }
134         } catch (RemoteException e) {
135             throw e.rethrowFromSystemServer();
136         }
137         return false;
138     }
139 
140     /**
141      * Removes this listener from device discovery notifications. The given callback is guaranteed
142      * not to receive any invocations that happen after this method is invoked.
143      *
144      * @param callback the callback for the previously started discovery to be ended
145      * @hide
146      */
147     // Suppressed lint: Registration methods should have overload that accepts delivery Executor.
148     // Already have executor in startDiscovery() method.
149     @SuppressLint("ExecutorRegistration")
150     // TODO(b/290092977): Add back after M-2023-11 release - @SystemApi(client = MODULE_LIBRARIES)
151     // TODO(b/297301535): @RequiresPermission(MANAGE_REMOTE_AUTH)
stopDiscovery(@onNull DeviceDiscoveryCallback callback)152     public void stopDiscovery(@NonNull DeviceDiscoveryCallback callback) {
153         Preconditions.checkNotNull(callback, "invalid null scanCallback");
154         try {
155             DeviceDiscoveryListenerTransport transport;
156             synchronized (mDiscoveryListeners) {
157                 WeakReference<DeviceDiscoveryListenerTransport> reference =
158                         mDiscoveryListeners.remove(callback);
159                 transport = (reference != null) ? reference.get() : null;
160             }
161             if (transport != null) {
162                 mService.unregisterDiscoveryListener(
163                         transport,
164                         transport.getUserId(),
165                         mContext.getPackageName(),
166                         mContext.getAttributionTag());
167             } else {
168                 Log.d(
169                         TAG,
170                         "Cannot stop discovery with this callback "
171                                 + "because it is not registered.");
172             }
173         } catch (RemoteException e) {
174             throw e.rethrowFromSystemServer();
175         }
176     }
177 
178     private class DeviceDiscoveryListenerTransport extends IDeviceDiscoveryListener.Stub {
179 
180         private volatile @NonNull DeviceDiscoveryCallback mDeviceDiscoveryCallback;
181         private Executor mExecutor;
182         private @UserIdInt int mUserId;
183 
DeviceDiscoveryListenerTransport( DeviceDiscoveryCallback deviceDiscoveryCallback, @UserIdInt int userId, @CallbackExecutor Executor executor)184         DeviceDiscoveryListenerTransport(
185                 DeviceDiscoveryCallback deviceDiscoveryCallback,
186                 @UserIdInt int userId,
187                 @CallbackExecutor Executor executor) {
188             Preconditions.checkNotNull(deviceDiscoveryCallback, "invalid null callback");
189             mDeviceDiscoveryCallback = deviceDiscoveryCallback;
190             mUserId = userId;
191             mExecutor = executor;
192         }
193 
194         @UserIdInt
getUserId()195         int getUserId() {
196             return mUserId;
197         }
198 
199         @Override
onDiscovered(RemoteDevice remoteDevice)200         public void onDiscovered(RemoteDevice remoteDevice) throws RemoteException {
201             if (remoteDevice == null) {
202                 Log.w(TAG, "onDiscovered is called with null device");
203                 return;
204             }
205             Log.i(TAG, "Notifying the caller about discovered: " + remoteDevice);
206             mExecutor.execute(
207                     () -> {
208                         mDeviceDiscoveryCallback.onDeviceUpdate(remoteDevice, STATE_SEEN);
209                     });
210         }
211 
212         @Override
onLost(RemoteDevice remoteDevice)213         public void onLost(RemoteDevice remoteDevice) throws RemoteException {
214             if (remoteDevice == null) {
215                 Log.w(TAG, "onLost is called with null device");
216                 return;
217             }
218             Log.i(TAG, "Notifying the caller about lost: " + remoteDevice);
219             mExecutor.execute(
220                     () -> {
221                         mDeviceDiscoveryCallback.onDeviceUpdate(remoteDevice, STATE_LOST);
222                     });
223         }
224 
225         @Override
onTimeout()226         public void onTimeout() {
227             Log.i(TAG, "Notifying the caller about discovery timeout");
228             mExecutor.execute(
229                     () -> {
230                         mDeviceDiscoveryCallback.onTimeout();
231                     });
232             synchronized (mDiscoveryListeners) {
233                 mDiscoveryListeners.remove(mDeviceDiscoveryCallback);
234             }
235             mDeviceDiscoveryCallback = null;
236         }
237     }
238 }
239