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