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