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.app.Activity;
     21 import android.content.Intent;
     22 import android.content.pm.PackageManager;
     23 import android.os.Environment;
     24 import android.os.Looper;
     25 import android.support.v4.app.ActivityCompat;
     26 import android.support.v7.app.AppCompatActivity;
     27 import android.os.Bundle;
     28 import android.util.Log;
     29 import android.view.View;
     30 import android.widget.Button;
     31 import android.widget.TextView;
     32 
     33 import com.example.android.wearable.runtimepermissions.common.Constants;
     34 
     35 import com.google.android.gms.common.ConnectionResult;
     36 import com.google.android.gms.common.api.GoogleApiClient;
     37 import com.google.android.gms.common.api.PendingResult;
     38 import com.google.android.gms.common.api.ResultCallback;
     39 import com.google.android.gms.wearable.CapabilityApi;
     40 import com.google.android.gms.wearable.CapabilityInfo;
     41 import com.google.android.gms.wearable.DataMap;
     42 import com.google.android.gms.wearable.MessageApi;
     43 import com.google.android.gms.wearable.MessageEvent;
     44 import com.google.android.gms.wearable.Node;
     45 import com.google.android.gms.wearable.Wearable;
     46 
     47 import java.io.File;
     48 import java.util.Set;
     49 import java.util.concurrent.TimeUnit;
     50 
     51 /**
     52  * Displays data that requires runtime permissions both locally (READ_EXTERNAL_STORAGE) and
     53  * remotely on wear (BODY_SENSORS).
     54  *
     55  * The class also handles sending back the results of a permission request from a remote wear device
     56  * when the permission has not been approved yet on the phone (uses EXTRA as trigger). In that case,
     57  * the IncomingRequestPhoneService launches the splash Activity (PhonePermissionRequestActivity) to
     58  * inform user of permission request. After the user decides what to do, it falls back to this
     59  * Activity (which has all the GoogleApiClient code) to handle sending data across and keeps user
     60  * in app experience.
     61  */
     62 public class MainPhoneActivity extends AppCompatActivity implements
     63         GoogleApiClient.ConnectionCallbacks,
     64         GoogleApiClient.OnConnectionFailedListener,
     65         CapabilityApi.CapabilityListener,
     66         MessageApi.MessageListener,
     67         ResultCallback<MessageApi.SendMessageResult> {
     68 
     69     private static final String TAG = "MainPhoneActivity";
     70 
     71     /*
     72      * Alerts Activity that the initial request for permissions came from wear, and the Activity
     73      * needs to send back the results (data or permission rejection).
     74      */
     75     public static final String EXTRA_PROMPT_PERMISSION_FROM_WEAR =
     76             "com.example.android.wearable.runtimepermissions.extra.PROMPT_PERMISSION_FROM_WEAR";
     77 
     78     private static final int REQUEST_WEAR_PERMISSION_RATIONALE = 1;
     79 
     80     private boolean mWearBodySensorsPermissionApproved;
     81     private boolean mPhoneStoragePermissionApproved;
     82 
     83     private boolean mWearRequestingPhoneStoragePermission;
     84 
     85     private Button mWearBodySensorsPermissionButton;
     86     private Button mPhoneStoragePermissionButton;
     87     private TextView mOutputTextView;
     88 
     89     private Set<Node> mWearNodeIds;
     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         mWearBodySensorsPermissionApproved = false;
    103 
    104         setContentView(R.layout.activity_main);
    105 
    106         // Checks if wear app requested phone permission (permission request opens later if true).
    107         mWearRequestingPhoneStoragePermission =
    108                 getIntent().getBooleanExtra(EXTRA_PROMPT_PERMISSION_FROM_WEAR, false);
    109 
    110         mPhoneStoragePermissionButton =
    111                 (Button) findViewById(R.id.phoneStoragePermissionButton);
    112 
    113         mWearBodySensorsPermissionButton =
    114                 (Button) findViewById(R.id.wearBodySensorsPermissionButton);
    115 
    116         mOutputTextView = (TextView) findViewById(R.id.output);
    117 
    118         mGoogleApiClient = new GoogleApiClient.Builder(this)
    119                 .addApi(Wearable.API)
    120                 .addConnectionCallbacks(this)
    121                 .addOnConnectionFailedListener(this)
    122                 .build();
    123     }
    124 
    125     public void onClickWearBodySensors(View view) {
    126 
    127         logToUi("Requested info from wear device(s). New approval may be required.");
    128 
    129         DataMap dataMap = new DataMap();
    130         dataMap.putInt(Constants.KEY_COMM_TYPE, Constants.COMM_TYPE_REQUEST_DATA);
    131         sendMessage(dataMap);
    132     }
    133 
    134     public void onClickPhoneStorage(View view) {
    135 
    136         if (mPhoneStoragePermissionApproved) {
    137             logToUi(getPhoneStorageInformation());
    138 
    139         } else {
    140             // On 23+ (M+) devices, Storage permission not granted. Request permission.
    141             Intent startIntent = new Intent(this, PhonePermissionRequestActivity.class);
    142             startActivity(startIntent);
    143         }
    144     }
    145 
    146     @Override
    147     protected void onPause() {
    148         Log.d(TAG, "onPause()");
    149         super.onPause();
    150         if ((mGoogleApiClient != null) && (mGoogleApiClient.isConnected())) {
    151             Wearable.CapabilityApi.removeCapabilityListener(
    152                     mGoogleApiClient,
    153                     this,
    154                     Constants.CAPABILITY_WEAR_APP);
    155             Wearable.MessageApi.removeListener(mGoogleApiClient, this);
    156             mGoogleApiClient.disconnect();
    157         }
    158     }
    159 
    160     @Override
    161     protected void onResume() {
    162         Log.d(TAG, "onResume()");
    163         super.onResume();
    164 
    165         /* Enables app to handle 23+ (M+) style permissions. It also covers user changing
    166          * permission in settings and coming back to the app.
    167          */
    168         mPhoneStoragePermissionApproved =
    169                 ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
    170                         == PackageManager.PERMISSION_GRANTED;
    171 
    172         if (mPhoneStoragePermissionApproved) {
    173             mPhoneStoragePermissionButton.setCompoundDrawablesWithIntrinsicBounds(
    174                     R.drawable.ic_permission_approved, 0, 0, 0);
    175         }
    176 
    177         if (mGoogleApiClient != null) {
    178             mGoogleApiClient.connect();
    179         }
    180     }
    181 
    182     @Override
    183     protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    184         Log.d(TAG, "onActivityResult()");
    185         if (requestCode == REQUEST_WEAR_PERMISSION_RATIONALE) {
    186 
    187             if (resultCode == Activity.RESULT_OK) {
    188                 logToUi("Requested permission on wear device(s).");
    189 
    190                 DataMap dataMap = new DataMap();
    191                 dataMap.putInt(Constants.KEY_COMM_TYPE,
    192                         Constants.COMM_TYPE_REQUEST_PROMPT_PERMISSION);
    193                 sendMessage(dataMap);
    194             }
    195         }
    196     }
    197 
    198     @Override
    199     public void onConnected(Bundle bundle) {
    200         Log.d(TAG, "onConnected()");
    201 
    202         // Set up listeners for capability and message changes.
    203         Wearable.CapabilityApi.addCapabilityListener(
    204                 mGoogleApiClient,
    205                 this,
    206                 Constants.CAPABILITY_WEAR_APP);
    207         Wearable.MessageApi.addListener(mGoogleApiClient, this);
    208 
    209         // Initial check of capabilities to find the wear nodes.
    210         PendingResult<CapabilityApi.GetCapabilityResult> pendingResult =
    211                 Wearable.CapabilityApi.getCapability(
    212                         mGoogleApiClient,
    213                         Constants.CAPABILITY_WEAR_APP,
    214                         CapabilityApi.FILTER_REACHABLE);
    215 
    216         pendingResult.setResultCallback(new ResultCallback<CapabilityApi.GetCapabilityResult>() {
    217             @Override
    218             public void onResult(CapabilityApi.GetCapabilityResult getCapabilityResult) {
    219 
    220                 CapabilityInfo capabilityInfo = getCapabilityResult.getCapability();
    221                 String capabilityName = capabilityInfo.getName();
    222 
    223                 boolean wearSupportsSampleApp =
    224                         capabilityName.equals(Constants.CAPABILITY_WEAR_APP);
    225 
    226                 if (wearSupportsSampleApp) {
    227                     mWearNodeIds = capabilityInfo.getNodes();
    228 
    229                     /*
    230                      * Upon getting all wear nodes, we now need to check if the original request to
    231                      * launch this activity (and PhonePermissionRequestActivity) was initiated by
    232                      * a wear device. If it was, we need to send back the permission results (data
    233                      * or rejection of permission) to the wear device.
    234                      *
    235                      * Also, note we set variable to false, this enables the user to continue
    236                      * changing permissions without sending updates to the wear every time.
    237                      */
    238                     if (mWearRequestingPhoneStoragePermission) {
    239                         mWearRequestingPhoneStoragePermission = false;
    240                         sendWearPermissionResults();
    241                     }
    242                 }
    243             }
    244         });
    245     }
    246 
    247     @Override
    248     public void onConnectionSuspended(int i) {
    249         Log.d(TAG, "onConnectionSuspended(): connection to location client suspended");
    250     }
    251 
    252     @Override
    253     public void onConnectionFailed(ConnectionResult connectionResult) {
    254         Log.e(TAG, "onConnectionFailed(): connection to location client failed");
    255     }
    256 
    257 
    258     public void onCapabilityChanged(CapabilityInfo capabilityInfo) {
    259         Log.d(TAG, "onCapabilityChanged(): " + capabilityInfo);
    260 
    261         mWearNodeIds = capabilityInfo.getNodes();
    262     }
    263 
    264     public void onMessageReceived(MessageEvent messageEvent) {
    265         Log.d(TAG, "onMessageReceived(): " + messageEvent);
    266 
    267         String messagePath = messageEvent.getPath();
    268 
    269         if (messagePath.equals(Constants.MESSAGE_PATH_PHONE)) {
    270             DataMap dataMap = DataMap.fromByteArray(messageEvent.getData());
    271 
    272             int commType = dataMap.getInt(Constants.KEY_COMM_TYPE, 0);
    273 
    274             if (commType == Constants.COMM_TYPE_RESPONSE_PERMISSION_REQUIRED) {
    275                 mWearBodySensorsPermissionApproved = false;
    276                 updateWearButtonOnUiThread();
    277 
    278                 /* Because our request for remote data requires a remote permission, we now launch
    279                  * a splash activity informing the user we need those permissions (along with
    280                  * other helpful information to approve).
    281                  */
    282                 Intent wearPermissionRationale =
    283                         new Intent(this, WearPermissionRequestActivity.class);
    284                 startActivityForResult(wearPermissionRationale, REQUEST_WEAR_PERMISSION_RATIONALE);
    285 
    286             } else if (commType == Constants.COMM_TYPE_RESPONSE_USER_APPROVED_PERMISSION) {
    287                 mWearBodySensorsPermissionApproved = true;
    288                 updateWearButtonOnUiThread();
    289                 logToUi("User approved permission on remote device, requesting data again.");
    290                 DataMap outgoingDataRequestDataMap = new DataMap();
    291                 outgoingDataRequestDataMap.putInt(Constants.KEY_COMM_TYPE,
    292                         Constants.COMM_TYPE_REQUEST_DATA);
    293                 sendMessage(outgoingDataRequestDataMap);
    294 
    295             } else if (commType == Constants.COMM_TYPE_RESPONSE_USER_DENIED_PERMISSION) {
    296                 mWearBodySensorsPermissionApproved = false;
    297                 updateWearButtonOnUiThread();
    298                 logToUi("User denied permission on remote device.");
    299 
    300             } else if (commType == Constants.COMM_TYPE_RESPONSE_DATA) {
    301                 mWearBodySensorsPermissionApproved = true;
    302                 String storageDetails = dataMap.getString(Constants.KEY_PAYLOAD);
    303                 updateWearButtonOnUiThread();
    304                 logToUi(storageDetails);
    305 
    306             } else {
    307                 Log.d(TAG, "Unrecognized communication type received.");
    308             }
    309         }
    310     }
    311 
    312     @Override
    313     public void onResult(MessageApi.SendMessageResult sendMessageResult) {
    314         if (!sendMessageResult.getStatus().isSuccess()) {
    315             Log.d(TAG, "Sending message failed, onResult: " + sendMessageResult);
    316             updateWearButtonOnUiThread();
    317             logToUi("Sending message failed.");
    318 
    319         } else {
    320             Log.d(TAG, "Message sent.");
    321         }
    322     }
    323 
    324     private void sendMessage(DataMap dataMap) {
    325         Log.d(TAG, "sendMessage(): " + mWearNodeIds);
    326 
    327         if ((mWearNodeIds != null) && (!mWearNodeIds.isEmpty())) {
    328 
    329             PendingResult<MessageApi.SendMessageResult> pendingResult;
    330 
    331             for (Node node : mWearNodeIds) {
    332 
    333                 pendingResult = Wearable.MessageApi.sendMessage(
    334                         mGoogleApiClient,
    335                         node.getId(),
    336                         Constants.MESSAGE_PATH_WEAR,
    337                         dataMap.toByteArray());
    338 
    339                 pendingResult.setResultCallback(this, Constants.CONNECTION_TIME_OUT_MS,
    340                         TimeUnit.SECONDS);
    341             }
    342         } else {
    343             // Unable to retrieve node with proper capability
    344             mWearBodySensorsPermissionApproved = false;
    345             updateWearButtonOnUiThread();
    346             logToUi("Wear devices not available to send message.");
    347         }
    348     }
    349 
    350     private void updateWearButtonOnUiThread() {
    351         runOnUiThread(new Runnable() {
    352             @Override
    353             public void run() {
    354                 if (mWearBodySensorsPermissionApproved) {
    355                     mWearBodySensorsPermissionButton.setCompoundDrawablesWithIntrinsicBounds(
    356                             R.drawable.ic_permission_approved, 0, 0, 0);
    357                 } else {
    358                     mWearBodySensorsPermissionButton.setCompoundDrawablesWithIntrinsicBounds(
    359                             R.drawable.ic_permission_denied, 0, 0, 0);
    360                 }
    361             }
    362         });
    363     }
    364 
    365     /*
    366      * Handles all messages for the UI coming on and off the main thread. Not all callbacks happen
    367      * on the main thread.
    368      */
    369     private void logToUi(final String message) {
    370 
    371         boolean mainUiThread = (Looper.myLooper() == Looper.getMainLooper());
    372 
    373         if (mainUiThread) {
    374 
    375             if (!message.isEmpty()) {
    376                 Log.d(TAG, message);
    377                 mOutputTextView.setText(message);
    378             }
    379 
    380         } else {
    381             if (!message.isEmpty()) {
    382 
    383                 runOnUiThread(new Runnable() {
    384                     @Override
    385                     public void run() {
    386 
    387                         Log.d(TAG, message);
    388                         mOutputTextView.setText(message);
    389                     }
    390                 });
    391             }
    392         }
    393     }
    394 
    395     private String getPhoneStorageInformation() {
    396 
    397         StringBuilder stringBuilder = new StringBuilder();
    398 
    399         String state = Environment.getExternalStorageState();
    400         boolean isExternalStorageReadable = Environment.MEDIA_MOUNTED.equals(state)
    401                 || Environment.MEDIA_MOUNTED_READ_ONLY.equals(state);
    402 
    403         if (isExternalStorageReadable) {
    404             File externalStorageDirectory = Environment.getExternalStorageDirectory();
    405             String[] fileList = externalStorageDirectory.list();
    406 
    407             if (fileList.length > 0) {
    408 
    409                 stringBuilder.append("List of files\n");
    410                 for (String file : fileList) {
    411                     stringBuilder.append(" - " + file + "\n");
    412                 }
    413 
    414             } else {
    415                 stringBuilder.append("No files in external storage.");
    416             }
    417 
    418         } else {
    419             stringBuilder.append("No external media is available.");
    420         }
    421 
    422         return stringBuilder.toString();
    423     }
    424 
    425     private void sendWearPermissionResults() {
    426 
    427         Log.d(TAG, "sendWearPermissionResults()");
    428 
    429         DataMap dataMap = new DataMap();
    430 
    431         if (mPhoneStoragePermissionApproved) {
    432             dataMap.putInt(Constants.KEY_COMM_TYPE,
    433                     Constants.COMM_TYPE_RESPONSE_USER_APPROVED_PERMISSION);
    434         } else {
    435             dataMap.putInt(Constants.KEY_COMM_TYPE,
    436                     Constants.COMM_TYPE_RESPONSE_USER_DENIED_PERMISSION);
    437         }
    438         sendMessage(dataMap);
    439     }
    440 }
    441