/* * Copyright (C) 2017 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.Manifest; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.SystemApi; import android.annotation.SystemService; import android.app.Activity; import android.app.Application; import android.app.PendingIntent; import android.bluetooth.BluetoothDevice; import android.content.ComponentName; import android.content.Context; import android.content.IntentSender; import android.content.pm.PackageManager; import android.net.MacAddress; import android.os.Bundle; import android.os.Handler; import android.os.RemoteException; import android.os.UserHandle; import android.service.notification.NotificationListenerService; import android.util.ExceptionUtils; import android.util.Log; import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.function.BiConsumer; /** * System level service for managing companion devices * * See this guide * for a usage example. * *
To obtain an instance call {@link Context#getSystemService}({@link * Context#COMPANION_DEVICE_SERVICE}) Then, call {@link #associate(AssociationRequest, * Callback, Handler)} to initiate the flow of associating current package with a * device selected by user.
* * @see CompanionDeviceManager#associate * @see AssociationRequest */ @SystemService(Context.COMPANION_DEVICE_SERVICE) public final class CompanionDeviceManager { private static final boolean DEBUG = false; private static final String LOG_TAG = "CompanionDeviceManager"; /** * A device, returned in the activity result of the {@link IntentSender} received in * {@link Callback#onDeviceFound} * * Type is: *Once at least one appropriate device is found, {@code callback} will be called with a * {@link PendingIntent} that can be used to show the list of available devices for the user * to select. * It should be started for result (i.e. using * {@link android.app.Activity#startIntentSenderForResult}), as the resulting * {@link android.content.Intent} will contain extra {@link #EXTRA_DEVICE}, with the selected * device. (e.g. {@link android.bluetooth.BluetoothDevice})
* *If your app needs to be excluded from battery optimizations (run in the background) * or to have unrestricted data access (use data in the background) you can declare that * you use the {@link android.Manifest.permission#REQUEST_COMPANION_RUN_IN_BACKGROUND} and {@link * android.Manifest.permission#REQUEST_COMPANION_USE_DATA_IN_BACKGROUND} respectively. Note that these * special capabilities have a negative effect on the device's battery and user's data * usage, therefore you should request them when absolutely necessary.
* *You can call {@link #getAssociations} to get the list of currently associated * devices, and {@link #disassociate} to remove an association. Consider doing so when the * association is no longer relevant to avoid unnecessary battery and/or data drain resulting * from special privileges that the association provides
* *Calling this API requires a uses-feature * {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} declaration in the manifest
* *When using {@link AssociationRequest#DEVICE_PROFILE_WATCH watch} * {@link AssociationRequest.Builder#setDeviceProfile profile}, caller must also hold * {@link Manifest.permission#REQUEST_COMPANION_PROFILE_WATCH}
* * @param request specific details about this request * @param callback will be called once there's at least one device found for user to choose from * @param handler A handler to control which thread the callback will be delivered on, or null, * to deliver it on main thread * * @see AssociationRequest */ @RequiresPermission( value = Manifest.permission.REQUEST_COMPANION_PROFILE_WATCH, conditional = true) public void associate( @NonNull AssociationRequest request, @NonNull Callback callback, @Nullable Handler handler) { if (!checkFeaturePresent()) { return; } Objects.requireNonNull(request, "Request cannot be null"); Objects.requireNonNull(callback, "Callback cannot be null"); try { mService.associate( request, new CallbackProxy(request, callback, Handler.mainIfNull(handler)), getCallingPackage()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** *Calling this API requires a uses-feature * {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} declaration in the manifest
* * @return a list of MAC addresses of devices that have been previously associated with the * current app. You can use these with {@link #disassociate} */ @NonNull public ListAny privileges provided via being associated with a given device will be revoked
* *Consider doing so when the * association is no longer relevant to avoid unnecessary battery and/or data drain resulting * from special privileges that the association provides
* *Calling this API requires a uses-feature * {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} declaration in the manifest
* * @param deviceMacAddress the MAC address of device to disassociate from this app */ public void disassociate(@NonNull String deviceMacAddress) { if (!checkFeaturePresent()) { return; } try { mService.disassociate(deviceMacAddress, getCallingPackage()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Request notification access for the given component. * * The given component must follow the protocol specified in {@link NotificationListenerService} * * Only components from the same {@link ComponentName#getPackageName package} as the calling app * are allowed. * * Your app must have an association with a device before calling this API * *Calling this API requires a uses-feature * {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} declaration in the manifest
*/ public void requestNotificationAccess(ComponentName component) { if (!checkFeaturePresent()) { return; } try { IntentSender intentSender = mService.requestNotificationAccess(component) .getIntentSender(); mContext.startIntentSender(intentSender, null, 0, 0, 0); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } catch (IntentSender.SendIntentException e) { throw new RuntimeException(e); } } /** * Check whether the given component can access the notifications via a * {@link NotificationListenerService} * * Your app must have an association with a device before calling this API * *Calling this API requires a uses-feature * {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} declaration in the manifest
* * @param component the name of the component * @return whether the given component has the notification listener permission */ public boolean hasNotificationAccess(ComponentName component) { if (!checkFeaturePresent()) { return false; } try { return mService.hasNotificationAccess(component); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Check if a given package was {@link #associate associated} with a device with given * Wi-Fi MAC address for a given user. * *This is a system API protected by the * {@link andrioid.Manifest.permission#MANAGE_COMPANION_DEVICES} permission, that’s currently * called by the Android Wi-Fi stack to determine whether user consent is required to connect * to a Wi-Fi network. Devices that have been pre-registered as companion devices will not * require user consent to connect.
* *Note if the caller has the * {@link android.Manifest.permission#COMPANION_APPROVE_WIFI_CONNECTIONS} permission, this * method will return true by default.
* * @param packageName the name of the package that has the association with the companion device * @param macAddress the Wi-Fi MAC address or BSSID of the companion device to check for * @param user the user handle that currently hosts the package being queried for a companion * device association * @return whether a corresponding association record exists * * @hide */ @SystemApi @RequiresPermission(android.Manifest.permission.MANAGE_COMPANION_DEVICES) public boolean isDeviceAssociatedForWifiConnection( @NonNull String packageName, @NonNull MacAddress macAddress, @NonNull UserHandle user) { if (!checkFeaturePresent()) { return false; } Objects.requireNonNull(packageName, "package name cannot be null"); Objects.requireNonNull(macAddress, "mac address cannot be null"); Objects.requireNonNull(user, "user cannot be null"); try { return mService.isDeviceAssociatedForWifiConnection( packageName, macAddress.toString(), user.getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Gets all package-device {@link Association}s for the current user. * * @return the associations list * @hide */ @RequiresPermission(android.Manifest.permission.MANAGE_COMPANION_DEVICES) public @NonNull List