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 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 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 onClickWearBodySensors(View view)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 onClickPhoneStorage(View view)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 onPause()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 onResume()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 onActivityResult(int requestCode, int resultCode, Intent data)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 onConnected(Bundle bundle)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 onConnectionSuspended(int i)248 public void onConnectionSuspended(int i) { 249 Log.d(TAG, "onConnectionSuspended(): connection to location client suspended"); 250 } 251 252 @Override onConnectionFailed(ConnectionResult connectionResult)253 public void onConnectionFailed(ConnectionResult connectionResult) { 254 Log.e(TAG, "onConnectionFailed(): connection to location client failed"); 255 } 256 257 onCapabilityChanged(CapabilityInfo capabilityInfo)258 public void onCapabilityChanged(CapabilityInfo capabilityInfo) { 259 Log.d(TAG, "onCapabilityChanged(): " + capabilityInfo); 260 261 mWearNodeIds = capabilityInfo.getNodes(); 262 } 263 onMessageReceived(MessageEvent messageEvent)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 onResult(MessageApi.SendMessageResult sendMessageResult)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 sendMessage(DataMap dataMap)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 updateWearButtonOnUiThread()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 */ logToUi(final String message)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 getPhoneStorageInformation()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 sendWearPermissionResults()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