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