Data URL fixes - bookmarks, save, and wallpaper

 Bug 5383517
 Images kept in data URLs can now be selected and this fixes the
 Save and Set Wallpaper options. It also removes the bookmarking
 capability.

Change-Id: I461bdcb4c950f6fcd8db8b38f4c599212106b027
diff --git a/src/com/android/browser/Controller.java b/src/com/android/browser/Controller.java
index 282d4f2..81ba941 100644
--- a/src/com/android/browser/Controller.java
+++ b/src/com/android/browser/Controller.java
@@ -40,6 +40,7 @@
 import android.net.http.SslError;
 import android.os.AsyncTask;
 import android.os.Bundle;
+import android.os.Environment;
 import android.os.Handler;
 import android.os.Message;
 import android.os.PowerManager;
@@ -68,6 +69,7 @@
 import android.webkit.CookieManager;
 import android.webkit.CookieSyncManager;
 import android.webkit.HttpAuthHandler;
+import android.webkit.MimeTypeMap;
 import android.webkit.SslErrorHandler;
 import android.webkit.ValueCallback;
 import android.webkit.WebChromeClient;
@@ -86,9 +88,15 @@
 import com.android.common.Search;
 
 import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
 import java.net.URLEncoder;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
 import java.util.ArrayList;
 import java.util.Calendar;
+import java.util.Date;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -140,7 +148,7 @@
     // "source" parameter for Google search through simplily type
     final static String GOOGLE_SEARCH_SOURCE_TYPE = "browser-type";
 
-    // "no-crash-recovery" parameter in intetnt to suppress crash recovery
+    // "no-crash-recovery" parameter in intent to suppress crash recovery
     final static String NO_CRASH_RECOVERY = "no-crash-recovery";
 
     // A bitmap that is re-used in createScreenshot as scratch space
@@ -2048,10 +2056,16 @@
         private Activity mActivity;
         private String mText;
         private boolean mPrivateBrowsing;
+        private static final String FALLBACK_EXTENSION = "dat";
+        private static final String IMAGE_BASE_FORMAT = "yyyy-MM-dd-HH-mm-ss-";
 
         public boolean onMenuItemClick(MenuItem item) {
-            DownloadHandler.onDownloadStartNoStream(mActivity, mText, null,
-                    null, null, mPrivateBrowsing);
+            if (DataUri.isDataUri(mText)) {
+                saveDataUri();
+            } else {
+                DownloadHandler.onDownloadStartNoStream(mActivity, mText, null,
+                        null, null, mPrivateBrowsing);
+            }
             return true;
         }
 
@@ -2060,6 +2074,56 @@
             mText = toDownload;
             mPrivateBrowsing = privateBrowsing;
         }
+
+        /**
+         * Treats mText as a data URI and writes its contents to a file
+         * based on the current time.
+         */
+        private void saveDataUri() {
+            FileOutputStream outputStream = null;
+            try {
+                DataUri uri = new DataUri(mText);
+                File target = getTarget(uri);
+                outputStream = new FileOutputStream(target);
+                outputStream.write(uri.getData());
+                final DownloadManager manager =
+                        (DownloadManager) mActivity.getSystemService(Context.DOWNLOAD_SERVICE);
+                 manager.addCompletedDownload(target.getName(),
+                        mActivity.getTitle().toString(), false,
+                        uri.getMimeType(), target.getAbsolutePath(),
+                        (long)uri.getData().length, false);
+            } catch (IOException e) {
+                Log.e(LOGTAG, "Could not save data URL");
+            } finally {
+                if (outputStream != null) {
+                    try {
+                        outputStream.close();
+                    } catch (IOException e) {
+                        // ignore close errors
+                    }
+                }
+            }
+        }
+
+        /**
+         * Creates a File based on the current time stamp and uses
+         * the mime type of the DataUri to get the extension.
+         */
+        private File getTarget(DataUri uri) throws IOException {
+            File dir = mActivity.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS);
+            DateFormat format = new SimpleDateFormat(IMAGE_BASE_FORMAT);
+            String nameBase = format.format(new Date());
+            String mimeType = uri.getMimeType();
+            MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton();
+            String extension = mimeTypeMap.getExtensionFromMimeType(mimeType);
+            if (extension == null) {
+                Log.w(LOGTAG, "Unknown mime type in data URI" + mimeType);
+                extension = FALLBACK_EXTENSION;
+            }
+            extension = "." + extension; // createTempFile needs the '.'
+            File targetFile = File.createTempFile(nameBase, extension, dir);
+            return targetFile;
+        }
     }
 
     private static class SelectText implements OnMenuItemClickListener {
diff --git a/src/com/android/browser/DataUri.java b/src/com/android/browser/DataUri.java
new file mode 100644
index 0000000..642b060
--- /dev/null
+++ b/src/com/android/browser/DataUri.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2010 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.browser;
+
+import java.net.MalformedURLException;
+
+import libcore.io.Base64;
+
+/**
+ * Class extracts the mime type and data from a data uri.
+ * A data URI is of the form:
+ * <pre>
+ * data:[&lt;MIME-type&gt;][;charset=&lt;encoding&gt;][;base64],&lt;data&gt;
+ * </pre>
+ */
+public class DataUri {
+    private static final String DATA_URI_PREFIX = "data:";
+    private static final String BASE_64_ENCODING = ";base64";
+
+    private String mMimeType;
+    private byte[] mData;
+
+    public DataUri(String uri) throws MalformedURLException {
+        if (!isDataUri(uri)) {
+            throw new MalformedURLException("Not a data URI");
+        }
+
+        int commaIndex = uri.indexOf(',', DATA_URI_PREFIX.length());
+        if (commaIndex < 0) {
+            throw new MalformedURLException("Comma expected in data URI");
+        }
+        String contentType = uri.substring(DATA_URI_PREFIX.length(),
+                commaIndex);
+        mData = uri.substring(commaIndex + 1).getBytes();
+        if (contentType.contains(BASE_64_ENCODING)) {
+            mData = Base64.decode(mData);
+        }
+        int semiIndex = contentType.indexOf(';');
+        if (semiIndex > 0) {
+            mMimeType = contentType.substring(0, semiIndex);
+        } else {
+            mMimeType = contentType;
+        }
+    }
+
+    /**
+     * Returns true if the text passed in appears to be a data URI.
+     */
+    public static boolean isDataUri(String text)
+    {
+        return text.startsWith(DATA_URI_PREFIX);
+    }
+
+    public String getMimeType() {
+        return mMimeType;
+    }
+
+    public byte[] getData() {
+        return mData;
+    }
+}
diff --git a/src/com/android/browser/NavigationBarTablet.java b/src/com/android/browser/NavigationBarTablet.java
index c22b1a0..be3a9ab 100644
--- a/src/com/android/browser/NavigationBarTablet.java
+++ b/src/com/android/browser/NavigationBarTablet.java
@@ -151,6 +151,12 @@
     }
 
     @Override
+    public void onTabDataChanged(Tab tab) {
+        super.onTabDataChanged(tab);
+        showHideStar(tab);
+    }
+
+    @Override
     public void setCurrentUrlIsBookmark(boolean isBookmark) {
         mStar.setActivated(isBookmark);
     }
@@ -235,7 +241,7 @@
             }
             mGoButton.setVisibility(View.GONE);
             mVoiceSearch.setVisibility(View.GONE);
-            mStar.setVisibility(View.VISIBLE);
+            showHideStar(mUiController.getCurrentTab());
             mClearButton.setVisibility(View.GONE);
             if (mTitleBar.useQuickControls()) {
                 mSearchButton.setVisibility(View.GONE);
@@ -321,4 +327,16 @@
         combo.start();
     }
 
+    private void showHideStar(Tab tab) {
+        // hide the bookmark star for data URLs
+        if (tab != null && tab.inForeground()) {
+            int starVisibility = View.VISIBLE;
+            String url = tab.getUrl();
+            if (DataUri.isDataUri(url)) {
+                starVisibility = View.GONE;
+            }
+            mStar.setVisibility(starVisibility);
+        }
+    }
+
 }
diff --git a/src/com/android/browser/PhoneUi.java b/src/com/android/browser/PhoneUi.java
index 0e2710d..ddb4e0e 100644
--- a/src/com/android/browser/PhoneUi.java
+++ b/src/com/android/browser/PhoneUi.java
@@ -196,7 +196,9 @@
     public void updateMenuState(Tab tab, Menu menu) {
         MenuItem bm = menu.findItem(R.id.bookmarks_menu_id);
         if (bm != null) {
-            bm.setVisible(!showingNavScreen());
+            String url = tab.getUrl();
+            boolean isDataUrl = DataUri.isDataUri(url);
+            bm.setVisible(!showingNavScreen() && !isDataUrl);
         }
         MenuItem nt = menu.findItem(R.id.new_tab_menu_id);
         if (nt != null) {
diff --git a/src/com/android/browser/WallpaperHandler.java b/src/com/android/browser/WallpaperHandler.java
index 6437b1a..b76861c 100644
--- a/src/com/android/browser/WallpaperHandler.java
+++ b/src/com/android/browser/WallpaperHandler.java
@@ -27,8 +27,8 @@
 import android.util.Log;
 import android.view.MenuItem;
 import android.view.MenuItem.OnMenuItemClickListener;
-
 import java.io.BufferedInputStream;
+import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.net.MalformedURLException;
@@ -41,24 +41,19 @@
 public class WallpaperHandler extends Thread
         implements OnMenuItemClickListener, DialogInterface.OnCancelListener {
 
-
     private static final String LOGTAG = "WallpaperHandler";
     // This should be large enough for BitmapFactory to decode the header so
     // that we can mark and reset the input stream to avoid duplicate network i/o
     private static final int BUFFER_SIZE = 128 * 1024;
 
     private Context mContext;
-    private URL mUrl;
+    private String  mUrl;
     private ProgressDialog mWallpaperProgress;
     private boolean mCanceled = false;
 
     public WallpaperHandler(Context context, String url) {
         mContext = context;
-        try {
-            mUrl = new URL(url);
-        } catch (MalformedURLException e) {
-            mUrl = null;
-        }
+        mUrl = url;
     }
 
     @Override
@@ -97,7 +92,7 @@
             // version and instead open an input stream on that. This pattern
             // could also be used in the download manager where the same problem
             // exists.
-            inputstream = mUrl.openStream();
+            inputstream = openStream();
             if (inputstream != null) {
                 if (!inputstream.markSupported()) {
                     inputstream = new BufferedInputStream(inputstream, BUFFER_SIZE);
@@ -118,7 +113,7 @@
                 int bmHeight = options.outHeight;
 
                 int scale = 1;
-                while (bmWidth > maxWidth || bmHeight > maxWidth) {
+                while (bmWidth > maxWidth || bmHeight > maxHeight) {
                     scale <<= 1;
                     bmWidth >>= 1;
                     bmHeight >>= 1;
@@ -131,7 +126,7 @@
                     // BitmapFactory read more than we could buffer
                     // Re-open the stream
                     inputstream.close();
-                    inputstream = mUrl.openStream();
+                    inputstream = openStream();
                 }
                 Bitmap scaledWallpaper = BitmapFactory.decodeStream(inputstream,
                         null, options);
@@ -175,4 +170,24 @@
             mWallpaperProgress.dismiss();
         }
     }
+
+    /**
+     * Opens the input stream for the URL that the class should
+     * use to set the wallpaper. Abstracts the difference between
+     * standard URLs and data URLs.
+     * @return An open InputStream for the data at the URL
+     * @throws IOException if there is an error opening the URL stream
+     * @throws MalformedURLException if the URL is malformed
+     */
+    private InputStream openStream() throws IOException, MalformedURLException {
+        InputStream inputStream = null;
+        if (DataUri.isDataUri(mUrl)) {
+            DataUri dataUri = new DataUri(mUrl);
+            inputStream = new ByteArrayInputStream(dataUri.getData());
+        } else {
+            URL url = new URL(mUrl);
+            inputStream = url.openStream();
+        }
+        return inputStream;
+    }
 }