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.os.AsyncTask; 26 import android.os.Bundle; 27 import android.os.Handler; 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.ResultCallback; 41 import com.google.android.gms.common.api.GoogleApiClient; 42 import com.google.android.gms.common.api.GoogleApiClient.ConnectionCallbacks; 43 import com.google.android.gms.common.api.GoogleApiClient.OnConnectionFailedListener; 44 import com.google.android.gms.common.data.FreezableUtils; 45 import com.google.android.gms.wearable.Asset; 46 import com.google.android.gms.wearable.DataApi.DataItemResult; 47 import com.google.android.gms.wearable.DataEvent; 48 import com.google.android.gms.wearable.DataEventBuffer; 49 import com.google.android.gms.wearable.MessageApi.SendMessageResult; 50 import com.google.android.gms.wearable.DataApi; 51 import com.google.android.gms.wearable.MessageApi; 52 import com.google.android.gms.wearable.MessageEvent; 53 import com.google.android.gms.wearable.Node; 54 import com.google.android.gms.wearable.NodeApi; 55 import com.google.android.gms.wearable.PutDataMapRequest; 56 import com.google.android.gms.wearable.PutDataRequest; 57 import com.google.android.gms.wearable.Wearable; 58 59 import java.io.ByteArrayOutputStream; 60 import java.io.IOException; 61 import java.util.Collection; 62 import java.util.Date; 63 import java.util.HashSet; 64 import java.util.List; 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 to 73 * the paired wearable. 74 */ 75 public class MainActivity extends Activity implements DataApi.DataListener, 76 MessageApi.MessageListener, NodeApi.NodeListener, ConnectionCallbacks, 77 OnConnectionFailedListener { 78 79 private static final String TAG = "MainActivity"; 80 81 /** Request code for launching the Intent to resolve Google Play services errors. */ 82 private static final int REQUEST_RESOLVE_ERROR = 1000; 83 84 private static final String START_ACTIVITY_PATH = "/start-activity"; 85 private static final String COUNT_PATH = "/count"; 86 private static final String IMAGE_PATH = "/image"; 87 private static final String IMAGE_KEY = "photo"; 88 private static final String COUNT_KEY = "count"; 89 90 private GoogleApiClient mGoogleApiClient; 91 private boolean mResolvingError = false; 92 private boolean mCameraSupported = false; 93 94 private ListView mDataItemList; 95 private Button mTakePhotoBtn; 96 private Button mSendPhotoBtn; 97 private ImageView mThumbView; 98 private Bitmap mImageBitmap; 99 private View mStartActivityBtn; 100 101 private DataItemAdapter mDataItemListAdapter; 102 private Handler mHandler; 103 104 // Send DataItems. 105 private ScheduledExecutorService mGeneratorExecutor; 106 private ScheduledFuture<?> mDataItemGeneratorFuture; 107 108 static final int REQUEST_IMAGE_CAPTURE = 1; 109 110 @Override onCreate(Bundle b)111 public void onCreate(Bundle b) { 112 super.onCreate(b); 113 mHandler = new Handler(); 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 onActivityResult(int requestCode, int resultCode, Intent data)133 protected void onActivityResult(int requestCode, int resultCode, Intent data) { 134 if (requestCode == REQUEST_IMAGE_CAPTURE && resultCode == RESULT_OK) { 135 Bundle extras = data.getExtras(); 136 mImageBitmap = (Bitmap) extras.get("data"); 137 mThumbView.setImageBitmap(mImageBitmap); 138 } 139 } 140 141 @Override onStart()142 protected void onStart() { 143 super.onStart(); 144 if (!mResolvingError) { 145 mGoogleApiClient.connect(); 146 } 147 } 148 149 @Override onResume()150 public void onResume() { 151 super.onResume(); 152 mDataItemGeneratorFuture = mGeneratorExecutor.scheduleWithFixedDelay( 153 new DataItemGenerator(), 1, 5, TimeUnit.SECONDS); 154 } 155 156 @Override onPause()157 public void onPause() { 158 super.onPause(); 159 mDataItemGeneratorFuture.cancel(true /* mayInterruptIfRunning */); 160 } 161 162 @Override onStop()163 protected void onStop() { 164 if (!mResolvingError) { 165 Wearable.DataApi.removeListener(mGoogleApiClient, this); 166 Wearable.MessageApi.removeListener(mGoogleApiClient, this); 167 Wearable.NodeApi.removeListener(mGoogleApiClient, this); 168 mGoogleApiClient.disconnect(); 169 } 170 super.onStop(); 171 } 172 173 @Override //ConnectionCallbacks 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.NodeApi.addListener(mGoogleApiClient, this); 182 } 183 184 @Override //ConnectionCallbacks onConnectionSuspended(int cause)185 public void onConnectionSuspended(int cause) { 186 LOGD(TAG, "Connection to Google API client was suspended"); 187 mStartActivityBtn.setEnabled(false); 188 mSendPhotoBtn.setEnabled(false); 189 } 190 191 @Override //OnConnectionFailedListener onConnectionFailed(ConnectionResult result)192 public void onConnectionFailed(ConnectionResult result) { 193 if (mResolvingError) { 194 // Already attempting to resolve an error. 195 return; 196 } else 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.NodeApi.removeListener(mGoogleApiClient, this); 212 } 213 } 214 215 @Override //DataListener onDataChanged(DataEventBuffer dataEvents)216 public void onDataChanged(DataEventBuffer dataEvents) { 217 LOGD(TAG, "onDataChanged: " + dataEvents); 218 final List<DataEvent> events = FreezableUtils.freezeIterable(dataEvents); 219 dataEvents.close(); 220 runOnUiThread(new Runnable() { 221 @Override 222 public void run() { 223 for (DataEvent event : events) { 224 if (event.getType() == DataEvent.TYPE_CHANGED) { 225 mDataItemListAdapter.add( 226 new Event("DataItem Changed", event.getDataItem().toString())); 227 } else if (event.getType() == DataEvent.TYPE_DELETED) { 228 mDataItemListAdapter.add( 229 new Event("DataItem Deleted", event.getDataItem().toString())); 230 } 231 } 232 } 233 }); 234 } 235 236 @Override //MessageListener onMessageReceived(final MessageEvent messageEvent)237 public void onMessageReceived(final MessageEvent messageEvent) { 238 LOGD(TAG, "onMessageReceived() A message from watch was received:" + messageEvent 239 .getRequestId() + " " + messageEvent.getPath()); 240 mHandler.post(new Runnable() { 241 @Override 242 public void run() { 243 mDataItemListAdapter.add(new Event("Message from watch", messageEvent.toString())); 244 } 245 }); 246 247 } 248 249 @Override //NodeListener onPeerConnected(final Node peer)250 public void onPeerConnected(final Node peer) { 251 LOGD(TAG, "onPeerConnected: " + peer); 252 mHandler.post(new Runnable() { 253 @Override 254 public void run() { 255 mDataItemListAdapter.add(new Event("Connected", peer.toString())); 256 } 257 }); 258 259 } 260 261 @Override //NodeListener onPeerDisconnected(final Node peer)262 public void onPeerDisconnected(final Node peer) { 263 LOGD(TAG, "onPeerDisconnected: " + peer); 264 mHandler.post(new Runnable() { 265 @Override 266 public void run() { 267 mDataItemListAdapter.add(new Event("Disconnected", peer.toString())); 268 } 269 }); 270 } 271 272 /** 273 * A View Adapter for presenting the Event objects in a list 274 */ 275 private static class DataItemAdapter extends ArrayAdapter<Event> { 276 277 private final Context mContext; 278 DataItemAdapter(Context context, int unusedResource)279 public DataItemAdapter(Context context, int unusedResource) { 280 super(context, unusedResource); 281 mContext = context; 282 } 283 284 @Override getView(int position, View convertView, ViewGroup parent)285 public View getView(int position, View convertView, ViewGroup parent) { 286 ViewHolder holder; 287 if (convertView == null) { 288 holder = new ViewHolder(); 289 LayoutInflater inflater = (LayoutInflater) mContext.getSystemService( 290 Context.LAYOUT_INFLATER_SERVICE); 291 convertView = inflater.inflate(android.R.layout.two_line_list_item, null); 292 convertView.setTag(holder); 293 holder.text1 = (TextView) convertView.findViewById(android.R.id.text1); 294 holder.text2 = (TextView) convertView.findViewById(android.R.id.text2); 295 } else { 296 holder = (ViewHolder) convertView.getTag(); 297 } 298 Event event = getItem(position); 299 holder.text1.setText(event.title); 300 holder.text2.setText(event.text); 301 return convertView; 302 } 303 304 private class ViewHolder { 305 306 TextView text1; 307 TextView text2; 308 } 309 } 310 311 private class Event { 312 313 String title; 314 String text; 315 Event(String title, String text)316 public Event(String title, String text) { 317 this.title = title; 318 this.text = text; 319 } 320 } 321 getNodes()322 private Collection<String> getNodes() { 323 HashSet<String> results = new HashSet<String>(); 324 NodeApi.GetConnectedNodesResult nodes = 325 Wearable.NodeApi.getConnectedNodes(mGoogleApiClient).await(); 326 327 for (Node node : nodes.getNodes()) { 328 results.add(node.getId()); 329 } 330 331 return results; 332 } 333 sendStartActivityMessage(String node)334 private void sendStartActivityMessage(String node) { 335 Wearable.MessageApi.sendMessage( 336 mGoogleApiClient, node, START_ACTIVITY_PATH, new byte[0]).setResultCallback( 337 new ResultCallback<SendMessageResult>() { 338 @Override 339 public void onResult(SendMessageResult sendMessageResult) { 340 if (!sendMessageResult.getStatus().isSuccess()) { 341 Log.e(TAG, "Failed to send message with status code: " 342 + sendMessageResult.getStatus().getStatusCode()); 343 } 344 } 345 } 346 ); 347 } 348 349 private class StartWearableActivityTask extends AsyncTask<Void, Void, Void> { 350 351 @Override doInBackground(Void... args)352 protected Void doInBackground(Void... args) { 353 Collection<String> nodes = getNodes(); 354 for (String node : nodes) { 355 sendStartActivityMessage(node); 356 } 357 return null; 358 } 359 } 360 361 /** Sends an RPC to start a fullscreen Activity on the wearable. */ onStartWearableActivityClick(View view)362 public void onStartWearableActivityClick(View view) { 363 LOGD(TAG, "Generating RPC"); 364 365 // Trigger an AsyncTask that will query for a list of connected nodes and send a 366 // "start-activity" message to each connected node. 367 new StartWearableActivityTask().execute(); 368 } 369 370 /** Generates a DataItem based on an incrementing count. */ 371 private class DataItemGenerator implements Runnable { 372 373 private int count = 0; 374 375 @Override run()376 public void run() { 377 PutDataMapRequest putDataMapRequest = PutDataMapRequest.create(COUNT_PATH); 378 putDataMapRequest.getDataMap().putInt(COUNT_KEY, count++); 379 PutDataRequest request = putDataMapRequest.asPutDataRequest(); 380 381 LOGD(TAG, "Generating DataItem: " + request); 382 if (!mGoogleApiClient.isConnected()) { 383 return; 384 } 385 Wearable.DataApi.putDataItem(mGoogleApiClient, request) 386 .setResultCallback(new ResultCallback<DataItemResult>() { 387 @Override 388 public void onResult(DataItemResult dataItemResult) { 389 if (!dataItemResult.getStatus().isSuccess()) { 390 Log.e(TAG, "ERROR: failed to putDataItem, status code: " 391 + dataItemResult.getStatus().getStatusCode()); 392 } 393 } 394 }); 395 } 396 } 397 398 /** 399 * Dispatches an {@link android.content.Intent} to take a photo. Result will be returned back 400 * in onActivityResult(). 401 */ dispatchTakePictureIntent()402 private void dispatchTakePictureIntent() { 403 Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); 404 if (takePictureIntent.resolveActivity(getPackageManager()) != null) { 405 startActivityForResult(takePictureIntent, REQUEST_IMAGE_CAPTURE); 406 } 407 } 408 409 /** 410 * Builds an {@link com.google.android.gms.wearable.Asset} from a bitmap. The image that we get 411 * back from the camera in "data" is a thumbnail size. Typically, your image should not exceed 412 * 320x320 and if you want to have zoom and parallax effect in your app, limit the size of your 413 * image to 640x400. Resize your image before transferring to your wearable device. 414 */ toAsset(Bitmap bitmap)415 private static Asset toAsset(Bitmap bitmap) { 416 ByteArrayOutputStream byteStream = null; 417 try { 418 byteStream = new ByteArrayOutputStream(); 419 bitmap.compress(Bitmap.CompressFormat.PNG, 100, byteStream); 420 return Asset.createFromBytes(byteStream.toByteArray()); 421 } finally { 422 if (null != byteStream) { 423 try { 424 byteStream.close(); 425 } catch (IOException e) { 426 // ignore 427 } 428 } 429 } 430 } 431 432 /** 433 * Sends the asset that was created form the photo we took by adding it to the Data Item store. 434 */ sendPhoto(Asset asset)435 private void sendPhoto(Asset asset) { 436 PutDataMapRequest dataMap = PutDataMapRequest.create(IMAGE_PATH); 437 dataMap.getDataMap().putAsset(IMAGE_KEY, asset); 438 dataMap.getDataMap().putLong("time", new Date().getTime()); 439 PutDataRequest request = dataMap.asPutDataRequest(); 440 Wearable.DataApi.putDataItem(mGoogleApiClient, request) 441 .setResultCallback(new ResultCallback<DataItemResult>() { 442 @Override 443 public void onResult(DataItemResult dataItemResult) { 444 LOGD(TAG, "Sending image was successful: " + dataItemResult.getStatus() 445 .isSuccess()); 446 } 447 }); 448 449 } 450 onTakePhotoClick(View view)451 public void onTakePhotoClick(View view) { 452 dispatchTakePictureIntent(); 453 } 454 onSendPhotoClick(View view)455 public void onSendPhotoClick(View view) { 456 if (null != mImageBitmap && mGoogleApiClient.isConnected()) { 457 sendPhoto(toAsset(mImageBitmap)); 458 } 459 } 460 461 /** 462 * Sets up UI components and their callback handlers. 463 */ setupViews()464 private void setupViews() { 465 mTakePhotoBtn = (Button) findViewById(R.id.takePhoto); 466 mSendPhotoBtn = (Button) findViewById(R.id.sendPhoto); 467 468 // Shows the image received from the handset 469 mThumbView = (ImageView) findViewById(R.id.imageView); 470 mDataItemList = (ListView) findViewById(R.id.data_item_list); 471 472 mStartActivityBtn = findViewById(R.id.start_wearable_activity); 473 } 474 475 /** 476 * As simple wrapper around Log.d 477 */ LOGD(final String tag, String message)478 private static void LOGD(final String tag, String message) { 479 if (Log.isLoggable(tag, Log.DEBUG)) { 480 Log.d(tag, message); 481 } 482 } 483 484 } 485