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