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 }