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