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.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