blob: 2c62fe47a92d10d9b8a7db671b4bb6b604de478e [file] [log] [blame]
/*
* 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 com.android.contacts.Collapser;
import com.android.contacts.ContactPhotoManager;
import com.android.contacts.R;
import com.android.contacts.model.AccountTypeManager;
import com.android.contacts.model.DataKind;
import com.android.contacts.util.DataStatus;
import com.android.contacts.util.NotifyingAsyncQueryHandler;
import com.android.contacts.util.NotifyingAsyncQueryHandler.AsyncQueryListener;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import android.app.Activity;
import android.app.Fragment;
import android.app.FragmentManager;
import android.content.ActivityNotFoundException;
import android.content.ContentUris;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.res.AssetFileDescriptor;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.provider.ContactsContract.CommonDataKinds.Email;
import android.provider.ContactsContract.CommonDataKinds.Im;
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.provider.ContactsContract.CommonDataKinds.Photo;
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.Data;
import android.provider.ContactsContract.DisplayPhoto;
import android.provider.ContactsContract.QuickContact;
import android.provider.ContactsContract.RawContacts;
import android.support.v13.app.FragmentPagerAdapter;
import android.support.v4.view.ViewPager;
import android.support.v4.view.ViewPager.SimpleOnPageChangeListener;
import android.text.TextUtils;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.HorizontalScrollView;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.Toast;
import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
// TODO: Save selected tab index during rotation
/**
* 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";
@SuppressWarnings("deprecation")
private static final String LEGACY_AUTHORITY = android.provider.Contacts.AUTHORITY;
private NotifyingAsyncQueryHandler mHandler;
private Uri mLookupUri;
private String[] mExcludeMimes;
private List<String> mSortedActionMimeTypes = Lists.newArrayList();
private boolean mHasFinishedAnimatingIn = false;
private boolean mHasStartedAnimatingOut = false;
private FloatingChildLayout mFloatingLayout;
private View mPhotoContainer;
private ViewGroup mTrack;
private HorizontalScrollView mTrackScroller;
private View mSelectedTabRectangle;
private View mLineAfterTrack;
private ImageButton mOpenDetailsButton;
private ImageButton mOpenDetailsPushLayerButton;
private ViewPager mListPager;
/**
* 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 handler that loads the data */
private static final int HANDLER_ID_DATA = 1;
@Override
protected void onCreate(Bundle icicle) {
super.onCreate(icicle);
// 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);
mFloatingLayout = (FloatingChildLayout) findViewById(R.id.floating_layout);
mTrack = (ViewGroup) findViewById(R.id.track);
mTrackScroller = (HorizontalScrollView) findViewById(R.id.track_scroller);
mOpenDetailsButton = (ImageButton) findViewById(R.id.open_details_button);
mOpenDetailsPushLayerButton = (ImageButton) findViewById(R.id.open_details_push_layer);
mListPager = (ViewPager) findViewById(R.id.item_list_pager);
mSelectedTabRectangle = findViewById(R.id.selected_tab_rectangle);
mLineAfterTrack = findViewById(R.id.line_after_track);
mFloatingLayout.setOnOutsideTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
return handleOutsideTouch();
}
});
final OnClickListener openDetailsClickHandler = new OnClickListener() {
@Override
public void onClick(View v) {
final Intent intent = new Intent(Intent.ACTION_VIEW, mLookupUri);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
hide(false);
}
};
mOpenDetailsButton.setOnClickListener(openDetailsClickHandler);
mOpenDetailsPushLayerButton.setOnClickListener(openDetailsClickHandler);
mListPager.setAdapter(new ViewPagerAdapter(getFragmentManager()));
mListPager.setOnPageChangeListener(new PageChangeListener());
mHandler = new NotifyingAsyncQueryHandler(this, mQueryListener);
show();
}
private void show() {
if (TRACE_LAUNCH) {
android.os.Debug.startMethodTracing(TRACE_TAG);
}
final Intent intent = getIntent();
Uri lookupUri = intent.getData();
// Check to see whether it comes from the old version.
if (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");
// Read requested parameters for displaying
final Rect targetScreen = intent.getSourceBounds();
Preconditions.checkNotNull(targetScreen, "missing targetScreen");
mFloatingLayout.setChildTargetScreen(targetScreen);
mExcludeMimes = intent.getStringArrayExtra(QuickContact.EXTRA_EXCLUDE_MIMES);
// find and prepare correct header view
mPhotoContainer = findViewById(R.id.photo_container);
setHeaderNameText(R.id.name, R.string.missing_name);
// Start background query for data, but only select photo rows when they
// directly match the super-primary PHOTO_ID.
final Uri dataUri = Uri.withAppendedPath(lookupUri, Contacts.Data.CONTENT_DIRECTORY);
mHandler.cancelOperation(HANDLER_ID_DATA);
// Select all data items of the contact (except for photos, where we only select the display
// photo)
mHandler.startQuery(HANDLER_ID_DATA, lookupUri, dataUri, DataQuery.PROJECTION, Data.MIMETYPE
+ "!=? OR (" + Data.MIMETYPE + "=? AND " + Data._ID + "=" + Contacts.PHOTO_ID
+ ")", new String[] { Photo.CONTENT_ITEM_TYPE, Photo.CONTENT_ITEM_TYPE }, null);
}
private boolean handleOutsideTouch() {
if (!mHasFinishedAnimatingIn) return false;
if (mHasStartedAnimatingOut) return false;
mHasStartedAnimatingOut = true;
hide(true);
return true;
}
private void hide(boolean withAnimation) {
// cancel any pending queries
mHandler.cancelOperation(HANDLER_ID_DATA);
if (withAnimation) {
mFloatingLayout.hideChild(new Runnable() {
@Override
public void run() {
finish();
}
});
} else {
mFloatingLayout.hideChild(null);
finish();
}
}
@Override
public void onBackPressed() {
hide(true);
}
private final AsyncQueryListener mQueryListener = new AsyncQueryListener() {
@Override
public synchronized void onQueryComplete(int token, Object cookie, Cursor cursor) {
try {
if (isFinishing()) {
hide(false);
return;
} else if (cursor == null || cursor.getCount() == 0) {
Toast.makeText(QuickContactActivity.this, R.string.invalidContactMessage,
Toast.LENGTH_LONG).show();
hide(false);
return;
}
bindData(cursor);
if (TRACE_LAUNCH) {
android.os.Debug.stopMethodTracing();
}
// Data bound and ready, pull curtain to show. Put this on the Handler to ensure
// that the layout passes are completed
mHandler.post(new Runnable() {
@Override
public void run() {
mFloatingLayout.showChild(new Runnable() {
@Override
public void run() {
mHasFinishedAnimatingIn = true;
}
});
}
});
} finally {
if (cursor != null) {
cursor.close();
}
}
}
};
/** 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);
}
}
}
/**
* Assign this string to the view (if found in {@link #mPhotoContainer}), or hiding this view
* if there is no string.
*/
private void setHeaderText(int id, int resId) {
setHeaderText(id, getText(resId));
}
/**
* Assign this string to the view (if found in {@link #mPhotoContainer}), or hiding this view
* if there is no string.
*/
private void setHeaderText(int id, CharSequence value) {
final View view = mPhotoContainer.findViewById(id);
if (view instanceof TextView) {
((TextView)view).setText(value);
view.setVisibility(TextUtils.isEmpty(value) ? View.GONE : View.VISIBLE);
}
}
/** Assign this image to the view, if found in {@link #mPhotoContainer}. */
private void setHeaderImage(int id, Drawable drawable) {
final View view = mPhotoContainer.findViewById(id);
if (view instanceof ImageView) {
((ImageView)view).setImageDrawable(drawable);
view.setVisibility(drawable == null ? View.GONE : View.VISIBLE);
}
}
/**
* 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 {@link #TOKEN_DATA} query.
*/
private void bindData(Cursor cursor) {
final ResolveCache cache = ResolveCache.getInstance(this);
final Context context = this;
mOpenDetailsButton.setVisibility(isMimeExcluded(Contacts.CONTENT_ITEM_TYPE) ? View.GONE
: View.VISIBLE);
mDefaultsMap.clear();
final DataStatus status = new DataStatus();
final AccountTypeManager accountTypes = AccountTypeManager.getInstance(
context.getApplicationContext());
final ImageView photoView = (ImageView) mPhotoContainer.findViewById(R.id.photo);
Bitmap photoBitmap = null;
while (cursor.moveToNext()) {
// Handle any social status updates from this row
status.possibleUpdate(cursor);
final String mimeType = cursor.getString(DataQuery.MIMETYPE);
// Skip this data item if MIME-type excluded
if (isMimeExcluded(mimeType)) continue;
final long dataId = cursor.getLong(DataQuery._ID);
final String accountType = cursor.getString(DataQuery.ACCOUNT_TYPE);
final String dataSet = cursor.getString(DataQuery.DATA_SET);
final boolean isPrimary = cursor.getInt(DataQuery.IS_PRIMARY) != 0;
final boolean isSuperPrimary = cursor.getInt(DataQuery.IS_SUPER_PRIMARY) != 0;
// Handle photos included as data row
if (Photo.CONTENT_ITEM_TYPE.equals(mimeType)) {
final int displayPhotoColumnIndex = cursor.getColumnIndex(Photo.PHOTO_FILE_ID);
final boolean hasDisplayPhoto = !cursor.isNull(displayPhotoColumnIndex);
if (hasDisplayPhoto) {
final long displayPhotoId = cursor.getLong(displayPhotoColumnIndex);
final Uri displayPhotoUri = ContentUris.withAppendedId(
DisplayPhoto.CONTENT_URI, displayPhotoId);
// Fetch and JPEG uncompress on the background thread
new AsyncTask<Void, Void, Bitmap>() {
@Override
protected Bitmap doInBackground(Void... params) {
try {
AssetFileDescriptor fd = getContentResolver()
.openAssetFileDescriptor(displayPhotoUri, "r");
return BitmapFactory.decodeStream(fd.createInputStream());
} catch (IOException e) {
Log.e(TAG, "Error getting display photo. Ignoring, as we already " +
"have the thumbnail", e);
return null;
}
}
@Override
protected void onPostExecute(Bitmap result) {
if (result == null) return;
photoView.setImageBitmap(result);
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[]) null);
}
final int photoColumnIndex = cursor.getColumnIndex(Photo.PHOTO);
final byte[] photoBlob = cursor.getBlob(photoColumnIndex);
if (photoBlob != null) {
photoBitmap = BitmapFactory.decodeByteArray(photoBlob, 0, photoBlob.length);
}
continue;
}
final DataKind kind = accountTypes.getKindOrFallback(accountType, dataSet, mimeType);
if (kind != 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, mimeType, kind, dataId, cursor);
final boolean wasAdded = considerAdd(action, cache);
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 boolean hasPresence = !cursor.isNull(DataQuery.PRESENCE);
if (hasPresence && Email.CONTENT_ITEM_TYPE.equals(mimeType)) {
final DataKind imKind = accountTypes.getKindOrFallback(accountType, dataSet,
Im.CONTENT_ITEM_TYPE);
if (imKind != null) {
final DataAction action = new DataAction(context, Im.CONTENT_ITEM_TYPE, imKind,
dataId, cursor);
considerAdd(action, cache);
}
}
}
// Collapse Action Lists (remove e.g. duplicate e-mail addresses from different sources)
for (List<Action> actionChildren : mActions.values()) {
Collapser.collapseList(actionChildren);
}
if (cursor.moveToLast()) {
// Read contact name from last data row
final String name = cursor.getString(DataQuery.DISPLAY_NAME);
setHeaderNameText(R.id.name, name);
}
if (photoView != null) {
// Place photo when discovered in data, otherwise show generic avatar
if (photoBitmap != null) {
photoView.setImageBitmap(photoBitmap);
} else {
photoView.setImageResource(ContactPhotoManager.getDefaultAvatarResId(true, false));
}
}
// 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);
}
}
// 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);
}
}
// Then, add TRAILING_MIMETYPES, which are least common.
for (String mimeType : TRAILING_MIMETYPES) {
if (containedTypes.contains(mimeType)) {
containedTypes.remove(mimeType);
mSortedActionMimeTypes.add(mimeType);
}
}
// Add buttons for each mimetype
for (String mimeType : mSortedActionMimeTypes) {
final View actionView = inflateAction(mimeType, cache, mTrack);
mTrack.addView(actionView);
}
final boolean hasData = !mSortedActionMimeTypes.isEmpty();
mTrackScroller.setVisibility(hasData ? View.VISIBLE : View.GONE);
mSelectedTabRectangle.setVisibility(hasData ? View.VISIBLE : View.GONE);
mLineAfterTrack.setVisibility(hasData ? View.VISIBLE : View.GONE);
mListPager.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()}.
* @return true if action has been added
*/
private boolean considerAdd(Action action, ResolveCache resolveCache) {
if (resolveCache.hasResolve(action)) {
mActions.put(action.getMimeType(), action);
return true;
}
return false;
}
/**
* Inflate the in-track view for the action of the given MIME-type, collapsing duplicate values.
* Will use the icon provided by the {@link DataKind}.
*/
private View inflateAction(String mimeType, ResolveCache resolveCache, ViewGroup root) {
final CheckableImageView typeView = (CheckableImageView) getLayoutInflater().inflate(
R.layout.quickcontact_track_button, root, false);
List<Action> children = mActions.get(mimeType);
typeView.setTag(mimeType);
final Action firstInfo = children.get(0);
// Set icon and listen for clicks
final CharSequence descrip = resolveCache.getDescription(firstInfo);
final Drawable icon = resolveCache.getIcon(firstInfo);
typeView.setChecked(false);
typeView.setContentDescription(descrip);
typeView.setImageDrawable(icon);
typeView.setOnClickListener(mTypeViewClickListener);
return typeView;
}
private CheckableImageView getActionViewAt(int position) {
return (CheckableImageView) mTrack.getChildAt(position);
}
@Override
public void onAttachFragment(Fragment fragment) {
final QuickContactListFragment listFragment = (QuickContactListFragment) fragment;
listFragment.setListener(mListFragmentListener);
}
/** A type (e.g. Call/Addresses was clicked) */
private final OnClickListener mTypeViewClickListener = new OnClickListener() {
@Override
public void onClick(View view) {
final CheckableImageView actionView = (CheckableImageView)view;
final String mimeType = (String) actionView.getTag();
int index = mSortedActionMimeTypes.indexOf(mimeType);
mListPager.setCurrentItem(index, true);
}
};
private class ViewPagerAdapter extends FragmentPagerAdapter {
public ViewPagerAdapter(FragmentManager fragmentManager) {
super(fragmentManager);
}
@Override
public Fragment getItem(int position) {
QuickContactListFragment fragment = new QuickContactListFragment();
final String mimeType = mSortedActionMimeTypes.get(position);
final List<Action> actions = mActions.get(mimeType);
fragment.setActions(actions);
return fragment;
}
@Override
public int getCount() {
return mSortedActionMimeTypes.size();
}
}
private class PageChangeListener extends SimpleOnPageChangeListener {
@Override
public void onPageSelected(int position) {
final CheckableImageView actionView = getActionViewAt(position);
mTrackScroller.requestChildRectangleOnScreen(actionView,
new Rect(0, 0, actionView.getWidth(), actionView.getHeight()), false);
}
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
final RelativeLayout.LayoutParams layoutParams =
(RelativeLayout.LayoutParams) mSelectedTabRectangle.getLayoutParams();
final int width = mSelectedTabRectangle.getWidth();
layoutParams.leftMargin = (int) ((position + positionOffset) * width);
mSelectedTabRectangle.setLayoutParams(layoutParams);
}
}
private final QuickContactListFragment.Listener mListFragmentListener =
new QuickContactListFragment.Listener() {
@Override
public void onOutsideClick() {
// If there is no background, we want to dismiss, because to the user it seems
// like he had touched outside. If the ViewPager is solid however, those taps
// must be ignored
final boolean isTransparent = mListPager.getBackground() == null;
if (isTransparent) handleOutsideTouch();
}
@Override
public void onItemClicked(final Action action, final boolean alternate) {
final Runnable startAppRunnable = new Runnable() {
@Override
public void run() {
try {
startActivity(alternate ? action.getAlternateIntent() : action.getIntent());
} catch (ActivityNotFoundException e) {
Toast.makeText(QuickContactActivity.this, R.string.quickcontact_missing_app,
Toast.LENGTH_SHORT).show();
}
hide(false);
}
};
// Defer the action to make the window properly repaint
new Handler().post(startAppRunnable);
}
};
private interface DataQuery {
final String[] PROJECTION = new String[] {
Data._ID,
RawContacts.ACCOUNT_TYPE,
RawContacts.DATA_SET,
Contacts.STARRED,
Contacts.DISPLAY_NAME,
Data.STATUS,
Data.STATUS_RES_PACKAGE,
Data.STATUS_ICON,
Data.STATUS_LABEL,
Data.STATUS_TIMESTAMP,
Data.PRESENCE,
Data.CHAT_CAPABILITY,
Data.RES_PACKAGE,
Data.MIMETYPE,
Data.IS_PRIMARY,
Data.IS_SUPER_PRIMARY,
Data.RAW_CONTACT_ID,
Data.DATA1, Data.DATA2, Data.DATA3, Data.DATA4, Data.DATA5,
Data.DATA6, Data.DATA7, Data.DATA8, Data.DATA9, Data.DATA10, Data.DATA11,
Data.DATA12, Data.DATA13, Data.DATA14, Data.DATA15,
};
final int _ID = 0;
final int ACCOUNT_TYPE = 1;
final int DATA_SET = 2;
final int STARRED = 3;
final int DISPLAY_NAME = 4;
final int STATUS = 5;
final int STATUS_RES_PACKAGE = 6;
final int STATUS_ICON = 7;
final int STATUS_LABEL = 8;
final int STATUS_TIMESTAMP = 9;
final int PRESENCE = 10;
final int CHAT_CAPABILITY = 11;
final int RES_PACKAGE = 12;
final int MIMETYPE = 13;
final int IS_PRIMARY = 14;
final int IS_SUPER_PRIMARY = 15;
}
}