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 }