diff options
author | Mindy Pereira <mindyp@google.com> | 2011-12-13 10:12:27 -0800 |
---|---|---|
committer | Mindy Pereira <mindyp@google.com> | 2011-12-13 10:24:52 -0800 |
commit | 1a73ea8bc1b08ff4e7baac515ed54a15de13ffe2 (patch) | |
tree | ebb121487dcdce3625f9bd782aa07e9f4507e72c /src-mailcommon | |
parent | 0b2aaf43159dbb2d4627e687c21b226b6d61a5a1 (diff) | |
download | android_packages_apps_UnifiedEmail-1a73ea8bc1b08ff4e7baac515ed54a15de13ffe2.tar.gz android_packages_apps_UnifiedEmail-1a73ea8bc1b08ff4e7baac515ed54a15de13ffe2.tar.bz2 android_packages_apps_UnifiedEmail-1a73ea8bc1b08ff4e7baac515ed54a15de13ffe2.zip |
Move mailcommon code to our new unified code base.
Change-Id: I1cf861371a54bb9af97b1952af8e7a44c2856f5b
Diffstat (limited to 'src-mailcommon')
3 files changed, 728 insertions, 0 deletions
diff --git a/src-mailcommon/com/android/mailcommon/MergedAdapter.java b/src-mailcommon/com/android/mailcommon/MergedAdapter.java new file mode 100644 index 000000000..77f8327f2 --- /dev/null +++ b/src-mailcommon/com/android/mailcommon/MergedAdapter.java @@ -0,0 +1,193 @@ +/* + * Copyright (C) 2011 Google Inc. + * Licensed to 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.mailcommon; + +import android.database.DataSetObserver; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.ListAdapter; +import android.widget.SpinnerAdapter; + +import java.util.Arrays; +import java.util.List; + +/** + * An adapter that combines items from multiple provided adapters into a single list. + * + * @param <T> the class of each constituent adapter + */ +public class MergedAdapter<T extends MergedAdapter.ListSpinnerAdapter> extends BaseAdapter { + + private List<T> mAdapters; + private final DataSetObserver mObserver; + + /** + * A Mergeable adapter must implement both ListAdapter and SpinnerAdapter to be useful in lists + * and spinners. + */ + public interface ListSpinnerAdapter extends ListAdapter, SpinnerAdapter { + } + + public static class LocalAdapterPosition<T extends ListSpinnerAdapter> { + public final T mAdapter; + public final int mLocalPosition; + + public LocalAdapterPosition(T adapter, int offset) { + mAdapter = adapter; + mLocalPosition = offset; + } + } + + public MergedAdapter() { + mObserver = new DataSetObserver() { + @Override + public void onChanged() { + notifyDataSetChanged(); + } + }; + } + + public void setAdapters(T... adapters) { + if (mAdapters != null) { + for (T adapter : mAdapters) { + adapter.unregisterDataSetObserver(mObserver); + } + } + + mAdapters = Arrays.asList(adapters); + + for (T adapter : mAdapters) { + adapter.registerDataSetObserver(mObserver); + } + } + + public int getSubAdapterCount() { + return mAdapters.size(); + } + + public T getSubAdapter(int index) { + return mAdapters.get(index); + } + + @Override + public int getCount() { + int count = 0; + for (T adapter : mAdapters) { + count += adapter.getCount(); + } + return count; + // TODO: cache counts until next onChanged + } + + /** + * For a given merged position, find the corresponding Adapter and local position within that + * Adapter by iterating through Adapters and summing their counts until the merged position is + * found. + * + * @param position a merged (global) position + * @return the matching Adapter and local position, or null if not found + */ + public LocalAdapterPosition<T> getAdapterOffsetForItem(final int position) { + final int adapterCount = mAdapters.size(); + int i = 0; + int count = 0; + + while (i < adapterCount) { + T a = mAdapters.get(i); + int newCount = count + a.getCount(); + if (position < newCount) { + return new LocalAdapterPosition<T>(a, position - count); + } + count = newCount; + i++; + } + return null; + } + + @Override + public Object getItem(int position) { + LocalAdapterPosition<T> result = getAdapterOffsetForItem(position); + if (result == null) { + return null; + } + return result.mAdapter.getItem(result.mLocalPosition); + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public int getViewTypeCount() { + int count = 0; + for (T adapter : mAdapters) { + count += adapter.getViewTypeCount(); + } + return count; + } + + @Override + public int getItemViewType(int position) { + LocalAdapterPosition<T> result = getAdapterOffsetForItem(position); + int otherViewTypeCount = 0; + for (T adapter : mAdapters) { + if (adapter == result.mAdapter) { + break; + } + otherViewTypeCount += adapter.getViewTypeCount(); + } + int type = result.mAdapter.getItemViewType(result.mLocalPosition); + // Headers (negative types) are in a separate global namespace and their values should not + // be affected by preceding adapter view types. + if (type >= 0) { + type += otherViewTypeCount; + } + return type; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + LocalAdapterPosition<T> result = getAdapterOffsetForItem(position); + return result.mAdapter.getView(result.mLocalPosition, convertView, parent); + } + + @Override + public View getDropDownView(int position, View convertView, ViewGroup parent) { + LocalAdapterPosition<T> result = getAdapterOffsetForItem(position); + return result.mAdapter.getDropDownView(result.mLocalPosition, convertView, parent); + } + + @Override + public boolean areAllItemsEnabled() { + boolean enabled = true; + for (T adapter : mAdapters) { + enabled &= adapter.areAllItemsEnabled(); + } + return enabled; + } + + @Override + public boolean isEnabled(int position) { + LocalAdapterPosition<T> result = getAdapterOffsetForItem(position); + return result.mAdapter.isEnabled(result.mLocalPosition); + } + +}
\ No newline at end of file diff --git a/src-mailcommon/com/android/mailcommon/MultiAdapterSpinner.java b/src-mailcommon/com/android/mailcommon/MultiAdapterSpinner.java new file mode 100644 index 000000000..cd58f3ea3 --- /dev/null +++ b/src-mailcommon/com/android/mailcommon/MultiAdapterSpinner.java @@ -0,0 +1,186 @@ +/* + * Copyright (C) 2011 Google Inc. + * Licensed to 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.mailcommon; + +import com.android.mailcommon.MergedAdapter.ListSpinnerAdapter; +import com.android.mailcommon.MergedAdapter.LocalAdapterPosition; + +import android.content.Context; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.FrameLayout; +import android.widget.ListPopupWindow; +import android.widget.ListView; + + +/** + * <p>A spinner-like widget that combines data and views from multiple adapters (via MergedAdapter) + * and forwards certain events to those adapters. This widget also supports clickable but + * unselectable dropdown items, useful when displaying extra items that should not affect spinner + * selection state.</p> + * + * <p>The framework's Spinner widget can't be extended for this task because it uses a private list + * adapter (which prevents setting multiple item types) and hides access to its popup, which is + * useful for clients to know about (like when it's been opened).</p> + * + * <p>Clients must provide a set of adapters which the widget will use to load dropdown views, + * receive callbacks, and load the selected item's view.</p> + * + * <p>Apps incorporating this widget must declare a custom attribute: "dropDownWidth" under the + * "MultiAdapterSpinner" name as a "reference" format (see Gmail's attrs.xml file for an + * example). This attribute controls the width of the dropdown, similar to the attribute in the + * framework's Spinner widget.</p> + * + */ +public class MultiAdapterSpinner extends FrameLayout + implements AdapterView.OnItemClickListener, View.OnClickListener { + + protected MergedAdapter<FancySpinnerAdapter> mAdapter; + protected ListPopupWindow mPopup; + + private int mSelectedPosition = -1; + private Rect mTempRect = new Rect(); + + /** + * A basic adapter with some callbacks added so clients can be involved in spinner behavior. + */ + public interface FancySpinnerAdapter extends ListSpinnerAdapter { + /** + * Whether or not an item at position should become the new selected spinner item and change + * the spinner item view. + */ + boolean canSelect(int position); + /** + * Handle a click on an enabled item. + */ + void onClick(int position); + /** + * Fired when the popup window is about to be displayed. + */ + void onShowPopup(); + } + + private static class MergedSpinnerAdapter extends MergedAdapter<FancySpinnerAdapter> { + /** + * ListPopupWindow uses getView() but spinners return dropdown views in getDropDownView(). + */ + @Override + public View getView(int position, View convertView, ViewGroup parent) { + return super.getDropDownView(position, convertView, parent); + } + } + + public MultiAdapterSpinner(Context context) { + this(context, null); + } + + public MultiAdapterSpinner(Context context, AttributeSet attrs) { + super(context, attrs); + + mAdapter = new MergedSpinnerAdapter(); + mPopup = new ListPopupWindow(context, attrs); + mPopup.setAnchorView(this); + mPopup.setOnItemClickListener(this); + mPopup.setModal(true); + mPopup.setAdapter(mAdapter); + } + + public void setAdapters(FancySpinnerAdapter... adapters) { + mAdapter.setAdapters(adapters); + } + + public void setSelectedItem(final FancySpinnerAdapter adapter, final int position) { + int globalPosition = 0; + boolean found = false; + + for (int i = 0, count = mAdapter.getSubAdapterCount(); i < count; i++) { + ListSpinnerAdapter a = mAdapter.getSubAdapter(i); + if (a == adapter) { + globalPosition += position; + found = true; + break; + } + globalPosition += a.getCount(); + } + if (found) { + if (adapter.canSelect(position)) { + removeAllViews(); + View itemView = adapter.getView(position, null, this); + itemView.setClickable(true); + itemView.setOnClickListener(this); + addView(itemView); + + if (position < adapter.getCount()) { + mSelectedPosition = globalPosition; + } + } + } + } + + @Override + public void onClick(View v) { + if (!mPopup.isShowing()) { + + for (int i = 0, count = mAdapter.getSubAdapterCount(); i < count; i++) { + mAdapter.getSubAdapter(i).onShowPopup(); + } + + final int spinnerPaddingLeft = getPaddingLeft(); + final Drawable background = mPopup.getBackground(); + int bgOffset = 0; + if (background != null) { + background.getPadding(mTempRect); + bgOffset = -mTempRect.left; + } + mPopup.setHorizontalOffset(bgOffset + spinnerPaddingLeft); + mPopup.show(); + mPopup.getListView().setChoiceMode(ListView.CHOICE_MODE_SINGLE); + mPopup.setSelection(mSelectedPosition); + } + } + + @Override + public void onItemClick(AdapterView<?> parent, View view, int position, long id) { + + if (position != mSelectedPosition) { + final LocalAdapterPosition<FancySpinnerAdapter> result = + mAdapter.getAdapterOffsetForItem(position); + + if (result.mAdapter.canSelect(result.mLocalPosition)) { + mSelectedPosition = position; + } else { + mPopup.clearListSelection(); + } + + post(new Runnable() { + @Override + public void run() { + result.mAdapter.onClick(result.mLocalPosition); + } + }); + } + + mPopup.dismiss(); + } + +} diff --git a/src-mailcommon/com/android/mailcommon/WebViewContextMenu.java b/src-mailcommon/com/android/mailcommon/WebViewContextMenu.java new file mode 100644 index 000000000..0de87d60d --- /dev/null +++ b/src-mailcommon/com/android/mailcommon/WebViewContextMenu.java @@ -0,0 +1,349 @@ +/* + * Copyright (C) 2011 Google Inc. + * Licensed to 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.mailcommon; + +import android.app.Activity; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.net.Uri; +import android.provider.ContactsContract; +import android.view.ContextMenu; +import android.view.ContextMenu.ContextMenuInfo; +import android.view.LayoutInflater; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.View.OnCreateContextMenuListener; +import android.webkit.WebView; +import android.widget.TextView; + +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.nio.charset.Charset; + +/** + * <p>Handles display and behavior of the context menu for known actionable content in WebViews. + * Requires an Activity to bind to for Context resolution and to start other activites.</p> + * <br> + * <p>Activity/Fragment clients must forward the 'onContextItemSelected' method.</p> + * <br> + * Dependencies: + * <ul> + * <li>res/menu/webview_context_menu.xml</li> + * <li>res/values/webview_context_menu_strings.xml</li> + * </ul> + */ +public abstract class WebViewContextMenu implements OnCreateContextMenuListener, + MenuItem.OnMenuItemClickListener { + + private Activity mActivity; + + protected static enum MenuType { + OPEN_MENU, + COPY_LINK_MENU, + SHARE_LINK_MENU, + DIAL_MENU, + SMS_MENU, + ADD_CONTACT_MENU, + COPY_PHONE_MENU, + EMAIL_CONTACT_MENU, + COPY_MAIL_MENU, + MAP_MENU, + COPY_GEO_MENU, + } + + protected static enum MenuGroupType { + PHONE_GROUP, + EMAIL_GROUP, + GEO_GROUP, + ANCHOR_GROUP, + } + + public WebViewContextMenu(Activity host) { + this.mActivity = host; + } + + // For our copy menu items. + private class Copy implements MenuItem.OnMenuItemClickListener { + private final CharSequence mText; + + public Copy(CharSequence text) { + mText = text; + } + + @Override + public boolean onMenuItemClick(MenuItem item) { + copy(mText); + return true; + } + } + + // For our share menu items. + private class Share implements MenuItem.OnMenuItemClickListener { + private final String mUri; + + public Share(String text) { + mUri = text; + } + + @Override + public boolean onMenuItemClick(MenuItem item) { + shareLink(mUri); + return true; + } + } + + private boolean showShareLinkMenuItem() { + PackageManager pm = mActivity.getPackageManager(); + Intent send = new Intent(Intent.ACTION_SEND); + send.setType("text/plain"); + ResolveInfo ri = pm.resolveActivity(send, PackageManager.MATCH_DEFAULT_ONLY); + return ri != null; + } + + private void shareLink(String url) { + Intent send = new Intent(Intent.ACTION_SEND); + send.setType("text/plain"); + send.putExtra(Intent.EXTRA_TEXT, url); + + try { + mActivity.startActivity(Intent.createChooser(send, mActivity.getText( + getChooserTitleStringResIdForMenuType(MenuType.SHARE_LINK_MENU)))); + } catch(android.content.ActivityNotFoundException ex) { + // if no app handles it, do nothing + } + } + + private void copy(CharSequence text) { + ClipboardManager clipboard = + (ClipboardManager) mActivity.getSystemService(Context.CLIPBOARD_SERVICE); + clipboard.setPrimaryClip(ClipData.newPlainText(null, text)); + } + + public void onCreateContextMenu(ContextMenu menu, View v, + ContextMenuInfo info) { + // FIXME: This is copied over almost directly from BrowserActivity. + // Would like to find a way to combine the two (Bug 1251210). + + WebView webview = (WebView) v; + WebView.HitTestResult result = webview.getHitTestResult(); + if (result == null) { + return; + } + + int type = result.getType(); + switch (type) { + case WebView.HitTestResult.UNKNOWN_TYPE: + case WebView.HitTestResult.EDIT_TEXT_TYPE: + return; + default: + break; + } + + // Note, http://b/issue?id=1106666 is requesting that + // an inflated menu can be used again. This is not available + // yet, so inflate each time (yuk!) + MenuInflater inflater = mActivity.getMenuInflater(); + // Also, we are copying the menu file from browser until + // 1251210 is fixed. + inflater.inflate(getMenuResourceId(), menu); + + // Initially make set the menu item handler this WebViewContextMenu, which will default to + // calling the non-abstract subclass's implementation. + for (int i = 0; i < menu.size(); i++) { + final MenuItem menuItem = menu.getItem(i); + menuItem.setOnMenuItemClickListener(this); + } + + + // Show the correct menu group + String extra = result.getExtra(); + menu.setGroupVisible(getMenuGroupResId(MenuGroupType.PHONE_GROUP), + type == WebView.HitTestResult.PHONE_TYPE); + menu.setGroupVisible(getMenuGroupResId(MenuGroupType.EMAIL_GROUP), + type == WebView.HitTestResult.EMAIL_TYPE); + menu.setGroupVisible(getMenuGroupResId(MenuGroupType.GEO_GROUP), + type == WebView.HitTestResult.GEO_TYPE); + menu.setGroupVisible(getMenuGroupResId(MenuGroupType.ANCHOR_GROUP), + type == WebView.HitTestResult.SRC_ANCHOR_TYPE + || type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE); + + // Setup custom handling depending on the type + switch (type) { + case WebView.HitTestResult.PHONE_TYPE: + String decodedPhoneExtra; + try { + decodedPhoneExtra = URLDecoder.decode(extra, Charset.defaultCharset().name()); + } + catch (UnsupportedEncodingException ignore) { + // Should never happen; default charset is UTF-8 + decodedPhoneExtra = extra; + } + + menu.setHeaderTitle(decodedPhoneExtra); + // Dial + final MenuItem dialMenuItem = + menu.findItem(getMenuResIdForMenuType(MenuType.DIAL_MENU)); + // remove the on click listener + dialMenuItem.setOnMenuItemClickListener(null); + dialMenuItem.setIntent(new Intent(Intent.ACTION_VIEW, + Uri.parse(WebView.SCHEME_TEL + extra))); + + // Send SMS + final MenuItem sendSmsMenuItem = + menu.findItem(getMenuResIdForMenuType(MenuType.SMS_MENU)); + // remove the on click listener + sendSmsMenuItem.setOnMenuItemClickListener(null); + sendSmsMenuItem.setIntent(new Intent(Intent.ACTION_SENDTO, + Uri.parse("smsto:" + extra))); + + // Add to contacts + final Intent addIntent = new Intent(Intent.ACTION_INSERT_OR_EDIT); + addIntent.setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE); + + addIntent.putExtra(ContactsContract.Intents.Insert.PHONE, decodedPhoneExtra); + final MenuItem addToContactsMenuItem = + menu.findItem(getMenuResIdForMenuType(MenuType.ADD_CONTACT_MENU)); + // remove the on click listener + addToContactsMenuItem.setOnMenuItemClickListener(null); + addToContactsMenuItem.setIntent(addIntent); + + // Copy + menu.findItem(getMenuResIdForMenuType(MenuType.COPY_PHONE_MENU)). + setOnMenuItemClickListener(new Copy(extra)); + break; + + case WebView.HitTestResult.EMAIL_TYPE: + menu.setHeaderTitle(extra); + menu.findItem(getMenuResIdForMenuType(MenuType.EMAIL_CONTACT_MENU)).setIntent( + new Intent(Intent.ACTION_VIEW, Uri + .parse(WebView.SCHEME_MAILTO + extra))); + menu.findItem(getMenuResIdForMenuType(MenuType.COPY_MAIL_MENU)). + setOnMenuItemClickListener(new Copy(extra)); + break; + + case WebView.HitTestResult.GEO_TYPE: + menu.setHeaderTitle(extra); + String geoExtra = ""; + try { + geoExtra = URLEncoder.encode(extra, Charset.defaultCharset().name()); + } + catch (UnsupportedEncodingException ignore) { + // Should never happen; default charset is UTF-8 + } + final MenuItem viewMapMenuItem = + menu.findItem(getMenuResIdForMenuType(MenuType.MAP_MENU)); + // remove the on click listener + viewMapMenuItem.setOnMenuItemClickListener(null); + viewMapMenuItem.setIntent(new Intent(Intent.ACTION_VIEW, + Uri.parse(WebView.SCHEME_GEO + geoExtra))); + menu.findItem(getMenuResIdForMenuType(MenuType.COPY_GEO_MENU)). + setOnMenuItemClickListener(new Copy(extra)); + break; + + case WebView.HitTestResult.SRC_ANCHOR_TYPE: + case WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE: + // FIXME: Make this look like the normal menu header + // We cannot use the normal menu header because we need to + // edit the ContextMenu after it has been created. + final TextView titleView = (TextView) LayoutInflater.from(mActivity) + .inflate(getTitleViewLayoutResId(MenuType.SHARE_LINK_MENU), null); + menu.setHeaderView(titleView); + + menu.findItem(getMenuResIdForMenuType(MenuType.SHARE_LINK_MENU)).setVisible( + showShareLinkMenuItem()); + + // The documentation for WebView indicates that if the HitTestResult is + // SRC_ANCHOR_TYPE or the url would be specified in the extra. We don't need to + // call requestFocusNodeHref(). If we wanted to handle UNKNOWN HitTestResults, we + // would. With this knowledge, we can just set the title + titleView.setText(extra); + + menu.findItem(getMenuResIdForMenuType(MenuType.COPY_LINK_MENU)). + setOnMenuItemClickListener(new Copy(extra)); + + final MenuItem openLinkMenuItem = + menu.findItem(getMenuResIdForMenuType(MenuType.OPEN_MENU)); + // remove the on click listener + openLinkMenuItem.setOnMenuItemClickListener(null); + openLinkMenuItem.setIntent(new Intent(Intent.ACTION_VIEW, Uri.parse(extra))); + + menu.findItem(getMenuResIdForMenuType(MenuType.SHARE_LINK_MENU)). + setOnMenuItemClickListener(new Share(extra)); + break; + default: + break; + } + } + + @Override + public boolean onMenuItemClick(MenuItem item) { + return onMenuItemSelected(item); + } + + /** + * Returns the menu type from the given resource id + * @param menuResId resource id of the menu + * @return MenuType for the specified menu resource id + */ + abstract protected MenuType getMenuTypeFromResId(int menuResId); + + /** + * Returns the menu resource id for the specified menu type + * @param menuType type of the specified menu + * @return menu resource id + */ + abstract protected int getMenuResIdForMenuType(MenuType menuType); + + /** + * Returns the resource id of the string to be used when showing a chooser for a menu + * @param menuType type of the specified menu + * @return string resource id + */ + abstract protected int getChooserTitleStringResIdForMenuType(MenuType menuType); + + /** + * Returns the resource id of the layout to be used for the title of the specified menu + * @param menuType type of the specified menu + * @return layout resource id + */ + abstract protected int getTitleViewLayoutResId(MenuType menuType); + + /** + * Returns the menu group resource id for the specified menu group type. + * @param menuGroupType menu group type + * @return menu group resource id + */ + abstract protected int getMenuGroupResId(MenuGroupType menuGroupType); + + /** + * Returns the resource id for the web view context menu + */ + abstract protected int getMenuResourceId(); + + + /** + * Called when a menu item is not handled by the context menu. + */ + abstract protected boolean onMenuItemSelected(MenuItem menuItem); +} |