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