Home | History | Annotate | Download | only in com.example.android.wearable.datalayer
      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