blob: a119cce54a16cb68df1bc4c8ef592b79e6cb228d [file] [log] [blame]
/*
* 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);
}
}
};
}