1 /*
2  * Copyright (C) 2020 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 
18 package android.companion;
19 
20 import android.annotation.MainThread;
21 import android.annotation.NonNull;
22 import android.annotation.Nullable;
23 import android.annotation.RequiresPermission;
24 import android.annotation.TestApi;
25 import android.app.Service;
26 import android.bluetooth.BluetoothSocket;
27 import android.content.Intent;
28 import android.os.Handler;
29 import android.os.IBinder;
30 import android.util.Log;
31 
32 import java.io.InputStream;
33 import java.io.OutputStream;
34 import java.util.Objects;
35 import java.util.concurrent.Executor;
36 
37 /**
38  * A service that receives calls from the system when the associated companion device appears
39  * nearby or is connected, as well as when the device is no longer "present" or connected.
40  * See {@link #onDeviceAppeared(AssociationInfo)}/{@link #onDeviceDisappeared(AssociationInfo)}.
41  *
42  * <p>
43  * Companion applications must create a service that {@code extends}
44  * {@link CompanionDeviceService}, and declare it in their AndroidManifest.xml with the
45  * "android.permission.BIND_COMPANION_DEVICE_SERVICE" permission
46  * (see {@link android.Manifest.permission#BIND_COMPANION_DEVICE_SERVICE}),
47  * as well as add an intent filter for the "android.companion.CompanionDeviceService" action
48  * (see {@link #SERVICE_INTERFACE}).
49  *
50  * <p>
51  * Following is an example of such declaration:
52  * <pre>{@code
53  * <service
54  *        android:name=".CompanionService"
55  *        android:label="@string/service_name"
56  *        android:exported="true"
57  *        android:permission="android.permission.BIND_COMPANION_DEVICE_SERVICE">
58  *    <intent-filter>
59  *        <action android:name="android.companion.CompanionDeviceService" />
60  *    </intent-filter>
61  * </service>
62  * }</pre>
63  *
64  * <p>
65  * If the companion application has requested observing device presence (see
66  * {@link CompanionDeviceManager#startObservingDevicePresence(String)}) the system will
67  * <a href="https://developer.android.com/guide/components/bound-services"> bind the service</a>
68  * when it detects the device nearby (for BLE devices) or when the device is connected
69  * (for Bluetooth devices).
70  *
71  * <p>
72  * The system binding {@link CompanionDeviceService} elevates the priority of the process that
73  * the service is running in, and thus may prevent
74  * <a href="https://developer.android.com/topic/performance/memory-management#low-memory_killer">
75  * the Low-memory killer</a> from killing the process at expense of other processes with lower
76  * priority.
77  *
78  * <p>
79  * It is possible for an application to declare multiple {@link CompanionDeviceService}-s.
80  * In such case, the system will bind all declared services, but will deliver
81  * {@link #onDeviceAppeared(AssociationInfo)} and {@link #onDeviceDisappeared(AssociationInfo)}
82  * only to one "primary" services.
83  * Applications that declare multiple {@link CompanionDeviceService}-s should indicate the "primary"
84  * service using "android.companion.PROPERTY_PRIMARY_COMPANION_DEVICE_SERVICE" service level
85  * property.
86  * <pre>{@code
87  * <property
88  *       android:name="android.companion.PROPERTY_PRIMARY_COMPANION_DEVICE_SERVICE"
89  *       android:value="true" />
90  * }</pre>
91  *
92  * <p>
93  * If the application declares multiple {@link CompanionDeviceService}-s, but does not indicate
94  * the "primary" one, the system will pick one of the declared services to use as "primary".
95  *
96  * <p>
97  * If the application declares multiple "primary" {@link CompanionDeviceService}-s, the system
98  * will pick single one of them to use as "primary".
99  */
100 public abstract class CompanionDeviceService extends Service {
101 
102     private static final String LOG_TAG = "CDM_CompanionDeviceService";
103 
104     /**
105      * An intent action for a service to be bound whenever this app's companion device(s)
106      * are nearby.
107      *
108      * <p>The app will be kept alive for as long as the device is nearby or companion app reports
109      * appeared.
110      * If the app is not running at the time device gets connected, the app will be woken up.</p>
111      *
112      * <p>Shortly after the device goes out of range or the companion app reports disappeared,
113      * the service will be unbound, and the app will be eligible for cleanup, unless any other
114      * user-visible components are running.</p>
115      *
116      * If running in background is not essential for the devices that this app can manage,
117      * app should avoid declaring this service.</p>
118      *
119      * <p>The service must also require permission
120      * {@link android.Manifest.permission#BIND_COMPANION_DEVICE_SERVICE}</p>
121      */
122     public static final String SERVICE_INTERFACE = "android.companion.CompanionDeviceService";
123 
124     private final Stub mRemote = new Stub();
125 
126     /**
127      * Called by system whenever a device associated with this app is available.
128      *
129      * @param address the MAC address of the device
130      * @deprecated please override {@link #onDeviceAppeared(AssociationInfo)} instead.
131      */
132     @Deprecated
133     @MainThread
onDeviceAppeared(@onNull String address)134     public void onDeviceAppeared(@NonNull String address) {
135         // Do nothing. Companion apps can override this function.
136     }
137 
138     /**
139      * Called by system whenever a device associated with this app stops being available.
140      *
141      * Usually this means the device goes out of range or is turned off.
142      *
143      * @param address the MAC address of the device
144      * @deprecated please override {@link #onDeviceDisappeared(AssociationInfo)} instead.
145      */
146     @Deprecated
147     @MainThread
onDeviceDisappeared(@onNull String address)148     public void onDeviceDisappeared(@NonNull String address) {
149         // Do nothing. Companion apps can override this function.
150     }
151 
152     /**
153      * Called by system whenever the system dispatches a message to the app to send it to
154      * an associated device.
155      *
156      * @param messageId system assigned id of the message to be sent
157      * @param associationId association id of the associated device
158      * @param message message to be sent
159      * @hide
160      */
161     @Deprecated
onMessageDispatchedFromSystem(int messageId, int associationId, @NonNull byte[] message)162     public void onMessageDispatchedFromSystem(int messageId, int associationId,
163             @NonNull byte[] message) {
164         Log.w(LOG_TAG, "Replaced by attachSystemDataTransport");
165         // do nothing. Companion apps can override this function for system to send messages.
166     }
167 
168     /**
169      * App calls this method when there's a message received from an associated device,
170      * which needs to be dispatched to system for processing.
171      *
172      * <p>Calling app must declare uses-permission
173      * {@link android.Manifest.permission#DELIVER_COMPANION_MESSAGES}</p>
174      *
175      * <p>You need to start the service before calling this method, otherwise the system can't
176      * get the context and the dispatch would fail.</p>
177      *
178      * <p>Note 1: messageId was assigned by the system, and sender should send the messageId along
179      * with the message to the receiver. messageId will later be used for verification purpose.
180      * Misusing the messageId will result in no action.</p>
181      *
182      * <p>Note 2: associationId should be local to your device which is calling this API. It's not
183      * the associationId on your remote device. If you don't have one, you can call
184      * {@link CompanionDeviceManager#associate(AssociationRequest, Executor,
185      * CompanionDeviceManager.Callback)} to create one. Misusing the associationId will result in
186      * {@link DeviceNotAssociatedException}.</p>
187      *
188      * @param messageId id of the message
189      * @param associationId id of the associated device
190      * @param message message received from the associated device
191      * @hide
192      */
193     @Deprecated
194     @RequiresPermission(android.Manifest.permission.DELIVER_COMPANION_MESSAGES)
dispatchMessageToSystem(int messageId, int associationId, @NonNull byte[] message)195     public final void dispatchMessageToSystem(int messageId, int associationId,
196             @NonNull byte[] message)
197             throws DeviceNotAssociatedException {
198         Log.w(LOG_TAG, "Replaced by attachSystemDataTransport");
199     }
200 
201     /**
202      * Attach the given bidirectional communication streams to be used for
203      * transporting system data between associated devices.
204      * <p>
205      * The companion service providing these streams is responsible for ensuring
206      * that all data is transported accurately and in-order between the two
207      * devices, including any fragmentation and re-assembly when carried over a
208      * size-limited transport.
209      * <p>
210      * As an example, it's valid to provide streams obtained from a
211      * {@link BluetoothSocket} to this method, since {@link BluetoothSocket}
212      * meets the API contract described above.
213      * <p>
214      * This method passes through to
215      * {@link CompanionDeviceManager#attachSystemDataTransport(int, InputStream, OutputStream)}
216      * for your convenience if you get callbacks in this class.
217      *
218      * @param associationId id of the associated device
219      * @param in already connected stream of data incoming from remote
220      *            associated device
221      * @param out already connected stream of data outgoing to remote associated
222      *            device
223      */
224     @RequiresPermission(android.Manifest.permission.DELIVER_COMPANION_MESSAGES)
attachSystemDataTransport(int associationId, @NonNull InputStream in, @NonNull OutputStream out)225     public final void attachSystemDataTransport(int associationId, @NonNull InputStream in,
226             @NonNull OutputStream out) throws DeviceNotAssociatedException {
227         getSystemService(CompanionDeviceManager.class)
228                 .attachSystemDataTransport(associationId,
229                         Objects.requireNonNull(in),
230                         Objects.requireNonNull(out));
231     }
232 
233     /**
234      * Detach any bidirectional communication streams previously configured
235      * through {@link #attachSystemDataTransport}.
236      * <p>
237      * This method passes through to
238      * {@link CompanionDeviceManager#detachSystemDataTransport(int)}
239      * for your convenience if you get callbacks in this class.
240      *
241      * @param associationId id of the associated device
242      */
243     @RequiresPermission(android.Manifest.permission.DELIVER_COMPANION_MESSAGES)
detachSystemDataTransport(int associationId)244     public final void detachSystemDataTransport(int associationId)
245             throws DeviceNotAssociatedException {
246         getSystemService(CompanionDeviceManager.class)
247                 .detachSystemDataTransport(associationId);
248     }
249 
250     /**
251      * Called by system whenever a device associated with this app is connected.
252      *
253      * @param associationInfo A record for the companion device.
254      */
255     @MainThread
onDeviceAppeared(@onNull AssociationInfo associationInfo)256     public void onDeviceAppeared(@NonNull AssociationInfo associationInfo) {
257         if (!associationInfo.isSelfManaged()) {
258             onDeviceAppeared(associationInfo.getDeviceMacAddressAsString());
259         }
260     }
261 
262     /**
263      * Called by system whenever a device associated with this app is disconnected.
264      *
265      * @param associationInfo A record for the companion device.
266      */
267     @MainThread
onDeviceDisappeared(@onNull AssociationInfo associationInfo)268     public void onDeviceDisappeared(@NonNull AssociationInfo associationInfo) {
269         if (!associationInfo.isSelfManaged()) {
270             onDeviceDisappeared(associationInfo.getDeviceMacAddressAsString());
271         }
272     }
273 
274     @Nullable
275     @Override
onBind(@onNull Intent intent)276     public final IBinder onBind(@NonNull Intent intent) {
277         if (Objects.equals(intent.getAction(), SERVICE_INTERFACE)) {
278             onBindCompanionDeviceService(intent);
279             return mRemote;
280         }
281         Log.w(LOG_TAG,
282                 "Tried to bind to wrong intent (should be " + SERVICE_INTERFACE + "): " + intent);
283         return null;
284     }
285 
286     /**
287      * Used to track the state of Binder connection in CTS tests.
288      * @hide
289      */
290     @TestApi
onBindCompanionDeviceService(@onNull Intent intent)291     public void onBindCompanionDeviceService(@NonNull Intent intent) {
292     }
293 
294     private class Stub extends ICompanionDeviceService.Stub {
295         final Handler mMainHandler = Handler.getMain();
296         final CompanionDeviceService mService = CompanionDeviceService.this;
297 
298         @Override
onDeviceAppeared(AssociationInfo associationInfo)299         public void onDeviceAppeared(AssociationInfo associationInfo) {
300             mMainHandler.postAtFrontOfQueue(() -> mService.onDeviceAppeared(associationInfo));
301         }
302 
303         @Override
onDeviceDisappeared(AssociationInfo associationInfo)304         public void onDeviceDisappeared(AssociationInfo associationInfo) {
305             mMainHandler.postAtFrontOfQueue(() -> mService.onDeviceDisappeared(associationInfo));
306         }
307     }
308 }
309