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 }