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