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.BroadcastReceiver;
21 import android.content.Context;
22 import android.content.Intent;
23 import android.content.IntentFilter;
24 import android.graphics.Color;
25 import android.os.Bundle;
26 import android.os.Handler;
27 import android.os.Message;
28 import android.util.Log;
29 import android.view.View;
30 import android.widget.TextView;
31 
32 import androidx.fragment.app.FragmentActivity;
33 import androidx.wear.ambient.AmbientModeSupport;
34 
35 import java.lang.ref.WeakReference;
36 import java.text.SimpleDateFormat;
37 import java.util.Date;
38 import java.util.Locale;
39 import java.util.concurrent.TimeUnit;
40 
41 /**
42  * Demonstrates support for <i>Ambient Mode</i> by attaching ambient mode support to the activity,
43  * and listening for ambient mode updates (onEnterAmbient, onUpdateAmbient, and onExitAmbient) via a
44  * named AmbientCallback subclass.
45  *
46  * <p>Also demonstrates how to update the display more frequently than every 60 seconds, which is
47  * the default frequency, using an AlarmManager. The Alarm code is only necessary for the custom
48  * refresh frequency; it can be ignored for basic ambient mode support where you can simply rely on
49  * calls to onUpdateAmbient() by the system.
50  *
51  * <p>There are two modes: <i>ambient</i> and <i>active</i>. To trigger future display updates, we
52  * use a custom Handler for active mode and an Alarm for ambient mode.
53  *
54  * <p>Why not use just one or the other? Handlers are generally less battery intensive and can be
55  * triggered every second. However, they can not wake up the processor (common in ambient mode).
56  *
57  * <p>Alarms can wake up the processor (what we need for ambient move), but they are less efficient
58  * compared to Handlers when it comes to quick update frequencies.
59  *
60  * <p>Therefore, we use Handler for active mode (can trigger every second and are better on the
61  * battery), and we use an Alarm for ambient mode (only need to update once every 10 seconds and
62  * they can wake up a sleeping processor).
63  *
64  * <p>The activity waits 10 seconds between doing any processing (getting data, updating display
65  * etc.) while in ambient mode to conserving battery life (processor allowed to sleep). If your app
66  * can wait 60 seconds for display updates, you can disregard the Alarm code and simply use
67  * onUpdateAmbient() to save even more battery life.
68  *
69  * <p>As always, you will still want to apply the performance guidelines outlined in the Watch Faces
70  * documentation to your app.
71  *
72  * <p>Finally, in ambient mode, this activity follows the same best practices outlined in the Watch
73  * Faces API documentation: keeping most pixels black, avoiding large blocks of white pixels, using
74  * only black and white, disabling anti-aliasing, etc.
75  */
76 public class MainActivity extends FragmentActivity
77         implements AmbientModeSupport.AmbientCallbackProvider {
78 
79     private static final String TAG = "MainActivity";
80 
81     /** Custom 'what' for Message sent to Handler. */
82     private static final int MSG_UPDATE_SCREEN = 0;
83 
84     /** Milliseconds between updates based on state. */
85     private static final long ACTIVE_INTERVAL_MS = TimeUnit.SECONDS.toMillis(1);
86 
87     private static final long AMBIENT_INTERVAL_MS = TimeUnit.SECONDS.toMillis(10);
88 
89     /** Action for updating the display in ambient mode, per our custom refresh cycle. */
90     private static final String AMBIENT_UPDATE_ACTION =
91             "com.example.android.wearable.wear.alwayson.action.AMBIENT_UPDATE";
92 
93     /** Number of pixels to offset the content rendered in the display to prevent screen burn-in. */
94     private static final int BURN_IN_OFFSET_PX = 10;
95 
96     /**
97      * Ambient mode controller attached to this display. Used by Activity to see if it is in ambient
98      * mode.
99      */
100     private AmbientModeSupport.AmbientController mAmbientController;
101 
102     /** If the display is low-bit in ambient mode. i.e. it requires anti-aliased fonts. */
103     boolean mIsLowBitAmbient;
104 
105     /**
106      * If the display requires burn-in protection in ambient mode, rendered pixels need to be
107      * intermittently offset to avoid screen burn-in.
108      */
109     boolean mDoBurnInProtection;
110 
111     private View mContentView;
112     private TextView mTimeTextView;
113     private TextView mTimeStampTextView;
114     private TextView mStateTextView;
115     private TextView mUpdateRateTextView;
116     private TextView mDrawCountTextView;
117 
118     private final SimpleDateFormat sDateFormat = new SimpleDateFormat("HH:mm:ss", Locale.US);
119 
120     private volatile int mDrawCount = 0;
121 
122     /**
123      * Since the handler (used in active mode) can't wake up the processor when the device is in
124      * ambient mode and undocked, we use an Alarm to cover ambient mode updates when we need them
125      * more frequently than every minute. Remember, if getting updates once a minute in ambient mode
126      * is enough, you can do away with the Alarm code and just rely on the onUpdateAmbient()
127      * callback.
128      */
129     private AlarmManager mAmbientUpdateAlarmManager;
130 
131     private PendingIntent mAmbientUpdatePendingIntent;
132     private BroadcastReceiver mAmbientUpdateBroadcastReceiver;
133 
134     /**
135      * This custom handler is used for updates in "Active" mode. We use a separate static class to
136      * help us avoid memory leaks.
137      */
138     private final Handler mActiveModeUpdateHandler = new ActiveModeUpdateHandler(this);
139 
140     @Override
onCreate(Bundle savedInstanceState)141     public void onCreate(Bundle savedInstanceState) {
142         Log.d(TAG, "onCreate()");
143         super.onCreate(savedInstanceState);
144 
145         setContentView(R.layout.activity_main);
146 
147         mAmbientController = AmbientModeSupport.attach(this);
148 
149         mAmbientUpdateAlarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
150 
151         /*
152          * Create a PendingIntent which we'll give to the AlarmManager to send ambient mode updates
153          * on an interval which we've define.
154          */
155         Intent ambientUpdateIntent = new Intent(AMBIENT_UPDATE_ACTION);
156 
157         /*
158          * Retrieves a PendingIntent that will perform a broadcast. You could also use getActivity()
159          * to retrieve a PendingIntent that will start a new activity, but be aware that actually
160          * triggers onNewIntent() which causes lifecycle changes (onPause() and onResume()) which
161          * might trigger code to be re-executed more often than you want.
162          *
163          * If you do end up using getActivity(), also make sure you have set activity launchMode to
164          * singleInstance in the manifest.
165          *
166          * Otherwise, it is easy for the AlarmManager launch Intent to open a new activity
167          * every time the Alarm is triggered rather than reusing this Activity.
168          */
169         mAmbientUpdatePendingIntent =
170                 PendingIntent.getBroadcast(
171                         this, 0, ambientUpdateIntent, PendingIntent.FLAG_UPDATE_CURRENT);
172 
173         /*
174          * An anonymous broadcast receiver which will receive ambient update requests and trigger
175          * display refresh.
176          */
177         mAmbientUpdateBroadcastReceiver =
178                 new BroadcastReceiver() {
179                     @Override
180                     public void onReceive(Context context, Intent intent) {
181                         refreshDisplayAndSetNextUpdate();
182                     }
183                 };
184 
185         mContentView = findViewById(R.id.content_view);
186         mTimeTextView = findViewById(R.id.time);
187         mTimeStampTextView = findViewById(R.id.time_stamp);
188         mStateTextView = findViewById(R.id.state);
189         mUpdateRateTextView = findViewById(R.id.update_rate);
190         mDrawCountTextView = findViewById(R.id.draw_count);
191     }
192 
193     @Override
onResume()194     public void onResume() {
195         Log.d(TAG, "onResume()");
196         super.onResume();
197 
198         IntentFilter filter = new IntentFilter(AMBIENT_UPDATE_ACTION);
199         registerReceiver(mAmbientUpdateBroadcastReceiver, filter);
200 
201         refreshDisplayAndSetNextUpdate();
202     }
203 
204     @Override
onPause()205     public void onPause() {
206         Log.d(TAG, "onPause()");
207         super.onPause();
208 
209         unregisterReceiver(mAmbientUpdateBroadcastReceiver);
210 
211         mActiveModeUpdateHandler.removeMessages(MSG_UPDATE_SCREEN);
212         mAmbientUpdateAlarmManager.cancel(mAmbientUpdatePendingIntent);
213     }
214 
215     /**
216      * Loads data/updates screen (via method), but most importantly, sets up the next refresh
217      * (active mode = Handler and ambient mode = Alarm).
218      */
refreshDisplayAndSetNextUpdate()219     private void refreshDisplayAndSetNextUpdate() {
220 
221         loadDataAndUpdateScreen();
222 
223         long timeMs = System.currentTimeMillis();
224 
225         if (mAmbientController.isAmbient()) {
226             /* Calculate next trigger time (based on state). */
227             long delayMs = AMBIENT_INTERVAL_MS - (timeMs % AMBIENT_INTERVAL_MS);
228             long triggerTimeMs = timeMs + delayMs;
229 
230             mAmbientUpdateAlarmManager.setExact(
231                     AlarmManager.RTC_WAKEUP, triggerTimeMs, mAmbientUpdatePendingIntent);
232         } else {
233             /* Calculate next trigger time (based on state). */
234             long delayMs = ACTIVE_INTERVAL_MS - (timeMs % ACTIVE_INTERVAL_MS);
235 
236             mActiveModeUpdateHandler.removeMessages(MSG_UPDATE_SCREEN);
237             mActiveModeUpdateHandler.sendEmptyMessageDelayed(MSG_UPDATE_SCREEN, delayMs);
238         }
239     }
240 
241     /** Updates display based on Ambient state. If you need to pull data, you should do it here. */
loadDataAndUpdateScreen()242     private void loadDataAndUpdateScreen() {
243 
244         mDrawCount += 1;
245         long currentTimeMs = System.currentTimeMillis();
246         Log.d(
247                 TAG,
248                 "loadDataAndUpdateScreen(): "
249                         + currentTimeMs
250                         + "("
251                         + mAmbientController.isAmbient()
252                         + ")");
253 
254         if (mAmbientController.isAmbient()) {
255 
256             mTimeTextView.setText(sDateFormat.format(new Date()));
257             mTimeStampTextView.setText(getString(R.string.timestamp_label, currentTimeMs));
258 
259             mStateTextView.setText(getString(R.string.mode_ambient_label));
260             mUpdateRateTextView.setText(
261                     getString(R.string.update_rate_label, (AMBIENT_INTERVAL_MS / 1000)));
262 
263             mDrawCountTextView.setText(getString(R.string.draw_count_label, mDrawCount));
264 
265         } else {
266 
267             mTimeTextView.setText(sDateFormat.format(new Date()));
268             mTimeStampTextView.setText(getString(R.string.timestamp_label, currentTimeMs));
269 
270             mStateTextView.setText(getString(R.string.mode_active_label));
271             mUpdateRateTextView.setText(
272                     getString(R.string.update_rate_label, (ACTIVE_INTERVAL_MS / 1000)));
273 
274             mDrawCountTextView.setText(getString(R.string.draw_count_label, mDrawCount));
275         }
276     }
277 
278     @Override
getAmbientCallback()279     public AmbientModeSupport.AmbientCallback getAmbientCallback() {
280         return new MyAmbientCallback();
281     }
282 
283     private class MyAmbientCallback extends AmbientModeSupport.AmbientCallback {
284         /** Prepares the UI for ambient mode. */
285         @Override
onEnterAmbient(Bundle ambientDetails)286         public void onEnterAmbient(Bundle ambientDetails) {
287             super.onEnterAmbient(ambientDetails);
288 
289             mIsLowBitAmbient =
290                     ambientDetails.getBoolean(AmbientModeSupport.EXTRA_LOWBIT_AMBIENT, false);
291             mDoBurnInProtection =
292                     ambientDetails.getBoolean(AmbientModeSupport.EXTRA_BURN_IN_PROTECTION, false);
293 
294             /* Clears Handler queue (only needed for updates in active mode). */
295             mActiveModeUpdateHandler.removeMessages(MSG_UPDATE_SCREEN);
296 
297             /*
298              * Following best practices outlined in WatchFaces API (keeping most pixels black,
299              * avoiding large blocks of white pixels, using only black and white, and disabling
300              * anti-aliasing, etc.)
301              */
302             mStateTextView.setTextColor(Color.WHITE);
303             mUpdateRateTextView.setTextColor(Color.WHITE);
304             mDrawCountTextView.setTextColor(Color.WHITE);
305 
306             if (mIsLowBitAmbient) {
307                 mTimeTextView.getPaint().setAntiAlias(false);
308                 mTimeStampTextView.getPaint().setAntiAlias(false);
309                 mStateTextView.getPaint().setAntiAlias(false);
310                 mUpdateRateTextView.getPaint().setAntiAlias(false);
311                 mDrawCountTextView.getPaint().setAntiAlias(false);
312             }
313 
314             refreshDisplayAndSetNextUpdate();
315         }
316 
317         /**
318          * Updates the display in ambient mode on the standard interval. Since we're using a custom
319          * refresh cycle, this method does NOT update the data in the display. Rather, this method
320          * simply updates the positioning of the data in the screen to avoid burn-in, if the display
321          * requires it.
322          */
323         @Override
onUpdateAmbient()324         public void onUpdateAmbient() {
325             super.onUpdateAmbient();
326 
327             /*
328              * If the screen requires burn-in protection, views must be shifted around periodically
329              * in ambient mode. To ensure that content isn't shifted off the screen, avoid placing
330              * content within 10 pixels of the edge of the screen.
331              *
332              * Since we're potentially applying negative padding, we have ensured
333              * that the containing view is sufficiently padded (see res/layout/activity_main.xml).
334              *
335              * Activities should also avoid solid white areas to prevent pixel burn-in. Both of
336              * these requirements only apply in ambient mode, and only when this property is set
337              * to true.
338              */
339             if (mDoBurnInProtection) {
340                 int x = (int) (Math.random() * 2 * BURN_IN_OFFSET_PX - BURN_IN_OFFSET_PX);
341                 int y = (int) (Math.random() * 2 * BURN_IN_OFFSET_PX - BURN_IN_OFFSET_PX);
342                 mContentView.setPadding(x, y, 0, 0);
343             }
344         }
345 
346         /** Restores the UI to active (non-ambient) mode. */
347         @Override
onExitAmbient()348         public void onExitAmbient() {
349             super.onExitAmbient();
350 
351             /* Clears out Alarms since they are only used in ambient mode. */
352             mAmbientUpdateAlarmManager.cancel(mAmbientUpdatePendingIntent);
353 
354             mStateTextView.setTextColor(Color.GREEN);
355             mUpdateRateTextView.setTextColor(Color.GREEN);
356             mDrawCountTextView.setTextColor(Color.GREEN);
357 
358             if (mIsLowBitAmbient) {
359                 mTimeTextView.getPaint().setAntiAlias(true);
360                 mTimeStampTextView.getPaint().setAntiAlias(true);
361                 mStateTextView.getPaint().setAntiAlias(true);
362                 mUpdateRateTextView.getPaint().setAntiAlias(true);
363                 mDrawCountTextView.getPaint().setAntiAlias(true);
364             }
365 
366             /* Reset any random offset applied for burn-in protection. */
367             if (mDoBurnInProtection) {
368                 mContentView.setPadding(0, 0, 0, 0);
369             }
370 
371             refreshDisplayAndSetNextUpdate();
372         }
373     }
374 
375     /** Handler separated into static class to avoid memory leaks. */
376     private static class ActiveModeUpdateHandler extends Handler {
377         private final WeakReference<MainActivity> mMainActivityWeakReference;
378 
ActiveModeUpdateHandler(MainActivity reference)379         ActiveModeUpdateHandler(MainActivity reference) {
380             mMainActivityWeakReference = new WeakReference<>(reference);
381         }
382 
383         @Override
handleMessage(Message message)384         public void handleMessage(Message message) {
385             MainActivity mainActivity = mMainActivityWeakReference.get();
386 
387             if (mainActivity != null) {
388                 switch (message.what) {
389                     case MSG_UPDATE_SCREEN:
390                         mainActivity.refreshDisplayAndSetNextUpdate();
391                         break;
392                 }
393             }
394         }
395     }
396 }
397