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