1 /*
2  * Copyright (C) 2014 The Android Open Source Project
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.datalayer;
18 
19 import android.app.Activity;
20 import android.content.Context;
21 import android.content.Intent;
22 import android.content.IntentSender;
23 import android.content.pm.PackageManager;
24 import android.graphics.Bitmap;
25 import android.net.Uri;
26 import android.os.AsyncTask;
27 import android.os.Bundle;
28 import android.provider.MediaStore;
29 import android.util.Log;
30 import android.view.LayoutInflater;
31 import android.view.View;
32 import android.view.ViewGroup;
33 import android.widget.ArrayAdapter;
34 import android.widget.Button;
35 import android.widget.ImageView;
36 import android.widget.ListView;
37 import android.widget.TextView;
38 
39 import com.google.android.gms.common.ConnectionResult;
40 import com.google.android.gms.common.api.GoogleApiClient;
41 import com.google.android.gms.common.api.GoogleApiClient.ConnectionCallbacks;
42 import com.google.android.gms.common.api.GoogleApiClient.OnConnectionFailedListener;
43 import com.google.android.gms.common.api.ResultCallback;
44 import com.google.android.gms.wearable.Asset;
45 import com.google.android.gms.wearable.CapabilityApi;
46 import com.google.android.gms.wearable.CapabilityInfo;
47 import com.google.android.gms.wearable.DataApi;
48 import com.google.android.gms.wearable.DataApi.DataItemResult;
49 import com.google.android.gms.wearable.DataEvent;
50 import com.google.android.gms.wearable.DataEventBuffer;
51 import com.google.android.gms.wearable.MessageApi;
52 import com.google.android.gms.wearable.MessageApi.SendMessageResult;
53 import com.google.android.gms.wearable.MessageEvent;
54 import com.google.android.gms.wearable.Node;
55 import com.google.android.gms.wearable.NodeApi;
56 import com.google.android.gms.wearable.PutDataMapRequest;
57 import com.google.android.gms.wearable.PutDataRequest;
58 import com.google.android.gms.wearable.Wearable;
59 
60 import java.io.ByteArrayOutputStream;
61 import java.io.IOException;
62 import java.util.Collection;
63 import java.util.Date;
64 import java.util.HashSet;
65 import java.util.concurrent.ScheduledExecutorService;
66 import java.util.concurrent.ScheduledFuture;
67 import java.util.concurrent.ScheduledThreadPoolExecutor;
68 import java.util.concurrent.TimeUnit;
69 
70 /**
71  * Receives its own events using a listener API designed for foreground activities. Updates a data
72  * item every second while it is open. Also allows user to take a photo and send that as an asset
73  * to the paired wearable.
74  */
75 public class MainActivity extends Activity implements
76         CapabilityApi.CapabilityListener,
77         MessageApi.MessageListener,
78         DataApi.DataListener,
79         ConnectionCallbacks,
80         OnConnectionFailedListener {
81 
82     private static final String TAG = "MainActivity";
83 
84     //Request code for launching the Intent to resolve Google Play services errors.
85     private static final int REQUEST_RESOLVE_ERROR = 1000;
86 
87     private static final int REQUEST_IMAGE_CAPTURE = 1;
88 
89     private static final String START_ACTIVITY_PATH = "/start-activity";
90     private static final String COUNT_PATH = "/count";
91     private static final String IMAGE_PATH = "/image";
92     private static final String IMAGE_KEY = "photo";
93     private static final String COUNT_KEY = "count";
94 
95     private GoogleApiClient mGoogleApiClient;
96     private boolean mResolvingError = false;
97     private boolean mCameraSupported = false;
98 
99     private ListView mDataItemList;
100     private Button mSendPhotoBtn;
101     private ImageView mThumbView;
102     private Bitmap mImageBitmap;
103     private View mStartActivityBtn;
104 
105     private DataItemAdapter mDataItemListAdapter;
106 
107     // Send DataItems.
108     private ScheduledExecutorService mGeneratorExecutor;
109     private ScheduledFuture<?> mDataItemGeneratorFuture;
110 
111     @Override
onCreate(Bundle savedInstanceState)112     public void onCreate(Bundle savedInstanceState) {
113         super.onCreate(savedInstanceState);
114         LOGD(TAG, "onCreate");
115         mCameraSupported = getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA);
116         setContentView(R.layout.main_activity);
117         setupViews();
118 
119         // Stores DataItems received by the local broadcaster or from the paired watch.
120         mDataItemListAdapter = new DataItemAdapter(this, android.R.layout.simple_list_item_1);
121         mDataItemList.setAdapter(mDataItemListAdapter);
122 
123         mGeneratorExecutor = new ScheduledThreadPoolExecutor(1);
124 
125         mGoogleApiClient = new GoogleApiClient.Builder(this)
126                 .addApi(Wearable.API)
127                 .addConnectionCallbacks(this)
128                 .addOnConnectionFailedListener(this)
129                 .build();
130     }
131 
132     @Override
onStart()133     protected void onStart() {
134         super.onStart();
135         if (!mResolvingError) {
136             mGoogleApiClient.connect();
137         }
138     }
139 
140     @Override
onResume()141     public void onResume() {
142         super.onResume();
143         mDataItemGeneratorFuture = mGeneratorExecutor.scheduleWithFixedDelay(
144                 new DataItemGenerator(), 1, 5, TimeUnit.SECONDS);
145     }
146 
147     @Override
onPause()148     public void onPause() {
149         super.onPause();
150         mDataItemGeneratorFuture.cancel(true /* mayInterruptIfRunning */);
151     }
152 
153     @Override
onStop()154     protected void onStop() {
155         if (!mResolvingError && (mGoogleApiClient != null) && (mGoogleApiClient.isConnected())) {
156             Wearable.DataApi.removeListener(mGoogleApiClient, this);
157             Wearable.MessageApi.removeListener(mGoogleApiClient, this);
158             Wearable.CapabilityApi.removeListener(mGoogleApiClient, this);
159             mGoogleApiClient.disconnect();
160         }
161         super.onStop();
162     }
163 
164     @Override
onActivityResult(int requestCode, int resultCode, Intent data)165     protected void onActivityResult(int requestCode, int resultCode, Intent data) {
166         if (requestCode == REQUEST_IMAGE_CAPTURE && resultCode == RESULT_OK) {
167             Bundle extras = data.getExtras();
168             mImageBitmap = (Bitmap) extras.get("data");
169             mThumbView.setImageBitmap(mImageBitmap);
170         }
171     }
172 
173     @Override
onConnected(Bundle connectionHint)174     public void onConnected(Bundle connectionHint) {
175         LOGD(TAG, "Google API Client was connected");
176         mResolvingError = false;
177         mStartActivityBtn.setEnabled(true);
178         mSendPhotoBtn.setEnabled(mCameraSupported);
179         Wearable.DataApi.addListener(mGoogleApiClient, this);
180         Wearable.MessageApi.addListener(mGoogleApiClient, this);
181         Wearable.CapabilityApi.addListener(
182                 mGoogleApiClient, this, Uri.parse("wear://"), CapabilityApi.FILTER_REACHABLE);
183     }
184 
185     @Override
onConnectionSuspended(int cause)186     public void onConnectionSuspended(int cause) {
187         LOGD(TAG, "Connection to Google API client was suspended");
188         mStartActivityBtn.setEnabled(false);
189         mSendPhotoBtn.setEnabled(false);
190     }
191 
192     @Override
onConnectionFailed(ConnectionResult result)193     public void onConnectionFailed(ConnectionResult result) {
194         if (!mResolvingError) {
195 
196             if (result.hasResolution()) {
197                 try {
198                     mResolvingError = true;
199                     result.startResolutionForResult(this, REQUEST_RESOLVE_ERROR);
200                 } catch (IntentSender.SendIntentException e) {
201                     // There was an error with the resolution intent. Try again.
202                     mGoogleApiClient.connect();
203                 }
204             } else {
205                 Log.e(TAG, "Connection to Google API client has failed");
206                 mResolvingError = false;
207                 mStartActivityBtn.setEnabled(false);
208                 mSendPhotoBtn.setEnabled(false);
209                 Wearable.DataApi.removeListener(mGoogleApiClient, this);
210                 Wearable.MessageApi.removeListener(mGoogleApiClient, this);
211                 Wearable.CapabilityApi.removeListener(mGoogleApiClient, this);
212             }
213         }
214     }
215 
216     @Override
onDataChanged(DataEventBuffer dataEvents)217     public void onDataChanged(DataEventBuffer dataEvents) {
218         LOGD(TAG, "onDataChanged: " + dataEvents);
219 
220         for (DataEvent event : dataEvents) {
221             if (event.getType() == DataEvent.TYPE_CHANGED) {
222                 mDataItemListAdapter.add(
223                         new Event("DataItem Changed", event.getDataItem().toString()));
224             } else if (event.getType() == DataEvent.TYPE_DELETED) {
225                 mDataItemListAdapter.add(
226                         new Event("DataItem Deleted", event.getDataItem().toString()));
227             }
228         }
229     }
230 
231     @Override
onMessageReceived(final MessageEvent messageEvent)232     public void onMessageReceived(final MessageEvent messageEvent) {
233         LOGD(TAG, "onMessageReceived() A message from watch was received:"
234                 + messageEvent.getRequestId() + " " + messageEvent.getPath());
235 
236         mDataItemListAdapter.add(new Event("Message from watch", messageEvent.toString()));
237     }
238 
239     @Override
onCapabilityChanged(final CapabilityInfo capabilityInfo)240     public void onCapabilityChanged(final CapabilityInfo capabilityInfo) {
241         LOGD(TAG, "onCapabilityChanged: " + capabilityInfo);
242 
243         mDataItemListAdapter.add(new Event("onCapabilityChanged", capabilityInfo.toString()));
244     }
245 
246     /**
247      * Sets up UI components and their callback handlers.
248      */
setupViews()249     private void setupViews() {
250         mSendPhotoBtn = (Button) findViewById(R.id.sendPhoto);
251         mThumbView = (ImageView) findViewById(R.id.imageView);
252         mDataItemList = (ListView) findViewById(R.id.data_item_list);
253         mStartActivityBtn = findViewById(R.id.start_wearable_activity);
254     }
255 
onTakePhotoClick(View view)256     public void onTakePhotoClick(View view) {
257         dispatchTakePictureIntent();
258     }
259 
onSendPhotoClick(View view)260     public void onSendPhotoClick(View view) {
261         if (null != mImageBitmap && mGoogleApiClient.isConnected()) {
262             sendPhoto(toAsset(mImageBitmap));
263         }
264     }
265 
266     /**
267      * Sends an RPC to start a fullscreen Activity on the wearable.
268      */
onStartWearableActivityClick(View view)269     public void onStartWearableActivityClick(View view) {
270         LOGD(TAG, "Generating RPC");
271 
272         // Trigger an AsyncTask that will query for a list of connected nodes and send a
273         // "start-activity" message to each connected node.
274         new StartWearableActivityTask().execute();
275     }
276 
sendStartActivityMessage(String node)277     private void sendStartActivityMessage(String node) {
278         Wearable.MessageApi.sendMessage(
279                 mGoogleApiClient, node, START_ACTIVITY_PATH, new byte[0]).setResultCallback(
280                 new ResultCallback<SendMessageResult>() {
281                     @Override
282                     public void onResult(SendMessageResult sendMessageResult) {
283                         if (!sendMessageResult.getStatus().isSuccess()) {
284                             Log.e(TAG, "Failed to send message with status code: "
285                                     + sendMessageResult.getStatus().getStatusCode());
286                         }
287                     }
288                 }
289         );
290     }
291 
292     /**
293      * Dispatches an {@link android.content.Intent} to take a photo. Result will be returned back
294      * in onActivityResult().
295      */
dispatchTakePictureIntent()296     private void dispatchTakePictureIntent() {
297         Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
298         if (takePictureIntent.resolveActivity(getPackageManager()) != null) {
299             startActivityForResult(takePictureIntent, REQUEST_IMAGE_CAPTURE);
300         }
301     }
302 
303     /**
304      * Builds an {@link com.google.android.gms.wearable.Asset} from a bitmap. The image that we get
305      * back from the camera in "data" is a thumbnail size. Typically, your image should not exceed
306      * 320x320 and if you want to have zoom and parallax effect in your app, limit the size of your
307      * image to 640x400. Resize your image before transferring to your wearable device.
308      */
toAsset(Bitmap bitmap)309     private static Asset toAsset(Bitmap bitmap) {
310         ByteArrayOutputStream byteStream = null;
311         try {
312             byteStream = new ByteArrayOutputStream();
313             bitmap.compress(Bitmap.CompressFormat.PNG, 100, byteStream);
314             return Asset.createFromBytes(byteStream.toByteArray());
315         } finally {
316             if (null != byteStream) {
317                 try {
318                     byteStream.close();
319                 } catch (IOException e) {
320                     // ignore
321                 }
322             }
323         }
324     }
325 
326     /**
327      * Sends the asset that was created from the photo we took by adding it to the Data Item store.
328      */
sendPhoto(Asset asset)329     private void sendPhoto(Asset asset) {
330         PutDataMapRequest dataMap = PutDataMapRequest.create(IMAGE_PATH);
331         dataMap.getDataMap().putAsset(IMAGE_KEY, asset);
332         dataMap.getDataMap().putLong("time", new Date().getTime());
333         PutDataRequest request = dataMap.asPutDataRequest();
334         request.setUrgent();
335 
336         Wearable.DataApi.putDataItem(mGoogleApiClient, request)
337                 .setResultCallback(new ResultCallback<DataItemResult>() {
338                     @Override
339                     public void onResult(DataItemResult dataItemResult) {
340                         LOGD(TAG, "Sending image was successful: " + dataItemResult.getStatus()
341                                 .isSuccess());
342                     }
343                 });
344     }
345 
getNodes()346     private Collection<String> getNodes() {
347         HashSet<String> results = new HashSet<>();
348         NodeApi.GetConnectedNodesResult nodes =
349                 Wearable.NodeApi.getConnectedNodes(mGoogleApiClient).await();
350 
351         for (Node node : nodes.getNodes()) {
352             results.add(node.getId());
353         }
354 
355         return results;
356     }
357 
358     /**
359      * As simple wrapper around Log.d
360      */
LOGD(final String tag, String message)361     private static void LOGD(final String tag, String message) {
362         if (Log.isLoggable(tag, Log.DEBUG)) {
363             Log.d(tag, message);
364         }
365     }
366 
367     /**
368      * A View Adapter for presenting the Event objects in a list
369      */
370     private static class DataItemAdapter extends ArrayAdapter<Event> {
371 
372         private final Context mContext;
373 
DataItemAdapter(Context context, int unusedResource)374         public DataItemAdapter(Context context, int unusedResource) {
375             super(context, unusedResource);
376             mContext = context;
377         }
378 
379         @Override
getView(int position, View convertView, ViewGroup parent)380         public View getView(int position, View convertView, ViewGroup parent) {
381             ViewHolder holder;
382             if (convertView == null) {
383                 holder = new ViewHolder();
384                 LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(
385                         Context.LAYOUT_INFLATER_SERVICE);
386                 convertView = inflater.inflate(android.R.layout.two_line_list_item, null);
387                 convertView.setTag(holder);
388                 holder.text1 = (TextView) convertView.findViewById(android.R.id.text1);
389                 holder.text2 = (TextView) convertView.findViewById(android.R.id.text2);
390             } else {
391                 holder = (ViewHolder) convertView.getTag();
392             }
393             Event event = getItem(position);
394             holder.text1.setText(event.title);
395             holder.text2.setText(event.text);
396             return convertView;
397         }
398 
399         private class ViewHolder {
400             TextView text1;
401             TextView text2;
402         }
403     }
404 
405     private class Event {
406 
407         String title;
408         String text;
409 
Event(String title, String text)410         public Event(String title, String text) {
411             this.title = title;
412             this.text = text;
413         }
414     }
415 
416     private class StartWearableActivityTask extends AsyncTask<Void, Void, Void> {
417 
418         @Override
doInBackground(Void... args)419         protected Void doInBackground(Void... args) {
420             Collection<String> nodes = getNodes();
421             for (String node : nodes) {
422                 sendStartActivityMessage(node);
423             }
424             return null;
425         }
426     }
427 
428     /**
429      * Generates a DataItem based on an incrementing count.
430      */
431     private class DataItemGenerator implements Runnable {
432 
433         private int count = 0;
434 
435         @Override
run()436         public void run() {
437             PutDataMapRequest putDataMapRequest = PutDataMapRequest.create(COUNT_PATH);
438             putDataMapRequest.getDataMap().putInt(COUNT_KEY, count++);
439 
440             PutDataRequest request = putDataMapRequest.asPutDataRequest();
441             request.setUrgent();
442 
443             LOGD(TAG, "Generating DataItem: " + request);
444             if (!mGoogleApiClient.isConnected()) {
445                 return;
446             }
447             Wearable.DataApi.putDataItem(mGoogleApiClient, request)
448                     .setResultCallback(new ResultCallback<DataItemResult>() {
449                         @Override
450                         public void onResult(DataItemResult dataItemResult) {
451                             if (!dataItemResult.getStatus().isSuccess()) {
452                                 Log.e(TAG, "ERROR: failed to putDataItem, status code: "
453                                         + dataItemResult.getStatus().getStatusCode());
454                             }
455                         }
456                     });
457         }
458     }
459 }