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.Context;
22import android.content.Intent;
23import android.media.AudioManager;
24import android.os.Bundle;
25import android.support.annotation.VisibleForTesting;
26import android.support.design.widget.TabLayout;
27import android.support.design.widget.TabLayout.Tab;
28import android.support.design.widget.TabLayout.ViewPagerOnTabSelectedListener;
29import android.support.v13.app.FragmentPagerAdapter;
30import android.support.v4.view.ViewPager.OnPageChangeListener;
31import android.support.v7.app.AppCompatActivity;
32import android.support.v7.widget.Toolbar;
33import android.util.ArraySet;
34import android.view.Menu;
35import android.view.MenuItem;
36import android.view.View;
37import android.view.View.OnClickListener;
38import android.view.ViewGroup;
39import android.widget.ImageButton;
40import android.widget.ImageView;
41
42import com.android.deskclock.actionbarmenu.ActionBarMenuManager;
43import com.android.deskclock.actionbarmenu.MenuItemControllerFactory;
44import com.android.deskclock.actionbarmenu.NightModeMenuItemController;
45import com.android.deskclock.actionbarmenu.SettingMenuItemController;
46import com.android.deskclock.alarms.AlarmStateManager;
47import com.android.deskclock.data.DataModel;
48import com.android.deskclock.events.Events;
49import com.android.deskclock.provider.Alarm;
50import com.android.deskclock.stopwatch.StopwatchFragment;
51import com.android.deskclock.timer.TimerFragment;
52import com.android.deskclock.widget.RtlViewPager;
53
54import java.util.ArrayList;
55import java.util.List;
56import java.util.Set;
57
58/**
59 * DeskClock clock view for desk docks.
60 */
61public class DeskClock extends BaseActivity
62        implements LabelDialogFragment.AlarmLabelDialogHandler {
63
64    private static final String TAG = "DeskClock";
65
66    // Alarm action for midnight (so we can update the date display).
67    private static final String KEY_SELECTED_TAB = "selected_tab";
68    public static final String SELECT_TAB_INTENT_EXTRA = "deskclock.select.tab";
69
70    public static final int ALARM_TAB_INDEX = 0;
71    public static final int CLOCK_TAB_INDEX = 1;
72    public static final int TIMER_TAB_INDEX = 2;
73    public static final int STOPWATCH_TAB_INDEX = 3;
74
75    private final ActionBarMenuManager mActionBarMenuManager = new ActionBarMenuManager(this);
76
77    private TabLayout mTabLayout;
78    private RtlViewPager mViewPager;
79    private ImageView mFab;
80    private ImageButton mLeftButton;
81    private ImageButton mRightButton;
82
83    private TabsAdapter mTabsAdapter;
84    private int mSelectedTab;
85
86    /** {@code true} when a settings change necessitates recreating this activity. */
87    private boolean mRecreateActivity;
88
89    @Override
90    public void onNewIntent(Intent newIntent) {
91        super.onNewIntent(newIntent);
92        LogUtils.d(TAG, "onNewIntent with intent: %s", newIntent);
93
94        // update our intent so that we can consult it to determine whether or
95        // not the most recent launch was via a dock event
96        setIntent(newIntent);
97
98        // Honor the tab requested by the intent, if any.
99        int tab = newIntent.getIntExtra(SELECT_TAB_INTENT_EXTRA, -1);
100        if (tab != -1 && mTabLayout != null) {
101            mTabLayout.getTabAt(tab).select();
102            mViewPager.setCurrentItem(tab);
103        }
104    }
105
106    @VisibleForTesting
107    DeskClockFragment getSelectedFragment() {
108        return (DeskClockFragment) mTabsAdapter.getItem(mSelectedTab);
109    }
110
111    private void createTabs() {
112        final TabLayout.Tab alarmTab = mTabLayout.newTab();
113        alarmTab.setIcon(R.drawable.ic_tab_alarm).setContentDescription(R.string.menu_alarm);
114        mTabsAdapter.addTab(alarmTab, AlarmClockFragment.class, ALARM_TAB_INDEX);
115
116        final Tab clockTab = mTabLayout.newTab();
117        clockTab.setIcon(R.drawable.ic_tab_clock).setContentDescription(R.string.menu_clock);
118        mTabsAdapter.addTab(clockTab, ClockFragment.class, CLOCK_TAB_INDEX);
119
120        final Tab timerTab = mTabLayout.newTab();
121        timerTab.setIcon(R.drawable.ic_tab_timer).setContentDescription(R.string.menu_timer);
122        mTabsAdapter.addTab(timerTab, TimerFragment.class, TIMER_TAB_INDEX);
123
124        final Tab stopwatchTab = mTabLayout.newTab();
125        stopwatchTab.setIcon(R.drawable.ic_tab_stopwatch)
126                .setContentDescription(R.string.menu_stopwatch);
127        mTabsAdapter.addTab(stopwatchTab, StopwatchFragment.class, STOPWATCH_TAB_INDEX);
128
129        mTabLayout.getTabAt(mSelectedTab).select();
130        mViewPager.setCurrentItem(mSelectedTab);
131        mTabsAdapter.notifySelectedPage(mSelectedTab);
132    }
133
134    @Override
135    protected void onCreate(Bundle icicle) {
136        super.onCreate(icicle);
137        setVolumeControlStream(AudioManager.STREAM_ALARM);
138
139        if (icicle != null) {
140            mSelectedTab = icicle.getInt(KEY_SELECTED_TAB, CLOCK_TAB_INDEX);
141        } else {
142            mSelectedTab = CLOCK_TAB_INDEX;
143
144            // Set the background color to initially match the theme value so that we can
145            // smoothly transition to the dynamic color.
146            setBackgroundColor(getResources().getColor(R.color.default_background),
147                    false /* animate */);
148        }
149
150        // Honor the tab requested by the intent, if any.
151        final Intent intent = getIntent();
152        if (intent != null) {
153            int tab = intent.getIntExtra(SELECT_TAB_INTENT_EXTRA, -1);
154            if (tab != -1) {
155                mSelectedTab = tab;
156            }
157        }
158
159        setContentView(R.layout.desk_clock);
160        final Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
161        setSupportActionBar(toolbar);
162        mTabLayout = (TabLayout) findViewById(R.id.sliding_tabs);
163        mFab = (ImageView) findViewById(R.id.fab);
164        mLeftButton = (ImageButton) findViewById(R.id.left_button);
165        mRightButton = (ImageButton) findViewById(R.id.right_button);
166        if (mTabsAdapter == null) {
167            mViewPager = (RtlViewPager) findViewById(R.id.desk_clock_pager);
168            // Keep all four tabs to minimize jank.
169            mViewPager.setOffscreenPageLimit(3);
170            // Set Accessibility Delegate to null so ViewPager doesn't intercept movements and
171            // prevent the fab from being selected.
172            mViewPager.setAccessibilityDelegate(null);
173            mTabsAdapter = new TabsAdapter(this, mViewPager);
174            createTabs();
175            mTabLayout.setOnTabSelectedListener(new ViewPagerOnTabSelectedListener(mViewPager));
176        }
177
178        mFab.setOnClickListener(new OnClickListener() {
179            @Override
180            public void onClick(View view) {
181                getSelectedFragment().onFabClick(view);
182            }
183        });
184        mLeftButton.setOnClickListener(new OnClickListener() {
185            @Override
186            public void onClick(View view) {
187                getSelectedFragment().onLeftButtonClick(view);
188            }
189        });
190        mRightButton.setOnClickListener(new OnClickListener() {
191            @Override
192            public void onClick(View view) {
193                getSelectedFragment().onRightButtonClick(view);
194            }
195        });
196
197        // Configure the menu item controllers.
198        mActionBarMenuManager
199                .addMenuItemController(new SettingMenuItemController(this))
200                .addMenuItemController(new NightModeMenuItemController(this))
201                .addMenuItemController(MenuItemControllerFactory.getInstance()
202                        .buildMenuItemControllers(this));
203
204        // Inflate the menu during creation to avoid a double layout pass. Otherwise, the menu
205        // inflation occurs *after* the initial draw and a second layout pass adds in the menu.
206        onCreateOptionsMenu(toolbar.getMenu());
207
208        // We need to update the system next alarm time on app startup because the
209        // user might have clear our data.
210        AlarmStateManager.updateNextAlarm(this);
211    }
212
213    @Override
214    protected void onResume() {
215        super.onResume();
216        DataModel.getDataModel().setApplicationInForeground(true);
217    }
218
219    @Override
220    protected void onPostResume() {
221        super.onPostResume();
222
223        if (mRecreateActivity) {
224            mRecreateActivity = false;
225
226            // A runnable must be posted here or the new DeskClock activity will be recreated in a
227            // paused state, even though it is the foreground activity.
228            mViewPager.post(new Runnable() {
229                @Override
230                public void run() {
231                    recreate();
232                }
233            });
234        }
235    }
236
237    @Override
238    public void onPause() {
239        DataModel.getDataModel().setApplicationInForeground(false);
240        super.onPause();
241    }
242
243    @Override
244    protected void onSaveInstanceState(Bundle outState) {
245        super.onSaveInstanceState(outState);
246        outState.putInt(KEY_SELECTED_TAB, mTabLayout.getSelectedTabPosition());
247    }
248
249    @Override
250    public boolean onCreateOptionsMenu(Menu menu) {
251        mActionBarMenuManager.createOptionsMenu(menu, getMenuInflater());
252        return true;
253    }
254
255    @Override
256    public boolean onPrepareOptionsMenu(Menu menu) {
257        super.onPrepareOptionsMenu(menu);
258        mActionBarMenuManager.prepareShowMenu(menu);
259        return true;
260    }
261
262    @Override
263    public boolean onOptionsItemSelected(MenuItem item) {
264        if (mActionBarMenuManager.handleMenuItemClick(item)) {
265            return true;
266        }
267        return super.onOptionsItemSelected(item);
268    }
269
270    @Override
271    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
272        // Recreate the activity if any settings have been changed
273        if (requestCode == SettingMenuItemController.REQUEST_CHANGE_SETTINGS
274                && resultCode == RESULT_OK) {
275            mRecreateActivity = true;
276        }
277    }
278
279    public void registerPageChangedListener(DeskClockFragment frag) {
280        if (mTabsAdapter != null) {
281            mTabsAdapter.registerPageChangedListener(frag);
282        }
283    }
284
285    public void unregisterPageChangedListener(DeskClockFragment frag) {
286        if (mTabsAdapter != null) {
287            mTabsAdapter.unregisterPageChangedListener(frag);
288        }
289    }
290
291    /**
292     * Adapter for wrapping together the ActionBar's tab with the ViewPager
293     */
294    private class TabsAdapter extends FragmentPagerAdapter implements OnPageChangeListener {
295
296        private static final String KEY_TAB_POSITION = "tab_position";
297
298        final class TabInfo {
299            private final Class<?> clss;
300            private final Bundle args;
301
302            TabInfo(Class<?> _class, int position) {
303                clss = _class;
304                args = new Bundle();
305                args.putInt(KEY_TAB_POSITION, position);
306            }
307
308            public int getPosition() {
309                return args.getInt(KEY_TAB_POSITION, 0);
310            }
311        }
312
313        private final List<TabInfo> mTabs = new ArrayList<>(4 /* number of fragments */);
314        private final Context mContext;
315        private final RtlViewPager mPager;
316        // Used for doing callbacks to fragments.
317        private final Set<String> mFragmentTags = new ArraySet<>(4 /* number of fragments */);
318
319        public TabsAdapter(AppCompatActivity activity, RtlViewPager pager) {
320            super(activity.getFragmentManager());
321            mContext = activity;
322            mPager = pager;
323            mPager.setAdapter(this);
324            mPager.setOnRTLPageChangeListener(this);
325        }
326
327        @Override
328        public Object instantiateItem(ViewGroup container, int position) {
329            return super.instantiateItem(container, mViewPager.getRtlAwareIndex(position));
330        }
331
332        @Override
333        public Fragment getItem(int position) {
334            // Because this public method is called outside many times,
335            // check if it exits first before creating a new one.
336            final String name = makeFragmentName(R.id.desk_clock_pager, position);
337            Fragment fragment = getFragmentManager().findFragmentByTag(name);
338            if (fragment == null) {
339                TabInfo info = mTabs.get(position);
340                fragment = Fragment.instantiate(mContext, info.clss.getName(), info.args);
341                if (fragment instanceof TimerFragment) {
342                    ((TimerFragment) fragment).setFabAppearance();
343                    ((TimerFragment) fragment).setLeftRightButtonAppearance();
344                }
345            }
346            return fragment;
347        }
348
349        /**
350         * Copied from:
351         * android/frameworks/support/v13/java/android/support/v13/app/FragmentPagerAdapter.java#94
352         * Create unique name for the fragment so fragment manager knows it exist.
353         */
354        private String makeFragmentName(int viewId, int index) {
355            return "android:switcher:" + viewId + ":" + index;
356        }
357
358        @Override
359        public int getCount() {
360            return mTabs.size();
361        }
362
363        public void addTab(TabLayout.Tab tab, Class<?> clss, int position) {
364            TabInfo info = new TabInfo(clss, position);
365            mTabs.add(info);
366            mTabLayout.addTab(tab);
367            notifyDataSetChanged();
368        }
369
370        @Override
371        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
372            // Do nothing
373        }
374
375        @Override
376        public void onPageSelected(int position) {
377            // Set the page before doing the menu so that onCreateOptionsMenu knows what page it is.
378            mTabLayout.getTabAt(position).select();
379            notifyPageChanged(position);
380
381            mSelectedTab = position;
382
383            // Avoid sending events for the initial tab selection on launch and the reselecting a
384            // tab after a configuration change.
385            if (DataModel.getDataModel().isApplicationInForeground()) {
386                switch (mSelectedTab) {
387                    case ALARM_TAB_INDEX:
388                        Events.sendAlarmEvent(R.string.action_show, R.string.label_deskclock);
389                        break;
390                    case CLOCK_TAB_INDEX:
391                        Events.sendClockEvent(R.string.action_show, R.string.label_deskclock);
392                        break;
393                    case TIMER_TAB_INDEX:
394                        Events.sendTimerEvent(R.string.action_show, R.string.label_deskclock);
395                        break;
396                    case STOPWATCH_TAB_INDEX:
397                        Events.sendStopwatchEvent(R.string.action_show, R.string.label_deskclock);
398                        break;
399                }
400            }
401
402            final DeskClockFragment f = (DeskClockFragment) getItem(position);
403            if (f != null) {
404                f.setFabAppearance();
405                f.setLeftRightButtonAppearance();
406            }
407        }
408
409        @Override
410        public void onPageScrollStateChanged(int state) {
411            // Do nothing
412        }
413
414        public void notifySelectedPage(int page) {
415            notifyPageChanged(page);
416        }
417
418        private void notifyPageChanged(int newPage) {
419            for (String tag : mFragmentTags) {
420                final FragmentManager fm = getFragmentManager();
421                DeskClockFragment f = (DeskClockFragment) fm.findFragmentByTag(tag);
422                if (f != null) {
423                    f.onPageChanged(newPage);
424                }
425            }
426        }
427
428        public void registerPageChangedListener(DeskClockFragment frag) {
429            String tag = frag.getTag();
430            if (mFragmentTags.contains(tag)) {
431                LogUtils.wtf(TAG, "Trying to add an existing fragment " + tag);
432            } else {
433                mFragmentTags.add(frag.getTag());
434            }
435            // Since registering a listener by the fragment is done sometimes after the page
436            // was already changed, make sure the fragment gets the current page
437            frag.onPageChanged(mTabLayout.getSelectedTabPosition());
438        }
439
440        public void unregisterPageChangedListener(DeskClockFragment frag) {
441            mFragmentTags.remove(frag.getTag());
442        }
443    }
444
445    /**
446     * Called by the LabelDialogFormat class after the dialog is finished.
447     */
448    @Override
449    public void onDialogLabelSet(Alarm alarm, String label, String tag) {
450        Fragment frag = getFragmentManager().findFragmentByTag(tag);
451        if (frag instanceof AlarmClockFragment) {
452            ((AlarmClockFragment) frag).setLabel(alarm, label);
453        }
454    }
455
456    public int getSelectedTab() {
457        return mSelectedTab;
458    }
459
460    public ImageView getFab() {
461        return mFab;
462    }
463
464    public ImageButton getLeftButton() {
465        return mLeftButton;
466    }
467
468    public ImageButton getRightButton() {
469        return mRightButton;
470    }
471}
472