DeskClock.java revision 11c38e64f05bbcfa4503edb1680f255946ff6350
1/*
2 * Copyright (C) 2009 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
17package com.android.deskclock;
18
19import android.app.ActionBar;
20import android.app.ActionBar.Tab;
21import android.app.Activity;
22import android.app.Fragment;
23import android.app.FragmentManager;
24import android.app.FragmentTransaction;
25import android.content.ActivityNotFoundException;
26import android.content.Context;
27import android.content.Intent;
28import android.content.SharedPreferences;
29import android.content.res.Configuration;
30import android.os.Bundle;
31import android.preference.PreferenceManager;
32import android.support.v13.app.FragmentPagerAdapter;
33import android.support.v4.view.ViewPager;
34import android.text.TextUtils;
35import android.util.Log;
36import android.view.Menu;
37import android.view.MenuItem;
38import android.view.MotionEvent;
39import android.view.View;
40import android.view.View.OnTouchListener;
41import android.widget.PopupMenu;
42import android.widget.TextView;
43
44import com.android.deskclock.alarms.AlarmStateManager;
45import com.android.deskclock.provider.Alarm;
46import com.android.deskclock.stopwatch.StopwatchFragment;
47import com.android.deskclock.stopwatch.StopwatchService;
48import com.android.deskclock.stopwatch.Stopwatches;
49import com.android.deskclock.timer.TimerFragment;
50import com.android.deskclock.timer.TimerObj;
51import com.android.deskclock.timer.Timers;
52import com.android.deskclock.worldclock.CitiesActivity;
53
54import java.util.ArrayList;
55import java.util.HashSet;
56import java.util.Locale;
57import java.util.TimeZone;
58
59/**
60 * DeskClock clock view for desk docks.
61 */
62public class DeskClock extends Activity implements LabelDialogFragment.TimerLabelDialogHandler,
63            LabelDialogFragment.AlarmLabelDialogHandler{
64    private static final boolean DEBUG = false;
65
66    private static final String LOG_TAG = "DeskClock";
67
68    // Alarm action for midnight (so we can update the date display).
69    private static final String KEY_SELECTED_TAB = "selected_tab";
70    private static final String KEY_CLOCK_STATE = "clock_state";
71
72    public static final String SELECT_TAB_INTENT_EXTRA = "deskclock.select.tab";
73
74    private ActionBar mActionBar;
75    private Tab mAlarmTab;
76    private Tab mClockTab;
77    private Tab mTimerTab;
78    private Tab mStopwatchTab;
79    private Menu mMenu;
80
81    private ViewPager mViewPager;
82    private TabsAdapter mTabsAdapter;
83
84    public static final int ALARM_TAB_INDEX = 0;
85    public static final int CLOCK_TAB_INDEX = 1;
86    public static final int TIMER_TAB_INDEX = 2;
87    public static final int STOPWATCH_TAB_INDEX = 3;
88    // Tabs indices are switched for right-to-left since there is no
89    // native support for RTL in the ViewPager.
90    public static final int RTL_ALARM_TAB_INDEX = 3;
91    public static final int RTL_CLOCK_TAB_INDEX = 2;
92    public static final int RTL_TIMER_TAB_INDEX = 1;
93    public static final int RTL_STOPWATCH_TAB_INDEX = 0;
94
95    private int mSelectedTab;
96
97    @Override
98    public void onNewIntent(Intent newIntent) {
99        super.onNewIntent(newIntent);
100        if (DEBUG) Log.d(LOG_TAG, "onNewIntent with intent: " + newIntent);
101
102        // update our intent so that we can consult it to determine whether or
103        // not the most recent launch was via a dock event
104        setIntent(newIntent);
105
106        // Timer receiver may ask to go to the timers fragment if a timer expired.
107        int tab = newIntent.getIntExtra(SELECT_TAB_INTENT_EXTRA, -1);
108        if (tab != -1) {
109            if (mActionBar != null) {
110                mActionBar.setSelectedNavigationItem(tab);
111            }
112        }
113    }
114
115    private void initViews() {
116        if (mTabsAdapter == null) {
117            mViewPager = new ViewPager(this);
118            mViewPager.setId(R.id.desk_clock_pager);
119            // Keep all four tabs to minimize jank.
120            mViewPager.setOffscreenPageLimit(3);
121            mTabsAdapter = new TabsAdapter(this, mViewPager);
122            createTabs(mSelectedTab);
123        }
124        setContentView(mViewPager);
125        mActionBar.setSelectedNavigationItem(mSelectedTab);
126    }
127
128    private void createTabs(int selectedIndex) {
129        mActionBar = getActionBar();
130
131        if (mActionBar != null) {
132            mActionBar.setDisplayOptions(0);
133            mActionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
134
135            mAlarmTab = mActionBar.newTab();
136            mAlarmTab.setIcon(R.drawable.alarm_tab);
137            mAlarmTab.setContentDescription(R.string.menu_alarm);
138            mTabsAdapter.addTab(mAlarmTab, AlarmClockFragment.class, ALARM_TAB_INDEX);
139
140            mClockTab = mActionBar.newTab();
141            mClockTab.setIcon(R.drawable.clock_tab);
142            mClockTab.setContentDescription(R.string.menu_clock);
143            mTabsAdapter.addTab(mClockTab, ClockFragment.class, CLOCK_TAB_INDEX);
144
145            mTimerTab = mActionBar.newTab();
146            mTimerTab.setIcon(R.drawable.timer_tab);
147            mTimerTab.setContentDescription(R.string.menu_timer);
148            mTabsAdapter.addTab(mTimerTab, TimerFragment.class, TIMER_TAB_INDEX);
149
150            mStopwatchTab = mActionBar.newTab();
151            mStopwatchTab.setIcon(R.drawable.stopwatch_tab);
152            mStopwatchTab.setContentDescription(R.string.menu_stopwatch);
153            mTabsAdapter.addTab(mStopwatchTab, StopwatchFragment.class,STOPWATCH_TAB_INDEX);
154
155            mActionBar.setSelectedNavigationItem(selectedIndex);
156            mTabsAdapter.notifySelectedPage(selectedIndex);
157        }
158    }
159
160    @Override
161    protected void onCreate(Bundle icicle) {
162        super.onCreate(icicle);
163
164        mSelectedTab = CLOCK_TAB_INDEX;
165        if (icicle != null) {
166            mSelectedTab = icicle.getInt(KEY_SELECTED_TAB, CLOCK_TAB_INDEX);
167        }
168
169        // Timer receiver may ask the app to go to the timer fragment if a timer expired
170        Intent i = getIntent();
171        if (i != null) {
172            int tab = i.getIntExtra(SELECT_TAB_INTENT_EXTRA, -1);
173            if (tab != -1) {
174                mSelectedTab = tab;
175            }
176        }
177        initViews();
178        setHomeTimeZone();
179
180        // We need to update the system next alarm time on app startup because the
181        // user might have clear our data.
182        AlarmStateManager.updateNextAlarm(this);
183        ExtensionsFactory.init(getAssets());
184    }
185
186    @Override
187    protected void onResume() {
188        super.onResume();
189
190        // We only want to show notifications for stopwatch/timer when the app is closed so
191        // that we don't have to worry about keeping the notifications in perfect sync with
192        // the app.
193        Intent stopwatchIntent = new Intent(getApplicationContext(), StopwatchService.class);
194        stopwatchIntent.setAction(Stopwatches.KILL_NOTIF);
195        startService(stopwatchIntent);
196
197        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
198        SharedPreferences.Editor editor = prefs.edit();
199        editor.putBoolean(Timers.NOTIF_APP_OPEN, true);
200        editor.apply();
201        Intent timerIntent = new Intent();
202        timerIntent.setAction(Timers.NOTIF_IN_USE_CANCEL);
203        sendBroadcast(timerIntent);
204    }
205
206    @Override
207    public void onPause() {
208        Intent intent = new Intent(getApplicationContext(), StopwatchService.class);
209        intent.setAction(Stopwatches.SHOW_NOTIF);
210        startService(intent);
211
212        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
213        SharedPreferences.Editor editor = prefs.edit();
214        editor.putBoolean(Timers.NOTIF_APP_OPEN, false);
215        editor.apply();
216        Utils.showInUseNotifications(this);
217
218        super.onPause();
219    }
220
221    @Override
222    protected void onSaveInstanceState(Bundle outState) {
223        super.onSaveInstanceState(outState);
224        outState.putInt(KEY_SELECTED_TAB, mActionBar.getSelectedNavigationIndex());
225    }
226
227    public void clockButtonsOnClick(View v) {
228        if (v == null) {
229            return;
230        }
231        switch (v.getId()) {
232            case R.id.cities_button:
233                startActivity(new Intent(this, CitiesActivity.class));
234                break;
235            default:
236                break;
237        }
238    }
239
240    @Override
241    public boolean onCreateOptionsMenu(Menu menu) {
242        // We only want to show it as a menu in landscape, and only for clock/alarm fragment.
243        mMenu = menu;
244        if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
245            if (mActionBar.getSelectedNavigationIndex() == ALARM_TAB_INDEX ||
246                    mActionBar.getSelectedNavigationIndex() == CLOCK_TAB_INDEX) {
247                // Clear the menu so that it doesn't get duplicate items in case onCreateOptionsMenu
248                // was called multiple times.
249                menu.clear();
250                getMenuInflater().inflate(R.menu.desk_clock_menu, menu);
251            }
252            // Always return true for landscape, regardless of whether we've inflated the menu, so
253            // that when we switch tabs this method will get called and we can inflate the menu.
254            return true;
255        }
256        return false;
257    }
258
259    @Override
260    public boolean onPrepareOptionsMenu(Menu menu) {
261        updateMenu(menu);
262        return true;
263    }
264
265    private void updateMenu(Menu menu) {
266        // Hide "help" if we don't have a URI for it.
267        MenuItem help = menu.findItem(R.id.menu_item_help);
268        if (help != null) {
269            Utils.prepareHelpMenuItem(this, help);
270        }
271
272        // Hide "lights out" for timer.
273        MenuItem nightMode = menu.findItem(R.id.menu_item_night_mode);
274        if (mActionBar.getSelectedNavigationIndex() == ALARM_TAB_INDEX) {
275            nightMode.setVisible(false);
276        } else if (mActionBar.getSelectedNavigationIndex() == CLOCK_TAB_INDEX) {
277            nightMode.setVisible(true);
278        }
279    }
280
281    @Override
282    public boolean onOptionsItemSelected(MenuItem item) {
283        if (processMenuClick(item)) {
284            return true;
285        }
286
287        return super.onOptionsItemSelected(item);
288    }
289
290    private boolean processMenuClick(MenuItem item) {
291        switch (item.getItemId()) {
292            case R.id.menu_item_settings:
293                startActivity(new Intent(DeskClock.this, SettingsActivity.class));
294                return true;
295            case R.id.menu_item_help:
296                Intent i = item.getIntent();
297                if (i != null) {
298                    try {
299                        startActivity(i);
300                    } catch (ActivityNotFoundException e) {
301                        // No activity found to match the intent - ignore
302                    }
303                }
304                return true;
305            case R.id.menu_item_night_mode:
306                startActivity(new Intent(DeskClock.this, ScreensaverActivity.class));
307            default:
308                break;
309        }
310        return true;
311    }
312
313    /***
314     * Insert the local time zone as the Home Time Zone if one is not set
315     */
316    private void setHomeTimeZone() {
317        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
318        String homeTimeZone = prefs.getString(SettingsActivity.KEY_HOME_TZ, "");
319        if (!homeTimeZone.isEmpty()) {
320            return;
321        }
322        homeTimeZone = TimeZone.getDefault().getID();
323        SharedPreferences.Editor editor = prefs.edit();
324        editor.putString(SettingsActivity.KEY_HOME_TZ, homeTimeZone);
325        editor.apply();
326        Log.v(LOG_TAG, "Setting home time zone to " + homeTimeZone);
327    }
328
329    public void registerPageChangedListener(DeskClockFragment frag) {
330        if (mTabsAdapter != null) {
331            mTabsAdapter.registerPageChangedListener(frag);
332        }
333    }
334
335    public void unregisterPageChangedListener(DeskClockFragment frag) {
336        if (mTabsAdapter != null) {
337            mTabsAdapter.unregisterPageChangedListener(frag);
338        }
339    }
340
341
342    /***
343     * Adapter for wrapping together the ActionBar's tab with the ViewPager
344     */
345
346    private class TabsAdapter extends FragmentPagerAdapter
347            implements ActionBar.TabListener, ViewPager.OnPageChangeListener {
348
349        private static final String KEY_TAB_POSITION = "tab_position";
350
351        final class TabInfo {
352            private final Class<?> clss;
353            private final Bundle args;
354
355            TabInfo(Class<?> _class, int position) {
356                clss = _class;
357                args = new Bundle();
358                args.putInt(KEY_TAB_POSITION, position);
359            }
360
361            public int getPosition() {
362                return args.getInt(KEY_TAB_POSITION, 0);
363            }
364        }
365
366        private final ArrayList<TabInfo> mTabs = new ArrayList <TabInfo>();
367        ActionBar mMainActionBar;
368        Context mContext;
369        ViewPager mPager;
370        // Used for doing callbacks to fragments.
371        HashSet<String> mFragmentTags = new HashSet<String>();
372
373        public TabsAdapter(Activity activity, ViewPager pager) {
374            super(activity.getFragmentManager());
375            mContext = activity;
376            mMainActionBar = activity.getActionBar();
377            mPager = pager;
378            mPager.setAdapter(this);
379            mPager.setOnPageChangeListener(this);
380        }
381
382        @Override
383        public Fragment getItem(int position) {
384            TabInfo info = mTabs.get(getRtlPosition(position));
385            DeskClockFragment f = (DeskClockFragment) Fragment.instantiate(
386                    mContext, info.clss.getName(), info.args);
387            return f;
388        }
389
390        @Override
391        public int getCount() {
392            return mTabs.size();
393        }
394
395        public void addTab(ActionBar.Tab tab, Class<?> clss, int position) {
396            TabInfo info = new TabInfo(clss, position);
397            tab.setTag(info);
398            tab.setTabListener(this);
399            mTabs.add(info);
400            mMainActionBar.addTab(tab);
401            notifyDataSetChanged();
402        }
403
404        @Override
405        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
406            // Do nothing
407        }
408
409        @Override
410        public void onPageSelected(int position) {
411            // Set the page before doing the menu so that onCreateOptionsMenu knows what page it is.
412            mMainActionBar.setSelectedNavigationItem(getRtlPosition(position));
413            notifyPageChanged(position);
414
415            // Only show the overflow menu for alarm and world clock.
416            if (mMenu != null) {
417                // Make sure the menu's been initialized.
418                if (position == ALARM_TAB_INDEX || position == CLOCK_TAB_INDEX) {
419                    mMenu.setGroupVisible(R.id.menu_items, true);
420                    onCreateOptionsMenu(mMenu);
421                } else {
422                    mMenu.setGroupVisible(R.id.menu_items, false);
423                }
424            }
425        }
426
427        @Override
428        public void onPageScrollStateChanged(int state) {
429            // Do nothing
430        }
431
432        @Override
433        public void onTabReselected(Tab arg0, FragmentTransaction arg1) {
434            // Do nothing
435        }
436
437        @Override
438        public void onTabSelected(Tab tab, FragmentTransaction ft) {
439            TabInfo info = (TabInfo)tab.getTag();
440            int position = info.getPosition();
441            mPager.setCurrentItem(getRtlPosition(position));
442        }
443
444        @Override
445        public void onTabUnselected(Tab arg0, FragmentTransaction arg1) {
446            // Do nothing
447        }
448
449        public void notifySelectedPage(int page) {
450            notifyPageChanged(page);
451        }
452
453        private void notifyPageChanged(int newPage) {
454            for (String tag : mFragmentTags) {
455                final FragmentManager fm = getFragmentManager();
456                DeskClockFragment f = (DeskClockFragment) fm.findFragmentByTag(tag);
457                if (f != null) {
458                    f.onPageChanged(newPage);
459                }
460            }
461        }
462
463        public void registerPageChangedListener(DeskClockFragment frag) {
464            String tag = frag.getTag();
465            if (mFragmentTags.contains(tag)) {
466                Log.wtf(LOG_TAG, "Trying to add an existing fragment " + tag);
467            } else {
468                mFragmentTags.add(frag.getTag());
469            }
470            // Since registering a listener by the fragment is done sometimes after the page
471            // was already changed, make sure the fragment gets the current page
472            frag.onPageChanged(mMainActionBar.getSelectedNavigationIndex());
473        }
474
475        public void unregisterPageChangedListener(DeskClockFragment frag) {
476            mFragmentTags.remove(frag.getTag());
477        }
478
479        private boolean isRtl() {
480            return TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()) ==
481                    View.LAYOUT_DIRECTION_RTL;
482        }
483
484        private int getRtlPosition(int position) {
485            if (isRtl()) {
486                switch (position) {
487                    case TIMER_TAB_INDEX:
488                        return RTL_TIMER_TAB_INDEX;
489                    case CLOCK_TAB_INDEX:
490                        return RTL_CLOCK_TAB_INDEX;
491                    case STOPWATCH_TAB_INDEX:
492                        return RTL_STOPWATCH_TAB_INDEX;
493                    case ALARM_TAB_INDEX:
494                        return RTL_ALARM_TAB_INDEX;
495                    default:
496                        break;
497                }
498            }
499            return position;
500        }
501    }
502
503    public static abstract class OnTapListener implements OnTouchListener {
504        private float mLastTouchX;
505        private float mLastTouchY;
506        private long mLastTouchTime;
507        private final TextView mMakePressedTextView;
508        private final int mPressedColor, mGrayColor;
509        private final float MAX_MOVEMENT_ALLOWED = 20;
510        private final long MAX_TIME_ALLOWED = 500;
511
512        public OnTapListener(Activity activity, TextView makePressedView) {
513            mMakePressedTextView = makePressedView;
514            mPressedColor = activity.getResources().getColor(Utils.getPressedColorId());
515            mGrayColor = activity.getResources().getColor(Utils.getGrayColorId());
516        }
517
518        @Override
519        public boolean onTouch(View v, MotionEvent e) {
520            switch (e.getAction()) {
521                case (MotionEvent.ACTION_DOWN):
522                    mLastTouchTime = Utils.getTimeNow();
523                    mLastTouchX = e.getX();
524                    mLastTouchY = e.getY();
525                    if (mMakePressedTextView != null) {
526                        mMakePressedTextView.setTextColor(mPressedColor);
527                    }
528                    break;
529                case (MotionEvent.ACTION_UP):
530                    float xDiff = Math.abs(e.getX()-mLastTouchX);
531                    float yDiff = Math.abs(e.getY()-mLastTouchY);
532                    long timeDiff = (Utils.getTimeNow() - mLastTouchTime);
533                    if (xDiff < MAX_MOVEMENT_ALLOWED && yDiff < MAX_MOVEMENT_ALLOWED
534                            && timeDiff < MAX_TIME_ALLOWED) {
535                        if (mMakePressedTextView != null) {
536                            v = mMakePressedTextView;
537                        }
538                        processClick(v);
539                        resetValues();
540                        return true;
541                    }
542                    resetValues();
543                    break;
544                case (MotionEvent.ACTION_MOVE):
545                    xDiff = Math.abs(e.getX()-mLastTouchX);
546                    yDiff = Math.abs(e.getY()-mLastTouchY);
547                    if (xDiff >= MAX_MOVEMENT_ALLOWED || yDiff >= MAX_MOVEMENT_ALLOWED) {
548                        resetValues();
549                    }
550                    break;
551                default:
552                    resetValues();
553            }
554            return false;
555        }
556
557        private void resetValues() {
558            mLastTouchX = -1*MAX_MOVEMENT_ALLOWED + 1;
559            mLastTouchY = -1*MAX_MOVEMENT_ALLOWED + 1;
560            mLastTouchTime = -1*MAX_TIME_ALLOWED + 1;
561            if (mMakePressedTextView != null) {
562                mMakePressedTextView.setTextColor(mGrayColor);
563            }
564        }
565
566        protected abstract void processClick(View v);
567    }
568
569    /** Called by the LabelDialogFormat class after the dialog is finished. **/
570    @Override
571    public void onDialogLabelSet(TimerObj timer, String label, String tag) {
572        Fragment frag = getFragmentManager().findFragmentByTag(tag);
573        if (frag instanceof TimerFragment) {
574            ((TimerFragment) frag).setLabel(timer, label);
575        }
576    }
577
578    /** Called by the LabelDialogFormat class after the dialog is finished. **/
579    @Override
580    public void onDialogLabelSet(Alarm alarm, String label, String tag) {
581        Fragment frag = getFragmentManager().findFragmentByTag(tag);
582        if (frag instanceof AlarmClockFragment) {
583            ((AlarmClockFragment) frag).setLabel(alarm, label);
584        }
585    }
586}
587