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 }