| /* |
| * Copyright (C) 2020 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.google.android.connecteddevice.api; |
| |
| import static com.google.android.connecteddevice.util.SafeLog.logd; |
| import static com.google.android.connecteddevice.util.SafeLog.loge; |
| import static com.google.android.connecteddevice.util.SafeLog.logw; |
| |
| import android.content.ComponentName; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.ServiceConnection; |
| import android.os.Handler; |
| import android.os.IBinder; |
| import android.os.Looper; |
| import android.os.ParcelUuid; |
| import android.os.RemoteException; |
| import androidx.annotation.CallSuper; |
| import androidx.annotation.NonNull; |
| import androidx.annotation.Nullable; |
| import com.google.android.connecteddevice.model.AssociatedDevice; |
| import com.google.android.connecteddevice.model.ConnectedDevice; |
| import com.google.android.connecteddevice.util.Logger; |
| import java.util.List; |
| |
| /** |
| * Base class for a feature that must bind to {@code ConnectedDeviceService}. Callbacks are |
| * registered automatically and events are forwarded to internal methods. Override these to add |
| * custom logic for callback triggers. |
| */ |
| public abstract class RemoteFeature { |
| |
| private static final String TAG = "RemoteFeature"; |
| |
| private static final String FULLY_QUALIFIED_SERVICE_NAME = |
| "com.google.android.connecteddevice.service.ConnectedDeviceService"; |
| |
| /** |
| * When a client calls {@link Context#bindService(Intent, ServiceConnection, int)} to get the |
| * IConnectedDeviceManager, this action is required in the param {@link Intent}. |
| */ |
| public static final String ACTION_BIND_REMOTE_FEATURE = |
| "com.google.android.connecteddevice.api.BIND_REMOTE_FEATURE"; |
| |
| /** Intent action used to request a device be associated. */ |
| public static final String ACTION_ASSOCIATION_SETTING = |
| "com.google.android.connecteddevice.api.ASSOCIATION_ACTIVITY"; |
| |
| /** Data name for associated device. */ |
| public static final String ASSOCIATED_DEVICE_DATA_NAME_EXTRA = |
| "com.google.android.connecteddevice.api.ASSOCIATED_DEVICE"; |
| |
| private static final long BIND_RETRY_DURATION_MS = 1000; |
| |
| private static final int MAX_BIND_ATTEMPTS = 3; |
| |
| private final Context context; |
| |
| private final ParcelUuid featureId; |
| |
| private IConnectedDeviceManager connectedDeviceManager; |
| |
| private int bindAttempts; |
| |
| protected RemoteFeature(@NonNull Context context, @NonNull ParcelUuid featureId) { |
| this.context = context; |
| this.featureId = featureId; |
| } |
| |
| /** Start setup process and begin binding to {@code ConnectedDeviceService}. */ |
| @CallSuper |
| public void start() { |
| bindAttempts = 0; |
| bindToService(); |
| } |
| |
| /** Called when the hosting service is being destroyed. Cleans up internal feature logic. */ |
| @CallSuper |
| public void stop() { |
| if (connectedDeviceManager == null) { |
| return; |
| } |
| try { |
| connectedDeviceManager.unregisterConnectionCallback(connectionCallback); |
| for (ConnectedDevice device : connectedDeviceManager.getActiveUserConnectedDevices()) { |
| connectedDeviceManager.unregisterDeviceCallback(device, featureId, deviceCallback); |
| } |
| connectedDeviceManager.unregisterDeviceAssociationCallback(deviceAssociationCallback); |
| connectedDeviceManager.unregisterOnLogRequestedListener( |
| Logger.getLogger().getLoggerId(), onLogRequestedListener); |
| } catch (RemoteException e) { |
| loge(TAG, "Error while stopping remote feature.", e); |
| } |
| context.unbindService(serviceConnection); |
| } |
| |
| /** Return the {@link Context} registered with the feature. */ |
| @NonNull |
| public Context getContext() { |
| return context; |
| } |
| |
| /** |
| * Return the {@link IConnectedDeviceManager} bound with the feature. Returns {@code null} if |
| * binding has not completed yet. |
| */ |
| @Nullable |
| public IConnectedDeviceManager getConnectedDeviceManager() { |
| return connectedDeviceManager; |
| } |
| |
| /** Return the {@link ParcelUuid} feature id registered for the feature. */ |
| @NonNull |
| public ParcelUuid getFeatureId() { |
| return featureId; |
| } |
| |
| /** Securely send message to a device. */ |
| public void sendMessageSecurely(@NonNull String deviceId, @NonNull byte[] message) { |
| if (connectedDeviceManager == null) { |
| loge(TAG, "Unable to send message, ConnectedDeviceManager not actively connected."); |
| return; |
| } |
| ConnectedDevice device = getConnectedDeviceById(deviceId); |
| if (device == null) { |
| loge( |
| TAG, |
| "No matching device found with id " |
| + deviceId |
| + " when trying to send " |
| + "secure message."); |
| onMessageFailedToSend(deviceId, message, false); |
| return; |
| } |
| |
| sendMessageSecurely(device, message); |
| } |
| |
| /** Securely send message to a device. */ |
| public void sendMessageSecurely(@NonNull ConnectedDevice device, @NonNull byte[] message) { |
| if (connectedDeviceManager == null) { |
| loge(TAG, "Unable to send message, ConnectedDeviceManager not actively connected."); |
| return; |
| } |
| try { |
| getConnectedDeviceManager().sendMessageSecurely(device, getFeatureId(), message); |
| } catch (RemoteException e) { |
| loge(TAG, "Error while sending secure message.", e); |
| onMessageFailedToSend(device.getDeviceId(), message, true); |
| } |
| } |
| |
| /** Send a message to a device without encryption. */ |
| public void sendMessageUnsecurely(@NonNull String deviceId, @NonNull byte[] message) { |
| if (connectedDeviceManager == null) { |
| loge(TAG, "Unable to send message, ConnectedDeviceManager not actively connected."); |
| return; |
| } |
| ConnectedDevice device = getConnectedDeviceById(deviceId); |
| if (device == null) { |
| loge( |
| TAG, |
| "No matching device found with id " |
| + deviceId |
| + " when trying to send " |
| + "unsecure message."); |
| onMessageFailedToSend(deviceId, message, false); |
| return; |
| } |
| } |
| |
| /** Send a message to a device without encryption. */ |
| public void sendMessageUnsecurely(@NonNull ConnectedDevice device, @NonNull byte[] message) { |
| if (connectedDeviceManager == null) { |
| loge(TAG, "Unable to send message, ConnectedDeviceManager not actively connected."); |
| return; |
| } |
| try { |
| getConnectedDeviceManager().sendMessageUnsecurely(device, getFeatureId(), message); |
| } catch (RemoteException e) { |
| loge(TAG, "Error while sending unsecure message.", e); |
| onMessageFailedToSend(device.getDeviceId(), message, true); |
| } |
| } |
| |
| /** |
| * Return the {@link ConnectedDevice} with a matching device id for the currently active user. |
| * Returns {@code null} if no match found. |
| */ |
| @Nullable |
| public ConnectedDevice getConnectedDeviceById(@NonNull String deviceId) { |
| if (connectedDeviceManager == null) { |
| loge( |
| TAG, |
| "Unable to get connected device. ConnectedDeviceManager not actively " + "connected."); |
| return null; |
| } |
| List<ConnectedDevice> connectedDevices; |
| try { |
| connectedDevices = getConnectedDeviceManager().getActiveUserConnectedDevices(); |
| } catch (RemoteException e) { |
| loge(TAG, "Exception while retrieving connected devices.", e); |
| return null; |
| } |
| |
| for (ConnectedDevice device : connectedDevices) { |
| if (device.getDeviceId().equals(deviceId)) { |
| return device; |
| } |
| } |
| |
| return null; |
| } |
| |
| // These can be overridden to perform custom actions. |
| |
| /** Called when a new {@link ConnectedDevice} is connected. */ |
| protected void onDeviceConnected(@NonNull ConnectedDevice device) {} |
| |
| /** Called when a {@link ConnectedDevice} disconnects. */ |
| protected void onDeviceDisconnected(@NonNull ConnectedDevice device) {} |
| |
| /** Called when a secure channel has been established with a {@link ConnectedDevice}. */ |
| protected void onSecureChannelEstablished(@NonNull ConnectedDevice device) {} |
| |
| /** |
| * Called when a message fails to send to a device. |
| * |
| * @param deviceId Id of the device the message failed to send to. |
| * @param message Message to send. |
| * @param isTransient {@code true} if cause of failure is transient and can be retried. {@code |
| * false} if failure is permanent. |
| */ |
| protected void onMessageFailedToSend( |
| @NonNull String deviceId, @NonNull byte[] message, boolean isTransient) {} |
| |
| /** Called when a new {@link byte[]} message is received for this feature. */ |
| protected void onMessageReceived(@NonNull ConnectedDevice device, @NonNull byte[] message) {} |
| |
| /** Called when an error has occurred with the connection. */ |
| protected void onDeviceError(@NonNull ConnectedDevice device, int error) {} |
| |
| /** Called when a new {@link AssociatedDevice} is added for the given user. */ |
| protected void onAssociatedDeviceAdded(@NonNull AssociatedDevice device) {} |
| |
| /** Called when an {@link AssociatedDevice} is removed for the given user. */ |
| protected void onAssociatedDeviceRemoved(@NonNull AssociatedDevice device) {} |
| |
| /** Called when an {@link AssociatedDevice} is updated for the given user. */ |
| protected void onAssociatedDeviceUpdated(@NonNull AssociatedDevice device) {} |
| |
| private void bindToService() { |
| String packageName = context.getApplicationContext().getPackageName(); |
| Intent intent = new Intent(); |
| intent.setComponent(new ComponentName(packageName, FULLY_QUALIFIED_SERVICE_NAME)); |
| intent.setAction(ACTION_BIND_REMOTE_FEATURE); |
| boolean success = context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE); |
| if (!success) { |
| bindAttempts++; |
| if (bindAttempts > MAX_BIND_ATTEMPTS) { |
| loge( |
| TAG, |
| "Failed to bind to ConnectedDeviceService after " |
| + bindAttempts |
| + " attempts. Aborting."); |
| return; |
| } |
| logw(TAG, "Unable to bind to ConnectedDeviceService. Trying again."); |
| new Handler(Looper.getMainLooper()).postDelayed(this::bindToService, BIND_RETRY_DURATION_MS); |
| } |
| } |
| |
| private final ServiceConnection serviceConnection = |
| new ServiceConnection() { |
| @Override |
| public void onServiceConnected(ComponentName name, IBinder service) { |
| connectedDeviceManager = IConnectedDeviceManager.Stub.asInterface(service); |
| try { |
| connectedDeviceManager.registerActiveUserConnectionCallback(connectionCallback); |
| connectedDeviceManager.registerDeviceAssociationCallback(deviceAssociationCallback); |
| connectedDeviceManager.registerOnLogRequestedListener( |
| Logger.getLogger().getLoggerId(), onLogRequestedListener); |
| logd(TAG, "Successfully bound to ConnectedDeviceManager."); |
| List<ConnectedDevice> activeUserConnectedDevices = |
| connectedDeviceManager.getActiveUserConnectedDevices(); |
| for (ConnectedDevice device : activeUserConnectedDevices) { |
| if (device.hasSecureChannel()) { |
| onSecureChannelEstablished(device); |
| } |
| connectedDeviceManager.registerDeviceCallback(device, featureId, deviceCallback); |
| } |
| } catch (RemoteException e) { |
| loge(TAG, "Error while inspecting connected devices.", e); |
| } |
| } |
| |
| @Override |
| public void onServiceDisconnected(ComponentName name) { |
| logd(TAG, "Disconnected from ConnectedDeviceManager."); |
| connectedDeviceManager = null; |
| stop(); |
| } |
| }; |
| |
| private final IConnectionCallback connectionCallback = |
| new IConnectionCallback.Stub() { |
| @Override |
| public void onDeviceConnected(ConnectedDevice connectedDevice) throws RemoteException { |
| connectedDeviceManager.registerDeviceCallback( |
| connectedDevice, featureId, deviceCallback); |
| RemoteFeature.this.onDeviceConnected(connectedDevice); |
| } |
| |
| @Override |
| public void onDeviceDisconnected(ConnectedDevice connectedDevice) throws RemoteException { |
| connectedDeviceManager.unregisterDeviceCallback( |
| connectedDevice, featureId, deviceCallback); |
| RemoteFeature.this.onDeviceDisconnected(connectedDevice); |
| } |
| }; |
| |
| private final IDeviceCallback deviceCallback = |
| new IDeviceCallback.Stub() { |
| @Override |
| public void onSecureChannelEstablished(ConnectedDevice connectedDevice) { |
| RemoteFeature.this.onSecureChannelEstablished(connectedDevice); |
| } |
| |
| @Override |
| public void onMessageReceived(ConnectedDevice connectedDevice, byte[] message) { |
| RemoteFeature.this.onMessageReceived(connectedDevice, message); |
| } |
| |
| @Override |
| public void onDeviceError(ConnectedDevice connectedDevice, int error) { |
| RemoteFeature.this.onDeviceError(connectedDevice, error); |
| } |
| }; |
| |
| private final IDeviceAssociationCallback deviceAssociationCallback = |
| new IDeviceAssociationCallback.Stub() { |
| @Override |
| public void onAssociatedDeviceAdded(AssociatedDevice device) { |
| RemoteFeature.this.onAssociatedDeviceAdded(device); |
| } |
| |
| @Override |
| public void onAssociatedDeviceRemoved(AssociatedDevice device) { |
| RemoteFeature.this.onAssociatedDeviceRemoved(device); |
| } |
| |
| @Override |
| public void onAssociatedDeviceUpdated(AssociatedDevice device) { |
| RemoteFeature.this.onAssociatedDeviceUpdated(device); |
| } |
| }; |
| |
| private final IOnLogRequestedListener onLogRequestedListener = |
| new IOnLogRequestedListener.Stub() { |
| @Override |
| public void onLogRecordsRequested() { |
| Logger logger = Logger.getLogger(); |
| try { |
| connectedDeviceManager.processLogRecords(logger.getLoggerId(), logger.toByteArray()); |
| } catch (RemoteException exception) { |
| loge(TAG, "Failed to send log records for logger" + logger + ".", exception); |
| } |
| } |
| }; |
| } |