ContactDetailActivity.java revision 58fc577bd7966e6566cdcac09589a0d3e05128e7
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        if (globalSearch) {
178            super.startSearch(initialQuery, selectInitialQuery, appSearchData, globalSearch);
179        } else {
180            ContactsSearchManager.startSearch(this, initialQuery);
181        }
182    }
183
184    @Override
185    public boolean onCreateOptionsMenu(Menu menu) {
186        super.onCreateOptionsMenu(menu);
187        MenuInflater inflater = getMenuInflater();
188        inflater.inflate(R.menu.star, menu);
189        return true;
190    }
191
192    @Override
193    public boolean onPrepareOptionsMenu(Menu menu) {
194        MenuItem starredMenuItem = menu.findItem(R.id.menu_star);
195        ViewGroup starredContainer = (ViewGroup) getLayoutInflater().inflate(
196                R.layout.favorites_star, null, false);
197        final CheckBox starredView = (CheckBox) starredContainer.findViewById(R.id.star);
198        starredView.setOnClickListener(new OnClickListener() {
199            @Override
200            public void onClick(View v) {
201                // Toggle "starred" state
202                // Make sure there is a contact
203                if (mLookupUri != null) {
204                    Intent intent = ContactSaveService.createSetStarredIntent(
205                            ContactDetailActivity.this, mLookupUri, starredView.isChecked());
206                    ContactDetailActivity.this.startService(intent);
207                }
208            }
209        });
210        // If there is contact data, update the starred state
211        if (mContactData != null) {
212            ContactDetailDisplayUtils.setStarred(mContactData, starredView);
213        }
214        starredMenuItem.setActionView(starredContainer);
215        return true;
216    }
217
218    @Override
219    public boolean onKeyDown(int keyCode, KeyEvent event) {
220        // First check if the {@link ContactLoaderFragment} can handle the key
221        if (mLoaderFragment.handleKeyDown(keyCode)) return true;
222
223        // Otherwise find the correct fragment to handle the event
224        FragmentKeyListener mCurrentFragment;
225        switch (getCurrentPage()) {
226            case 0:
227                mCurrentFragment = mDetailFragment;
228                break;
229            case 1:
230                mCurrentFragment = mUpdatesFragment;
231                break;
232            default:
233                throw new IllegalStateException("Invalid current item for ViewPager");
234        }
235        if (mCurrentFragment.handleKeyDown(keyCode)) return true;
236
237        // In the last case, give the key event to the superclass.
238        return super.onKeyDown(keyCode, event);
239    }
240
241    private int getCurrentPage() {
242        // If the contact doesn't have any social updates, there is only 1 page (detail fragment).
243        if (!mContactHasUpdates) {
244            return 0;
245        }
246        // Otherwise find the current page based on the {@link ViewPager} or fragment carousel.
247        if (mViewPager != null) {
248            return mViewPager.getCurrentItem();
249        } else if (mFragmentCarousel != null) {
250            return mFragmentCarousel.getCurrentPage();
251        }
252        throw new IllegalStateException("Can't figure out the currently selected page. If the " +
253                "contact has social updates, there must be a ViewPager or fragment carousel");
254    }
255
256    @Override
257    protected void onSaveInstanceState(Bundle outState) {
258        super.onSaveInstanceState(outState);
259        if (mViewPager != null) {
260            outState.putString(KEY_DETAIL_FRAGMENT_TAG, mDetailFragment.getTag());
261            outState.putString(KEY_UPDATES_FRAGMENT_TAG, mUpdatesFragment.getTag());
262            return;
263        }
264    }
265
266    private final ContactLoaderFragmentListener mLoaderFragmentListener =
267            new ContactLoaderFragmentListener() {
268        @Override
269        public void onContactNotFound() {
270            finish();
271        }
272
273        @Override
274        public void onDetailsLoaded(final ContactLoader.Result result) {
275            if (result == null) {
276                return;
277            }
278            // Since {@link FragmentTransaction}s cannot be done in the onLoadFinished() of the
279            // {@link LoaderCallbacks}, then post this {@link Runnable} to the {@link Handler}
280            // on the main thread to execute later.
281            mHandler.post(new Runnable() {
282                @Override
283                public void run() {
284                    mContactData = result;
285                    mLookupUri = result.getLookupUri();
286                    mContactHasUpdates = result.getSocialSnippet() != null;
287                    invalidateOptionsMenu();
288                    setupTitle();
289                    if (mContactHasUpdates) {
290                        setupContactWithUpdates();
291                    } else {
292                        setupContactWithoutUpdates();
293                    }
294                }
295            });
296        }
297
298        @Override
299        public void onEditRequested(Uri contactLookupUri) {
300            startActivity(new Intent(Intent.ACTION_EDIT, contactLookupUri));
301        }
302
303        @Override
304        public void onDeleteRequested(Uri contactUri) {
305            ContactDeletionInteraction.start(ContactDetailActivity.this, contactUri, true);
306        }
307    };
308
309    /**
310     * Setup the activity title and subtitle with contact name and company.
311     */
312    private void setupTitle() {
313        CharSequence displayName = ContactDetailDisplayUtils.getDisplayName(this, mContactData);
314        String company =  ContactDetailDisplayUtils.getCompany(this, mContactData);
315
316        ActionBar actionBar = getActionBar();
317        actionBar.setTitle(displayName);
318        actionBar.setSubtitle(company);
319    }
320
321    private void setupContactWithUpdates() {
322        if (mContentView == null) {
323            mContentView = (ViewGroup) mInflater.inflate(
324                    R.layout.contact_detail_container_with_updates, mRootView, false);
325            mRootView.addView(mContentView);
326        }
327
328        // Narrow width screens have a {@link ViewPager} and {@link ContactDetailTabCarousel}
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            mTabCarousel.loadData(mContactData);
340        }
341
342        // Otherwise, wide width screens have a {@link ContactDetailFragmentCarousel}
343        mFragmentCarousel = (ContactDetailFragmentCarousel) findViewById(R.id.fragment_carousel);
344        if (mFragmentCarousel != null) {
345            if (mDetailFragment != null) mFragmentCarousel.setAboutFragment(mDetailFragment);
346            if (mUpdatesFragment != null) mFragmentCarousel.setUpdatesFragment(mUpdatesFragment);
347        }
348    }
349
350    private void setupContactWithoutUpdates() {
351        if (mContentView == null) {
352            mContentView = (ViewGroup) mInflater.inflate(
353                    R.layout.contact_detail_container_without_updates, mRootView, false);
354            mRootView.addView(mContentView);
355        }
356    }
357
358    private final ContactDetailFragment.Listener mFragmentListener =
359            new ContactDetailFragment.Listener() {
360        @Override
361        public void onItemClicked(Intent intent) {
362            try {
363                startActivity(intent);
364            } catch (ActivityNotFoundException e) {
365                Log.e(TAG, "No activity found for intent: " + intent);
366            }
367        }
368
369        @Override
370        public void onCreateRawContactRequested(
371                ArrayList<ContentValues> values, Account account) {
372            Toast.makeText(ContactDetailActivity.this, R.string.toast_making_personal_copy,
373                    Toast.LENGTH_LONG).show();
374            Intent serviceIntent = ContactSaveService.createNewRawContactIntent(
375                    ContactDetailActivity.this, values, account,
376                    ContactDetailActivity.class, Intent.ACTION_VIEW);
377            startService(serviceIntent);
378
379        }
380    };
381
382    public class ViewPagerAdapter extends FragmentPagerAdapter{
383
384        public ViewPagerAdapter(FragmentManager fm) {
385            super(fm);
386        }
387
388        @Override
389        public Fragment getItem(int position) {
390            switch (position) {
391                case 0:
392                    return new ContactDetailFragment();
393                case 1:
394                    return new ContactDetailUpdatesFragment();
395            }
396            throw new IllegalStateException("No fragment at position " + position);
397        }
398
399        @Override
400        public int getCount() {
401            return FRAGMENT_COUNT;
402        }
403    }
404
405    private OnPageChangeListener mOnPageChangeListener = new OnPageChangeListener() {
406
407        @Override
408        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
409            // The user is horizontally dragging the {@link ViewPager}, so send
410            // these scroll changes to the tab carousel. Ignore these events though if the carousel
411            // is actually controlling the {@link ViewPager} scrolls because it will already be
412            // in the correct position.
413            if (mViewPager.isFakeDragging()) {
414                return;
415            }
416            int x = (int) ((position + positionOffset) *
417                    mTabCarousel.getAllowedHorizontalScrollLength());
418            mTabCarousel.scrollTo(x, 0);
419        }
420
421        @Override
422        public void onPageSelected(int position) {
423            // Since a new page has been selected by the {@link ViewPager},
424            // update the tab selection in the carousel.
425            mTabCarousel.setCurrentTab(position);
426        }
427
428        @Override
429        public void onPageScrollStateChanged(int state) {}
430
431    };
432
433    private ContactDetailTabCarousel.Listener mTabCarouselListener =
434            new ContactDetailTabCarousel.Listener() {
435
436        @Override
437        public void onTouchDown() {
438            // The user just started scrolling the carousel, so begin "fake dragging" the
439            // {@link ViewPager} if it's not already doing so.
440            if (mViewPager.isFakeDragging()) {
441                return;
442            }
443            mViewPager.beginFakeDrag();
444        }
445
446        @Override
447        public void onTouchUp() {
448            // The user just stopped scrolling the carousel, so stop "fake dragging" the
449            // {@link ViewPager} if was doing so before.
450            if (mViewPager.isFakeDragging()) {
451                mViewPager.endFakeDrag();
452            }
453        }
454
455        @Override
456        public void onScrollChanged(int l, int t, int oldl, int oldt) {
457            // The user is scrolling the carousel, so send the scroll deltas to the
458            // {@link ViewPager} so it can move in sync.
459            if (mViewPager.isFakeDragging()) {
460                mViewPager.fakeDragBy(oldl-l);
461            }
462        }
463
464        @Override
465        public void onTabSelected(int position) {
466            // The user selected a tab, so update the {@link ViewPager}
467            mViewPager.setCurrentItem(position);
468        }
469    };
470
471    private OnScrollListener mVerticalScrollListener = new OnScrollListener() {
472
473        @Override
474        public void onScroll(
475                AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
476            if (mTabCarousel == null) {
477                return;
478            }
479            // If the FIRST item is not visible on the screen, then the carousel must be pinned
480            // at the top of the screen.
481            if (firstVisibleItem != 0) {
482                mTabCarousel.setY(-mTabCarousel.getAllowedVerticalScrollLength());
483                return;
484            }
485            View topView = view.getChildAt(firstVisibleItem);
486            if (topView == null) {
487                return;
488            }
489            int amtToScroll = Math.max((int) view.getChildAt(firstVisibleItem).getY(),
490                    - mTabCarousel.getAllowedVerticalScrollLength());
491            mTabCarousel.setY(amtToScroll);
492        }
493
494        @Override
495        public void onScrollStateChanged(AbsListView view, int scrollState) {}
496
497    };
498
499    /**
500     * This interface should be implemented by {@link Fragment}s within this
501     * activity so that the activity can determine whether the currently
502     * displayed view is handling the key event or not.
503     */
504    public interface FragmentKeyListener {
505        /**
506         * Returns true if the key down event will be handled by the implementing class, or false
507         * otherwise.
508         */
509        public boolean handleKeyDown(int keyCode);
510    }
511
512    @Override
513    public boolean onOptionsItemSelected(MenuItem item) {
514
515        switch (item.getItemId()) {
516            case android.R.id.home:
517                finish();
518                return true;
519            default:
520                break;
521        }
522        return super.onOptionsItemSelected(item);
523    }
524}
525