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
onCreate(Bundle savedInstanceState)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
onNewIntent(Intent intent)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
onDestroy()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
onEnterAmbient(Bundle ambientDetails)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
onUpdateAmbient()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
onExitAmbient()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      */
refreshDisplayAndSetNextUpdate()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      */
loadDataAndUpdateScreen()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 
UpdateHandler(MainActivity reference)319         public UpdateHandler(MainActivity reference) {
320             mMainActivityWeakReference = new WeakReference<MainActivity>(reference);
321         }
322 
323         @Override
handleMessage(Message message)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 }