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