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 }