1 /* 2 * Copyright (C) 2014 The Android Open Source Project 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 17 package com.android.tv.settings.system; 18 19 import com.android.tv.settings.ActionBehavior; 20 import com.android.tv.settings.ActionKey; 21 import com.android.tv.settings.BaseSettingsActivity; 22 import com.android.tv.settings.R; 23 import com.android.tv.settings.util.SettingsHelper; 24 import com.android.tv.settings.dialog.old.Action; 25 import com.android.tv.settings.dialog.old.ActionAdapter; 26 import com.android.tv.settings.dialog.old.ActionFragment; 27 import com.android.tv.settings.dialog.old.ContentFragment; 28 import com.android.tv.settings.widget.picker.DatePicker; 29 import com.android.tv.settings.widget.picker.TimePicker; 30 import com.android.tv.settings.widget.picker.Picker; 31 import com.android.tv.settings.widget.picker.PickerConstant; 32 33 import org.xmlpull.v1.XmlPullParserException; 34 35 import android.app.AlarmManager; 36 import android.content.BroadcastReceiver; 37 import android.content.Context; 38 import android.content.Intent; 39 import android.content.IntentFilter; 40 import android.content.res.XmlResourceParser; 41 import android.os.Bundle; 42 import android.provider.Settings; 43 import android.provider.Settings.SettingNotFoundException; 44 import android.text.format.DateFormat; 45 import android.util.Log; 46 47 import java.util.ArrayList; 48 import java.util.Calendar; 49 import java.util.Collections; 50 import java.util.Date; 51 import java.util.List; 52 import java.util.TimeZone; 53 54 public class DateTimeActivity extends BaseSettingsActivity implements ActionAdapter.Listener { 55 56 private static final String TAG = "DateTimeActivity"; 57 private static final boolean DEBUG = false; 58 59 private static final String HOURS_12 = "12"; 60 private static final String HOURS_24 = "24"; 61 62 private static final int HOURS_IN_HALF_DAY = 12; 63 64 private static final String XMLTAG_TIMEZONE = "timezone"; 65 66 private Calendar mDummyDate; 67 private boolean mIsResumed; 68 69 private IntentFilter mIntentFilter; 70 71 private String mNowDate; 72 private String mNowTime; 73 74 private ArrayList<Action> mTimeZoneActions; 75 76 private SettingsHelper mHelper; 77 78 /** 79 * Flag indicating whether this UpdateView call is from onCreate. 80 */ 81 private boolean mOnCreateUpdateView = false; 82 83 /** 84 * Flag indicating whether this UpdateView call is from onResume. 85 */ 86 private boolean mOnResumeUpdateView = false; 87 88 private BroadcastReceiver mIntentReceiver = new BroadcastReceiver() { 89 @Override 90 public void onReceive(Context context, Intent intent) { 91 if (mIsResumed) { 92 switch ((ActionType) mState) { 93 case DATE_TIME_OVERVIEW: 94 case DATE: 95 case TIME: 96 updateTimeAndDateDisplay(); 97 } 98 } 99 } 100 }; 101 102 @Override onCreate(Bundle savedInstanceState)103 public void onCreate(Bundle savedInstanceState) { 104 mDummyDate = Calendar.getInstance(); 105 mIntentFilter = new IntentFilter(); 106 mIntentFilter.addAction(Intent.ACTION_TIME_TICK); 107 mIntentFilter.addAction(Intent.ACTION_TIME_CHANGED); 108 mIntentFilter.addAction(Intent.ACTION_TIMEZONE_CHANGED); 109 mIntentFilter.addAction(Intent.ACTION_DATE_CHANGED); 110 registerReceiver(mIntentReceiver, mIntentFilter); 111 112 mHelper = new SettingsHelper(getApplicationContext()); 113 114 setSampleDate(); 115 116 updateTimeAndDateStrings(); 117 mOnCreateUpdateView = true; 118 119 super.onCreate(savedInstanceState); 120 } 121 setSampleDate()122 private void setSampleDate() { 123 Calendar now = Calendar.getInstance(); 124 mDummyDate.setTimeZone(now.getTimeZone()); 125 // We use December 31st because it's unambiguous when demonstrating the date format. 126 // We use 15:14 so we can demonstrate the 12/24 hour options. 127 mDummyDate.set(now.get(Calendar.YEAR), 11, 31, 15, 14, 0); 128 } 129 getAutoState(String name)130 private boolean getAutoState(String name) { 131 try { 132 return Settings.Global.getInt(getContentResolver(), name) > 0; 133 } catch (SettingNotFoundException snfe) { 134 return false; 135 } 136 } 137 setDate(Context context, int year, int month, int day)138 static void setDate(Context context, int year, int month, int day) { 139 Calendar c = Calendar.getInstance(); 140 141 c.set(Calendar.YEAR, year); 142 c.set(Calendar.MONTH, month); 143 c.set(Calendar.DAY_OF_MONTH, day); 144 long when = c.getTimeInMillis(); 145 146 if (when / 1000 < Integer.MAX_VALUE) { 147 ((AlarmManager) context.getSystemService(Context.ALARM_SERVICE)).setTime(when); 148 } 149 } 150 setTime(Context context, int hourOfDay, int minute)151 static void setTime(Context context, int hourOfDay, int minute) { 152 Calendar c = Calendar.getInstance(); 153 154 c.set(Calendar.HOUR_OF_DAY, hourOfDay); 155 c.set(Calendar.MINUTE, minute); 156 c.set(Calendar.SECOND, 0); 157 c.set(Calendar.MILLISECOND, 0); 158 long when = c.getTimeInMillis(); 159 160 if (when / 1000 < Integer.MAX_VALUE) { 161 ((AlarmManager) context.getSystemService(Context.ALARM_SERVICE)).setTime(when); 162 } 163 } 164 isTimeFormat24h()165 private boolean isTimeFormat24h() { 166 return DateFormat.is24HourFormat(this); 167 } 168 setTime24Hour(boolean is24Hour)169 private void setTime24Hour(boolean is24Hour) { 170 Settings.System.putString(getContentResolver(), 171 Settings.System.TIME_12_24, 172 is24Hour ? HOURS_24 : HOURS_12); 173 } 174 setAutoDateTime(boolean on)175 private void setAutoDateTime(boolean on) { 176 Settings.Global.putInt(getContentResolver(), Settings.Global.AUTO_TIME, on ? 1 : 0); 177 } 178 179 @Override onResume()180 protected void onResume() { 181 super.onResume(); 182 mIsResumed = true; 183 registerReceiver(mIntentReceiver, mIntentFilter); 184 185 mOnResumeUpdateView = true; 186 187 updateTimeAndDateDisplay(); 188 } 189 190 @Override onPause()191 protected void onPause() { 192 super.onPause(); 193 mIsResumed = false; 194 unregisterReceiver(mIntentReceiver); 195 } 196 197 // Updates the member strings to reflect the current date and time. updateTimeAndDateStrings()198 private void updateTimeAndDateStrings() { 199 final Calendar now = Calendar.getInstance(); 200 java.text.DateFormat dateFormat = DateFormat.getDateFormat(this); 201 mNowDate = dateFormat.format(now.getTime()); 202 java.text.DateFormat timeFormat = DateFormat.getTimeFormat(this); 203 mNowTime = timeFormat.format(now.getTime()); 204 } 205 206 @Override onActionClicked(Action action)207 public void onActionClicked(Action action) { 208 /* 209 * For list preferences 210 */ 211 final String key = action.getKey(); 212 switch ((ActionType) mState) { 213 case TIME_SET_TIME_ZONE: 214 setTimeZone(key); 215 updateTimeAndDateStrings(); 216 goBack(); 217 return; 218 } 219 /* 220 * For other preferences 221 */ 222 ActionKey<ActionType, ActionBehavior> actionKey = new ActionKey<ActionType, ActionBehavior>( 223 ActionType.class, ActionBehavior.class, key); 224 final ActionType type = actionKey.getType(); 225 final ActionBehavior behavior = actionKey.getBehavior(); 226 if (type == null || behavior == null) { 227 // Possible race condition manifested by monkey test. 228 Log.e(TAG, "type or behavior is null - exiting b/17404946"); 229 return; 230 } 231 switch (type) { 232 case DATE: 233 case TIME: 234 case TIME_CHOOSE_FORMAT: 235 case DATE_SET_DATE: 236 case TIME_SET_TIME: 237 case TIME_SET_TIME_ZONE: 238 case AUTO_DATE_TIME: 239 if (behavior == ActionBehavior.INIT) { 240 setState(type, true); 241 } 242 break; 243 } 244 switch (behavior) { 245 case ON: 246 if (mState == ActionType.TIME_CHOOSE_FORMAT) { 247 setTime24Hour(true); 248 updateTimeAndDateStrings(); 249 } else if (mState == ActionType.AUTO_DATE_TIME) { 250 setAutoDateTime(true); 251 } 252 goBack(); 253 break; 254 case OFF: 255 if (mState == ActionType.TIME_CHOOSE_FORMAT) { 256 setTime24Hour(false); 257 updateTimeAndDateStrings(); 258 } else if (mState == ActionType.AUTO_DATE_TIME) { 259 setAutoDateTime(false); 260 } 261 goBack(); 262 break; 263 } 264 } 265 266 @Override getInitialState()267 protected Object getInitialState() { 268 return ActionType.DATE_TIME_OVERVIEW; 269 } 270 271 // Updates the Date and Time entries in the current view, without resetting the 272 // Action fragment, so we don't trigger an animation. updateTimeAndDateDisplay()273 protected void updateTimeAndDateDisplay() { 274 updateTimeAndDateStrings(); 275 276 if (mActionFragment instanceof ActionFragment) { 277 ActionAdapter adapter = (ActionAdapter) ((ActionFragment)mActionFragment).getAdapter(); 278 279 if (adapter != null) { 280 mActions.clear(); 281 282 switch ((ActionType) mState) { 283 case DATE_TIME_OVERVIEW: 284 mActions.add(ActionType.AUTO_DATE_TIME.toAction(mResources, 285 mHelper.getStatusStringFromBoolean(getAutoState( 286 Settings.Global.AUTO_TIME)))); 287 mActions.add(ActionType.DATE.toAction(mResources, mNowDate)); 288 mActions.add(ActionType.TIME.toAction(mResources, mNowTime)); 289 break; 290 case DATE: 291 mActions.add(ActionType.DATE_SET_DATE.toAction(mResources, mNowDate)); 292 break; 293 case TIME: 294 mActions.add(ActionType.TIME_SET_TIME.toAction(mResources, mNowTime)); 295 mActions.add(ActionType.TIME_SET_TIME_ZONE.toAction(mResources, 296 getCurrentTimeZoneName())); 297 mActions.add(ActionType.TIME_CHOOSE_FORMAT.toAction( 298 mResources, getTimeFormatDescription())); 299 break; 300 } 301 adapter.setActions(mActions); 302 return; 303 } 304 } 305 306 // If we don't have an ActionFragment or adapter, fall back to the regular updateView 307 updateView(); 308 } 309 310 @Override refreshActionList()311 protected void refreshActionList() { 312 mActions.clear(); 313 boolean autoTime = getAutoState(Settings.Global.AUTO_TIME); 314 switch ((ActionType)mState) { 315 case DATE_TIME_OVERVIEW: 316 mActions.add(ActionType.AUTO_DATE_TIME.toAction(mResources, 317 mHelper.getStatusStringFromBoolean(autoTime))); 318 mActions.add(ActionType.DATE.toAction(mResources, mNowDate)); 319 mActions.add(ActionType.TIME.toAction(mResources, mNowTime)); 320 break; 321 case DATE: 322 mActions.add(ActionType.DATE_SET_DATE.toAction(mResources, mNowDate, !autoTime)); 323 break; 324 case TIME: 325 mActions.add(ActionType.TIME_SET_TIME.toAction(mResources, mNowTime, !autoTime)); 326 mActions.add(ActionType.TIME_SET_TIME_ZONE.toAction( 327 mResources, getCurrentTimeZoneName())); 328 mActions.add(ActionType.TIME_CHOOSE_FORMAT.toAction( 329 mResources, getTimeFormatDescription())); 330 break; 331 case TIME_CHOOSE_FORMAT: 332 mActions.add(ActionBehavior.ON.toAction(ActionBehavior.getOnKey( 333 ActionType.TIME_CHOOSE_FORMAT.name()), mResources, isTimeFormat24h())); 334 mActions.add(ActionBehavior.OFF.toAction(ActionBehavior.getOffKey( 335 ActionType.TIME_CHOOSE_FORMAT.name()), mResources, !isTimeFormat24h())); 336 break; 337 case TIME_SET_TIME_ZONE: 338 mActions = getZoneActions(this); 339 break; 340 case AUTO_DATE_TIME: 341 mActions.add(ActionBehavior.ON.toAction(ActionBehavior.getOnKey( 342 ActionType.AUTO_DATE_TIME.name()), mResources, autoTime)); 343 mActions.add(ActionBehavior.OFF.toAction(ActionBehavior.getOffKey( 344 ActionType.AUTO_DATE_TIME.name()), mResources, !autoTime)); 345 break; 346 default: 347 break; 348 } 349 } 350 getTimeFormatDescription()351 private String getTimeFormatDescription() { 352 String status = mHelper.getStatusStringFromBoolean(isTimeFormat24h()); 353 String desc = String.format("%s (%s)", status, 354 DateFormat.getTimeFormat(this).format(mDummyDate.getTime())); 355 return desc; 356 } 357 358 @Override updateView()359 protected void updateView() { 360 refreshActionList(); 361 362 switch ((ActionType) mState) { 363 case DATE_TIME_OVERVIEW: 364 if (mOnCreateUpdateView && mOnResumeUpdateView) { 365 // If current updateView call is due to onResume following onCreate, 366 // avoid duplicate setView, which will lead to broken animation. 367 mOnCreateUpdateView = false; 368 mOnResumeUpdateView = false; 369 370 return; 371 } else { 372 mOnResumeUpdateView = false; 373 } 374 mActionFragment = ActionFragment.newInstance(mActions); 375 break; 376 case DATE_SET_DATE: 377 DatePicker datePicker = 378 DatePicker.newInstance(new String(DateFormat.getDateFormatOrder(this))); 379 datePicker.setResultListener(new Picker.ResultListener() { 380 381 @Override 382 public void onCommitResult(List<String> result) { 383 String formatOrder = new String( 384 DateFormat.getDateFormatOrder(DateTimeActivity.this)).toUpperCase(); 385 int yIndex = formatOrder.indexOf('Y'); 386 int mIndex = formatOrder.indexOf('M'); 387 int dIndex = formatOrder.indexOf('D'); 388 if (yIndex < 0 || mIndex < 0 || dIndex < 0 || 389 yIndex > 2 || mIndex > 2 || dIndex > 2) { 390 // Badly formatted input. Use default order. 391 mIndex = 0; 392 dIndex = 1; 393 yIndex = 2; 394 } 395 String month = result.get(mIndex); 396 int day = Integer.parseInt(result.get(dIndex)); 397 int year = Integer.parseInt(result.get(yIndex)); 398 int monthInt = 0; 399 String[] months = PickerConstant.getInstance(mResources).months; 400 int totalMonths = months.length; 401 for (int i = 0; i < totalMonths; i++) { 402 if (months[i].equals(month)) { 403 monthInt = i; 404 } 405 } 406 407 // turn off Auto date/time 408 setAutoDateTime(false); 409 410 setDate(DateTimeActivity.this, year, monthInt, day); 411 goBack(); 412 } 413 }); 414 mActionFragment = datePicker; 415 break; 416 case TIME_SET_TIME: 417 Picker timePicker = TimePicker.newInstance(isTimeFormat24h(), true); 418 timePicker.setResultListener(new Picker.ResultListener() { 419 420 @Override 421 public void onCommitResult(List<String> result) { 422 boolean is24hFormat = isTimeFormat24h(); 423 int hour = Integer.parseInt(result.get(0)); 424 int minute = Integer.parseInt(result.get(1)); 425 if (!is24hFormat) { 426 String ampm = result.get(2); 427 if (ampm.equals(getResources().getStringArray(R.array.ampm)[1])) { 428 // PM case, valid hours: 12-23 429 hour = (hour % HOURS_IN_HALF_DAY) + HOURS_IN_HALF_DAY; 430 } else { 431 // AM case, valid hours: 0-11 432 hour = hour % HOURS_IN_HALF_DAY; 433 } 434 } 435 436 // turn off Auto date/time 437 setAutoDateTime(false); 438 439 setTime(DateTimeActivity.this, hour, minute); 440 goBack(); 441 } 442 }); 443 mActionFragment = timePicker; 444 break; 445 default: 446 mActionFragment = ActionFragment.newInstance(mActions); 447 break; 448 } 449 450 setViewWithActionFragment( 451 ((ActionType) mState).getTitle(mResources), getPrevState() != null ? 452 ((ActionType) getPrevState()).getTitle(mResources) 453 : getString(R.string.settings_app_name), 454 ((ActionType) mState).getDescription(mResources), R.drawable.ic_settings_datetime); 455 } 456 setViewWithActionFragment(String title, String breadcrumb, String description, int iconResId)457 protected void setViewWithActionFragment(String title, String breadcrumb, String description, 458 int iconResId) { 459 mContentFragment = ContentFragment.newInstance(title, breadcrumb, description, iconResId, 460 getResources().getColor(R.color.icon_background)); 461 setContentAndActionFragments(mContentFragment, mActionFragment); 462 } 463 464 @Override setProperty(boolean enable)465 protected void setProperty(boolean enable) { 466 } 467 468 /** 469 * Returns a string representing the current time zone set in the system. 470 */ getCurrentTimeZoneName()471 private String getCurrentTimeZoneName() { 472 final Calendar now = Calendar.getInstance(); 473 TimeZone tz = now.getTimeZone(); 474 475 Date date = new Date(); 476 return formatOffset(new StringBuilder(), tz.getOffset(date.getTime())). 477 append(", "). 478 append(tz.getDisplayName(tz.inDaylightTime(date), TimeZone.LONG)).toString(); 479 } 480 481 /** 482 * Formats the provided timezone offset into a string of the form GMT+XX:XX 483 */ formatOffset(StringBuilder sb, long offset)484 private static StringBuilder formatOffset(StringBuilder sb, long offset) { 485 long off = offset / 1000 / 60; 486 487 sb.append("GMT"); 488 if (off < 0) { 489 sb.append('-'); 490 off = -off; 491 } else { 492 sb.append('+'); 493 } 494 495 int hours = (int) (off / 60); 496 int minutes = (int) (off % 60); 497 498 sb.append((char) ('0' + hours / 10)); 499 sb.append((char) ('0' + hours % 10)); 500 501 sb.append(':'); 502 503 sb.append((char) ('0' + minutes / 10)); 504 sb.append((char) ('0' + minutes % 10)); 505 506 return sb; 507 } 508 509 /** 510 * Helper class to hold the time zone data parsed from the Time Zones XML 511 * file. 512 */ 513 private class TimeZoneInfo implements Comparable<TimeZoneInfo> { 514 public String tzId; 515 public String tzName; 516 public long tzOffset; 517 TimeZoneInfo(String id, String name, long offset)518 public TimeZoneInfo(String id, String name, long offset) { 519 tzId = id; 520 tzName = name; 521 tzOffset = offset; 522 } 523 524 @Override compareTo(TimeZoneInfo another)525 public int compareTo(TimeZoneInfo another) { 526 return (int) (tzOffset - another.tzOffset); 527 } 528 } 529 530 /** 531 * Parse the Time Zones information from the XML file and creates Action 532 * objects for each time zone. 533 */ getZoneActions(Context context)534 private ArrayList<Action> getZoneActions(Context context) { 535 if (mTimeZoneActions != null && mTimeZoneActions.size() != 0) { 536 return mTimeZoneActions; 537 } 538 539 ArrayList<TimeZoneInfo> timeZones = getTimeZones(context); 540 541 mTimeZoneActions = new ArrayList<Action>(); 542 543 // Sort the Time Zones list in ascending offset order 544 Collections.sort(timeZones); 545 546 TimeZone currentTz = TimeZone.getDefault(); 547 548 for (TimeZoneInfo tz : timeZones) { 549 StringBuilder name = new StringBuilder(); 550 boolean checked = currentTz.getID().equals(tz.tzId); 551 mTimeZoneActions.add(getTimeZoneAction(tz.tzId, tz.tzName, 552 formatOffset(name, tz.tzOffset).toString(), checked)); 553 } 554 555 return mTimeZoneActions; 556 } 557 558 /** 559 * Parses the XML time zone information into an array of TimeZoneInfo 560 * objects. 561 */ getTimeZones(Context context)562 private ArrayList<TimeZoneInfo> getTimeZones(Context context) { 563 ArrayList<TimeZoneInfo> timeZones = new ArrayList<TimeZoneInfo>(); 564 final long date = Calendar.getInstance().getTimeInMillis(); 565 try { 566 XmlResourceParser xrp = context.getResources().getXml(R.xml.timezones); 567 while (xrp.next() != XmlResourceParser.START_TAG) 568 continue; 569 xrp.next(); 570 while (xrp.getEventType() != XmlResourceParser.END_TAG) { 571 while (xrp.getEventType() != XmlResourceParser.START_TAG && 572 xrp.getEventType() != XmlResourceParser.END_DOCUMENT) { 573 xrp.next(); 574 } 575 576 if (xrp.getEventType() == XmlResourceParser.END_DOCUMENT) { 577 break; 578 } 579 580 if (xrp.getName().equals(XMLTAG_TIMEZONE)) { 581 String id = xrp.getAttributeValue(0); 582 String displayName = xrp.nextText(); 583 TimeZone tz = TimeZone.getTimeZone(id); 584 long offset; 585 if (tz != null) { 586 offset = tz.getOffset(date); 587 timeZones.add(new TimeZoneInfo(id, displayName, offset)); 588 } else { 589 continue; 590 } 591 } 592 while (xrp.getEventType() != XmlResourceParser.END_TAG) { 593 xrp.next(); 594 } 595 xrp.next(); 596 } 597 xrp.close(); 598 } catch (XmlPullParserException xppe) { 599 Log.e(TAG, "Ill-formatted timezones.xml file"); 600 } catch (java.io.IOException ioe) { 601 Log.e(TAG, "Unable to read timezones.xml file"); 602 } 603 return timeZones; 604 } 605 getTimeZoneAction(String tzId, String displayName, String gmt, boolean setChecked)606 private static Action getTimeZoneAction(String tzId, String displayName, String gmt, 607 boolean setChecked) { 608 return new Action.Builder().key(tzId).title(displayName).description(gmt). 609 checked(setChecked).build(); 610 } 611 setTimeZone(String tzId)612 private void setTimeZone(String tzId) { 613 // Update the system timezone value 614 final AlarmManager alarm = (AlarmManager) getSystemService(Context.ALARM_SERVICE); 615 alarm.setTimeZone(tzId); 616 617 setSampleDate(); 618 } 619 } 620