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.ActionBar; 20 import android.app.ActionBar.Tab; 21 import android.app.Activity; 22 import android.app.Fragment; 23 import android.app.FragmentManager; 24 import android.app.FragmentTransaction; 25 import android.content.ActivityNotFoundException; 26 import android.content.Context; 27 import android.content.Intent; 28 import android.content.SharedPreferences; 29 import android.content.res.Configuration; 30 import android.os.Bundle; 31 import android.preference.PreferenceManager; 32 import android.support.v13.app.FragmentPagerAdapter; 33 import android.support.v4.view.ViewPager; 34 import android.text.TextUtils; 35 import android.util.Log; 36 import android.view.Menu; 37 import android.view.MenuItem; 38 import android.view.MotionEvent; 39 import android.view.View; 40 import android.view.View.OnTouchListener; 41 import android.widget.PopupMenu; 42 import android.widget.TextView; 43 44 import com.android.deskclock.alarms.AlarmStateManager; 45 import com.android.deskclock.provider.Alarm; 46 import com.android.deskclock.stopwatch.StopwatchFragment; 47 import com.android.deskclock.stopwatch.StopwatchService; 48 import com.android.deskclock.stopwatch.Stopwatches; 49 import com.android.deskclock.timer.TimerFragment; 50 import com.android.deskclock.timer.TimerObj; 51 import com.android.deskclock.timer.Timers; 52 import com.android.deskclock.worldclock.CitiesActivity; 53 54 import java.util.ArrayList; 55 import java.util.HashSet; 56 import java.util.Locale; 57 import java.util.TimeZone; 58 59 /** 60 * DeskClock clock view for desk docks. 61 */ 62 public 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