/* * 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 android.companion; import android.annotation.FlaggedApi; import android.annotation.MainThread; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.TestApi; import android.app.Service; import android.bluetooth.BluetoothSocket; import android.content.Intent; import android.os.Handler; import android.os.IBinder; import android.util.Log; import java.io.InputStream; import java.io.OutputStream; import java.util.Objects; import java.util.concurrent.Executor; /** * A service that receives calls from the system with device events. * *
* Companion applications must create a service that {@code extends} * {@link CompanionDeviceService}, and declare it in their AndroidManifest.xml with the * "android.permission.BIND_COMPANION_DEVICE_SERVICE" permission * (see {@link android.Manifest.permission#BIND_COMPANION_DEVICE_SERVICE}), * as well as add an intent filter for the "android.companion.CompanionDeviceService" action * (see {@link #SERVICE_INTERFACE}). * *
* Following is an example of such declaration: *
{@code ** ** * }* **
* If the companion application has requested observing device presence (see * {@link CompanionDeviceManager#startObservingDevicePresence(String)}) the system will * bind the service * when it detects the device nearby (for BLE devices) or when the device is connected * (for Bluetooth devices). * *
* The system binding {@link CompanionDeviceService} elevates the priority of the process that * the service is running in, and thus may prevent * * the Low-memory killer from killing the process at expense of other processes with lower * priority. * *
* It is possible for an application to declare multiple {@link CompanionDeviceService}-s. * In such case, the system will bind all declared services, but will deliver * {@link #onDeviceAppeared(AssociationInfo)} and {@link #onDeviceDisappeared(AssociationInfo)} * only to one "primary" services. * Applications that declare multiple {@link CompanionDeviceService}-s should indicate the "primary" * service using "android.companion.PROPERTY_PRIMARY_COMPANION_DEVICE_SERVICE" service level * property. *
{@code ** ** }
* If the application declares multiple {@link CompanionDeviceService}-s, but does not indicate * the "primary" one, the system will pick one of the declared services to use as "primary". * *
* If the application declares multiple "primary" {@link CompanionDeviceService}-s, the system * will pick single one of them to use as "primary". */ public abstract class CompanionDeviceService extends Service { private static final String LOG_TAG = "CDM_CompanionDeviceService"; /** * An intent action for a service to be bound whenever this app's companion device(s) * are nearby. * *
The app will be kept alive for as long as the device is nearby or companion app reports * appeared. * If the app is not running at the time device gets connected, the app will be woken up.
* *Shortly after the device goes out of range or the companion app reports disappeared, * the service will be unbound, and the app will be eligible for cleanup, unless any other * user-visible components are running.
* * If running in background is not essential for the devices that this app can manage, * app should avoid declaring this service. * *The service must also require permission * {@link android.Manifest.permission#BIND_COMPANION_DEVICE_SERVICE}
*/ public static final String SERVICE_INTERFACE = "android.companion.CompanionDeviceService"; private final Stub mRemote = new Stub(); /** * Called by system whenever a device associated with this app is available. * * @param address the MAC address of the device * @deprecated please override {@link #onDeviceAppeared(AssociationInfo)} instead. */ @Deprecated @MainThread public void onDeviceAppeared(@NonNull String address) { // Do nothing. Companion apps can override this function. } /** * Called by system whenever a device associated with this app stops being available. * * Usually this means the device goes out of range or is turned off. * * @param address the MAC address of the device * @deprecated please override {@link #onDeviceDisappeared(AssociationInfo)} instead. */ @Deprecated @MainThread public void onDeviceDisappeared(@NonNull String address) { // Do nothing. Companion apps can override this function. } /** * Called by system whenever the system dispatches a message to the app to send it to * an associated device. * * @param messageId system assigned id of the message to be sent * @param associationId association id of the associated device * @param message message to be sent * @hide */ @Deprecated public void onMessageDispatchedFromSystem(int messageId, int associationId, @NonNull byte[] message) { Log.w(LOG_TAG, "Replaced by attachSystemDataTransport"); // do nothing. Companion apps can override this function for system to send messages. } /** * App calls this method when there's a message received from an associated device, * which needs to be dispatched to system for processing. * *Calling app must declare uses-permission * {@link android.Manifest.permission#DELIVER_COMPANION_MESSAGES}
* *You need to start the service before calling this method, otherwise the system can't * get the context and the dispatch would fail.
* *Note 1: messageId was assigned by the system, and sender should send the messageId along * with the message to the receiver. messageId will later be used for verification purpose. * Misusing the messageId will result in no action.
* *Note 2: associationId should be local to your device which is calling this API. It's not * the associationId on your remote device. If you don't have one, you can call * {@link CompanionDeviceManager#associate(AssociationRequest, Executor, * CompanionDeviceManager.Callback)} to create one. Misusing the associationId will result in * {@link DeviceNotAssociatedException}.
* * @param messageId id of the message * @param associationId id of the associated device * @param message message received from the associated device * @hide */ @Deprecated @RequiresPermission(android.Manifest.permission.DELIVER_COMPANION_MESSAGES) public final void dispatchMessageToSystem(int messageId, int associationId, @NonNull byte[] message) throws DeviceNotAssociatedException { Log.w(LOG_TAG, "Replaced by attachSystemDataTransport"); } /** * Attach the given bidirectional communication streams to be used for * transporting system data between associated devices. ** The companion service providing these streams is responsible for ensuring * that all data is transported accurately and in-order between the two * devices, including any fragmentation and re-assembly when carried over a * size-limited transport. *
* As an example, it's valid to provide streams obtained from a * {@link BluetoothSocket} to this method, since {@link BluetoothSocket} * meets the API contract described above. *
* This method passes through to * {@link CompanionDeviceManager#attachSystemDataTransport(int, InputStream, OutputStream)} * for your convenience if you get callbacks in this class. * * @param associationId id of the associated device * @param in already connected stream of data incoming from remote * associated device * @param out already connected stream of data outgoing to remote associated * device */ @RequiresPermission(android.Manifest.permission.DELIVER_COMPANION_MESSAGES) public final void attachSystemDataTransport(int associationId, @NonNull InputStream in, @NonNull OutputStream out) throws DeviceNotAssociatedException { getSystemService(CompanionDeviceManager.class) .attachSystemDataTransport(associationId, Objects.requireNonNull(in), Objects.requireNonNull(out)); } /** * Detach any bidirectional communication streams previously configured * through {@link #attachSystemDataTransport}. *
* This method passes through to * {@link CompanionDeviceManager#detachSystemDataTransport(int)} * for your convenience if you get callbacks in this class. * * @param associationId id of the associated device */ @RequiresPermission(android.Manifest.permission.DELIVER_COMPANION_MESSAGES) public final void detachSystemDataTransport(int associationId) throws DeviceNotAssociatedException { getSystemService(CompanionDeviceManager.class) .detachSystemDataTransport(associationId); } // TODO(b/315163162) Add @Deprecated keyword after 24Q2 cut. /** * Called by system whenever a device associated with this app is connected. * * @param associationInfo A record for the companion device. */ @MainThread public void onDeviceAppeared(@NonNull AssociationInfo associationInfo) { if (!associationInfo.isSelfManaged()) { onDeviceAppeared(associationInfo.getDeviceMacAddressAsString()); } } // TODO(b/315163162) Add @Deprecated keyword after 24Q2 cut. /** * Called by system whenever a device associated with this app is disconnected. * * @param associationInfo A record for the companion device. */ @MainThread public void onDeviceDisappeared(@NonNull AssociationInfo associationInfo) { if (!associationInfo.isSelfManaged()) { onDeviceDisappeared(associationInfo.getDeviceMacAddressAsString()); } } /** * Called by the system during device events. * * @see CompanionDeviceManager#startObservingDevicePresence(ObservingDevicePresenceRequest) */ @FlaggedApi(Flags.FLAG_DEVICE_PRESENCE) @MainThread public void onDevicePresenceEvent(@NonNull DevicePresenceEvent event) { // Do nothing. Companion apps can override this function. } @Nullable @Override public final IBinder onBind(@NonNull Intent intent) { if (Objects.equals(intent.getAction(), SERVICE_INTERFACE)) { onBindCompanionDeviceService(intent); return mRemote; } Log.w(LOG_TAG, "Tried to bind to wrong intent (should be " + SERVICE_INTERFACE + "): " + intent); return null; } /** * Used to track the state of Binder connection in CTS tests. * @hide */ @TestApi public void onBindCompanionDeviceService(@NonNull Intent intent) { } private class Stub extends ICompanionDeviceService.Stub { final Handler mMainHandler = Handler.getMain(); final CompanionDeviceService mService = CompanionDeviceService.this; @Override public void onDeviceAppeared(AssociationInfo associationInfo) { mMainHandler.postAtFrontOfQueue(() -> mService.onDeviceAppeared(associationInfo)); } @Override public void onDeviceDisappeared(AssociationInfo associationInfo) { mMainHandler.postAtFrontOfQueue(() -> mService.onDeviceDisappeared(associationInfo)); } @Override public void onDevicePresenceEvent(DevicePresenceEvent event) { if (Flags.devicePresence()) { mMainHandler.postAtFrontOfQueue(() -> mService.onDevicePresenceEvent(event)); } } } }