| /* |
| * 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; |
| |
| import com.android.contacts.NotifyingAsyncQueryHandler.QueryCompleteListener; |
| import com.android.contacts.SocialStreamActivity.Mapping; |
| import com.android.contacts.SocialStreamActivity.MappingCache; |
| import com.android.internal.policy.PolicyManager; |
| |
| import android.content.ActivityNotFoundException; |
| import android.content.ContentUris; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.res.Resources; |
| import android.database.Cursor; |
| import android.graphics.Bitmap; |
| import android.graphics.BitmapFactory; |
| import android.graphics.Color; |
| import android.graphics.Rect; |
| import android.net.Uri; |
| import android.provider.ContactsContract; |
| import android.provider.SocialContract; |
| import android.provider.Contacts.Phones; |
| import android.provider.ContactsContract.Aggregates; |
| import android.provider.ContactsContract.CommonDataKinds; |
| import android.provider.ContactsContract.Contacts; |
| import android.provider.ContactsContract.Data; |
| import android.provider.ContactsContract.Presence; |
| import android.provider.ContactsContract.CommonDataKinds.Email; |
| import android.provider.ContactsContract.CommonDataKinds.Phone; |
| import android.provider.ContactsContract.CommonDataKinds.Photo; |
| import android.provider.Im.PresenceColumns; |
| import android.provider.SocialContract.Activities; |
| import android.text.SpannableStringBuilder; |
| import android.text.format.DateUtils; |
| import android.text.style.CharacterStyle; |
| import android.text.style.ForegroundColorSpan; |
| import android.text.style.StyleSpan; |
| import android.util.Log; |
| import android.view.ContextThemeWrapper; |
| import android.view.Gravity; |
| import android.view.KeyEvent; |
| import android.view.LayoutInflater; |
| import android.view.Menu; |
| import android.view.MenuItem; |
| import android.view.MotionEvent; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.view.Window; |
| import android.view.WindowManager; |
| import android.view.View.OnClickListener; |
| import android.view.accessibility.AccessibilityEvent; |
| import android.widget.AbsListView; |
| import android.widget.AdapterView; |
| import android.widget.BaseAdapter; |
| import android.widget.HorizontalScrollView; |
| import android.widget.ImageView; |
| import android.widget.LinearLayout; |
| import android.widget.ListView; |
| import android.widget.RemoteViews; |
| import android.widget.TextView; |
| import android.widget.Toast; |
| |
| import java.util.Arrays; |
| import java.util.HashMap; |
| import java.util.LinkedList; |
| import java.util.Set; |
| |
| /** |
| * Window that shows fast-track contact details for a specific |
| * {@link Aggregates#_ID}. |
| */ |
| public class FastTrackWindow implements Window.Callback, QueryCompleteListener, OnClickListener, |
| AbsListView.OnItemClickListener { |
| private static final String TAG = "FastTrackWindow"; |
| |
| /** |
| * Interface used to allow the person showing a {@link FastTrackWindow} to |
| * know when the window has been dismissed. |
| */ |
| interface OnDismissListener { |
| public void onDismiss(FastTrackWindow dialog); |
| } |
| |
| final Context mContext; |
| final LayoutInflater mInflater; |
| final WindowManager mWindowManager; |
| Window mWindow; |
| View mDecor; |
| |
| private boolean mQuerying = false; |
| private boolean mShowing = false; |
| |
| /** Mapping cache from mime-type to icons and actions */ |
| private MappingCache mMappingCache; |
| |
| private NotifyingAsyncQueryHandler mHandler; |
| private OnDismissListener mDismissListener; |
| |
| private long mAggId; |
| private Rect mAnchor; |
| |
| private boolean mHasSummary = false; |
| private boolean mHasSocial = false; |
| private boolean mHasActions = false; |
| |
| private View mArrowUp; |
| private View mArrowDown; |
| |
| private ImageView mPhoto; |
| private ImageView mPresence; |
| private TextView mContent; |
| private TextView mPublished; |
| private HorizontalScrollView mTrackScroll; |
| private ViewGroup mTrack; |
| private ListView mResolveList; |
| |
| private String mDisplayName = null; |
| private String mSocialTitle = null; |
| |
| private SpannableStringBuilder mBuilder = new SpannableStringBuilder(); |
| private CharacterStyle mStyleBold = new StyleSpan(android.graphics.Typeface.BOLD); |
| private CharacterStyle mStyleBlack = new ForegroundColorSpan(Color.BLACK); |
| |
| /** |
| * Set of {@link ActionInfo} that are associated with the aggregate |
| * currently displayed by this fast-track window, represented as a map from |
| * {@link String} mimetype to {@link ActionList}. |
| */ |
| private ActionMap mActions = new ActionMap(); |
| |
| // TODO We should move this to someplace more general as it is needed in a few places in the app |
| // code. |
| /** |
| * Specific mime-type for {@link Phone#CONTENT_ITEM_TYPE} entries that |
| * distinguishes actions that should initiate a text message. |
| */ |
| public static final String MIME_SMS_ADDRESS = "vnd.android.cursor.item/sms-address"; |
| |
| /** |
| * Specific mime-types that should be bumped to the front of the fast-track. |
| * Other mime-types not appearing in this list follow in alphabetic order. |
| */ |
| private static final String[] ORDERED_MIMETYPES = new String[] { |
| Phones.CONTENT_ITEM_TYPE, |
| Aggregates.CONTENT_ITEM_TYPE, |
| MIME_SMS_ADDRESS, |
| Email.CONTENT_ITEM_TYPE, |
| }; |
| |
| private static final boolean INCLUDE_PROFILE_ACTION = true; |
| |
| private static final int TOKEN_SUMMARY = 1; |
| private static final int TOKEN_SOCIAL = 2; |
| private static final int TOKEN_DATA = 3; |
| |
| /** Message to show when no activity is found to perform an action */ |
| // TODO: move this value into a resources string |
| private static final String NOT_FOUND = "Couldn't find an app to handle this action"; |
| |
| /** |
| * Prepare a fast-track window to show in the given {@link Context}. |
| */ |
| public FastTrackWindow(Context context) { |
| mContext = new ContextThemeWrapper(context, R.style.FastTrack); |
| mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); |
| mWindowManager = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE); |
| |
| mWindow = PolicyManager.makeNewWindow(mContext); |
| mWindow.setCallback(this); |
| mWindow.setWindowManager(mWindowManager, null, null); |
| |
| mWindow.setContentView(R.layout.fasttrack); |
| |
| mArrowUp = mWindow.findViewById(R.id.arrow_up); |
| mArrowDown = mWindow.findViewById(R.id.arrow_down); |
| |
| mPhoto = (ImageView)mWindow.findViewById(R.id.photo); |
| mPresence = (ImageView)mWindow.findViewById(R.id.presence); |
| mContent = (TextView)mWindow.findViewById(R.id.content); |
| mPublished = (TextView)mWindow.findViewById(R.id.published); |
| mTrack = (ViewGroup)mWindow.findViewById(R.id.fasttrack); |
| mTrackScroll = (HorizontalScrollView)mWindow.findViewById(R.id.scroll); |
| mResolveList = (ListView)mWindow.findViewById(android.R.id.list); |
| |
| // TODO: move generation of mime-type cache to more-efficient place |
| generateMappingCache(); |
| |
| } |
| |
| /** |
| * Prepare a fast-track window to show in the given {@link Context}, and |
| * notify the given {@link OnDismissListener} each time this dialog is |
| * dismissed. |
| */ |
| public FastTrackWindow(Context context, OnDismissListener dismissListener) { |
| this(context); |
| mDismissListener = dismissListener; |
| } |
| |
| /** |
| * Generate {@link MappingCache} specifically for fast-track windows. This |
| * cache knows how to display {@link CommonDataKinds#PACKAGE_COMMON} data |
| * types using generic icons. |
| */ |
| private void generateMappingCache() { |
| mMappingCache = MappingCache.createAndFill(mContext); |
| |
| Resources res = mContext.getResources(); |
| Mapping mapping; |
| |
| mapping = new Mapping(CommonDataKinds.PACKAGE_COMMON, Aggregates.CONTENT_ITEM_TYPE); |
| mapping.icon = BitmapFactory.decodeResource(res, R.drawable.ic_contacts_details); |
| mMappingCache.addMapping(mapping); |
| |
| mapping = new Mapping(CommonDataKinds.PACKAGE_COMMON, Phone.CONTENT_ITEM_TYPE); |
| mapping.summaryColumn = Phone.TYPE; |
| mapping.detailColumn = Phone.NUMBER; |
| mapping.icon = BitmapFactory.decodeResource(res, android.R.drawable.sym_action_call); |
| mMappingCache.addMapping(mapping); |
| |
| mapping = new Mapping(CommonDataKinds.PACKAGE_COMMON, MIME_SMS_ADDRESS); |
| mapping.summaryColumn = Phone.TYPE; |
| mapping.detailColumn = Phone.NUMBER; |
| mapping.icon = BitmapFactory.decodeResource(res, R.drawable.sym_action_sms); |
| mMappingCache.addMapping(mapping); |
| |
| mapping = new Mapping(CommonDataKinds.PACKAGE_COMMON, Email.CONTENT_ITEM_TYPE); |
| mapping.summaryColumn = Email.TYPE; |
| mapping.detailColumn = Email.DATA; |
| mapping.icon = BitmapFactory.decodeResource(res, android.R.drawable.sym_action_email); |
| mMappingCache.addMapping(mapping); |
| |
| } |
| |
| /** |
| * Start showing a fast-track window for the given {@link Aggregate#_ID} |
| * pointing towards the given location. |
| */ |
| public void show(Uri aggUri, Rect anchor) { |
| if (mShowing || mQuerying) { |
| Log.w(TAG, "already in process of showing"); |
| return; |
| } |
| |
| mAggId = ContentUris.parseId(aggUri); |
| mAnchor = new Rect(anchor); |
| mQuerying = true; |
| |
| Uri aggSummary = ContentUris.withAppendedId( |
| ContactsContract.Aggregates.CONTENT_SUMMARY_URI, mAggId); |
| Uri aggSocial = ContentUris.withAppendedId( |
| SocialContract.Activities.CONTENT_AGGREGATE_STATUS_URI, mAggId); |
| Uri aggData = Uri.withAppendedPath(aggUri, |
| ContactsContract.Aggregates.Data.CONTENT_DIRECTORY); |
| |
| // Start data query in background |
| mHandler = new NotifyingAsyncQueryHandler(mContext, this); |
| mHandler.startQuery(TOKEN_SUMMARY, null, aggSummary, null, null, null, null); |
| mHandler.startQuery(TOKEN_SOCIAL, null, aggSocial, null, null, null, null); |
| mHandler.startQuery(TOKEN_DATA, null, aggData, null, null, null, null); |
| |
| } |
| |
| /** |
| * Show the correct callout arrow based on a {@link R.id} reference. |
| */ |
| private void showArrow(int whichArrow, int requestedX) { |
| final View showArrow = (whichArrow == R.id.arrow_up) ? mArrowUp : mArrowDown; |
| final View hideArrow = (whichArrow == R.id.arrow_up) ? mArrowDown : mArrowUp; |
| |
| final int arrowWidth = mArrowUp.getMeasuredWidth(); |
| |
| showArrow.setVisibility(View.VISIBLE); |
| LinearLayout.LayoutParams param = (LinearLayout.LayoutParams)showArrow.getLayoutParams(); |
| param.leftMargin = requestedX - arrowWidth / 2; |
| |
| hideArrow.setVisibility(View.INVISIBLE); |
| } |
| |
| /** |
| * Actual internal method to show this fast-track window. Called only by |
| * {@link #considerShowing()} when all data requirements have been met. |
| */ |
| private void showInternal() { |
| mDecor = mWindow.getDecorView(); |
| WindowManager.LayoutParams l = mWindow.getAttributes(); |
| |
| l.width = WindowManager.LayoutParams.FILL_PARENT; |
| l.height = WindowManager.LayoutParams.WRAP_CONTENT; |
| |
| // Force layout measuring pass so we have baseline numbers |
| mDecor.measure(l.width, l.height); |
| |
| final int blockHeight = mDecor.getMeasuredHeight(); |
| final int arrowHeight = mArrowUp.getHeight(); |
| |
| l.gravity = Gravity.TOP | Gravity.LEFT; |
| l.x = 0; |
| |
| if (mAnchor.top > blockHeight) { |
| // Show downwards callout when enough room, aligning bottom block |
| // edge with top of anchor area, and adjusting to inset arrow. |
| showArrow(R.id.arrow_down, mAnchor.centerX()); |
| l.y = mAnchor.top - blockHeight + arrowHeight; |
| |
| } else { |
| // Otherwise show upwards callout, aligning block top with bottom of |
| // anchor area, and adjusting to inset arrow. |
| showArrow(R.id.arrow_up, mAnchor.centerX()); |
| l.y = mAnchor.bottom - arrowHeight; |
| |
| } |
| |
| l.dimAmount = 0.6f; |
| l.flags = WindowManager.LayoutParams.FLAG_DIM_BEHIND |
| | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH |
| | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL |
| | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN |
| | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR; |
| |
| mWindowManager.addView(mDecor, l); |
| mShowing = true; |
| mQuerying = false; |
| } |
| |
| /** |
| * Dismiss this fast-track window if showing. |
| */ |
| public void dismiss() { |
| if (!mQuerying && !mShowing) { |
| Log.d(TAG, "not visible, ignore"); |
| return; |
| } |
| |
| // Cancel any pending queries |
| mHandler.cancelOperation(TOKEN_SUMMARY); |
| mHandler.cancelOperation(TOKEN_SOCIAL); |
| mHandler.cancelOperation(TOKEN_DATA); |
| |
| // Reset all views to prepare for possible recycling |
| mPhoto.setImageResource(R.drawable.ic_contact_list_picture); |
| mPresence.setImageDrawable(null); |
| mPublished.setText(null); |
| mContent.setText(null); |
| |
| mDisplayName = null; |
| mSocialTitle = null; |
| |
| // Clear track actions and scroll to hard left |
| mActions.clear(); |
| mTrack.removeAllViews(); |
| mTrackScroll.fullScroll(View.FOCUS_LEFT); |
| mWasDownArrow = false; |
| |
| showResolveList(View.GONE); |
| |
| mHasSocial = false; |
| mHasActions = false; |
| |
| if (mDecor == null || !mShowing) { |
| Log.d(TAG, "not showing, ignore"); |
| return; |
| } |
| |
| mWindowManager.removeView(mDecor); |
| mDecor = null; |
| mWindow.closeAllPanels(); |
| mShowing = false; |
| |
| // Notify any listeners that we've been dismissed |
| if (mDismissListener != null) { |
| mDismissListener.onDismiss(this); |
| } |
| } |
| |
| /** |
| * Returns true if this fast-track window is showing or querying. |
| */ |
| public boolean isShowing() { |
| return mShowing || mQuerying; |
| } |
| |
| /** |
| * Consider showing this window, which will only call through to |
| * {@link #showInternal()} when all data items are present. |
| */ |
| private synchronized void considerShowing() { |
| if (mHasSummary && mHasSocial && mHasActions && !mShowing) { |
| // Now that all queries have been finished, build summary string. |
| mBuilder.clear(); |
| mBuilder.append(mDisplayName); |
| mBuilder.append(" "); |
| mBuilder.append(mSocialTitle); |
| mBuilder.setSpan(mStyleBold, 0, mDisplayName.length(), 0); |
| mBuilder.setSpan(mStyleBlack, 0, mDisplayName.length(), 0); |
| mContent.setText(mBuilder); |
| |
| showInternal(); |
| } |
| } |
| |
| /** {@inheritDoc} */ |
| public void onQueryComplete(int token, Object cookie, Cursor cursor) { |
| if (cursor == null) { |
| return; |
| } else if (token == TOKEN_SUMMARY) { |
| handleSummary(cursor); |
| } else if (token == TOKEN_SOCIAL) { |
| handleSocial(cursor); |
| } else if (token == TOKEN_DATA) { |
| handleData(cursor); |
| } |
| } |
| |
| /** |
| * Handle the result from the {@link TOKEN_SUMMARY} query. |
| */ |
| private void handleSummary(Cursor cursor) { |
| final int colDisplayName = cursor.getColumnIndex(Aggregates.DISPLAY_NAME); |
| final int colStatus = cursor.getColumnIndex(PresenceColumns.PRESENCE_STATUS); |
| |
| if (cursor.moveToNext()) { |
| mDisplayName = cursor.getString(colDisplayName); |
| |
| int status = cursor.getInt(colStatus); |
| int statusRes = Presence.getPresenceIconResourceId(status); |
| mPresence.setImageResource(statusRes); |
| } |
| |
| mHasSummary = true; |
| considerShowing(); |
| } |
| |
| /** |
| * Handle the result from the {@link TOKEN_SOCIAL} query. |
| */ |
| private void handleSocial(Cursor cursor) { |
| final int colTitle = cursor.getColumnIndex(Activities.TITLE); |
| final int colPublished = cursor.getColumnIndex(Activities.PUBLISHED); |
| |
| if (cursor.moveToNext()) { |
| mSocialTitle = cursor.getString(colTitle); |
| |
| long published = cursor.getLong(colPublished); |
| CharSequence relativePublished = DateUtils.getRelativeTimeSpanString(published, |
| System.currentTimeMillis(), DateUtils.MINUTE_IN_MILLIS); |
| mPublished.setText(relativePublished); |
| |
| } |
| |
| mHasSocial = true; |
| considerShowing(); |
| } |
| |
| /** |
| * Description of a specific, actionable {@link Data#_ID} item. May have a |
| * {@link Mapping} associated with it to find {@link RemoteViews} or icon, |
| * and may have built a summary of itself for UI display. |
| */ |
| private class ActionInfo { |
| long dataId; |
| String packageName; |
| String mimeType; |
| |
| Mapping mapping; |
| CharSequence summaryValue; |
| String detailValue; |
| |
| /** |
| * Create an action from common {@link Data} elements. |
| */ |
| public ActionInfo(long dataId, String packageName, String mimeType) { |
| this.dataId = dataId; |
| this.packageName = packageName; |
| this.mimeType = mimeType; |
| } |
| |
| /** |
| * Attempt to find a {@link Mapping} for the package and mime-type |
| * defined by this action. Returns true if one was found. |
| */ |
| public boolean findMapping(MappingCache cache) { |
| mapping = cache.findMapping(packageName, mimeType); |
| return (mapping != null); |
| } |
| |
| /** |
| * Given a {@link Cursor} pointed at the {@link Data} row associated |
| * with this action, use the {@link Mapping} to build a text summary. |
| */ |
| public void buildDetails(Cursor cursor) { |
| if (mapping == null) return; |
| |
| // Try finding common display label for this item, otherwise fall |
| // back to reading from defined summary column. |
| summaryValue = ContactsUtils.getDisplayLabel(mContext, mimeType, cursor); |
| if (summaryValue == null && mapping.summaryColumn != null) { |
| summaryValue = cursor.getString(cursor.getColumnIndex(mapping.summaryColumn)); |
| } |
| |
| // Read detailed value, if mapping was defined |
| if (mapping.detailColumn != null) { |
| detailValue = cursor.getString(cursor.getColumnIndex(mapping.detailColumn)); |
| } |
| } |
| |
| /** |
| * Build an {@link Intent} that will perform this action. |
| */ |
| public Intent buildIntent() { |
| // Handle well-known mime-types with special care |
| if (CommonDataKinds.Phone.CONTENT_ITEM_TYPE.equals(mimeType)) { |
| Uri callUri = Uri.parse("tel:" + Uri.encode(detailValue)); |
| return new Intent(Intent.ACTION_DIAL, callUri); |
| |
| } else if (MIME_SMS_ADDRESS.equals(mimeType)) { |
| Uri smsUri = Uri.fromParts("smsto", detailValue, null); |
| return new Intent(Intent.ACTION_SENDTO, smsUri); |
| |
| } else if (CommonDataKinds.Email.CONTENT_ITEM_TYPE.equals(mimeType)) { |
| Uri mailUri = Uri.fromParts("mailto", detailValue, null); |
| return new Intent(Intent.ACTION_SENDTO, mailUri); |
| |
| } |
| |
| // Otherwise fall back to default VIEW action |
| Uri dataUri = ContentUris.withAppendedId(ContactsContract.Data.CONTENT_URI, dataId); |
| |
| Intent intent = new Intent(Intent.ACTION_VIEW); |
| intent.setData(dataUri); |
| |
| return intent; |
| } |
| } |
| |
| /** |
| * Provide a strongly-typed {@link LinkedList} that holds a list of |
| * {@link ActionInfo} objects. |
| */ |
| private class ActionList extends LinkedList<ActionInfo> { |
| } |
| |
| /** |
| * Provide a simple way of collecting one or more {@link ActionInfo} objects |
| * under a mime-type key. |
| */ |
| private class ActionMap extends HashMap<String, ActionList> { |
| private void collect(String mimeType, ActionInfo info) { |
| // Create list for this mimetype when needed |
| ActionList collectList = get(mimeType); |
| if (collectList == null) { |
| collectList = new ActionList(); |
| put(mimeType, collectList); |
| } |
| collectList.add(info); |
| } |
| } |
| |
| /** |
| * Handle the result from the {@link TOKEN_DATA} query. |
| */ |
| private void handleData(Cursor cursor) { |
| final int colId = cursor.getColumnIndex(Data._ID); |
| final int colPackage = cursor.getColumnIndex(Contacts.PACKAGE); |
| final int colMimeType = cursor.getColumnIndex(Data.MIMETYPE); |
| final int colPhoto = cursor.getColumnIndex(Photo.PHOTO); |
| |
| ActionInfo info; |
| |
| // Add the profile shortcut action if requested |
| if (INCLUDE_PROFILE_ACTION) { |
| final String mimeType = Aggregates.CONTENT_ITEM_TYPE; |
| info = new ActionInfo(mAggId, CommonDataKinds.PACKAGE_COMMON, mimeType); |
| if (info.findMapping(mMappingCache)) { |
| mActions.collect(mimeType, info); |
| } |
| } |
| |
| while (cursor.moveToNext()) { |
| final long dataId = cursor.getLong(colId); |
| final String packageName = cursor.getString(colPackage); |
| final String mimeType = cursor.getString(colMimeType); |
| |
| // Handle when a photo appears in the various data items |
| // TODO: accept a photo only if its marked as primary |
| if (Photo.CONTENT_ITEM_TYPE.equals(mimeType)) { |
| byte[] photoBlob = cursor.getBlob(colPhoto); |
| Bitmap photoBitmap = BitmapFactory.decodeByteArray(photoBlob, 0, photoBlob.length); |
| mPhoto.setImageBitmap(photoBitmap); |
| continue; |
| } |
| |
| // 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. |
| info = new ActionInfo(dataId, packageName, mimeType); |
| if (info.findMapping(mMappingCache)) { |
| info.buildDetails(cursor); |
| mActions.collect(info.mimeType, info); |
| } |
| |
| // If phone number, also insert as text message action |
| if (Phones.CONTENT_ITEM_TYPE.equals(mimeType)) { |
| info = new ActionInfo(dataId, packageName, MIME_SMS_ADDRESS); |
| if (info.findMapping(mMappingCache)) { |
| info.buildDetails(cursor); |
| mActions.collect(info.mimeType, info); |
| } |
| } |
| } |
| |
| cursor.close(); |
| |
| // Turn our list of actions into UI elements, starting with common types |
| Set<String> containedTypes = mActions.keySet(); |
| for (String mimeType : ORDERED_MIMETYPES) { |
| if (containedTypes.contains(mimeType)) { |
| mTrack.addView(inflateAction(mimeType)); |
| containedTypes.remove(mimeType); |
| } |
| } |
| |
| // Then continue with remaining mime-types in alphabetical order |
| String[] remainingTypes = containedTypes.toArray(new String[containedTypes.size()]); |
| Arrays.sort(remainingTypes); |
| for (String mimeType : remainingTypes) { |
| mTrack.addView(inflateAction(mimeType)); |
| } |
| |
| mHasActions = true; |
| considerShowing(); |
| } |
| |
| /** |
| * Inflate the in-track view for the action of the given mime-type. Will use |
| * the icon provided by the {@link Mapping}. |
| */ |
| private View inflateAction(String mimeType) { |
| ImageView view = (ImageView)mInflater.inflate(R.layout.fasttrack_item, mTrack, false); |
| |
| // Add direct intent if single child, otherwise flag for multiple |
| ActionList children = mActions.get(mimeType); |
| ActionInfo firstInfo = children.get(0); |
| if (children.size() == 1) { |
| view.setTag(firstInfo.buildIntent()); |
| } else { |
| view.setTag(children); |
| } |
| |
| // Set icon and listen for clicks |
| view.setImageBitmap(firstInfo.mapping.icon); |
| view.setOnClickListener(this); |
| return view; |
| } |
| |
| /** {@inheritDoc} */ |
| public void onItemClick(AdapterView<?> parent, View view, int position, long id) { |
| // Pass list item clicks along so that Intents are handled uniformly |
| onClick(view); |
| } |
| |
| /** |
| * Flag indicating if {@link #mArrowDown} was visible during the last call |
| * to {@link #showResolveList(int)}. Used to decide during a later call if |
| * the arrow should be restored. |
| */ |
| private boolean mWasDownArrow = false; |
| |
| /** |
| * Helper for showing and hiding {@link #mResolveList}, which will correctly |
| * manage {@link #mArrowDown} as needed. |
| */ |
| private void showResolveList(int visibility) { |
| // Show or hide the resolve list if needed |
| if (mResolveList.getVisibility() != visibility) { |
| mResolveList.setVisibility(visibility); |
| } |
| |
| if (visibility == View.VISIBLE) { |
| // If showing list, then hide and save state of down arrow |
| mWasDownArrow = mWasDownArrow || (mArrowDown.getVisibility() == View.VISIBLE); |
| mArrowDown.setVisibility(View.INVISIBLE); |
| } else { |
| // If hiding list, restore any down arrow state |
| mArrowDown.setVisibility(mWasDownArrow ? View.VISIBLE : View.INVISIBLE); |
| } |
| |
| } |
| |
| /** {@inheritDoc} */ |
| public void onClick(View v) { |
| final Object tag = v.getTag(); |
| if (tag instanceof Intent) { |
| // Hide the resolution list, if present |
| showResolveList(View.GONE); |
| |
| // Incoming tag is concrete intent, so launch |
| try { |
| mContext.startActivity((Intent)tag); |
| } catch (ActivityNotFoundException e) { |
| Log.w(TAG, NOT_FOUND); |
| Toast.makeText(mContext, NOT_FOUND, Toast.LENGTH_SHORT).show(); |
| } |
| } else if (tag instanceof ActionList) { |
| // Incoming tag is a mime-type, so show resolution list |
| final ActionList children = (ActionList)tag; |
| |
| // Show resolution list and set adapter |
| showResolveList(View.VISIBLE); |
| |
| mResolveList.setOnItemClickListener(this); |
| mResolveList.setAdapter(new BaseAdapter() { |
| public int getCount() { |
| return children.size(); |
| } |
| |
| public Object getItem(int position) { |
| return children.get(position); |
| } |
| |
| public long getItemId(int position) { |
| return position; |
| } |
| |
| public View getView(int position, View convertView, ViewGroup parent) { |
| if (convertView == null) { |
| convertView = mInflater.inflate(R.layout.fasttrack_resolve_item, parent, false); |
| } |
| |
| // Set action title based on summary value |
| ActionInfo info = (ActionInfo)getItem(position); |
| |
| ImageView icon1 = (ImageView)convertView.findViewById(android.R.id.icon1); |
| TextView text1 = (TextView)convertView.findViewById(android.R.id.text1); |
| TextView text2 = (TextView)convertView.findViewById(android.R.id.text2); |
| |
| icon1.setImageBitmap(info.mapping.icon); |
| text1.setText(info.summaryValue); |
| text2.setText(info.detailValue); |
| |
| convertView.setTag(info.buildIntent()); |
| return convertView; |
| } |
| }); |
| |
| // Make sure we resize to make room for ListView |
| onWindowAttributesChanged(mWindow.getAttributes()); |
| |
| } |
| } |
| |
| /** {@inheritDoc} */ |
| public boolean dispatchKeyEvent(KeyEvent event) { |
| if (event.getAction() == KeyEvent.ACTION_DOWN |
| && event.getKeyCode() == KeyEvent.KEYCODE_BACK) { |
| // Back key will first dismiss any expanded resolve list, otherwise |
| // it will close the entire dialog. |
| if (mResolveList.getVisibility() == View.VISIBLE) { |
| showResolveList(View.GONE); |
| } else { |
| dismiss(); |
| } |
| return true; |
| } |
| return mWindow.superDispatchKeyEvent(event); |
| } |
| |
| /** {@inheritDoc} */ |
| public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { |
| // TODO: make this window accessible |
| return false; |
| } |
| |
| /** {@inheritDoc} */ |
| public boolean dispatchTouchEvent(MotionEvent event) { |
| if (event.getAction() == MotionEvent.ACTION_OUTSIDE) { |
| dismiss(); |
| return true; |
| } |
| return mWindow.superDispatchTouchEvent(event); |
| } |
| |
| /** {@inheritDoc} */ |
| public boolean dispatchTrackballEvent(MotionEvent event) { |
| return mWindow.superDispatchTrackballEvent(event); |
| } |
| |
| /** {@inheritDoc} */ |
| public void onContentChanged() { |
| } |
| |
| /** {@inheritDoc} */ |
| public boolean onCreatePanelMenu(int featureId, Menu menu) { |
| return false; |
| } |
| |
| /** {@inheritDoc} */ |
| public View onCreatePanelView(int featureId) { |
| return null; |
| } |
| |
| /** {@inheritDoc} */ |
| public boolean onMenuItemSelected(int featureId, MenuItem item) { |
| return false; |
| } |
| |
| /** {@inheritDoc} */ |
| public boolean onMenuOpened(int featureId, Menu menu) { |
| return false; |
| } |
| |
| /** {@inheritDoc} */ |
| public void onPanelClosed(int featureId, Menu menu) { |
| } |
| |
| /** {@inheritDoc} */ |
| public boolean onPreparePanel(int featureId, View view, Menu menu) { |
| return false; |
| } |
| |
| /** {@inheritDoc} */ |
| public boolean onSearchRequested() { |
| return false; |
| } |
| |
| /** {@inheritDoc} */ |
| public void onWindowAttributesChanged(android.view.WindowManager.LayoutParams attrs) { |
| if (mDecor != null) { |
| mWindowManager.updateViewLayout(mDecor, attrs); |
| } |
| } |
| |
| /** {@inheritDoc} */ |
| public void onWindowFocusChanged(boolean hasFocus) { |
| } |
| } |