Home | History | Annotate | Download | only in com.example.android.wearable.wear.alwayson
      1 /*
      2  * Copyright (C) 2015 Google Inc. All Rights Reserved.
      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 package com.example.android.wearable.wear.alwayson;
     17 
     18 import android.app.AlarmManager;
     19 import android.app.PendingIntent;
     20 import android.content.Context;
     21 import android.content.Intent;
     22 import android.graphics.Color;
     23 import android.os.Bundle;
     24 import android.os.Handler;
     25 import android.os.Message;
     26 import android.support.wearable.activity.WearableActivity;
     27 import android.support.wearable.view.WatchViewStub;
     28 import android.util.Log;
     29 import android.widget.TextView;
     30 
     31 import java.lang.ref.WeakReference;
     32 import java.text.SimpleDateFormat;
     33 import java.util.Date;
     34 import java.util.Locale;
     35 import java.util.concurrent.TimeUnit;
     36 
     37 /**
     38  * Demonstrates support for Ambient screens by extending WearableActivity and overriding
     39  * onEnterAmbient, onUpdateAmbient, and onExitAmbient.
     40  *
     41  * There are two modes (Active and Ambient). To trigger future updates (data/screen), we use a
     42  * custom Handler for the "Active" mode and an Alarm for the "Ambient" mode.
     43  *
     44  * Why don't we use just one? Handlers are generally less battery intensive and can be triggered
     45  * every second. However, they can not wake up the processor (common in Ambient mode).
     46  *
     47  * Alarms can wake up the processor (what we need for Ambient), but they struggle with quick updates
     48  * (less than one second) and are much less efficient compared to Handlers.
     49  *
     50  * Therefore, we use Handlers for "Active" mode (can trigger every second and are better on the
     51  * battery), and we use Alarms for "Ambient" mode (only need to update once every 20 seconds and
     52  * they can wake up a sleeping processor).
     53  *
     54  * Again, the Activity waits 20 seconds between doing any processing (getting data, updating screen
     55  * etc.) while in ambient mode to conserving battery life (processor allowed to sleep). If you can
     56  * hold off on updates for a full minute, you can throw away all the Alarm code and just use
     57  * onUpdateAmbient() to save even more battery life.
     58  *
     59  * As always, you will still want to apply the performance guidelines outlined in the Watch Faces
     60  * documention to your app.
     61  *
     62  * Finally, in ambient mode, this Activity follows the same best practices outlined in the
     63  * Watch Faces API documentation, e.g., keep most pixels black, avoid large blocks of white pixels,
     64  * use only black and white, and disable anti-aliasing.
     65  *
     66  */
     67 public class MainActivity extends WearableActivity {
     68 
     69     private static final String TAG = "MainActivity";
     70 
     71     /** Custom 'what' for Message sent to Handler. */
     72     private static final int MSG_UPDATE_SCREEN = 0;
     73 
     74     /** Milliseconds between updates based on state. */
     75     private static final long ACTIVE_INTERVAL_MS = TimeUnit.SECONDS.toMillis(1);
     76     private static final long AMBIENT_INTERVAL_MS = TimeUnit.SECONDS.toMillis(20);
     77 
     78     /** Tracks latest ambient details, such as burnin offsets, etc. */
     79     private Bundle mAmbientDetails;
     80 
     81     private TextView mTimeTextView;
     82     private TextView mTimeStampTextView;
     83     private TextView mStateTextView;
     84     private TextView mUpdateRateTextView;
     85     private TextView mDrawCountTextView;
     86 
     87     private final SimpleDateFormat sDateFormat =
     88             new SimpleDateFormat("HH:mm:ss", Locale.US);
     89 
     90     private volatile int mDrawCount = 0;
     91 
     92 
     93     /**
     94      * Since the handler (used in active mode) can't wake up the processor when the device is in
     95      * ambient mode and undocked, we use an Alarm to cover ambient mode updates when we need them
     96      * more frequently than every minute. Remember, if getting updates once a minute in ambient
     97      * mode is enough, you can do away with the Alarm code and just rely on the onUpdateAmbient()
     98      * callback.
     99      */
    100     private AlarmManager mAmbientStateAlarmManager;
    101     private PendingIntent mAmbientStatePendingIntent;
    102 
    103     /**
    104      * This custom handler is used for updates in "Active" mode. We use a separate static class to
    105      * help us avoid memory leaks.
    106      */
    107     private final Handler mActiveModeUpdateHandler = new UpdateHandler(this);
    108 
    109     @Override
    110     public void onCreate(Bundle savedInstanceState) {
    111         Log.d(TAG, "onCreate()");
    112         super.onCreate(savedInstanceState);
    113 
    114         setContentView(R.layout.activity_main);
    115 
    116         setAmbientEnabled();
    117 
    118         mAmbientStateAlarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
    119         Intent ambientStateIntent = new Intent(getApplicationContext(), MainActivity.class);
    120 
    121         mAmbientStatePendingIntent = PendingIntent.getActivity(
    122                 getApplicationContext(),
    123                 0 /* requestCode */,
    124                 ambientStateIntent,
    125                 PendingIntent.FLAG_UPDATE_CURRENT);
    126 
    127 
    128         /** Determines whether watch is round or square and applies proper view. **/
    129         final WatchViewStub stub = (WatchViewStub) findViewById(R.id.watch_view_stub);
    130         stub.setOnLayoutInflatedListener(new WatchViewStub.OnLayoutInflatedListener() {
    131             @Override
    132             public void onLayoutInflated(WatchViewStub stub) {
    133 
    134                 mTimeTextView = (TextView) stub.findViewById(R.id.time);
    135                 mTimeStampTextView = (TextView) stub.findViewById(R.id.time_stamp);
    136                 mStateTextView = (TextView) stub.findViewById(R.id.state);
    137                 mUpdateRateTextView = (TextView) stub.findViewById(R.id.update_rate);
    138                 mDrawCountTextView = (TextView) stub.findViewById(R.id.draw_count);
    139 
    140                 refreshDisplayAndSetNextUpdate();
    141             }
    142         });
    143     }
    144 
    145     /**
    146      * This is mostly triggered by the Alarms we set in Ambient mode and informs us we need to
    147      * update the screen (and process any data).
    148      */
    149     @Override
    150     public void onNewIntent(Intent intent) {
    151         Log.d(TAG, "onNewIntent(): " + intent);
    152         super.onNewIntent(intent);
    153 
    154         setIntent(intent);
    155 
    156         refreshDisplayAndSetNextUpdate();
    157     }
    158 
    159     @Override
    160     public void onDestroy() {
    161         Log.d(TAG, "onDestroy()");
    162 
    163         mActiveModeUpdateHandler.removeMessages(MSG_UPDATE_SCREEN);
    164         mAmbientStateAlarmManager.cancel(mAmbientStatePendingIntent);
    165 
    166         super.onDestroy();
    167     }
    168 
    169     /**
    170      * Prepares UI for Ambient view.
    171      */
    172     @Override
    173     public void onEnterAmbient(Bundle ambientDetails) {
    174         Log.d(TAG, "onEnterAmbient()");
    175         super.onEnterAmbient(ambientDetails);
    176 
    177         /**
    178          * In this sample, we aren't using the ambient details bundle (EXTRA_BURN_IN_PROTECTION or
    179          * EXTRA_LOWBIT_AMBIENT), but if you need them, you can pull them from the local variable
    180          * set here.
    181          */
    182         mAmbientDetails = ambientDetails;
    183 
    184         /** Clears Handler queue (only needed for updates in active mode). */
    185         mActiveModeUpdateHandler.removeMessages(MSG_UPDATE_SCREEN);
    186 
    187         /**
    188          * Following best practices outlined in WatchFaces API (keeping most pixels black,
    189          * avoiding large blocks of white pixels, using only black and white,
    190          * and disabling anti-aliasing anti-aliasing, etc.)
    191          */
    192         mStateTextView.setTextColor(Color.WHITE);
    193         mUpdateRateTextView.setTextColor(Color.WHITE);
    194         mDrawCountTextView.setTextColor(Color.WHITE);
    195 
    196         mTimeTextView.getPaint().setAntiAlias(false);
    197         mTimeStampTextView.getPaint().setAntiAlias(false);
    198         mStateTextView.getPaint().setAntiAlias(false);
    199         mUpdateRateTextView.getPaint().setAntiAlias(false);
    200         mDrawCountTextView.getPaint().setAntiAlias(false);
    201 
    202         refreshDisplayAndSetNextUpdate();
    203     }
    204 
    205     /**
    206      * Updates UI in Ambient view (once a minute). Because we need to update UI sooner than that
    207      * (every ~20 seconds), we also use an Alarm. However, since the processor is awake for this
    208      * callback, we might as well call refreshDisplayAndSetNextUpdate() to update screen and reset
    209      * the Alarm.
    210      *
    211      * If you are happy with just updating the screen once a minute in Ambient Mode (which will be
    212      * the case a majority of the time), then you can just use this method and remove all
    213      * references/code regarding Alarms.
    214      */
    215     @Override
    216     public void onUpdateAmbient() {
    217         Log.d(TAG, "onUpdateAmbient()");
    218         super.onUpdateAmbient();
    219 
    220         refreshDisplayAndSetNextUpdate();
    221     }
    222 
    223     /**
    224      * Prepares UI for Active view (non-Ambient).
    225      */
    226     @Override
    227     public void onExitAmbient() {
    228         Log.d(TAG, "onExitAmbient()");
    229         super.onExitAmbient();
    230 
    231         /** Clears out Alarms since they are only used in ambient mode. */
    232         mAmbientStateAlarmManager.cancel(mAmbientStatePendingIntent);
    233 
    234         mStateTextView.setTextColor(Color.GREEN);
    235         mUpdateRateTextView.setTextColor(Color.GREEN);
    236         mDrawCountTextView.setTextColor(Color.GREEN);
    237 
    238         mTimeTextView.getPaint().setAntiAlias(true);
    239         mTimeStampTextView.getPaint().setAntiAlias(true);
    240         mStateTextView.getPaint().setAntiAlias(true);
    241         mUpdateRateTextView.getPaint().setAntiAlias(true);
    242         mDrawCountTextView.getPaint().setAntiAlias(true);
    243 
    244         refreshDisplayAndSetNextUpdate();
    245     }
    246 
    247     /**
    248      * Loads data/updates screen (via method), but most importantly, sets up the next refresh
    249      * (active mode = Handler and ambient mode = Alarm).
    250      */
    251     private void refreshDisplayAndSetNextUpdate() {
    252 
    253         loadDataAndUpdateScreen();
    254 
    255         long timeMs = System.currentTimeMillis();
    256 
    257         if (isAmbient()) {
    258             /** Calculate next trigger time (based on state). */
    259             long delayMs = AMBIENT_INTERVAL_MS - (timeMs % AMBIENT_INTERVAL_MS);
    260             long triggerTimeMs = timeMs + delayMs;
    261 
    262             /**
    263              * Note: Make sure you have set activity launchMode to singleInstance in the manifest.
    264              * Otherwise, it is easy for the AlarmManager launch intent to open a new activity
    265              * every time the Alarm is triggered rather than reusing this Activity.
    266              */
    267             mAmbientStateAlarmManager.setExact(
    268                     AlarmManager.RTC_WAKEUP,
    269                     triggerTimeMs,
    270                     mAmbientStatePendingIntent);
    271 
    272         } else {
    273             /** Calculate next trigger time (based on state). */
    274             long delayMs = ACTIVE_INTERVAL_MS - (timeMs % ACTIVE_INTERVAL_MS);
    275 
    276             mActiveModeUpdateHandler.removeMessages(MSG_UPDATE_SCREEN);
    277             mActiveModeUpdateHandler.sendEmptyMessageDelayed(MSG_UPDATE_SCREEN, delayMs);
    278         }
    279     }
    280 
    281     /**
    282      * Updates display based on Ambient state. If you need to pull data, you should do it here.
    283      */
    284     private void loadDataAndUpdateScreen() {
    285 
    286         mDrawCount += 1;
    287         long currentTimeMs = System.currentTimeMillis();
    288         Log.d(TAG, "loadDataAndUpdateScreen(): " + currentTimeMs + "(" + isAmbient() + ")");
    289 
    290         if (isAmbient()) {
    291 
    292             mTimeTextView.setText(sDateFormat.format(new Date()));
    293             mTimeStampTextView.setText(getString(R.string.timestamp_label, currentTimeMs));
    294 
    295             mStateTextView.setText(getString(R.string.mode_ambient_label));
    296             mUpdateRateTextView.setText(
    297                     getString(R.string.update_rate_label, (AMBIENT_INTERVAL_MS / 1000)));
    298 
    299             mDrawCountTextView.setText(getString(R.string.draw_count_label, mDrawCount));
    300 
    301         } else {
    302             mTimeTextView.setText(sDateFormat.format(new Date()));
    303             mTimeStampTextView.setText(getString(R.string.timestamp_label, currentTimeMs));
    304 
    305             mStateTextView.setText(getString(R.string.mode_active_label));
    306             mUpdateRateTextView.setText(
    307                     getString(R.string.update_rate_label, (ACTIVE_INTERVAL_MS / 1000)));
    308 
    309             mDrawCountTextView.setText(getString(R.string.draw_count_label, mDrawCount));
    310         }
    311     }
    312 
    313     /**
    314      * Handler separated into static class to avoid memory leaks.
    315      */
    316     private static class UpdateHandler extends Handler {
    317         private final WeakReference<MainActivity> mMainActivityWeakReference;
    318 
    319         public UpdateHandler(MainActivity reference) {
    320             mMainActivityWeakReference = new WeakReference<MainActivity>(reference);
    321         }
    322 
    323         @Override
    324         public void handleMessage(Message message) {
    325             MainActivity mainActivity = mMainActivityWeakReference.get();
    326 
    327             if (mainActivity != null) {
    328                 switch (message.what) {
    329                     case MSG_UPDATE_SCREEN:
    330                         mainActivity.refreshDisplayAndSetNextUpdate();
    331                         break;
    332                 }
    333             }
    334         }
    335     }
    336 }