Home | History | Annotate | Download | only in com.example.android.wearable.runtimepermissions
      1 /*
      2  * Copyright (C) 2015 Google Inc. All Rights Reserved.
      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 com.example.android.wearable.runtimepermissions;
     18 
     19 import android.Manifest;
     20 import android.content.Intent;
     21 import android.content.pm.PackageManager;
     22 import android.hardware.Sensor;
     23 import android.hardware.SensorManager;
     24 import android.os.Bundle;
     25 import android.os.Looper;
     26 import android.support.annotation.NonNull;
     27 import android.support.v4.app.ActivityCompat;
     28 import android.support.wearable.activity.WearableActivity;
     29 import android.support.wearable.view.WatchViewStub;
     30 import android.util.Log;
     31 import android.view.View;
     32 import android.widget.Button;
     33 import android.widget.TextView;
     34 
     35 import com.example.android.wearable.runtimepermissions.common.Constants;
     36 
     37 import com.google.android.gms.common.ConnectionResult;
     38 import com.google.android.gms.common.api.GoogleApiClient;
     39 import com.google.android.gms.common.api.PendingResult;
     40 import com.google.android.gms.common.api.ResultCallback;
     41 import com.google.android.gms.wearable.CapabilityApi;
     42 import com.google.android.gms.wearable.CapabilityInfo;
     43 import com.google.android.gms.wearable.DataMap;
     44 import com.google.android.gms.wearable.MessageApi;
     45 import com.google.android.gms.wearable.MessageEvent;
     46 import com.google.android.gms.wearable.Node;
     47 import com.google.android.gms.wearable.Wearable;
     48 
     49 import java.util.List;
     50 import java.util.Set;
     51 import java.util.concurrent.TimeUnit;
     52 
     53 /**
     54  * Displays data that requires runtime permissions both locally (BODY_SENSORS) and remotely on
     55  * the phone (READ_EXTERNAL_STORAGE).
     56  *
     57  * The class is also launched by IncomingRequestWearService when the permission for the data the
     58  * phone is trying to access hasn't been granted (wear's sensors). If granted in that scenario,
     59  * this Activity also sends back the results of the permission request to the phone device (and
     60  * the sensor data if approved).
     61  */
     62 public class MainWearActivity extends WearableActivity implements
     63         GoogleApiClient.ConnectionCallbacks,
     64         GoogleApiClient.OnConnectionFailedListener,
     65         CapabilityApi.CapabilityListener,
     66         MessageApi.MessageListener,
     67         ActivityCompat.OnRequestPermissionsResultCallback {
     68 
     69     private static final String TAG = "MainWearActivity";
     70 
     71     /* Id to identify local permission request for body sensors. */
     72     private static final int PERMISSION_REQUEST_READ_BODY_SENSORS = 1;
     73 
     74     /* Id to identify starting/closing RequestPermissionOnPhoneActivity (startActivityForResult). */
     75     private static final int REQUEST_PHONE_PERMISSION = 1;
     76 
     77     public static final String EXTRA_PROMPT_PERMISSION_FROM_PHONE =
     78             "com.example.android.wearable.runtimepermissions.extra.PROMPT_PERMISSION_FROM_PHONE";
     79 
     80     private boolean mWearBodySensorsPermissionApproved;
     81     private boolean mPhoneStoragePermissionApproved;
     82 
     83     private boolean mPhoneRequestingWearSensorPermission;
     84 
     85     private Button mWearBodySensorsPermissionButton;
     86     private Button mPhoneStoragePermissionButton;
     87     private TextView mOutputTextView;
     88 
     89     private String mPhoneNodeId;
     90 
     91     private GoogleApiClient mGoogleApiClient;
     92 
     93     @Override
     94     protected void onCreate(Bundle savedInstanceState) {
     95         Log.d(TAG, "onCreate()");
     96         super.onCreate(savedInstanceState);;
     97 
     98         /*
     99          * Since this is a remote permission, we initialize it to false and then check the remote
    100          * permission once the GoogleApiClient is connected.
    101          */
    102         mPhoneStoragePermissionApproved = false;
    103 
    104         setContentView(R.layout.activity_main);
    105         setAmbientEnabled();
    106 
    107         // Checks if phone app requested wear permission (permission request opens later if true).
    108         mPhoneRequestingWearSensorPermission =
    109                 getIntent().getBooleanExtra(EXTRA_PROMPT_PERMISSION_FROM_PHONE, false);
    110 
    111         final WatchViewStub stub = (WatchViewStub) findViewById(R.id.watch_view_stub);
    112         stub.setOnLayoutInflatedListener(new WatchViewStub.OnLayoutInflatedListener() {
    113             @Override
    114             public void onLayoutInflated(WatchViewStub stub) {
    115 
    116                 mWearBodySensorsPermissionButton =
    117                         (Button) stub.findViewById(R.id.wearBodySensorsPermissionButton);
    118 
    119                 if (mWearBodySensorsPermissionApproved) {
    120                     mWearBodySensorsPermissionButton.setCompoundDrawablesWithIntrinsicBounds(
    121                             R.drawable.ic_permission_approved, 0, 0, 0);
    122                 }
    123 
    124                 mPhoneStoragePermissionButton =
    125                         (Button) stub.findViewById(R.id.phoneStoragePermissionButton);
    126 
    127                 mOutputTextView = (TextView) stub.findViewById(R.id.output);
    128 
    129                 if (mPhoneRequestingWearSensorPermission) {
    130                     launchPermissionDialogForPhone();
    131                 }
    132 
    133             }
    134         });
    135 
    136         mGoogleApiClient = new GoogleApiClient.Builder(this)
    137                 .addApi(Wearable.API)
    138                 .addConnectionCallbacks(this)
    139                 .addOnConnectionFailedListener(this)
    140                 .build();
    141     }
    142 
    143     public void onClickWearBodySensors(View view) {
    144 
    145         if (mWearBodySensorsPermissionApproved) {
    146 
    147             // To keep the sample simple, we are only displaying the number of sensors.
    148             SensorManager sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
    149             List<Sensor> sensorList = sensorManager.getSensorList(Sensor.TYPE_ALL);
    150             int numberOfSensorsOnDevice = sensorList.size();
    151 
    152             logToUi(numberOfSensorsOnDevice + " sensors on device(s)!");
    153 
    154         } else {
    155             logToUi("Requested local permission.");
    156             // On 23+ (M+) devices, GPS permission not granted. Request permission.
    157             ActivityCompat.requestPermissions(
    158                     this,
    159                     new String[]{Manifest.permission.BODY_SENSORS},
    160                     PERMISSION_REQUEST_READ_BODY_SENSORS);
    161         }
    162     }
    163 
    164     public void onClickPhoneStorage(View view) {
    165 
    166         logToUi("Requested info from phone. New approval may be required.");
    167         DataMap dataMap = new DataMap();
    168         dataMap.putInt(Constants.KEY_COMM_TYPE,
    169                 Constants.COMM_TYPE_REQUEST_DATA);
    170         sendMessage(dataMap);
    171     }
    172 
    173     @Override
    174     protected void onPause() {
    175         Log.d(TAG, "onPause()");
    176         super.onPause();
    177         if ((mGoogleApiClient != null) && mGoogleApiClient.isConnected()) {
    178             Wearable.CapabilityApi.removeCapabilityListener(
    179                     mGoogleApiClient,
    180                     this,
    181                     Constants.CAPABILITY_PHONE_APP);
    182             Wearable.MessageApi.removeListener(mGoogleApiClient, this);
    183             mGoogleApiClient.disconnect();
    184         }
    185     }
    186 
    187     @Override
    188     protected void onResume() {
    189         Log.d(TAG, "onResume()");
    190         super.onResume();
    191         if (mGoogleApiClient != null) {
    192             mGoogleApiClient.connect();
    193         }
    194 
    195         // Enables app to handle 23+ (M+) style permissions.
    196         mWearBodySensorsPermissionApproved =
    197                 ActivityCompat.checkSelfPermission(this, Manifest.permission.BODY_SENSORS)
    198                         == PackageManager.PERMISSION_GRANTED;
    199     }
    200 
    201      /*
    202       * Because this wear activity is marked "android:launchMode='singleInstance'" in the manifest,
    203       * we need to allow the permissions dialog to be opened up from the phone even if the wear app
    204       * is in the foreground. By overriding onNewIntent, we can cover that use case.
    205       */
    206     @Override
    207     protected void onNewIntent (Intent intent) {
    208         Log.d(TAG, "onNewIntent()");
    209         super.onNewIntent(intent);
    210 
    211         // Checks if phone app requested wear permissions (opens up permission request if true).
    212         mPhoneRequestingWearSensorPermission =
    213                 intent.getBooleanExtra(EXTRA_PROMPT_PERMISSION_FROM_PHONE, false);
    214 
    215         if (mPhoneRequestingWearSensorPermission) {
    216             launchPermissionDialogForPhone();
    217         }
    218     }
    219 
    220     @Override
    221     public void onEnterAmbient(Bundle ambientDetails) {
    222         Log.d(TAG, "onEnterAmbient() " + ambientDetails);
    223 
    224         if (mWearBodySensorsPermissionApproved) {
    225             mWearBodySensorsPermissionButton.setCompoundDrawablesWithIntrinsicBounds(
    226                     R.drawable.ic_permission_approved_bw, 0, 0, 0);
    227         } else {
    228             mWearBodySensorsPermissionButton.setCompoundDrawablesWithIntrinsicBounds(
    229                     R.drawable.ic_permission_denied_bw, 0, 0, 0);
    230         }
    231 
    232         if (mPhoneStoragePermissionApproved) {
    233             mPhoneStoragePermissionButton.setCompoundDrawablesWithIntrinsicBounds(
    234                     R.drawable.ic_permission_approved_bw, 0, 0, 0);
    235         } else {
    236             mPhoneStoragePermissionButton.setCompoundDrawablesWithIntrinsicBounds(
    237                     R.drawable.ic_permission_denied_bw, 0, 0, 0);
    238         }
    239         super.onEnterAmbient(ambientDetails);
    240     }
    241 
    242     @Override
    243     public void onExitAmbient() {
    244         Log.d(TAG, "onExitAmbient()");
    245 
    246         if (mWearBodySensorsPermissionApproved) {
    247             mWearBodySensorsPermissionButton.setCompoundDrawablesWithIntrinsicBounds(
    248                     R.drawable.ic_permission_approved, 0, 0, 0);
    249         } else {
    250             mWearBodySensorsPermissionButton.setCompoundDrawablesWithIntrinsicBounds(
    251                     R.drawable.ic_permission_denied, 0, 0, 0);
    252         }
    253 
    254         if (mPhoneStoragePermissionApproved) {
    255             mPhoneStoragePermissionButton.setCompoundDrawablesWithIntrinsicBounds(
    256                     R.drawable.ic_permission_approved, 0, 0, 0);
    257         } else {
    258             mPhoneStoragePermissionButton.setCompoundDrawablesWithIntrinsicBounds(
    259                     R.drawable.ic_permission_denied, 0, 0, 0);
    260         }
    261         super.onExitAmbient();
    262     }
    263 
    264     @Override
    265     public void onConnected(Bundle bundle) {
    266         Log.d(TAG, "onConnected()");
    267 
    268         // Set up listeners for capability and message changes.
    269         Wearable.CapabilityApi.addCapabilityListener(
    270                 mGoogleApiClient,
    271                 this,
    272                 Constants.CAPABILITY_PHONE_APP);
    273         Wearable.MessageApi.addListener(mGoogleApiClient, this);
    274 
    275         // Initial check of capabilities to find the phone.
    276         PendingResult<CapabilityApi.GetCapabilityResult> pendingResult =
    277                 Wearable.CapabilityApi.getCapability(
    278                         mGoogleApiClient,
    279                         Constants.CAPABILITY_PHONE_APP,
    280                         CapabilityApi.FILTER_REACHABLE);
    281 
    282         pendingResult.setResultCallback(new ResultCallback<CapabilityApi.GetCapabilityResult>() {
    283             @Override
    284             public void onResult(CapabilityApi.GetCapabilityResult getCapabilityResult) {
    285 
    286                 if (getCapabilityResult.getStatus().isSuccess()) {
    287                     CapabilityInfo capabilityInfo = getCapabilityResult.getCapability();
    288                     mPhoneNodeId = pickBestNodeId(capabilityInfo.getNodes());
    289 
    290                 } else {
    291                     Log.d(TAG, "Failed CapabilityApi result: "
    292                             + getCapabilityResult.getStatus());
    293                 }
    294             }
    295         });
    296     }
    297 
    298     @Override
    299     public void onConnectionSuspended(int i) {
    300         Log.d(TAG, "onConnectionSuspended(): connection to location client suspended");
    301     }
    302 
    303     @Override
    304     public void onConnectionFailed(ConnectionResult connectionResult) {
    305         Log.e(TAG, "onConnectionFailed(): connection to location client failed");
    306     }
    307 
    308     public void onCapabilityChanged(CapabilityInfo capabilityInfo) {
    309         Log.d(TAG, "onCapabilityChanged(): " + capabilityInfo);
    310 
    311         mPhoneNodeId = pickBestNodeId(capabilityInfo.getNodes());
    312     }
    313 
    314     /*
    315      * Callback received when a permissions request has been completed.
    316      */
    317     @Override
    318     public void onRequestPermissionsResult(
    319             int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
    320 
    321         String permissionResult = "Request code: " + requestCode + ", Permissions: " + permissions
    322                 + ", Results: " + grantResults;
    323         Log.d(TAG, "onRequestPermissionsResult(): " + permissionResult);
    324 
    325 
    326         if (requestCode == PERMISSION_REQUEST_READ_BODY_SENSORS) {
    327 
    328             if ((grantResults.length == 1)
    329                     && (grantResults[0] == PackageManager.PERMISSION_GRANTED)) {
    330 
    331                 mWearBodySensorsPermissionApproved = true;
    332                 mWearBodySensorsPermissionButton.setCompoundDrawablesWithIntrinsicBounds(
    333                         R.drawable.ic_permission_approved, 0, 0, 0);
    334 
    335                 // To keep the sample simple, we are only displaying the number of sensors.
    336                 SensorManager sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
    337                 List<Sensor> sensorList = sensorManager.getSensorList(Sensor.TYPE_ALL);
    338                 int numberOfSensorsOnDevice = sensorList.size();
    339 
    340                 String sensorSummary = numberOfSensorsOnDevice + " sensors on this device!";
    341                 logToUi(sensorSummary);
    342 
    343                 if (mPhoneRequestingWearSensorPermission) {
    344                     // Resets so this isn't triggered every time permission is changed in app.
    345                     mPhoneRequestingWearSensorPermission = false;
    346 
    347                     // Send 'approved' message to remote phone since it started Activity.
    348                     DataMap dataMap = new DataMap();
    349                     dataMap.putInt(Constants.KEY_COMM_TYPE,
    350                             Constants.COMM_TYPE_RESPONSE_USER_APPROVED_PERMISSION);
    351                     sendMessage(dataMap);
    352                 }
    353 
    354             } else {
    355 
    356                 mWearBodySensorsPermissionApproved = false;
    357                 mWearBodySensorsPermissionButton.setCompoundDrawablesWithIntrinsicBounds(
    358                         R.drawable.ic_permission_denied, 0, 0, 0);
    359 
    360                 if (mPhoneRequestingWearSensorPermission) {
    361                     // Resets so this isn't triggered every time permission is changed in app.
    362                     mPhoneRequestingWearSensorPermission = false;
    363                     // Send 'denied' message to remote phone since it started Activity.
    364                     DataMap dataMap = new DataMap();
    365                     dataMap.putInt(Constants.KEY_COMM_TYPE,
    366                             Constants.COMM_TYPE_RESPONSE_USER_DENIED_PERMISSION);
    367                     sendMessage(dataMap);
    368                 }
    369             }
    370         }
    371     }
    372 
    373     public void onMessageReceived(MessageEvent messageEvent) {
    374         Log.d(TAG, "onMessageReceived(): " + messageEvent);
    375 
    376         String messagePath = messageEvent.getPath();
    377 
    378         if (messagePath.equals(Constants.MESSAGE_PATH_WEAR)) {
    379 
    380             DataMap dataMap = DataMap.fromByteArray(messageEvent.getData());
    381             int commType = dataMap.getInt(Constants.KEY_COMM_TYPE, 0);
    382 
    383             if (commType == Constants.COMM_TYPE_RESPONSE_PERMISSION_REQUIRED) {
    384                 mPhoneStoragePermissionApproved = false;
    385                 updatePhoneButtonOnUiThread();
    386 
    387                 /* Because our request for remote data requires a remote permission, we now launch
    388                  * a splash activity informing the user we need those permissions (along with
    389                  * other helpful information to approve).
    390                  */
    391                 Intent phonePermissionRationaleIntent =
    392                         new Intent(this, RequestPermissionOnPhoneActivity.class);
    393                 startActivityForResult(phonePermissionRationaleIntent, REQUEST_PHONE_PERMISSION);
    394 
    395             } else if (commType == Constants.COMM_TYPE_RESPONSE_USER_APPROVED_PERMISSION) {
    396                 mPhoneStoragePermissionApproved = true;
    397                 updatePhoneButtonOnUiThread();
    398                 logToUi("User approved permission on remote device, requesting data again.");
    399                 DataMap outgoingDataRequestDataMap = new DataMap();
    400                 outgoingDataRequestDataMap.putInt(Constants.KEY_COMM_TYPE,
    401                         Constants.COMM_TYPE_REQUEST_DATA);
    402                 sendMessage(outgoingDataRequestDataMap);
    403 
    404             } else if (commType == Constants.COMM_TYPE_RESPONSE_USER_DENIED_PERMISSION) {
    405                 mPhoneStoragePermissionApproved = false;
    406                 updatePhoneButtonOnUiThread();
    407                 logToUi("User denied permission on remote device.");
    408 
    409             } else if (commType == Constants.COMM_TYPE_RESPONSE_DATA) {
    410                 mPhoneStoragePermissionApproved = true;
    411                 String storageDetails = dataMap.getString(Constants.KEY_PAYLOAD);
    412                 updatePhoneButtonOnUiThread();
    413                 logToUi(storageDetails);
    414             }
    415         }
    416     }
    417 
    418     private void sendMessage(DataMap dataMap) {
    419         Log.d(TAG, "sendMessage(): " + mPhoneNodeId);
    420 
    421         if (mPhoneNodeId != null) {
    422 
    423             PendingResult<MessageApi.SendMessageResult> pendingResult =
    424                     Wearable.MessageApi.sendMessage(
    425                             mGoogleApiClient,
    426                             mPhoneNodeId,
    427                             Constants.MESSAGE_PATH_PHONE,
    428                             dataMap.toByteArray());
    429 
    430             pendingResult.setResultCallback(new ResultCallback<MessageApi.SendMessageResult>() {
    431                 @Override
    432                 public void onResult(MessageApi.SendMessageResult sendMessageResult) {
    433 
    434                     if (!sendMessageResult.getStatus().isSuccess()) {
    435                         updatePhoneButtonOnUiThread();
    436                         logToUi("Sending message failed.");
    437 
    438                     } else {
    439                         Log.d(TAG, "Message sent successfully.");
    440                     }
    441                 }
    442             }, Constants.CONNECTION_TIME_OUT_MS, TimeUnit.SECONDS);
    443 
    444         } else {
    445             // Unable to retrieve node with proper capability
    446             mPhoneStoragePermissionApproved = false;
    447             updatePhoneButtonOnUiThread();
    448             logToUi("Phone not available to send message.");
    449         }
    450     }
    451 
    452     @Override
    453     protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    454         // Check which request we're responding to
    455         if (requestCode == REQUEST_PHONE_PERMISSION) {
    456             // Make sure the request was successful
    457             if (resultCode == RESULT_OK) {
    458                 logToUi("Requested permission on phone.");
    459                 DataMap dataMap = new DataMap();
    460                 dataMap.putInt(Constants.KEY_COMM_TYPE,
    461                         Constants.COMM_TYPE_REQUEST_PROMPT_PERMISSION);
    462                 sendMessage(dataMap);
    463             }
    464         }
    465     }
    466 
    467     /*
    468      * There should only ever be one phone in a node set (much less w/ the correct capability), so
    469      * I am just grabbing the first one (which should be the only one).
    470      */
    471     private String pickBestNodeId(Set<Node> nodes) {
    472 
    473         String bestNodeId = null;
    474         // Find a nearby node or pick one arbitrarily.
    475         for (Node node : nodes) {
    476             if (node.isNearby()) {
    477                 return node.getId();
    478             }
    479             bestNodeId = node.getId();
    480         }
    481         return bestNodeId;
    482     }
    483 
    484     /*
    485      * If Phone triggered the wear app for permissions, we open up the permission
    486      * dialog after inflation.
    487      */
    488     private void launchPermissionDialogForPhone() {
    489         Log.d(TAG, "launchPermissionDialogForPhone()");
    490 
    491         if (!mWearBodySensorsPermissionApproved) {
    492             // On 23+ (M+) devices, GPS permission not granted. Request permission.
    493             ActivityCompat.requestPermissions(
    494                     MainWearActivity.this,
    495                     new String[]{Manifest.permission.BODY_SENSORS},
    496                     PERMISSION_REQUEST_READ_BODY_SENSORS);
    497         }
    498     }
    499 
    500     private void updatePhoneButtonOnUiThread() {
    501         runOnUiThread(new Runnable() {
    502             @Override
    503             public void run() {
    504 
    505                 if (mPhoneStoragePermissionApproved) {
    506 
    507                     if (isAmbient()) {
    508                         mPhoneStoragePermissionButton.setCompoundDrawablesWithIntrinsicBounds(
    509                                 R.drawable.ic_permission_approved_bw, 0, 0, 0);
    510                     } else {
    511                         mPhoneStoragePermissionButton.setCompoundDrawablesWithIntrinsicBounds(
    512                                 R.drawable.ic_permission_approved, 0, 0, 0);
    513                     }
    514 
    515                 } else {
    516 
    517                     if (isAmbient()) {
    518                         mPhoneStoragePermissionButton.setCompoundDrawablesWithIntrinsicBounds(
    519                                 R.drawable.ic_permission_denied_bw, 0, 0, 0);
    520                     } else {
    521                         mPhoneStoragePermissionButton.setCompoundDrawablesWithIntrinsicBounds(
    522                                 R.drawable.ic_permission_denied, 0, 0, 0);
    523                     }
    524                 }
    525             }
    526         });
    527     }
    528 
    529     /*
    530      * Handles all messages for the UI coming on and off the main thread. Not all callbacks happen
    531      * on the main thread.
    532      */
    533     private void logToUi(final String message) {
    534 
    535         boolean mainUiThread = (Looper.myLooper() == Looper.getMainLooper());
    536 
    537         if (mainUiThread) {
    538 
    539             if (!message.isEmpty()) {
    540                 Log.d(TAG, message);
    541                 mOutputTextView.setText(message);
    542             }
    543 
    544         } else {
    545             runOnUiThread(new Runnable() {
    546                 @Override
    547                 public void run() {
    548                     if (!message.isEmpty()) {
    549                         Log.d(TAG, message);
    550                         mOutputTextView.setText(message);
    551                     }
    552                 }
    553             });
    554         }
    555     }
    556 }