/* * Copyright (C) 2012 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.deskclock; import android.app.Activity; import android.app.AlarmManager; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.res.Resources; import android.database.ContentObserver; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.provider.Settings; import android.support.annotation.NonNull; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.text.format.DateUtils; import android.view.GestureDetector; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.widget.Button; import android.widget.ImageView; import android.widget.TextClock; import android.widget.TextView; import com.android.deskclock.data.City; import com.android.deskclock.data.CityListener; import com.android.deskclock.data.DataModel; import com.android.deskclock.events.Events; import com.android.deskclock.uidata.UiDataModel; import com.android.deskclock.worldclock.CitySelectionActivity; import java.util.Calendar; import java.util.List; import java.util.TimeZone; import static android.app.AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED; import static android.view.View.GONE; import static android.view.View.INVISIBLE; import static android.view.View.VISIBLE; import static com.android.deskclock.uidata.UiDataModel.Tab.CLOCKS; import static java.util.Calendar.DAY_OF_WEEK; /** * Fragment that shows the clock (analog or digital), the next alarm info and the world clock. */ public final class ClockFragment extends DeskClockFragment { // Updates dates in the UI on every quarter-hour. private final Runnable mQuarterHourUpdater = new QuarterHourRunnable(); // Updates the UI in response to changes to the scheduled alarm. private BroadcastReceiver mAlarmChangeReceiver; // Detects changes to the next scheduled alarm pre-L. private ContentObserver mAlarmObserver; private TextClock mDigitalClock; private AnalogClock mAnalogClock; private View mClockFrame; private SelectedCitiesAdapter mCityAdapter; private RecyclerView mCityList; private String mDateFormat; private String mDateFormatForAccessibility; /** * The public no-arg constructor required by all fragments. */ public ClockFragment() { super(CLOCKS); } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mAlarmObserver = Utils.isPreL() ? new AlarmObserverPreL() : null; mAlarmChangeReceiver = Utils.isLOrLater() ? new AlarmChangedBroadcastReceiver() : null; } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle icicle) { super.onCreateView(inflater, container, icicle); final View fragmentView = inflater.inflate(R.layout.clock_fragment, container, false); mDateFormat = getString(R.string.abbrev_wday_month_day_no_year); mDateFormatForAccessibility = getString(R.string.full_wday_month_day_no_year); mCityAdapter = new SelectedCitiesAdapter(getActivity(), mDateFormat, mDateFormatForAccessibility); mCityList = (RecyclerView) fragmentView.findViewById(R.id.cities); mCityList.setLayoutManager(new LinearLayoutManager(getActivity())); mCityList.setAdapter(mCityAdapter); mCityList.setItemAnimator(null); DataModel.getDataModel().addCityListener(mCityAdapter); final ScrollPositionWatcher scrollPositionWatcher = new ScrollPositionWatcher(); mCityList.addOnScrollListener(scrollPositionWatcher); final Context context = container.getContext(); mCityList.setOnTouchListener(new CityListOnLongClickListener(context)); fragmentView.setOnLongClickListener(new StartScreenSaverListener()); // On tablet landscape, the clock frame will be a distinct view. Otherwise, it'll be added // on as a header to the main listview. mClockFrame = fragmentView.findViewById(R.id.main_clock_left_pane); if (mClockFrame != null) { mDigitalClock = (TextClock) mClockFrame.findViewById(R.id.digital_clock); mAnalogClock = (AnalogClock) mClockFrame.findViewById(R.id.analog_clock); Utils.setClockIconTypeface(mClockFrame); Utils.updateDate(mDateFormat, mDateFormatForAccessibility, mClockFrame); Utils.setClockStyle(mDigitalClock, mAnalogClock); Utils.setClockSecondsEnabled(mDigitalClock, mAnalogClock); } // Schedule a runnable to update the date every quarter hour. UiDataModel.getUiDataModel().addQuarterHourCallback(mQuarterHourUpdater, 100); return fragmentView; } @Override public void onResume() { super.onResume(); final Activity activity = getActivity(); mDateFormat = getString(R.string.abbrev_wday_month_day_no_year); mDateFormatForAccessibility = getString(R.string.full_wday_month_day_no_year); // Watch for system events that effect clock time or format. if (mAlarmChangeReceiver != null) { final IntentFilter filter = new IntentFilter(ACTION_NEXT_ALARM_CLOCK_CHANGED); activity.registerReceiver(mAlarmChangeReceiver, filter); } // Resume can be invoked after changing the clock style or seconds display. if (mDigitalClock != null && mAnalogClock != null) { Utils.setClockStyle(mDigitalClock, mAnalogClock); Utils.setClockSecondsEnabled(mDigitalClock, mAnalogClock); } final View view = getView(); if (view != null && view.findViewById(R.id.main_clock_left_pane) != null) { // Center the main clock frame by hiding the world clocks when none are selected. mCityList.setVisibility(mCityAdapter.getItemCount() == 0 ? GONE : VISIBLE); } refreshAlarm(); // Alarm observer is null on L or later. if (mAlarmObserver != null) { @SuppressWarnings("deprecation") final Uri uri = Settings.System.getUriFor(Settings.System.NEXT_ALARM_FORMATTED); activity.getContentResolver().registerContentObserver(uri, false, mAlarmObserver); } } @Override public void onPause() { super.onPause(); final Activity activity = getActivity(); if (mAlarmChangeReceiver != null) { activity.unregisterReceiver(mAlarmChangeReceiver); } if (mAlarmObserver != null) { activity.getContentResolver().unregisterContentObserver(mAlarmObserver); } } @Override public void onDestroyView() { super.onDestroyView(); UiDataModel.getUiDataModel().removePeriodicCallback(mQuarterHourUpdater); DataModel.getDataModel().removeCityListener(mCityAdapter); } @Override public void onFabClick(@NonNull ImageView fab) { startActivity(new Intent(getActivity(), CitySelectionActivity.class)); } @Override public void onUpdateFab(@NonNull ImageView fab) { fab.setVisibility(VISIBLE); fab.setImageResource(R.drawable.ic_public); fab.setContentDescription(fab.getResources().getString(R.string.button_cities)); } @Override public void onUpdateFabButtons(@NonNull Button left, @NonNull Button right) { left.setVisibility(INVISIBLE); right.setVisibility(INVISIBLE); } /** * Refresh the next alarm time. */ private void refreshAlarm() { if (mClockFrame != null) { Utils.refreshAlarm(getActivity(), mClockFrame); } else { mCityAdapter.refreshAlarm(); } } /** * Long pressing over the main clock starts the screen saver. */ private final class StartScreenSaverListener implements View.OnLongClickListener { @Override public boolean onLongClick(View view) { startActivity(new Intent(getActivity(), ScreensaverActivity.class) .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) .putExtra(Events.EXTRA_EVENT_LABEL, R.string.label_deskclock)); return true; } } /** * Long pressing over the city list starts the screen saver. */ private final class CityListOnLongClickListener extends GestureDetector.SimpleOnGestureListener implements View.OnTouchListener { private final GestureDetector mGestureDetector; private CityListOnLongClickListener(Context context) { mGestureDetector = new GestureDetector(context, this); } @Override public void onLongPress(MotionEvent e) { final View view = getView(); if (view != null) { view.performLongClick(); } } @Override public boolean onDown(MotionEvent e) { return true; } @Override public boolean onTouch(View v, MotionEvent event) { return mGestureDetector.onTouchEvent(event); } } /** * This runnable executes at every quarter-hour (e.g. 1:00, 1:15, 1:30, 1:45, etc...) and * updates the dates displayed within the UI. Quarter-hour increments were chosen to accommodate * the "weirdest" timezones (e.g. Nepal is UTC/GMT +05:45). */ private final class QuarterHourRunnable implements Runnable { @Override public void run() { mCityAdapter.notifyDataSetChanged(); } } /** * Prior to L, a ContentObserver was used to monitor changes to the next scheduled alarm. * In L and beyond this is accomplished via a system broadcast of * {@link AlarmManager#ACTION_NEXT_ALARM_CLOCK_CHANGED}. */ private final class AlarmObserverPreL extends ContentObserver { private AlarmObserverPreL() { super(new Handler()); } @Override public void onChange(boolean selfChange) { refreshAlarm(); } } /** * Update the display of the scheduled alarm as it changes. */ private final class AlarmChangedBroadcastReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { refreshAlarm(); } } /** * Updates the vertical scroll state of this tab in the {@link UiDataModel} as the user scrolls * the recyclerview or when the size/position of elements within the recyclerview changes. */ private final class ScrollPositionWatcher extends RecyclerView.OnScrollListener implements View.OnLayoutChangeListener { @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { setTabScrolledToTop(Utils.isScrolledToTop(mCityList)); } @Override public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { setTabScrolledToTop(Utils.isScrolledToTop(mCityList)); } } /** * This adapter lists all of the selected world clocks. Optionally, it also includes a clock at * the top for the home timezone if "Automatic home clock" is turned on in settings and the * current time at home does not match the current time in the timezone of the current location. * If the phone is in portrait mode it will also include the main clock at the top. */ private static final class SelectedCitiesAdapter extends RecyclerView.Adapter implements CityListener { private final static int MAIN_CLOCK = R.layout.main_clock_frame; private final static int WORLD_CLOCK = R.layout.world_clock_item; private final LayoutInflater mInflater; private final Context mContext; private final boolean mIsPortrait; private final boolean mShowHomeClock; private final String mDateFormat; private final String mDateFormatForAccessibility; private SelectedCitiesAdapter(Context context, String dateFormat, String dateFormatForAccessibility) { mContext = context; mDateFormat = dateFormat; mDateFormatForAccessibility = dateFormatForAccessibility; mInflater = LayoutInflater.from(context); mIsPortrait = Utils.isPortrait(context); mShowHomeClock = DataModel.getDataModel().getShowHomeClock(); } @Override public int getItemViewType(int position) { if (position == 0 && mIsPortrait) { return MAIN_CLOCK; } return WORLD_CLOCK; } @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { final View view = mInflater.inflate(viewType, parent, false); switch (viewType) { case WORLD_CLOCK: return new CityViewHolder(view); case MAIN_CLOCK: return new MainClockViewHolder(view); default: throw new IllegalArgumentException("View type not recognized"); } } @Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { final int viewType = getItemViewType(position); switch (viewType) { case WORLD_CLOCK: // Retrieve the city to bind. final City city; // If showing home clock, put it at the top if (mShowHomeClock && position == (mIsPortrait ? 1 : 0)) { city = getHomeCity(); } else { final int positionAdjuster = (mIsPortrait ? 1 : 0) + (mShowHomeClock ? 1 : 0); city = getCities().get(position - positionAdjuster); } ((CityViewHolder) holder).bind(mContext, city, position, mIsPortrait); break; case MAIN_CLOCK: ((MainClockViewHolder) holder).bind(mContext, mDateFormat, mDateFormatForAccessibility, getItemCount() > 1); break; default: throw new IllegalArgumentException("Unexpected view type: " + viewType); } } @Override public int getItemCount() { final int mainClockCount = mIsPortrait ? 1 : 0; final int homeClockCount = mShowHomeClock ? 1 : 0; final int worldClockCount = getCities().size(); return mainClockCount + homeClockCount + worldClockCount; } private City getHomeCity() { return DataModel.getDataModel().getHomeCity(); } private List getCities() { return DataModel.getDataModel().getSelectedCities(); } private void refreshAlarm() { if (mIsPortrait && getItemCount() > 0) { notifyItemChanged(0); } } @Override public void citiesChanged(List oldCities, List newCities) { notifyDataSetChanged(); } private static final class CityViewHolder extends RecyclerView.ViewHolder { private final TextView mName; private final TextClock mDigitalClock; private final AnalogClock mAnalogClock; private final TextView mHoursAhead; private CityViewHolder(View itemView) { super(itemView); mName = (TextView) itemView.findViewById(R.id.city_name); mDigitalClock = (TextClock) itemView.findViewById(R.id.digital_clock); mAnalogClock = (AnalogClock) itemView.findViewById(R.id.analog_clock); mHoursAhead = (TextView) itemView.findViewById(R.id.hours_ahead); } private void bind(Context context, City city, int position, boolean isPortrait) { final String cityTimeZoneId = city.getTimeZone().getID(); // Configure the digital clock or analog clock depending on the user preference. if (DataModel.getDataModel().getClockStyle() == DataModel.ClockStyle.ANALOG) { mDigitalClock.setVisibility(GONE); mAnalogClock.setVisibility(VISIBLE); mAnalogClock.setTimeZone(cityTimeZoneId); mAnalogClock.enableSeconds(false); } else { mAnalogClock.setVisibility(GONE); mDigitalClock.setVisibility(VISIBLE); mDigitalClock.setTimeZone(cityTimeZoneId); mDigitalClock.setFormat12Hour(Utils.get12ModeFormat(0.3f /* amPmRatio */, false)); mDigitalClock.setFormat24Hour(Utils.get24ModeFormat(false)); } // Supply top and bottom padding dynamically. final Resources res = context.getResources(); final int padding = res.getDimensionPixelSize(R.dimen.medium_space_top); final int top = position == 0 && !isPortrait ? 0 : padding; final int left = itemView.getPaddingLeft(); final int right = itemView.getPaddingRight(); final int bottom = itemView.getPaddingBottom(); itemView.setPadding(left, top, right, bottom); // Bind the city name. mName.setText(city.getName()); // Compute if the city week day matches the weekday of the current timezone. final Calendar localCal = Calendar.getInstance(TimeZone.getDefault()); final Calendar cityCal = Calendar.getInstance(city.getTimeZone()); final boolean displayDayOfWeek = localCal.get(DAY_OF_WEEK) != cityCal.get(DAY_OF_WEEK); // Compare offset from UTC time on today's date (daylight savings time, etc.) final TimeZone currentTimeZone = TimeZone.getDefault(); final TimeZone cityTimeZone = TimeZone.getTimeZone(cityTimeZoneId); final long currentTimeMillis = System.currentTimeMillis(); final long currentUtcOffset = currentTimeZone.getOffset(currentTimeMillis); final long cityUtcOffset = cityTimeZone.getOffset(currentTimeMillis); final long offsetDelta = cityUtcOffset - currentUtcOffset; final int hoursDifferent = (int) (offsetDelta / DateUtils.HOUR_IN_MILLIS); final int minutesDifferent = (int) (offsetDelta / DateUtils.MINUTE_IN_MILLIS) % 60; final boolean displayMinutes = offsetDelta % DateUtils.HOUR_IN_MILLIS != 0; final boolean isAhead = hoursDifferent > 0 || (hoursDifferent == 0 && minutesDifferent > 0); if (!Utils.isLandscape(context)) { // Bind the number of hours ahead or behind, or hide if the time is the same. final boolean displayDifference = hoursDifferent != 0 || displayMinutes; mHoursAhead.setVisibility(displayDifference ? VISIBLE : GONE); final String timeString = Utils.createHoursDifferentString( context, displayMinutes, isAhead, hoursDifferent, minutesDifferent); mHoursAhead.setText(displayDayOfWeek ? (context.getString(isAhead ? R.string.world_hours_tomorrow : R.string.world_hours_yesterday, timeString)) : timeString); } else { // Only tomorrow/yesterday should be shown in landscape view. mHoursAhead.setVisibility(displayDayOfWeek ? View.VISIBLE : View.GONE); if (displayDayOfWeek) { mHoursAhead.setText(context.getString(isAhead ? R.string.world_tomorrow : R.string.world_yesterday)); } } } } private static final class MainClockViewHolder extends RecyclerView.ViewHolder { private final View mHairline; private final TextClock mDigitalClock; private final AnalogClock mAnalogClock; private MainClockViewHolder(View itemView) { super(itemView); mHairline = itemView.findViewById(R.id.hairline); mDigitalClock = (TextClock) itemView.findViewById(R.id.digital_clock); mAnalogClock = (AnalogClock) itemView.findViewById(R.id.analog_clock); Utils.setClockIconTypeface(itemView); } private void bind(Context context, String dateFormat, String dateFormatForAccessibility, boolean showHairline) { Utils.refreshAlarm(context, itemView); Utils.updateDate(dateFormat, dateFormatForAccessibility, itemView); Utils.setClockStyle(mDigitalClock, mAnalogClock); mHairline.setVisibility(showHairline ? VISIBLE : GONE); Utils.setClockSecondsEnabled(mDigitalClock, mAnalogClock); } } } }