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.database.ContentObserver; 26 import android.net.Uri; 27 import android.os.Bundle; 28 import android.os.Handler; 29 import android.provider.Settings; 30 import android.view.LayoutInflater; 31 import android.view.MotionEvent; 32 import android.view.View; 33 import android.view.View.OnTouchListener; 34 import android.view.ViewConfiguration; 35 import android.view.ViewGroup; 36 import android.widget.BaseAdapter; 37 import android.widget.ListView; 38 import android.widget.TextClock; 39 import android.widget.TextView; 40 41 import com.android.deskclock.data.City; 42 import com.android.deskclock.data.DataModel; 43 import com.android.deskclock.worldclock.CitySelectionActivity; 44 45 import java.util.Calendar; 46 import java.util.List; 47 import java.util.Locale; 48 import java.util.TimeZone; 49 50 import static android.view.View.GONE; 51 import static android.view.View.INVISIBLE; 52 import static android.view.View.VISIBLE; 53 import static java.util.Calendar.DAY_OF_WEEK; 54 55 /** 56 * Fragment that shows the clock (analog or digital), the next alarm info and the world clock. 57 */ 58 public final class ClockFragment extends DeskClockFragment { 59 60 // Updates the UI in response to system setting changes that alter time values and time display. 61 private final BroadcastReceiver mBroadcastReceiver = new SystemBroadcastReceiver(); 62 63 // Updates dates in the UI on every quarter-hour. 64 private final Runnable mQuarterHourUpdater = new QuarterHourRunnable(); 65 66 // Detects changes to the next scheduled alarm pre-L. 67 private ContentObserver mAlarmObserver; 68 69 private Handler mHandler; 70 71 private TextClock mDigitalClock; 72 private View mAnalogClock, mClockFrame; 73 private SelectedCitiesAdapter mCityAdapter; 74 private ListView mCityList; 75 private String mDateFormat; 76 private String mDateFormatForAccessibility; 77 78 /** The public no-arg constructor required by all fragments. */ ClockFragment()79 public ClockFragment() {} 80 81 @Override onCreate(Bundle savedInstanceState)82 public void onCreate(Bundle savedInstanceState) { 83 super.onCreate(savedInstanceState); 84 85 mHandler = new Handler(); 86 mAlarmObserver = Utils.isPreL() ? new AlarmObserverPreL(mHandler) : null; 87 } 88 89 @Override onCreateView(LayoutInflater inflater, ViewGroup container, Bundle icicle)90 public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle icicle) { 91 super.onCreateView(inflater, container, icicle); 92 93 final OnTouchListener startScreenSaverListener = new StartScreenSaverListener(); 94 final View footerView = inflater.inflate(R.layout.blank_footer_view, mCityList, false); 95 final View fragmentView = inflater.inflate(R.layout.clock_fragment, container, false); 96 97 mCityAdapter = new SelectedCitiesAdapter(getActivity()); 98 99 mCityList = (ListView) fragmentView.findViewById(R.id.cities); 100 mCityList.setDivider(null); 101 mCityList.setAdapter(mCityAdapter); 102 mCityList.addFooterView(footerView, null, false); 103 mCityList.setOnTouchListener(startScreenSaverListener); 104 105 // On tablet landscape, the clock frame will be a distinct view. Otherwise, it'll be added 106 // on as a header to the main listview. 107 mClockFrame = fragmentView.findViewById(R.id.main_clock_left_pane); 108 if (mClockFrame == null) { 109 mClockFrame = inflater.inflate(R.layout.main_clock_frame, mCityList, false); 110 mCityList.addHeaderView(mClockFrame, null, false); 111 final View hairline = mClockFrame.findViewById(R.id.hairline); 112 hairline.setVisibility(mCityAdapter.getCount() == 0 ? GONE : VISIBLE); 113 } else { 114 final View hairline = mClockFrame.findViewById(R.id.hairline); 115 hairline.setVisibility(GONE); 116 // The main clock frame needs its own touch listener for night mode now. 117 fragmentView.setOnTouchListener(startScreenSaverListener); 118 } 119 120 mDigitalClock = (TextClock) mClockFrame.findViewById(R.id.digital_clock); 121 mAnalogClock = mClockFrame.findViewById(R.id.analog_clock); 122 return fragmentView; 123 } 124 125 @Override onActivityCreated(Bundle savedInstanceState)126 public void onActivityCreated(Bundle savedInstanceState) { 127 super.onActivityCreated(savedInstanceState); 128 129 Utils.setTimeFormat(getActivity(), mDigitalClock); 130 } 131 132 @Override onResume()133 public void onResume() { 134 super.onResume(); 135 136 final Activity activity = getActivity(); 137 setFabAppearance(); 138 setLeftRightButtonAppearance(); 139 140 mDateFormat = getString(R.string.abbrev_wday_month_day_no_year); 141 mDateFormatForAccessibility = getString(R.string.full_wday_month_day_no_year); 142 143 // Schedule a runnable to update the date every quarter hour. 144 Utils.setQuarterHourUpdater(mHandler, mQuarterHourUpdater); 145 146 // Watch for system events that effect clock time or format. 147 final IntentFilter filter = new IntentFilter(); 148 filter.addAction(Intent.ACTION_TIME_CHANGED); 149 filter.addAction(Intent.ACTION_TIMEZONE_CHANGED); 150 filter.addAction(AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED); 151 activity.registerReceiver(mBroadcastReceiver, filter); 152 153 // Resume can be invoked after changing the clock style. 154 Utils.setClockStyle(mDigitalClock, mAnalogClock); 155 156 final View view = getView(); 157 if (view != null && view.findViewById(R.id.main_clock_left_pane) != null) { 158 // Center the main clock frame by hiding the world clocks when none are selected. 159 mCityList.setVisibility(mCityAdapter.getCount() == 0 ? GONE : VISIBLE); 160 } 161 162 refreshDates(); 163 refreshAlarm(); 164 165 if (mAlarmObserver != null) { 166 final Uri uri = Settings.System.getUriFor(Settings.System.NEXT_ALARM_FORMATTED); 167 activity.getContentResolver().registerContentObserver(uri, false, mAlarmObserver); 168 } 169 } 170 171 @Override onPause()172 public void onPause() { 173 super.onPause(); 174 Utils.cancelQuarterHourUpdater(mHandler, mQuarterHourUpdater); 175 176 final Activity activity = getActivity(); 177 activity.unregisterReceiver(mBroadcastReceiver); 178 if (mAlarmObserver != null) { 179 activity.getContentResolver().unregisterContentObserver(mAlarmObserver); 180 } 181 } 182 183 @Override onFabClick(View view)184 public void onFabClick(View view) { 185 startActivity(new Intent(getActivity(), CitySelectionActivity.class)); 186 } 187 188 @Override setFabAppearance()189 public void setFabAppearance() { 190 if (mFab == null || getSelectedTab() != DeskClock.CLOCK_TAB_INDEX) { 191 return; 192 } 193 194 mFab.setVisibility(VISIBLE); 195 mFab.setImageResource(R.drawable.ic_language_white_24dp); 196 mFab.setContentDescription(getString(R.string.button_cities)); 197 } 198 199 @Override setLeftRightButtonAppearance()200 public void setLeftRightButtonAppearance() { 201 if (getSelectedTab() != DeskClock.CLOCK_TAB_INDEX) { 202 return; 203 } 204 205 if (mLeftButton != null) { 206 mLeftButton.setVisibility(INVISIBLE); 207 } 208 209 if (mRightButton != null) { 210 mRightButton.setVisibility(INVISIBLE); 211 } 212 } 213 214 /** 215 * Refresh the displayed dates in response to a change that may have changed them. 216 */ refreshDates()217 private void refreshDates() { 218 // Refresh the date in the main clock. 219 Utils.updateDate(mDateFormat, mDateFormatForAccessibility, mClockFrame); 220 221 // Refresh the day-of-week in each world clock. 222 mCityAdapter.notifyDataSetChanged(); 223 } 224 225 /** 226 * Refresh the next alarm time. 227 */ refreshAlarm()228 private void refreshAlarm() { 229 Utils.refreshAlarm(getActivity(), mClockFrame); 230 } 231 232 /** 233 * Long pressing over the main clock or any world clock item starts the screen saver. 234 */ 235 private final class StartScreenSaverListener implements OnTouchListener, Runnable { 236 237 private float mTouchSlop = -1; 238 private int mLongPressTimeout = -1; 239 private float mLastTouchX, mLastTouchY; 240 241 @Override onTouch(View v, MotionEvent event)242 public boolean onTouch(View v, MotionEvent event) { 243 if (mTouchSlop == -1) { 244 mTouchSlop = ViewConfiguration.get(getActivity()).getScaledTouchSlop(); 245 mLongPressTimeout = ViewConfiguration.getLongPressTimeout(); 246 } 247 248 switch (event.getAction()) { 249 case (MotionEvent.ACTION_DOWN): 250 // Create and post a runnable to start the screen saver in the future. 251 mHandler.postDelayed(this, mLongPressTimeout); 252 mLastTouchX = event.getX(); 253 mLastTouchY = event.getY(); 254 return true; 255 256 case (MotionEvent.ACTION_MOVE): 257 final float xDiff = Math.abs(event.getX() - mLastTouchX); 258 final float yDiff = Math.abs(event.getY() - mLastTouchY); 259 if (xDiff >= mTouchSlop || yDiff >= mTouchSlop) { 260 mHandler.removeCallbacks(this); 261 } 262 break; 263 default: 264 mHandler.removeCallbacks(this); 265 } 266 return false; 267 } 268 269 @Override run()270 public void run() { 271 startActivity(new Intent(getActivity(), ScreensaverActivity.class)); 272 } 273 } 274 275 /** 276 * This runnable executes at every quarter-hour (e.g. 1:00, 1:15, 1:30, 1:45, etc...) and 277 * updates the dates displayed within the UI. Quarter-hour increments were chosen to accommodate 278 * the "weirdest" timezones (e.g. Nepal is UTC/GMT +05:45). 279 */ 280 private final class QuarterHourRunnable implements Runnable { 281 @Override run()282 public void run() { 283 refreshDates(); 284 285 // Schedule the next quarter-hour callback. 286 Utils.setQuarterHourUpdater(mHandler, mQuarterHourUpdater); 287 } 288 } 289 290 /** 291 * Prior to L, a ContentObserver was used to monitor changes to the next scheduled alarm. 292 * In L and beyond this is accomplished via a system broadcast of 293 * {@link AlarmManager#ACTION_NEXT_ALARM_CLOCK_CHANGED}. 294 */ 295 private final class AlarmObserverPreL extends ContentObserver { AlarmObserverPreL(Handler handler)296 public AlarmObserverPreL(Handler handler) { 297 super(handler); 298 } 299 300 @Override onChange(boolean selfChange)301 public void onChange(boolean selfChange) { 302 Utils.refreshAlarm(getActivity(), mClockFrame); 303 } 304 } 305 306 /** 307 * Handle system broadcasts that influence the display of this fragment. Since this fragment 308 * displays time-related information, ACTION_TIME_CHANGED and ACTION_TIMEZONE_CHANGED both 309 * alter the actual time values displayed. ACTION_NEXT_ALARM_CLOCK_CHANGED indicates the time at 310 * which the next alarm will fire has changed. 311 */ 312 private final class SystemBroadcastReceiver extends BroadcastReceiver { 313 @Override onReceive(Context context, Intent intent)314 public void onReceive(Context context, Intent intent) { 315 switch (intent.getAction()) { 316 case Intent.ACTION_TIME_CHANGED: 317 case Intent.ACTION_TIMEZONE_CHANGED: 318 refreshDates(); 319 320 case AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED: 321 refreshAlarm(); 322 } 323 } 324 } 325 326 /** 327 * This adapter lists all of the selected world clocks. Optionally, it also includes a clock at 328 * the top for the home timezone if "Automatic home clock" is turned on in settings and the 329 * current time at home does not match the current time in the timezone of the current location. 330 */ 331 private static final class SelectedCitiesAdapter extends BaseAdapter { 332 333 private final LayoutInflater mInflater; 334 private final Context mContext; 335 SelectedCitiesAdapter(Context context)336 public SelectedCitiesAdapter(Context context) { 337 mContext = context; 338 mInflater = LayoutInflater.from(context); 339 } 340 341 @Override getCount()342 public int getCount() { 343 final int homeClockCount = getShowHomeClock() ? 1 : 0; 344 final int worldClockCount = getCities().size(); 345 return homeClockCount + worldClockCount; 346 } 347 348 @Override getItem(int position)349 public Object getItem(int position) { 350 if (getShowHomeClock()) { 351 return position == 0 ? getHomeCity() : getCities().get(position - 1); 352 } 353 354 return getCities().get(position); 355 } 356 357 @Override getItemId(int position)358 public long getItemId(int position) { 359 return position; 360 } 361 362 @Override getView(int position, View view, ViewGroup parent)363 public View getView(int position, View view, ViewGroup parent) { 364 // Retrieve the city to bind. 365 final City city = (City) getItem(position); 366 367 // Inflate a new view for the city, if necessary. 368 if (view == null) { 369 view = mInflater.inflate(R.layout.world_clock_list_item, parent, false); 370 } 371 372 final View clock = view.findViewById(R.id.city_left); 373 374 // Configure the digital clock or analog clock depending on the user preference. 375 final TextClock digitalClock = (TextClock) clock.findViewById(R.id.digital_clock); 376 final AnalogClock analogClock = (AnalogClock) clock.findViewById(R.id.analog_clock); 377 if (DataModel.getDataModel().getClockStyle() == DataModel.ClockStyle.ANALOG) { 378 digitalClock.setVisibility(GONE); 379 analogClock.setVisibility(VISIBLE); 380 analogClock.setTimeZone(city.getTimeZoneId()); 381 analogClock.enableSeconds(false); 382 } else { 383 digitalClock.setVisibility(VISIBLE); 384 analogClock.setVisibility(GONE); 385 digitalClock.setTimeZone(city.getTimeZoneId()); 386 Utils.setTimeFormat(mContext, digitalClock); 387 } 388 389 // Bind the city name. 390 final TextView name = (TextView) clock.findViewById(R.id.city_name); 391 name.setText(city.getName()); 392 393 // Compute if the city week day matches the weekday of the current timezone. 394 final Calendar localCal = Calendar.getInstance(TimeZone.getDefault()); 395 final Calendar cityCal = Calendar.getInstance(city.getTimeZone()); 396 final boolean displayDayOfWeek = localCal.get(DAY_OF_WEEK) != cityCal.get(DAY_OF_WEEK); 397 398 // Bind the week day display. 399 final TextView dayOfWeek = (TextView) clock.findViewById(R.id.city_day); 400 dayOfWeek.setVisibility(displayDayOfWeek ? VISIBLE : GONE); 401 if (displayDayOfWeek) { 402 final Locale locale = Locale.getDefault(); 403 final String weekday = cityCal.getDisplayName(DAY_OF_WEEK, Calendar.SHORT, locale); 404 dayOfWeek.setText(mContext.getString(R.string.world_day_of_week_label, weekday)); 405 } 406 407 return view; 408 } 409 410 /** 411 * @return {@code false} to prevent the cities from responding to touch 412 */ 413 @Override isEnabled(int position)414 public boolean isEnabled(int position) { 415 return false; 416 } 417 getHomeCity()418 private City getHomeCity() { 419 return DataModel.getDataModel().getHomeCity(); 420 } 421 getCities()422 private List<City> getCities() { 423 return DataModel.getDataModel().getSelectedCities(); 424 } 425 getShowHomeClock()426 private boolean getShowHomeClock() { 427 return DataModel.getDataModel().getShowHomeClock(); 428 } 429 } 430 } 431