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
onCreate(Bundle savedInstanceState)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 
onClickWearBodySensors(View view)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 
onClickPhoneStorage(View view)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
onPause()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
onResume()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
onNewIntent(Intent intent)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
onEnterAmbient(Bundle ambientDetails)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
onExitAmbient()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
onConnected(Bundle bundle)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
onConnectionSuspended(int i)299     public void onConnectionSuspended(int i) {
300         Log.d(TAG, "onConnectionSuspended(): connection to location client suspended");
301     }
302 
303     @Override
onConnectionFailed(ConnectionResult connectionResult)304     public void onConnectionFailed(ConnectionResult connectionResult) {
305         Log.e(TAG, "onConnectionFailed(): connection to location client failed");
306     }
307 
onCapabilityChanged(CapabilityInfo capabilityInfo)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
onRequestPermissionsResult( int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults)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 
onMessageReceived(MessageEvent messageEvent)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 
sendMessage(DataMap dataMap)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
onActivityResult(int requestCode, int resultCode, Intent data)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      */
pickBestNodeId(Set<Node> nodes)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      */
launchPermissionDialogForPhone()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 
updatePhoneButtonOnUiThread()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      */
logToUi(final String message)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 }