| /* |
| * Copyright (C) 2009 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.contacts.quickcontact; |
| |
| import android.animation.ObjectAnimator; |
| import android.app.Activity; |
| import android.app.LoaderManager.LoaderCallbacks; |
| import android.content.ContentUris; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.Loader; |
| import android.content.pm.PackageManager; |
| import android.net.Uri; |
| import android.os.Bundle; |
| import android.provider.ContactsContract.CommonDataKinds.Email; |
| import android.provider.ContactsContract.CommonDataKinds.Phone; |
| import android.provider.ContactsContract.CommonDataKinds.SipAddress; |
| import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; |
| import android.provider.ContactsContract.CommonDataKinds.Website; |
| import android.provider.ContactsContract.Contacts; |
| import android.provider.ContactsContract.DisplayNameSources; |
| import android.provider.ContactsContract.Intents.Insert; |
| import android.provider.ContactsContract.Directory; |
| import android.provider.ContactsContract.QuickContact; |
| import android.provider.ContactsContract.RawContacts; |
| import android.text.TextUtils; |
| import android.util.Log; |
| import android.view.View; |
| import android.view.View.OnClickListener; |
| import android.view.WindowManager; |
| import android.view.animation.AccelerateDecelerateInterpolator; |
| import android.view.animation.Interpolator; |
| import android.widget.ImageView; |
| import android.widget.TextView; |
| import android.widget.Toast; |
| |
| import com.android.contacts.ContactSaveService; |
| import com.android.contacts.common.Collapser; |
| import com.android.contacts.R; |
| import com.android.contacts.common.model.AccountTypeManager; |
| import com.android.contacts.common.model.Contact; |
| import com.android.contacts.common.model.ContactLoader; |
| import com.android.contacts.common.model.RawContact; |
| import com.android.contacts.common.model.account.AccountType; |
| import com.android.contacts.common.model.dataitem.DataItem; |
| import com.android.contacts.common.model.dataitem.DataKind; |
| import com.android.contacts.common.model.dataitem.EmailDataItem; |
| import com.android.contacts.common.model.dataitem.ImDataItem; |
| import com.android.contacts.common.util.Constants; |
| import com.android.contacts.common.util.DataStatus; |
| import com.android.contacts.common.util.UriUtils; |
| import com.android.contacts.quickcontact.ExpandingEntryCardView.Entry; |
| import com.android.contacts.util.ImageViewDrawableSetter; |
| import com.android.contacts.common.util.StopWatch; |
| import com.android.contacts.util.SchedulingUtils; |
| import com.android.contacts.widget.MultiShrinkScroller; |
| import com.android.contacts.widget.MultiShrinkScroller.MultiShrinkScrollerListener; |
| |
| import com.google.common.base.Preconditions; |
| import com.google.common.collect.Lists; |
| |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Set; |
| |
| /** |
| * Mostly translucent {@link Activity} that shows QuickContact dialog. It loads |
| * data asynchronously, and then shows a popup with details centered around |
| * {@link Intent#getSourceBounds()}. |
| */ |
| public class QuickContactActivity extends Activity { |
| private static final String TAG = "QuickContact"; |
| |
| private static final boolean TRACE_LAUNCH = false; |
| private static final String TRACE_TAG = "quickcontact"; |
| private static final int ANIMATION_DURATION = 250; |
| private static final boolean ENABLE_STOPWATCH = false; |
| |
| |
| @SuppressWarnings("deprecation") |
| private static final String LEGACY_AUTHORITY = android.provider.Contacts.AUTHORITY; |
| |
| private Uri mLookupUri; |
| private String[] mExcludeMimes; |
| private List<String> mSortedActionMimeTypes = Lists.newArrayList(); |
| |
| private View mPhotoContainer; |
| |
| private ImageView mPhotoView; |
| private ImageView mEditOrAddContactImage; |
| private ImageView mStarImage; |
| private ExpandingEntryCardView mCommunicationCard; |
| private MultiShrinkScroller mScroller; |
| |
| private Contact mContactData; |
| private ContactLoader mContactLoader; |
| |
| private final ImageViewDrawableSetter mPhotoSetter = new ImageViewDrawableSetter(); |
| |
| /** |
| * Keeps the default action per mimetype. Empty if no default actions are set |
| */ |
| private HashMap<String, Action> mDefaultsMap = new HashMap<String, Action>(); |
| |
| /** |
| * Set of {@link Action} that are associated with the aggregate currently |
| * displayed by this dialog, represented as a map from {@link String} |
| * MIME-type to a list of {@link Action}. |
| */ |
| private ActionMultiMap mActions = new ActionMultiMap(); |
| |
| /** |
| * {@link #LEADING_MIMETYPES} and {@link #TRAILING_MIMETYPES} are used to sort MIME-types. |
| * |
| * <p>The MIME-types in {@link #LEADING_MIMETYPES} appear in the front of the dialog, |
| * in the order specified here.</p> |
| * |
| * <p>The ones in {@link #TRAILING_MIMETYPES} appear in the end of the dialog, in the order |
| * specified here.</p> |
| * |
| * <p>The rest go between them, in the order in the array.</p> |
| */ |
| private static final List<String> LEADING_MIMETYPES = Lists.newArrayList( |
| Phone.CONTENT_ITEM_TYPE, SipAddress.CONTENT_ITEM_TYPE, Email.CONTENT_ITEM_TYPE); |
| |
| /** See {@link #LEADING_MIMETYPES}. */ |
| private static final List<String> TRAILING_MIMETYPES = Lists.newArrayList( |
| StructuredPostal.CONTENT_ITEM_TYPE, Website.CONTENT_ITEM_TYPE); |
| |
| /** Id for the background loader */ |
| private static final int LOADER_ID = 0; |
| |
| private StopWatch mStopWatch = ENABLE_STOPWATCH |
| ? StopWatch.start("QuickContact") : StopWatch.getNullStopWatch(); |
| |
| final OnClickListener mEditContactClickHandler = new OnClickListener() { |
| @Override |
| public void onClick(View v) { |
| final Intent intent = new Intent(Intent.ACTION_EDIT, mLookupUri); |
| mContactLoader.cacheResult(); |
| intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); |
| startActivity(intent); |
| } |
| }; |
| |
| final OnClickListener mAddToContactsClickHandler = new OnClickListener() { |
| @Override |
| public void onClick(View v) { |
| if (mContactData == null) { |
| Log.e(TAG, "Empty contact data when trying to add to contact"); |
| return; |
| } |
| final Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT); |
| intent.setType(Contacts.CONTENT_ITEM_TYPE); |
| |
| // Only pre-fill the name field if the provided display name is an organization |
| // name or better (e.g. structured name, nickname) |
| if (mContactData.getDisplayNameSource() >= DisplayNameSources.ORGANIZATION) { |
| intent.putExtra(Insert.NAME, mContactData.getDisplayName()); |
| } |
| intent.putExtra(Insert.DATA, mContactData.getContentValues()); |
| startActivity(intent); |
| } |
| }; |
| |
| final OnClickListener mEntryClickHandler = new OnClickListener() { |
| @Override |
| public void onClick(View v) { |
| Log.i(TAG, "mEntryClickHandler onClick"); |
| Object intent = v.getTag(); |
| if (intent == null || !(intent instanceof Intent)) { |
| return; |
| } |
| startActivity((Intent) intent); |
| } |
| }; |
| |
| final MultiShrinkScrollerListener mMultiShrinkScrollerListener |
| = new MultiShrinkScrollerListener() { |
| @Override |
| public void onScrolledOffBottom() { |
| onBackPressed(); |
| } |
| }; |
| |
| @Override |
| protected void onCreate(Bundle icicle) { |
| mStopWatch.lap("c"); // create start |
| super.onCreate(icicle); |
| |
| mStopWatch.lap("sc"); // super.onCreate |
| |
| if (TRACE_LAUNCH) android.os.Debug.startMethodTracing(TRACE_TAG); |
| |
| // Parse intent |
| final Intent intent = getIntent(); |
| |
| Uri lookupUri = intent.getData(); |
| |
| // Check to see whether it comes from the old version. |
| if (lookupUri != null && LEGACY_AUTHORITY.equals(lookupUri.getAuthority())) { |
| final long rawContactId = ContentUris.parseId(lookupUri); |
| lookupUri = RawContacts.getContactLookupUri(getContentResolver(), |
| ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId)); |
| } |
| |
| mLookupUri = Preconditions.checkNotNull(lookupUri, "missing lookupUri"); |
| |
| mExcludeMimes = intent.getStringArrayExtra(QuickContact.EXTRA_EXCLUDE_MIMES); |
| |
| mStopWatch.lap("i"); // intent parsed |
| |
| mContactLoader = (ContactLoader) getLoaderManager().initLoader( |
| LOADER_ID, null, mLoaderCallbacks); |
| |
| mStopWatch.lap("ld"); // loader started |
| |
| // Show QuickContact in front of soft input |
| getWindow().setFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM, |
| WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM); |
| |
| setContentView(R.layout.quickcontact_activity); |
| |
| mStopWatch.lap("l"); // layout inflated |
| |
| mEditOrAddContactImage = (ImageView) findViewById(R.id.contact_edit_image); |
| mStarImage = (ImageView) findViewById(R.id.quickcontact_star_button); |
| mCommunicationCard = (ExpandingEntryCardView) findViewById(R.id.communication_card); |
| mScroller = (MultiShrinkScroller) findViewById(R.id.multiscroller); |
| mCommunicationCard.setTitle(getResources().getString(R.string.communication_card_title)); |
| |
| if (mScroller != null) { |
| mScroller.initialize(mMultiShrinkScrollerListener); |
| } |
| |
| mEditOrAddContactImage.setOnClickListener(mEditContactClickHandler); |
| mCommunicationCard.setOnClickListener(mEntryClickHandler); |
| |
| // find and prepare correct header view |
| mPhotoContainer = findViewById(R.id.photo_container); |
| |
| setHeaderNameText(R.id.name, R.string.missing_name); |
| |
| mPhotoView = (ImageView) mPhotoContainer.findViewById(R.id.photo); |
| mPhotoView.setOnClickListener(mEditContactClickHandler); |
| |
| mStopWatch.lap("v"); // view initialized |
| |
| if (mScroller != null) { |
| mScroller.setVisibility(View.GONE); |
| } |
| |
| mStopWatch.lap("cf"); // onCreate finished |
| } |
| |
| private void runEntranceAnimation() { |
| final int bottomScroll = mScroller.getScrollUntilOffBottom() - 1; |
| final ObjectAnimator scrollAnimation |
| = ObjectAnimator.ofInt(mScroller, "scroll", -bottomScroll, 0); |
| scrollAnimation.setDuration(ANIMATION_DURATION); |
| scrollAnimation.start(); |
| } |
| |
| /** Assign this string to the view if it is not empty. */ |
| private void setHeaderNameText(int id, int resId) { |
| setHeaderNameText(id, getText(resId)); |
| } |
| |
| /** Assign this string to the view if it is not empty. */ |
| private void setHeaderNameText(int id, CharSequence value) { |
| final View view = mPhotoContainer.findViewById(id); |
| if (view instanceof TextView) { |
| if (!TextUtils.isEmpty(value)) { |
| ((TextView)view).setText(value); |
| } |
| } |
| } |
| |
| /** |
| * Check if the given MIME-type appears in the list of excluded MIME-types |
| * that the most-recent caller requested. |
| */ |
| private boolean isMimeExcluded(String mimeType) { |
| if (mExcludeMimes == null) return false; |
| for (String excludedMime : mExcludeMimes) { |
| if (TextUtils.equals(excludedMime, mimeType)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Handle the result from the ContactLoader |
| */ |
| private void bindData(Contact data) { |
| mContactData = data; |
| final ResolveCache cache = ResolveCache.getInstance(this); |
| final Context context = this; |
| |
| mEditOrAddContactImage.setVisibility(isMimeExcluded(Contacts.CONTENT_ITEM_TYPE) ? |
| View.GONE : View.VISIBLE); |
| final boolean isStarred = data.getStarred(); |
| if (isStarred) { |
| mStarImage.setImageResource(R.drawable.ic_favorite_on_lt); |
| mStarImage.setContentDescription( |
| getResources().getString(R.string.menu_removeStar)); |
| } else { |
| mStarImage.setImageResource(R.drawable.ic_favorite_off_lt); |
| mStarImage.setContentDescription( |
| getResources().getString(R.string.menu_addStar)); |
| } |
| final Uri lookupUri = data.getLookupUri(); |
| |
| // If this is a json encoded URI, there is no local contact to star |
| if (UriUtils.isEncodedContactUri(lookupUri)) { |
| mStarImage.setVisibility(View.GONE); |
| |
| // If directory export support is not allowed, then don't allow the user to add |
| // to contacts |
| if (mContactData.getDirectoryExportSupport() == Directory.EXPORT_SUPPORT_NONE) { |
| configureHeaderClickActions(false); |
| } else { |
| configureHeaderClickActions(true); |
| } |
| } else { |
| configureHeaderClickActions(false); |
| mStarImage.setVisibility(View.VISIBLE); |
| mStarImage.setOnClickListener(new OnClickListener() { |
| @Override |
| public void onClick(View view) { |
| // Toggle "starred" state |
| // Make sure there is a contact |
| if (lookupUri != null) { |
| // Changes the state of the image already before sending updates to the |
| // database |
| if (isStarred) { |
| mStarImage.setImageResource(R.drawable.ic_favorite_off_lt); |
| } else { |
| mStarImage.setImageResource(R.drawable.ic_favorite_on_lt); |
| } |
| |
| // Now perform the real save |
| final Intent intent = ContactSaveService.createSetStarredIntent(context, |
| lookupUri, !isStarred); |
| context.startService(intent); |
| } |
| } |
| }); |
| } |
| |
| mDefaultsMap.clear(); |
| |
| mStopWatch.lap("sph"); // Start photo setting |
| |
| mPhotoSetter.setupContactPhoto(data, mPhotoView); |
| |
| mStopWatch.lap("ph"); // Photo set |
| |
| for (RawContact rawContact : data.getRawContacts()) { |
| for (DataItem dataItem : rawContact.getDataItems()) { |
| final String mimeType = dataItem.getMimeType(); |
| final AccountType accountType = rawContact.getAccountType(this); |
| final DataKind dataKind = AccountTypeManager.getInstance(this) |
| .getKindOrFallback(accountType, mimeType); |
| |
| // Skip this data item if MIME-type excluded |
| if (isMimeExcluded(mimeType)) continue; |
| |
| final long dataId = dataItem.getId(); |
| final boolean isPrimary = dataItem.isPrimary(); |
| final boolean isSuperPrimary = dataItem.isSuperPrimary(); |
| |
| if (dataKind != null) { |
| // Build an action for this data entry, find a mapping to a UI |
| // element, build its summary from the cursor, and collect it |
| // along with all others of this MIME-type. |
| final Action action = new DataAction(context, dataItem, dataKind); |
| final boolean wasAdded = considerAdd(action, cache, isSuperPrimary); |
| if (wasAdded) { |
| // Remember the default |
| if (isSuperPrimary || (isPrimary && (mDefaultsMap.get(mimeType) == null))) { |
| mDefaultsMap.put(mimeType, action); |
| } |
| } |
| } |
| |
| // Handle Email rows with presence data as Im entry |
| final DataStatus status = data.getStatuses().get(dataId); |
| if (status != null && dataItem instanceof EmailDataItem) { |
| final EmailDataItem email = (EmailDataItem) dataItem; |
| final ImDataItem im = ImDataItem.createFromEmail(email); |
| if (dataKind != null) { |
| final DataAction action = new DataAction(context, im, dataKind); |
| action.setPresence(status.getPresence()); |
| considerAdd(action, cache, isSuperPrimary); |
| } |
| } |
| } |
| } |
| |
| mStopWatch.lap("e"); // Entities inflated |
| |
| // Collapse Action Lists (remove e.g. duplicate e-mail addresses from different sources) |
| for (List<Action> actionChildren : mActions.values()) { |
| Collapser.collapseList(actionChildren); |
| } |
| |
| mStopWatch.lap("c"); // List collapsed |
| |
| setHeaderNameText(R.id.name, data.getDisplayName()); |
| |
| // List of Entry that makes up the ExpandingEntryCardView |
| final List<Entry> entries = new ArrayList<>(); |
| // All the mime-types to add. |
| final Set<String> containedTypes = new HashSet<String>(mActions.keySet()); |
| mSortedActionMimeTypes.clear(); |
| // First, add LEADING_MIMETYPES, which are most common. |
| for (String mimeType : LEADING_MIMETYPES) { |
| if (containedTypes.contains(mimeType)) { |
| mSortedActionMimeTypes.add(mimeType); |
| containedTypes.remove(mimeType); |
| entries.addAll(actionsToEntries(mActions.get(mimeType))); |
| } |
| } |
| |
| // Add all the remaining ones that are not TRAILING |
| for (String mimeType : containedTypes.toArray(new String[containedTypes.size()])) { |
| if (!TRAILING_MIMETYPES.contains(mimeType)) { |
| mSortedActionMimeTypes.add(mimeType); |
| containedTypes.remove(mimeType); |
| entries.addAll(actionsToEntries(mActions.get(mimeType))); |
| } |
| } |
| |
| // Then, add TRAILING_MIMETYPES, which are least common. |
| for (String mimeType : TRAILING_MIMETYPES) { |
| if (containedTypes.contains(mimeType)) { |
| containedTypes.remove(mimeType); |
| mSortedActionMimeTypes.add(mimeType); |
| entries.addAll(actionsToEntries(mActions.get(mimeType))); |
| } |
| } |
| mCommunicationCard.initialize(entries, /* numInitialVisibleEntries = */ 2, |
| /* isExpanded = */ false, /* themeColor = */ 0); |
| |
| final boolean hasData = !mSortedActionMimeTypes.isEmpty(); |
| mCommunicationCard.setVisibility(hasData ? View.VISIBLE: View.GONE); |
| } |
| |
| /** |
| * Consider adding the given {@link Action}, which will only happen if |
| * {@link PackageManager} finds an application to handle |
| * {@link Action#getIntent()}. |
| * @param action the action to handle |
| * @param resolveCache cache of applications that can handle actions |
| * @param front indicates whether to add the action to the front of the list |
| * @return true if action has been added |
| */ |
| private boolean considerAdd(Action action, ResolveCache resolveCache, boolean front) { |
| if (resolveCache.hasResolve(action)) { |
| mActions.put(action.getMimeType(), action, front); |
| return true; |
| } |
| return false; |
| } |
| |
| /** |
| * Bind the correct image resource and click handlers to the header views |
| * |
| * @param canAdd Whether or not the user can directly add information in this quick contact |
| * to their local contacts |
| */ |
| private void configureHeaderClickActions(boolean canAdd) { |
| if (canAdd) { |
| mEditOrAddContactImage.setImageResource(R.drawable.ic_person_add_24dp); |
| mEditOrAddContactImage.setOnClickListener(mAddToContactsClickHandler); |
| mPhotoView.setOnClickListener(mAddToContactsClickHandler); |
| } else { |
| mEditOrAddContactImage.setImageResource(R.drawable.ic_create_24dp); |
| mEditOrAddContactImage.setOnClickListener(mEditContactClickHandler); |
| mPhotoView.setOnClickListener(mEditContactClickHandler); |
| } |
| } |
| |
| /** |
| * Converts a list of Action into a list of Entry |
| * @param actions The list of Action to convert |
| * @return The converted list of Entry |
| */ |
| private List<Entry> actionsToEntries(List<Action> actions) { |
| List<Entry> entries = new ArrayList<>(); |
| for (Action action : actions) { |
| entries.add(new Entry(ResolveCache.getInstance(this).getIcon(action), |
| action.getMimeType(), action.getSubtitle().toString(), |
| action.getBody().toString(), action.getIntent(), /* isEditable= */ false)); |
| } |
| return entries; |
| } |
| |
| private LoaderCallbacks<Contact> mLoaderCallbacks = |
| new LoaderCallbacks<Contact>() { |
| @Override |
| public void onLoaderReset(Loader<Contact> loader) { |
| } |
| |
| @Override |
| public void onLoadFinished(Loader<Contact> loader, Contact data) { |
| mStopWatch.lap("lf"); // onLoadFinished |
| if (isFinishing()) { |
| return; |
| } |
| if (data.isError()) { |
| // This shouldn't ever happen, so throw an exception. The {@link ContactLoader} |
| // should log the actual exception. |
| throw new IllegalStateException("Failed to load contact", data.getException()); |
| } |
| if (data.isNotFound()) { |
| Log.i(TAG, "No contact found: " + ((ContactLoader)loader).getLookupUri()); |
| Toast.makeText(QuickContactActivity.this, R.string.invalidContactMessage, |
| Toast.LENGTH_LONG).show(); |
| return; |
| } |
| |
| bindData(data); |
| |
| mStopWatch.lap("bd"); // bindData finished |
| |
| if (TRACE_LAUNCH) android.os.Debug.stopMethodTracing(); |
| if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) { |
| Log.d(Constants.PERFORMANCE_TAG, "QuickContact shown"); |
| } |
| |
| if (mScroller != null) { |
| // Data bound and ready, pull curtain to show. Put this on the Handler to ensure |
| // that the layout passes are completed |
| mScroller.setVisibility(View.VISIBLE); |
| SchedulingUtils.doOnPreDraw(mScroller, /* drawNextFrame = */ false, |
| new Runnable() { |
| @Override |
| public void run() { |
| runEntranceAnimation(); |
| } |
| }); |
| } |
| mStopWatch.stopAndLog(TAG, 0); |
| mStopWatch = StopWatch.getNullStopWatch(); |
| } |
| |
| @Override |
| public Loader<Contact> onCreateLoader(int id, Bundle args) { |
| if (mLookupUri == null) { |
| Log.wtf(TAG, "Lookup uri wasn't initialized. Loader was started too early"); |
| } |
| return new ContactLoader(getApplicationContext(), mLookupUri, |
| false /*loadGroupMetaData*/, false /*loadInvitableAccountTypes*/, |
| false /*postViewNotification*/, true /*computeFormattedPhoneNumber*/); |
| } |
| }; |
| @Override |
| public void onBackPressed() { |
| if (mScroller != null) { |
| // TODO: implement exit animation if the scroller isn't already off the screen |
| finish(); |
| } else { |
| super.onBackPressed(); |
| } |
| } |
| } |