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