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    }
184
185    @Override
186    protected void onResume() {
187        super.onResume();
188
189        // We only want to show notifications for stopwatch/timer when the app is closed so
190        // that we don't have to worry about keeping the notifications in perfect sync with
191        // the app.
192        Intent stopwatchIntent = new Intent(getApplicationContext(), StopwatchService.class);
193        stopwatchIntent.setAction(Stopwatches.KILL_NOTIF);
194        startService(stopwatchIntent);
195
196        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
197        SharedPreferences.Editor editor = prefs.edit();
198        editor.putBoolean(Timers.NOTIF_APP_OPEN, true);
199        editor.apply();
200        Intent timerIntent = new Intent();
201        timerIntent.setAction(Timers.NOTIF_IN_USE_CANCEL);
202        sendBroadcast(timerIntent);
203    }
204
205    @Override
206    public void onPause() {
207        Intent intent = new Intent(getApplicationContext(), StopwatchService.class);
208        intent.setAction(Stopwatches.SHOW_NOTIF);
209        startService(intent);
210
211        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
212        SharedPreferences.Editor editor = prefs.edit();
213        editor.putBoolean(Timers.NOTIF_APP_OPEN, false);
214        editor.apply();
215        Utils.showInUseNotifications(this);
216
217        super.onPause();
218    }
219
220    @Override
221    protected void onSaveInstanceState(Bundle outState) {
222        super.onSaveInstanceState(outState);
223        outState.putInt(KEY_SELECTED_TAB, mActionBar.getSelectedNavigationIndex());
224    }
225
226    public void clockButtonsOnClick(View v) {
227        if (v == null) {
228            return;
229        }
230        switch (v.getId()) {
231            case R.id.cities_button:
232                startActivity(new Intent(this, CitiesActivity.class));
233                break;
234            default:
235                break;
236        }
237    }
238
239    @Override
240    public boolean onCreateOptionsMenu(Menu menu) {
241        // We only want to show it as a menu in landscape, and only for clock/alarm fragment.
242        mMenu = menu;
243        if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
244            if (mActionBar.getSelectedNavigationIndex() == ALARM_TAB_INDEX ||
245                    mActionBar.getSelectedNavigationIndex() == CLOCK_TAB_INDEX) {
246                // Clear the menu so that it doesn't get duplicate items in case onCreateOptionsMenu
247                // was called multiple times.
248                menu.clear();
249                getMenuInflater().inflate(R.menu.desk_clock_menu, menu);
250            }
251            // Always return true for landscape, regardless of whether we've inflated the menu, so
252            // that when we switch tabs this method will get called and we can inflate the menu.
253            return true;
254        }
255        return false;
256    }
257
258    @Override
259    public boolean onPrepareOptionsMenu(Menu menu) {
260        updateMenu(menu);
261        return true;
262    }
263
264    private void updateMenu(Menu menu) {
265        // Hide "help" if we don't have a URI for it.
266        MenuItem help = menu.findItem(R.id.menu_item_help);
267        if (help != null) {
268            Utils.prepareHelpMenuItem(this, help);
269        }
270
271        // Hide "lights out" for timer.
272        MenuItem nightMode = menu.findItem(R.id.menu_item_night_mode);
273        if (mActionBar.getSelectedNavigationIndex() == ALARM_TAB_INDEX) {
274            nightMode.setVisible(false);
275        } else if (mActionBar.getSelectedNavigationIndex() == CLOCK_TAB_INDEX) {
276            nightMode.setVisible(true);
277        }
278    }
279
280    @Override
281    public boolean onOptionsItemSelected(MenuItem item) {
282        if (processMenuClick(item)) {
283            return true;
284        }
285
286        return super.onOptionsItemSelected(item);
287    }
288
289    private boolean processMenuClick(MenuItem item) {
290        switch (item.getItemId()) {
291            case R.id.menu_item_settings:
292                startActivity(new Intent(DeskClock.this, SettingsActivity.class));
293                return true;
294            case R.id.menu_item_help:
295                Intent i = item.getIntent();
296                if (i != null) {
297                    try {
298                        startActivity(i);
299                    } catch (ActivityNotFoundException e) {
300                        // No activity found to match the intent - ignore
301                    }
302                }
303                return true;
304            case R.id.menu_item_night_mode:
305                startActivity(new Intent(DeskClock.this, ScreensaverActivity.class));
306            default:
307                break;
308        }
309        return true;
310    }
311
312    /***
313     * Insert the local time zone as the Home Time Zone if one is not set
314     */
315    private void setHomeTimeZone() {
316        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
317        String homeTimeZone = prefs.getString(SettingsActivity.KEY_HOME_TZ, "");
318        if (!homeTimeZone.isEmpty()) {
319            return;
320        }
321        homeTimeZone = TimeZone.getDefault().getID();
322        SharedPreferences.Editor editor = prefs.edit();
323        editor.putString(SettingsActivity.KEY_HOME_TZ, homeTimeZone);
324        editor.apply();
325        Log.v(LOG_TAG, "Setting home time zone to " + homeTimeZone);
326    }
327
328    public void registerPageChangedListener(DeskClockFragment frag) {
329        if (mTabsAdapter != null) {
330            mTabsAdapter.registerPageChangedListener(frag);
331        }
332    }
333
334    public void unregisterPageChangedListener(DeskClockFragment frag) {
335        if (mTabsAdapter != null) {
336            mTabsAdapter.unregisterPageChangedListener(frag);
337        }
338    }
339
340
341    /***
342     * Adapter for wrapping together the ActionBar's tab with the ViewPager
343     */
344
345    private class TabsAdapter extends FragmentPagerAdapter
346            implements ActionBar.TabListener, ViewPager.OnPageChangeListener {
347
348        private static final String KEY_TAB_POSITION = "tab_position";
349
350        final class TabInfo {
351            private final Class<?> clss;
352            private final Bundle args;
353
354            TabInfo(Class<?> _class, int position) {
355                clss = _class;
356                args = new Bundle();
357                args.putInt(KEY_TAB_POSITION, position);
358            }
359
360            public int getPosition() {
361                return args.getInt(KEY_TAB_POSITION, 0);
362            }
363        }
364
365        private final ArrayList<TabInfo> mTabs = new ArrayList <TabInfo>();
366        ActionBar mMainActionBar;
367        Context mContext;
368        ViewPager mPager;
369        // Used for doing callbacks to fragments.
370        HashSet<String> mFragmentTags = new HashSet<String>();
371
372        public TabsAdapter(Activity activity, ViewPager pager) {
373            super(activity.getFragmentManager());
374            mContext = activity;
375            mMainActionBar = activity.getActionBar();
376            mPager = pager;
377            mPager.setAdapter(this);
378            mPager.setOnPageChangeListener(this);
379        }
380
381        @Override
382        public Fragment getItem(int position) {
383            TabInfo info = mTabs.get(getRtlPosition(position));
384            DeskClockFragment f = (DeskClockFragment) Fragment.instantiate(
385                    mContext, info.clss.getName(), info.args);
386            return f;
387        }
388
389        @Override
390        public int getCount() {
391            return mTabs.size();
392        }
393
394        public void addTab(ActionBar.Tab tab, Class<?> clss, int position) {
395            TabInfo info = new TabInfo(clss, position);
396            tab.setTag(info);
397            tab.setTabListener(this);
398            mTabs.add(info);
399            mMainActionBar.addTab(tab);
400            notifyDataSetChanged();
401        }
402
403        @Override
404        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
405            // Do nothing
406        }
407
408        @Override
409        public void onPageSelected(int position) {
410            // Set the page before doing the menu so that onCreateOptionsMenu knows what page it is.
411            mMainActionBar.setSelectedNavigationItem(getRtlPosition(position));
412            notifyPageChanged(position);
413
414            // Only show the overflow menu for alarm and world clock.
415            if (mMenu != null) {
416                // Make sure the menu's been initialized.
417                if (position == ALARM_TAB_INDEX || position == CLOCK_TAB_INDEX) {
418                    mMenu.setGroupVisible(R.id.menu_items, true);
419                    onCreateOptionsMenu(mMenu);
420                } else {
421                    mMenu.setGroupVisible(R.id.menu_items, false);
422                }
423            }
424        }
425
426        @Override
427        public void onPageScrollStateChanged(int state) {
428            // Do nothing
429        }
430
431        @Override
432        public void onTabReselected(Tab arg0, FragmentTransaction arg1) {
433            // Do nothing
434        }
435
436        @Override
437        public void onTabSelected(Tab tab, FragmentTransaction ft) {
438            TabInfo info = (TabInfo)tab.getTag();
439            int position = info.getPosition();
440            mPager.setCurrentItem(getRtlPosition(position));
441        }
442
443        @Override
444        public void onTabUnselected(Tab arg0, FragmentTransaction arg1) {
445            // Do nothing
446        }
447
448        public void notifySelectedPage(int page) {
449            notifyPageChanged(page);
450        }
451
452        private void notifyPageChanged(int newPage) {
453            for (String tag : mFragmentTags) {
454                final FragmentManager fm = getFragmentManager();
455                DeskClockFragment f = (DeskClockFragment) fm.findFragmentByTag(tag);
456                if (f != null) {
457                    f.onPageChanged(newPage);
458                }
459            }
460        }
461
462        public void registerPageChangedListener(DeskClockFragment frag) {
463            String tag = frag.getTag();
464            if (mFragmentTags.contains(tag)) {
465                Log.wtf(LOG_TAG, "Trying to add an existing fragment " + tag);
466            } else {
467                mFragmentTags.add(frag.getTag());
468            }
469            // Since registering a listener by the fragment is done sometimes after the page
470            // was already changed, make sure the fragment gets the current page
471            frag.onPageChanged(mMainActionBar.getSelectedNavigationIndex());
472        }
473
474        public void unregisterPageChangedListener(DeskClockFragment frag) {
475            mFragmentTags.remove(frag.getTag());
476        }
477
478        private boolean isRtl() {
479            return TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()) ==
480                    View.LAYOUT_DIRECTION_RTL;
481        }
482
483        private int getRtlPosition(int position) {
484            if (isRtl()) {
485                switch (position) {
486                    case TIMER_TAB_INDEX:
487                        return RTL_TIMER_TAB_INDEX;
488                    case CLOCK_TAB_INDEX:
489                        return RTL_CLOCK_TAB_INDEX;
490                    case STOPWATCH_TAB_INDEX:
491                        return RTL_STOPWATCH_TAB_INDEX;
492                    case ALARM_TAB_INDEX:
493                        return RTL_ALARM_TAB_INDEX;
494                    default:
495                        break;
496                }
497            }
498            return position;
499        }
500    }
501
502    public static abstract class OnTapListener implements OnTouchListener {
503        private float mLastTouchX;
504        private float mLastTouchY;
505        private long mLastTouchTime;
506        private final TextView mMakePressedTextView;
507        private final int mPressedColor, mGrayColor;
508        private final float MAX_MOVEMENT_ALLOWED = 20;
509        private final long MAX_TIME_ALLOWED = 500;
510
511        public OnTapListener(Activity activity, TextView makePressedView) {
512            mMakePressedTextView = makePressedView;
513            mPressedColor = activity.getResources().getColor(Utils.getPressedColorId());
514            mGrayColor = activity.getResources().getColor(Utils.getGrayColorId());
515        }
516
517        @Override
518        public boolean onTouch(View v, MotionEvent e) {
519            switch (e.getAction()) {
520                case (MotionEvent.ACTION_DOWN):
521                    mLastTouchTime = Utils.getTimeNow();
522                    mLastTouchX = e.getX();
523                    mLastTouchY = e.getY();
524                    if (mMakePressedTextView != null) {
525                        mMakePressedTextView.setTextColor(mPressedColor);
526                    }
527                    break;
528                case (MotionEvent.ACTION_UP):
529                    float xDiff = Math.abs(e.getX()-mLastTouchX);
530                    float yDiff = Math.abs(e.getY()-mLastTouchY);
531                    long timeDiff = (Utils.getTimeNow() - mLastTouchTime);
532                    if (xDiff < MAX_MOVEMENT_ALLOWED && yDiff < MAX_MOVEMENT_ALLOWED
533                            && timeDiff < MAX_TIME_ALLOWED) {
534                        if (mMakePressedTextView != null) {
535                            v = mMakePressedTextView;
536                        }
537                        processClick(v);
538                        resetValues();
539                        return true;
540                    }
541                    resetValues();
542                    break;
543                case (MotionEvent.ACTION_MOVE):
544                    xDiff = Math.abs(e.getX()-mLastTouchX);
545                    yDiff = Math.abs(e.getY()-mLastTouchY);
546                    if (xDiff >= MAX_MOVEMENT_ALLOWED || yDiff >= MAX_MOVEMENT_ALLOWED) {
547                        resetValues();
548                    }
549                    break;
550                default:
551                    resetValues();
552            }
553            return false;
554        }
555
556        private void resetValues() {
557            mLastTouchX = -1*MAX_MOVEMENT_ALLOWED + 1;
558            mLastTouchY = -1*MAX_MOVEMENT_ALLOWED + 1;
559            mLastTouchTime = -1*MAX_TIME_ALLOWED + 1;
560            if (mMakePressedTextView != null) {
561                mMakePressedTextView.setTextColor(mGrayColor);
562            }
563        }
564
565        protected abstract void processClick(View v);
566    }
567
568    /** Called by the LabelDialogFormat class after the dialog is finished. **/
569    @Override
570    public void onDialogLabelSet(TimerObj timer, String label, String tag) {
571        Fragment frag = getFragmentManager().findFragmentByTag(tag);
572        if (frag instanceof TimerFragment) {
573            ((TimerFragment) frag).setLabel(timer, label);
574        }
575    }
576
577    /** Called by the LabelDialogFormat class after the dialog is finished. **/
578    @Override
579    public void onDialogLabelSet(Alarm alarm, String label, String tag) {
580        Fragment frag = getFragmentManager().findFragmentByTag(tag);
581        if (frag instanceof AlarmClockFragment) {
582            ((AlarmClockFragment) frag).setLabel(alarm, label);
583        }
584    }
585}
586