Home | History | Annotate | Download | only in 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.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
    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
    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
    142     protected void onStart() {
    143         super.onStart();
    144         if (!mResolvingError) {
    145             mGoogleApiClient.connect();
    146         }
    147     }
    148 
    149     @Override
    150     public void onResume() {
    151         super.onResume();
    152         mDataItemGeneratorFuture = mGeneratorExecutor.scheduleWithFixedDelay(
    153                 new DataItemGenerator(), 1, 5, TimeUnit.SECONDS);
    154     }
    155 
    156     @Override
    157     public void onPause() {
    158         super.onPause();
    159         mDataItemGeneratorFuture.cancel(true /* mayInterruptIfRunning */);
    160     }
    161 
    162     @Override
    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
    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
    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
    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
    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
    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
    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
    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 
    279         public DataItemAdapter(Context context, int unusedResource) {
    280             super(context, unusedResource);
    281             mContext = context;
    282         }
    283 
    284         @Override
    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 
    316         public Event(String title, String text) {
    317             this.title = title;
    318             this.text = text;
    319         }
    320     }
    321 
    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 
    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
    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. */
    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
    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      */
    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      */
    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      */
    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 
    451     public void onTakePhotoClick(View view) {
    452         dispatchTakePictureIntent();
    453     }
    454 
    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      */
    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      */
    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