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