Home | History | Annotate | Download | only in deskclock
      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. */
     79     public ClockFragment() {}
     80 
     81     @Override
     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
     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
    126     public void onActivityCreated(Bundle savedInstanceState) {
    127         super.onActivityCreated(savedInstanceState);
    128 
    129         Utils.setTimeFormat(getActivity(), mDigitalClock);
    130     }
    131 
    132     @Override
    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
    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
    184     public void onFabClick(View view) {
    185         startActivity(new Intent(getActivity(), CitySelectionActivity.class));
    186     }
    187 
    188     @Override
    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
    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      */
    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      */
    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
    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
    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
    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 {
    296         public AlarmObserverPreL(Handler handler) {
    297             super(handler);
    298         }
    299 
    300         @Override
    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
    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 
    336         public SelectedCitiesAdapter(Context context) {
    337             mContext = context;
    338             mInflater = LayoutInflater.from(context);
    339         }
    340 
    341         @Override
    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
    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
    358         public long getItemId(int position) {
    359             return position;
    360         }
    361 
    362         @Override
    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
    414         public boolean isEnabled(int position) {
    415             return false;
    416         }
    417 
    418         private City getHomeCity() {
    419             return DataModel.getDataModel().getHomeCity();
    420         }
    421 
    422         private List<City> getCities() {
    423             return DataModel.getDataModel().getSelectedCities();
    424         }
    425 
    426         private boolean getShowHomeClock() {
    427             return DataModel.getDataModel().getShowHomeClock();
    428         }
    429     }
    430 }
    431