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