Home | History | Annotate | Download | only in companion
      1 /*
      2  * Copyright (C) 2017 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 package android.companion;
     18 
     19 
     20 import static com.android.internal.util.Preconditions.checkNotNull;
     21 
     22 import android.annotation.NonNull;
     23 import android.annotation.Nullable;
     24 import android.annotation.SystemService;
     25 import android.app.Activity;
     26 import android.app.Application;
     27 import android.app.PendingIntent;
     28 import android.content.ComponentName;
     29 import android.content.Context;
     30 import android.content.IntentSender;
     31 import android.content.pm.PackageManager;
     32 import android.os.Bundle;
     33 import android.os.Handler;
     34 import android.os.RemoteException;
     35 import android.service.notification.NotificationListenerService;
     36 import android.util.Log;
     37 
     38 import java.util.Collections;
     39 import java.util.List;
     40 
     41 /**
     42  * System level service for managing companion devices
     43  *
     44  * <p>To obtain an instance call {@link Context#getSystemService}({@link
     45  * Context#COMPANION_DEVICE_SERVICE}) Then, call {@link #associate(AssociationRequest,
     46  * Callback, Handler)} to initiate the flow of associating current package with a
     47  * device selected by user.</p>
     48  *
     49  * @see AssociationRequest
     50  */
     51 @SystemService(Context.COMPANION_DEVICE_SERVICE)
     52 public final class CompanionDeviceManager {
     53 
     54     private static final boolean DEBUG = false;
     55     private static final String LOG_TAG = "CompanionDeviceManager";
     56 
     57     /**
     58      * A device, returned in the activity result of the {@link IntentSender} received in
     59      * {@link Callback#onDeviceFound}
     60      */
     61     public static final String EXTRA_DEVICE = "android.companion.extra.DEVICE";
     62 
     63     /**
     64      * The package name of the companion device discovery component.
     65      *
     66      * @hide
     67      */
     68     public static final String COMPANION_DEVICE_DISCOVERY_PACKAGE_NAME =
     69             "com.android.companiondevicemanager";
     70 
     71     /**
     72      * A callback to receive once at least one suitable device is found, or the search failed
     73      * (e.g. timed out)
     74      */
     75     public abstract static class Callback {
     76 
     77         /**
     78          * Called once at least one suitable device is found
     79          *
     80          * @param chooserLauncher a {@link IntentSender} to launch the UI for user to select a
     81          *                        device
     82          */
     83         public abstract void onDeviceFound(IntentSender chooserLauncher);
     84 
     85         /**
     86          * Called if there was an error looking for device(s), e.g. timeout
     87          *
     88          * @param error the cause of the error
     89          */
     90         public abstract void onFailure(CharSequence error);
     91     }
     92 
     93     private final ICompanionDeviceManager mService;
     94     private final Context mContext;
     95 
     96     /** @hide */
     97     public CompanionDeviceManager(
     98             @Nullable ICompanionDeviceManager service, @NonNull Context context) {
     99         mService = service;
    100         mContext = context;
    101     }
    102 
    103     /**
    104      * Associate this app with a companion device, selected by user
    105      *
    106      * <p>Once at least one appropriate device is found, {@code callback} will be called with a
    107      * {@link PendingIntent} that can be used to show the list of available devices for the user
    108      * to select.
    109      * It should be started for result (i.e. using
    110      * {@link android.app.Activity#startIntentSenderForResult}), as the resulting
    111      * {@link android.content.Intent} will contain extra {@link #EXTRA_DEVICE}, with the selected
    112      * device. (e.g. {@link android.bluetooth.BluetoothDevice})</p>
    113      *
    114      * <p>If your app needs to be excluded from battery optimizations (run in the background)
    115      * or to have unrestricted data access (use data in the background) you can declare that
    116      * you use the {@link android.Manifest.permission#REQUEST_COMPANION_RUN_IN_BACKGROUND} and {@link
    117      * android.Manifest.permission#REQUEST_COMPANION_USE_DATA_IN_BACKGROUND} respectively. Note that these
    118      * special capabilities have a negative effect on the device's battery and user's data
    119      * usage, therefore you should requested them when absolutely necessary.</p>
    120      *
    121      * <p>You can call {@link #getAssociations} to get the list of currently associated
    122      * devices, and {@link #disassociate} to remove an association. Consider doing so when the
    123      * association is no longer relevant to avoid unnecessary battery and/or data drain resulting
    124      * from special privileges that the association provides</p>
    125      *
    126      * <p>Calling this API requires a uses-feature
    127      * {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} declaration in the manifest</p>
    128      *
    129      * @param request specific details about this request
    130      * @param callback will be called once there's at least one device found for user to choose from
    131      * @param handler A handler to control which thread the callback will be delivered on, or null,
    132      *                to deliver it on main thread
    133      *
    134      * @see AssociationRequest
    135      */
    136     public void associate(
    137             @NonNull AssociationRequest request,
    138             @NonNull Callback callback,
    139             @Nullable Handler handler) {
    140         if (!checkFeaturePresent()) {
    141             return;
    142         }
    143         checkNotNull(request, "Request cannot be null");
    144         checkNotNull(callback, "Callback cannot be null");
    145         try {
    146             mService.associate(
    147                     request,
    148                     new CallbackProxy(request, callback, Handler.mainIfNull(handler)),
    149                     getCallingPackage());
    150         } catch (RemoteException e) {
    151             throw e.rethrowFromSystemServer();
    152         }
    153     }
    154 
    155     /**
    156      * <p>Calling this API requires a uses-feature
    157      * {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} declaration in the manifest</p>
    158      *
    159      * @return a list of MAC addresses of devices that have been previously associated with the
    160      * current app. You can use these with {@link #disassociate}
    161      */
    162     @NonNull
    163     public List<String> getAssociations() {
    164         if (!checkFeaturePresent()) {
    165             return Collections.emptyList();
    166         }
    167         try {
    168             return mService.getAssociations(getCallingPackage(), mContext.getUserId());
    169         } catch (RemoteException e) {
    170             throw e.rethrowFromSystemServer();
    171         }
    172     }
    173 
    174     /**
    175      * Remove the association between this app and the device with the given mac address.
    176      *
    177      * <p>Any privileges provided via being associated with a given device will be revoked</p>
    178      *
    179      * <p>Consider doing so when the
    180      * association is no longer relevant to avoid unnecessary battery and/or data drain resulting
    181      * from special privileges that the association provides</p>
    182      *
    183      * <p>Calling this API requires a uses-feature
    184      * {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} declaration in the manifest</p>
    185      *
    186      * @param deviceMacAddress the MAC address of device to disassociate from this app
    187      */
    188     public void disassociate(@NonNull String deviceMacAddress) {
    189         if (!checkFeaturePresent()) {
    190             return;
    191         }
    192         try {
    193             mService.disassociate(deviceMacAddress, getCallingPackage());
    194         } catch (RemoteException e) {
    195             throw e.rethrowFromSystemServer();
    196         }
    197     }
    198 
    199     /**
    200      * Request notification access for the given component.
    201      *
    202      * The given component must follow the protocol specified in {@link NotificationListenerService}
    203      *
    204      * Only components from the same {@link ComponentName#getPackageName package} as the calling app
    205      * are allowed.
    206      *
    207      * Your app must have an association with a device before calling this API
    208      *
    209      * <p>Calling this API requires a uses-feature
    210      * {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} declaration in the manifest</p>
    211      */
    212     public void requestNotificationAccess(ComponentName component) {
    213         if (!checkFeaturePresent()) {
    214             return;
    215         }
    216         try {
    217             IntentSender intentSender = mService.requestNotificationAccess(component)
    218                     .getIntentSender();
    219             mContext.startIntentSender(intentSender, null, 0, 0, 0);
    220         } catch (RemoteException e) {
    221             throw e.rethrowFromSystemServer();
    222         } catch (IntentSender.SendIntentException e) {
    223             throw new RuntimeException(e);
    224         }
    225     }
    226 
    227     /**
    228      * Check whether the given component can access the notifications via a
    229      * {@link NotificationListenerService}
    230      *
    231      * Your app must have an association with a device before calling this API
    232      *
    233      * <p>Calling this API requires a uses-feature
    234      * {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} declaration in the manifest</p>
    235      *
    236      * @param component the name of the component
    237      * @return whether the given component has the notification listener permission
    238      */
    239     public boolean hasNotificationAccess(ComponentName component) {
    240         if (!checkFeaturePresent()) {
    241             return false;
    242         }
    243         try {
    244             return mService.hasNotificationAccess(component);
    245         } catch (RemoteException e) {
    246             throw e.rethrowFromSystemServer();
    247         }
    248     }
    249 
    250     private boolean checkFeaturePresent() {
    251         boolean featurePresent = mService != null;
    252         if (!featurePresent && DEBUG) {
    253             Log.d(LOG_TAG, "Feature " + PackageManager.FEATURE_COMPANION_DEVICE_SETUP
    254                     + " not available");
    255         }
    256         return featurePresent;
    257     }
    258 
    259     private Activity getActivity() {
    260         return (Activity) mContext;
    261     }
    262 
    263     private String getCallingPackage() {
    264         return mContext.getPackageName();
    265     }
    266 
    267     private class CallbackProxy extends IFindDeviceCallback.Stub
    268             implements Application.ActivityLifecycleCallbacks {
    269 
    270         private Callback mCallback;
    271         private Handler mHandler;
    272         private AssociationRequest mRequest;
    273 
    274         private CallbackProxy(AssociationRequest request, Callback callback, Handler handler) {
    275             mCallback = callback;
    276             mHandler = handler;
    277             mRequest = request;
    278             getActivity().getApplication().registerActivityLifecycleCallbacks(this);
    279         }
    280 
    281         @Override
    282         public void onSuccess(PendingIntent launcher) {
    283             Handler handler = mHandler;
    284             if (handler == null) return;
    285             handler.post(() -> {
    286                 Callback callback = mCallback;
    287                 if (callback == null) return;
    288                 callback.onDeviceFound(launcher.getIntentSender());
    289             });
    290         }
    291 
    292         @Override
    293         public void onFailure(CharSequence reason) {
    294             Handler handler = mHandler;
    295             if (handler == null) return;
    296             handler.post(() -> {
    297                 Callback callback = mCallback;
    298                 if (callback == null) return;
    299                 callback.onFailure(reason);
    300             });
    301         }
    302 
    303         @Override
    304         public void onActivityDestroyed(Activity activity) {
    305             if (activity != getActivity()) return;
    306             try {
    307                 mService.stopScan(mRequest, this, getCallingPackage());
    308             } catch (RemoteException e) {
    309                 e.rethrowFromSystemServer();
    310             }
    311             getActivity().getApplication().unregisterActivityLifecycleCallbacks(this);
    312             mCallback = null;
    313             mHandler = null;
    314             mRequest = null;
    315         }
    316 
    317         @Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) {}
    318         @Override public void onActivityStarted(Activity activity) {}
    319         @Override public void onActivityResumed(Activity activity) {}
    320         @Override public void onActivityPaused(Activity activity) {}
    321         @Override public void onActivityStopped(Activity activity) {}
    322         @Override public void onActivitySaveInstanceState(Activity activity, Bundle outState) {}
    323     }
    324 }
    325