/* * Copyright (C) 2015 Google Inc. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.example.android.wearable.runtimepermissions; import android.Manifest; import android.content.Intent; import android.content.pm.PackageManager; import android.hardware.Sensor; import android.hardware.SensorManager; import android.os.Bundle; import android.os.Looper; import android.support.annotation.NonNull; import android.support.v4.app.ActivityCompat; import android.support.wearable.activity.WearableActivity; import android.support.wearable.view.WatchViewStub; import android.util.Log; import android.view.View; import android.widget.Button; import android.widget.TextView; import com.example.android.wearable.runtimepermissions.common.Constants; import com.google.android.gms.common.ConnectionResult; import com.google.android.gms.common.api.GoogleApiClient; import com.google.android.gms.common.api.PendingResult; import com.google.android.gms.common.api.ResultCallback; import com.google.android.gms.wearable.CapabilityApi; import com.google.android.gms.wearable.CapabilityInfo; import com.google.android.gms.wearable.DataMap; import com.google.android.gms.wearable.MessageApi; import com.google.android.gms.wearable.MessageEvent; import com.google.android.gms.wearable.Node; import com.google.android.gms.wearable.Wearable; import java.util.List; import java.util.Set; import java.util.concurrent.TimeUnit; /** * Displays data that requires runtime permissions both locally (BODY_SENSORS) and remotely on * the phone (READ_EXTERNAL_STORAGE). * * The class is also launched by IncomingRequestWearService when the permission for the data the * phone is trying to access hasn't been granted (wear's sensors). If granted in that scenario, * this Activity also sends back the results of the permission request to the phone device (and * the sensor data if approved). */ public class MainWearActivity extends WearableActivity implements GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener, CapabilityApi.CapabilityListener, MessageApi.MessageListener, ActivityCompat.OnRequestPermissionsResultCallback { private static final String TAG = "MainWearActivity"; /* Id to identify local permission request for body sensors. */ private static final int PERMISSION_REQUEST_READ_BODY_SENSORS = 1; /* Id to identify starting/closing RequestPermissionOnPhoneActivity (startActivityForResult). */ private static final int REQUEST_PHONE_PERMISSION = 1; public static final String EXTRA_PROMPT_PERMISSION_FROM_PHONE = "com.example.android.wearable.runtimepermissions.extra.PROMPT_PERMISSION_FROM_PHONE"; private boolean mWearBodySensorsPermissionApproved; private boolean mPhoneStoragePermissionApproved; private boolean mPhoneRequestingWearSensorPermission; private Button mWearBodySensorsPermissionButton; private Button mPhoneStoragePermissionButton; private TextView mOutputTextView; private String mPhoneNodeId; private GoogleApiClient mGoogleApiClient; @Override protected void onCreate(Bundle savedInstanceState) { Log.d(TAG, "onCreate()"); super.onCreate(savedInstanceState);; /* * Since this is a remote permission, we initialize it to false and then check the remote * permission once the GoogleApiClient is connected. */ mPhoneStoragePermissionApproved = false; setContentView(R.layout.activity_main); setAmbientEnabled(); // Checks if phone app requested wear permission (permission request opens later if true). mPhoneRequestingWearSensorPermission = getIntent().getBooleanExtra(EXTRA_PROMPT_PERMISSION_FROM_PHONE, false); final WatchViewStub stub = (WatchViewStub) findViewById(R.id.watch_view_stub); stub.setOnLayoutInflatedListener(new WatchViewStub.OnLayoutInflatedListener() { @Override public void onLayoutInflated(WatchViewStub stub) { mWearBodySensorsPermissionButton = (Button) stub.findViewById(R.id.wearBodySensorsPermissionButton); if (mWearBodySensorsPermissionApproved) { mWearBodySensorsPermissionButton.setCompoundDrawablesWithIntrinsicBounds( R.drawable.ic_permission_approved, 0, 0, 0); } mPhoneStoragePermissionButton = (Button) stub.findViewById(R.id.phoneStoragePermissionButton); mOutputTextView = (TextView) stub.findViewById(R.id.output); if (mPhoneRequestingWearSensorPermission) { launchPermissionDialogForPhone(); } } }); mGoogleApiClient = new GoogleApiClient.Builder(this) .addApi(Wearable.API) .addConnectionCallbacks(this) .addOnConnectionFailedListener(this) .build(); } public void onClickWearBodySensors(View view) { if (mWearBodySensorsPermissionApproved) { // To keep the sample simple, we are only displaying the number of sensors. SensorManager sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE); List sensorList = sensorManager.getSensorList(Sensor.TYPE_ALL); int numberOfSensorsOnDevice = sensorList.size(); logToUi(numberOfSensorsOnDevice + " sensors on device(s)!"); } else { logToUi("Requested local permission."); // On 23+ (M+) devices, GPS permission not granted. Request permission. ActivityCompat.requestPermissions( this, new String[]{Manifest.permission.BODY_SENSORS}, PERMISSION_REQUEST_READ_BODY_SENSORS); } } public void onClickPhoneStorage(View view) { logToUi("Requested info from phone. New approval may be required."); DataMap dataMap = new DataMap(); dataMap.putInt(Constants.KEY_COMM_TYPE, Constants.COMM_TYPE_REQUEST_DATA); sendMessage(dataMap); } @Override protected void onPause() { Log.d(TAG, "onPause()"); super.onPause(); if ((mGoogleApiClient != null) && mGoogleApiClient.isConnected()) { Wearable.CapabilityApi.removeCapabilityListener( mGoogleApiClient, this, Constants.CAPABILITY_PHONE_APP); Wearable.MessageApi.removeListener(mGoogleApiClient, this); mGoogleApiClient.disconnect(); } } @Override protected void onResume() { Log.d(TAG, "onResume()"); super.onResume(); if (mGoogleApiClient != null) { mGoogleApiClient.connect(); } // Enables app to handle 23+ (M+) style permissions. mWearBodySensorsPermissionApproved = ActivityCompat.checkSelfPermission(this, Manifest.permission.BODY_SENSORS) == PackageManager.PERMISSION_GRANTED; } /* * Because this wear activity is marked "android:launchMode='singleInstance'" in the manifest, * we need to allow the permissions dialog to be opened up from the phone even if the wear app * is in the foreground. By overriding onNewIntent, we can cover that use case. */ @Override protected void onNewIntent (Intent intent) { Log.d(TAG, "onNewIntent()"); super.onNewIntent(intent); // Checks if phone app requested wear permissions (opens up permission request if true). mPhoneRequestingWearSensorPermission = intent.getBooleanExtra(EXTRA_PROMPT_PERMISSION_FROM_PHONE, false); if (mPhoneRequestingWearSensorPermission) { launchPermissionDialogForPhone(); } } @Override public void onEnterAmbient(Bundle ambientDetails) { Log.d(TAG, "onEnterAmbient() " + ambientDetails); if (mWearBodySensorsPermissionApproved) { mWearBodySensorsPermissionButton.setCompoundDrawablesWithIntrinsicBounds( R.drawable.ic_permission_approved_bw, 0, 0, 0); } else { mWearBodySensorsPermissionButton.setCompoundDrawablesWithIntrinsicBounds( R.drawable.ic_permission_denied_bw, 0, 0, 0); } if (mPhoneStoragePermissionApproved) { mPhoneStoragePermissionButton.setCompoundDrawablesWithIntrinsicBounds( R.drawable.ic_permission_approved_bw, 0, 0, 0); } else { mPhoneStoragePermissionButton.setCompoundDrawablesWithIntrinsicBounds( R.drawable.ic_permission_denied_bw, 0, 0, 0); } super.onEnterAmbient(ambientDetails); } @Override public void onExitAmbient() { Log.d(TAG, "onExitAmbient()"); if (mWearBodySensorsPermissionApproved) { mWearBodySensorsPermissionButton.setCompoundDrawablesWithIntrinsicBounds( R.drawable.ic_permission_approved, 0, 0, 0); } else { mWearBodySensorsPermissionButton.setCompoundDrawablesWithIntrinsicBounds( R.drawable.ic_permission_denied, 0, 0, 0); } if (mPhoneStoragePermissionApproved) { mPhoneStoragePermissionButton.setCompoundDrawablesWithIntrinsicBounds( R.drawable.ic_permission_approved, 0, 0, 0); } else { mPhoneStoragePermissionButton.setCompoundDrawablesWithIntrinsicBounds( R.drawable.ic_permission_denied, 0, 0, 0); } super.onExitAmbient(); } @Override public void onConnected(Bundle bundle) { Log.d(TAG, "onConnected()"); // Set up listeners for capability and message changes. Wearable.CapabilityApi.addCapabilityListener( mGoogleApiClient, this, Constants.CAPABILITY_PHONE_APP); Wearable.MessageApi.addListener(mGoogleApiClient, this); // Initial check of capabilities to find the phone. PendingResult pendingResult = Wearable.CapabilityApi.getCapability( mGoogleApiClient, Constants.CAPABILITY_PHONE_APP, CapabilityApi.FILTER_REACHABLE); pendingResult.setResultCallback(new ResultCallback() { @Override public void onResult(CapabilityApi.GetCapabilityResult getCapabilityResult) { if (getCapabilityResult.getStatus().isSuccess()) { CapabilityInfo capabilityInfo = getCapabilityResult.getCapability(); mPhoneNodeId = pickBestNodeId(capabilityInfo.getNodes()); } else { Log.d(TAG, "Failed CapabilityApi result: " + getCapabilityResult.getStatus()); } } }); } @Override public void onConnectionSuspended(int i) { Log.d(TAG, "onConnectionSuspended(): connection to location client suspended"); } @Override public void onConnectionFailed(ConnectionResult connectionResult) { Log.e(TAG, "onConnectionFailed(): connection to location client failed"); } public void onCapabilityChanged(CapabilityInfo capabilityInfo) { Log.d(TAG, "onCapabilityChanged(): " + capabilityInfo); mPhoneNodeId = pickBestNodeId(capabilityInfo.getNodes()); } /* * Callback received when a permissions request has been completed. */ @Override public void onRequestPermissionsResult( int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { String permissionResult = "Request code: " + requestCode + ", Permissions: " + permissions + ", Results: " + grantResults; Log.d(TAG, "onRequestPermissionsResult(): " + permissionResult); if (requestCode == PERMISSION_REQUEST_READ_BODY_SENSORS) { if ((grantResults.length == 1) && (grantResults[0] == PackageManager.PERMISSION_GRANTED)) { mWearBodySensorsPermissionApproved = true; mWearBodySensorsPermissionButton.setCompoundDrawablesWithIntrinsicBounds( R.drawable.ic_permission_approved, 0, 0, 0); // To keep the sample simple, we are only displaying the number of sensors. SensorManager sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE); List sensorList = sensorManager.getSensorList(Sensor.TYPE_ALL); int numberOfSensorsOnDevice = sensorList.size(); String sensorSummary = numberOfSensorsOnDevice + " sensors on this device!"; logToUi(sensorSummary); if (mPhoneRequestingWearSensorPermission) { // Resets so this isn't triggered every time permission is changed in app. mPhoneRequestingWearSensorPermission = false; // Send 'approved' message to remote phone since it started Activity. DataMap dataMap = new DataMap(); dataMap.putInt(Constants.KEY_COMM_TYPE, Constants.COMM_TYPE_RESPONSE_USER_APPROVED_PERMISSION); sendMessage(dataMap); } } else { mWearBodySensorsPermissionApproved = false; mWearBodySensorsPermissionButton.setCompoundDrawablesWithIntrinsicBounds( R.drawable.ic_permission_denied, 0, 0, 0); if (mPhoneRequestingWearSensorPermission) { // Resets so this isn't triggered every time permission is changed in app. mPhoneRequestingWearSensorPermission = false; // Send 'denied' message to remote phone since it started Activity. DataMap dataMap = new DataMap(); dataMap.putInt(Constants.KEY_COMM_TYPE, Constants.COMM_TYPE_RESPONSE_USER_DENIED_PERMISSION); sendMessage(dataMap); } } } } public void onMessageReceived(MessageEvent messageEvent) { Log.d(TAG, "onMessageReceived(): " + messageEvent); String messagePath = messageEvent.getPath(); if (messagePath.equals(Constants.MESSAGE_PATH_WEAR)) { DataMap dataMap = DataMap.fromByteArray(messageEvent.getData()); int commType = dataMap.getInt(Constants.KEY_COMM_TYPE, 0); if (commType == Constants.COMM_TYPE_RESPONSE_PERMISSION_REQUIRED) { mPhoneStoragePermissionApproved = false; updatePhoneButtonOnUiThread(); /* Because our request for remote data requires a remote permission, we now launch * a splash activity informing the user we need those permissions (along with * other helpful information to approve). */ Intent phonePermissionRationaleIntent = new Intent(this, RequestPermissionOnPhoneActivity.class); startActivityForResult(phonePermissionRationaleIntent, REQUEST_PHONE_PERMISSION); } else if (commType == Constants.COMM_TYPE_RESPONSE_USER_APPROVED_PERMISSION) { mPhoneStoragePermissionApproved = true; updatePhoneButtonOnUiThread(); logToUi("User approved permission on remote device, requesting data again."); DataMap outgoingDataRequestDataMap = new DataMap(); outgoingDataRequestDataMap.putInt(Constants.KEY_COMM_TYPE, Constants.COMM_TYPE_REQUEST_DATA); sendMessage(outgoingDataRequestDataMap); } else if (commType == Constants.COMM_TYPE_RESPONSE_USER_DENIED_PERMISSION) { mPhoneStoragePermissionApproved = false; updatePhoneButtonOnUiThread(); logToUi("User denied permission on remote device."); } else if (commType == Constants.COMM_TYPE_RESPONSE_DATA) { mPhoneStoragePermissionApproved = true; String storageDetails = dataMap.getString(Constants.KEY_PAYLOAD); updatePhoneButtonOnUiThread(); logToUi(storageDetails); } } } private void sendMessage(DataMap dataMap) { Log.d(TAG, "sendMessage(): " + mPhoneNodeId); if (mPhoneNodeId != null) { PendingResult pendingResult = Wearable.MessageApi.sendMessage( mGoogleApiClient, mPhoneNodeId, Constants.MESSAGE_PATH_PHONE, dataMap.toByteArray()); pendingResult.setResultCallback(new ResultCallback() { @Override public void onResult(MessageApi.SendMessageResult sendMessageResult) { if (!sendMessageResult.getStatus().isSuccess()) { updatePhoneButtonOnUiThread(); logToUi("Sending message failed."); } else { Log.d(TAG, "Message sent successfully."); } } }, Constants.CONNECTION_TIME_OUT_MS, TimeUnit.SECONDS); } else { // Unable to retrieve node with proper capability mPhoneStoragePermissionApproved = false; updatePhoneButtonOnUiThread(); logToUi("Phone not available to send message."); } } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { // Check which request we're responding to if (requestCode == REQUEST_PHONE_PERMISSION) { // Make sure the request was successful if (resultCode == RESULT_OK) { logToUi("Requested permission on phone."); DataMap dataMap = new DataMap(); dataMap.putInt(Constants.KEY_COMM_TYPE, Constants.COMM_TYPE_REQUEST_PROMPT_PERMISSION); sendMessage(dataMap); } } } /* * There should only ever be one phone in a node set (much less w/ the correct capability), so * I am just grabbing the first one (which should be the only one). */ private String pickBestNodeId(Set nodes) { String bestNodeId = null; // Find a nearby node or pick one arbitrarily. for (Node node : nodes) { if (node.isNearby()) { return node.getId(); } bestNodeId = node.getId(); } return bestNodeId; } /* * If Phone triggered the wear app for permissions, we open up the permission * dialog after inflation. */ private void launchPermissionDialogForPhone() { Log.d(TAG, "launchPermissionDialogForPhone()"); if (!mWearBodySensorsPermissionApproved) { // On 23+ (M+) devices, GPS permission not granted. Request permission. ActivityCompat.requestPermissions( MainWearActivity.this, new String[]{Manifest.permission.BODY_SENSORS}, PERMISSION_REQUEST_READ_BODY_SENSORS); } } private void updatePhoneButtonOnUiThread() { runOnUiThread(new Runnable() { @Override public void run() { if (mPhoneStoragePermissionApproved) { if (isAmbient()) { mPhoneStoragePermissionButton.setCompoundDrawablesWithIntrinsicBounds( R.drawable.ic_permission_approved_bw, 0, 0, 0); } else { mPhoneStoragePermissionButton.setCompoundDrawablesWithIntrinsicBounds( R.drawable.ic_permission_approved, 0, 0, 0); } } else { if (isAmbient()) { mPhoneStoragePermissionButton.setCompoundDrawablesWithIntrinsicBounds( R.drawable.ic_permission_denied_bw, 0, 0, 0); } else { mPhoneStoragePermissionButton.setCompoundDrawablesWithIntrinsicBounds( R.drawable.ic_permission_denied, 0, 0, 0); } } } }); } /* * Handles all messages for the UI coming on and off the main thread. Not all callbacks happen * on the main thread. */ private void logToUi(final String message) { boolean mainUiThread = (Looper.myLooper() == Looper.getMainLooper()); if (mainUiThread) { if (!message.isEmpty()) { Log.d(TAG, message); mOutputTextView.setText(message); } } else { runOnUiThread(new Runnable() { @Override public void run() { if (!message.isEmpty()) { Log.d(TAG, message); mOutputTextView.setText(message); } } }); } } }