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