| /* |
| * Copyright (C) 2014 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.example.android.wearable.datalayer; |
| |
| import android.app.Activity; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.IntentSender; |
| import android.content.pm.PackageManager; |
| import android.graphics.Bitmap; |
| import android.net.Uri; |
| import android.os.AsyncTask; |
| import android.os.Bundle; |
| import android.provider.MediaStore; |
| import android.util.Log; |
| import android.view.LayoutInflater; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.widget.ArrayAdapter; |
| import android.widget.Button; |
| import android.widget.ImageView; |
| import android.widget.ListView; |
| import android.widget.TextView; |
| |
| import com.google.android.gms.common.ConnectionResult; |
| import com.google.android.gms.common.api.GoogleApiClient; |
| import com.google.android.gms.common.api.GoogleApiClient.ConnectionCallbacks; |
| import com.google.android.gms.common.api.GoogleApiClient.OnConnectionFailedListener; |
| import com.google.android.gms.common.api.ResultCallback; |
| import com.google.android.gms.wearable.Asset; |
| import com.google.android.gms.wearable.CapabilityApi; |
| import com.google.android.gms.wearable.CapabilityInfo; |
| import com.google.android.gms.wearable.DataApi; |
| import com.google.android.gms.wearable.DataApi.DataItemResult; |
| import com.google.android.gms.wearable.DataEvent; |
| import com.google.android.gms.wearable.DataEventBuffer; |
| import com.google.android.gms.wearable.MessageApi; |
| import com.google.android.gms.wearable.MessageApi.SendMessageResult; |
| import com.google.android.gms.wearable.MessageEvent; |
| import com.google.android.gms.wearable.Node; |
| import com.google.android.gms.wearable.NodeApi; |
| import com.google.android.gms.wearable.PutDataMapRequest; |
| import com.google.android.gms.wearable.PutDataRequest; |
| import com.google.android.gms.wearable.Wearable; |
| |
| import java.io.ByteArrayOutputStream; |
| import java.io.IOException; |
| import java.util.Collection; |
| import java.util.Date; |
| import java.util.HashSet; |
| import java.util.concurrent.ScheduledExecutorService; |
| import java.util.concurrent.ScheduledFuture; |
| import java.util.concurrent.ScheduledThreadPoolExecutor; |
| import java.util.concurrent.TimeUnit; |
| |
| /** |
| * Receives its own events using a listener API designed for foreground activities. Updates a data |
| * item every second while it is open. Also allows user to take a photo and send that as an asset |
| * to the paired wearable. |
| */ |
| public class MainActivity extends Activity implements |
| CapabilityApi.CapabilityListener, |
| MessageApi.MessageListener, |
| DataApi.DataListener, |
| ConnectionCallbacks, |
| OnConnectionFailedListener { |
| |
| private static final String TAG = "MainActivity"; |
| |
| //Request code for launching the Intent to resolve Google Play services errors. |
| private static final int REQUEST_RESOLVE_ERROR = 1000; |
| |
| private static final int REQUEST_IMAGE_CAPTURE = 1; |
| |
| private static final String START_ACTIVITY_PATH = "/start-activity"; |
| private static final String COUNT_PATH = "/count"; |
| private static final String IMAGE_PATH = "/image"; |
| private static final String IMAGE_KEY = "photo"; |
| private static final String COUNT_KEY = "count"; |
| |
| private GoogleApiClient mGoogleApiClient; |
| private boolean mResolvingError = false; |
| private boolean mCameraSupported = false; |
| |
| private ListView mDataItemList; |
| private Button mSendPhotoBtn; |
| private ImageView mThumbView; |
| private Bitmap mImageBitmap; |
| private View mStartActivityBtn; |
| |
| private DataItemAdapter mDataItemListAdapter; |
| |
| // Send DataItems. |
| private ScheduledExecutorService mGeneratorExecutor; |
| private ScheduledFuture<?> mDataItemGeneratorFuture; |
| |
| @Override |
| public void onCreate(Bundle savedInstanceState) { |
| super.onCreate(savedInstanceState); |
| LOGD(TAG, "onCreate"); |
| mCameraSupported = getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA); |
| setContentView(R.layout.main_activity); |
| setupViews(); |
| |
| // Stores DataItems received by the local broadcaster or from the paired watch. |
| mDataItemListAdapter = new DataItemAdapter(this, android.R.layout.simple_list_item_1); |
| mDataItemList.setAdapter(mDataItemListAdapter); |
| |
| mGeneratorExecutor = new ScheduledThreadPoolExecutor(1); |
| |
| mGoogleApiClient = new GoogleApiClient.Builder(this) |
| .addApi(Wearable.API) |
| .addConnectionCallbacks(this) |
| .addOnConnectionFailedListener(this) |
| .build(); |
| } |
| |
| @Override |
| protected void onStart() { |
| super.onStart(); |
| if (!mResolvingError) { |
| mGoogleApiClient.connect(); |
| } |
| } |
| |
| @Override |
| public void onResume() { |
| super.onResume(); |
| mDataItemGeneratorFuture = mGeneratorExecutor.scheduleWithFixedDelay( |
| new DataItemGenerator(), 1, 5, TimeUnit.SECONDS); |
| } |
| |
| @Override |
| public void onPause() { |
| super.onPause(); |
| mDataItemGeneratorFuture.cancel(true /* mayInterruptIfRunning */); |
| } |
| |
| @Override |
| protected void onStop() { |
| if (!mResolvingError && (mGoogleApiClient != null) && (mGoogleApiClient.isConnected())) { |
| Wearable.DataApi.removeListener(mGoogleApiClient, this); |
| Wearable.MessageApi.removeListener(mGoogleApiClient, this); |
| Wearable.CapabilityApi.removeListener(mGoogleApiClient, this); |
| mGoogleApiClient.disconnect(); |
| } |
| super.onStop(); |
| } |
| |
| @Override |
| protected void onActivityResult(int requestCode, int resultCode, Intent data) { |
| if (requestCode == REQUEST_IMAGE_CAPTURE && resultCode == RESULT_OK) { |
| Bundle extras = data.getExtras(); |
| mImageBitmap = (Bitmap) extras.get("data"); |
| mThumbView.setImageBitmap(mImageBitmap); |
| } |
| } |
| |
| @Override |
| public void onConnected(Bundle connectionHint) { |
| LOGD(TAG, "Google API Client was connected"); |
| mResolvingError = false; |
| mStartActivityBtn.setEnabled(true); |
| mSendPhotoBtn.setEnabled(mCameraSupported); |
| Wearable.DataApi.addListener(mGoogleApiClient, this); |
| Wearable.MessageApi.addListener(mGoogleApiClient, this); |
| Wearable.CapabilityApi.addListener( |
| mGoogleApiClient, this, Uri.parse("wear://"), CapabilityApi.FILTER_REACHABLE); |
| } |
| |
| @Override |
| public void onConnectionSuspended(int cause) { |
| LOGD(TAG, "Connection to Google API client was suspended"); |
| mStartActivityBtn.setEnabled(false); |
| mSendPhotoBtn.setEnabled(false); |
| } |
| |
| @Override |
| public void onConnectionFailed(ConnectionResult result) { |
| if (!mResolvingError) { |
| |
| if (result.hasResolution()) { |
| try { |
| mResolvingError = true; |
| result.startResolutionForResult(this, REQUEST_RESOLVE_ERROR); |
| } catch (IntentSender.SendIntentException e) { |
| // There was an error with the resolution intent. Try again. |
| mGoogleApiClient.connect(); |
| } |
| } else { |
| Log.e(TAG, "Connection to Google API client has failed"); |
| mResolvingError = false; |
| mStartActivityBtn.setEnabled(false); |
| mSendPhotoBtn.setEnabled(false); |
| Wearable.DataApi.removeListener(mGoogleApiClient, this); |
| Wearable.MessageApi.removeListener(mGoogleApiClient, this); |
| Wearable.CapabilityApi.removeListener(mGoogleApiClient, this); |
| } |
| } |
| } |
| |
| @Override |
| public void onDataChanged(DataEventBuffer dataEvents) { |
| LOGD(TAG, "onDataChanged: " + dataEvents); |
| |
| for (DataEvent event : dataEvents) { |
| if (event.getType() == DataEvent.TYPE_CHANGED) { |
| mDataItemListAdapter.add( |
| new Event("DataItem Changed", event.getDataItem().toString())); |
| } else if (event.getType() == DataEvent.TYPE_DELETED) { |
| mDataItemListAdapter.add( |
| new Event("DataItem Deleted", event.getDataItem().toString())); |
| } |
| } |
| } |
| |
| @Override |
| public void onMessageReceived(final MessageEvent messageEvent) { |
| LOGD(TAG, "onMessageReceived() A message from watch was received:" |
| + messageEvent.getRequestId() + " " + messageEvent.getPath()); |
| |
| mDataItemListAdapter.add(new Event("Message from watch", messageEvent.toString())); |
| } |
| |
| @Override |
| public void onCapabilityChanged(final CapabilityInfo capabilityInfo) { |
| LOGD(TAG, "onCapabilityChanged: " + capabilityInfo); |
| |
| mDataItemListAdapter.add(new Event("onCapabilityChanged", capabilityInfo.toString())); |
| } |
| |
| /** |
| * Sets up UI components and their callback handlers. |
| */ |
| private void setupViews() { |
| mSendPhotoBtn = (Button) findViewById(R.id.sendPhoto); |
| mThumbView = (ImageView) findViewById(R.id.imageView); |
| mDataItemList = (ListView) findViewById(R.id.data_item_list); |
| mStartActivityBtn = findViewById(R.id.start_wearable_activity); |
| } |
| |
| public void onTakePhotoClick(View view) { |
| dispatchTakePictureIntent(); |
| } |
| |
| public void onSendPhotoClick(View view) { |
| if (null != mImageBitmap && mGoogleApiClient.isConnected()) { |
| sendPhoto(toAsset(mImageBitmap)); |
| } |
| } |
| |
| /** |
| * Sends an RPC to start a fullscreen Activity on the wearable. |
| */ |
| public void onStartWearableActivityClick(View view) { |
| LOGD(TAG, "Generating RPC"); |
| |
| // Trigger an AsyncTask that will query for a list of connected nodes and send a |
| // "start-activity" message to each connected node. |
| new StartWearableActivityTask().execute(); |
| } |
| |
| private void sendStartActivityMessage(String node) { |
| Wearable.MessageApi.sendMessage( |
| mGoogleApiClient, node, START_ACTIVITY_PATH, new byte[0]).setResultCallback( |
| new ResultCallback<SendMessageResult>() { |
| @Override |
| public void onResult(SendMessageResult sendMessageResult) { |
| if (!sendMessageResult.getStatus().isSuccess()) { |
| Log.e(TAG, "Failed to send message with status code: " |
| + sendMessageResult.getStatus().getStatusCode()); |
| } |
| } |
| } |
| ); |
| } |
| |
| /** |
| * Dispatches an {@link android.content.Intent} to take a photo. Result will be returned back |
| * in onActivityResult(). |
| */ |
| private void dispatchTakePictureIntent() { |
| Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); |
| if (takePictureIntent.resolveActivity(getPackageManager()) != null) { |
| startActivityForResult(takePictureIntent, REQUEST_IMAGE_CAPTURE); |
| } |
| } |
| |
| /** |
| * Builds an {@link com.google.android.gms.wearable.Asset} from a bitmap. The image that we get |
| * back from the camera in "data" is a thumbnail size. Typically, your image should not exceed |
| * 320x320 and if you want to have zoom and parallax effect in your app, limit the size of your |
| * image to 640x400. Resize your image before transferring to your wearable device. |
| */ |
| private static Asset toAsset(Bitmap bitmap) { |
| ByteArrayOutputStream byteStream = null; |
| try { |
| byteStream = new ByteArrayOutputStream(); |
| bitmap.compress(Bitmap.CompressFormat.PNG, 100, byteStream); |
| return Asset.createFromBytes(byteStream.toByteArray()); |
| } finally { |
| if (null != byteStream) { |
| try { |
| byteStream.close(); |
| } catch (IOException e) { |
| // ignore |
| } |
| } |
| } |
| } |
| |
| /** |
| * Sends the asset that was created from the photo we took by adding it to the Data Item store. |
| */ |
| private void sendPhoto(Asset asset) { |
| PutDataMapRequest dataMap = PutDataMapRequest.create(IMAGE_PATH); |
| dataMap.getDataMap().putAsset(IMAGE_KEY, asset); |
| dataMap.getDataMap().putLong("time", new Date().getTime()); |
| PutDataRequest request = dataMap.asPutDataRequest(); |
| request.setUrgent(); |
| |
| Wearable.DataApi.putDataItem(mGoogleApiClient, request) |
| .setResultCallback(new ResultCallback<DataItemResult>() { |
| @Override |
| public void onResult(DataItemResult dataItemResult) { |
| LOGD(TAG, "Sending image was successful: " + dataItemResult.getStatus() |
| .isSuccess()); |
| } |
| }); |
| } |
| |
| private Collection<String> getNodes() { |
| HashSet<String> results = new HashSet<>(); |
| NodeApi.GetConnectedNodesResult nodes = |
| Wearable.NodeApi.getConnectedNodes(mGoogleApiClient).await(); |
| |
| for (Node node : nodes.getNodes()) { |
| results.add(node.getId()); |
| } |
| |
| return results; |
| } |
| |
| /** |
| * As simple wrapper around Log.d |
| */ |
| private static void LOGD(final String tag, String message) { |
| if (Log.isLoggable(tag, Log.DEBUG)) { |
| Log.d(tag, message); |
| } |
| } |
| |
| /** |
| * A View Adapter for presenting the Event objects in a list |
| */ |
| private static class DataItemAdapter extends ArrayAdapter<Event> { |
| |
| private final Context mContext; |
| |
| public DataItemAdapter(Context context, int unusedResource) { |
| super(context, unusedResource); |
| mContext = context; |
| } |
| |
| @Override |
| public View getView(int position, View convertView, ViewGroup parent) { |
| ViewHolder holder; |
| if (convertView == null) { |
| holder = new ViewHolder(); |
| LayoutInflater inflater = (LayoutInflater) mContext.getSystemService( |
| Context.LAYOUT_INFLATER_SERVICE); |
| convertView = inflater.inflate(android.R.layout.two_line_list_item, null); |
| convertView.setTag(holder); |
| holder.text1 = (TextView) convertView.findViewById(android.R.id.text1); |
| holder.text2 = (TextView) convertView.findViewById(android.R.id.text2); |
| } else { |
| holder = (ViewHolder) convertView.getTag(); |
| } |
| Event event = getItem(position); |
| holder.text1.setText(event.title); |
| holder.text2.setText(event.text); |
| return convertView; |
| } |
| |
| private class ViewHolder { |
| TextView text1; |
| TextView text2; |
| } |
| } |
| |
| private class Event { |
| |
| String title; |
| String text; |
| |
| public Event(String title, String text) { |
| this.title = title; |
| this.text = text; |
| } |
| } |
| |
| private class StartWearableActivityTask extends AsyncTask<Void, Void, Void> { |
| |
| @Override |
| protected Void doInBackground(Void... args) { |
| Collection<String> nodes = getNodes(); |
| for (String node : nodes) { |
| sendStartActivityMessage(node); |
| } |
| return null; |
| } |
| } |
| |
| /** |
| * Generates a DataItem based on an incrementing count. |
| */ |
| private class DataItemGenerator implements Runnable { |
| |
| private int count = 0; |
| |
| @Override |
| public void run() { |
| PutDataMapRequest putDataMapRequest = PutDataMapRequest.create(COUNT_PATH); |
| putDataMapRequest.getDataMap().putInt(COUNT_KEY, count++); |
| |
| PutDataRequest request = putDataMapRequest.asPutDataRequest(); |
| request.setUrgent(); |
| |
| LOGD(TAG, "Generating DataItem: " + request); |
| if (!mGoogleApiClient.isConnected()) { |
| return; |
| } |
| Wearable.DataApi.putDataItem(mGoogleApiClient, request) |
| .setResultCallback(new ResultCallback<DataItemResult>() { |
| @Override |
| public void onResult(DataItemResult dataItemResult) { |
| if (!dataItemResult.getStatus().isSuccess()) { |
| Log.e(TAG, "ERROR: failed to putDataItem, status code: " |
| + dataItemResult.getStatus().getStatusCode()); |
| } |
| } |
| }); |
| } |
| } |
| } |