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