1 /* 2 * Copyright (C) 2012 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.deskclock; 18 19 import android.app.Activity; 20 import android.app.AlarmManager; 21 import android.content.BroadcastReceiver; 22 import android.content.Context; 23 import android.content.Intent; 24 import android.content.IntentFilter; 25 import android.content.res.Resources; 26 import android.database.ContentObserver; 27 import android.net.Uri; 28 import android.os.Bundle; 29 import android.os.Handler; 30 import android.provider.Settings; 31 import android.support.annotation.NonNull; 32 import android.support.v7.widget.LinearLayoutManager; 33 import android.support.v7.widget.RecyclerView; 34 import android.text.format.DateUtils; 35 import android.view.GestureDetector; 36 import android.view.LayoutInflater; 37 import android.view.MotionEvent; 38 import android.view.View; 39 import android.view.ViewGroup; 40 import android.widget.Button; 41 import android.widget.ImageView; 42 import android.widget.TextClock; 43 import android.widget.TextView; 44 45 import com.android.deskclock.data.City; 46 import com.android.deskclock.data.CityListener; 47 import com.android.deskclock.data.DataModel; 48 import com.android.deskclock.events.Events; 49 import com.android.deskclock.uidata.UiDataModel; 50 import com.android.deskclock.worldclock.CitySelectionActivity; 51 52 import java.util.Calendar; 53 import java.util.List; 54 import java.util.TimeZone; 55 56 import static android.app.AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED; 57 import static android.view.View.GONE; 58 import static android.view.View.INVISIBLE; 59 import static android.view.View.VISIBLE; 60 import static com.android.deskclock.uidata.UiDataModel.Tab.CLOCKS; 61 import static java.util.Calendar.DAY_OF_WEEK; 62 63 /** 64 * Fragment that shows the clock (analog or digital), the next alarm info and the world clock. 65 */ 66 public final class ClockFragment extends DeskClockFragment { 67 68 // Updates dates in the UI on every quarter-hour. 69 private final Runnable mQuarterHourUpdater = new QuarterHourRunnable(); 70 71 // Updates the UI in response to changes to the scheduled alarm. 72 private BroadcastReceiver mAlarmChangeReceiver; 73 74 // Detects changes to the next scheduled alarm pre-L. 75 private ContentObserver mAlarmObserver; 76 77 private TextClock mDigitalClock; 78 private AnalogClock mAnalogClock; 79 private View mClockFrame; 80 private SelectedCitiesAdapter mCityAdapter; 81 private RecyclerView mCityList; 82 private String mDateFormat; 83 private String mDateFormatForAccessibility; 84 85 /** 86 * The public no-arg constructor required by all fragments. 87 */ 88 public ClockFragment() { 89 super(CLOCKS); 90 } 91 92 @Override 93 public void onCreate(Bundle savedInstanceState) { 94 super.onCreate(savedInstanceState); 95 96 mAlarmObserver = Utils.isPreL() ? new AlarmObserverPreL() : null; 97 mAlarmChangeReceiver = Utils.isLOrLater() ? new AlarmChangedBroadcastReceiver() : null; 98 } 99 100 @Override 101 public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle icicle) { 102 super.onCreateView(inflater, container, icicle); 103 104 final View fragmentView = inflater.inflate(R.layout.clock_fragment, container, false); 105 106 mDateFormat = getString(R.string.abbrev_wday_month_day_no_year); 107 mDateFormatForAccessibility = getString(R.string.full_wday_month_day_no_year); 108 109 mCityAdapter = new SelectedCitiesAdapter(getActivity(), mDateFormat, 110 mDateFormatForAccessibility); 111 112 mCityList = (RecyclerView) fragmentView.findViewById(R.id.cities); 113 mCityList.setLayoutManager(new LinearLayoutManager(getActivity())); 114 mCityList.setAdapter(mCityAdapter); 115 mCityList.setItemAnimator(null); 116 DataModel.getDataModel().addCityListener(mCityAdapter); 117 118 final ScrollPositionWatcher scrollPositionWatcher = new ScrollPositionWatcher(); 119 mCityList.addOnScrollListener(scrollPositionWatcher); 120 121 final Context context = container.getContext(); 122 mCityList.setOnTouchListener(new CityListOnLongClickListener(context)); 123 fragmentView.setOnLongClickListener(new StartScreenSaverListener()); 124 125 // On tablet landscape, the clock frame will be a distinct view. Otherwise, it'll be added 126 // on as a header to the main listview. 127 mClockFrame = fragmentView.findViewById(R.id.main_clock_left_pane); 128 if (mClockFrame != null) { 129 mDigitalClock = (TextClock) mClockFrame.findViewById(R.id.digital_clock); 130 mAnalogClock = (AnalogClock) mClockFrame.findViewById(R.id.analog_clock); 131 Utils.setClockIconTypeface(mClockFrame); 132 Utils.updateDate(mDateFormat, mDateFormatForAccessibility, mClockFrame); 133 Utils.setClockStyle(mDigitalClock, mAnalogClock); 134 Utils.setClockSecondsEnabled(mDigitalClock, mAnalogClock); 135 } 136 137 // Schedule a runnable to update the date every quarter hour. 138 UiDataModel.getUiDataModel().addQuarterHourCallback(mQuarterHourUpdater, 100); 139 140 return fragmentView; 141 } 142 143 @Override 144 public void onResume() { 145 super.onResume(); 146 147 final Activity activity = getActivity(); 148 149 mDateFormat = getString(R.string.abbrev_wday_month_day_no_year); 150 mDateFormatForAccessibility = getString(R.string.full_wday_month_day_no_year); 151 152 // Watch for system events that effect clock time or format. 153 if (mAlarmChangeReceiver != null) { 154 final IntentFilter filter = new IntentFilter(ACTION_NEXT_ALARM_CLOCK_CHANGED); 155 activity.registerReceiver(mAlarmChangeReceiver, filter); 156 } 157 158 // Resume can be invoked after changing the clock style or seconds display. 159 if (mDigitalClock != null && mAnalogClock != null) { 160 Utils.setClockStyle(mDigitalClock, mAnalogClock); 161 Utils.setClockSecondsEnabled(mDigitalClock, mAnalogClock); 162 } 163 164 final View view = getView(); 165 if (view != null && view.findViewById(R.id.main_clock_left_pane) != null) { 166 // Center the main clock frame by hiding the world clocks when none are selected. 167 mCityList.setVisibility(mCityAdapter.getItemCount() == 0 ? GONE : VISIBLE); 168 } 169 170 refreshAlarm(); 171 172 // Alarm observer is null on L or later. 173 if (mAlarmObserver != null) { 174 @SuppressWarnings("deprecation") 175 final Uri uri = Settings.System.getUriFor(Settings.System.NEXT_ALARM_FORMATTED); 176 activity.getContentResolver().registerContentObserver(uri, false, mAlarmObserver); 177 } 178 } 179 180 @Override 181 public void onPause() { 182 super.onPause(); 183 184 final Activity activity = getActivity(); 185 if (mAlarmChangeReceiver != null) { 186 activity.unregisterReceiver(mAlarmChangeReceiver); 187 } 188 if (mAlarmObserver != null) { 189 activity.getContentResolver().unregisterContentObserver(mAlarmObserver); 190 } 191 } 192 193 @Override 194 public void onDestroyView() { 195 super.onDestroyView(); 196 UiDataModel.getUiDataModel().removePeriodicCallback(mQuarterHourUpdater); 197 DataModel.getDataModel().removeCityListener(mCityAdapter); 198 } 199 200 @Override 201 public void onFabClick(@NonNull ImageView fab) { 202 startActivity(new Intent(getActivity(), CitySelectionActivity.class)); 203 } 204 205 @Override 206 public void onUpdateFab(@NonNull ImageView fab) { 207 fab.setVisibility(VISIBLE); 208 fab.setImageResource(R.drawable.ic_public); 209 fab.setContentDescription(fab.getResources().getString(R.string.button_cities)); 210 } 211 212 @Override 213 public void onUpdateFabButtons(@NonNull Button left, @NonNull Button right) { 214 left.setVisibility(INVISIBLE); 215 right.setVisibility(INVISIBLE); 216 } 217 218 /** 219 * Refresh the next alarm time. 220 */ 221 private void refreshAlarm() { 222 if (mClockFrame != null) { 223 Utils.refreshAlarm(getActivity(), mClockFrame); 224 } else { 225 mCityAdapter.refreshAlarm(); 226 } 227 } 228 229 /** 230 * Long pressing over the main clock starts the screen saver. 231 */ 232 private final class StartScreenSaverListener implements View.OnLongClickListener { 233 234 @Override 235 public boolean onLongClick(View view) { 236 startActivity(new Intent(getActivity(), ScreensaverActivity.class) 237 .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 238 .putExtra(Events.EXTRA_EVENT_LABEL, R.string.label_deskclock)); 239 return true; 240 } 241 } 242 243 /** 244 * Long pressing over the city list starts the screen saver. 245 */ 246 private final class CityListOnLongClickListener extends GestureDetector.SimpleOnGestureListener 247 implements View.OnTouchListener { 248 249 private final GestureDetector mGestureDetector; 250 251 private CityListOnLongClickListener(Context context) { 252 mGestureDetector = new GestureDetector(context, this); 253 } 254 255 @Override 256 public void onLongPress(MotionEvent e) { 257 final View view = getView(); 258 if (view != null) { 259 view.performLongClick(); 260 } 261 } 262 263 @Override 264 public boolean onDown(MotionEvent e) { 265 return true; 266 } 267 268 @Override 269 public boolean onTouch(View v, MotionEvent event) { 270 return mGestureDetector.onTouchEvent(event); 271 } 272 } 273 274 /** 275 * This runnable executes at every quarter-hour (e.g. 1:00, 1:15, 1:30, 1:45, etc...) and 276 * updates the dates displayed within the UI. Quarter-hour increments were chosen to accommodate 277 * the "weirdest" timezones (e.g. Nepal is UTC/GMT +05:45). 278 */ 279 private final class QuarterHourRunnable implements Runnable { 280 @Override 281 public void run() { 282 mCityAdapter.notifyDataSetChanged(); 283 } 284 } 285 286 /** 287 * Prior to L, a ContentObserver was used to monitor changes to the next scheduled alarm. 288 * In L and beyond this is accomplished via a system broadcast of 289 * {@link AlarmManager#ACTION_NEXT_ALARM_CLOCK_CHANGED}. 290 */ 291 private final class AlarmObserverPreL extends ContentObserver { 292 private AlarmObserverPreL() { 293 super(new Handler()); 294 } 295 296 @Override 297 public void onChange(boolean selfChange) { 298 refreshAlarm(); 299 } 300 } 301 302 /** 303 * Update the display of the scheduled alarm as it changes. 304 */ 305 private final class AlarmChangedBroadcastReceiver extends BroadcastReceiver { 306 @Override 307 public void onReceive(Context context, Intent intent) { 308 refreshAlarm(); 309 } 310 } 311 312 /** 313 * Updates the vertical scroll state of this tab in the {@link UiDataModel} as the user scrolls 314 * the recyclerview or when the size/position of elements within the recyclerview changes. 315 */ 316 private final class ScrollPositionWatcher extends RecyclerView.OnScrollListener 317 implements View.OnLayoutChangeListener { 318 @Override 319 public void onScrolled(RecyclerView recyclerView, int dx, int dy) { 320 setTabScrolledToTop(Utils.isScrolledToTop(mCityList)); 321 } 322 323 @Override 324 public void onLayoutChange(View v, int left, int top, int right, int bottom, 325 int oldLeft, int oldTop, int oldRight, int oldBottom) { 326 setTabScrolledToTop(Utils.isScrolledToTop(mCityList)); 327 } 328 } 329 330 /** 331 * This adapter lists all of the selected world clocks. Optionally, it also includes a clock at 332 * the top for the home timezone if "Automatic home clock" is turned on in settings and the 333 * current time at home does not match the current time in the timezone of the current location. 334 * If the phone is in portrait mode it will also include the main clock at the top. 335 */ 336 private static final class SelectedCitiesAdapter extends RecyclerView.Adapter 337 implements CityListener { 338 339 private final static int MAIN_CLOCK = R.layout.main_clock_frame; 340 private final static int WORLD_CLOCK = R.layout.world_clock_item; 341 342 private final LayoutInflater mInflater; 343 private final Context mContext; 344 private final boolean mIsPortrait; 345 private final boolean mShowHomeClock; 346 private final String mDateFormat; 347 private final String mDateFormatForAccessibility; 348 349 private SelectedCitiesAdapter(Context context, String dateFormat, 350 String dateFormatForAccessibility) { 351 mContext = context; 352 mDateFormat = dateFormat; 353 mDateFormatForAccessibility = dateFormatForAccessibility; 354 mInflater = LayoutInflater.from(context); 355 mIsPortrait = Utils.isPortrait(context); 356 mShowHomeClock = DataModel.getDataModel().getShowHomeClock(); 357 } 358 359 @Override 360 public int getItemViewType(int position) { 361 if (position == 0 && mIsPortrait) { 362 return MAIN_CLOCK; 363 } 364 return WORLD_CLOCK; 365 } 366 367 @Override 368 public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 369 final View view = mInflater.inflate(viewType, parent, false); 370 switch (viewType) { 371 case WORLD_CLOCK: 372 return new CityViewHolder(view); 373 case MAIN_CLOCK: 374 return new MainClockViewHolder(view); 375 default: 376 throw new IllegalArgumentException("View type not recognized"); 377 } 378 } 379 380 @Override 381 public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { 382 final int viewType = getItemViewType(position); 383 switch (viewType) { 384 case WORLD_CLOCK: 385 // Retrieve the city to bind. 386 final City city; 387 // If showing home clock, put it at the top 388 if (mShowHomeClock && position == (mIsPortrait ? 1 : 0)) { 389 city = getHomeCity(); 390 } else { 391 final int positionAdjuster = (mIsPortrait ? 1 : 0) 392 + (mShowHomeClock ? 1 : 0); 393 city = getCities().get(position - positionAdjuster); 394 } 395 ((CityViewHolder) holder).bind(mContext, city, position, mIsPortrait); 396 break; 397 case MAIN_CLOCK: 398 ((MainClockViewHolder) holder).bind(mContext, mDateFormat, 399 mDateFormatForAccessibility, getItemCount() > 1); 400 break; 401 default: 402 throw new IllegalArgumentException("Unexpected view type: " + viewType); 403 } 404 } 405 406 @Override 407 public int getItemCount() { 408 final int mainClockCount = mIsPortrait ? 1 : 0; 409 final int homeClockCount = mShowHomeClock ? 1 : 0; 410 final int worldClockCount = getCities().size(); 411 return mainClockCount + homeClockCount + worldClockCount; 412 } 413 414 private City getHomeCity() { 415 return DataModel.getDataModel().getHomeCity(); 416 } 417 418 private List<City> getCities() { 419 return DataModel.getDataModel().getSelectedCities(); 420 } 421 422 private void refreshAlarm() { 423 if (mIsPortrait && getItemCount() > 0) { 424 notifyItemChanged(0); 425 } 426 } 427 428 @Override 429 public void citiesChanged(List<City> oldCities, List<City> newCities) { 430 notifyDataSetChanged(); 431 } 432 433 private static final class CityViewHolder extends RecyclerView.ViewHolder { 434 435 private final TextView mName; 436 private final TextClock mDigitalClock; 437 private final AnalogClock mAnalogClock; 438 private final TextView mHoursAhead; 439 440 private CityViewHolder(View itemView) { 441 super(itemView); 442 443 mName = (TextView) itemView.findViewById(R.id.city_name); 444 mDigitalClock = (TextClock) itemView.findViewById(R.id.digital_clock); 445 mAnalogClock = (AnalogClock) itemView.findViewById(R.id.analog_clock); 446 mHoursAhead = (TextView) itemView.findViewById(R.id.hours_ahead); 447 } 448 449 private void bind(Context context, City city, int position, boolean isPortrait) { 450 final String cityTimeZoneId = city.getTimeZone().getID(); 451 452 // Configure the digital clock or analog clock depending on the user preference. 453 if (DataModel.getDataModel().getClockStyle() == DataModel.ClockStyle.ANALOG) { 454 mDigitalClock.setVisibility(GONE); 455 mAnalogClock.setVisibility(VISIBLE); 456 mAnalogClock.setTimeZone(cityTimeZoneId); 457 mAnalogClock.enableSeconds(false); 458 } else { 459 mAnalogClock.setVisibility(GONE); 460 mDigitalClock.setVisibility(VISIBLE); 461 mDigitalClock.setTimeZone(cityTimeZoneId); 462 mDigitalClock.setFormat12Hour(Utils.get12ModeFormat(0.3f /* amPmRatio */, 463 false)); 464 mDigitalClock.setFormat24Hour(Utils.get24ModeFormat(false)); 465 } 466 467 // Supply top and bottom padding dynamically. 468 final Resources res = context.getResources(); 469 final int padding = res.getDimensionPixelSize(R.dimen.medium_space_top); 470 final int top = position == 0 && !isPortrait ? 0 : padding; 471 final int left = itemView.getPaddingLeft(); 472 final int right = itemView.getPaddingRight(); 473 final int bottom = itemView.getPaddingBottom(); 474 itemView.setPadding(left, top, right, bottom); 475 476 // Bind the city name. 477 mName.setText(city.getName()); 478 479 // Compute if the city week day matches the weekday of the current timezone. 480 final Calendar localCal = Calendar.getInstance(TimeZone.getDefault()); 481 final Calendar cityCal = Calendar.getInstance(city.getTimeZone()); 482 final boolean displayDayOfWeek = 483 localCal.get(DAY_OF_WEEK) != cityCal.get(DAY_OF_WEEK); 484 485 // Compare offset from UTC time on today's date (daylight savings time, etc.) 486 final TimeZone currentTimeZone = TimeZone.getDefault(); 487 final TimeZone cityTimeZone = TimeZone.getTimeZone(cityTimeZoneId); 488 final long currentTimeMillis = System.currentTimeMillis(); 489 final long currentUtcOffset = currentTimeZone.getOffset(currentTimeMillis); 490 final long cityUtcOffset = cityTimeZone.getOffset(currentTimeMillis); 491 final long offsetDelta = cityUtcOffset - currentUtcOffset; 492 493 final int hoursDifferent = (int) (offsetDelta / DateUtils.HOUR_IN_MILLIS); 494 final int minutesDifferent = (int) (offsetDelta / DateUtils.MINUTE_IN_MILLIS) % 60; 495 final boolean displayMinutes = offsetDelta % DateUtils.HOUR_IN_MILLIS != 0; 496 final boolean isAhead = hoursDifferent > 0 || (hoursDifferent == 0 497 && minutesDifferent > 0); 498 if (!Utils.isLandscape(context)) { 499 // Bind the number of hours ahead or behind, or hide if the time is the same. 500 final boolean displayDifference = hoursDifferent != 0 || displayMinutes; 501 mHoursAhead.setVisibility(displayDifference ? VISIBLE : GONE); 502 final String timeString = Utils.createHoursDifferentString( 503 context, displayMinutes, isAhead, hoursDifferent, minutesDifferent); 504 mHoursAhead.setText(displayDayOfWeek ? 505 (context.getString(isAhead ? R.string.world_hours_tomorrow 506 : R.string.world_hours_yesterday, timeString)) 507 : timeString); 508 } else { 509 // Only tomorrow/yesterday should be shown in landscape view. 510 mHoursAhead.setVisibility(displayDayOfWeek ? View.VISIBLE : View.GONE); 511 if (displayDayOfWeek) { 512 mHoursAhead.setText(context.getString(isAhead ? R.string.world_tomorrow 513 : R.string.world_yesterday)); 514 } 515 516 } 517 } 518 } 519 520 private static final class MainClockViewHolder extends RecyclerView.ViewHolder { 521 522 private final View mHairline; 523 private final TextClock mDigitalClock; 524 private final AnalogClock mAnalogClock; 525 526 private MainClockViewHolder(View itemView) { 527 super(itemView); 528 529 mHairline = itemView.findViewById(R.id.hairline); 530 mDigitalClock = (TextClock) itemView.findViewById(R.id.digital_clock); 531 mAnalogClock = (AnalogClock) itemView.findViewById(R.id.analog_clock); 532 Utils.setClockIconTypeface(itemView); 533 } 534 535 private void bind(Context context, String dateFormat, 536 String dateFormatForAccessibility, boolean showHairline) { 537 Utils.refreshAlarm(context, itemView); 538 539 Utils.updateDate(dateFormat, dateFormatForAccessibility, itemView); 540 Utils.setClockStyle(mDigitalClock, mAnalogClock); 541 mHairline.setVisibility(showHairline ? VISIBLE : GONE); 542 543 Utils.setClockSecondsEnabled(mDigitalClock, mAnalogClock); 544 } 545 } 546 } 547 } 548