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 
17 package com.android.deskclock;
18 
19 import android.app.Fragment;
20 import android.app.FragmentManager;
21 import android.content.Context;
22 import android.content.Intent;
23 import android.media.AudioManager;
24 import android.os.Bundle;
25 import android.support.annotation.VisibleForTesting;
26 import android.support.design.widget.TabLayout;
27 import android.support.design.widget.TabLayout.Tab;
28 import android.support.design.widget.TabLayout.ViewPagerOnTabSelectedListener;
29 import android.support.v13.app.FragmentPagerAdapter;
30 import android.support.v4.view.ViewPager.OnPageChangeListener;
31 import android.support.v7.app.AppCompatActivity;
32 import android.support.v7.widget.Toolbar;
33 import android.util.ArraySet;
34 import android.view.Menu;
35 import android.view.MenuItem;
36 import android.view.View;
37 import android.view.View.OnClickListener;
38 import android.view.ViewGroup;
39 import android.widget.ImageButton;
40 import android.widget.ImageView;
41 
42 import com.android.deskclock.actionbarmenu.ActionBarMenuManager;
43 import com.android.deskclock.actionbarmenu.MenuItemControllerFactory;
44 import com.android.deskclock.actionbarmenu.NightModeMenuItemController;
45 import com.android.deskclock.actionbarmenu.SettingMenuItemController;
46 import com.android.deskclock.alarms.AlarmStateManager;
47 import com.android.deskclock.data.DataModel;
48 import com.android.deskclock.events.Events;
49 import com.android.deskclock.provider.Alarm;
50 import com.android.deskclock.stopwatch.StopwatchFragment;
51 import com.android.deskclock.timer.TimerFragment;
52 import com.android.deskclock.widget.RtlViewPager;
53 
54 import java.util.ArrayList;
55 import java.util.List;
56 import java.util.Set;
57 
58 /**
59  * DeskClock clock view for desk docks.
60  */
61 public 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
onNewIntent(Intent newIntent)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
getSelectedFragment()107     DeskClockFragment getSelectedFragment() {
108         return (DeskClockFragment) mTabsAdapter.getItem(mSelectedTab);
109     }
110 
createTabs()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
onCreate(Bundle icicle)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
onResume()214     protected void onResume() {
215         super.onResume();
216         DataModel.getDataModel().setApplicationInForeground(true);
217     }
218 
219     @Override
onPostResume()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
onPause()238     public void onPause() {
239         DataModel.getDataModel().setApplicationInForeground(false);
240         super.onPause();
241     }
242 
243     @Override
onSaveInstanceState(Bundle outState)244     protected void onSaveInstanceState(Bundle outState) {
245         super.onSaveInstanceState(outState);
246         outState.putInt(KEY_SELECTED_TAB, mTabLayout.getSelectedTabPosition());
247     }
248 
249     @Override
onCreateOptionsMenu(Menu menu)250     public boolean onCreateOptionsMenu(Menu menu) {
251         mActionBarMenuManager.createOptionsMenu(menu, getMenuInflater());
252         return true;
253     }
254 
255     @Override
onPrepareOptionsMenu(Menu menu)256     public boolean onPrepareOptionsMenu(Menu menu) {
257         super.onPrepareOptionsMenu(menu);
258         mActionBarMenuManager.prepareShowMenu(menu);
259         return true;
260     }
261 
262     @Override
onOptionsItemSelected(MenuItem item)263     public boolean onOptionsItemSelected(MenuItem item) {
264         if (mActionBarMenuManager.handleMenuItemClick(item)) {
265             return true;
266         }
267         return super.onOptionsItemSelected(item);
268     }
269 
270     @Override
onActivityResult(int requestCode, int resultCode, Intent data)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 
registerPageChangedListener(DeskClockFragment frag)279     public void registerPageChangedListener(DeskClockFragment frag) {
280         if (mTabsAdapter != null) {
281             mTabsAdapter.registerPageChangedListener(frag);
282         }
283     }
284 
unregisterPageChangedListener(DeskClockFragment frag)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 
TabInfo(Class<?> _class, int position)302             TabInfo(Class<?> _class, int position) {
303                 clss = _class;
304                 args = new Bundle();
305                 args.putInt(KEY_TAB_POSITION, position);
306             }
307 
getPosition()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 
TabsAdapter(AppCompatActivity activity, RtlViewPager pager)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
instantiateItem(ViewGroup container, int position)328         public Object instantiateItem(ViewGroup container, int position) {
329             return super.instantiateItem(container, mViewPager.getRtlAwareIndex(position));
330         }
331 
332         @Override
getItem(int position)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          */
makeFragmentName(int viewId, int index)354         private String makeFragmentName(int viewId, int index) {
355             return "android:switcher:" + viewId + ":" + index;
356         }
357 
358         @Override
getCount()359         public int getCount() {
360             return mTabs.size();
361         }
362 
addTab(TabLayout.Tab tab, Class<?> clss, int position)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
onPageScrolled(int position, float positionOffset, int positionOffsetPixels)371         public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
372             // Do nothing
373         }
374 
375         @Override
onPageSelected(int position)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
onPageScrollStateChanged(int state)410         public void onPageScrollStateChanged(int state) {
411             // Do nothing
412         }
413 
notifySelectedPage(int page)414         public void notifySelectedPage(int page) {
415             notifyPageChanged(page);
416         }
417 
notifyPageChanged(int newPage)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 
registerPageChangedListener(DeskClockFragment frag)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 
unregisterPageChangedListener(DeskClockFragment frag)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
onDialogLabelSet(Alarm alarm, String label, String tag)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 
getSelectedTab()456     public int getSelectedTab() {
457         return mSelectedTab;
458     }
459 
getFab()460     public ImageView getFab() {
461         return mFab;
462     }
463 
getLeftButton()464     public ImageButton getLeftButton() {
465         return mLeftButton;
466     }
467 
getRightButton()468     public ImageButton getRightButton() {
469         return mRightButton;
470     }
471 }
472