ContactDetailActivity.java revision 8167b14342c4a1e1d1f336470aa3a9be45cb38d3
1/* 2 * Copyright (C) 2010 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.contacts.activities; 18 19import com.android.contacts.ContactLoader; 20import com.android.contacts.ContactSaveService; 21import com.android.contacts.ContactsActivity; 22import com.android.contacts.R; 23import com.android.contacts.detail.ContactDetailDisplayUtils; 24import com.android.contacts.detail.ContactDetailFragment; 25import com.android.contacts.detail.ContactDetailFragmentCarousel; 26import com.android.contacts.detail.ContactDetailTabCarousel; 27import com.android.contacts.detail.ContactDetailUpdatesFragment; 28import com.android.contacts.detail.ContactDetailViewPagerAdapter; 29import com.android.contacts.detail.ContactLoaderFragment; 30import com.android.contacts.detail.ContactLoaderFragment.ContactLoaderFragmentListener; 31import com.android.contacts.detail.TabCarouselScrollManager; 32import com.android.contacts.interactions.ContactDeletionInteraction; 33import com.android.contacts.model.AccountWithDataSet; 34import com.android.contacts.util.PhoneCapabilityTester; 35 36import android.app.ActionBar; 37import android.app.Fragment; 38import android.app.FragmentManager; 39import android.app.FragmentTransaction; 40import android.content.ActivityNotFoundException; 41import android.content.ContentValues; 42import android.content.Context; 43import android.content.Intent; 44import android.net.Uri; 45import android.os.Bundle; 46import android.os.Handler; 47import android.support.v13.app.FragmentPagerAdapter; 48import android.support.v4.view.ViewPager; 49import android.support.v4.view.ViewPager.OnPageChangeListener; 50import android.util.Log; 51import android.view.KeyEvent; 52import android.view.LayoutInflater; 53import android.view.Menu; 54import android.view.MenuInflater; 55import android.view.MenuItem; 56import android.view.View; 57import android.view.View.OnClickListener; 58import android.view.ViewGroup; 59import android.widget.CheckBox; 60import android.widget.Toast; 61 62import java.util.ArrayList; 63 64// TODO: Use {@link ContactDetailLayoutController} so there isn't duplicated code 65public class ContactDetailActivity extends ContactsActivity { 66 private static final String TAG = "ContactDetailActivity"; 67 68 /** 69 * Intent key for a boolean that specifies whether the "up" afforance in this activity should 70 * behave as default (return user back to {@link PeopleActivity}) or whether the activity should 71 * instead be finished. 72 */ 73 public static final String INTENT_KEY_IGNORE_DEFAULT_UP_BEHAVIOR = "ignoreDefaultUpBehavior"; 74 75 private static final String KEY_CONTACT_HAS_UPDATES = "contactHasUpdates"; 76 private static final String KEY_CURRENT_PAGE_INDEX = "currentPageIndex"; 77 78 public static final int FRAGMENT_COUNT = 2; 79 80 private ContactLoader.Result mContactData; 81 private Uri mLookupUri; 82 private boolean mIgnoreDefaultUpBehavior; 83 84 private ContactLoaderFragment mLoaderFragment; 85 private ContactDetailFragment mDetailFragment; 86 private ContactDetailUpdatesFragment mUpdatesFragment; 87 88 private ContactDetailTabCarousel mTabCarousel; 89 private ViewPager mViewPager; 90 91 private ContactDetailFragmentCarousel mFragmentCarousel; 92 93 private boolean mFragmentsAddedToFragmentManager; 94 95 private ViewGroup mRootView; 96 private ViewGroup mContentView; 97 private LayoutInflater mInflater; 98 99 private Handler mHandler = new Handler(); 100 101 /** 102 * Whether or not the contact has updates, which dictates whether the 103 * {@link ContactDetailUpdatesFragment} will be shown. 104 */ 105 private boolean mContactHasUpdates; 106 107 @Override 108 public void onCreate(Bundle savedState) { 109 super.onCreate(savedState); 110 if (PhoneCapabilityTester.isUsingTwoPanes(this)) { 111 // This activity must not be shown. We have to select the contact in the 112 // PeopleActivity instead ==> Create a forward intent and finish 113 final Intent originalIntent = getIntent(); 114 Intent intent = new Intent(); 115 intent.setAction(originalIntent.getAction()); 116 intent.setDataAndType(originalIntent.getData(), originalIntent.getType()); 117 intent.setFlags( 118 Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS | Intent.FLAG_ACTIVITY_FORWARD_RESULT 119 | Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP); 120 121 intent.setClass(this, PeopleActivity.class); 122 startActivity(intent); 123 finish(); 124 return; 125 } 126 127 mIgnoreDefaultUpBehavior = getIntent().getBooleanExtra( 128 INTENT_KEY_IGNORE_DEFAULT_UP_BEHAVIOR, false); 129 130 setContentView(R.layout.contact_detail_activity); 131 mRootView = (ViewGroup) findViewById(R.id.contact_detail_view); 132 mInflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE); 133 134 FragmentManager fragmentManager = getFragmentManager(); 135 mDetailFragment = (ContactDetailFragment) 136 fragmentManager.findFragmentByTag( 137 ContactDetailViewPagerAdapter.ABOUT_FRAGMENT_TAG); 138 mUpdatesFragment = (ContactDetailUpdatesFragment) 139 fragmentManager.findFragmentByTag( 140 ContactDetailViewPagerAdapter.UPDTES_FRAGMENT_TAG); 141 142 // If the fragment were found in the {@link FragmentManager} then we don't need to add 143 // it again. 144 if (mDetailFragment != null) { 145 mFragmentsAddedToFragmentManager = true; 146 } else { 147 // Otherwise, create the fragments dynamically and remember to add them to the 148 // {@link FragmentManager}. 149 mDetailFragment = new ContactDetailFragment(); 150 mUpdatesFragment = new ContactDetailUpdatesFragment(); 151 mFragmentsAddedToFragmentManager = false; 152 } 153 154 mDetailFragment.setListener(mFragmentListener); 155 TabCarouselScrollManager.bind(mTabCarousel, mDetailFragment, mUpdatesFragment); 156 mDetailFragment.setData(mLookupUri, mContactData); 157 mUpdatesFragment.setData(mLookupUri, mContactData); 158 159 if (savedState != null) { 160 mContactHasUpdates = savedState.getBoolean(KEY_CONTACT_HAS_UPDATES); 161 if (mContactHasUpdates) { 162 setupContactWithUpdates(savedState.getInt(KEY_CURRENT_PAGE_INDEX, 0)); 163 } else { 164 setupContactWithoutUpdates(); 165 } 166 } 167 168 // We want the UP affordance but no app icon. 169 // Setting HOME_AS_UP, SHOW_TITLE and clearing SHOW_HOME does the trick. 170 ActionBar actionBar = getActionBar(); 171 if (actionBar != null) { 172 actionBar.setDisplayOptions(ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_TITLE, 173 ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_TITLE 174 | ActionBar.DISPLAY_SHOW_HOME); 175 actionBar.setTitle(""); 176 } 177 178 Log.i(TAG, getIntent().getData().toString()); 179 } 180 181 @Override 182 public void onAttachFragment(Fragment fragment) { 183 if (fragment instanceof ContactLoaderFragment) { 184 mLoaderFragment = (ContactLoaderFragment) fragment; 185 mLoaderFragment.setRetainInstance(true); 186 mLoaderFragment.setListener(mLoaderFragmentListener); 187 mLoaderFragment.loadUri(getIntent().getData()); 188 } 189 } 190 191 @Override 192 public boolean onSearchRequested() { 193 return true; // Don't respond to the search key. 194 } 195 196 @Override 197 public boolean onCreateOptionsMenu(Menu menu) { 198 super.onCreateOptionsMenu(menu); 199 MenuInflater inflater = getMenuInflater(); 200 inflater.inflate(R.menu.star, menu); 201 return true; 202 } 203 204 @Override 205 public boolean onPrepareOptionsMenu(Menu menu) { 206 MenuItem starredMenuItem = menu.findItem(R.id.menu_star); 207 ViewGroup starredContainer = (ViewGroup) getLayoutInflater().inflate( 208 R.layout.favorites_star, null, false); 209 final CheckBox starredView = (CheckBox) starredContainer.findViewById(R.id.star); 210 starredView.setOnClickListener(new OnClickListener() { 211 @Override 212 public void onClick(View v) { 213 // Toggle "starred" state 214 // Make sure there is a contact 215 if (mLookupUri != null) { 216 Intent intent = ContactSaveService.createSetStarredIntent( 217 ContactDetailActivity.this, mLookupUri, starredView.isChecked()); 218 ContactDetailActivity.this.startService(intent); 219 } 220 } 221 }); 222 // If there is contact data, update the starred state 223 if (mContactData != null) { 224 ContactDetailDisplayUtils.setStarred(mContactData, starredView); 225 } 226 starredMenuItem.setActionView(starredContainer); 227 return true; 228 } 229 230 @Override 231 public boolean onKeyDown(int keyCode, KeyEvent event) { 232 // First check if the {@link ContactLoaderFragment} can handle the key 233 if (mLoaderFragment != null && mLoaderFragment.handleKeyDown(keyCode)) return true; 234 235 // Otherwise find the correct fragment to handle the event 236 FragmentKeyListener mCurrentFragment; 237 switch (getCurrentPage()) { 238 case 0: 239 mCurrentFragment = mDetailFragment; 240 break; 241 case 1: 242 mCurrentFragment = mUpdatesFragment; 243 break; 244 default: 245 throw new IllegalStateException("Invalid current item for ViewPager"); 246 } 247 if (mCurrentFragment != null && mCurrentFragment.handleKeyDown(keyCode)) return true; 248 249 // In the last case, give the key event to the superclass. 250 return super.onKeyDown(keyCode, event); 251 } 252 253 private int getCurrentPage() { 254 // If the contact doesn't have any social updates, there is only 1 page (detail fragment). 255 if (!mContactHasUpdates) { 256 return 0; 257 } 258 // Otherwise find the current page based on the {@link ViewPager} or fragment carousel. 259 if (mViewPager != null) { 260 return mViewPager.getCurrentItem(); 261 } else if (mFragmentCarousel != null) { 262 return mFragmentCarousel.getCurrentPage(); 263 } 264 throw new IllegalStateException("Can't figure out the currently selected page. If the " + 265 "contact has social updates, there must be a ViewPager or fragment carousel"); 266 } 267 268 @Override 269 protected void onSaveInstanceState(Bundle outState) { 270 super.onSaveInstanceState(outState); 271 outState.putBoolean(KEY_CONTACT_HAS_UPDATES, mContactHasUpdates); 272 outState.putInt(KEY_CURRENT_PAGE_INDEX, getCurrentPage()); 273 } 274 275 private final ContactLoaderFragmentListener mLoaderFragmentListener = 276 new ContactLoaderFragmentListener() { 277 @Override 278 public void onContactNotFound() { 279 finish(); 280 } 281 282 @Override 283 public void onDetailsLoaded(final ContactLoader.Result result) { 284 if (result == null) { 285 return; 286 } 287 // Since {@link FragmentTransaction}s cannot be done in the onLoadFinished() of the 288 // {@link LoaderCallbacks}, then post this {@link Runnable} to the {@link Handler} 289 // on the main thread to execute later. 290 mHandler.post(new Runnable() { 291 @Override 292 public void run() { 293 // If the activity is destroyed (or will be destroyed soon), don't update the UI 294 if (isFinishing()) { 295 return; 296 } 297 mContactData = result; 298 mLookupUri = result.getLookupUri(); 299 mContactHasUpdates = !result.getStreamItems().isEmpty(); 300 invalidateOptionsMenu(); 301 setupTitle(); 302 if (mContactHasUpdates) { 303 setupContactWithUpdates(null /* Don't change the current page */); 304 } else { 305 setupContactWithoutUpdates(); 306 } 307 } 308 }); 309 } 310 311 @Override 312 public void onEditRequested(Uri contactLookupUri) { 313 startActivity(new Intent(Intent.ACTION_EDIT, contactLookupUri)); 314 finish(); 315 } 316 317 @Override 318 public void onDeleteRequested(Uri contactUri) { 319 ContactDeletionInteraction.start(ContactDetailActivity.this, contactUri, true); 320 } 321 }; 322 323 /** 324 * Setup the activity title and subtitle with contact name and company. 325 */ 326 private void setupTitle() { 327 CharSequence displayName = ContactDetailDisplayUtils.getDisplayName(this, mContactData); 328 String company = ContactDetailDisplayUtils.getCompany(this, mContactData); 329 330 ActionBar actionBar = getActionBar(); 331 actionBar.setTitle(displayName); 332 actionBar.setSubtitle(company); 333 } 334 335 /** 336 * Setup the layout for the contact with updates. Pass in the index of the current page to 337 * select or null if the current selection should be left as is. 338 */ 339 private void setupContactWithUpdates(Integer currentPageIndex) { 340 if (mContentView == null) { 341 mContentView = (ViewGroup) mInflater.inflate( 342 R.layout.contact_detail_container_with_updates, mRootView, false); 343 mRootView.addView(mContentView); 344 345 // Make sure all needed views are retrieved. Note that narrow width screens have a 346 // {@link ViewPager} and {@link ContactDetailTabCarousel}, while wide width screens have 347 // a {@link ContactDetailFragmentCarousel}. 348 mTabCarousel = (ContactDetailTabCarousel) findViewById(R.id.tab_carousel); 349 if (mTabCarousel != null) { 350 mTabCarousel.setListener(mTabCarouselListener); 351 } 352 353 mViewPager = (ViewPager) findViewById(R.id.pager); 354 if (mViewPager != null) { 355 // Inflate 2 view containers to pass in as children to the {@link ViewPager}, 356 // which will in turn be the parents to the mDetailFragment and mUpdatesFragment 357 // since the fragments must have the same parent view IDs in both landscape and 358 // portrait layouts. 359 ViewGroup detailContainer = (ViewGroup) mInflater.inflate( 360 R.layout.contact_detail_about_fragment_container, mViewPager, false); 361 ViewGroup updatesContainer = (ViewGroup) mInflater.inflate( 362 R.layout.contact_detail_updates_fragment_container, mViewPager, false); 363 364 ContactDetailViewPagerAdapter adapter = new ContactDetailViewPagerAdapter(); 365 adapter.setAboutFragmentView(detailContainer); 366 adapter.setUpdatesFragmentView(updatesContainer); 367 368 mViewPager.addView(detailContainer); 369 mViewPager.addView(updatesContainer); 370 mViewPager.setAdapter(adapter); 371 mViewPager.setOnPageChangeListener(mOnPageChangeListener); 372 373 if (!mFragmentsAddedToFragmentManager) { 374 FragmentManager fragmentManager = getFragmentManager(); 375 FragmentTransaction transaction = fragmentManager.beginTransaction(); 376 transaction.add(R.id.about_fragment_container, mDetailFragment, 377 ContactDetailViewPagerAdapter.ABOUT_FRAGMENT_TAG); 378 transaction.add(R.id.updates_fragment_container, mUpdatesFragment, 379 ContactDetailViewPagerAdapter.UPDTES_FRAGMENT_TAG); 380 transaction.commit(); 381 fragmentManager.executePendingTransactions(); 382 } 383 384 // Select page if applicable 385 if (currentPageIndex != null) { 386 mViewPager.setCurrentItem(currentPageIndex); 387 } 388 } 389 390 mFragmentCarousel = (ContactDetailFragmentCarousel) 391 findViewById(R.id.fragment_carousel); 392 // Add the fragments to the fragment containers in the carousel using a 393 // {@link FragmentTransaction} if they haven't already been added to the 394 // {@link FragmentManager}. 395 if (mFragmentCarousel != null) { 396 if (!mFragmentsAddedToFragmentManager) { 397 FragmentManager fragmentManager = getFragmentManager(); 398 FragmentTransaction transaction = fragmentManager.beginTransaction(); 399 transaction.add(R.id.about_fragment_container, mDetailFragment, 400 ContactDetailViewPagerAdapter.ABOUT_FRAGMENT_TAG); 401 transaction.add(R.id.updates_fragment_container, mUpdatesFragment, 402 ContactDetailViewPagerAdapter.UPDTES_FRAGMENT_TAG); 403 transaction.commit(); 404 fragmentManager.executePendingTransactions(); 405 } 406 407 // Select page if applicable 408 if (currentPageIndex != null) { 409 mFragmentCarousel.setCurrentPage(currentPageIndex); 410 } 411 } 412 } 413 414 // Then reset the contact data to the appropriate views 415 if (mTabCarousel != null) { 416 mTabCarousel.loadData(mContactData); 417 } 418 if (mFragmentCarousel != null && mDetailFragment != null && mUpdatesFragment != null) { 419 mFragmentCarousel.setFragments(mDetailFragment, mUpdatesFragment); 420 } 421 if (mDetailFragment != null) { 422 mDetailFragment.setData(mLookupUri, mContactData); 423 } 424 if (mUpdatesFragment != null) { 425 mUpdatesFragment.setData(mLookupUri, mContactData); 426 } 427 } 428 429 private void setupContactWithoutUpdates() { 430 if (mContentView == null) { 431 mContentView = (ViewGroup) mInflater.inflate( 432 R.layout.contact_detail_container_without_updates, mRootView, false); 433 mRootView.addView(mContentView); 434 mDetailFragment = (ContactDetailFragment) getFragmentManager().findFragmentById( 435 R.id.contact_detail_fragment); 436 mDetailFragment.setListener(mFragmentListener); 437 } 438 // Reset contact data 439 if (mDetailFragment != null) { 440 mDetailFragment.setData(mLookupUri, mContactData); 441 } 442 } 443 444 private final ContactDetailFragment.Listener mFragmentListener = 445 new ContactDetailFragment.Listener() { 446 @Override 447 public void onItemClicked(Intent intent) { 448 try { 449 startActivity(intent); 450 } catch (ActivityNotFoundException e) { 451 Log.e(TAG, "No activity found for intent: " + intent); 452 } 453 } 454 455 @Override 456 public void onCreateRawContactRequested( 457 ArrayList<ContentValues> values, AccountWithDataSet account) { 458 Toast.makeText(ContactDetailActivity.this, R.string.toast_making_personal_copy, 459 Toast.LENGTH_LONG).show(); 460 Intent serviceIntent = ContactSaveService.createNewRawContactIntent( 461 ContactDetailActivity.this, values, account, 462 ContactDetailActivity.class, Intent.ACTION_VIEW); 463 startService(serviceIntent); 464 465 } 466 }; 467 468 public class ViewPagerAdapter extends FragmentPagerAdapter{ 469 470 public ViewPagerAdapter(FragmentManager fm) { 471 super(fm); 472 } 473 474 @Override 475 public Fragment getItem(int position) { 476 switch (position) { 477 case 0: 478 return new ContactDetailFragment(); 479 case 1: 480 return new ContactDetailUpdatesFragment(); 481 } 482 throw new IllegalStateException("No fragment at position " + position); 483 } 484 485 @Override 486 public int getCount() { 487 return FRAGMENT_COUNT; 488 } 489 } 490 491 private OnPageChangeListener mOnPageChangeListener = new OnPageChangeListener() { 492 493 @Override 494 public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { 495 // The user is horizontally dragging the {@link ViewPager}, so send 496 // these scroll changes to the tab carousel. Ignore these events though if the carousel 497 // is actually controlling the {@link ViewPager} scrolls because it will already be 498 // in the correct position. 499 if (mViewPager.isFakeDragging()) { 500 return; 501 } 502 int x = (int) ((position + positionOffset) * 503 mTabCarousel.getAllowedHorizontalScrollLength()); 504 mTabCarousel.scrollTo(x, 0); 505 } 506 507 @Override 508 public void onPageSelected(int position) { 509 // Since a new page has been selected by the {@link ViewPager}, 510 // update the tab selection in the carousel. 511 mTabCarousel.setCurrentTab(position); 512 } 513 514 @Override 515 public void onPageScrollStateChanged(int state) {} 516 517 }; 518 519 private ContactDetailTabCarousel.Listener mTabCarouselListener = 520 new ContactDetailTabCarousel.Listener() { 521 522 @Override 523 public void onTouchDown() { 524 // The user just started scrolling the carousel, so begin "fake dragging" the 525 // {@link ViewPager} if it's not already doing so. 526 if (mViewPager.isFakeDragging()) { 527 return; 528 } 529 mViewPager.beginFakeDrag(); 530 } 531 532 @Override 533 public void onTouchUp() { 534 // The user just stopped scrolling the carousel, so stop "fake dragging" the 535 // {@link ViewPager} if was doing so before. 536 if (mViewPager.isFakeDragging()) { 537 mViewPager.endFakeDrag(); 538 } 539 } 540 541 @Override 542 public void onScrollChanged(int l, int t, int oldl, int oldt) { 543 // The user is scrolling the carousel, so send the scroll deltas to the 544 // {@link ViewPager} so it can move in sync. 545 if (mViewPager.isFakeDragging()) { 546 mViewPager.fakeDragBy(oldl-l); 547 } 548 } 549 550 @Override 551 public void onTabSelected(int position) { 552 // The user selected a tab, so update the {@link ViewPager} 553 mViewPager.setCurrentItem(position); 554 } 555 }; 556 557 /** 558 * This interface should be implemented by {@link Fragment}s within this 559 * activity so that the activity can determine whether the currently 560 * displayed view is handling the key event or not. 561 */ 562 public interface FragmentKeyListener { 563 /** 564 * Returns true if the key down event will be handled by the implementing class, or false 565 * otherwise. 566 */ 567 public boolean handleKeyDown(int keyCode); 568 } 569 570 @Override 571 public boolean onOptionsItemSelected(MenuItem item) { 572 573 switch (item.getItemId()) { 574 case android.R.id.home: 575 if (mIgnoreDefaultUpBehavior) { 576 finish(); 577 return true; 578 } 579 Intent intent = new Intent(this, PeopleActivity.class); 580 intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); 581 startActivity(intent); 582 finish(); 583 return true; 584 default: 585 break; 586 } 587 return super.onOptionsItemSelected(item); 588 } 589} 590