summaryrefslogtreecommitdiffstats
path: root/mobile/android/base/java/org/mozilla/gecko/widget/GeckoActionProvider.java
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/base/java/org/mozilla/gecko/widget/GeckoActionProvider.java')
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/widget/GeckoActionProvider.java360
1 files changed, 360 insertions, 0 deletions
diff --git a/mobile/android/base/java/org/mozilla/gecko/widget/GeckoActionProvider.java b/mobile/android/base/java/org/mozilla/gecko/widget/GeckoActionProvider.java
new file mode 100644
index 000000000..d864792a6
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/widget/GeckoActionProvider.java
@@ -0,0 +1,360 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.widget;
+
+import android.app.Activity;
+import android.net.Uri;
+import android.support.design.widget.Snackbar;
+import android.util.Base64;
+import android.view.Menu;
+
+import org.mozilla.gecko.GeckoApp;
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.SnackbarBuilder;
+import org.mozilla.gecko.Telemetry;
+import org.mozilla.gecko.TelemetryContract;
+import org.mozilla.gecko.overlays.ui.ShareDialog;
+import org.mozilla.gecko.menu.MenuItemSwitcherLayout;
+import org.mozilla.gecko.util.IOUtils;
+import org.mozilla.gecko.util.IntentUtils;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.view.MenuItem;
+import android.view.MenuItem.OnMenuItemClickListener;
+import android.view.SubMenu;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.text.TextUtils;
+import android.webkit.MimeTypeMap;
+import android.webkit.URLUtil;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.HashMap;
+
+public class GeckoActionProvider {
+ private static final int MAX_HISTORY_SIZE_DEFAULT = 2;
+
+ /**
+ * A listener to know when a target was selected.
+ * When setting a provider, the activity can listen to this,
+ * to close the menu.
+ */
+ public interface OnTargetSelectedListener {
+ public void onTargetSelected();
+ }
+
+ final Context mContext;
+
+ public final static String DEFAULT_MIME_TYPE = "text/plain";
+
+ public static final String DEFAULT_HISTORY_FILE_NAME = "history.xml";
+
+ // History file.
+ String mHistoryFileName = DEFAULT_HISTORY_FILE_NAME;
+
+ OnTargetSelectedListener mOnTargetListener;
+
+ private final Callbacks mCallbacks = new Callbacks();
+
+ private static final HashMap<String, GeckoActionProvider> mProviders = new HashMap<String, GeckoActionProvider>();
+
+ private static String getFilenameFromMimeType(String mimeType) {
+ String[] mime = mimeType.split("/");
+
+ // All text mimetypes use the default provider
+ if ("text".equals(mime[0])) {
+ return DEFAULT_HISTORY_FILE_NAME;
+ }
+
+ return "history-" + mime[0] + ".xml";
+ }
+
+ // Gets the action provider for a particular mimetype
+ public static GeckoActionProvider getForType(String mimeType, Context context) {
+ if (!mProviders.keySet().contains(mimeType)) {
+ GeckoActionProvider provider = new GeckoActionProvider(context);
+
+ // For empty types, we just return a default provider
+ if (TextUtils.isEmpty(mimeType)) {
+ return provider;
+ }
+
+ provider.setHistoryFileName(getFilenameFromMimeType(mimeType));
+ mProviders.put(mimeType, provider);
+ }
+ return mProviders.get(mimeType);
+ }
+
+ public GeckoActionProvider(Context context) {
+ mContext = context;
+ }
+
+ /**
+ * Creates the action view using the default history size.
+ */
+ public View onCreateActionView(final ActionViewType viewType) {
+ return onCreateActionView(MAX_HISTORY_SIZE_DEFAULT, viewType);
+ }
+
+ public View onCreateActionView(final int maxHistorySize, final ActionViewType viewType) {
+ // Create the view and set its data model.
+ ActivityChooserModel dataModel = ActivityChooserModel.get(mContext, mHistoryFileName);
+ final MenuItemSwitcherLayout view;
+ switch (viewType) {
+ case DEFAULT:
+ view = new MenuItemSwitcherLayout(mContext, null);
+ break;
+
+ case CONTEXT_MENU:
+ view = new MenuItemSwitcherLayout(mContext, null);
+ view.initContextMenuStyles();
+ break;
+
+ default:
+ throw new IllegalArgumentException(
+ "Unknown " + ActionViewType.class.getSimpleName() + ": " + viewType);
+ }
+ view.addActionButtonClickListener(mCallbacks);
+
+ final PackageManager packageManager = mContext.getPackageManager();
+ int historySize = dataModel.getDistinctActivityCountInHistory();
+ if (historySize > maxHistorySize) {
+ historySize = maxHistorySize;
+ }
+
+ // Historical data is dependent on past selection of activities.
+ // Activity count is determined by the number of activities that can handle
+ // the particular intent. When no intent is set, the activity count is 0,
+ // while the history count can be a valid number.
+ if (historySize > dataModel.getActivityCount()) {
+ return view;
+ }
+
+ for (int i = 0; i < historySize; i++) {
+ view.addActionButton(dataModel.getActivity(i).loadIcon(packageManager),
+ dataModel.getActivity(i).loadLabel(packageManager));
+ }
+
+ return view;
+ }
+
+ public boolean hasSubMenu() {
+ return true;
+ }
+
+ public void onPrepareSubMenu(SubMenu subMenu) {
+ // Clear since the order of items may change.
+ subMenu.clear();
+
+ ActivityChooserModel dataModel = ActivityChooserModel.get(mContext, mHistoryFileName);
+ PackageManager packageManager = mContext.getPackageManager();
+
+ // Populate the sub-menu with a sub set of the activities.
+ final String shareDialogClassName = ShareDialog.class.getCanonicalName();
+ final String sendTabLabel = mContext.getResources().getString(R.string.overlay_share_send_other);
+ final int count = dataModel.getActivityCount();
+ for (int i = 0; i < count; i++) {
+ ResolveInfo activity = dataModel.getActivity(i);
+ final CharSequence activityLabel = activity.loadLabel(packageManager);
+
+ // Pin internal actions to the top. Note:
+ // the order here does not affect quick share.
+ final int order;
+ if (shareDialogClassName.equals(activity.activityInfo.name) &&
+ sendTabLabel.equals(activityLabel)) {
+ order = Menu.FIRST + i;
+ } else {
+ order = Menu.FIRST + (i | Menu.CATEGORY_SECONDARY);
+ }
+
+ subMenu.add(0, i, order, activityLabel)
+ .setIcon(activity.loadIcon(packageManager))
+ .setOnMenuItemClickListener(mCallbacks);
+ }
+ }
+
+ public void setHistoryFileName(String historyFile) {
+ mHistoryFileName = historyFile;
+ }
+
+ public Intent getIntent() {
+ ActivityChooserModel dataModel = ActivityChooserModel.get(mContext, mHistoryFileName);
+ return dataModel.getIntent();
+ }
+
+ public void setIntent(Intent intent) {
+ ActivityChooserModel dataModel = ActivityChooserModel.get(mContext, mHistoryFileName);
+ dataModel.setIntent(intent);
+
+ // Inform the target listener to refresh it's UI, if needed.
+ if (mOnTargetListener != null) {
+ mOnTargetListener.onTargetSelected();
+ }
+ }
+
+ public void setOnTargetSelectedListener(OnTargetSelectedListener listener) {
+ mOnTargetListener = listener;
+ }
+
+ public ArrayList<ResolveInfo> getSortedActivities() {
+ ArrayList<ResolveInfo> infos = new ArrayList<ResolveInfo>();
+
+ ActivityChooserModel dataModel = ActivityChooserModel.get(mContext, mHistoryFileName);
+
+ // Populate the sub-menu with a sub set of the activities.
+ final int count = dataModel.getActivityCount();
+ for (int i = 0; i < count; i++) {
+ infos.add(dataModel.getActivity(i));
+ }
+
+ return infos;
+ }
+
+ public void chooseActivity(int position) {
+ mCallbacks.chooseActivity(position);
+ }
+
+ /**
+ * Listener for handling default activity / menu item clicks.
+ */
+ private class Callbacks implements OnMenuItemClickListener,
+ OnClickListener {
+ void chooseActivity(int index) {
+ final ActivityChooserModel dataModel = ActivityChooserModel.get(mContext, mHistoryFileName);
+ final Intent launchIntent = dataModel.chooseActivity(index);
+ if (launchIntent != null) {
+ // This may cause a download to happen. Make sure we're on the background thread.
+ ThreadUtils.postToBackgroundThread(new Runnable() {
+ @Override
+ public void run() {
+ // Share image downloads the image before sharing it.
+ String type = launchIntent.getType();
+ if (Intent.ACTION_SEND.equals(launchIntent.getAction()) && type != null && type.startsWith("image/")) {
+ downloadImageForIntent(launchIntent);
+ }
+
+ launchIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
+ mContext.startActivity(launchIntent);
+ }
+ });
+ }
+
+ if (mOnTargetListener != null) {
+ mOnTargetListener.onTargetSelected();
+ }
+ }
+
+ @Override
+ public boolean onMenuItemClick(MenuItem item) {
+ chooseActivity(item.getItemId());
+
+ // Context: Sharing via chrome mainmenu list (no explicit session is active)
+ Telemetry.sendUIEvent(TelemetryContract.Event.SHARE, TelemetryContract.Method.LIST, "actionprovider");
+ return true;
+ }
+
+ @Override
+ public void onClick(View view) {
+ Integer index = (Integer) view.getTag();
+ chooseActivity(index);
+
+ // Context: Sharing via chrome mainmenu and content contextmenu quickshare (no explicit session is active)
+ Telemetry.sendUIEvent(TelemetryContract.Event.SHARE, TelemetryContract.Method.BUTTON, "actionprovider");
+ }
+ }
+
+ public enum ActionViewType {
+ DEFAULT,
+ CONTEXT_MENU,
+ }
+
+
+ /**
+ * Downloads the URI pointed to by a share intent, and alters the intent to point to the
+ * locally stored file.
+ *
+ * @param intent share intent to alter in place.
+ */
+ public void downloadImageForIntent(final Intent intent) {
+ final String src = IntentUtils.getStringExtraSafe(intent, Intent.EXTRA_TEXT);
+ final File dir = GeckoApp.getTempDirectory();
+
+ if (src == null || dir == null) {
+ // We should be, but currently aren't, statically guaranteed an Activity context.
+ // Try our best.
+ if (mContext instanceof Activity) {
+ SnackbarBuilder.builder((Activity) mContext)
+ .message(mContext.getApplicationContext().getString(R.string.share_image_failed))
+ .duration(Snackbar.LENGTH_LONG)
+ .buildAndShow();
+ }
+ return;
+ }
+
+ GeckoApp.deleteTempFiles();
+
+ String type = intent.getType();
+ OutputStream os = null;
+ try {
+ // Create a temporary file for the image
+ if (src.startsWith("data:")) {
+ final int dataStart = src.indexOf(",");
+
+ String extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(type);
+
+ // If we weren't given an explicit mimetype, try to dig one out of the data uri.
+ if (TextUtils.isEmpty(extension) && dataStart > 5) {
+ type = src.substring(5, dataStart).replace(";base64", "");
+ extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(type);
+ }
+
+ final File imageFile = File.createTempFile("image", "." + extension, dir);
+ os = new FileOutputStream(imageFile);
+
+ byte[] buf = Base64.decode(src.substring(dataStart + 1), Base64.DEFAULT);
+ os.write(buf);
+
+ // Only alter the intent when we're sure everything has worked
+ intent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(imageFile));
+ } else {
+ InputStream is = null;
+ try {
+ final byte[] buf = new byte[2048];
+ final URL url = new URL(src);
+ final String filename = URLUtil.guessFileName(src, null, type);
+ is = url.openStream();
+
+ final File imageFile = new File(dir, filename);
+ os = new FileOutputStream(imageFile);
+
+ int length;
+ while ((length = is.read(buf)) != -1) {
+ os.write(buf, 0, length);
+ }
+
+ // Only alter the intent when we're sure everything has worked
+ intent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(imageFile));
+ } finally {
+ IOUtils.safeStreamClose(is);
+ }
+ }
+ } catch (IOException ex) {
+ // If something went wrong, we'll just leave the intent un-changed
+ } finally {
+ IOUtils.safeStreamClose(os);
+ }
+ }
+}