diff options
Diffstat (limited to 'src')
39 files changed, 5756 insertions, 2600 deletions
diff --git a/src/com/android/browser/ActiveTabsPage.java b/src/com/android/browser/ActiveTabsPage.java index 2de778712..e450a9988 100644 --- a/src/com/android/browser/ActiveTabsPage.java +++ b/src/com/android/browser/ActiveTabsPage.java @@ -20,6 +20,7 @@ import android.content.Context; import android.graphics.Bitmap; import android.os.Handler; import android.util.AttributeSet; +import android.util.Log; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; @@ -32,6 +33,7 @@ import android.widget.ListView; import android.widget.TextView; public class ActiveTabsPage extends LinearLayout { + private static final String LOGTAG = "TabPicker"; private final BrowserActivity mBrowserActivity; private final LayoutInflater mFactory; private final TabControl mControl; @@ -51,12 +53,15 @@ public class ActiveTabsPage extends LinearLayout { public void onItemClick(AdapterView<?> parent, View view, int position, long id) { if (mControl.canCreateNewTab()) { - position--; + position -= 2; } boolean needToAttach = false; - if (position == -1) { + if (position == -2) { // Create a new tab mBrowserActivity.openTabToHomePage(); + } else if (position == -1) { + // Create a new incognito tab + mBrowserActivity.openIncognitoTab(); } else { // Open the corresponding tab // If the tab is the current one, switchToTab will @@ -98,7 +103,7 @@ public class ActiveTabsPage extends LinearLayout { public int getCount() { int count = mControl.getTabCount(); if (mControl.canCreateNewTab()) { - count++; + count += 2; } // XXX: This is a workaround to be more like a real adapter. Most // adapters call notifyDataSetChanged() whenever the internal data @@ -128,23 +133,28 @@ public class ActiveTabsPage extends LinearLayout { } public int getItemViewType(int position) { if (mControl.canCreateNewTab()) { - position--; + position -= 2; } // Do not recycle the "add new tab" item. - return position == -1 ? IGNORE_ITEM_VIEW_TYPE : 1; + return position < 0 ? IGNORE_ITEM_VIEW_TYPE : 1; } public View getView(int position, View convertView, ViewGroup parent) { final int tabCount = mControl.getTabCount(); if (mControl.canCreateNewTab()) { - position--; + position -= 2; } if (convertView == null) { - convertView = mFactory.inflate(position == -1 ? - R.layout.tab_view_add_tab : R.layout.tab_view, null); + if (position == -2) { + convertView = mFactory.inflate(R.layout.tab_view_add_tab, null); + } else if (position == -1) { + convertView = mFactory.inflate(R.layout.tab_view_add_incognito_tab, null); + } else { + convertView = mFactory.inflate(R.layout.tab_view, null); + } } - if (position != -1) { + if (position >= 0) { TextView title = (TextView) convertView.findViewById(R.id.title); TextView url = (TextView) convertView.findViewById(R.id.url); @@ -152,7 +162,19 @@ public class ActiveTabsPage extends LinearLayout { (ImageView) convertView.findViewById(R.id.favicon); View close = convertView.findViewById(R.id.close); Tab tab = mControl.getTab(position); + if (tab.getWebView() == null) { + // This means that populatePickerData will have to use the + // saved state. + Log.w(LOGTAG, "Tab " + position + " has a null WebView and " + + (tab.getSavedState() == null ? "null" : "non-null") + + " saved state "); + } tab.populatePickerData(); + if (tab.getTitle() == null || tab.getTitle().length() == 0) { + Log.w(LOGTAG, "Tab " + position + " has no title. " + + "Check above in the Logs to see whether it has a " + + "null WebView or null WebHistoryItem"); + } title.setText(tab.getTitle()); url.setText(tab.getUrl()); Bitmap icon = tab.getFavicon(); diff --git a/src/com/android/browser/AddBookmarkPage.java b/src/com/android/browser/AddBookmarkPage.java index 1104d5e25..1d6edc5cc 100644 --- a/src/com/android/browser/AddBookmarkPage.java +++ b/src/com/android/browser/AddBookmarkPage.java @@ -51,6 +51,7 @@ public class AddBookmarkPage extends Activity { private String mTouchIconUrl; private Bitmap mThumbnail; private String mOriginalUrl; + private boolean mIsUrlEditable = true; // Message IDs private static final int SAVE_BOOKMARK = 100; @@ -74,13 +75,24 @@ public class AddBookmarkPage extends Activity { protected void onCreate(Bundle icicle) { super.onCreate(icicle); requestWindowFeature(Window.FEATURE_LEFT_ICON); - setContentView(R.layout.browser_add_bookmark); + + mMap = getIntent().getExtras(); + if (mMap != null) { + mIsUrlEditable = mMap.getBoolean("url_editable", true); + } + + if (mIsUrlEditable) { + setContentView(R.layout.browser_add_bookmark); + } else { + setContentView(R.layout.browser_add_bookmark_const_url); + } + setTitle(R.string.save_to_bookmarks); getWindow().setFeatureDrawableResource(Window.FEATURE_LEFT_ICON, R.drawable.ic_list_bookmark); String title = null; String url = null; - mMap = getIntent().getExtras(); + if (mMap != null) { Bundle b = mMap.getBundle("bookmark"); if (b != null) { @@ -96,8 +108,11 @@ public class AddBookmarkPage extends Activity { mTitle = (EditText) findViewById(R.id.title); mTitle.setText(title); - mAddress = (EditText) findViewById(R.id.address); - mAddress.setText(url); + + if (mIsUrlEditable) { + mAddress = (EditText) findViewById(R.id.address); + mAddress.setText(url); + } View.OnClickListener accept = mSaveBookmark; mButton = (TextView) findViewById(R.id.OK); @@ -133,9 +148,9 @@ public class AddBookmarkPage extends Activity { // Save to the bookmarks DB. try { final ContentResolver cr = getContentResolver(); - Bookmarks.addBookmark(null, cr, url, title, thumbnail, true); + Bookmarks.addBookmark(AddBookmarkPage.this, false, url, title, thumbnail, true); if (touchIconUrl != null) { - new DownloadTouchIcon(cr, url).execute(mTouchIconUrl); + new DownloadTouchIcon(AddBookmarkPage.this, cr, url).execute(mTouchIconUrl); } mMessage.arg1 = 1; } catch (IllegalStateException e) { @@ -173,8 +188,14 @@ public class AddBookmarkPage extends Activity { createHandler(); String title = mTitle.getText().toString().trim(); - String unfilteredUrl = - BrowserActivity.fixUrl(mAddress.getText().toString()); + String unfilteredUrl; + if (mIsUrlEditable) { + unfilteredUrl = + BrowserActivity.fixUrl(mAddress.getText().toString()); + } else { + unfilteredUrl = mOriginalUrl; + } + boolean emptyTitle = title.length() == 0; boolean emptyUrl = unfilteredUrl.trim().length() == 0; Resources r = getResources(); @@ -183,9 +204,15 @@ public class AddBookmarkPage extends Activity { mTitle.setError(r.getText(R.string.bookmark_needs_title)); } if (emptyUrl) { - mAddress.setError(r.getText(R.string.bookmark_needs_url)); + if (mIsUrlEditable) { + mAddress.setError(r.getText(R.string.bookmark_needs_url)); + } else { + Toast.makeText(AddBookmarkPage.this, R.string.bookmark_needs_url, + Toast.LENGTH_LONG).show(); + } } return false; + } String url = unfilteredUrl.trim(); try { @@ -200,7 +227,12 @@ public class AddBookmarkPage extends Activity { // can't save their bookmark. If it was null, we'll assume // they meant http when we parse it in the WebAddress class. if (scheme != null) { - mAddress.setError(r.getText(R.string.bookmark_cannot_save_url)); + if (mIsUrlEditable) { + mAddress.setError(r.getText(R.string.bookmark_cannot_save_url)); + } else { + Toast.makeText(AddBookmarkPage.this, R.string.bookmark_cannot_save_url, + Toast.LENGTH_LONG).show(); + } return false; } WebAddress address; @@ -216,7 +248,12 @@ public class AddBookmarkPage extends Activity { } } } catch (URISyntaxException e) { - mAddress.setError(r.getText(R.string.bookmark_url_not_valid)); + if (mIsUrlEditable) { + mAddress.setError(r.getText(R.string.bookmark_url_not_valid)); + } else { + Toast.makeText(AddBookmarkPage.this, R.string.bookmark_url_not_valid, + Toast.LENGTH_LONG).show(); + } return false; } diff --git a/src/com/android/browser/BookmarkUtils.java b/src/com/android/browser/BookmarkUtils.java new file mode 100644 index 000000000..0fdad153f --- /dev/null +++ b/src/com/android/browser/BookmarkUtils.java @@ -0,0 +1,168 @@ +/* + * 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 android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffXfermode; +import android.graphics.Rect; +import android.graphics.RectF; +import android.net.Uri; +import android.provider.Browser; +import android.util.Log; + +class BookmarkUtils { + private final static String LOGTAG = "BookmarkUtils"; + + // XXX: There is no public string defining this intent so if Home changes the value, we + // have to update this string. + private static final String INSTALL_SHORTCUT = "com.android.launcher.action.INSTALL_SHORTCUT"; + + enum BookmarkIconType { + ICON_INSTALLABLE_WEB_APP, // Icon for an installable web app (launches WebAppRuntime). + ICON_HOME_SHORTCUT // Icon for a shortcut on the home screen (launches Browser). + }; + + /** + * Creates an icon to be associated with this bookmark. If available, the apple touch icon + * will be used, else we draw our own depending on the type of "bookmark" being created. + */ + static Bitmap createIcon(Context context, Bitmap touchIcon, Bitmap favicon, + BookmarkIconType type) { + int iconDimension = context.getResources().getDimensionPixelSize( + android.R.dimen.app_icon_size); + + Bitmap bm = Bitmap.createBitmap(iconDimension, iconDimension, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bm); + Rect iconBounds = new Rect(0, 0, bm.getWidth(), bm.getHeight()); + + // Use the apple-touch-icon if available + if (touchIcon != null) { + drawTouchIconToCanvas(touchIcon, canvas, iconBounds); + } else { + // No touch icon so create our own. + // Set the background based on the type of shortcut (either webapp or home shortcut). + Bitmap icon = getIconBackground(context, type); + + if (icon != null) { + // Now draw the correct icon background into our new bitmap. + canvas.drawBitmap(icon, null, iconBounds, null); + } + + // If we have a favicon, overlay it in a nice rounded white box on top of the + // background. + if (favicon != null) { + drawFaviconToCanvas(favicon, canvas, iconBounds, + context.getResources().getDisplayMetrics().density); + } + } + return bm; + } + + /** + * Convenience method for creating an intent that will add a shortcut to the home screen. + */ + static Intent createAddToHomeIntent(Context context, String url, String title, + Bitmap touchIcon, Bitmap favicon) { + Intent i = new Intent(INSTALL_SHORTCUT); + Intent shortcutIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); + long urlHash = url.hashCode(); + long uniqueId = (urlHash << 32) | shortcutIntent.hashCode(); + shortcutIntent.putExtra(Browser.EXTRA_APPLICATION_ID, Long.toString(uniqueId)); + i.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent); + i.putExtra(Intent.EXTRA_SHORTCUT_NAME, title); + i.putExtra(Intent.EXTRA_SHORTCUT_ICON, createIcon(context, touchIcon, favicon, + BookmarkIconType.ICON_HOME_SHORTCUT)); + + // Do not allow duplicate items + i.putExtra("duplicate", false); + return i; + } + + private static Bitmap getIconBackground(Context context, BookmarkIconType type) { + if (type == BookmarkIconType.ICON_HOME_SHORTCUT) { + // Want to create a shortcut icon on the homescreen, so the icon + // background is the red bookmark. + return BitmapFactory.decodeResource(context.getResources(), + R.drawable.ic_launcher_shortcut_browser_bookmark); + } else if (type == BookmarkIconType.ICON_INSTALLABLE_WEB_APP) { + // Use the web browser icon as the background for the icon for an installable + // web app. + return BitmapFactory.decodeResource(context.getResources(), + R.drawable.ic_launcher_browser); + } + return null; + } + + private static void drawTouchIconToCanvas(Bitmap touchIcon, Canvas canvas, Rect iconBounds) { + Rect src = new Rect(0, 0, touchIcon.getWidth(), touchIcon.getHeight()); + + // Paint used for scaling the bitmap and drawing the rounded rect. + Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); + paint.setFilterBitmap(true); + canvas.drawBitmap(touchIcon, src, iconBounds, paint); + + // Construct a path from a round rect. This will allow drawing with + // an inverse fill so we can punch a hole using the round rect. + Path path = new Path(); + path.setFillType(Path.FillType.INVERSE_WINDING); + RectF rect = new RectF(iconBounds); + rect.inset(1, 1); + path.addRoundRect(rect, 8f, 8f, Path.Direction.CW); + + // Reuse the paint and clear the outside of the rectangle. + paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); + canvas.drawPath(path, paint); + } + + private static void drawFaviconToCanvas(Bitmap favicon, Canvas canvas, Rect iconBounds, + float density) { + // Make a Paint for the white background rectangle and for + // filtering the favicon. + Paint p = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG); + p.setStyle(Paint.Style.FILL_AND_STROKE); + p.setColor(Color.WHITE); + + // Create a rectangle that is slightly wider than the favicon + final float iconSize = 16 * density; // 16x16 favicon + final float padding = 2 * density; // white padding around icon + final float rectSize = iconSize + 2 * padding; + final float x = iconBounds.exactCenterX() - (rectSize / 2); + // Note: Subtract 2 dip from the y position since the box is + // slightly higher than center. Use padding since it is already + // 2 * density. + final float y = iconBounds.exactCenterY() - (rectSize / 2) - padding; + RectF r = new RectF(x, y, x + rectSize, y + rectSize); + + // Draw a white rounded rectangle behind the favicon + canvas.drawRoundRect(r, 2, 2, p); + + // Draw the favicon in the same rectangle as the rounded + // rectangle but inset by the padding + // (results in a 16x16 favicon). + r.inset(padding, padding); + canvas.drawBitmap(favicon, null, r, p); + } + +}; diff --git a/src/com/android/browser/Bookmarks.java b/src/com/android/browser/Bookmarks.java index 5ada9dcb8..0bccbed97 100644 --- a/src/com/android/browser/Bookmarks.java +++ b/src/com/android/browser/Bookmarks.java @@ -20,16 +20,20 @@ import android.content.ContentResolver; import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; +import android.content.SharedPreferences; import android.database.Cursor; import android.graphics.Bitmap; import android.net.Uri; -import android.provider.Browser; +import android.os.AsyncTask; +import android.preference.PreferenceManager; +import android.provider.BrowserContract; +import android.provider.BrowserContract.Combined; +import android.provider.BrowserContract.Images; import android.util.Log; import android.webkit.WebIconDatabase; import android.widget.Toast; import java.io.ByteArrayOutputStream; -import java.util.Date; /** * This class is purely to have a common place for adding/deleting bookmarks. @@ -61,72 +65,23 @@ import java.util.Date; * This will usually be <code>true</code> except when bookmarks are * added by a settings restore agent. */ - /* package */ static void addBookmark(Context context, - ContentResolver cr, String url, String name, - Bitmap thumbnail, boolean retainIcon) { + /* package */ static void addBookmark(Context context, boolean showToast, String url, + String name, Bitmap thumbnail, boolean retainIcon) { // Want to append to the beginning of the list - long creationTime = new Date().getTime(); - ContentValues map = new ContentValues(); + ContentValues values = new ContentValues(); Cursor cursor = null; try { - cursor = Browser.getVisitedLike(cr, url); - if (cursor.moveToFirst() && cursor.getInt( - Browser.HISTORY_PROJECTION_BOOKMARK_INDEX) == 0) { - // This means we have been to this site but not bookmarked - // it, so convert the history item to a bookmark - map.put(Browser.BookmarkColumns.CREATED, creationTime); - map.put(Browser.BookmarkColumns.TITLE, name); - map.put(Browser.BookmarkColumns.BOOKMARK, 1); - map.put(Browser.BookmarkColumns.THUMBNAIL, - bitmapToBytes(thumbnail)); - cr.update(Browser.BOOKMARKS_URI, map, - "_id = " + cursor.getInt(0), null); - } else { - int count = cursor.getCount(); - boolean matchedTitle = false; - for (int i = 0; i < count; i++) { - // One or more bookmarks already exist for this site. - // Check the names of each - cursor.moveToPosition(i); - if (cursor.getString(Browser.HISTORY_PROJECTION_TITLE_INDEX) - .equals(name)) { - // The old bookmark has the same name. - // Update its creation time. - map.put(Browser.BookmarkColumns.CREATED, - creationTime); - cr.update(Browser.BOOKMARKS_URI, map, - "_id = " + cursor.getInt(0), null); - matchedTitle = true; - break; - } - } - if (!matchedTitle) { - // Adding a bookmark for a site the user has visited, - // or a new bookmark (with a different name) for a site - // the user has visited - map.put(Browser.BookmarkColumns.TITLE, name); - map.put(Browser.BookmarkColumns.URL, url); - map.put(Browser.BookmarkColumns.CREATED, creationTime); - map.put(Browser.BookmarkColumns.BOOKMARK, 1); - map.put(Browser.BookmarkColumns.DATE, 0); - map.put(Browser.BookmarkColumns.THUMBNAIL, - bitmapToBytes(thumbnail)); - int visits = 0; - if (count > 0) { - // The user has already bookmarked, and possibly - // visited this site. However, they are creating - // a new bookmark with the same url but a different - // name. The new bookmark should have the same - // number of visits as the already created bookmark. - visits = cursor.getInt( - Browser.HISTORY_PROJECTION_VISITS_INDEX); - } - // Bookmark starts with 3 extra visits so that it will - // bubble up in the most visited and goto search box - map.put(Browser.BookmarkColumns.VISITS, visits + 3); - cr.insert(Browser.BOOKMARKS_URI, map); - } - } + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + String accountType = prefs.getString(BrowserBookmarksPage.PREF_ACCOUNT_TYPE, null); + String accountName = prefs.getString(BrowserBookmarksPage.PREF_ACCOUNT_NAME, null); + values.put(BrowserContract.Bookmarks.ACCOUNT_TYPE, accountType); + values.put(BrowserContract.Bookmarks.ACCOUNT_NAME, accountName); + values.put(BrowserContract.Bookmarks.TITLE, name); + values.put(BrowserContract.Bookmarks.URL, url); + values.put(BrowserContract.Bookmarks.IS_FOLDER, 0); + values.put(BrowserContract.Bookmarks.THUMBNAIL, + bitmapToBytes(thumbnail)); + context.getContentResolver().insert(BrowserContract.Bookmarks.CONTENT_URI, values); } catch (IllegalStateException e) { Log.e(LOGTAG, "addBookmark", e); } finally { @@ -135,7 +90,7 @@ import java.util.Date; if (retainIcon) { WebIconDatabase.getInstance().retainIconForPageUrl(url); } - if (context != null) { + if (showToast) { Toast.makeText(context, R.string.added_to_bookmarks, Toast.LENGTH_LONG).show(); } @@ -155,40 +110,26 @@ import java.util.Date; ContentResolver cr, String url, String title) { Cursor cursor = null; try { - cursor = cr.query( - Browser.BOOKMARKS_URI, - Browser.HISTORY_PROJECTION, - "url = ? AND title = ?", + cursor = cr.query(BrowserContract.Bookmarks.CONTENT_URI, + new String[] { BrowserContract.Bookmarks._ID }, + BrowserContract.Bookmarks.URL + " = ? AND " + + BrowserContract.Bookmarks.TITLE + " = ?", new String[] { url, title }, null); - boolean first = cursor.moveToFirst(); + // Should be in the database no matter what - if (!first) { + if (!cursor.moveToFirst()) { throw new AssertionError("URL is not in the database! " + url + " " + title); } + // Remove from bookmarks WebIconDatabase.getInstance().releaseIconForPageUrl(url); - Uri uri = ContentUris.withAppendedId(Browser.BOOKMARKS_URI, - cursor.getInt(Browser.HISTORY_PROJECTION_ID_INDEX)); - int numVisits = cursor.getInt( - Browser.HISTORY_PROJECTION_VISITS_INDEX); - if (0 == numVisits) { - cr.delete(uri, null, null); - } else { - // It is no longer a bookmark, but it is still a visited - // site. - ContentValues values = new ContentValues(); - values.put(Browser.BookmarkColumns.BOOKMARK, 0); - try { - cr.update(uri, values, null, null); - } catch (IllegalStateException e) { - Log.e("removeFromBookmarks", "no database!"); - } - } + Uri uri = ContentUris.withAppendedId(BrowserContract.Bookmarks.CONTENT_URI, + cursor.getLong(0)); + cr.delete(uri, null, null); if (context != null) { - Toast.makeText(context, R.string.removed_from_bookmarks, - Toast.LENGTH_LONG).show(); + Toast.makeText(context, R.string.removed_from_bookmarks, Toast.LENGTH_LONG).show(); } } catch (IllegalStateException e) { Log.e(LOGTAG, "removeFromBookmarks", e); @@ -219,4 +160,86 @@ import java.util.Date; } return false; } + + static final String QUERY_BOOKMARKS_WHERE = + Combined.URL + " == ? OR " + + Combined.URL + " == ? OR " + + Combined.URL + " LIKE ? || '%' OR " + + Combined.URL + " LIKE ? || '%'"; + + /* package */ static Cursor queryCombinedForUrl(ContentResolver cr, + String originalUrl, String url) { + if (cr == null || url == null) { + return null; + } + + // If originalUrl is null, just set it to url. + if (originalUrl == null) { + originalUrl = url; + } + + // Look for both the original url and the actual url. This takes in to + // account redirects. + String originalUrlNoQuery = removeQuery(originalUrl); + String urlNoQuery = removeQuery(url); + originalUrl = originalUrlNoQuery + '?'; + url = urlNoQuery + '?'; + + // Use NoQuery to search for the base url (i.e. if the url is + // http://www.yahoo.com/?rs=1, search for http://www.yahoo.com) + // Use url to match the base url with other queries (i.e. if the url is + // http://www.google.com/m, search for + // http://www.google.com/m?some_query) + final String[] selArgs = new String[] { originalUrlNoQuery, urlNoQuery, originalUrl, url }; + final String[] projection = new String[] { Combined.URL }; + return cr.query(Combined.CONTENT_URI, projection, QUERY_BOOKMARKS_WHERE, selArgs, null); + } + + // Strip the query from the given url. + static String removeQuery(String url) { + if (url == null) { + return null; + } + int query = url.indexOf('?'); + String noQuery = url; + if (query != -1) { + noQuery = url.substring(0, query); + } + return noQuery; + } + + /** + * Update the bookmark's favicon. This is a convenience method for updating + * a bookmark favicon for the originalUrl and url of the passed in WebView. + * @param cr The ContentResolver to use. + * @param originalUrl The original url before any redirects. + * @param url The current url. + * @param favicon The favicon bitmap to write to the db. + */ + /* package */ static void updateFavicon(final ContentResolver cr, + final String originalUrl, final String url, final Bitmap favicon) { + new AsyncTask<Void, Void, Void>() { + @Override + protected Void doInBackground(Void... unused) { + Cursor cursor = queryCombinedForUrl(cr, originalUrl, url); + try { + if (cursor.moveToFirst()) { + final ByteArrayOutputStream os = new ByteArrayOutputStream(); + favicon.compress(Bitmap.CompressFormat.PNG, 100, os); + + ContentValues values = new ContentValues(); + values.put(Images.FAVICON, os.toByteArray()); + values.put(Images.URL, cursor.getString(0)); + + do { + cr.update(Images.CONTENT_URI, values, null, null); + } while (cursor.moveToNext()); + } + } finally { + if (cursor != null) cursor.close(); + } + return null; + } + }.execute(); + } } diff --git a/src/com/android/browser/BookmarksLoader.java b/src/com/android/browser/BookmarksLoader.java new file mode 100644 index 000000000..094718484 --- /dev/null +++ b/src/com/android/browser/BookmarksLoader.java @@ -0,0 +1,70 @@ +/* + * 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 android.content.Context; +import android.content.CursorLoader; +import android.net.Uri; +import android.provider.BrowserContract.Bookmarks; +import android.text.TextUtils; + +public class BookmarksLoader extends CursorLoader { + public static final String ARG_ACCOUNT_TYPE = "acct_type"; + public static final String ARG_ACCOUNT_NAME = "acct_name"; + + public static final int COLUMN_INDEX_ID = 0; + public static final int COLUMN_INDEX_URL = 1; + public static final int COLUMN_INDEX_TITLE = 2; + public static final int COLUMN_INDEX_FAVICON = 3; + public static final int COLUMN_INDEX_THUMBNAIL = 4; + public static final int COLUMN_INDEX_TOUCH_ICON = 5; + public static final int COLUMN_INDEX_IS_FOLDER = 6; + + public static final String[] PROJECTION = new String[] { + Bookmarks._ID, // 0 + Bookmarks.URL, // 1 + Bookmarks.TITLE, // 2 + Bookmarks.FAVICON, // 3 + Bookmarks.THUMBNAIL, // 4 + Bookmarks.TOUCH_ICON, // 5 + Bookmarks.IS_FOLDER, // 6 + Bookmarks.POSITION, // 7 + }; + + private String mAccountType; + private String mAccountName; + + public BookmarksLoader(Context context, String accountType, String accountName) { + super(context, addAccount(Bookmarks.CONTENT_URI_DEFAULT_FOLDER, accountType, accountName), + PROJECTION, null, null, null); + mAccountType = accountType; + mAccountName = accountName; + } + + @Override + public void setUri(Uri uri) { + super.setUri(addAccount(uri, mAccountType, mAccountName)); + } + + private static Uri addAccount(Uri uri, String accountType, String accountName) { + if (!TextUtils.isEmpty(accountType) && !TextUtils.isEmpty(accountName)) { + return uri.buildUpon().appendQueryParameter(Bookmarks.PARAM_ACCOUNT_TYPE, accountType). + appendQueryParameter(Bookmarks.PARAM_ACCOUNT_NAME, accountName).build(); + } + return uri; + } +} diff --git a/src/com/android/browser/BrowserActivity.java b/src/com/android/browser/BrowserActivity.java index 098e3b2f4..c3d3d9c95 100644 --- a/src/com/android/browser/BrowserActivity.java +++ b/src/com/android/browser/BrowserActivity.java @@ -16,12 +16,20 @@ package com.android.browser; +import com.android.browser.ScrollWebView.ScrollListener; +import com.android.browser.search.SearchEngine; +import com.android.common.Search; +import com.android.common.speech.LoggingEvents; + +import android.app.ActionBar; import android.app.Activity; import android.app.AlertDialog; +import android.app.Dialog; import android.app.ProgressDialog; import android.app.SearchManager; import android.content.ActivityNotFoundException; import android.content.BroadcastReceiver; +import android.content.ClipboardManager; import android.content.ComponentName; import android.content.ContentProvider; import android.content.ContentProviderClient; @@ -32,19 +40,16 @@ import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; -import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.res.Configuration; import android.content.res.Resources; import android.database.Cursor; -import android.database.DatabaseUtils; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Picture; import android.graphics.PixelFormat; -import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.net.ConnectivityManager; import android.net.NetworkInfo; @@ -60,38 +65,39 @@ import android.os.Handler; import android.os.Message; import android.os.PowerManager; import android.os.Process; -import android.os.ServiceManager; import android.os.SystemClock; import android.provider.Browser; +import android.provider.BrowserContract; import android.provider.ContactsContract; +import android.provider.BrowserContract.Images; import android.provider.ContactsContract.Intents.Insert; import android.provider.Downloads; import android.provider.MediaStore; import android.speech.RecognizerResultsIntent; -import android.text.IClipboard; import android.text.TextUtils; import android.text.format.DateFormat; -import android.util.AttributeSet; import android.util.Log; import android.util.Patterns; +import android.view.ActionMode; import android.view.ContextMenu; +import android.view.ContextMenu.ContextMenuInfo; import android.view.Gravity; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; +import android.view.MenuItem.OnMenuItemClickListener; +import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.Window; import android.view.WindowManager; -import android.view.ContextMenu.ContextMenuInfo; -import android.view.MenuItem.OnMenuItemClickListener; +import android.view.accessibility.AccessibilityManager; import android.webkit.CookieManager; import android.webkit.CookieSyncManager; import android.webkit.DownloadListener; import android.webkit.HttpAuthHandler; -import android.webkit.PluginManager; import android.webkit.SslErrorHandler; import android.webkit.URLUtil; import android.webkit.ValueCallback; @@ -102,41 +108,30 @@ import android.webkit.WebView; import android.widget.EditText; import android.widget.FrameLayout; import android.widget.LinearLayout; +import android.widget.PopupMenu; import android.widget.TextView; import android.widget.Toast; -import android.accounts.Account; -import android.accounts.AccountManager; -import android.accounts.AccountManagerFuture; -import android.accounts.AuthenticatorException; -import android.accounts.OperationCanceledException; -import android.accounts.AccountManagerCallback; - -import com.android.browser.search.SearchEngine; -import com.android.common.Search; -import com.android.common.speech.LoggingEvents; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.net.MalformedURLException; -import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.net.URLEncoder; -import java.text.ParseException; +import java.util.Calendar; import java.util.Date; import java.util.HashMap; -import java.util.HashSet; import java.util.Iterator; -import java.util.List; import java.util.Map; -import java.util.Set; +import java.util.Vector; import java.util.regex.Matcher; import java.util.regex.Pattern; public class BrowserActivity extends Activity - implements View.OnCreateContextMenuListener, DownloadListener { + implements View.OnCreateContextMenuListener, DownloadListener, + PopupMenu.OnMenuItemClickListener { /* Define some aliases to make these debugging flags easier to refer to. * This file imports android.provider.Browser, so we can't just refer to "Browser.DEBUG". @@ -172,6 +167,11 @@ public class BrowserActivity extends Activity */ private FrameLayout mBrowserFrameLayout; + private boolean mXLargeScreenSize; + + private Boolean mIsProviderPresent = null; + private Uri mRlzUri = null; + @Override public void onCreate(Bundle icicle) { if (LOGV_ENABLED) { @@ -187,7 +187,11 @@ public class BrowserActivity extends Activity BitmapFactory.setDefaultConfig(Bitmap.Config.ARGB_8888); } - setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL); + if (AccessibilityManager.getInstance(this).isEnabled()) { + setDefaultKeyMode(DEFAULT_KEYS_DISABLE); + } else { + setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL); + } mResolver = getContentResolver(); @@ -203,6 +207,13 @@ public class BrowserActivity extends Activity mMixLockIcon = Resources.getSystem().getDrawable( android.R.drawable.ic_partial_secure); + // Create the tab control and our initial tab + mTabControl = new TabControl(this); + + mXLargeScreenSize = (getResources().getConfiguration().screenLayout + & Configuration.SCREENLAYOUT_SIZE_MASK) + == Configuration.SCREENLAYOUT_SIZE_XLARGE; + FrameLayout frameLayout = (FrameLayout) getWindow().getDecorView() .findViewById(com.android.internal.R.id.content); mBrowserFrameLayout = (FrameLayout) LayoutInflater.from(this) @@ -214,13 +225,25 @@ public class BrowserActivity extends Activity mCustomViewContainer = (FrameLayout) mBrowserFrameLayout .findViewById(R.id.fullscreen_custom_content); frameLayout.addView(mBrowserFrameLayout, COVER_SCREEN_PARAMS); - mTitleBar = new TitleBar(this); - // mTitleBar will be always shown in the fully loaded mode - mTitleBar.setProgress(100); - mFakeTitleBar = new TitleBar(this); - // Create the tab control and our initial tab - mTabControl = new TabControl(this); + if (mXLargeScreenSize) { + mTitleBar = new TitleBarXLarge(this); + mTitleBar.setProgress(100); + mFakeTitleBar = new TitleBarXLarge(this); + ActionBar actionBar = getActionBar(); + actionBar.setBackgroundDrawable(getResources(). + getDrawable(R.drawable.tabbar_bg)); + mTabBar = new TabBar(this, mTabControl, (TitleBarXLarge) mFakeTitleBar); + actionBar.setCustomNavigationMode(mTabBar); + // disable built in zoom controls + mTabControl.setDisplayZoomControls(false); + } else { + mTitleBar = new TitleBar(this); + // mTitleBar will be always be shown in the fully loaded mode on + // phone + mTitleBar.setProgress(100); + mFakeTitleBar = new TitleBar(this); + } // Open the icon database and retain all the bookmark urls for favicons retainIconsOnStartup(); @@ -263,59 +286,19 @@ public class BrowserActivity extends Activity } }; - IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED); - filter.addAction(Intent.ACTION_PACKAGE_REMOVED); - filter.addDataScheme("package"); - mPackageInstallationReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - final String action = intent.getAction(); - final String packageName = intent.getData() - .getSchemeSpecificPart(); - final boolean replacing = intent.getBooleanExtra( - Intent.EXTRA_REPLACING, false); - if (Intent.ACTION_PACKAGE_REMOVED.equals(action) && replacing) { - // if it is replacing, refreshPlugins() when adding - return; - } + // Unless the last browser usage was within 24 hours, destroy any + // remaining incognito tabs. - if (sGoogleApps.contains(packageName)) { - BrowserActivity.this.packageChanged(packageName, - Intent.ACTION_PACKAGE_ADDED.equals(action)); - } + Calendar lastActiveDate = icicle != null ? (Calendar) icicle.getSerializable("lastActiveDate") : null; + Calendar today = Calendar.getInstance(); + Calendar yesterday = Calendar.getInstance(); + yesterday.add(Calendar.DATE, -1); - PackageManager pm = BrowserActivity.this.getPackageManager(); - PackageInfo pkgInfo = null; - try { - pkgInfo = pm.getPackageInfo(packageName, - PackageManager.GET_PERMISSIONS); - } catch (PackageManager.NameNotFoundException e) { - return; - } - if (pkgInfo != null) { - String permissions[] = pkgInfo.requestedPermissions; - if (permissions == null) { - return; - } - boolean permissionOk = false; - for (String permit : permissions) { - if (PluginManager.PLUGIN_PERMISSION.equals(permit)) { - permissionOk = true; - break; - } - } - if (permissionOk) { - PluginManager.getInstance(BrowserActivity.this) - .refreshPlugins( - Intent.ACTION_PACKAGE_ADDED - .equals(action)); - } - } - } - }; - registerReceiver(mPackageInstallationReceiver, filter); + boolean dontRestoreIncognitoTabs = lastActiveDate == null + || lastActiveDate.before(yesterday) + || lastActiveDate.after(today); - if (!mTabControl.restoreState(icicle)) { + if (!mTabControl.restoreState(icicle, dontRestoreIncognitoTabs)) { // clear up the thumbnail directory if we can't restore the state as // none of the files in the directory are referenced any more. new ClearThumbnails().execute( @@ -323,6 +306,8 @@ public class BrowserActivity extends Activity // there is no quit on Android. But if we can't restore the state, // we can treat it as a new Browser, remove the old session cookies. CookieManager.getInstance().removeSessionCookie(); + // remove any incognito files + WebView.cleanupPrivateBrowsingFiles(this); final Intent intent = getIntent(); final Bundle extra = intent.getExtras(); // Create an initial tab. @@ -337,7 +322,8 @@ public class BrowserActivity extends Activity intent.getData() != null) || RecognizerResultsIntent.ACTION_VOICE_SEARCH_RESULTS .equals(action), - intent.getStringExtra(Browser.EXTRA_APPLICATION_ID), urlData.mUrl); + intent.getStringExtra(Browser.EXTRA_APPLICATION_ID), + urlData.mUrl, false); mTabControl.setCurrentTab(t); attachTabToContentView(t); WebView webView = t.getWebView(); @@ -354,6 +340,10 @@ public class BrowserActivity extends Activity loadUrlDataIn(t, urlData); } } else { + if (dontRestoreIncognitoTabs) { + WebView.cleanupPrivateBrowsingFiles(this); + } + // TabControl.restoreState() will create a new tab even if // restoring the state fails. attachTabToContentView(mTabControl.getCurrentTab()); @@ -373,8 +363,6 @@ public class BrowserActivity extends Activity if (jsFlags.trim().length() != 0) { mTabControl.getCurrentWebView().setJsFlags(jsFlags); } - // Work out which packages are installed on the system. - getInstalledPackages(); // Start watching the default geolocation permissions mSystemAllowGeolocationOrigins @@ -382,6 +370,10 @@ public class BrowserActivity extends Activity mSystemAllowGeolocationOrigins.start(); } + ScrollListener getScrollListener() { + return mTabBar; + } + /** * Feed the previously stored results strings to the BrowserProvider so that * the SearchDialog will show them instead of the standard searches. @@ -621,13 +613,16 @@ public class BrowserActivity extends Activity final ContentResolver cr = mResolver; final String newUrl = url; - new AsyncTask<Void, Void, Void>() { - protected Void doInBackground(Void... unused) { - Browser.updateVisitedHistory(cr, newUrl, false); - Browser.addSearchUrl(cr, newUrl); - return null; - } - }.execute(); + if (mTabControl == null || !mTabControl.getCurrentWebView().isPrivateBrowsingEnabled()) { + new AsyncTask<Void, Void, Void>() { + @Override + protected Void doInBackground(Void... unused) { + Browser.updateVisitedHistory(cr, newUrl, false); + Browser.addSearchUrl(cr, newUrl); + return null; + } + }.execute(); + } SearchEngine searchEngine = mSettings.getSearchEngine(); if (searchEngine == null) return false; @@ -674,12 +669,17 @@ public class BrowserActivity extends Activity url = smartUrlFilter(url); final ContentResolver cr = mResolver; final String newUrl = url; - new AsyncTask<Void, Void, Void>() { - protected Void doInBackground(Void... unused) { - Browser.updateVisitedHistory(cr, newUrl, false); - return null; - } - }.execute(); + if (mTabControl == null + || mTabControl.getCurrentWebView() == null + || !mTabControl.getCurrentWebView().isPrivateBrowsingEnabled()) { + new AsyncTask<Void, Void, Void>() { + @Override + protected Void doInBackground(Void... unused) { + Browser.updateVisitedHistory(cr, newUrl, false); + return null; + } + }.execute(); + } String searchSource = "&source=android-" + GOOGLE_SEARCH_SOURCE_SUGGEST + "&"; if (url.contains(searchSource)) { String source = null; @@ -699,16 +699,14 @@ public class BrowserActivity extends Activity } /* package */ void showVoiceTitleBar(String title) { mTitleBar.setInVoiceMode(true); - mFakeTitleBar.setInVoiceMode(true); - mTitleBar.setDisplayTitle(title); + mFakeTitleBar.setInVoiceMode(true); mFakeTitleBar.setDisplayTitle(title); } /* package */ void revertVoiceTitleBar() { mTitleBar.setInVoiceMode(false); - mFakeTitleBar.setInVoiceMode(false); - mTitleBar.setDisplayTitle(mUrl); + mFakeTitleBar.setInVoiceMode(false); mFakeTitleBar.setDisplayTitle(mUrl); } /* package */ static String fixUrl(String inUrl) { @@ -772,7 +770,7 @@ public class BrowserActivity extends Activity * would change its appearance, use a different TitleBar to show overlayed * at the top of the screen, when the menu is open or the page is loading. */ - private TitleBar mFakeTitleBar; + private TitleBarBase mFakeTitleBar; /** * Keeps track of whether the options menu is open. This is important in @@ -828,9 +826,8 @@ public class BrowserActivity extends Activity return true; } - private void showFakeTitleBar() { - if (mFakeTitleBar.getParent() == null && mActiveTabsPage == null - && !mActivityInPause) { + void showFakeTitleBar() { + if (!isFakeTitleBarShowing() && mActiveTabsPage == null && !mActivityInPause) { WebView mainView = mTabControl.getCurrentWebView(); // if there is no current WebView, don't show the faked title bar; if (mainView == null) { @@ -838,28 +835,31 @@ public class BrowserActivity extends Activity } // Do not need to check for null, since the current tab will have // at least a main WebView, or we would have returned above. - if (dialogIsUp()) { - // Do not show the fake title bar, which would cover up the - // find or select dialog. + if (isInCustomActionMode()) { + // Do not show the fake title bar, while a custom ActionMode + // (i.e. find or select) is showing. return; } - - WindowManager manager - = (WindowManager) getSystemService(Context.WINDOW_SERVICE); - - // Add the title bar to the window manager so it can receive touches - // while the menu is up - WindowManager.LayoutParams params - = new WindowManager.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.WRAP_CONTENT, - WindowManager.LayoutParams.TYPE_APPLICATION, - WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, - PixelFormat.TRANSLUCENT); - params.gravity = Gravity.TOP; - boolean atTop = mainView.getScrollY() == 0; - params.windowAnimations = atTop ? 0 : R.style.TitleBar; - manager.addView(mFakeTitleBar, params); + if (mXLargeScreenSize) { + mContentView.addView(mFakeTitleBar); + mTabBar.onShowTitleBar(); + } else { + WindowManager manager = (WindowManager) getSystemService(Context.WINDOW_SERVICE); + + // Add the title bar to the window manager so it can receive + // touches + // while the menu is up + WindowManager.LayoutParams params = + new WindowManager.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT, + WindowManager.LayoutParams.TYPE_APPLICATION, + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, + PixelFormat.TRANSLUCENT); + params.gravity = Gravity.TOP; + boolean atTop = mainView.getScrollY() == 0; + params.windowAnimations = atTop ? 0 : R.style.TitleBar; + manager.addView(mFakeTitleBar, params); + } } } @@ -876,21 +876,34 @@ public class BrowserActivity extends Activity } } - private void hideFakeTitleBar() { - if (mFakeTitleBar.getParent() == null) return; - WindowManager.LayoutParams params = (WindowManager.LayoutParams) - mFakeTitleBar.getLayoutParams(); - WebView mainView = mTabControl.getCurrentWebView(); - // Although we decided whether or not to animate based on the current - // scroll position, the scroll position may have changed since the - // fake title bar was displayed. Make sure it has the appropriate - // animation/lack thereof before removing. - params.windowAnimations = mainView != null && mainView.getScrollY() == 0 - ? 0 : R.style.TitleBar; - WindowManager manager - = (WindowManager) getSystemService(Context.WINDOW_SERVICE); - manager.updateViewLayout(mFakeTitleBar, params); - manager.removeView(mFakeTitleBar); + void stopScrolling() { + ((ScrollWebView) mTabControl.getCurrentWebView()).stopScroll(); + } + + void hideFakeTitleBar() { + if (!isFakeTitleBarShowing()) return; + if (mXLargeScreenSize) { + mContentView.removeView(mFakeTitleBar); + mTabBar.onHideTitleBar(); + } else { + WindowManager.LayoutParams params = + (WindowManager.LayoutParams) mFakeTitleBar.getLayoutParams(); + WebView mainView = mTabControl.getCurrentWebView(); + // Although we decided whether or not to animate based on the + // current + // scroll position, the scroll position may have changed since the + // fake title bar was displayed. Make sure it has the appropriate + // animation/lack thereof before removing. + params.windowAnimations = + mainView != null && mainView.getScrollY() == 0 ? 0 : R.style.TitleBar; + WindowManager manager = (WindowManager) getSystemService(Context.WINDOW_SERVICE); + manager.updateViewLayout(mFakeTitleBar, params); + manager.removeView(mFakeTitleBar); + } + } + + boolean isFakeTitleBarShowing() { + return (mFakeTitleBar.getParent() != null); } /** @@ -931,6 +944,9 @@ public class BrowserActivity extends Activity // Save all the tabs mTabControl.saveState(outState); + + // Save time so that we know how old incognito tabs (if any) are. + outState.putSerializable("lastActiveDate", Calendar.getInstance()); } @Override @@ -992,8 +1008,6 @@ public class BrowserActivity extends Activity mTabControl.destroy(); WebIconDatabase.getInstance().close(); - unregisterReceiver(mPackageInstallationReceiver); - // Stop watching the default geolocation permissions mSystemAllowGeolocationOrigins.stop(); mSystemAllowGeolocationOrigins = null; @@ -1122,12 +1136,14 @@ public class BrowserActivity extends Activity if (mMenu == null) { return; } + MenuItem dest = mMenu.findItem(R.id.stop_reload_menu_id); MenuItem src = mInLoad ? mMenu.findItem(R.id.stop_menu_id): - mMenu.findItem(R.id.reload_menu_id); - MenuItem dest = mMenu.findItem(R.id.stop_reload_menu_id); - dest.setIcon(src.getIcon()); - dest.setTitle(src.getTitle()); + mMenu.findItem(R.id.reload_menu_id); + if (src != null) { + dest.setIcon(src.getIcon()); + dest.setTitle(src.getTitle()); + } } @Override @@ -1154,7 +1170,6 @@ public class BrowserActivity extends Activity break; // -- Browser context menu case R.id.open_context_menu_id: - case R.id.open_newtab_context_menu_id: case R.id.bookmark_context_menu_id: case R.id.save_link_context_menu_id: case R.id.share_link_context_menu_id: @@ -1278,6 +1293,7 @@ public class BrowserActivity extends Activity */ /* package */ void removeActiveTabPage(boolean needToAttach) { mContentView.removeView(mActiveTabsPage); + mTitleBar.setVisibility(View.VISIBLE); mActiveTabsPage = null; mMenuState = R.id.MAIN_MENU; if (needToAttach) { @@ -1286,23 +1302,22 @@ public class BrowserActivity extends Activity getTopWindow().requestFocus(); } - private WebView showDialog(WebDialog dialog) { - Tab tab = mTabControl.getCurrentTab(); - if (tab.getSubWebView() == null) { - // If the find or select is being performed on the main webview, - // remove the embedded title bar. - WebView mainView = tab.getWebView(); - if (mainView != null) { - mainView.setEmbeddedTitleBar(null); - } - } + @Override + public ActionMode onStartActionMode(ActionMode.Callback callback) { + mActionMode = super.onStartActionMode(callback); hideFakeTitleBar(); - mMenuState = EMPTY_MENU; - return tab.showDialog(dialog); + // Would like to change the MENU, but onEndActionMode may not be called + return mActionMode; } @Override public boolean onOptionsItemSelected(MenuItem item) { + // check the action bar button before mCanChord check, as the prepare call + // doesn't come for action bar buttons + if (item.getItemId() == R.id.newtab) { + openTabToHomePage(); + return true; + } if (!mCanChord) { // The user has already fired a shortcut with this hold down of the // menu key. @@ -1324,17 +1339,22 @@ public class BrowserActivity extends Activity openTabToHomePage(); break; + case R.id.incognito_menu_id: + openIncognitoTab(); + break; + case R.id.goto_menu_id: editUrl(); break; case R.id.bookmarks_menu_id: - bookmarksOrHistoryPicker(false); + bookmarksOrHistoryPicker(false, false); break; case R.id.active_tabs_menu_id: mActiveTabsPage = new ActiveTabsPage(this, mTabControl); removeTabFromContentView(mTabControl.getCurrentTab()); + mTitleBar.setVisibility(View.GONE); hideFakeTitleBar(); mContentView.addView(mActiveTabsPage, COVER_SCREEN_PARAMS); mActiveTabsPage.requestFocus(); @@ -1342,14 +1362,7 @@ public class BrowserActivity extends Activity break; case R.id.add_bookmark_menu_id: - Intent i = new Intent(BrowserActivity.this, - AddBookmarkPage.class); - WebView w = getTopWindow(); - i.putExtra("url", w.getUrl()); - i.putExtra("title", w.getTitle()); - i.putExtra("touch_icon_url", w.getTouchIconUrl()); - i.putExtra("thumbnail", createScreenshot(w)); - startActivity(i); + bookmarkCurrentPage(); break; case R.id.stop_reload_menu_id: @@ -1394,17 +1407,39 @@ public class BrowserActivity extends Activity break; case R.id.find_menu_id: - showFindDialog(); + getTopWindow().showFindDialog(null); break; - case R.id.select_text_id: - if (true) { - Tab currentTab = mTabControl.getCurrentTab(); - if (currentTab != null) { - currentTab.getWebView().setUpSelect(); + case R.id.save_webarchive_menu_id: + if (LOGD_ENABLED) { + Log.d(LOGTAG, "Save as Web Archive"); + } + String state = Environment.getExternalStorageState(); + if (Environment.MEDIA_MOUNTED.equals(state)) { + String directory = Environment.getExternalStoragePublicDirectory( + Environment.DIRECTORY_DOWNLOADS).getAbsolutePath() + File.separator; + File dir = new File(directory); + if (!dir.exists() && !dir.mkdirs()) { + Log.e(LOGTAG, "Save as Web Archive: mkdirs for " + directory + " failed!"); + Toast.makeText(BrowserActivity.this, R.string.webarchive_failed, + Toast.LENGTH_SHORT).show(); + break; } + getTopWindow().saveWebArchive(directory, true, new ValueCallback<String>() { + @Override + public void onReceiveValue(String value) { + if (value != null) { + Toast.makeText(BrowserActivity.this, R.string.webarchive_saved, + Toast.LENGTH_SHORT).show(); + } else { + Toast.makeText(BrowserActivity.this, R.string.webarchive_failed, + Toast.LENGTH_SHORT).show(); + } + } + }); } else { - showSelectDialog(); + Toast.makeText(BrowserActivity.this, R.string.webarchive_failed, + Toast.LENGTH_SHORT).show(); } break; @@ -1413,7 +1448,7 @@ public class BrowserActivity extends Activity break; case R.id.classic_history_menu_id: - bookmarksOrHistoryPicker(true); + bookmarksOrHistoryPicker(true, false); break; case R.id.title_bar_share_page_url: @@ -1426,7 +1461,8 @@ public class BrowserActivity extends Activity currentTab.populatePickerData(); sharePage(this, currentTab.getTitle(), currentTab.getUrl(), currentTab.getFavicon(), - createScreenshot(currentTab.getWebView())); + createScreenshot(currentTab.getWebView(), getDesiredThumbnailWidth(this), + getDesiredThumbnailHeight(this))); break; case R.id.dump_nav_menu_id: @@ -1482,60 +1518,57 @@ public class BrowserActivity extends Activity return true; } - private boolean dialogIsUp() { - return null != mFindDialog && mFindDialog.isVisible() || - null != mSelectDialog && mSelectDialog.isVisible(); + /* package */ void bookmarkCurrentPage() { + Intent i = new Intent(BrowserActivity.this, + AddBookmarkPage.class); + WebView w = getTopWindow(); + i.putExtra("url", w.getUrl()); + i.putExtra("title", w.getTitle()); + i.putExtra("touch_icon_url", w.getTouchIconUrl()); + i.putExtra("thumbnail", createScreenshot(w, getDesiredThumbnailWidth(this), + getDesiredThumbnailHeight(this))); + i.putExtra("url_editable", false); + startActivity(i); } - private boolean closeDialog(WebDialog dialog) { - if (null == dialog || !dialog.isVisible()) return false; - Tab currentTab = mTabControl.getCurrentTab(); - currentTab.closeDialog(dialog); - dialog.dismiss(); - return true; + /* + * True if a custom ActionMode (i.e. find or select) is in use. + */ + private boolean isInCustomActionMode() { + return mActionMode != null; } /* - * Remove the find dialog or select dialog. + * End the current ActionMode. */ - public void closeDialogs() { - if (!(closeDialog(mFindDialog) || closeDialog(mSelectDialog))) return; - // If the Find was being performed in the main WebView, replace the - // embedded title bar. - Tab currentTab = mTabControl.getCurrentTab(); - if (currentTab.getSubWebView() == null) { - WebView mainView = currentTab.getWebView(); - if (mainView != null) { - mainView.setEmbeddedTitleBar(mTitleBar); - } + void endActionMode() { + if (mActionMode != null) { + ActionMode mode = mActionMode; + onEndActionMode(); + mode.finish(); } - mMenuState = R.id.MAIN_MENU; + } + + /* + * Called by find and select when they are finished. Replace title bars + * as necessary. + */ + public void onEndActionMode() { + if (!isInCustomActionMode()) return; if (mInLoad) { // The title bar was hidden, because otherwise it would cover up the - // find or select dialog. Now that the dialog has been removed, + // find or select dialog. Now that the dialog has been removed, // show the fake title bar once again. showFakeTitleBar(); } + // Would like to return the menu state to normal, but this does not + // necessarily get called. + mActionMode = null; } - public void showFindDialog() { - if (null == mFindDialog) { - mFindDialog = new FindDialog(this); - } - showDialog(mFindDialog).setFindIsUp(true); - } - - public void setFindDialogText(String text) { - mFindDialog.setText(text); - } - - public void showSelectDialog() { - if (null == mSelectDialog) { - mSelectDialog = new SelectDialog(this); - } - showDialog(mSelectDialog).setUpSelect(); - mSelectDialog.hideSoftInput(); - } + // For select and find, we keep track of the ActionMode so that + // finish() can be called as desired. + private ActionMode mActionMode; @Override public boolean onPrepareOptionsMenu(Menu menu) { @@ -1575,11 +1608,13 @@ public class BrowserActivity extends Activity final MenuItem home = menu.findItem(R.id.homepage_menu_id); home.setEnabled(!isHome); - menu.findItem(R.id.forward_menu_id) - .setEnabled(canGoForward); + final MenuItem forward = menu.findItem(R.id.forward_menu_id); + forward.setEnabled(canGoForward); - menu.findItem(R.id.new_tab_menu_id).setEnabled( - mTabControl.canCreateNewTab()); + if (!mXLargeScreenSize) { + final MenuItem newtab = menu.findItem(R.id.new_tab_menu_id); + newtab.setEnabled(mTabControl.canCreateNewTab()); + } // decide whether to show the share link option PackageManager pm = getPackageManager(); @@ -1607,7 +1642,7 @@ public class BrowserActivity extends Activity @Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { - if (v instanceof TitleBar) { + if (v instanceof TitleBarBase) { return; } WebView webview = (WebView) v; @@ -1634,7 +1669,7 @@ public class BrowserActivity extends Activity inflater.inflate(R.menu.browsercontext, menu); // Show the correct menu group - String extra = result.getExtra(); + final String extra = result.getExtra(); menu.setGroupVisible(R.id.PHONE_MENU, type == WebView.HitTestResult.PHONE_TYPE); menu.setGroupVisible(R.id.EMAIL_MENU, @@ -1691,8 +1726,23 @@ public class BrowserActivity extends Activity titleView.setText(extra); menu.setHeaderView(titleView); // decide whether to show the open link in new tab option - menu.findItem(R.id.open_newtab_context_menu_id).setVisible( - mTabControl.canCreateNewTab()); + boolean showNewTab = mTabControl.canCreateNewTab(); + MenuItem newTabItem + = menu.findItem(R.id.open_newtab_context_menu_id); + newTabItem.setVisible(showNewTab); + if (showNewTab) { + newTabItem.setOnMenuItemClickListener( + new MenuItem.OnMenuItemClickListener() { + public boolean onMenuItemClick(MenuItem item) { + final Tab parent = mTabControl.getCurrentTab(); + final Tab newTab = openTab(extra, false); + if (newTab != parent) { + parent.addChildTab(newTab); + } + return true; + } + }); + } menu.findItem(R.id.bookmark_context_menu_id).setVisible( Bookmarks.urlHasAcceptableScheme(extra)); PackageManager pm = getPackageManager(); @@ -1752,6 +1802,9 @@ public class BrowserActivity extends Activity } // Request focus on the top window. t.getTopWindow().requestFocus(); + if (mTabControl.getTabChangeListener() != null) { + mTabControl.getTabChangeListener().onCurrentTab(t); + } } // Attach a sub window to the main WebView of the given tab. @@ -1799,7 +1852,7 @@ public class BrowserActivity extends Activity final Tab currentTab = mTabControl.getCurrentTab(); if (mTabControl.canCreateNewTab()) { final Tab tab = mTabControl.createNewTab(closeOnExit, appId, - urlData.mUrl); + urlData.mUrl, false); WebView webview = tab.getWebView(); // If the last tab was removed from the active tabs page, currentTab // will be null. @@ -1825,8 +1878,8 @@ public class BrowserActivity extends Activity } } - private Tab openTab(String url) { - if (mSettings.openInBackground()) { + private Tab openTab(String url, boolean forceForeground) { + if (mSettings.openInBackground() && !forceForeground) { Tab t = mTabControl.createNewTab(); if (t != null) { WebView view = t.getWebView(); @@ -1838,6 +1891,20 @@ public class BrowserActivity extends Activity } } + /* package */ Tab openIncognitoTab() { + if (mTabControl.canCreateNewTab()) { + Tab currentTab = mTabControl.getCurrentTab(); + Tab tab = mTabControl.createNewTab(false, null, null, true); + if (currentTab != null) { + removeTabFromContentView(currentTab); + } + mTabControl.setCurrentTab(tab); + attachTabToContentView(tab); + return tab; + } + return null; + } + private class Copy implements OnMenuItemClickListener { private CharSequence mText; @@ -1898,6 +1965,7 @@ public class BrowserActivity extends Activity return true; } + @Override public void run() { Drawable oldWallpaper = BrowserActivity.this.getWallpaper(); try { @@ -1941,14 +2009,8 @@ public class BrowserActivity extends Activity } private void copy(CharSequence text) { - try { - IClipboard clip = IClipboard.Stub.asInterface(ServiceManager.getService("clipboard")); - if (clip != null) { - clip.setClipboardText(text); - } - } catch (android.os.RemoteException e) { - Log.e(LOGTAG, "Copy failed", e); - } + ClipboardManager cm = (ClipboardManager)getSystemService(Context.CLIPBOARD_SERVICE); + cm.setText(text); } /** @@ -2061,6 +2123,11 @@ public class BrowserActivity extends Activity } mTabControl.setCurrentTab(mTabControl.getTab(currentIndex)); resetTitleIconAndProgress(); + updateLockIconToLatest(); + + if (!mTabControl.hasAnyOpenIncognitoTabs()) { + WebView.cleanupPrivateBrowsingFiles(this); + } } /* package */ void goBackOnePageOrQuit() { @@ -2159,7 +2226,7 @@ public class BrowserActivity extends Activity return true; } else if (mCustomView == null && mActiveTabsPage == null && event.isLongPress()) { - bookmarksOrHistoryPicker(true); + bookmarksOrHistoryPicker(true, false); return true; } break; @@ -2252,11 +2319,19 @@ public class BrowserActivity extends Activity static final int UPDATE_BOOKMARK_THUMBNAIL = 108; + private static final int TOUCH_ICON_DOWNLOADED = 109; + + private static final int OPEN_BOOKMARKS = 201; + // Private handler for handling javascript and saving passwords private Handler mHandler = new Handler() { + @Override public void handleMessage(Message msg) { switch (msg.what) { + case OPEN_BOOKMARKS: + bookmarksOrHistoryPicker(false, false); + break; case FOCUS_NODE_HREF: { String url = (String) msg.getData().get("url"); @@ -2275,13 +2350,6 @@ public class BrowserActivity extends Activity case R.id.view_image_context_menu_id: loadUrlFromContext(getTopWindow(), url); break; - case R.id.open_newtab_context_menu_id: - final Tab parent = mTabControl.getCurrentTab(); - final Tab newTab = openTab(url); - if (newTab != parent) { - parent.addChildTab(newTab); - } - break; case R.id.bookmark_context_menu_id: Intent intent = new Intent(BrowserActivity.this, AddBookmarkPage.class); @@ -2290,41 +2358,8 @@ public class BrowserActivity extends Activity startActivity(intent); break; case R.id.share_link_context_menu_id: - // See if this site has been visited before - StringBuilder sb = new StringBuilder( - Browser.BookmarkColumns.URL + " = "); - DatabaseUtils.appendEscapedSQLString(sb, url); - Cursor c = mResolver.query(Browser.BOOKMARKS_URI, - Browser.HISTORY_PROJECTION, - sb.toString(), - null, + sharePage(BrowserActivity.this, title, url, null, null); - if (c.moveToFirst()) { - // The site has been visited before, so grab the - // info from the database. - Bitmap favicon = null; - Bitmap thumbnail = null; - String linkTitle = c.getString(Browser. - HISTORY_PROJECTION_TITLE_INDEX); - byte[] data = c.getBlob(Browser. - HISTORY_PROJECTION_FAVICON_INDEX); - if (data != null) { - favicon = BitmapFactory.decodeByteArray( - data, 0, data.length); - } - data = c.getBlob(Browser. - HISTORY_PROJECTION_THUMBNAIL_INDEX); - if (data != null) { - thumbnail = BitmapFactory.decodeByteArray( - data, 0, data.length); - } - sharePage(BrowserActivity.this, - linkTitle, url, favicon, thumbnail); - } else { - Browser.sendString(BrowserActivity.this, url, - getString( - R.string.choosertitle_sharevia)); - } break; case R.id.copy_link_context_menu_id: copy(url); @@ -2361,6 +2396,14 @@ public class BrowserActivity extends Activity updateScreenshot(view); } break; + + case TOUCH_ICON_DOWNLOADED: + Bundle b = msg.getData(); + showSaveToHomescreenDialog(b.getString("url"), + b.getString("title"), + (Bitmap) b.getParcelable("touchIcon"), + (Bitmap) b.getParcelable("favicon")); + break; } } }; @@ -2401,7 +2444,8 @@ public class BrowserActivity extends Activity // draw, but the API for that (WebViewCore.pictureReady()) is not // currently accessible here. - final Bitmap bm = createScreenshot(view); + final Bitmap bm = createScreenshot(view, getDesiredThumbnailWidth(this), + getDesiredThumbnailHeight(this)); if (bm == null) { return; } @@ -2413,29 +2457,25 @@ public class BrowserActivity extends Activity new AsyncTask<Void, Void, Void>() { @Override protected Void doInBackground(Void... unused) { - Cursor c = null; + Cursor cursor = null; try { - c = BrowserBookmarksAdapter.queryBookmarksForUrl( - cr, originalUrl, url, true); - if (c != null) { - if (c.moveToFirst()) { - ContentValues values = new ContentValues(); - final ByteArrayOutputStream os - = new ByteArrayOutputStream(); - bm.compress(Bitmap.CompressFormat.PNG, 100, os); - values.put(Browser.BookmarkColumns.THUMBNAIL, - os.toByteArray()); - do { - cr.update(ContentUris.withAppendedId( - Browser.BOOKMARKS_URI, c.getInt(0)), - values, null, null); - } while (c.moveToNext()); - } + cursor = Bookmarks.queryCombinedForUrl(cr, originalUrl, url); + if (cursor != null && cursor.moveToFirst()) { + final ByteArrayOutputStream os = new ByteArrayOutputStream(); + bm.compress(Bitmap.CompressFormat.PNG, 100, os); + + ContentValues values = new ContentValues(); + values.put(Images.THUMBNAIL, os.toByteArray()); + values.put(Images.URL, cursor.getString(0)); + + do { + cr.update(Images.CONTENT_URI, values, null, null); + } while (cursor.moveToNext()); } } catch (IllegalStateException e) { // Ignore } finally { - if (c != null) c.close(); + if (cursor != null) cursor.close(); } return null; } @@ -2443,47 +2483,31 @@ public class BrowserActivity extends Activity } /** - * Values for the size of the thumbnail created when taking a screenshot. - * Lazily initialized. Instead of using these directly, use - * getDesiredThumbnailWidth() or getDesiredThumbnailHeight(). - */ - private static int THUMBNAIL_WIDTH = 0; - private static int THUMBNAIL_HEIGHT = 0; - - /** * Return the desired width for thumbnail screenshots, which are stored in * the database, and used on the bookmarks screen. * @param context Context for finding out the density of the screen. - * @return int desired width for thumbnail screenshot. + * @return desired width for thumbnail screenshot. */ /* package */ static int getDesiredThumbnailWidth(Context context) { - if (THUMBNAIL_WIDTH == 0) { - float density = context.getResources().getDisplayMetrics().density; - THUMBNAIL_WIDTH = (int) (90 * density); - THUMBNAIL_HEIGHT = (int) (80 * density); - } - return THUMBNAIL_WIDTH; + return context.getResources().getDimensionPixelOffset(R.dimen.bookmarkThumbnailWidth); } /** * Return the desired height for thumbnail screenshots, which are stored in * the database, and used on the bookmarks screen. * @param context Context for finding out the density of the screen. - * @return int desired height for thumbnail screenshot. + * @return desired height for thumbnail screenshot. */ /* package */ static int getDesiredThumbnailHeight(Context context) { - // To ensure that they are both initialized. - getDesiredThumbnailWidth(context); - return THUMBNAIL_HEIGHT; + return context.getResources().getDimensionPixelOffset(R.dimen.bookmarkThumbnailHeight); } - private Bitmap createScreenshot(WebView view) { + private Bitmap createScreenshot(WebView view, int width, int height) { Picture thumbnail = view.capturePicture(); if (thumbnail == null) { return null; } - Bitmap bm = Bitmap.createBitmap(getDesiredThumbnailWidth(this), - getDesiredThumbnailHeight(this), Bitmap.Config.RGB_565); + Bitmap bm = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565); Canvas canvas = new Canvas(bm); // May need to tweak these values to determine what is the // best scale factor @@ -2492,8 +2516,7 @@ public class BrowserActivity extends Activity float scaleFactorX = 1.0f; float scaleFactorY = 1.0f; if (thumbnailWidth > 0) { - scaleFactorX = (float) getDesiredThumbnailWidth(this) / - (float)thumbnailWidth; + scaleFactorX = (float) width / (float)thumbnailWidth; } else { return null; } @@ -2503,8 +2526,7 @@ public class BrowserActivity extends Activity // If the device is in landscape and the page is shorter // than the height of the view, stretch the thumbnail to fill the // space. - scaleFactorY = (float) getDesiredThumbnailHeight(this) / - (float)thumbnailHeight; + scaleFactorY = (float) height / (float)thumbnailHeight; } else { // In the portrait case, this looks nice. scaleFactorY = scaleFactorX; @@ -2545,7 +2567,7 @@ public class BrowserActivity extends Activity onProgressChanged(view, INITIAL_PROGRESS); mDidStopLoad = false; if (!mIsNetworkUp) createAndShowNetworkDialog(); - closeDialogs(); + endActionMode(); if (mSettings.isTracing()) { String host; try { @@ -2643,7 +2665,25 @@ public class BrowserActivity extends Activity } } + private void closeEmptyChildTab() { + Tab current = mTabControl.getCurrentTab(); + if (current != null + && current.getWebView().copyBackForwardList().getSize() == 0) { + Tab parent = current.getParentTab(); + if (parent != null) { + switchToTab(mTabControl.getTabIndex(parent)); + closeTab(current); + } + } + } + boolean shouldOverrideUrlLoading(WebView view, String url) { + if (view.isPrivateBrowsingEnabled()) { + // Don't allow urls to leave the browser app when in private browsing mode + loadUrl(view, url); + return true; + } + if (url.startsWith(SCHEME_WTAI)) { // wtai://wp/mc;number // number=string(phone-number) @@ -2652,6 +2692,11 @@ public class BrowserActivity extends Activity Uri.parse(WebView.SCHEME_TEL + url.substring(SCHEME_WTAI_MC.length()))); startActivity(intent); + // before leaving BrowserActivity, close the empty child tab. + // If a new tab is created through JavaScript open to load this + // url, we would like to close it as we will load this url in a + // different Activity. + closeEmptyChildTab(); return true; } // wtai://wp/sd;dtmf @@ -2675,6 +2720,29 @@ public class BrowserActivity extends Activity return false; } + // If this is a Google search, attempt to add an RLZ string (if one isn't already present). + if (rlzProviderPresent()) { + Uri siteUri = Uri.parse(url); + if (needsRlzString(siteUri)) { + String rlz = null; + Cursor cur = null; + try { + cur = getContentResolver().query(getRlzUri(), null, null, null, null); + if (cur != null && cur.moveToFirst() && !cur.isNull(0)) { + url = siteUri.buildUpon() + .appendQueryParameter("rlz", cur.getString(0)) + .build().toString(); + } + } finally { + if (cur != null) { + cur.close(); + } + } + loadUrl(view, url); + return true; + } + } + Intent intent; // perform generic parsing of the URI to turn it into an Intent. try { @@ -2693,6 +2761,11 @@ public class BrowserActivity extends Activity .parse("market://search?q=pname:" + packagename)); intent.addCategory(Intent.CATEGORY_BROWSABLE); startActivity(intent); + // before leaving BrowserActivity, close the empty child tab. + // If a new tab is created through JavaScript open to load this + // url, we would like to close it as we will load this url in a + // different Activity. + closeEmptyChildTab(); return true; } else { return false; @@ -2705,6 +2778,11 @@ public class BrowserActivity extends Activity intent.setComponent(null); try { if (startActivityIfNeeded(intent, -1)) { + // before leaving BrowserActivity, close the empty child tab. + // If a new tab is created through JavaScript open to load this + // url, we would like to close it as we will load this url in a + // different Activity. + closeEmptyChildTab(); return true; } } catch (ActivityNotFoundException ex) { @@ -2713,18 +2791,78 @@ public class BrowserActivity extends Activity } if (mMenuIsDown) { - openTab(url); + openTab(url, false); closeOptionsMenu(); return true; } return false; } + // Determine whether the RLZ provider is present on the system. + private boolean rlzProviderPresent() { + if (mIsProviderPresent == null) { + PackageManager pm = getPackageManager(); + mIsProviderPresent = pm.resolveContentProvider(BrowserSettings.RLZ_PROVIDER, 0) != null; + } + return mIsProviderPresent; + } + + // Retrieve the RLZ access point string and cache the URI used to retrieve RLZ values. + private Uri getRlzUri() { + if (mRlzUri == null) { + String ap = getResources().getString(R.string.rlz_access_point); + mRlzUri = Uri.withAppendedPath(BrowserSettings.RLZ_PROVIDER_URI, ap); + } + return mRlzUri; + } + + // Determine if this URI appears to be for a Google search and does not have an RLZ parameter. + // Taken largely from Chrome source, src/chrome/browser/google_url_tracker.cc + private static boolean needsRlzString(Uri uri) { + String scheme = uri.getScheme(); + if (("http".equals(scheme) || "https".equals(scheme)) && + (uri.getQueryParameter("q") != null) && (uri.getQueryParameter("rlz") == null)) { + String host = uri.getHost(); + if (host == null) { + return false; + } + String[] hostComponents = host.split("\\."); + + if (hostComponents.length < 2) { + return false; + } + int googleComponent = hostComponents.length - 2; + String component = hostComponents[googleComponent]; + if (!"google".equals(component)) { + if (hostComponents.length < 3 || + (!"co".equals(component) && !"com".equals(component))) { + return false; + } + googleComponent = hostComponents.length - 3; + if (!"google".equals(hostComponents[googleComponent])) { + return false; + } + } + + // Google corp network handling. + if (googleComponent > 0 && "corp".equals(hostComponents[googleComponent - 1])) { + return false; + } + + return true; + } + return false; + } + // ------------------------------------------------------------------------- // Helper function for WebChromeClient // ------------------------------------------------------------------------- void onProgressChanged(WebView view, int newProgress) { + + // On the phone, the fake title bar will always cover up the + // regular title bar (or the regular one is offscreen), so only the + // fake title bar needs to change its progress mFakeTitleBar.setProgress(newProgress); if (newProgress == 100) { @@ -2826,15 +2964,138 @@ public class BrowserActivity extends Activity * The Object used to inform the WebView of the file to upload. */ private ValueCallback<Uri> mUploadMessage; + private String mCameraFilePath; + + void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType) { + + final String imageMimeType = "image/*"; + final String videoMimeType = "video/*"; + final String audioMimeType = "audio/*"; + final String mediaSourceKey = "capture"; + final String mediaSourceValueCamera = "camera"; + final String mediaSourceValueFileSystem = "filesystem"; + final String mediaSourceValueCamcorder = "camcorder"; + final String mediaSourceValueMicrophone = "microphone"; + + // media source can be 'filesystem' or 'camera' or 'camcorder' or 'microphone'. + String mediaSource = ""; + + // We add the camera intent if there was no accept type (or '*/*' or 'image/*'). + boolean addCameraIntent = true; + // We add the camcorder intent if there was no accept type (or '*/*' or 'video/*'). + boolean addCamcorderIntent = true; + + if (mUploadMessage != null) { + // Already a file picker operation in progress. + return; + } - void openFileChooser(ValueCallback<Uri> uploadMsg) { - if (mUploadMessage != null) return; mUploadMessage = uploadMsg; + + // Parse the accept type. + String params[] = acceptType.split(";"); + String mimeType = params[0]; + + for (String p : params) { + String[] keyValue = p.split("="); + if (keyValue.length == 2) { + // Process key=value parameters. + if (mediaSourceKey.equals(keyValue[0])) { + mediaSource = keyValue[1]; + } + } + } + + // This intent will display the standard OPENABLE file picker. Intent i = new Intent(Intent.ACTION_GET_CONTENT); i.addCategory(Intent.CATEGORY_OPENABLE); - i.setType("*/*"); - BrowserActivity.this.startActivityForResult(Intent.createChooser(i, - getString(R.string.choose_upload)), FILE_SELECTED); + + // Create an intent to add to the standard file picker that will + // capture an image from the camera. We'll combine this intent with + // the standard OPENABLE picker unless the web developer specifically + // requested the camera or gallery be opened by passing a parameter + // in the accept type. + Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); + File externalDataDir = Environment.getExternalStoragePublicDirectory( + Environment.DIRECTORY_DCIM); + File cameraDataDir = new File(externalDataDir.getAbsolutePath() + + File.separator + "browser-photos"); + cameraDataDir.mkdirs(); + mCameraFilePath = cameraDataDir.getAbsolutePath() + File.separator + + System.currentTimeMillis() + ".jpg"; + cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(new File(mCameraFilePath))); + + Intent camcorderIntent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE); + + Intent soundRecIntent = new Intent(MediaStore.Audio.Media.RECORD_SOUND_ACTION); + + if (mimeType.equals(imageMimeType)) { + i.setType(imageMimeType); + addCamcorderIntent = false; + if (mediaSource.equals(mediaSourceValueCamera)) { + // Specified 'image/*' and requested the camera, so go ahead and launch the camera + // directly. + BrowserActivity.this.startActivityForResult(cameraIntent, FILE_SELECTED); + return; + } else if (mediaSource.equals(mediaSourceValueFileSystem)) { + // Specified filesytem as the source, so don't want to consider the camera. + addCameraIntent = false; + } + } else if (mimeType.equals(videoMimeType)) { + i.setType(videoMimeType); + addCameraIntent = false; + // The camcorder saves it's own file and returns it to us in the intent, so + // we don't need to generate one here. + mCameraFilePath = null; + + if (mediaSource.equals(mediaSourceValueCamcorder)) { + // Specified 'video/*' and requested the camcorder, so go ahead and launch the + // camcorder directly. + BrowserActivity.this.startActivityForResult(camcorderIntent, FILE_SELECTED); + return; + } else if (mediaSource.equals(mediaSourceValueFileSystem)) { + // Specified filesystem as the source, so don't want to consider the camcorder. + addCamcorderIntent = false; + } + } else if (mimeType.equals(audioMimeType)) { + i.setType(audioMimeType); + addCameraIntent = false; + addCamcorderIntent = false; + if (mediaSource.equals(mediaSourceValueMicrophone)) { + // Specified 'audio/*' and requested microphone, so go ahead and launch the sound + // recorder. + BrowserActivity.this.startActivityForResult(soundRecIntent, FILE_SELECTED); + return; + } + // On a default system, there is no single option to open an audio "gallery". Both the + // sound recorder and music browser respond to the OPENABLE/audio/* intent unlike the + // image/* and video/* OPENABLE intents where the image / video gallery are the only + // respondants (and so the user is not prompted by default). + } else { + i.setType("*/*"); + } + + // Combine the chooser and the extra choices (like camera or camcorder) + Intent chooser = new Intent(Intent.ACTION_CHOOSER); + chooser.putExtra(Intent.EXTRA_INTENT, i); + + Vector<Intent> extraInitialIntents = new Vector<Intent>(0); + + if (addCameraIntent) { + extraInitialIntents.add(cameraIntent); + } + + if (addCamcorderIntent) { + extraInitialIntents.add(camcorderIntent); + } + + if (extraInitialIntents.size() > 0) { + Intent[] extraIntents = new Intent[extraInitialIntents.size()]; + chooser.putExtra(Intent.EXTRA_INITIAL_INTENTS, extraInitialIntents.toArray(extraIntents)); + } + + chooser.putExtra(Intent.EXTRA_TITLE, getString(R.string.choose_upload)); + BrowserActivity.this.startActivityForResult(chooser, FILE_SELECTED); } // ------------------------------------------------------------------------- @@ -3021,7 +3282,10 @@ public class BrowserActivity extends Activity * Update the lock icon to correspond to our latest state. */ private void updateLockIconToLatest() { - updateLockIconImage(mTabControl.getCurrentTab().getLockIconType()); + Tab t = mTabControl.getCurrentTab(); + if (t != null) { + updateLockIconImage(t.getLockIconType()); + } } /** @@ -3525,16 +3789,33 @@ public class BrowserActivity extends Activity if (resultCode == RESULT_OK && intent != null) { String data = intent.getAction(); Bundle extras = intent.getExtras(); - if (extras != null && extras.getBoolean("new_window", false)) { - openTab(data); + if (extras != null && + extras.getBoolean( + CombinedBookmarkHistoryActivity.EXTRA_OPEN_NEW_WINDOW, + false)) { + openTab(data, false); + } else if ((extras != null) && + extras.getBoolean(CombinedBookmarkHistoryActivity.NEWTAB_MODE)) { + openTab(data, true); } else { - final Tab currentTab = - mTabControl.getCurrentTab(); + final Tab currentTab = mTabControl.getCurrentTab(); dismissSubWindow(currentTab); if (data != null && data.length() != 0) { loadUrl(getTopWindow(), data); } } + } else if (resultCode == RESULT_CANCELED) { + if (intent != null) { + float evtx = intent.getFloatExtra(CombinedBookmarkHistoryActivity.EVT_X, -1); + float evty = intent.getFloatExtra(CombinedBookmarkHistoryActivity.EVT_Y, -1); + long now = System.currentTimeMillis(); + MotionEvent evt = MotionEvent.obtain(now, now, + MotionEvent.ACTION_DOWN, evtx, evty, 0); + dispatchTouchEvent(evt); + MotionEvent up = MotionEvent.obtain(evt); + up.setAction(MotionEvent.ACTION_UP); + dispatchTouchEvent(up); + } } // Deliberately fall through to PREFERENCES_PAGE, since the // same extra may be attached to the COMBO_PAGE @@ -3551,8 +3832,25 @@ public class BrowserActivity extends Activity if (null == mUploadMessage) break; Uri result = intent == null || resultCode != RESULT_OK ? null : intent.getData(); + + // As we ask the camera to save the result of the user taking + // a picture, the camera application does not return anything other + // than RESULT_OK. So we need to check whether the file we expected + // was written to disk in the in the case that we + // did not get an intent returned but did get a RESULT_OK. If it was, + // we assume that this result has came back from the camera. + if (result == null && intent == null && resultCode == RESULT_OK) { + File cameraFile = new File(mCameraFilePath); + if (cameraFile.exists()) { + result = Uri.fromFile(cameraFile); + // Broadcast to the media scanner that we have a new photo + // so it will be added into the gallery for the user. + sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, result)); + } + } mUploadMessage.onReceiveValue(result); mUploadMessage = null; + mCameraFilePath = null; break; default: break; @@ -3573,12 +3871,78 @@ public class BrowserActivity extends Activity } + /* package */ void promptAddOrInstallBookmark(View anchor) { + PopupMenu popup = new PopupMenu(this, anchor); + popup.getMenuInflater().inflate(R.menu.bookmark_shortcut, popup.getMenu()); + popup.setOnMenuItemClickListener(this); + popup.show(); + } + + /** + * popup menu item click listener + * @param item + */ + public boolean onMenuItemClick(MenuItem item) { + switch(item.getItemId()) { + case R.id.add_bookmark_menu_id: + bookmarkCurrentPage(); + return true; + case R.id.shortcut_to_home_menu_id: + Tab current = mTabControl.getCurrentTab(); + current.populatePickerData(); + String touchIconUrl = mTabControl.getCurrentWebView().getTouchIconUrl(); + if (touchIconUrl != null) { + // Download the touch icon for this site then save + // it to the + // homescreen. + Bundle b = new Bundle(); + b.putString("url", current.getUrl()); + b.putString("title", current.getTitle()); + b.putParcelable("favicon", current.getFavicon()); + Message msg = mHandler.obtainMessage(TOUCH_ICON_DOWNLOADED); + msg.setData(b); + new DownloadTouchIcon(BrowserActivity.this, msg, + mTabControl.getCurrentWebView().getSettings().getUserAgentString()) + .execute(touchIconUrl); + } else { + // add to homescreen, can do it immediately as there + // is no touch + // icon. + showSaveToHomescreenDialog( + current.getUrl(), current.getTitle(), null, current.getFavicon()); + } + return true; + default: + return false; + } + } + + /* package */Dialog makeAddOrInstallDialog() { + final Tab current = mTabControl.getCurrentTab(); + Resources resources = getResources(); + CharSequence[] choices = + {resources.getString(R.string.save_to_bookmarks), + resources.getString(R.string.create_shortcut_bookmark)}; + + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(R.string.add_new_bookmark); + builder.setItems(choices, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int item) { + if (item == 0) { + bookmarkCurrentPage(); + } else if (item == 1) { + } + } + }); + return builder.create(); + } + /** * Open the Go page. * @param startWithHistory If true, open starting on the history tab. * Otherwise, start with the bookmarks tab. */ - /* package */ void bookmarksOrHistoryPicker(boolean startWithHistory) { + /* package */ void bookmarksOrHistoryPicker(boolean startWithHistory, boolean newTabMode) { WebView current = mTabControl.getCurrentWebView(); if (current == null) { return; @@ -3587,7 +3951,8 @@ public class BrowserActivity extends Activity CombinedBookmarkHistoryActivity.class); String title = current.getTitle(); String url = current.getUrl(); - Bitmap thumbnail = createScreenshot(current); + Bitmap thumbnail = createScreenshot(current, getDesiredThumbnailWidth(this), + getDesiredThumbnailHeight(this)); // Just in case the user opens bookmarks before a page finishes loading // so the current history item, and therefore the page, is null. @@ -3609,12 +3974,50 @@ public class BrowserActivity extends Activity intent.putExtra("disable_new_window", !mTabControl.canCreateNewTab()); intent.putExtra("touch_icon_url", current.getTouchIconUrl()); if (startWithHistory) { - intent.putExtra(CombinedBookmarkHistoryActivity.STARTING_TAB, - CombinedBookmarkHistoryActivity.HISTORY_TAB); + intent.putExtra(CombinedBookmarkHistoryActivity.STARTING_FRAGMENT, + CombinedBookmarkHistoryActivity.FRAGMENT_ID_HISTORY); + } + intent.putExtra(CombinedBookmarkHistoryActivity.NEWTAB_MODE, newTabMode); + int top = -1; + int height = -1; + if (mXLargeScreenSize) { + showFakeTitleBar(); + int titleBarHeight = ((TitleBarXLarge)mFakeTitleBar).getHeightWithoutProgress(); + top = mTabBar.getBottom() + titleBarHeight; + height = getTopWindow().getHeight() - titleBarHeight; } + intent.putExtra(CombinedBookmarkHistoryActivity.EXTRA_TOP, top); + intent.putExtra(CombinedBookmarkHistoryActivity.EXTRA_HEIGHT, height); startActivityForResult(intent, COMBO_PAGE); } + private void showSaveToHomescreenDialog(String url, String title, Bitmap touchIcon, + Bitmap favicon) { + Intent intent = new Intent(this, SaveToHomescreenDialog.class); + + // Just in case the user tries to save before a page finishes loading + // so the current history item, and therefore the page, is null. + if (null == url) { + url = mLastEnteredUrl; + // This can happen. + if (null == url) { + url = mSettings.getHomePage(); + } + } + + // In case the web page has not yet received its associated title. + if (title == null) { + title = url; + } + + intent.putExtra("title", title); + intent.putExtra("url", url); + intent.putExtra("favicon", favicon); + intent.putExtra("touchIcon", touchIcon); + startActivity(intent); + } + + // Called when loading from context menu or LOAD_URL message private void loadUrlFromContext(WebView view, String url) { // In case the user enters nothing. @@ -3788,54 +4191,6 @@ public class BrowserActivity extends Activity } } - private void packageChanged(String packageName, boolean wasAdded) { - WebView w = mTabControl.getCurrentWebView(); - if (w == null) { - return; - } - - if (wasAdded) { - w.addPackageName(packageName); - } else { - w.removePackageName(packageName); - } - } - - private void addPackageNames(Set<String> packageNames) { - WebView w = mTabControl.getCurrentWebView(); - if (w == null) { - return; - } - - w.addPackageNames(packageNames); - } - - private void getInstalledPackages() { - AsyncTask<Void, Void, Set<String> > task = - new AsyncTask<Void, Void, Set<String> >() { - protected Set<String> doInBackground(Void... unused) { - Set<String> installedPackages = new HashSet<String>(); - PackageManager pm = BrowserActivity.this.getPackageManager(); - if (pm != null) { - List<PackageInfo> packages = pm.getInstalledPackages(0); - for (PackageInfo p : packages) { - if (BrowserActivity.this.sGoogleApps.contains(p.packageName)) { - installedPackages.add(p.packageName); - } - } - } - - return installedPackages; - } - - // Executes on the UI thread - protected void onPostExecute(Set<String> installedPackages) { - addPackageNames(installedPackages); - } - }; - task.execute(); - } - final static int LOCK_ICON_UNSECURE = 0; final static int LOCK_ICON_SECURE = 1; final static int LOCK_ICON_MIXED = 2; @@ -3856,8 +4211,6 @@ public class BrowserActivity extends Activity private static final int EMPTY_MENU = -1; private Menu mMenu; - private FindDialog mFindDialog; - private SelectDialog mSelectDialog; // Used to prevent chording to result in firing two shortcuts immediately // one after another. Fixes bug 1211714. boolean mCanChord; @@ -3971,7 +4324,8 @@ public class BrowserActivity extends Activity private Toast mStopToast; - private TitleBar mTitleBar; + private TitleBarBase mTitleBar; + private TabBar mTabBar; private LinearLayout mErrorConsoleContainer = null; private boolean mShouldShowErrorConsole = false; @@ -3987,8 +4341,6 @@ public class BrowserActivity extends Activity private IntentFilter mNetworkStateChangedFilter; private BroadcastReceiver mNetworkStateIntentReceiver; - private BroadcastReceiver mPackageInstallationReceiver; - private SystemAllowGeolocationOrigins mSystemAllowGeolocationOrigins; // activity requestCode @@ -4002,14 +4354,6 @@ public class BrowserActivity extends Activity // the video progress view private View mVideoProgressView; - // The Google packages we monitor for the navigator.isApplicationInstalled() - // API. Add as needed. - private static Set<String> sGoogleApps; - static { - sGoogleApps = new HashSet<String>(); - sGoogleApps.add("com.google.android.youtube"); - } - /** * A UrlData class to abstract how the content will be set to WebView. * This base class uses loadUrl to show the content. diff --git a/src/com/android/browser/BrowserBackupAgent.java b/src/com/android/browser/BrowserBackupAgent.java index c968ce5d7..fb1933f3d 100644 --- a/src/com/android/browser/BrowserBackupAgent.java +++ b/src/com/android/browser/BrowserBackupAgent.java @@ -166,7 +166,7 @@ public class BrowserBackupAgent extends BackupAgent { if (DEBUG) Log.v(TAG, "Did not see url: " + mark.url); // Right now we do not reconstruct the db entry in its // entirety; we just add a new bookmark with the same data - Bookmarks.addBookmark(null, getContentResolver(), + Bookmarks.addBookmark(this, false, mark.url, mark.title, null, false); nUnique++; } else { diff --git a/src/com/android/browser/BrowserBookmarksAdapter.java b/src/com/android/browser/BrowserBookmarksAdapter.java index 241b33b75..6efb55414 100644 --- a/src/com/android/browser/BrowserBookmarksAdapter.java +++ b/src/com/android/browser/BrowserBookmarksAdapter.java @@ -16,567 +16,42 @@ package com.android.browser; -import android.content.ContentResolver; -import android.content.ContentUris; -import android.content.ContentValues; -import android.database.ContentObserver; +import android.content.Context; import android.database.Cursor; -import android.database.DataSetObserver; import android.graphics.Bitmap; import android.graphics.BitmapFactory; -import android.net.Uri; -import android.os.AsyncTask; -import android.os.Bundle; -import android.os.Handler; -import android.os.Looper; -import android.provider.Browser; -import android.provider.Browser.BookmarkColumns; -import android.view.KeyEvent; -import android.view.LayoutInflater; import android.view.View; -import android.view.ViewGroup; -import android.webkit.WebIconDatabase; -import android.webkit.WebView; -import android.widget.BaseAdapter; import android.widget.ImageView; +import android.widget.ResourceCursorAdapter; import android.widget.TextView; -import java.io.ByteArrayOutputStream; - -class BrowserBookmarksAdapter extends BaseAdapter { - - private String mCurrentPage; - private String mCurrentTitle; - private Bitmap mCurrentThumbnail; - private Cursor mCursor; - private int mCount; - private BrowserBookmarksPage mBookmarksPage; - private ContentResolver mContentResolver; - private boolean mDataValid; - private BookmarkViewMode mViewMode; - private boolean mMostVisited; - private boolean mNeedsOffset; - private int mExtraOffset; - +class BrowserBookmarksAdapter extends ResourceCursorAdapter { /** * Create a new BrowserBookmarksAdapter. - * @param b BrowserBookmarksPage that instantiated this. - * Necessary so it will adjust its focus - * appropriately after a search. - */ - public BrowserBookmarksAdapter(BrowserBookmarksPage b, String curPage, - String curTitle, Bitmap curThumbnail, boolean createShortcut, - boolean mostVisited) { - mNeedsOffset = !(createShortcut || mostVisited); - mMostVisited = mostVisited; - mExtraOffset = mNeedsOffset ? 1 : 0; - mBookmarksPage = b; - mCurrentPage = b.getResources().getString(R.string.current_page) - + curPage; - mCurrentTitle = curTitle; - mCurrentThumbnail = curThumbnail; - mContentResolver = b.getContentResolver(); - mViewMode = BookmarkViewMode.LIST; - - String whereClause; - // FIXME: Should have a default sort order that the user selects. - String orderBy = Browser.BookmarkColumns.VISITS + " DESC"; - if (mostVisited) { - whereClause = Browser.BookmarkColumns.VISITS + " != 0"; - } else { - whereClause = Browser.BookmarkColumns.BOOKMARK + " = 1"; - } - mCursor = b.managedQuery(Browser.BOOKMARKS_URI, - Browser.HISTORY_PROJECTION, whereClause, null, orderBy); - mCursor.registerContentObserver(new ChangeObserver()); - mCursor.registerDataSetObserver(new MyDataSetObserver()); - - mDataValid = true; - notifyDataSetChanged(); - - mCount = mCursor.getCount() + mExtraOffset; - } - - /** - * Return a hashmap with one row's Title, Url, and favicon. - * @param position Position in the list. - * @return Bundle Stores title, url of row position, favicon, and id - * for the url. Return a blank map if position is out of - * range. - */ - public Bundle getRow(int position) { - Bundle map = new Bundle(); - if (position < mExtraOffset || position >= mCount) { - return map; - } - mCursor.moveToPosition(position- mExtraOffset); - String url = mCursor.getString(Browser.HISTORY_PROJECTION_URL_INDEX); - map.putString(Browser.BookmarkColumns.TITLE, - mCursor.getString(Browser.HISTORY_PROJECTION_TITLE_INDEX)); - map.putString(Browser.BookmarkColumns.URL, url); - byte[] data = mCursor.getBlob(Browser.HISTORY_PROJECTION_FAVICON_INDEX); - if (data != null) { - map.putParcelable(Browser.BookmarkColumns.FAVICON, - BitmapFactory.decodeByteArray(data, 0, data.length)); - } - map.putInt("id", mCursor.getInt(Browser.HISTORY_PROJECTION_ID_INDEX)); - return map; - } - - /** - * Update a row in the database with new information. - * Requeries the database if the information has changed. - * @param map Bundle storing id, title and url of new information - */ - public void updateRow(Bundle map) { - - // Find the record - int id = map.getInt("id"); - int position = -1; - for (mCursor.moveToFirst(); !mCursor.isAfterLast(); mCursor.moveToNext()) { - if (mCursor.getInt(Browser.HISTORY_PROJECTION_ID_INDEX) == id) { - position = mCursor.getPosition(); - break; - } - } - if (position < 0) { - return; - } - - mCursor.moveToPosition(position); - ContentValues values = new ContentValues(); - String title = map.getString(Browser.BookmarkColumns.TITLE); - if (!title.equals(mCursor - .getString(Browser.HISTORY_PROJECTION_TITLE_INDEX))) { - values.put(Browser.BookmarkColumns.TITLE, title); - } - String url = map.getString(Browser.BookmarkColumns.URL); - if (!url.equals(mCursor. - getString(Browser.HISTORY_PROJECTION_URL_INDEX))) { - values.put(Browser.BookmarkColumns.URL, url); - } - - if (map.getBoolean("invalidateThumbnail") == true) { - values.put(Browser.BookmarkColumns.THUMBNAIL, new byte[0]); - } - if (values.size() > 0 - && mContentResolver.update(Browser.BOOKMARKS_URI, values, - "_id = " + id, null) != -1) { - refreshList(); - } - } - - /** - * Delete a row from the database. Requeries the database. - * Does nothing if the provided position is out of range. - * @param position Position in the list. - */ - public void deleteRow(int position) { - if (position < mExtraOffset || position >= getCount()) { - return; - } - mCursor.moveToPosition(position- mExtraOffset); - String url = mCursor.getString(Browser.HISTORY_PROJECTION_URL_INDEX); - String title = mCursor.getString(Browser.HISTORY_PROJECTION_TITLE_INDEX); - Bookmarks.removeFromBookmarks(null, mContentResolver, url, title); - refreshList(); - } - - /** - * Delete all bookmarks from the db. Requeries the database. - * All bookmarks with become visited URLs or if never visited - * are removed */ - public void deleteAllRows() { - StringBuilder deleteIds = null; - StringBuilder convertIds = null; - - for (mCursor.moveToFirst(); !mCursor.isAfterLast(); mCursor.moveToNext()) { - String url = mCursor.getString(Browser.HISTORY_PROJECTION_URL_INDEX); - WebIconDatabase.getInstance().releaseIconForPageUrl(url); - int id = mCursor.getInt(Browser.HISTORY_PROJECTION_ID_INDEX); - int numVisits = mCursor.getInt(Browser.HISTORY_PROJECTION_VISITS_INDEX); - if (0 == numVisits) { - if (deleteIds == null) { - deleteIds = new StringBuilder(); - deleteIds.append("( "); - } else { - deleteIds.append(" OR ( "); - } - deleteIds.append(BookmarkColumns._ID); - deleteIds.append(" = "); - deleteIds.append(id); - deleteIds.append(" )"); - } else { - // It is no longer a bookmark, but it is still a visited site. - if (convertIds == null) { - convertIds = new StringBuilder(); - convertIds.append("( "); - } else { - convertIds.append(" OR ( "); - } - convertIds.append(BookmarkColumns._ID); - convertIds.append(" = "); - convertIds.append(id); - convertIds.append(" )"); - } - } - - if (deleteIds != null) { - mContentResolver.delete(Browser.BOOKMARKS_URI, deleteIds.toString(), - null); - } - if (convertIds != null) { - ContentValues values = new ContentValues(); - values.put(Browser.BookmarkColumns.BOOKMARK, 0); - mContentResolver.update(Browser.BOOKMARKS_URI, values, - convertIds.toString(), null); - } - refreshList(); - } - - /** - * Refresh list to recognize a change in the database. - */ - public void refreshList() { - mCursor.requery(); - mCount = mCursor.getCount() + mExtraOffset; - notifyDataSetChanged(); - } - - /** - * Update the bookmark's favicon. This is a convenience method for updating - * a bookmark favicon for the originalUrl and url of the passed in WebView. - * @param cr The ContentResolver to use. - * @param originalUrl The original url before any redirects. - * @param url The current url. - * @param favicon The favicon bitmap to write to the db. - */ - /* package */ static void updateBookmarkFavicon(final ContentResolver cr, - final String originalUrl, final String url, final Bitmap favicon) { - new AsyncTask<Void, Void, Void>() { - protected Void doInBackground(Void... unused) { - final Cursor c = - queryBookmarksForUrl(cr, originalUrl, url, true); - if (c == null) { - return null; - } - if (c.moveToFirst()) { - ContentValues values = new ContentValues(); - final ByteArrayOutputStream os = - new ByteArrayOutputStream(); - favicon.compress(Bitmap.CompressFormat.PNG, 100, os); - values.put(Browser.BookmarkColumns.FAVICON, - os.toByteArray()); - do { - cr.update(ContentUris.withAppendedId( - Browser.BOOKMARKS_URI, c.getInt(0)), - values, null, null); - } while (c.moveToNext()); - } - c.close(); - return null; - } - }.execute(); + public BrowserBookmarksAdapter(Context context) { + // Make sure to tell the CursorAdapter to avoid the observer and auto-requery + // since the Loader will do that for us. + super(context, R.layout.bookmark_thumbnail, null); } - /* package */ static Cursor queryBookmarksForUrl(ContentResolver cr, - String originalUrl, String url, boolean onlyBookmarks) { - if (cr == null || url == null) { - return null; - } - - // If originalUrl is null, just set it to url. - if (originalUrl == null) { - originalUrl = url; - } - - // Look for both the original url and the actual url. This takes in to - // account redirects. - String originalUrlNoQuery = removeQuery(originalUrl); - String urlNoQuery = removeQuery(url); - originalUrl = originalUrlNoQuery + '?'; - url = urlNoQuery + '?'; - - // Use NoQuery to search for the base url (i.e. if the url is - // http://www.yahoo.com/?rs=1, search for http://www.yahoo.com) - // Use url to match the base url with other queries (i.e. if the url is - // http://www.google.com/m, search for - // http://www.google.com/m?some_query) - final String[] selArgs = new String[] { - originalUrlNoQuery, urlNoQuery, originalUrl, url }; - String where = BookmarkColumns.URL + " == ? OR " - + BookmarkColumns.URL + " == ? OR " - + BookmarkColumns.URL + " LIKE ? || '%' OR " - + BookmarkColumns.URL + " LIKE ? || '%'"; - if (onlyBookmarks) { - where = "(" + where + ") AND " + BookmarkColumns.BOOKMARK + " == 1"; - } - final String[] projection = - new String[] { Browser.BookmarkColumns._ID }; - return cr.query(Browser.BOOKMARKS_URI, projection, where, selArgs, - null); - } + @Override + public void bindView(View view, Context context, Cursor cursor) { + ImageView thumb = (ImageView) view.findViewById(R.id.thumb); + TextView tv = (TextView) view.findViewById(R.id.label); - // Strip the query from the given url. - private static String removeQuery(String url) { - if (url == null) { - return null; - } - int query = url.indexOf('?'); - String noQuery = url; - if (query != -1) { - noQuery = url.substring(0, query); - } - return noQuery; - } - - /** - * How many items should be displayed in the list. - * @return Count of items. - */ - public int getCount() { - if (mDataValid) { - return mCount; - } else { - return 0; - } - } - - public boolean areAllItemsEnabled() { - return true; - } - - public boolean isEnabled(int position) { - return true; - } - - /** - * Get the data associated with the specified position in the list. - * @param position Index of the item whose data we want. - * @return The data at the specified position. - */ - public Object getItem(int position) { - return null; - } + tv.setText(cursor.getString(BookmarksLoader.COLUMN_INDEX_TITLE)); - /** - * Get the row id associated with the specified position in the list. - * @param position Index of the item whose row id we want. - * @return The id of the item at the specified position. - */ - public long getItemId(int position) { - return position; - } - - /* package */ void switchViewMode(BookmarkViewMode viewMode) { - mViewMode = viewMode; - } - - /* package */ void populateBookmarkItem(BookmarkItem b, int position) { - mCursor.moveToPosition(position - mExtraOffset); - String url = mCursor.getString(Browser.HISTORY_PROJECTION_URL_INDEX); - b.setUrl(url); - b.setName(mCursor.getString(Browser.HISTORY_PROJECTION_TITLE_INDEX)); - byte[] data = mCursor.getBlob(Browser.HISTORY_PROJECTION_FAVICON_INDEX); - Bitmap bitmap = null; - if (data == null) { - bitmap = CombinedBookmarkHistoryActivity.getIconListenerSet() - .getFavicon(url); - } else { - bitmap = BitmapFactory.decodeByteArray(data, 0, data.length); - } - b.setFavicon(bitmap); - } - - /** - * Get a View that displays the data at the specified position - * in the list. - * @param position Index of the item whose view we want. - * @return A View corresponding to the data at the specified position. - */ - public View getView(int position, View convertView, ViewGroup parent) { - if (!mDataValid) { - throw new IllegalStateException( - "this should only be called when the cursor is valid"); - } - if (position < 0 || position > mCount) { - throw new AssertionError( - "BrowserBookmarksAdapter tried to get a view out of range"); - } - if (mViewMode == BookmarkViewMode.GRID) { - if (convertView == null || convertView instanceof AddNewBookmark - || convertView instanceof BookmarkItem) { - LayoutInflater factory = LayoutInflater.from(mBookmarksPage); - convertView - = factory.inflate(R.layout.bookmark_thumbnail, null); - } - View holder = convertView.findViewById(R.id.holder); - ImageView thumb = (ImageView) convertView.findViewById(R.id.thumb); - TextView tv = (TextView) convertView.findViewById(R.id.label); - - if (0 == position && mNeedsOffset) { - // This is to create a bookmark for the current page. - holder.setVisibility(View.VISIBLE); - tv.setText(mCurrentTitle); - - if (mCurrentThumbnail != null) { - thumb.setImageBitmap(mCurrentThumbnail); - } else { - thumb.setImageResource( - R.drawable.browser_thumbnail); - } - return convertView; - } - holder.setVisibility(View.GONE); - mCursor.moveToPosition(position - mExtraOffset); - tv.setText(mCursor.getString( - Browser.HISTORY_PROJECTION_TITLE_INDEX)); - Bitmap thumbnail = getScreenshot(position); - if (thumbnail == null) { - thumb.setImageResource(R.drawable.browser_thumbnail); - } else { - thumb.setImageBitmap(thumbnail); - } - - return convertView; - - } - if (position == 0 && mNeedsOffset) { - AddNewBookmark b; - if (convertView instanceof AddNewBookmark) { - b = (AddNewBookmark) convertView; - } else { - b = new AddNewBookmark(mBookmarksPage); - } - b.setUrl(mCurrentPage); - return b; - } - if (mMostVisited) { - if (convertView == null || !(convertView instanceof HistoryItem)) { - convertView = new HistoryItem(mBookmarksPage); - } - } else { - if (convertView == null || !(convertView instanceof BookmarkItem)) { - convertView = new BookmarkItem(mBookmarksPage); - } - } - bind((BookmarkItem) convertView, position); - if (mMostVisited) { - ((HistoryItem) convertView).setIsBookmark( - getIsBookmark(position)); - } - return convertView; - } - - /** - * Return the title for this item in the list. - */ - public String getTitle(int position) { - return getString(Browser.HISTORY_PROJECTION_TITLE_INDEX, position); - } - - /** - * Return the Url for this item in the list. - */ - public String getUrl(int position) { - return getString(Browser.HISTORY_PROJECTION_URL_INDEX, position); - } - - /** - * Return the screenshot for this item in the list. - */ - public Bitmap getScreenshot(int position) { - return getBitmap(Browser.HISTORY_PROJECTION_THUMBNAIL_INDEX, position); - } - - /** - * Return the favicon for this item in the list. - */ - public Bitmap getFavicon(int position) { - return getBitmap(Browser.HISTORY_PROJECTION_FAVICON_INDEX, position); - } - - public Bitmap getTouchIcon(int position) { - return getBitmap(Browser.HISTORY_PROJECTION_TOUCH_ICON_INDEX, position); - } - - private Bitmap getBitmap(int cursorIndex, int position) { - if (position < mExtraOffset || position > mCount) { - return null; - } - mCursor.moveToPosition(position - mExtraOffset); - byte[] data = mCursor.getBlob(cursorIndex); - if (data == null) { - return null; - } - return BitmapFactory.decodeByteArray(data, 0, data.length); - } - - /** - * Return whether or not this item represents a bookmarked site. - */ - public boolean getIsBookmark(int position) { - if (position < mExtraOffset || position > mCount) { - return false; - } - mCursor.moveToPosition(position - mExtraOffset); - return (1 == mCursor.getInt(Browser.HISTORY_PROJECTION_BOOKMARK_INDEX)); - } - - /** - * Private helper function to return the title or url. - */ - private String getString(int cursorIndex, int position) { - if (position < mExtraOffset || position > mCount) { - return ""; - } - mCursor.moveToPosition(position- mExtraOffset); - return mCursor.getString(cursorIndex); - } - - private void bind(BookmarkItem b, int position) { - mCursor.moveToPosition(position- mExtraOffset); - - b.setName(mCursor.getString(Browser.HISTORY_PROJECTION_TITLE_INDEX)); - String url = mCursor.getString(Browser.HISTORY_PROJECTION_URL_INDEX); - b.setUrl(url); - byte[] data = mCursor.getBlob(Browser.HISTORY_PROJECTION_FAVICON_INDEX); + Bitmap thumbnail = null; + byte[] data = cursor.getBlob(BookmarksLoader.COLUMN_INDEX_THUMBNAIL); if (data != null) { - b.setFavicon(BitmapFactory.decodeByteArray(data, 0, data.length)); - } else { - b.setFavicon(CombinedBookmarkHistoryActivity.getIconListenerSet() - .getFavicon(url)); + thumbnail = BitmapFactory.decodeByteArray(data, 0, data.length); } - } - private class ChangeObserver extends ContentObserver { - public ChangeObserver() { - super(new Handler(Looper.getMainLooper())); - } - - @Override - public boolean deliverSelfNotifications() { - return true; - } - - @Override - public void onChange(boolean selfChange) { - refreshList(); - } - } - - private class MyDataSetObserver extends DataSetObserver { - @Override - public void onChanged() { - mDataValid = true; - notifyDataSetChanged(); - } - - @Override - public void onInvalidated() { - mDataValid = false; - notifyDataSetInvalidated(); + if (thumbnail == null) { + thumb.setImageResource(R.drawable.browser_thumbnail); + } else { + thumb.setImageBitmap(thumbnail); } } } diff --git a/src/com/android/browser/BrowserBookmarksPage.java b/src/com/android/browser/BrowserBookmarksPage.java index dd01009f8..039aca08f 100644 --- a/src/com/android/browser/BrowserBookmarksPage.java +++ b/src/com/android/browser/BrowserBookmarksPage.java @@ -18,73 +18,232 @@ package com.android.browser; import android.app.Activity; import android.app.AlertDialog; +import android.app.Fragment; +import android.app.LoaderManager; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Context; +import android.content.CursorLoader; import android.content.DialogInterface; import android.content.Intent; +import android.content.Loader; import android.content.SharedPreferences; -import android.content.SharedPreferences.Editor; +import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.BitmapFactory; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Paint; -import android.graphics.Path; -import android.graphics.PorterDuff; -import android.graphics.PorterDuffXfermode; -import android.graphics.Rect; -import android.graphics.RectF; import android.net.Uri; -import android.os.AsyncTask; import android.os.Bundle; -import android.os.Handler; -import android.os.Message; -import android.os.ServiceManager; -import android.provider.Browser; -import android.text.IClipboard; -import android.util.Log; +import android.preference.PreferenceManager; +import android.provider.BrowserContract; +import android.provider.BrowserContract.Accounts; +import android.text.TextUtils; +import android.util.Pair; import android.view.ContextMenu; -import android.view.Menu; +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.OnClickListener; import android.view.ViewGroup; -import android.view.ContextMenu.ContextMenuInfo; import android.webkit.WebIconDatabase.IconListener; +import android.widget.Adapter; import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.AdapterView.OnItemSelectedListener; +import android.widget.ArrayAdapter; +import android.widget.Button; import android.widget.GridView; -import android.widget.ListView; +import android.widget.Spinner; import android.widget.Toast; -/*package*/ enum BookmarkViewMode { NONE, GRID, LIST } +import java.util.ArrayList; +import java.util.Stack; + /** * View showing the user's bookmarks in the browser. */ -public class BrowserBookmarksPage extends Activity implements - View.OnCreateContextMenuListener { - - private BookmarkViewMode mViewMode = BookmarkViewMode.NONE; - private GridView mGridPage; - private ListView mVerticalList; - private BrowserBookmarksAdapter mBookmarksAdapter; - private static final int BOOKMARKS_SAVE = 1; - private boolean mDisableNewWindow; - private BookmarkItem mContextHeader; - private AddNewBookmark mAddHeader; - private boolean mCanceled = false; - private boolean mCreateShortcut; - private boolean mMostVisited; - private View mEmptyView; - private int mIconSize; - // XXX: There is no public string defining this intent so if Home changes - // the value, we have to update this string. - private static final String INSTALL_SHORTCUT = - "com.android.launcher.action.INSTALL_SHORTCUT"; - - private final static String LOGTAG = "browser"; - private final static String PREF_BOOKMARK_VIEW_MODE = "pref_bookmark_view_mode"; - private final static String PREF_MOST_VISITED_VIEW_MODE = "pref_most_visited_view_mode"; +public class BrowserBookmarksPage extends Fragment implements View.OnCreateContextMenuListener, + LoaderManager.LoaderCallbacks<Cursor>, OnItemClickListener, IconListener, OnClickListener, + OnItemSelectedListener { + + static final int BOOKMARKS_SAVE = 1; + static final String LOGTAG = "browser"; + + static final int LOADER_BOOKMARKS = 1; + static final int LOADER_ACCOUNTS = 2; + static final int LOADER_ACCOUNTS_THEN_BOOKMARKS = 3; + + static final String EXTRA_SHORTCUT = "create_shortcut"; + static final String EXTRA_DISABLE_WINDOW = "disable_new_window"; + + public static final String PREF_ACCOUNT_TYPE = "acct_type"; + public static final String PREF_ACCOUNT_NAME = "acct_name"; + + static final String DEFAULT_ACCOUNT = "local"; + + BookmarksHistoryCallbacks mCallbacks; + GridView mGrid; + Spinner mAccountSelector; + BrowserBookmarksAdapter mAdapter; + boolean mDisableNewWindow; + BookmarkItem mContextHeader; + boolean mCanceled = false; + boolean mCreateShortcut; + View mEmptyView; + View mContentView; + Stack<Pair<String, Uri>> mFolderStack = new Stack<Pair<String, Uri>>(); + Button mUpButton; + + @Override + public Loader<Cursor> onCreateLoader(int id, Bundle args) { + switch (id) { + case LOADER_BOOKMARKS: { + String accountType = null; + String accountName = null; + if (args != null) { + accountType = args.getString(BookmarksLoader.ARG_ACCOUNT_TYPE); + accountName = args.getString(BookmarksLoader.ARG_ACCOUNT_NAME); + } + return new BookmarksLoader(getActivity(), accountType, accountName); + } + + case LOADER_ACCOUNTS: + case LOADER_ACCOUNTS_THEN_BOOKMARKS: { + return new CursorLoader(getActivity(), Accounts.CONTENT_URI, + new String[] { Accounts.ACCOUNT_TYPE, Accounts.ACCOUNT_NAME }, null, null, + null); + } + } + throw new UnsupportedOperationException("Unknown loader id " + id); + } + + @Override + public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) { + switch (loader.getId()) { + case LOADER_BOOKMARKS: { + // Set the visibility of the empty vs. content views + if (cursor == null || cursor.getCount() == 0) { + mEmptyView.setVisibility(View.VISIBLE); + mGrid.setVisibility(View.GONE); + } else { + mEmptyView.setVisibility(View.GONE); + mGrid.setVisibility(View.VISIBLE); + } + + // Fill in the "up" button if needed + BookmarksLoader bl = (BookmarksLoader) loader; + String path = bl.getUri().getPath(); + boolean rootFolder = + BrowserContract.Bookmarks.CONTENT_URI_DEFAULT_FOLDER.getPath().equals(path); + if (rootFolder) { + mUpButton.setText(R.string.defaultBookmarksUpButton); + mUpButton.setEnabled(false); + } else { + mUpButton.setText(mFolderStack.peek().first); + mUpButton.setEnabled(true); + } + mUpButton.setVisibility(View.VISIBLE); + + // Give the new data to the adapter + mAdapter.changeCursor(cursor); + + break; + } + + case LOADER_ACCOUNTS: + case LOADER_ACCOUNTS_THEN_BOOKMARKS: { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences( + getActivity()); + String storedAccountType = prefs.getString(PREF_ACCOUNT_TYPE, null); + String storedAccountName = prefs.getString(PREF_ACCOUNT_NAME, null); + String accountType = + TextUtils.isEmpty(storedAccountType) ? DEFAULT_ACCOUNT : storedAccountType; + String accountName = + TextUtils.isEmpty(storedAccountName) ? DEFAULT_ACCOUNT : storedAccountName; + + Bundle args = null; + if (cursor == null || !cursor.moveToFirst()) { + // No accounts, set the prefs to the default + accountType = DEFAULT_ACCOUNT; + accountName = DEFAULT_ACCOUNT; + mAccountSelector.setVisibility(View.GONE); + } else { + int accountPosition = -1; + + if (!DEFAULT_ACCOUNT.equals(accountType) && + !DEFAULT_ACCOUNT.equals(accountName)) { + // Check to see if the account in prefs still exists + cursor.moveToFirst(); + do { + if (accountType.equals(cursor.getString(0)) + && accountName.equals(cursor.getString(1))) { + accountPosition = cursor.getPosition(); + break; + } + } while (cursor.moveToNext()); + } + + if (accountPosition == -1) { + // No account is set in prefs and there is at least one, + // so pick the first one as the default + cursor.moveToFirst(); + accountType = cursor.getString(0); + accountName = cursor.getString(1); + accountPosition = 0; + } + + args = new Bundle(); + args.putString(BookmarksLoader.ARG_ACCOUNT_TYPE, accountType); + args.putString(BookmarksLoader.ARG_ACCOUNT_NAME, accountName); + + // Setup the account selector if there is more than 1 account + if (cursor.getCount() > 1) { + ArrayList<String> accounts = new ArrayList<String>(); + cursor.moveToFirst(); + do { + accounts.add(cursor.getString(1)); + } while (cursor.moveToNext()); + + mAccountSelector.setAdapter(new ArrayAdapter<String>(getActivity(), + android.R.layout.simple_list_item_1, android.R.id.text1, accounts)); + mAccountSelector.setVisibility(View.VISIBLE); + mAccountSelector.setSelection(accountPosition); + } + } + if (!accountType.equals(storedAccountType) + || !accountName.equals(storedAccountName)) { + prefs.edit() + .putString(PREF_ACCOUNT_TYPE, accountType) + .putString(PREF_ACCOUNT_NAME, accountName) + .apply(); + } + if (loader.getId() == LOADER_ACCOUNTS_THEN_BOOKMARKS) { + getLoaderManager().initLoader(LOADER_BOOKMARKS, args, this); + } + + break; + } + } + } + + @Override + public void onClick(View view) { + if (view == mUpButton) { + Pair<String, Uri> pair = mFolderStack.pop(); + BookmarksLoader loader = + (BookmarksLoader) ((Loader) getLoaderManager().getLoader(LOADER_BOOKMARKS)); + loader.setUri(pair.second); + loader.forceLoad(); + } + } @Override public boolean onContextItemSelected(MenuItem item) { + final Activity activity = getActivity(); // It is possible that the view has been canceled when we get to // this point as back has a higher priority if (mCanceled) { @@ -98,9 +257,6 @@ public class BrowserBookmarksPage extends Activity implements } switch (item.getItemId()) { - case R.id.new_context_menu_id: - saveCurrentPage(); - break; case R.id.open_context_menu_id: loadUrl(i.position); break; @@ -108,533 +264,272 @@ public class BrowserBookmarksPage extends Activity implements editBookmark(i.position); break; case R.id.shortcut_context_menu_id: - final Intent send = createShortcutIntent(i.position); - send.setAction(INSTALL_SHORTCUT); - sendBroadcast(send); + activity.sendBroadcast(createShortcutIntent(i.position)); break; case R.id.delete_context_menu_id: - if (mMostVisited) { - Browser.deleteFromHistory(getContentResolver(), - getUrl(i.position)); - refreshList(); - } else { - displayRemoveBookmarkDialog(i.position); - } + displayRemoveBookmarkDialog(i.position); break; case R.id.new_window_context_menu_id: openInNewWindow(i.position); break; - case R.id.share_link_context_menu_id: - BrowserActivity.sharePage(BrowserBookmarksPage.this, - mBookmarksAdapter.getTitle(i.position), getUrl(i.position), - getFavicon(i.position), - mBookmarksAdapter.getScreenshot(i.position)); + case R.id.share_link_context_menu_id: { + Cursor cursor = (Cursor) mAdapter.getItem(i.position); + BrowserActivity.sharePage(activity, + cursor.getString(BookmarksLoader.COLUMN_INDEX_TITLE), + cursor.getString(BookmarksLoader.COLUMN_INDEX_URL), + getBitmap(cursor, BookmarksLoader.COLUMN_INDEX_FAVICON), + getBitmap(cursor, BookmarksLoader.COLUMN_INDEX_THUMBNAIL)); break; + } case R.id.copy_url_context_menu_id: copy(getUrl(i.position)); break; - case R.id.homepage_context_menu_id: - BrowserSettings.getInstance().setHomePage(this, - getUrl(i.position)); - Toast.makeText(this, R.string.homepage_set, - Toast.LENGTH_LONG).show(); + case R.id.homepage_context_menu_id: { + BrowserSettings.getInstance().setHomePage(activity, getUrl(i.position)); + Toast.makeText(activity, R.string.homepage_set, Toast.LENGTH_LONG).show(); break; + } // Only for the Most visited page - case R.id.save_to_bookmarks_menu_id: - boolean isBookmark; - String name; - String url; - if (mViewMode == BookmarkViewMode.GRID) { - isBookmark = mBookmarksAdapter.getIsBookmark(i.position); - name = mBookmarksAdapter.getTitle(i.position); - url = mBookmarksAdapter.getUrl(i.position); - } else { - HistoryItem historyItem = ((HistoryItem) i.targetView); - isBookmark = historyItem.isBookmark(); - name = historyItem.getName(); - url = historyItem.getUrl(); - } + case R.id.save_to_bookmarks_menu_id: { + Cursor cursor = (Cursor) mAdapter.getItem(i.position); + String name = cursor.getString(BookmarksLoader.COLUMN_INDEX_TITLE); + String url = cursor.getString(BookmarksLoader.COLUMN_INDEX_URL); // If the site is bookmarked, the item becomes remove from // bookmarks. - if (isBookmark) { - Bookmarks.removeFromBookmarks(this, getContentResolver(), url, name); - } else { - Browser.saveBookmark(this, name, url); - } + Bookmarks.removeFromBookmarks(activity, activity.getContentResolver(), url, name); break; + } default: return super.onContextItemSelected(item); } return true; } - @Override - public void onCreateContextMenu(ContextMenu menu, View v, - ContextMenuInfo menuInfo) { - AdapterView.AdapterContextMenuInfo i = - (AdapterView.AdapterContextMenuInfo) menuInfo; - - MenuInflater inflater = getMenuInflater(); - if (mMostVisited) { - inflater.inflate(R.menu.historycontext, menu); - } else { - inflater.inflate(R.menu.bookmarkscontext, menu); - } - - if (0 == i.position && !mMostVisited) { - menu.setGroupVisible(R.id.CONTEXT_MENU, false); - if (mAddHeader == null) { - mAddHeader = new AddNewBookmark(BrowserBookmarksPage.this); - } else if (mAddHeader.getParent() != null) { - ((ViewGroup) mAddHeader.getParent()). - removeView(mAddHeader); - } - mAddHeader.setUrl(getIntent().getStringExtra("url")); - menu.setHeaderView(mAddHeader); - return; - } - if (mMostVisited) { - if ((mViewMode == BookmarkViewMode.LIST - && ((HistoryItem) i.targetView).isBookmark()) - || mBookmarksAdapter.getIsBookmark(i.position)) { - MenuItem item = menu.findItem( - R.id.save_to_bookmarks_menu_id); - item.setTitle(R.string.remove_from_bookmarks); - } - } else { - // The historycontext menu has no ADD_MENU group. - menu.setGroupVisible(R.id.ADD_MENU, false); - } - if (mDisableNewWindow) { - menu.findItem(R.id.new_window_context_menu_id).setVisible( - false); - } - if (mContextHeader == null) { - mContextHeader = new BookmarkItem(BrowserBookmarksPage.this); - } else if (mContextHeader.getParent() != null) { - ((ViewGroup) mContextHeader.getParent()). - removeView(mContextHeader); - } - if (mViewMode == BookmarkViewMode.GRID) { - mBookmarksAdapter.populateBookmarkItem(mContextHeader, - i.position); - } else { - BookmarkItem b = (BookmarkItem) i.targetView; - b.copyTo(mContextHeader); - } - menu.setHeaderView(mContextHeader); + Bitmap getBitmap(Cursor cursor, int columnIndex) { + byte[] data = cursor.getBlob(columnIndex); + if (data == null) { + return null; } + return BitmapFactory.decodeByteArray(data, 0, data.length); + } - /** - * Create a new BrowserBookmarksPage. - */ @Override - protected void onCreate(Bundle icicle) { - super.onCreate(icicle); + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { + AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo; - // Grab the app icon size as a resource. - mIconSize = getResources().getDimensionPixelSize( - android.R.dimen.app_icon_size); + final Activity activity = getActivity(); + MenuInflater inflater = activity.getMenuInflater(); + inflater.inflate(R.menu.bookmarkscontext, menu); - Intent intent = getIntent(); - if (Intent.ACTION_CREATE_SHORTCUT.equals(intent.getAction())) { - mCreateShortcut = true; + if (mDisableNewWindow) { + menu.findItem(R.id.new_window_context_menu_id).setVisible(false); } - mDisableNewWindow = intent.getBooleanExtra("disable_new_window", - false); - mMostVisited = intent.getBooleanExtra("mostVisited", false); - if (mCreateShortcut) { - setTitle(R.string.browser_bookmarks_page_bookmarks_text); + if (mContextHeader == null) { + mContextHeader = new BookmarkItem(activity); + } else if (mContextHeader.getParent() != null) { + ((ViewGroup) mContextHeader.getParent()).removeView(mContextHeader); } - setContentView(R.layout.empty_history); - mEmptyView = findViewById(R.id.empty_view); - mEmptyView.setVisibility(View.GONE); - - SharedPreferences p = getPreferences(MODE_PRIVATE); + populateBookmarkItem(mAdapter, mContextHeader, info.position); - // See if the user has set a preference for the view mode of their - // bookmarks. Otherwise default to grid mode. - BookmarkViewMode preference = BookmarkViewMode.NONE; - if (mMostVisited) { - // For the most visited page, only use list mode. - preference = BookmarkViewMode.LIST; - } else { - preference = BookmarkViewMode.values()[p.getInt( - PREF_BOOKMARK_VIEW_MODE, BookmarkViewMode.GRID.ordinal())]; - } - switchViewMode(preference); - - final boolean createShortcut = mCreateShortcut; - final boolean mostVisited = mMostVisited; - final String url = intent.getStringExtra("url"); - final String title = intent.getStringExtra("title"); - final Bitmap thumbnail = - (Bitmap) intent.getParcelableExtra("thumbnail"); - new AsyncTask<Void, Void, Void>() { - @Override - protected Void doInBackground(Void... unused) { - BrowserBookmarksAdapter adapter = new BrowserBookmarksAdapter( - BrowserBookmarksPage.this, - url, - title, - thumbnail, - createShortcut, - mostVisited); - mHandler.obtainMessage(ADAPTER_CREATED, adapter).sendToTarget(); - return null; - } - }.execute(); + menu.setHeaderView(mContextHeader); } - @Override - protected void onDestroy() { - mHandler.removeCallbacksAndMessages(null); - super.onDestroy(); + private void populateBookmarkItem(BrowserBookmarksAdapter adapter, BookmarkItem item, + int position) { + Cursor cursor = (Cursor) mAdapter.getItem(position); + String url = cursor.getString(BookmarksLoader.COLUMN_INDEX_URL); + item.setUrl(url); + item.setName(cursor.getString(BookmarksLoader.COLUMN_INDEX_TITLE)); + Bitmap bitmap = getBitmap(cursor, BookmarksLoader.COLUMN_INDEX_FAVICON); + if (bitmap == null) { + bitmap = CombinedBookmarkHistoryActivity.getIconListenerSet().getFavicon(url); + } + item.setFavicon(bitmap); } /** - * Set the ContentView to be either the grid of thumbnails or the vertical - * list. + * Create a new BrowserBookmarksPage. */ - private void switchViewMode(BookmarkViewMode viewMode) { - if (mViewMode == viewMode) { - return; - } + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); - mViewMode = viewMode; + Bundle args = getArguments(); + mCreateShortcut = args == null ? false : args.getBoolean("create_shortcut", false); + mDisableNewWindow = args == null ? false : args.getBoolean("disable_new_window", false); + } - // Update the preferences to make the new view mode sticky. - Editor ed = getPreferences(MODE_PRIVATE).edit(); - if (mMostVisited) { - ed.putInt(PREF_MOST_VISITED_VIEW_MODE, mViewMode.ordinal()); - } else { - ed.putInt(PREF_BOOKMARK_VIEW_MODE, mViewMode.ordinal()); - } - ed.apply(); + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + mCallbacks = (BookmarksHistoryCallbacks) activity; + } - if (mBookmarksAdapter != null) { - mBookmarksAdapter.switchViewMode(viewMode); + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + Context context = getActivity(); + + View root = inflater.inflate(R.layout.bookmarks, container, false); + mEmptyView = root.findViewById(android.R.id.empty); + mContentView = root.findViewById(android.R.id.content); + + mGrid = (GridView) root.findViewById(R.id.grid); + mGrid.setOnItemClickListener(this); + mGrid.setColumnWidth(BrowserActivity.getDesiredThumbnailWidth(getActivity())); + if (!mCreateShortcut) { + mGrid.setOnCreateContextMenuListener(this); } - if (mViewMode == BookmarkViewMode.GRID) { - if (mGridPage == null) { - mGridPage = new GridView(this); - if (mBookmarksAdapter != null) { - mGridPage.setAdapter(mBookmarksAdapter); - } - mGridPage.setOnItemClickListener(mListener); - mGridPage.setNumColumns(GridView.AUTO_FIT); - mGridPage.setColumnWidth( - BrowserActivity.getDesiredThumbnailWidth(this)); - mGridPage.setFocusable(true); - mGridPage.setFocusableInTouchMode(true); - mGridPage.setSelector(android.R.drawable.gallery_thumb); - float density = getResources().getDisplayMetrics().density; - mGridPage.setVerticalSpacing((int) (14 * density)); - mGridPage.setHorizontalSpacing((int) (8 * density)); - mGridPage.setStretchMode(GridView.STRETCH_SPACING); - mGridPage.setScrollBarStyle(View.SCROLLBARS_INSIDE_INSET); - mGridPage.setDrawSelectorOnTop(true); - if (mMostVisited) { - mGridPage.setEmptyView(mEmptyView); - } - if (!mCreateShortcut) { - mGridPage.setOnCreateContextMenuListener(this); - } - } - addContentView(mGridPage, FULL_SCREEN_PARAMS); - if (mVerticalList != null) { - ViewGroup parent = (ViewGroup) mVerticalList.getParent(); - if (parent != null) { - parent.removeView(mVerticalList); - } + + mAccountSelector = (Spinner) root.findViewById(R.id.accounts); + mAccountSelector.setOnItemSelectedListener(this); + mAccountSelector.setVisibility(View.INVISIBLE); + + mUpButton = (Button) root.findViewById(R.id.up); + mUpButton.setEnabled(false); + mUpButton.setOnClickListener(this); + mUpButton.setVisibility(View.GONE); + + mAdapter = new BrowserBookmarksAdapter(getActivity()); + mGrid.setAdapter(mAdapter); + + // Start the loaders + LoaderManager lm = getLoaderManager(); + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + String accountType = prefs.getString(PREF_ACCOUNT_TYPE, null); + String accountName = prefs.getString(PREF_ACCOUNT_NAME, null); + if (!TextUtils.isEmpty(accountType) && !TextUtils.isEmpty(accountName)) { + // There is an account set, load up that one + Bundle args = null; + if (!DEFAULT_ACCOUNT.equals(accountType) && !DEFAULT_ACCOUNT.equals(accountName)) { + args = new Bundle(); + args.putString(BookmarksLoader.ARG_ACCOUNT_TYPE, accountType); + args.putString(BookmarksLoader.ARG_ACCOUNT_NAME, accountName); } + lm.initLoader(LOADER_BOOKMARKS, args, this); + lm.initLoader(LOADER_ACCOUNTS, null, this); } else { - if (null == mVerticalList) { - ListView listView = new ListView(this); - if (mBookmarksAdapter != null) { - listView.setAdapter(mBookmarksAdapter); - } - listView.setDrawSelectorOnTop(false); - listView.setVerticalScrollBarEnabled(true); - listView.setOnItemClickListener(mListener); - if (mMostVisited) { - listView.setEmptyView(mEmptyView); - } - if (!mCreateShortcut) { - listView.setOnCreateContextMenuListener(this); - } - mVerticalList = listView; - } - addContentView(mVerticalList, FULL_SCREEN_PARAMS); - if (mGridPage != null) { - ViewGroup parent = (ViewGroup) mGridPage.getParent(); - if (parent != null) { - parent.removeView(mGridPage); - } - } + // No account set, load them first + lm.initLoader(LOADER_ACCOUNTS_THEN_BOOKMARKS, null, this); } + + // Add our own listener in case there are favicons that have yet to be loaded. + CombinedBookmarkHistoryActivity.getIconListenerSet().addListener(this); + + return root; } - private static final ViewGroup.LayoutParams FULL_SCREEN_PARAMS - = new ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT); - - private static final int SAVE_CURRENT_PAGE = 1000; - private static final int ADAPTER_CREATED = 1001; - private final Handler mHandler = new Handler() { - @Override - public void handleMessage(Message msg) { - switch (msg.what) { - case SAVE_CURRENT_PAGE: - saveCurrentPage(); - break; - case ADAPTER_CREATED: - mBookmarksAdapter = (BrowserBookmarksAdapter) msg.obj; - mBookmarksAdapter.switchViewMode(mViewMode); - if (mGridPage != null) { - mGridPage.setAdapter(mBookmarksAdapter); - } - if (mVerticalList != null) { - mVerticalList.setAdapter(mBookmarksAdapter); - } - // Add our own listener in case there are favicons that - // have yet to be loaded. - if (mMostVisited) { - IconListener listener = new IconListener() { - public void onReceivedIcon(String url, - Bitmap icon) { - if (mGridPage != null) { - mGridPage.setAdapter(mBookmarksAdapter); - } - if (mVerticalList != null) { - mVerticalList.setAdapter(mBookmarksAdapter); - } - } - }; - CombinedBookmarkHistoryActivity.getIconListenerSet() - .addListener(listener); - } - break; - } + @Override + public void onReceivedIcon(String url, Bitmap icon) { + // A new favicon has been loaded, so let anything attached to the adapter know about it + // so new icons will be loaded. + mAdapter.notifyDataSetChanged(); + } + + @Override + public void onItemClick(AdapterView parent, View v, int position, long id) { + // It is possible that the view has been canceled when we get to + // this point as back has a higher priority + if (mCanceled) { + android.util.Log.e(LOGTAG, "item clicked when dismissing"); + return; } - }; - - private AdapterView.OnItemClickListener mListener = new AdapterView.OnItemClickListener() { - public void onItemClick(AdapterView parent, View v, int position, long id) { - // It is possible that the view has been canceled when we get to - // this point as back has a higher priority - if (mCanceled) { - android.util.Log.e(LOGTAG, "item clicked when dismissing"); - return; - } - if (!mCreateShortcut) { - if (0 == position && !mMostVisited) { - // XXX: Work-around for a framework issue. - mHandler.sendEmptyMessage(SAVE_CURRENT_PAGE); - } else { - loadUrl(position); - } - } else { - final Intent intent = createShortcutIntent(position); - setResultToParent(RESULT_OK, intent); - finish(); - } + + if (mCreateShortcut) { + Intent intent = createShortcutIntent(position); + // the activity handles the intent in startActivityFromFragment + startActivity(intent); + return; } - }; - private Intent createShortcutIntent(int position) { - String url = getUrl(position); - String title = getBookmarkTitle(position); - Bitmap touchIcon = getTouchIcon(position); - - final Intent i = new Intent(); - final Intent shortcutIntent = new Intent(Intent.ACTION_VIEW, - Uri.parse(url)); - long urlHash = url.hashCode(); - long uniqueId = (urlHash << 32) | shortcutIntent.hashCode(); - shortcutIntent.putExtra(Browser.EXTRA_APPLICATION_ID, - Long.toString(uniqueId)); - i.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent); - i.putExtra(Intent.EXTRA_SHORTCUT_NAME, title); - // Use the apple-touch-icon if available - if (touchIcon != null) { - // Make a copy so we can modify the pixels. We can't use - // createScaledBitmap or copy since they will preserve the config - // and lose the ability to add alpha. - Bitmap bm = Bitmap.createBitmap(mIconSize, mIconSize, - Bitmap.Config.ARGB_8888); - Canvas canvas = new Canvas(bm); - Rect src = new Rect(0, 0, touchIcon.getWidth(), - touchIcon.getHeight()); - Rect dest = new Rect(0, 0, bm.getWidth(), bm.getHeight()); - - // Paint used for scaling the bitmap and drawing the rounded rect. - Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); - paint.setFilterBitmap(true); - canvas.drawBitmap(touchIcon, src, dest, paint); - - // Construct a path from a round rect. This will allow drawing with - // an inverse fill so we can punch a hole using the round rect. - Path path = new Path(); - path.setFillType(Path.FillType.INVERSE_WINDING); - RectF rect = new RectF(0, 0, bm.getWidth(), bm.getHeight()); - rect.inset(1, 1); - path.addRoundRect(rect, 8f, 8f, Path.Direction.CW); - - // Reuse the paint and clear the outside of the rectangle. - paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); - canvas.drawPath(path, paint); - - i.putExtra(Intent.EXTRA_SHORTCUT_ICON, bm); + Cursor cursor = (Cursor) mAdapter.getItem(position); + boolean isFolder = cursor.getInt(BookmarksLoader.COLUMN_INDEX_IS_FOLDER) != 0; + if (!isFolder) { + mCallbacks.onUrlSelected(getUrl(position), false); } else { - Bitmap favicon = getFavicon(position); - if (favicon == null) { - i.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, - Intent.ShortcutIconResource.fromContext( - BrowserBookmarksPage.this, - R.drawable.ic_launcher_shortcut_browser_bookmark)); + String title; + if (mFolderStack.size() != 0) { + title = cursor.getString(BookmarksLoader.COLUMN_INDEX_TITLE); } else { - Bitmap icon = BitmapFactory.decodeResource(getResources(), - R.drawable.ic_launcher_shortcut_browser_bookmark_icon); - - // Make a copy of the regular icon so we can modify the pixels. - Bitmap copy = icon.copy(Bitmap.Config.ARGB_8888, true); - Canvas canvas = new Canvas(copy); - - // Make a Paint for the white background rectangle and for - // filtering the favicon. - Paint p = new Paint(Paint.ANTI_ALIAS_FLAG - | Paint.FILTER_BITMAP_FLAG); - p.setStyle(Paint.Style.FILL_AND_STROKE); - p.setColor(Color.WHITE); - - final float density = - getResources().getDisplayMetrics().density; - // Create a rectangle that is slightly wider than the favicon - final float iconSize = 16 * density; // 16x16 favicon - final float padding = 2 * density; // white padding around icon - final float rectSize = iconSize + 2 * padding; - - final Rect iconBounds = - new Rect(0, 0, icon.getWidth(), icon.getHeight()); - final float x = iconBounds.exactCenterX() - (rectSize / 2); - // Note: Subtract 2 dip from the y position since the box is - // slightly higher than center. Use padding since it is already - // 2 * density. - final float y = iconBounds.exactCenterY() - (rectSize / 2) - - padding; - RectF r = new RectF(x, y, x + rectSize, y + rectSize); - - // Draw a white rounded rectangle behind the favicon - canvas.drawRoundRect(r, 2, 2, p); - - // Draw the favicon in the same rectangle as the rounded - // rectangle but inset by the padding - // (results in a 16x16 favicon). - r.inset(padding, padding); - canvas.drawBitmap(favicon, null, r, p); - i.putExtra(Intent.EXTRA_SHORTCUT_ICON, copy); + // TODO localize + title = "Bookmarks"; } + LoaderManager manager = getLoaderManager(); + BookmarksLoader loader = + (BookmarksLoader) ((Loader) manager.getLoader(LOADER_BOOKMARKS)); + mFolderStack.push(new Pair(title, loader.getUri())); + Uri uri = ContentUris.withAppendedId( + BrowserContract.Bookmarks.CONTENT_URI_DEFAULT_FOLDER, id); + loader.setUri(uri); + loader.forceLoad(); } - // Do not allow duplicate items - i.putExtra("duplicate", false); - return i; - } - - private void saveCurrentPage() { - Intent i = new Intent(BrowserBookmarksPage.this, - AddBookmarkPage.class); - i.putExtras(getIntent()); - startActivityForResult(i, BOOKMARKS_SAVE); - } - - private void loadUrl(int position) { - Intent intent = (new Intent()).setAction(getUrl(position)); - setResultToParent(RESULT_OK, intent); - finish(); } @Override - public boolean onCreateOptionsMenu(Menu menu) { - boolean result = super.onCreateOptionsMenu(menu); - if (!mCreateShortcut && !mMostVisited) { - MenuInflater inflater = getMenuInflater(); - inflater.inflate(R.menu.bookmarks, menu); - return true; - } - return result; + public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { + Adapter adapter = parent.getAdapter(); + String accountType = "com.google"; + String accountName = adapter.getItem(position).toString(); + + // Remember the selection for later + PreferenceManager.getDefaultSharedPreferences(getActivity()).edit() + .putString(PREF_ACCOUNT_TYPE, accountType) + .putString(PREF_ACCOUNT_NAME, accountName) + .apply(); + + Bundle args = new Bundle(); + args.putString(BookmarksLoader.ARG_ACCOUNT_TYPE, accountType); + args.putString(BookmarksLoader.ARG_ACCOUNT_NAME, accountName); + getLoaderManager().restartLoader(LOADER_BOOKMARKS, args, this); } @Override - public boolean onPrepareOptionsMenu(Menu menu) { - boolean result = super.onPrepareOptionsMenu(menu); - if (mCreateShortcut || mMostVisited || mBookmarksAdapter == null - || mBookmarksAdapter.getCount() == 0) { - // No need to show the menu if there are no items. - return result; - } - MenuItem switchItem = menu.findItem(R.id.switch_mode_menu_id); - int titleResId; - int iconResId; - if (mViewMode == BookmarkViewMode.GRID) { - titleResId = R.string.switch_to_list; - iconResId = R.drawable.ic_menu_list; - } else { - titleResId = R.string.switch_to_thumbnails; - iconResId = R.drawable.ic_menu_thumbnail; - } - switchItem.setTitle(titleResId); - switchItem.setIcon(iconResId); - return true; + public void onNothingSelected(AdapterView<?> parent) { + // Do nothing } - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.new_context_menu_id: - saveCurrentPage(); - break; - - case R.id.switch_mode_menu_id: - if (mViewMode == BookmarkViewMode.GRID) { - switchViewMode(BookmarkViewMode.LIST); - } else { - switchViewMode(BookmarkViewMode.GRID); - } - break; + private Intent createShortcutIntent(int position) { + Cursor cursor = (Cursor) mAdapter.getItem(position); + String url = cursor.getString(BookmarksLoader.COLUMN_INDEX_URL); + String title = cursor.getString(BookmarksLoader.COLUMN_INDEX_URL); + Bitmap touchIcon = getBitmap(cursor, BookmarksLoader.COLUMN_INDEX_TOUCH_ICON); + Bitmap favicon = getBitmap(cursor, BookmarksLoader.COLUMN_INDEX_FAVICON); + return BookmarkUtils.createAddToHomeIntent(getActivity(), url, title, touchIcon, favicon); + } - default: - return super.onOptionsItemSelected(item); - } - return true; + private void loadUrl(int position) { + mCallbacks.onUrlSelected(getUrl(position), false); } private void openInNewWindow(int position) { - Bundle b = new Bundle(); - b.putBoolean("new_window", true); - setResultToParent(RESULT_OK, - (new Intent()).setAction(getUrl(position)).putExtras(b)); - - finish(); + mCallbacks.onUrlSelected(getUrl(position), true); } - private void editBookmark(int position) { - Intent intent = new Intent(BrowserBookmarksPage.this, - AddBookmarkPage.class); - intent.putExtra("bookmark", getRow(position)); + Intent intent = new Intent(getActivity(), AddBookmarkPage.class); + Cursor cursor = (Cursor) mAdapter.getItem(position); + Bundle item = new Bundle(); + item.putString(BrowserContract.Bookmarks.TITLE, + cursor.getString(BookmarksLoader.COLUMN_INDEX_TITLE)); + item.putString(BrowserContract.Bookmarks.URL, + cursor.getString(BookmarksLoader.COLUMN_INDEX_URL)); + byte[] data = cursor.getBlob(BookmarksLoader.COLUMN_INDEX_FAVICON); + if (data != null) { + item.putParcelable(BrowserContract.Bookmarks.FAVICON, + BitmapFactory.decodeByteArray(data, 0, data.length)); + } + item.putInt("id", cursor.getInt(BookmarksLoader.COLUMN_INDEX_ID)); + intent.putExtra("bookmark", item); startActivityForResult(intent, BOOKMARKS_SAVE); } @Override - protected void onActivityResult(int requestCode, int resultCode, - Intent data) { + public void onActivityResult(int requestCode, int resultCode, Intent data) { switch(requestCode) { case BOOKMARKS_SAVE: - if (resultCode == RESULT_OK) { + if (resultCode == Activity.RESULT_OK) { Bundle extras; if (data != null && (extras = data.getExtras()) != null) { // If there are extras, then we need to save @@ -642,118 +537,88 @@ public class BrowserBookmarksPage extends Activity implements String title = extras.getString("title"); String url = extras.getString("url"); if (title != null && url != null) { - mBookmarksAdapter.updateRow(extras); + updateRow(extras); } - } else { - // extras == null then a new bookmark was added to - // the database. - refreshList(); } } break; - default: + } + } + + /** + * Update a row in the database with new information. + * @param map Bundle storing id, title and url of new information + */ + public void updateRow(Bundle map) { + + // Find the record + int id = map.getInt("id"); + int position = -1; + Cursor cursor = mAdapter.getCursor(); + for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { + if (cursor.getInt(BookmarksLoader.COLUMN_INDEX_ID) == id) { + position = cursor.getPosition(); break; + } + } + if (position < 0) { + return; + } + + cursor.moveToPosition(position); + ContentValues values = new ContentValues(); + String title = map.getString(BrowserContract.Bookmarks.TITLE); + if (!title.equals(cursor.getString(BookmarksLoader.COLUMN_INDEX_TITLE))) { + values.put(BrowserContract.Bookmarks.TITLE, title); + } + String url = map.getString(BrowserContract.Bookmarks.URL); + if (!url.equals(cursor.getString(BookmarksLoader.COLUMN_INDEX_URL))) { + values.put(BrowserContract.Bookmarks.URL, url); + } + + if (map.getBoolean("invalidateThumbnail") == true) { + values.putNull(BrowserContract.Bookmarks.THUMBNAIL); + } + + if (values.size() > 0) { + getActivity().getContentResolver().update( + ContentUris.withAppendedId(BrowserContract.Bookmarks.CONTENT_URI, id), + values, null, null); } } - private void displayRemoveBookmarkDialog(int position) { + private void displayRemoveBookmarkDialog(final int position) { // Put up a dialog asking if the user really wants to // delete the bookmark - final int deletePos = position; - new AlertDialog.Builder(this) + Cursor cursor = (Cursor) mAdapter.getItem(position); + Context context = getActivity(); + final ContentResolver resolver = context.getContentResolver(); + final Uri uri = ContentUris.withAppendedId(BrowserContract.Bookmarks.CONTENT_URI, + cursor.getLong(BookmarksLoader.COLUMN_INDEX_ID)); + + new AlertDialog.Builder(context) .setTitle(R.string.delete_bookmark) .setIcon(android.R.drawable.ic_dialog_alert) - .setMessage(getText(R.string.delete_bookmark_warning).toString().replace( - "%s", getBookmarkTitle(deletePos))) + .setMessage(context.getString(R.string.delete_bookmark_warning, + cursor.getString(BookmarksLoader.COLUMN_INDEX_TITLE))) .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { - deleteBookmark(deletePos); + resolver.delete(uri, null, null); } }) .setNegativeButton(R.string.cancel, null) .show(); } - /** - * Refresh the shown list after the database has changed. - */ - private void refreshList() { - if (mBookmarksAdapter == null) return; - mBookmarksAdapter.refreshList(); - } - - /** - * Return a hashmap representing the currently highlighted row. - */ - public Bundle getRow(int position) { - return mBookmarksAdapter == null ? null - : mBookmarksAdapter.getRow(position); - } - - /** - * Return the url of the currently highlighted row. - */ - public String getUrl(int position) { - return mBookmarksAdapter == null ? null - : mBookmarksAdapter.getUrl(position); - } - - /** - * Return the favicon of the currently highlighted row. - */ - public Bitmap getFavicon(int position) { - return mBookmarksAdapter == null ? null - : mBookmarksAdapter.getFavicon(position); - } - - private Bitmap getTouchIcon(int position) { - return mBookmarksAdapter == null ? null - : mBookmarksAdapter.getTouchIcon(position); + private String getUrl(int position) { + Cursor cursor = (Cursor) mAdapter.getItem(position); + return cursor.getString(BookmarksLoader.COLUMN_INDEX_URL); } private void copy(CharSequence text) { - try { - IClipboard clip = IClipboard.Stub.asInterface(ServiceManager.getService("clipboard")); - if (clip != null) { - clip.setClipboardText(text); - } - } catch (android.os.RemoteException e) { - Log.e(LOGTAG, "Copy failed", e); - } - } - - public String getBookmarkTitle(int position) { - return mBookmarksAdapter == null ? null - : mBookmarksAdapter.getTitle(position); - } - - /** - * Delete the currently highlighted row. - */ - public void deleteBookmark(int position) { - if (mBookmarksAdapter == null) return; - mBookmarksAdapter.deleteRow(position); - } - - @Override - public void onBackPressed() { - setResultToParent(RESULT_CANCELED, null); - mCanceled = true; - super.onBackPressed(); - } - - // This Activity is generally a sub-Activity of - // CombinedBookmarkHistoryActivity. In that situation, we need to pass our - // result code up to our parent. However, if someone calls this Activity - // directly, then this has no parent, and it needs to set it on itself. - private void setResultToParent(int resultCode, Intent data) { - Activity parent = getParent(); - if (parent == null) { - setResult(resultCode, data); - } else { - ((CombinedBookmarkHistoryActivity) parent).setResultFromChild( - resultCode, data); - } + ClipboardManager cm = (ClipboardManager) getActivity().getSystemService( + Context.CLIPBOARD_SERVICE); + cm.setPrimaryClip(ClipData.newRawUri(null, null, Uri.parse(text.toString()))); } } diff --git a/src/com/android/browser/BrowserDownloadAdapter.java b/src/com/android/browser/BrowserDownloadAdapter.java index 0f8f721e2..6f498b694 100644 --- a/src/com/android/browser/BrowserDownloadAdapter.java +++ b/src/com/android/browser/BrowserDownloadAdapter.java @@ -56,7 +56,8 @@ public class BrowserDownloadAdapter extends DateSortedExpandableListAdapter { private int mDateColumnId; public BrowserDownloadAdapter(Context context, Cursor c, int index) { - super(context, c, index); + super(context, index); + changeCursor(c); mTitleColumnId = c.getColumnIndexOrThrow(Downloads.Impl.COLUMN_TITLE); mDescColumnId = c.getColumnIndexOrThrow(Downloads.Impl.COLUMN_DESCRIPTION); mStatusColumnId = c.getColumnIndexOrThrow(Downloads.Impl.COLUMN_STATUS); diff --git a/src/com/android/browser/BrowserDownloadPage.java b/src/com/android/browser/BrowserDownloadPage.java index 18faf8b36..a897f9939 100644 --- a/src/com/android/browser/BrowserDownloadPage.java +++ b/src/com/android/browser/BrowserDownloadPage.java @@ -18,13 +18,10 @@ package com.android.browser; import android.app.AlertDialog; import android.app.ExpandableListActivity; -import android.content.ActivityNotFoundException; +import android.content.ContentUris; import android.content.ContentValues; import android.content.DialogInterface; import android.content.Intent; -import android.content.ContentUris; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; import android.database.ContentObserver; import android.database.Cursor; import android.net.Uri; @@ -34,17 +31,13 @@ import android.provider.Downloads; import android.util.Log; import android.view.ContextMenu; import android.view.ContextMenu.ContextMenuInfo; -import android.view.LayoutInflater; import android.view.Menu; -import android.view.MenuItem; import android.view.MenuInflater; +import android.view.MenuItem; import android.view.View; -import android.view.ViewGroup.LayoutParams; -import android.widget.AdapterView; import android.widget.ExpandableListView; import java.io.File; -import java.util.List; /** * View showing the user's current browser downloads @@ -63,6 +56,7 @@ public class BrowserDownloadPage extends ExpandableListActivity { // Only meaningful while a ContentObserver is registered. The ContextMenu // will be reopened on this View. private View mSelectedView; + private Handler mHandler; private final static String LOGTAG = "BrowserDownloadPage"; @Override @@ -85,7 +79,7 @@ public class BrowserDownloadPage extends ExpandableListActivity { Downloads.Impl._DATA, Downloads.Impl.COLUMN_MIME_TYPE}, null, Downloads.Impl.COLUMN_LAST_MODIFICATION + " DESC"); - + mHandler = new Handler(); // only attach everything to the listbox if we can access // the download database. Otherwise, just show it empty if (mDownloadCursor != null) { @@ -241,8 +235,8 @@ public class BrowserDownloadPage extends ExpandableListActivity { */ private class ChangeObserver extends ContentObserver { private final Uri mTrack; - public ChangeObserver(Uri track) { - super(new Handler()); + public ChangeObserver(Uri track, Handler handler) { + super(handler); mTrack = track; } @@ -313,7 +307,7 @@ public class BrowserDownloadPage extends ExpandableListActivity { getContentResolver().unregisterContentObserver( mContentObserver); } - mContentObserver = new ChangeObserver(track); + mContentObserver = new ChangeObserver(track, mHandler); mSelectedView = v; getContentResolver().registerContentObserver(track, false, mContentObserver); diff --git a/src/com/android/browser/BrowserHistoryPage.java b/src/com/android/browser/BrowserHistoryPage.java index 23080f86b..2295804e5 100644 --- a/src/com/android/browser/BrowserHistoryPage.java +++ b/src/com/android/browser/BrowserHistoryPage.java @@ -17,171 +17,193 @@ package com.android.browser; import android.app.Activity; -import android.app.ExpandableListActivity; +import android.app.Fragment; +import android.app.LoaderManager.LoaderCallbacks; +import android.content.ClipboardManager; import android.content.Context; +import android.content.CursorLoader; import android.content.Intent; +import android.content.Loader; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.os.Bundle; -import android.os.ServiceManager; import android.provider.Browser; -import android.text.IClipboard; -import android.util.Log; +import android.provider.BrowserContract.History; import android.view.ContextMenu; +import android.view.ContextMenu.ContextMenuInfo; +import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; -import android.view.ViewGroup.LayoutParams; -import android.view.ContextMenu.ContextMenuInfo; -import android.view.ViewStub; import android.webkit.WebIconDatabase.IconListener; -import android.widget.ExpandableListAdapter; import android.widget.ExpandableListView; import android.widget.ExpandableListView.ExpandableListContextMenuInfo; +import android.widget.ExpandableListView.OnChildClickListener; import android.widget.Toast; /** * Activity for displaying the browser's history, divided into * days of viewing. */ -public class BrowserHistoryPage extends ExpandableListActivity { - private HistoryAdapter mAdapter; - private boolean mDisableNewWindow; - private HistoryItem mContextHeader; +public class BrowserHistoryPage extends Fragment + implements LoaderCallbacks<Cursor>, OnChildClickListener { + + static final int LOADER_HISTORY = 1; - private final static String LOGTAG = "browser"; + BookmarksHistoryCallbacks mCallbacks; + ExpandableListView mList; + View mEmptyView; + HistoryAdapter mAdapter; + boolean mDisableNewWindow; + HistoryItem mContextHeader; // Implementation of WebIconDatabase.IconListener - private class IconReceiver implements IconListener { + class IconReceiver implements IconListener { + @Override public void onReceivedIcon(String url, Bitmap icon) { - setListAdapter(mAdapter); + mAdapter.notifyDataSetChanged(); } } + // Instance of IconReceiver - private final IconReceiver mIconReceiver = new IconReceiver(); - - /** - * Report back to the calling activity to load a site. - * @param url Site to load. - * @param newWindow True if the URL should be loaded in a new window - */ - private void loadUrl(String url, boolean newWindow) { - Intent intent = new Intent().setAction(url); - if (newWindow) { - Bundle b = new Bundle(); - b.putBoolean("new_window", true); - intent.putExtras(b); - } - setResultToParent(RESULT_OK, intent); - finish(); + final IconReceiver mIconReceiver = new IconReceiver(); + + static interface HistoryQuery { + static final String[] PROJECTION = new String[] { + History._ID, // 0 + History.DATE_LAST_VISITED, // 1 + History.TITLE, // 2 + History.URL, // 3 + History.FAVICON, // 4 + }; + + static final int INDEX_ID = 0; + static final int INDEX_DATE_LAST_VISITED = 1; + static final int INDEX_TITE = 2; + static final int INDEX_URL = 3; + static final int INDEX_FAVICON = 4; } - + private void copy(CharSequence text) { - try { - IClipboard clip = IClipboard.Stub.asInterface(ServiceManager.getService("clipboard")); - if (clip != null) { - clip.setClipboardText(text); + ClipboardManager cm = (ClipboardManager) getActivity().getSystemService( + Context.CLIPBOARD_SERVICE); + cm.setText(text); + } + + @Override + public Loader<Cursor> onCreateLoader(int id, Bundle args) { + switch (id) { + case LOADER_HISTORY: { + CursorLoader loader = new CursorLoader(getActivity(), History.CONTENT_URI, + HistoryQuery.PROJECTION, null, null, null); + return loader; + } + + default: { + throw new IllegalArgumentException(); } - } catch (android.os.RemoteException e) { - Log.e(LOGTAG, "Copy failed", e); } } @Override - protected void onCreate(Bundle icicle) { - super.onCreate(icicle); - setTitle(R.string.browser_history); - - final String whereClause = Browser.BookmarkColumns.VISITS + " > 0" - // In AddBookmarkPage, where we save new bookmarks, we add - // three visits to newly created bookmarks, so that - // bookmarks that have not been visited will show up in the - // most visited, and higher in the goto search box. - // However, this puts the site in the history, unless we - // ignore sites with a DATE of 0, which the next line does. - + " AND " + Browser.BookmarkColumns.DATE + " > 0"; - final String orderBy = Browser.BookmarkColumns.DATE + " DESC"; - - Cursor cursor = managedQuery( - Browser.BOOKMARKS_URI, - Browser.HISTORY_PROJECTION, - whereClause, null, orderBy); - - mAdapter = new HistoryAdapter(this, cursor, - Browser.HISTORY_PROJECTION_DATE_INDEX); - setListAdapter(mAdapter); - final ExpandableListView list = getExpandableListView(); - list.setOnCreateContextMenuListener(this); - View v = new ViewStub(this, R.layout.empty_history); - addContentView(v, new LayoutParams(LayoutParams.MATCH_PARENT, - LayoutParams.MATCH_PARENT)); - list.setEmptyView(v); - // Do not post the runnable if there is nothing in the list. - if (list.getExpandableListAdapter().getGroupCount() > 0) { - list.post(new Runnable() { - public void run() { - // In case the history gets cleared before this event - // happens. - if (list.getExpandableListAdapter().getGroupCount() > 0) { - list.expandGroup(0); - } + public void onLoadFinished(Loader<Cursor> loader, Cursor data) { + switch (loader.getId()) { + case LOADER_HISTORY: { + mAdapter.changeCursor(data); + + // Add an empty view late, so it does not claim an empty + // history before the adapter is present + mList.setEmptyView(mEmptyView); + + // Do not post the runnable if there is nothing in the list. + if (mList.getExpandableListAdapter().getGroupCount() > 0) { + mList.post(new Runnable() { + @Override + public void run() { + // In case the history gets cleared before this + // event happens + if (mList.getExpandableListAdapter().getGroupCount() > 0) { + mList.expandGroup(0); + } + } + }); } - }); + break; + } + + default: { + throw new IllegalArgumentException(); + } } - mDisableNewWindow = getIntent().getBooleanExtra("disable_new_window", - false); + } + + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + + setHasOptionsMenu(true); + + Bundle args = getArguments(); + mDisableNewWindow = args.getBoolean("disable_new_window", false); + } + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + mCallbacks = (BookmarksHistoryCallbacks) activity; + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View root = inflater.inflate(R.layout.history, container, false); + mList = (ExpandableListView) root.findViewById(android.R.id.list); + mList.setOnCreateContextMenuListener(this); + mList.setOnChildClickListener(this); + mAdapter = new HistoryAdapter(getActivity()); + mList.setAdapter(mAdapter); + + mEmptyView = root.findViewById(android.R.id.empty); + + // Start the loader + getLoaderManager().initLoader(LOADER_HISTORY, null, this); // Register to receive icons in case they haven't all been loaded. - CombinedBookmarkHistoryActivity.getIconListenerSet() - .addListener(mIconReceiver); - - Activity parent = getParent(); - if (null == parent - || !(parent instanceof CombinedBookmarkHistoryActivity)) { - throw new AssertionError("history page can only be viewed as a tab" - + "in CombinedBookmarkHistoryActivity"); - } - // initialize the result to canceled, so that if the user just presses - // back then it will have the correct result - setResultToParent(RESULT_CANCELED, null); + CombinedBookmarkHistoryActivity.getIconListenerSet().addListener(mIconReceiver); + + return root; } @Override - protected void onDestroy() { + public void onDestroy() { super.onDestroy(); - CombinedBookmarkHistoryActivity.getIconListenerSet() - .removeListener(mIconReceiver); + CombinedBookmarkHistoryActivity.getIconListenerSet().removeListener(mIconReceiver); } @Override - public boolean onCreateOptionsMenu(Menu menu) { - super.onCreateOptionsMenu(menu); - MenuInflater inflater = getMenuInflater(); + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { inflater.inflate(R.menu.history, menu); - return true; } @Override - public boolean onPrepareOptionsMenu(Menu menu) { - menu.findItem(R.id.clear_history_menu_id).setVisible(Browser.canClearHistory(this.getContentResolver())); - return true; + public void onPrepareOptionsMenu(Menu menu) { + menu.findItem(R.id.clear_history_menu_id).setVisible( + Browser.canClearHistory(getActivity().getContentResolver())); } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.clear_history_menu_id: - Browser.clearHistory(getContentResolver()); + Browser.clearHistory(getActivity().getContentResolver()); // BrowserHistoryPage is always a child of // CombinedBookmarkHistoryActivity - ((CombinedBookmarkHistoryActivity) getParent()) - .removeParentChildRelationShips(); - mAdapter.refreshData(); + mCallbacks.onRemoveParentChildRelationShips(); return true; default: @@ -191,24 +213,23 @@ public class BrowserHistoryPage extends ExpandableListActivity { } @Override - public void onCreateContextMenu(ContextMenu menu, View v, - ContextMenuInfo menuInfo) { - ExpandableListContextMenuInfo i = - (ExpandableListContextMenuInfo) menuInfo; + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { + ExpandableListContextMenuInfo i = (ExpandableListContextMenuInfo) menuInfo; // Do not allow a context menu to come up from the group views. if (!(i.targetView instanceof HistoryItem)) { return; } // Inflate the menu - MenuInflater inflater = getMenuInflater(); + Activity parent = getActivity(); + MenuInflater inflater = parent.getMenuInflater(); inflater.inflate(R.menu.historycontext, menu); HistoryItem historyItem = (HistoryItem) i.targetView; // Setup the header if (mContextHeader == null) { - mContextHeader = new HistoryItem(this); + mContextHeader = new HistoryItem(parent); } else if (mContextHeader.getParent() != null) { ((ViewGroup) mContextHeader.getParent()).removeView(mContextHeader); } @@ -225,7 +246,7 @@ public class BrowserHistoryPage extends ExpandableListActivity { item.setTitle(R.string.remove_from_bookmarks); } // decide whether to show the share link option - PackageManager pm = getPackageManager(); + PackageManager pm = parent.getPackageManager(); Intent send = new Intent(Intent.ACTION_SEND); send.setType("text/plain"); ResolveInfo ri = pm.resolveActivity(send, PackageManager.MATCH_DEFAULT_ONLY); @@ -241,72 +262,63 @@ public class BrowserHistoryPage extends ExpandableListActivity { HistoryItem historyItem = (HistoryItem) i.targetView; String url = historyItem.getUrl(); String title = historyItem.getName(); + Activity activity = getActivity(); switch (item.getItemId()) { case R.id.open_context_menu_id: - loadUrl(url, false); + mCallbacks.onUrlSelected(url, false); return true; case R.id.new_window_context_menu_id: - loadUrl(url, true); + mCallbacks.onUrlSelected(url, true); return true; case R.id.save_to_bookmarks_menu_id: if (historyItem.isBookmark()) { - Bookmarks.removeFromBookmarks(this, getContentResolver(), + Bookmarks.removeFromBookmarks(activity, activity.getContentResolver(), url, title); } else { - Browser.saveBookmark(this, title, url); + Browser.saveBookmark(activity, title, url); } return true; case R.id.share_link_context_menu_id: - Browser.sendString(this, url, - getText(R.string.choosertitle_sharevia).toString()); + Browser.sendString(activity, url, + activity.getText(R.string.choosertitle_sharevia).toString()); return true; case R.id.copy_url_context_menu_id: copy(url); return true; case R.id.delete_context_menu_id: - Browser.deleteFromHistory(getContentResolver(), url); - mAdapter.refreshData(); + Browser.deleteFromHistory(activity.getContentResolver(), url); return true; case R.id.homepage_context_menu_id: - BrowserSettings.getInstance().setHomePage(this, url); - Toast.makeText(this, R.string.homepage_set, - Toast.LENGTH_LONG).show(); + BrowserSettings.getInstance().setHomePage(activity, url); + Toast.makeText(activity, R.string.homepage_set, Toast.LENGTH_LONG).show(); return true; default: break; } return super.onContextItemSelected(item); } - + @Override public boolean onChildClick(ExpandableListView parent, View v, int groupPosition, int childPosition, long id) { - if (v instanceof HistoryItem) { - loadUrl(((HistoryItem) v).getUrl(), false); + if (v instanceof BookmarkItem) { + mCallbacks.onUrlSelected(((BookmarkItem) v).getUrl(), false); return true; } return false; } - // This Activity is always a sub-Activity of - // CombinedBookmarkHistoryActivity. Therefore, we need to pass our - // result code up to our parent. - private void setResultToParent(int resultCode, Intent data) { - ((CombinedBookmarkHistoryActivity) getParent()).setResultFromChild( - resultCode, data); - } - private class HistoryAdapter extends DateSortedExpandableListAdapter { - HistoryAdapter(Context context, Cursor cursor, int index) { - super(context, cursor, index); - + HistoryAdapter(Context context) { + super(context, HistoryQuery.INDEX_DATE_LAST_VISITED); } + @Override public View getChildView(int groupPosition, int childPosition, boolean isLastChild, View convertView, ViewGroup parent) { - HistoryItem item; - if (null == convertView || !(convertView instanceof HistoryItem)) { - item = new HistoryItem(BrowserHistoryPage.this); + BookmarkItem item; + if (null == convertView || !(convertView instanceof BookmarkItem)) { + item = new BookmarkItem(getContext()); // Add padding on the left so it will be indented from the // arrows on the group views. item.setPadding(item.getPaddingLeft() + 10, @@ -314,16 +326,18 @@ public class BrowserHistoryPage extends ExpandableListActivity { item.getPaddingRight(), item.getPaddingBottom()); } else { - item = (HistoryItem) convertView; + item = (BookmarkItem) convertView; } + // Bail early if the Cursor is closed. if (!moveCursorToChildPosition(groupPosition, childPosition)) { return item; } - item.setName(getString(Browser.HISTORY_PROJECTION_TITLE_INDEX)); - String url = getString(Browser.HISTORY_PROJECTION_URL_INDEX); + + item.setName(getString(HistoryQuery.INDEX_TITE)); + String url = getString(HistoryQuery.INDEX_URL); item.setUrl(url); - byte[] data = getBlob(Browser.HISTORY_PROJECTION_FAVICON_INDEX); + byte[] data = getBlob(HistoryQuery.INDEX_FAVICON); if (data != null) { item.setFavicon(BitmapFactory.decodeByteArray(data, 0, data.length)); @@ -331,8 +345,6 @@ public class BrowserHistoryPage extends ExpandableListActivity { item.setFavicon(CombinedBookmarkHistoryActivity .getIconListenerSet().getFavicon(url)); } - item.setIsBookmark(1 == - getInt(Browser.HISTORY_PROJECTION_BOOKMARK_INDEX)); return item; } } diff --git a/src/com/android/browser/BrowserProvider.java b/src/com/android/browser/BrowserProvider.java index f8574eda8..f371e24f7 100644 --- a/src/com/android/browser/BrowserProvider.java +++ b/src/com/android/browser/BrowserProvider.java @@ -29,8 +29,10 @@ import android.content.Intent; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; import android.content.UriMatcher; +import android.content.res.Configuration; import android.database.AbstractCursor; import android.database.Cursor; +import android.database.DatabaseUtils; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.net.Uri; @@ -38,6 +40,7 @@ import android.os.Process; import android.preference.PreferenceManager; import android.provider.Browser; import android.provider.Browser.BookmarkColumns; +import android.provider.Settings; import android.speech.RecognizerResultsIntent; import android.text.TextUtils; import android.util.Log; @@ -84,6 +87,17 @@ public class BrowserProvider extends ContentProvider { private static final int SUGGEST_COLUMN_QUERY_ID = 8; private static final int SUGGEST_COLUMN_INTENT_EXTRA_DATA = 9; + // how many suggestions will be shown in dropdown + // 0..SHORT: filled by browser db + private static final int MAX_SUGGEST_SHORT_SMALL = 3; + // SHORT..LONG: filled by search suggestions + private static final int MAX_SUGGEST_LONG_SMALL = 6; + + // large screen size shows more + private static final int MAX_SUGGEST_SHORT_LARGE = 6; + private static final int MAX_SUGGEST_LONG_LARGE = 9; + + // shared suggestion columns private static final String[] COLUMNS = new String[] { "_id", @@ -97,10 +111,6 @@ public class BrowserProvider extends ContentProvider { SearchManager.SUGGEST_COLUMN_QUERY, SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA}; - private static final int MAX_SUGGESTION_SHORT_ENTRIES = 3; - private static final int MAX_SUGGESTION_LONG_ENTRIES = 6; - private static final String MAX_SUGGESTION_LONG_ENTRIES_STRING = - Integer.valueOf(MAX_SUGGESTION_LONG_ENTRIES).toString(); // make sure that these match the index of TABLE_NAMES private static final int URI_MATCH_BOOKMARKS = 0; @@ -160,6 +170,9 @@ public class BrowserProvider extends ContentProvider { private BrowserSettings mSettings; + private int mMaxSuggestionShortSize; + private int mMaxSuggestionLongSize; + public BrowserProvider() { } @@ -343,6 +356,20 @@ public class BrowserProvider extends ContentProvider { @Override public boolean onCreate() { final Context context = getContext(); + boolean xlargeScreenSize = (context.getResources().getConfiguration().screenLayout + & Configuration.SCREENLAYOUT_SIZE_MASK) + == Configuration.SCREENLAYOUT_SIZE_XLARGE; + boolean isPortrait = (context.getResources().getConfiguration().orientation + == Configuration.ORIENTATION_PORTRAIT); + + + if (xlargeScreenSize && isPortrait) { + mMaxSuggestionLongSize = MAX_SUGGEST_LONG_LARGE; + mMaxSuggestionShortSize = MAX_SUGGEST_SHORT_LARGE; + } else { + mMaxSuggestionLongSize = MAX_SUGGEST_LONG_SMALL; + mMaxSuggestionShortSize = MAX_SUGGEST_SHORT_SMALL; + } mOpenHelper = new DatabaseHelper(context); mBackupManager = new BackupManager(context); // we added "picasa web album" into default bookmarks for version 19. @@ -409,10 +436,10 @@ public class BrowserProvider extends ContentProvider { public MySuggestionCursor(Cursor hc, Cursor sc, String string) { mHistoryCursor = hc; mSuggestCursor = sc; - mHistoryCount = hc.getCount(); + mHistoryCount = hc != null ? hc.getCount() : 0; mSuggestionCount = sc != null ? sc.getCount() : 0; - if (mSuggestionCount > (MAX_SUGGESTION_LONG_ENTRIES - mHistoryCount)) { - mSuggestionCount = MAX_SUGGESTION_LONG_ENTRIES - mHistoryCount; + if (mSuggestionCount > (mMaxSuggestionLongSize - mHistoryCount)) { + mSuggestionCount = mMaxSuggestionLongSize - mHistoryCount; } mString = string; mIncludeWebSearch = string.length() > 0; @@ -626,6 +653,7 @@ public class BrowserProvider extends ContentProvider { } // TODO Temporary change, finalize after jq's changes go in + @Override public void deactivate() { if (mHistoryCursor != null) { mHistoryCursor.deactivate(); @@ -636,12 +664,14 @@ public class BrowserProvider extends ContentProvider { super.deactivate(); } + @Override public boolean requery() { return (mHistoryCursor != null ? mHistoryCursor.requery() : false) | (mSuggestCursor != null ? mSuggestCursor.requery() : false); } // TODO Temporary change, finalize after jq's changes go in + @Override public void close() { super.close(); if (mHistoryCursor != null) { @@ -706,12 +736,15 @@ public class BrowserProvider extends ContentProvider { public ResultsCursor(ArrayList<String> results) { mResults = results; } + @Override public int getCount() { return mResults.size(); } + @Override public String[] getColumnNames() { return RESULTS_COLUMNS; } + @Override public String getString(int column) { switch (column) { case RESULT_ACTION_ID: @@ -733,30 +766,39 @@ public class BrowserProvider extends ContentProvider { return null; } } + @Override public short getShort(int column) { throw new UnsupportedOperationException(); } + @Override public int getInt(int column) { throw new UnsupportedOperationException(); } + @Override public long getLong(int column) { if ((mPos != -1) && column == 0) { return mPos; // use row# as the _id } throw new UnsupportedOperationException(); } + @Override public float getFloat(int column) { throw new UnsupportedOperationException(); } + @Override public double getDouble(int column) { throw new UnsupportedOperationException(); } + @Override public boolean isNull(int column) { throw new UnsupportedOperationException(); } } + /** Contains custom suggestions results set by the UI */ private ResultsCursor mResultsCursor; + /** Locks access to {@link #mResultsCursor} */ + private Object mResultsCursorLock = new Object(); /** * Provide a set of results to be returned to query, intended to be used @@ -764,10 +806,12 @@ public class BrowserProvider extends ContentProvider { * @param results Strings to display in the dropdown from the SearchDialog */ /* package */ void setQueryResults(ArrayList<String> results) { - if (results == null) { - mResultsCursor = null; - } else { - mResultsCursor = new ResultsCursor(results); + synchronized (mResultsCursorLock) { + if (results == null) { + mResultsCursor = null; + } else { + mResultsCursor = new ResultsCursor(results); + } } } @@ -779,57 +823,19 @@ public class BrowserProvider extends ContentProvider { if (match == -1) { throw new IllegalArgumentException("Unknown URL"); } - if (match == URI_MATCH_SUGGEST && mResultsCursor != null) { - Cursor results = mResultsCursor; - mResultsCursor = null; - return results; - } - SQLiteDatabase db = mOpenHelper.getReadableDatabase(); - if (match == URI_MATCH_SUGGEST || match == URI_MATCH_BOOKMARKS_SUGGEST) { - String suggestSelection; - String [] myArgs; - if (selectionArgs[0] == null || selectionArgs[0].equals("")) { - suggestSelection = null; - myArgs = null; - } else { - String like = selectionArgs[0] + "%"; - if (selectionArgs[0].startsWith("http") - || selectionArgs[0].startsWith("file")) { - myArgs = new String[1]; - myArgs[0] = like; - suggestSelection = selection; - } else { - SUGGEST_ARGS[0] = "http://" + like; - SUGGEST_ARGS[1] = "http://www." + like; - SUGGEST_ARGS[2] = "https://" + like; - SUGGEST_ARGS[3] = "https://www." + like; - // To match against titles. - SUGGEST_ARGS[4] = like; - myArgs = SUGGEST_ARGS; - suggestSelection = SUGGEST_SELECTION; - } + // If results for the suggestion are already ready just return them directly + synchronized (mResultsCursorLock) { + if (match == URI_MATCH_SUGGEST && mResultsCursor != null) { + Cursor results = mResultsCursor; + mResultsCursor = null; + return results; } + } - Cursor c = db.query(TABLE_NAMES[URI_MATCH_BOOKMARKS], - SUGGEST_PROJECTION, suggestSelection, myArgs, null, null, - ORDER_BY, MAX_SUGGESTION_LONG_ENTRIES_STRING); - - if (match == URI_MATCH_BOOKMARKS_SUGGEST - || Patterns.WEB_URL.matcher(selectionArgs[0]).matches()) { - return new MySuggestionCursor(c, null, ""); - } else { - // get search suggestions if there is still space in the list - if (myArgs != null && myArgs.length > 1 - && c.getCount() < (MAX_SUGGESTION_SHORT_ENTRIES - 1)) { - SearchEngine searchEngine = mSettings.getSearchEngine(); - if (searchEngine != null && searchEngine.supportsSuggestions()) { - Cursor sc = searchEngine.getSuggestions(getContext(), selectionArgs[0]); - return new MySuggestionCursor(c, sc, selectionArgs[0]); - } - } - return new MySuggestionCursor(c, null, selectionArgs[0]); - } + if (match == URI_MATCH_SUGGEST || match == URI_MATCH_BOOKMARKS_SUGGEST) { + // Handle suggestions + return doSuggestQuery(selection, selectionArgs, match == URI_MATCH_BOOKMARKS_SUGGEST); } String[] projection = null; @@ -839,27 +845,60 @@ public class BrowserProvider extends ContentProvider { projection[projectionIn.length] = "_id AS _id"; } - StringBuilder whereClause = new StringBuilder(256); + String whereClause = null; if (match == URI_MATCH_BOOKMARKS_ID || match == URI_MATCH_SEARCHES_ID) { - whereClause.append("(_id = ").append(url.getPathSegments().get(1)) - .append(")"); + whereClause = "_id = " + url.getPathSegments().get(1); } - // Tack on the user's selection, if present - if (selection != null && selection.length() > 0) { - if (whereClause.length() > 0) { - whereClause.append(" AND "); + Cursor c = mOpenHelper.getReadableDatabase().query(TABLE_NAMES[match % 10], projection, + DatabaseUtils.concatenateWhere(whereClause, selection), selectionArgs, + null, null, sortOrder, null); + c.setNotificationUri(getContext().getContentResolver(), url); + return c; + } + + private Cursor doSuggestQuery(String selection, String[] selectionArgs, boolean bookmarksOnly) { + String suggestSelection; + String [] myArgs; + if (selectionArgs[0] == null || selectionArgs[0].equals("")) { + return new MySuggestionCursor(null, null, ""); + } else { + String like = selectionArgs[0] + "%"; + if (selectionArgs[0].startsWith("http") + || selectionArgs[0].startsWith("file")) { + myArgs = new String[1]; + myArgs[0] = like; + suggestSelection = selection; + } else { + SUGGEST_ARGS[0] = "http://" + like; + SUGGEST_ARGS[1] = "http://www." + like; + SUGGEST_ARGS[2] = "https://" + like; + SUGGEST_ARGS[3] = "https://www." + like; + // To match against titles. + SUGGEST_ARGS[4] = like; + myArgs = SUGGEST_ARGS; + suggestSelection = SUGGEST_SELECTION; } + } + + Cursor c = mOpenHelper.getReadableDatabase().query(TABLE_NAMES[URI_MATCH_BOOKMARKS], + SUGGEST_PROJECTION, suggestSelection, myArgs, null, null, + ORDER_BY, Integer.toString(mMaxSuggestionLongSize)); - whereClause.append('('); - whereClause.append(selection); - whereClause.append(')'); + if (bookmarksOnly || Patterns.WEB_URL.matcher(selectionArgs[0]).matches()) { + return new MySuggestionCursor(c, null, ""); + } else { + // get search suggestions if there is still space in the list + if (myArgs != null && myArgs.length > 1 + && c.getCount() < (MAX_SUGGEST_SHORT_SMALL - 1)) { + SearchEngine searchEngine = mSettings.getSearchEngine(); + if (searchEngine != null && searchEngine.supportsSuggestions()) { + Cursor sc = searchEngine.getSuggestions(getContext(), selectionArgs[0]); + return new MySuggestionCursor(c, sc, selectionArgs[0]); + } + } + return new MySuggestionCursor(c, null, selectionArgs[0]); } - Cursor c = db.query(TABLE_NAMES[match % 10], projection, - whereClause.toString(), selectionArgs, null, null, sortOrder, - null); - c.setNotificationUri(getContext().getContentResolver(), url); - return c; } @Override @@ -1067,4 +1106,10 @@ public class BrowserProvider extends ContentProvider { } } + public static Cursor getBookmarksSuggestions(ContentResolver cr, String constraint) { + Uri uri = Uri.parse("content://browser/" + SearchManager.SUGGEST_URI_PATH_QUERY); + return cr.query(uri, SUGGEST_PROJECTION, SUGGEST_SELECTION, + new String[] { constraint }, ORDER_BY); + } + } diff --git a/src/com/android/browser/BrowserSettings.java b/src/com/android/browser/BrowserSettings.java index 3791eb07f..cb4918b50 100644 --- a/src/com/android/browser/BrowserSettings.java +++ b/src/com/android/browser/BrowserSettings.java @@ -28,6 +28,7 @@ import android.content.pm.ActivityInfo; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; import android.database.ContentObserver; +import android.net.Uri; import android.os.Handler; import android.preference.PreferenceActivity; import android.preference.PreferenceScreen; @@ -115,8 +116,8 @@ class BrowserSettings extends Observable { private boolean showConsole = true; // Private preconfigured values - private static int minimumFontSize = 8; - private static int minimumLogicalFontSize = 8; + private static int minimumFontSize = 1; + private static int minimumLogicalFontSize = 1; private static int defaultFontSize = 16; private static int defaultFixedFontSize = 13; private static WebSettings.TextSize textSize = @@ -166,6 +167,10 @@ class BrowserSettings extends Observable { // a ListView public final static int MAX_TEXTVIEW_LEN = 80; + public static final String RLZ_PROVIDER = "com.google.android.partnersetup.rlzappprovider"; + + public static final Uri RLZ_PROVIDER_URI = Uri.parse("content://" + RLZ_PROVIDER + "/"); + private TabControl mTabControl; // Single instance of the BrowserSettings for use in the Browser app. @@ -229,6 +234,9 @@ class BrowserSettings extends Observable { s.setNeedInitialFocus(false); // Browser supports multiple windows s.setSupportMultipleWindows(true); + // enable smooth transition for better performance during panning or + // zooming + s.setEnableSmoothTransition(true); // HTML5 API flags s.setAppCacheEnabled(b.appCacheEnabled); diff --git a/src/com/android/browser/CircularProgressView.java b/src/com/android/browser/CircularProgressView.java new file mode 100644 index 000000000..48f293a42 --- /dev/null +++ b/src/com/android/browser/CircularProgressView.java @@ -0,0 +1,140 @@ + +/* + * 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 android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.RectF; +import android.util.AttributeSet; +import android.widget.ImageButton; + +/** + * + */ +public class CircularProgressView extends ImageButton { + + private static final int[] ALPHAS = { + 64, 96, 128, 160, 192, 192, 160, 128, 96, 64 + }; + + // 100 ms delay between frames, 10fps + private static int ALPHA_REFRESH_DELAY = 100; + + private int mEndAngle; + private int mProgress; + private Paint mPaint; + private int mAlpha; + private boolean mAnimated; + private RectF mRect; + private int mMaxProgress; + + /** + * @param context + * @param attrs + * @param defStyle + */ + public CircularProgressView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(context); + } + + /** + * @param context + * @param attrs + */ + public CircularProgressView(Context context, AttributeSet attrs) { + super(context, attrs); + init(context); + } + + /** + * @param context + */ + public CircularProgressView(Context context) { + super(context); + init(context); + } + + private void init(Context ctx) { + mEndAngle = 0; + mProgress = 0; + mMaxProgress = 100; + mPaint = new Paint(); + mPaint.setAntiAlias(true); + mPaint.setColor(Color.BLACK); + mRect = new RectF(); + } + + void setMaxProgress(int max) { + mMaxProgress = max; + } + + private synchronized boolean isAnimated() { + return mAnimated; + } + + private synchronized void setAnimated(boolean animated) { + mAnimated = animated; + } + + void setProgress(int progress) { + mProgress = progress; + mEndAngle = 360 * progress / mMaxProgress; + invalidate(); + if (!isAnimated() && (progress > 0) && (progress < mMaxProgress)) { + setAnimated(true); + mAlpha = 0; + post(new Runnable() { + @Override + public void run() { + if (isAnimated()) { + mAlpha = (mAlpha + 1) % ALPHAS.length; + mPaint.setAlpha(ALPHAS[mAlpha]); + invalidate(); + postDelayed(this, ALPHA_REFRESH_DELAY); + } + } + }); + } else if ((progress <= 0) || (progress >= mMaxProgress)) { + setAnimated(false); + } + } + + @Override + public void onDraw(Canvas canvas) { + int w = getWidth(); + int h = getHeight(); + float cx = w * 0.5f; + float cy = h * 0.5f; + mRect.set(0, 0, w, h); + if ((mProgress > 0) && (mProgress < mMaxProgress)) { + Path p = new Path(); + p.moveTo(cx, cy); + p.lineTo(cx, 0); + p.arcTo(mRect, 270, mEndAngle); + p.lineTo(cx, cy); + int state = canvas.save(); + canvas.drawPath(p, mPaint); + canvas.restoreToCount(state); + } + super.onDraw(canvas); + } + +} diff --git a/src/com/android/browser/CombinedBookmarkHistoryActivity.java b/src/com/android/browser/CombinedBookmarkHistoryActivity.java index 194956f14..a98408c6c 100644 --- a/src/com/android/browser/CombinedBookmarkHistoryActivity.java +++ b/src/com/android/browser/CombinedBookmarkHistoryActivity.java @@ -17,22 +17,48 @@ package com.android.browser; import android.app.Activity; -import android.app.TabActivity; +import android.app.Fragment; +import android.app.FragmentManager; +import android.app.FragmentTransaction; import android.content.Intent; -import android.content.res.Resources; +import android.database.MatrixCursor; import android.graphics.Bitmap; import android.os.AsyncTask; import android.os.Bundle; import android.provider.Browser; +import android.view.Gravity; +import android.view.MotionEvent; +import android.view.View; +import android.view.WindowManager; import android.webkit.WebIconDatabase; import android.webkit.WebIconDatabase.IconListener; -import android.widget.TabHost; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.ListView; +import android.widget.SimpleCursorAdapter; import java.util.HashMap; import java.util.Vector; -public class CombinedBookmarkHistoryActivity extends TabActivity - implements TabHost.OnTabChangeListener { +interface BookmarksHistoryCallbacks { + public void onUrlSelected(String url, boolean newWindow); + public void onRemoveParentChildRelationShips(); +} + +public class CombinedBookmarkHistoryActivity extends Activity + implements BookmarksHistoryCallbacks, OnItemClickListener { + final static String NEWTAB_MODE = "newtab_mode"; + final static String EXTRA_OPEN_NEW_WINDOW = "new_window"; + final static String STARTING_FRAGMENT = "fragment"; + final static String EVT_X = "evt_x"; + final static String EVT_Y = "evt_y"; + final static String EXTRA_TOP = "top"; + final static String EXTRA_HEIGHT = "height"; + + + final static int FRAGMENT_ID_BOOKMARKS = 1; + final static int FRAGMENT_ID_HISTORY = 2; + /** * Used to inform BrowserActivity to remove the parent/child relationships * from all the tabs. @@ -48,10 +74,15 @@ public class CombinedBookmarkHistoryActivity extends TabActivity */ private int mResultCode; - /* package */ static String BOOKMARKS_TAB = "bookmark"; - /* package */ static String VISITED_TAB = "visited"; - /* package */ static String HISTORY_TAB = "history"; - /* package */ static String STARTING_TAB = "tab"; + /** + * Flag to inform the browser to force the result to open in a new tab. + */ + private boolean mNewTabMode; + + private int mRequestedTop; + private int mRequestedHeight; + + long mCurrentFragment; static class IconListenerSet implements IconListener { // Used to store favicons as we get them from the database @@ -64,6 +95,7 @@ public class CombinedBookmarkHistoryActivity extends TabActivity mUrlsToIcons = new HashMap<String, Bitmap>(); mListeners = new Vector<IconListener>(); } + @Override public void onReceivedIcon(String url, Bitmap icon) { mUrlsToIcons.put(url, icon); for (IconListener listener : mListeners) { @@ -77,9 +109,10 @@ public class CombinedBookmarkHistoryActivity extends TabActivity mListeners.remove(listener); } public Bitmap getFavicon(String url) { - return (Bitmap) mUrlsToIcons.get(url); + return mUrlsToIcons.get(url); } } + private static IconListenerSet sIconListenerSet; static IconListenerSet getIconListenerSet() { if (null == sIconListenerSet) { @@ -91,80 +124,104 @@ public class CombinedBookmarkHistoryActivity extends TabActivity @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setContentView(R.layout.tabs); + setContentView(R.layout.bookmarks_history); setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL); - getTabHost().setOnTabChangedListener(this); + ListView list = (ListView) findViewById(android.R.id.list); + list.setOnItemClickListener(this); + MatrixCursor cursor = new MatrixCursor(new String[] { "name", "_id" }); + cursor.newRow().add(getString(R.string.bookmarks)).add(FRAGMENT_ID_BOOKMARKS); + cursor.newRow().add(getString(R.string.history)).add(FRAGMENT_ID_HISTORY); + list.setAdapter(new SimpleCursorAdapter(this, android.R.layout.simple_list_item_1, cursor, + new String[] { "name" }, new int[] { android.R.id.text1 })); + int startingFragment = FRAGMENT_ID_BOOKMARKS; Bundle extras = getIntent().getExtras(); - - Intent bookmarksIntent = new Intent(this, BrowserBookmarksPage.class); if (extras != null) { - bookmarksIntent.putExtras(extras); + mNewTabMode = extras.getBoolean(NEWTAB_MODE); + mRequestedTop = extras.getInt(EXTRA_TOP, -1); + mRequestedHeight = extras.getInt(EXTRA_HEIGHT, -1); + startingFragment = extras.getInt(STARTING_FRAGMENT, FRAGMENT_ID_BOOKMARKS); } - createTab(bookmarksIntent, R.string.tab_bookmarks, - R.drawable.browser_bookmark_tab, BOOKMARKS_TAB); - - Intent visitedIntent = new Intent(this, BrowserBookmarksPage.class); - // Need to copy extras so the bookmarks activity and this one will be - // different - Bundle visitedExtras = extras == null ? new Bundle() : new Bundle(extras); - visitedExtras.putBoolean("mostVisited", true); - visitedIntent.putExtras(visitedExtras); - createTab(visitedIntent, R.string.tab_most_visited, - R.drawable.browser_visited_tab, VISITED_TAB); - - Intent historyIntent = new Intent(this, BrowserHistoryPage.class); - String defaultTab = null; - if (extras != null) { - historyIntent.putExtras(extras); - defaultTab = extras.getString(STARTING_TAB); - } - createTab(historyIntent, R.string.tab_history, - R.drawable.browser_history_tab, HISTORY_TAB); - if (defaultTab != null) { - getTabHost().setCurrentTab(2); - } + // Start up the default fragment + loadFragment(startingFragment); // XXX: Must do this before launching the AsyncTask to avoid a // potential crash if the icon database has not been created. WebIconDatabase.getInstance(); + // Do this every time we launch the activity in case a new favicon was // added to the webkit db. (new AsyncTask<Void, Void, Void>() { + @Override public Void doInBackground(Void... v) { Browser.requestAllIcons(getContentResolver(), - Browser.BookmarkColumns.FAVICON + " is NULL", - getIconListenerSet()); + Browser.BookmarkColumns.FAVICON + " is NULL", getIconListenerSet()); return null; } }).execute(); } - private void createTab(Intent intent, int labelResId, int iconResId, - String tab) { - Resources resources = getResources(); - TabHost tabHost = getTabHost(); - tabHost.addTab(tabHost.newTabSpec(tab).setIndicator( - resources.getText(labelResId), resources.getDrawable(iconResId)) - .setContent(intent)); + @Override + public void onAttachedToWindow() { + if (mRequestedTop > -1) { + WindowManager.LayoutParams lp = getWindow().getAttributes(); + lp.x = 0; + lp.y = mRequestedTop; + lp.height = mRequestedHeight; + lp.gravity = Gravity.TOP | Gravity.LEFT; + getWindow().setAttributes(lp); + } } - // Copied from DialTacts Activity - /** {@inheritDoc} */ - public void onTabChanged(String tabId) { - Activity activity = getLocalActivityManager().getActivity(tabId); - if (activity != null) { - activity.onWindowFocusChanged(true); + + @Override + public boolean onTouchEvent(MotionEvent evt) { + if (((evt.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_DOWN) && (evt.getY() < 0)) { + Intent result = new Intent(); + result.putExtra(EVT_X, evt.getRawX()); + result.putExtra(EVT_Y, evt.getRawY()); + setResultFromChild(Activity.RESULT_CANCELED, result); + finish(); + return true; } + return super.onTouchEvent(evt); + } + + private void loadFragment(int id) { + String fragmentClassName; + switch (id) { + case FRAGMENT_ID_BOOKMARKS: + fragmentClassName = BrowserBookmarksPage.class.getName(); + break; + case FRAGMENT_ID_HISTORY: + fragmentClassName = BrowserHistoryPage.class.getName(); + break; + default: + throw new IllegalArgumentException(); + } + mCurrentFragment = id; + + FragmentManager fm = getFragmentManager(); + FragmentTransaction transaction = fm.openTransaction(); + Fragment frag = Fragment.instantiate(this, fragmentClassName, getIntent().getExtras()); + transaction.replace(R.id.fragment, frag); + transaction.commit(); + } + + @Override + public void onItemClick(AdapterView<?> parent, View view, int position, long id) { + if (id == mCurrentFragment) return; + loadFragment((int) id); } /** * Store extra data in the Intent to return to the calling Activity to tell * it to clear the parent/child relationships from all tabs. */ - /* package */ void removeParentChildRelationShips() { + @Override + public void onRemoveParentChildRelationShips() { mExtraData = BrowserSettings.PREF_CLEAR_HISTORY; } @@ -174,7 +231,7 @@ public class CombinedBookmarkHistoryActivity extends TabActivity * @param resultCode Uses same codes as Activity.setResult * @param data Intent returned to onActivityResult. */ - /* package */ void setResultFromChild(int resultCode, Intent data) { + private void setResultFromChild(int resultCode, Intent data) { mResultCode = resultCode; mResultData = data; } @@ -186,7 +243,26 @@ public class CombinedBookmarkHistoryActivity extends TabActivity if (mResultData == null) mResultData = new Intent(); mResultData.putExtra(Intent.EXTRA_TEXT, mExtraData); } + if (mNewTabMode) { + if (mResultData == null) mResultData = new Intent(); + mResultData.putExtra(NEWTAB_MODE, true); + } setResult(mResultCode, mResultData); super.finish(); } + + /** + * Report back to the calling activity to load a site. + * @param url Site to load. + * @param newWindow True if the URL should be loaded in a new window + */ + @Override + public void onUrlSelected(String url, boolean newWindow) { + Intent intent = new Intent().setAction(url); + if (newWindow) { + intent.putExtra(EXTRA_OPEN_NEW_WINDOW, true); + } + setResultFromChild(RESULT_OK, intent); + finish(); + } } diff --git a/src/com/android/browser/DateSortedExpandableListAdapter.java b/src/com/android/browser/DateSortedExpandableListAdapter.java index 1d04493aa..a48efe679 100644 --- a/src/com/android/browser/DateSortedExpandableListAdapter.java +++ b/src/com/android/browser/DateSortedExpandableListAdapter.java @@ -17,65 +17,55 @@ package com.android.browser; import android.content.Context; -import android.database.ContentObserver; import android.database.Cursor; import android.database.DataSetObserver; -import android.os.Handler; -import android.provider.BaseColumns; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.webkit.DateSorter; -import android.widget.ExpandableListAdapter; +import android.widget.BaseExpandableListAdapter; import android.widget.ExpandableListView; import android.widget.TextView; -import java.util.Vector; - /** * ExpandableListAdapter which separates data into categories based on date. * Used for History and Downloads. */ -public class DateSortedExpandableListAdapter implements ExpandableListAdapter { +public class DateSortedExpandableListAdapter extends BaseExpandableListAdapter { // Array for each of our bins. Each entry represents how many items are // in that bin. private int mItemMap[]; // This is our GroupCount. We will have at most DateSorter.DAY_COUNT // bins, less if the user has no items in one or more bins. private int mNumberOfBins; - private Vector<DataSetObserver> mObservers; private Cursor mCursor; private DateSorter mDateSorter; private int mDateIndex; private int mIdIndex; private Context mContext; - private class ChangeObserver extends ContentObserver { - public ChangeObserver() { - super(new Handler()); - } + boolean mDataValid; + DataSetObserver mDataSetObserver = new DataSetObserver() { @Override - public boolean deliverSelfNotifications() { - return true; + public void onChanged() { + mDataValid = true; + notifyDataSetChanged(); } @Override - public void onChange(boolean selfChange) { - refreshData(); + public void onInvalidated() { + mDataValid = false; + notifyDataSetInvalidated(); } - } - - public DateSortedExpandableListAdapter(Context context, Cursor cursor, - int dateIndex) { + }; + + public DateSortedExpandableListAdapter(Context context, int dateIndex) { mContext = context; mDateSorter = new DateSorter(context); - mObservers = new Vector<DataSetObserver>(); - mCursor = cursor; - mIdIndex = cursor.getColumnIndexOrThrow(BaseColumns._ID); - cursor.registerContentObserver(new ChangeObserver()); mDateIndex = dateIndex; - buildMap(); + mDataValid = false; + mIdIndex = -1; } /** @@ -122,6 +112,7 @@ public class DateSortedExpandableListAdapter implements ExpandableListAdapter { * @return corresponding byte array from the Cursor. */ /* package */ byte[] getBlob(int cursorIndex) { + if (!mDataValid) return null; return mCursor.getBlob(cursorIndex); } @@ -138,6 +129,7 @@ public class DateSortedExpandableListAdapter implements ExpandableListAdapter { * @return corresponding integer from the Cursor. */ /* package */ int getInt(int cursorIndex) { + if (!mDataValid) return 0; return mCursor.getInt(cursorIndex); } @@ -146,6 +138,7 @@ public class DateSortedExpandableListAdapter implements ExpandableListAdapter { * already been moved to the correct position. */ /* package */ long getLong(int cursorIndex) { + if (!mDataValid) return 0; return mCursor.getLong(cursorIndex); } @@ -158,6 +151,7 @@ public class DateSortedExpandableListAdapter implements ExpandableListAdapter { * @return corresponding String from the Cursor. */ /* package */ String getString(int cursorIndex) { + if (!mDataValid) return null; return mCursor.getString(cursorIndex); } @@ -166,6 +160,7 @@ public class DateSortedExpandableListAdapter implements ExpandableListAdapter { * @param childId ID of the child view in question. * @return int Group position of the containing group. /* package */ int groupFromChildId(long childId) { + if (!mDataValid) return -1; int group = -1; for (mCursor.moveToFirst(); !mCursor.isAfterLast(); mCursor.moveToNext()) { @@ -173,11 +168,15 @@ public class DateSortedExpandableListAdapter implements ExpandableListAdapter { int bin = mDateSorter.getIndex(getLong(mDateIndex)); // bin is the same as the group if the number of bins is the // same as DateSorter - if (mDateSorter.DAY_COUNT == mNumberOfBins) return bin; + if (DateSorter.DAY_COUNT == mNumberOfBins) { + return bin; + } // There are some empty bins. Find the corresponding group. group = 0; for (int i = 0; i < bin; i++) { - if (mItemMap[i] != 0) group++; + if (mItemMap[i] != 0) { + group++; + } } break; } @@ -193,6 +192,7 @@ public class DateSortedExpandableListAdapter implements ExpandableListAdapter { * @return The corresponding bin that holds that group. */ private int groupPositionToBin(int groupPosition) { + if (!mDataValid) return -1; if (groupPosition < 0 || groupPosition >= DateSorter.DAY_COUNT) { throw new AssertionError("group position out of range"); } @@ -241,7 +241,9 @@ public class DateSortedExpandableListAdapter implements ExpandableListAdapter { */ /* package */ boolean moveCursorToChildPosition(int groupPosition, int childPosition) { - if (mCursor.isClosed()) return false; + if (!mDataValid || mCursor.isClosed()) { + return false; + } groupPosition = groupPositionToBin(groupPosition); int index = childPosition; for (int i = 0; i < groupPosition; i++) { @@ -250,19 +252,34 @@ public class DateSortedExpandableListAdapter implements ExpandableListAdapter { return mCursor.moveToPosition(index); } - /* package */ void refreshData() { - if (mCursor.isClosed()) { + public void changeCursor(Cursor cursor) { + if (cursor == mCursor) { return; } - mCursor.requery(); - buildMap(); - for (DataSetObserver o : mObservers) { - o.onChanged(); + if (mCursor != null) { + mCursor.unregisterDataSetObserver(mDataSetObserver); + mCursor.close(); + } + mCursor = cursor; + if (cursor != null) { + cursor.registerDataSetObserver(mDataSetObserver); + mIdIndex = cursor.getColumnIndexOrThrow("_id"); + mDataValid = true; + buildMap(); + // notify the observers about the new cursor + notifyDataSetChanged(); + } else { + mIdIndex = -1; + mDataValid = false; + // notify the observers about the lack of a data set + notifyDataSetInvalidated(); } } + @Override public View getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent) { + if (!mDataValid) throw new IllegalStateException("Data is not valid"); TextView item; if (null == convertView || !(convertView instanceof TextView)) { LayoutInflater factory = LayoutInflater.from(mContext); @@ -275,73 +292,87 @@ public class DateSortedExpandableListAdapter implements ExpandableListAdapter { return item; } + @Override public View getChildView(int groupPosition, int childPosition, boolean isLastChild, View convertView, ViewGroup parent) { + if (!mDataValid) throw new IllegalStateException("Data is not valid"); return null; } + @Override public boolean areAllItemsEnabled() { return true; } + @Override public boolean isChildSelectable(int groupPosition, int childPosition) { return true; } + @Override public int getGroupCount() { + if (!mDataValid) return 0; return mNumberOfBins; } + @Override public int getChildrenCount(int groupPosition) { + if (!mDataValid) return 0; return mItemMap[groupPositionToBin(groupPosition)]; } + @Override public Object getGroup(int groupPosition) { return null; } + @Override public Object getChild(int groupPosition, int childPosition) { return null; } + @Override public long getGroupId(int groupPosition) { + if (!mDataValid) return 0; return groupPosition; } + @Override public long getChildId(int groupPosition, int childPosition) { + if (!mDataValid) return 0; if (moveCursorToChildPosition(groupPosition, childPosition)) { return getLong(mIdIndex); } return 0; } + @Override public boolean hasStableIds() { return true; } - public void registerDataSetObserver(DataSetObserver observer) { - mObservers.add(observer); - } - - public void unregisterDataSetObserver(DataSetObserver observer) { - mObservers.remove(observer); - } - + @Override public void onGroupExpanded(int groupPosition) { } + @Override public void onGroupCollapsed(int groupPosition) { } + @Override public long getCombinedChildId(long groupId, long childId) { + if (!mDataValid) return 0; return childId; } + @Override public long getCombinedGroupId(long groupId) { + if (!mDataValid) return 0; return groupId; } + @Override public boolean isEmpty() { - return mCursor.isClosed() || mCursor.getCount() == 0; + return !mDataValid || mCursor == null || mCursor.isClosed() || mCursor.getCount() == 0; } } diff --git a/src/com/android/browser/DownloadTouchIcon.java b/src/com/android/browser/DownloadTouchIcon.java index 14404ff06..7bb93dc59 100644 --- a/src/com/android/browser/DownloadTouchIcon.java +++ b/src/com/android/browser/DownloadTouchIcon.java @@ -16,26 +16,29 @@ package com.android.browser; +import org.apache.http.HttpEntity; +import org.apache.http.HttpHost; +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.params.HttpClientParams; +import org.apache.http.conn.params.ConnRouteParams; + +import android.app.Activity; import android.content.ContentResolver; import android.content.ContentUris; import android.content.ContentValues; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.BitmapFactory; -import android.net.http.AndroidHttpClient; import android.net.Proxy; +import android.net.http.AndroidHttpClient; import android.os.AsyncTask; -import android.provider.Browser; +import android.os.Bundle; +import android.os.Message; +import android.provider.BrowserContract; +import android.provider.BrowserContract.Images; import android.webkit.WebView; - -import org.apache.http.HttpEntity; -import org.apache.http.HttpHost; -import org.apache.http.HttpResponse; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.client.params.HttpClientParams; -import org.apache.http.conn.params.ConnRouteParams; - import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; @@ -45,10 +48,17 @@ class DownloadTouchIcon extends AsyncTask<String, Void, Void> { private Cursor mCursor; private final String mOriginalUrl; private final String mUrl; - private final String mUserAgent; - private final BrowserActivity mActivity; + private final String mUserAgent; // Sites may serve a different icon to different UAs + private Message mMessage; + + private final Activity mActivity; /* package */ Tab mTab; + /** + * Use this ctor to store the touch icon in the bookmarks database for + * the originalUrl so we take account of redirects. Used when the user + * bookmarks a page from outside the bookmarks activity. + */ public DownloadTouchIcon(Tab tab, BrowserActivity activity, ContentResolver cr, WebView view) { mTab = tab; mActivity = activity; @@ -59,24 +69,49 @@ class DownloadTouchIcon extends AsyncTask<String, Void, Void> { mUserAgent = view.getSettings().getUserAgentString(); } - public DownloadTouchIcon(ContentResolver cr, String url) { + /** + * Use this ctor to download the touch icon and update the bookmarks database + * entry for the given url. Used when the user creates a bookmark from + * within the bookmarks activity and there haven't been any redirects. + * TODO: Would be nice to set the user agent here so that there is no + * potential for the three different ctors here to return different icons. + */ + public DownloadTouchIcon(AddBookmarkPage activity, ContentResolver cr, String url) { mTab = null; - mActivity = null; + mActivity = activity; mContentResolver = cr; mOriginalUrl = null; mUrl = url; mUserAgent = null; } + /** + * Use this ctor to not store the touch icon in a database, rather add it to + * the passed Message's data bundle with the key "touchIcon" and then send + * the message. + */ + public DownloadTouchIcon(BrowserActivity activity, Message msg, String userAgent) { + mMessage = msg; + mActivity = activity; + mContentResolver = null; + mOriginalUrl = null; + mUrl = null; + mUserAgent = userAgent; + } + @Override public Void doInBackground(String... values) { - mCursor = BrowserBookmarksAdapter.queryBookmarksForUrl(mContentResolver, - mOriginalUrl, mUrl, true); - if (mCursor != null && mCursor.getCount() > 0) { - String url = values[0]; + if (mContentResolver != null) { + mCursor = Bookmarks.queryCombinedForUrl(mContentResolver, + mOriginalUrl, mUrl); + } + + boolean inDatabase = mCursor != null && mCursor.getCount() > 0; + + String url = values[0]; - AndroidHttpClient client = AndroidHttpClient.newInstance( - mUserAgent); + if (inDatabase || mMessage != null) { + AndroidHttpClient client = AndroidHttpClient.newInstance(mUserAgent); HttpHost httpHost = Proxy.getPreferredHttpHost(mActivity, url); if (httpHost != null) { ConnRouteParams.setDefaultProxy(client.getParams(), httpHost); @@ -89,7 +124,6 @@ class DownloadTouchIcon extends AsyncTask<String, Void, Void> { try { HttpResponse response = client.execute(request); - if (response.getStatusLine().getStatusCode() == 200) { HttpEntity entity = response.getEntity(); if (entity != null) { @@ -97,7 +131,12 @@ class DownloadTouchIcon extends AsyncTask<String, Void, Void> { if (content != null) { Bitmap icon = BitmapFactory.decodeStream( content, null, null); - storeIcon(icon); + if (inDatabase) { + storeIcon(icon); + } else if (mMessage != null) { + Bundle b = mMessage.getData(); + b.putParcelable("touchIcon", icon); + } } } } @@ -109,9 +148,15 @@ class DownloadTouchIcon extends AsyncTask<String, Void, Void> { client.close(); } } + if (mCursor != null) { mCursor.close(); } + + if (mMessage != null) { + mMessage.sendToTarget(); + } + return null; } @@ -133,17 +178,16 @@ class DownloadTouchIcon extends AsyncTask<String, Void, Void> { return; } - final ByteArrayOutputStream os = new ByteArrayOutputStream(); - icon.compress(Bitmap.CompressFormat.PNG, 100, os); - ContentValues values = new ContentValues(); - values.put(Browser.BookmarkColumns.TOUCH_ICON, - os.toByteArray()); - if (mCursor.moveToFirst()) { + final ByteArrayOutputStream os = new ByteArrayOutputStream(); + icon.compress(Bitmap.CompressFormat.PNG, 100, os); + + ContentValues values = new ContentValues(); + values.put(Images.TOUCH_ICON, os.toByteArray()); + values.put(Images.URL, mCursor.getString(0)); + do { - mContentResolver.update(ContentUris.withAppendedId( - Browser.BOOKMARKS_URI, mCursor.getInt(0)), - values, null, null); + mContentResolver.update(Images.CONTENT_URI, values, null, null); } while (mCursor.moveToNext()); } } diff --git a/src/com/android/browser/FindDialog.java b/src/com/android/browser/FindDialog.java deleted file mode 100644 index 726138ef0..000000000 --- a/src/com/android/browser/FindDialog.java +++ /dev/null @@ -1,247 +0,0 @@ -/* - * Copyright (C) 2007 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 android.content.Context; -import android.text.Editable; -import android.text.Selection; -import android.text.Spannable; -import android.text.TextWatcher; -import android.view.Gravity; -import android.view.KeyEvent; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.view.animation.AnimationUtils; -import android.view.inputmethod.InputMethodManager; -import android.webkit.WebView; -import android.widget.EditText; -import android.widget.LinearLayout; -import android.widget.TextView; - -/* package */ class FindDialog extends WebDialog implements TextWatcher { - private TextView mMatches; - - // Views with which the user can interact. - private EditText mEditText; - private View mNextButton; - private View mPrevButton; - private View mMatchesView; - - // When the dialog is opened up with old text, enter needs to be pressed - // (or the text needs to be changed) before WebView.findAll can be called. - // Once it has been called, enter should move to the next match. - private boolean mMatchesFound; - private int mNumberOfMatches; - - private View.OnClickListener mFindListener = new View.OnClickListener() { - public void onClick(View v) { - findNext(); - } - }; - - private View.OnClickListener mFindPreviousListener = - new View.OnClickListener() { - public void onClick(View v) { - if (mWebView == null) { - throw new AssertionError("No WebView for FindDialog::onClick"); - } - mWebView.findNext(false); - updateMatchesString(); - hideSoftInput(); - } - }; - - private void disableButtons() { - mPrevButton.setEnabled(false); - mNextButton.setEnabled(false); - mPrevButton.setFocusable(false); - mNextButton.setFocusable(false); - } - - /* package */ FindDialog(BrowserActivity context) { - super(context); - - LayoutInflater factory = LayoutInflater.from(context); - factory.inflate(R.layout.browser_find, this); - - addCancel(); - mEditText = (EditText) findViewById(R.id.edit); - - View button = findViewById(R.id.next); - button.setOnClickListener(mFindListener); - mNextButton = button; - - button = findViewById(R.id.previous); - button.setOnClickListener(mFindPreviousListener); - mPrevButton = button; - - mMatches = (TextView) findViewById(R.id.matches); - mMatchesView = findViewById(R.id.matches_view); - disableButtons(); - - } - - /** - * Called by BrowserActivity.closeDialog. Start the animation to hide - * the dialog, inform the WebView that the dialog is being dismissed, - * and hide the soft keyboard. - */ - public void dismiss() { - super.dismiss(); - mWebView.notifyFindDialogDismissed(); - hideSoftInput(); - } - - @Override - public boolean dispatchKeyEventPreIme(KeyEvent event) { - if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) { - KeyEvent.DispatcherState state = getKeyDispatcherState(); - if (state != null) { - int action = event.getAction(); - if (KeyEvent.ACTION_DOWN == action - && event.getRepeatCount() == 0) { - state.startTracking(event, this); - return true; - } else if (KeyEvent.ACTION_UP == action - && !event.isCanceled() && state.isTracking(event)) { - mBrowserActivity.closeDialogs(); - return true; - } - } - } - return super.dispatchKeyEventPreIme(event); - } - - @Override - public boolean dispatchKeyEvent(KeyEvent event) { - int keyCode = event.getKeyCode(); - if (event.getAction() == KeyEvent.ACTION_UP) { - if (keyCode == KeyEvent.KEYCODE_ENTER - && mEditText.hasFocus()) { - if (mMatchesFound) { - findNext(); - } else { - findAll(); - // Set the selection to the end. - Spannable span = (Spannable) mEditText.getText(); - Selection.setSelection(span, span.length()); - } - return true; - } - } - return super.dispatchKeyEvent(event); - } - - private void findNext() { - if (mWebView == null) { - throw new AssertionError("No WebView for FindDialog::findNext"); - } - mWebView.findNext(true); - updateMatchesString(); - hideSoftInput(); - } - - public void show() { - super.show(); - // In case the matches view is showing from a previous search - mMatchesView.setVisibility(View.INVISIBLE); - mMatchesFound = false; - // This text is only here to ensure that mMatches has a height. - mMatches.setText("0"); - mEditText.requestFocus(); - Spannable span = (Spannable) mEditText.getText(); - int length = span.length(); - Selection.setSelection(span, 0, length); - span.setSpan(this, 0, length, Spannable.SPAN_INCLUSIVE_INCLUSIVE); - disableButtons(); - InputMethodManager imm = (InputMethodManager) - mBrowserActivity.getSystemService(Context.INPUT_METHOD_SERVICE); - imm.showSoftInput(mEditText, 0); - } - - // TextWatcher methods - public void beforeTextChanged(CharSequence s, - int start, - int count, - int after) { - } - - public void onTextChanged(CharSequence s, - int start, - int before, - int count) { - findAll(); - } - - private void findAll() { - if (mWebView == null) { - throw new AssertionError( - "No WebView for FindDialog::findAll"); - } - CharSequence find = mEditText.getText(); - if (0 == find.length()) { - disableButtons(); - mWebView.clearMatches(); - mMatchesView.setVisibility(View.INVISIBLE); - } else { - mMatchesView.setVisibility(View.VISIBLE); - int found = mWebView.findAll(find.toString()); - mMatchesFound = true; - setMatchesFound(found); - if (found < 2) { - disableButtons(); - if (found == 0) { - // Cannot use getQuantityString, which ignores the "zero" - // quantity. - // FIXME: is this fix is beyond the scope - // of adding touch selection to gingerbread? - // mMatches.setText(mBrowserActivity.getResources().getString( - // R.string.no_matches)); - } - } else { - mPrevButton.setFocusable(true); - mNextButton.setFocusable(true); - mPrevButton.setEnabled(true); - mNextButton.setEnabled(true); - } - } - } - - private void setMatchesFound(int found) { - mNumberOfMatches = found; - updateMatchesString(); - } - - public void setText(String text) { - mEditText.setText(text); - findAll(); - } - - private void updateMatchesString() { - // Note: updateMatchesString is only called by methods that have already - // checked mWebView for null. - String template = mBrowserActivity.getResources(). - getQuantityString(R.plurals.matches_found, mNumberOfMatches, - mWebView.findIndex() + 1, mNumberOfMatches); - - mMatches.setText(template); - } - - public void afterTextChanged(Editable s) { - } -} diff --git a/src/com/android/browser/HistoryItem.java b/src/com/android/browser/HistoryItem.java index 72e1b19e5..b591b03f1 100644 --- a/src/com/android/browser/HistoryItem.java +++ b/src/com/android/browser/HistoryItem.java @@ -18,12 +18,8 @@ package com.android.browser; import android.content.Context; -import android.graphics.Bitmap; -import android.provider.Browser; import android.view.View; import android.widget.CompoundButton; -import android.widget.ImageView; -import android.widget.TextView; /** * Layout representing a history item in the classic history viewer. @@ -45,8 +41,7 @@ import android.widget.TextView; public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { if (isChecked) { - Bookmarks.addBookmark(mContext, - mContext.getContentResolver(), mUrl, getName(), null, true); + Bookmarks.addBookmark(mContext, true, mUrl, getName(), null, true); LogTag.logBookmarkAdded(mUrl, "history"); } else { Bookmarks.removeFromBookmarks(mContext, diff --git a/src/com/android/browser/PageProgressView.java b/src/com/android/browser/PageProgressView.java new file mode 100644 index 000000000..183566add --- /dev/null +++ b/src/com/android/browser/PageProgressView.java @@ -0,0 +1,94 @@ + +/* + * 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 android.content.Context; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.widget.ImageView; + +/** + * + */ +public class PageProgressView extends ImageView { + + private int mProgress; + private int mMaxProgress; + private Rect mBounds; + + /** + * @param context + * @param attrs + * @param defStyle + */ + public PageProgressView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(context); + } + + /** + * @param context + * @param attrs + */ + public PageProgressView(Context context, AttributeSet attrs) { + super(context, attrs); + init(context); + } + + /** + * @param context + */ + public PageProgressView(Context context) { + super(context); + init(context); + } + + private void init(Context ctx) { + mMaxProgress = 10000; + mBounds = new Rect(0,0,0,0); + mProgress = 0; + } + + @Override + public void onLayout(boolean f, int l, int t, int r, int b) { + mBounds.left = 0; + mBounds.right = (r - l) * mProgress / mMaxProgress; + mBounds.top = 0; + mBounds.bottom = b-t; + } + + void setMaxProgress(int max) { + mMaxProgress = max; + } + + void setProgress(int progress) { + mProgress = progress; + mBounds.right = getWidth()*mProgress/mMaxProgress; + invalidate(); + } + + @Override + public void onDraw(Canvas canvas) { +// super.onDraw(canvas); + Drawable d = getDrawable(); + d.setBounds(mBounds); + d.draw(canvas); + } + +} diff --git a/src/com/android/browser/SaveToHomescreenDialog.java b/src/com/android/browser/SaveToHomescreenDialog.java new file mode 100644 index 000000000..15f0aeafc --- /dev/null +++ b/src/com/android/browser/SaveToHomescreenDialog.java @@ -0,0 +1,127 @@ +/* + * 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 android.app.Activity; +import android.content.ComponentName; +import android.content.ContentResolver; +import android.content.Intent; +import android.content.res.Resources; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.drawable.BitmapDrawable; +import android.net.ParseException; +import android.net.Uri; +import android.net.WebAddress; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.provider.Browser; +import android.util.Log; +import android.view.View; +import android.view.Window; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.EditText; +import android.widget.TextView; +import android.widget.Toast; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Date; + +public class SaveToHomescreenDialog extends Activity { + + private EditText mTitle; + private String mUrl; + private Bitmap mFavicon; + private Bitmap mTouchIcon; + + private View.OnClickListener mOk = new View.OnClickListener() { + public void onClick(View v) { + if (save()) { + finish(); + } + } + }; + + private View.OnClickListener mCancel = new View.OnClickListener() { + public void onClick(View v) { + finish(); + } + }; + + protected void onCreate(Bundle icicle) { + super.onCreate(icicle); + requestWindowFeature(Window.FEATURE_LEFT_ICON); + setContentView(R.layout.browser_add_bookmark_const_url); + setTitle(R.string.create_shortcut_bookmark); + getWindow().setFeatureDrawableResource(Window.FEATURE_LEFT_ICON, + R.drawable.ic_list_bookmark); + + String title = null; + String url = null; + Bundle map = getIntent().getExtras(); + if (map != null) { + title = map.getString("title"); + } + + mUrl = map.getString("url"); + mFavicon = (Bitmap)map.getParcelable("favicon"); + mTouchIcon = (Bitmap)map.getParcelable("touchIcon"); + + Bitmap icon = BookmarkUtils.createIcon(this, mTouchIcon, mFavicon, + BookmarkUtils.BookmarkIconType.ICON_HOME_SHORTCUT); + getWindow().setFeatureDrawable(Window.FEATURE_LEFT_ICON, new BitmapDrawable(icon)); + + mTitle = (EditText) findViewById(R.id.title); + mTitle.setText(title); + + Button okButton = (Button) findViewById(R.id.OK); + okButton.setOnClickListener(mOk); + + Button cancelButton = (Button) findViewById(R.id.cancel); + cancelButton.setOnClickListener(mCancel); + + if (!getWindow().getDecorView().isInTouchMode()) { + okButton.requestFocus(); + } + } + + /** + * Parse the data entered in the dialog and send an intent to create an + * icon on the homescreen. + */ + private boolean save() { + String title = mTitle.getText().toString().trim(); + String unfilteredUrl = BrowserActivity.fixUrl(mUrl); + if (title.length() == 0) { + mTitle.setError(getResources().getText(R.string.bookmark_needs_title)); + return false; + } + + String url = unfilteredUrl.trim(); + + sendBroadcast(BookmarkUtils.createAddToHomeIntent(this, url, title, + mTouchIcon, mFavicon)); + setResult(RESULT_OK); + return true; + } +} diff --git a/src/com/android/browser/ScrollWebView.java b/src/com/android/browser/ScrollWebView.java new file mode 100644 index 000000000..97bd2c7a1 --- /dev/null +++ b/src/com/android/browser/ScrollWebView.java @@ -0,0 +1,125 @@ +/* + * 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 android.content.Context; +import android.util.AttributeSet; +import android.view.View; +import android.webkit.WebView; + +import java.util.Map; + +/** + * Manage WebView scroll events + */ +public class ScrollWebView extends WebView { + + private ScrollListener mScrollListener; + private boolean mIsCancelled; + private Runnable mScrollRunnable; + + /** + * @param context + * @param attrs + * @param defStyle + * @param javascriptInterfaces + */ + public ScrollWebView(Context context, AttributeSet attrs, int defStyle, + Map<String, Object> javascriptInterfaces, boolean privateBrowsing) { + super(context, attrs, defStyle, javascriptInterfaces, privateBrowsing); + } + + /** + * @param context + * @param attrs + * @param defStyle + */ + public ScrollWebView(Context context, AttributeSet attrs, int defStyle, + boolean privateBrowsing) { + super(context, attrs, defStyle, privateBrowsing); + } + + /** + * @param context + * @param attrs + */ + public ScrollWebView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + /** + * @param context + */ + public ScrollWebView(Context context) { + super(context); + } + + void hideEmbeddedTitleBar() { + scrollBy(0, getVisibleTitleHeight()); + } + + @Override + public void setEmbeddedTitleBar(final View title) { + super.setEmbeddedTitleBar(title); + if (title != null && mScrollListener != null) { + // allow the scroll listener to initialize its state + post(new Runnable() { + @Override + public void run() { + mScrollListener.onScroll((title.getHeight() == 0 || + getVisibleTitleHeight() > 0)); + } + }); + } + } + + @Override + public void stopScroll() { + mIsCancelled = true; + super.stopScroll(); + } + + @Override + protected void onScrollChanged(int l, final int t, int ol, int ot) { + super.onScrollChanged(l, t, ol, ot); + if (!mIsCancelled) { + post(mScrollRunnable); + } else { + mIsCancelled = false; + } + } + + void setScrollListener(ScrollListener l) { + mScrollListener = l; + if (mScrollListener != null) { + mScrollRunnable = new Runnable() { + public void run() { + if (!mIsCancelled) { + mScrollListener.onScroll(getVisibleTitleHeight() > 0); + } + } + }; + } + } + + // callback for scroll events + + interface ScrollListener { + public void onScroll(boolean titleVisible); + } + +} diff --git a/src/com/android/browser/SelectDialog.java b/src/com/android/browser/SelectDialog.java deleted file mode 100644 index 461127a5e..000000000 --- a/src/com/android/browser/SelectDialog.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * 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 android.provider.Browser; -import android.view.LayoutInflater; -import android.view.View; - -/* package */ class SelectDialog extends WebDialog { - private View mCopyButton; - private View mSelectAllButton; - private View mShareButton; - private View mFindButton; - - SelectDialog(BrowserActivity context) { - super(context); - LayoutInflater factory = LayoutInflater.from(context); - factory.inflate(R.layout.browser_select, this); - addCancel(); - - mCopyButton = findViewById(R.id.copy); - mCopyButton.setOnClickListener(mCopyListener); - mSelectAllButton = findViewById(R.id.select_all); - mSelectAllButton.setOnClickListener(mSelectAllListener); - mShareButton = findViewById(R.id.share); - mShareButton.setOnClickListener(mShareListener); - mFindButton = findViewById(R.id.find); - mFindButton.setOnClickListener(mFindListener); - } - - private View.OnClickListener mCopyListener = new View.OnClickListener() { - public void onClick(View v) { - mWebView.copySelection(); - mBrowserActivity.closeDialogs(); - } - }; - - private View.OnClickListener mSelectAllListener = new View.OnClickListener() { - public void onClick(View v) { - mWebView.selectAll(); - } - }; - - private View.OnClickListener mShareListener = new View.OnClickListener() { - public void onClick(View v) { - String selection = mWebView.getSelection(); - Browser.sendString(mBrowserActivity, selection); - mBrowserActivity.closeDialogs(); - } - }; - - private View.OnClickListener mFindListener = new View.OnClickListener() { - public void onClick(View v) { - String selection = mWebView.getSelection(); - mBrowserActivity.closeDialogs(); - mBrowserActivity.showFindDialog(); - mBrowserActivity.setFindDialogText(selection); - } - }; - - /** - * Called by BrowserActivity.closeDialog. Start the animation to hide - * the dialog, and inform the WebView that the dialog is being dismissed. - */ - @Override - public void dismiss() { - super.dismiss(); - mWebView.notifySelectDialogDismissed(); - } - -} diff --git a/src/com/android/browser/ShortcutActivity.java b/src/com/android/browser/ShortcutActivity.java new file mode 100644 index 000000000..7994d0a77 --- /dev/null +++ b/src/com/android/browser/ShortcutActivity.java @@ -0,0 +1,69 @@ +/* + * 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 android.app.Activity; +import android.app.Fragment; +import android.app.FragmentManager; +import android.app.FragmentTransaction; +import android.content.Intent; +import android.os.Bundle; + +public class ShortcutActivity extends Activity + implements BookmarksHistoryCallbacks { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL); + FragmentManager fm = getFragmentManager(); + FragmentTransaction transaction = fm.openTransaction(); + Bundle extras = new Bundle(); + extras.putBoolean(BrowserBookmarksPage.EXTRA_SHORTCUT, true); + extras.putBoolean(BrowserBookmarksPage.EXTRA_DISABLE_WINDOW, true); + Fragment frag = Fragment.instantiate(this, BrowserBookmarksPage.class.getName(), extras); + transaction.add(android.R.id.content, frag); + transaction.commit(); + } + + /** + * not used for shortcuts + */ + @Override + public void onRemoveParentChildRelationShips() {} + + /** + * handle fragment startActivity + */ + @Override + public void startActivityFromFragment(Fragment f, Intent intent, int requestCode) { + setResult(RESULT_OK, intent); + finish(); + } + + @Override + public void finish() { + super.finish(); + } + + /** + * not used for shortcuts + */ + @Override + public void onUrlSelected(String url, boolean newWindow) {} + +} diff --git a/src/com/android/browser/Tab.java b/src/com/android/browser/Tab.java index 7019c8a01..dc4242892 100644 --- a/src/com/android/browser/Tab.java +++ b/src/com/android/browser/Tab.java @@ -16,13 +16,8 @@ package com.android.browser; -import java.io.File; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.Map; -import java.util.Vector; +import com.android.browser.TabControl.TabChangeListener; +import com.android.common.speech.LoggingEvents; import android.app.AlertDialog; import android.app.SearchManager; @@ -42,14 +37,15 @@ import android.os.Bundle; import android.os.Message; import android.os.SystemClock; import android.provider.Browser; +import android.provider.BrowserContract.History; import android.speech.RecognizerResultsIntent; import android.util.Log; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; +import android.view.View.OnClickListener; import android.view.ViewGroup; import android.view.ViewStub; -import android.view.View.OnClickListener; import android.webkit.ConsoleMessage; import android.webkit.CookieSyncManager; import android.webkit.DownloadListener; @@ -71,7 +67,12 @@ import android.widget.ImageButton; import android.widget.LinearLayout; import android.widget.TextView; -import com.android.common.speech.LoggingEvents; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.Map; +import java.util.Vector; /** * Class for maintaining Tabs with a main WebView and a subwindow. @@ -155,6 +156,7 @@ class Tab { static final String PARENTTAB = "parentTab"; static final String APPID = "appid"; static final String ORIGINALURL = "originalUrl"; + static final String INCOGNITO = "privateBrowsingEnabled"; // ------------------------------------------------------------------------- @@ -495,7 +497,7 @@ class Tab { // update the bookmark database for favicon if (favicon != null) { - BrowserBookmarksAdapter.updateBookmarkFavicon(mActivity + Bookmarks.updateFavicon(mActivity .getContentResolver(), null, url, favicon); } @@ -510,6 +512,9 @@ class Tab { if (mInForeground) { mActivity.onPageStarted(view, url, favicon); } + if (getTabChangeListener() != null) { + getTabChangeListener().onPageStarted(Tab.this); + } } @Override @@ -531,6 +536,9 @@ class Tab { if (mInForeground) { mActivity.onPageFinished(view, url); } + if (getTabChangeListener() != null) { + getTabChangeListener().onPageFinished(Tab.this); + } } // return true if want to hijack the url to let another app to handle it @@ -590,8 +598,12 @@ class Tab { errorCode != WebViewClient.ERROR_FILE) { queueError(errorCode, description); } - Log.e(LOGTAG, "onReceivedError " + errorCode + " " + failingUrl - + " " + description); + + // Don't log URLs when in private browsing mode + if (!getWebView().isPrivateBrowsingEnabled()) { + Log.e(LOGTAG, "onReceivedError " + errorCode + " " + failingUrl + + " " + description); + } // We need to reset the title after an error if it is in foreground. if (mInForeground) { @@ -661,6 +673,9 @@ class Tab { @Override public void doUpdateVisitedHistory(WebView view, String url, boolean isReload) { + // Don't save anything in private browsing mode + if (getWebView().isPrivateBrowsingEnabled()) return; + if (url.regionMatches(true, 0, "about:", 0, 6)) { return; } @@ -680,6 +695,7 @@ class Tab { final ContentResolver cr = mActivity.getContentResolver(); final String newUrl = url; new AsyncTask<Void, Void, Void>() { + @Override protected Void doInBackground(Void... unused) { Browser.updateVisitedHistory(cr, newUrl, true); return null; @@ -956,6 +972,9 @@ class Tab { if (mInForeground) { mActivity.onProgressChanged(view, newProgress); } + if (getTabChangeListener() != null) { + getTabChangeListener().onProgress(Tab.this, newProgress); + } } @Override @@ -965,69 +984,67 @@ class Tab { // here, if url is null, we want to reset the title mActivity.setUrlTitle(pageUrl, title); } + TabChangeListener tcl = getTabChangeListener(); + if (tcl != null) { + tcl.onUrlAndTitle(Tab.this, pageUrl,title); + } if (pageUrl == null || pageUrl.length() >= SQLiteDatabase.SQLITE_MAX_LIKE_PATTERN_LENGTH) { return; } - new AsyncTask<Void, Void, Void>() { - protected Void doInBackground(Void... unused) { - // See if we can find the current url in our history - // database and add the new title to it. - String url = pageUrl; - if (url.startsWith("http://www.")) { - url = url.substring(11); - } else if (url.startsWith("http://")) { - url = url.substring(4); - } - // Escape wildcards for LIKE operator. - url = url.replace("\\", "\\\\").replace("%", "\\%") - .replace("_", "\\_"); - Cursor c = null; - try { - final ContentResolver cr - = mActivity.getContentResolver(); - url = "%" + url; - String [] selArgs = new String[] { url }; - String where = Browser.BookmarkColumns.URL - + " LIKE ? ESCAPE '\\' AND " - + Browser.BookmarkColumns.BOOKMARK + " = 0"; - c = cr.query(Browser.BOOKMARKS_URI, new String[] - { Browser.BookmarkColumns._ID }, where, selArgs, - null); - if (c.moveToFirst()) { - // Current implementation of database only has one - // entry per url. - ContentValues map = new ContentValues(); - map.put(Browser.BookmarkColumns.TITLE, title); - String[] projection = new String[] - { Integer.valueOf(c.getInt(0)).toString() }; - cr.update(Browser.BOOKMARKS_URI, map, "_id = ?", - projection); + + // Update the title in the history database if not in private browsing mode + if (!getWebView().isPrivateBrowsingEnabled()) { + new AsyncTask<Void, Void, Void>() { + @Override + protected Void doInBackground(Void... unused) { + // See if we can find the current url in our history + // database and add the new title to it. + String url = pageUrl; + if (url.startsWith("http://www.")) { + url = url.substring(11); + } else if (url.startsWith("http://")) { + url = url.substring(4); } - } catch (IllegalStateException e) { - Log.e(LOGTAG, "Tab onReceived title", e); - } catch (SQLiteException ex) { - Log.e(LOGTAG, - "onReceivedTitle() caught SQLiteException: ", - ex); - } finally { - if (c != null) c.close(); + // Escape wildcards for LIKE operator. + url = url.replace("\\", "\\\\").replace("%", "\\%") + .replace("_", "\\_"); + Cursor c = null; + try { + final ContentResolver cr = mActivity.getContentResolver(); + String selection = History.URL + " LIKE ? ESCAPE '\\'"; + String [] selectionArgs = new String[] { "%" + url }; + ContentValues values = new ContentValues(); + values.put(History.TITLE, title); + cr.update(History.CONTENT_URI, values, selection, selectionArgs); + } catch (IllegalStateException e) { + Log.e(LOGTAG, "Tab onReceived title", e); + } catch (SQLiteException ex) { + Log.e(LOGTAG, + "onReceivedTitle() caught SQLiteException: ", + ex); + } finally { + if (c != null) c.close(); + } + return null; } - return null; - } - }.execute(); + }.execute(); + } } @Override public void onReceivedIcon(WebView view, Bitmap icon) { if (icon != null) { - BrowserBookmarksAdapter.updateBookmarkFavicon(mActivity + Bookmarks.updateFavicon(mActivity .getContentResolver(), view.getOriginalUrl(), view .getUrl(), icon); } if (mInForeground) { mActivity.setFavicon(icon); } + if (getTabChangeListener() != null) { + getTabChangeListener().onFavicon(Tab.this, icon); + } } @Override @@ -1048,16 +1065,6 @@ class Tab { } @Override - public void onSelectionDone(WebView view) { - if (mInForeground) mActivity.closeDialogs(); - } - - @Override - public void onSelectionStart(WebView view) { - if (false && mInForeground) mActivity.showSelectDialog(); - } - - @Override public void onShowCustomView(View view, WebChromeClient.CustomViewCallback callback) { if (mInForeground) mActivity.onShowCustomView(view, callback); @@ -1150,6 +1157,9 @@ class Tab { } } + // Don't log console messages in private browsing mode + if (getWebView().isPrivateBrowsingEnabled()) return true; + String message = "Console: " + consoleMessage.message() + " " + consoleMessage.sourceId() + ":" + consoleMessage.lineNumber(); @@ -1202,9 +1212,9 @@ class Tab { } @Override - public void openFileChooser(ValueCallback<Uri> uploadMsg) { + public void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType) { if (mInForeground) { - mActivity.openFileChooser(uploadMsg); + mActivity.openFileChooser(uploadMsg, acceptType); } else { uploadMsg.onReceiveValue(null); } @@ -1216,10 +1226,12 @@ class Tab { @Override public void getVisitedHistory(final ValueCallback<String[]> callback) { AsyncTask<Void, Void, String[]> task = new AsyncTask<Void, Void, String[]>() { + @Override public String[] doInBackground(Void... unused) { return Browser.getVisitedHistory(mActivity .getContentResolver()); } + @Override public void onPostExecute(String[] result) { callback.onReceiveValue(result); }; @@ -1248,7 +1260,7 @@ class Tab { // Unlike the others, do not call mClient's version, which would // change the progress bar. However, we do want to remove the // find or select dialog. - mBrowserActivity.closeDialogs(); + mBrowserActivity.endActionMode(); } @Override public void doUpdateVisitedHistory(WebView view, String url, @@ -1449,7 +1461,7 @@ class Tab { */ boolean createSubWindow() { if (mSubView == null) { - mActivity.closeDialogs(); + mActivity.endActionMode(); mSubViewContainer = mInflateService.inflate( R.layout.browser_subwindow, null); mSubView = (WebView) mSubViewContainer.findViewById(R.id.webview); @@ -1497,7 +1509,7 @@ class Tab { */ void dismissSubWindow() { if (mSubView != null) { - mActivity.closeDialogs(); + mActivity.endActionMode(); BrowserSettings.getInstance().deleteObserver( mSubView.getSettings()); mSubView.destroy(); @@ -1522,7 +1534,7 @@ class Tab { void removeSubWindow(ViewGroup content) { if (mSubView != null) { content.removeView(mSubViewContainer); - mActivity.closeDialogs(); + mActivity.endActionMode(); } } @@ -1581,7 +1593,7 @@ class Tab { (FrameLayout) mContainer.findViewById(R.id.webview_wrapper); wrapper.removeView(mMainView); content.removeView(mContainer); - mActivity.closeDialogs(); + mActivity.endActionMode(); removeSubWindow(content); } @@ -1855,6 +1867,9 @@ class Tab { // FIXME: The only place we cared about subwindow was for // bookmarking (i.e. not when saving state). Was this deliberate? final WebBackForwardList list = mMainView.copyBackForwardList(); + if (list == null) { + Log.w(LOGTAG, "populatePickerData called and WebBackForwardList is null"); + } final WebHistoryItem item = list != null ? list.getCurrentItem() : null; populatePickerData(item); } @@ -1863,7 +1878,9 @@ class Tab { // WebView. private void populatePickerData(WebHistoryItem item) { mPickerData = new PickerData(); - if (item != null) { + if (item == null) { + Log.w(LOGTAG, "populatePickerData called with a null WebHistoryItem"); + } else { mPickerData.mUrl = item.getUrl(); mPickerData.mTitle = item.getTitle(); mPickerData.mFavicon = item.getFavicon(); @@ -1962,35 +1979,12 @@ class Tab { return true; } - /* - * Opens the find and select text dialogs. Called by BrowserActivity. + /** + * always get the TabChangeListener form the tab control + * @return the TabControl change listener */ - WebView showDialog(WebDialog dialog) { - LinearLayout container; - WebView view; - if (mSubView != null) { - view = mSubView; - container = (LinearLayout) mSubViewContainer.findViewById( - R.id.inner_container); - } else { - view = mMainView; - container = mContainer; - } - dialog.show(); - container.addView(dialog, 0, new LinearLayout.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.WRAP_CONTENT)); - dialog.setWebView(view); - return view; + private TabChangeListener getTabChangeListener() { + return mActivity.getTabControl().getTabChangeListener(); } - /* - * Close the find or select dialog. Called by BrowserActivity.closeDialog. - */ - void closeDialog(WebDialog dialog) { - // The dialog may be attached to the subwindow. Ensure that the - // correct parent has it removed. - LinearLayout parent = (LinearLayout) dialog.getParent(); - if (parent != null) parent.removeView(dialog); - } } diff --git a/src/com/android/browser/TabBar.java b/src/com/android/browser/TabBar.java new file mode 100644 index 000000000..cf0f27ca4 --- /dev/null +++ b/src/com/android/browser/TabBar.java @@ -0,0 +1,431 @@ +/* + * 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 android.content.Context; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Color; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.LayerDrawable; +import android.graphics.drawable.PaintDrawable; +import android.view.ContextMenu; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.MenuInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.webkit.WebView; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.android.browser.ScrollWebView.ScrollListener; +import com.android.browser.TabControl.TabChangeListener; + +import java.util.HashMap; +import java.util.Map; + +/** + * tabbed title bar for xlarge screen browser + */ +public class TabBar extends LinearLayout + implements TabChangeListener, ScrollListener, OnClickListener { + + private static final int PROGRESS_MAX = 100; + + private BrowserActivity mBrowserActivity; + + private final int mTabWidthSelected; + private final int mTabWidthUnselected; + + private TitleBarXLarge mTitleBar; + + private TabScrollView mTabs; + private TabControl mControl; + + private Map<Tab, TabViewData> mTabMap; + + private float mDensityScale; + private boolean mUserRequestedUrlbar; + private boolean mTitleVisible; + private boolean mShowUrlMode; + + public TabBar(BrowserActivity context, TabControl tabcontrol, TitleBarXLarge titlebar) { + super(context); + Resources res = context.getResources(); + mTabWidthSelected = (int) res.getDimension(R.dimen.tab_width_selected); + mTabWidthUnselected = (int) res.getDimension(R.dimen.tab_width_unselected); + + mTitleBar = titlebar; + mTitleBar.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, + LayoutParams.WRAP_CONTENT)); + mDensityScale = context.getResources().getDisplayMetrics().density; + mTabMap = new HashMap<Tab, TabViewData>(); + mBrowserActivity = context; + mControl = tabcontrol; + Resources resources = context.getResources(); + LayoutInflater factory = LayoutInflater.from(context); + factory.inflate(R.layout.tab_bar, this); + mTabs = (TabScrollView) findViewById(R.id.tabs); + + // TODO: Change enabled states based on whether you can go + // back/forward. Probably should be done inside onPageStarted. + + // build tabs + int tabcount = mControl.getTabCount(); + for (int i = 0; i < tabcount; i++) { + Tab tab = mControl.getTab(i); + TabViewData data = buildTab(tab); + TabView tv = buildView(data); + } + mTabs.setSelectedTab(mControl.getCurrentIndex()); + + // register the tab change listener + mControl.setOnTabChangeListener(this); + mUserRequestedUrlbar = false; + mTitleVisible = true; + } + + public void onClick(View view) { + if (mTabs.getSelectedTab() == view) { + if (mBrowserActivity.isFakeTitleBarShowing() && !isLoading()) { + mBrowserActivity.hideFakeTitleBar(); + } else { + showUrlBar(); + } + // temporarily disabled + // mTitleBar.requestUrlInputFocus(); + } else { + TabViewData data = (TabViewData) view.getTag(); + int ix = mControl.getTabIndex(data.mTab); + mTabs.setSelectedTab(ix); + mBrowserActivity.switchToTab(ix); + } + } + + private void showUrlBar() { + mBrowserActivity.stopScrolling(); + mBrowserActivity.showFakeTitleBar(); + mUserRequestedUrlbar = true; + } + + private void setShowUrlMode(boolean showUrl) { + mShowUrlMode = showUrl; + } + + // callback after fake titlebar is shown + void onShowTitleBar() { + setShowUrlMode(false); + } + + // callback after fake titlebar is hidden + void onHideTitleBar() { + setShowUrlMode(!mTitleVisible); + Tab tab = mControl.getCurrentTab(); + tab.getWebView().requestFocus(); + mUserRequestedUrlbar = false; + } + + // webview scroll listener + + @Override + public void onScroll(boolean titleVisible) { + mTitleVisible = titleVisible; + if (!mShowUrlMode && !mTitleVisible && !isLoading()) { + if (mUserRequestedUrlbar) { + mBrowserActivity.hideFakeTitleBar(); + } else { + setShowUrlMode(true); + } + } else if (mTitleVisible && !isLoading()) { + if (mShowUrlMode) { + setShowUrlMode(false); + } + } + } + + @Override + public void createContextMenu(ContextMenu menu) { + MenuInflater inflater = mBrowserActivity.getMenuInflater(); + inflater.inflate(R.menu.title_context, menu); + mBrowserActivity.onCreateContextMenu(menu, this, null); + } + + private TabViewData buildTab(Tab tab) { + TabViewData data = new TabViewData(tab); + mTabMap.put(tab, data); + return data; + } + + private TabView buildView(final TabViewData data) { + TabView tv = new TabView(mBrowserActivity, data); + tv.setTag(data); + tv.setOnClickListener(this); + mTabs.addTab(tv); + return tv; + } + + /** + * View used in the tab bar + */ + class TabView extends LinearLayout implements OnClickListener { + + TabViewData mTabData; + View mTabContent; + TextView mTitle; + View mIncognito; + ImageView mIconView; + ImageView mLock; + ImageView mClose; + boolean mSelected; + boolean mInLoad; + + /** + * @param context + */ + public TabView(Context context, TabViewData tab) { + super(context); + mTabData = tab; + setGravity(Gravity.CENTER_VERTICAL); + setOrientation(LinearLayout.HORIZONTAL); + setBackgroundResource(R.drawable.tab_background); + LayoutInflater inflater = LayoutInflater.from(mContext); + mTabContent = inflater.inflate(R.layout.tab_title, this, true); + mTitle = (TextView) mTabContent.findViewById(R.id.title); + mIconView = (ImageView) mTabContent.findViewById(R.id.favicon); + mLock = (ImageView) mTabContent.findViewById(R.id.lock); + mClose = (ImageView) mTabContent.findViewById(R.id.close); + mClose.setOnClickListener(this); + mIncognito = mTabContent.findViewById(R.id.incognito); + mSelected = false; + mInLoad = false; + // update the status + updateFromData(); + } + + @Override + public void onClick(View v) { + if (v == mClose) { + closeTab(); + } + } + + private void updateFromData() { + mTabData.mTabView = this; + if (mTabData.mUrl != null) { + setDisplayTitle(mTabData.mUrl); + } + if (mTabData.mTitle != null) { + setDisplayTitle(mTabData.mTitle); + } + setProgress(mTabData.mProgress); + if (mTabData.mIcon != null) { + setFavicon(mTabData.mIcon); + } + if (mTabData.mLock != null) { + setLock(mTabData.mLock); + } + if (mTabData.mTab != null) { + mIncognito.setVisibility( + mTabData.mTab.getWebView().isPrivateBrowsingEnabled() ? + View.VISIBLE : View.GONE); + } + } + + @Override + public void setSelected(boolean selected) { + mSelected = selected; + mClose.setVisibility(mSelected ? View.VISIBLE : View.GONE); + mTitle.setTextAppearance(mBrowserActivity, mSelected ? + R.style.TabTitleSelected : R.style.TabTitleUnselected); + setHorizontalFadingEdgeEnabled(!mSelected); + setFadingEdgeLength(50); + super.setSelected(selected); + setLayoutParams(new LayoutParams(selected ? + mTabWidthSelected : mTabWidthUnselected, + LayoutParams.MATCH_PARENT)); + } + + void setDisplayTitle(String title) { + mTitle.setText(title); + } + + void setFavicon(Drawable d) { + mIconView.setImageDrawable(d); + } + + void setLock(Drawable d) { + if (null == d) { + mLock.setVisibility(View.GONE); + } else { + mLock.setImageDrawable(d); + mLock.setVisibility(View.VISIBLE); + } + } + + void setTitleCompoundDrawables(Drawable left, Drawable top, + Drawable right, Drawable bottom) { + mTitle.setCompoundDrawables(left, top, right, bottom); + } + + void setProgress(int newProgress) { + if (newProgress >= PROGRESS_MAX) { + mInLoad = false; + } else { + if (!mInLoad && getWindowToken() != null) { + mInLoad = true; + } + } + } + + private void closeTab() { + if (mTabData.mTab == mControl.getCurrentTab()) { + mBrowserActivity.closeCurrentWindow(); + } else { + mBrowserActivity.closeTab(mTabData.mTab); + } + } + + } + + /** + * Store tab state within the title bar + */ + class TabViewData { + + Tab mTab; + TabView mTabView; + int mProgress; + Drawable mIcon; + Drawable mLock; + String mTitle; + String mUrl; + + TabViewData(Tab tab) { + mTab = tab; + } + + void setUrlAndTitle(String url, String title) { + mUrl = url; + mTitle = title; + if (mTabView != null) { + if (title != null) { + mTabView.setDisplayTitle(title); + } else if (url != null) { + mTabView.setDisplayTitle(url); + } + } + } + + void setProgress(int newProgress) { + mProgress = newProgress; + if (mTabView != null) { + mTabView.setProgress(mProgress); + } + } + + void setFavicon(Bitmap icon) { + Drawable[] array = new Drawable[3]; + array[0] = new PaintDrawable(Color.BLACK); + array[1] = new PaintDrawable(Color.WHITE); + if (icon == null) { +// array[2] = mGenericFavicon; + } else { + array[2] = new BitmapDrawable(icon); + } + LayerDrawable d = new LayerDrawable(array); + d.setLayerInset(1, 1, 1, 1, 1); + d.setLayerInset(2, 2, 2, 2, 2); + mIcon = d; + if (mTabView != null) { + mTabView.setFavicon(mIcon); + } + } + + } + + // TabChangeListener implementation + + @Override + public void onCurrentTab(Tab tab) { + mTabs.setSelectedTab(mControl.getCurrentIndex()); + TabViewData tvd = mTabMap.get(tab); + if (tvd != null) { + tvd.setProgress(tvd.mProgress); + // update the scroll state + WebView webview = tab.getWebView(); + onScroll(webview.getVisibleTitleHeight() > 0); + } + } + + @Override + public void onFavicon(Tab tab, Bitmap favicon) { + TabViewData tvd = mTabMap.get(tab); + if (tvd != null) { + tvd.setFavicon(favicon); + } + } + + @Override + public void onNewTab(Tab tab) { + TabViewData tvd = buildTab(tab); + buildView(tvd); + } + + @Override + public void onProgress(Tab tab, int progress) { + TabViewData tvd = mTabMap.get(tab); + if (tvd != null) { + tvd.setProgress(progress); + } + } + + @Override + public void onRemoveTab(Tab tab) { + TabViewData tvd = mTabMap.get(tab); + TabView tv = tvd.mTabView; + if (tv != null) { + mTabs.removeTab(tv); + } + mTabMap.remove(tab); + } + + @Override + public void onUrlAndTitle(Tab tab, String url, String title) { + TabViewData tvd = mTabMap.get(tab); + if (tvd != null) { + tvd.setUrlAndTitle(url, title); + } + } + + @Override + public void onPageFinished(Tab tab) { + } + + @Override + public void onPageStarted(Tab tab) { + } + + + private boolean isLoading() { + return mTabMap.get(mControl.getCurrentTab()).mTabView.mInLoad; + } + +} diff --git a/src/com/android/browser/TabControl.java b/src/com/android/browser/TabControl.java index afd4ea827..d7435d712 100644 --- a/src/com/android/browser/TabControl.java +++ b/src/com/android/browser/TabControl.java @@ -29,6 +29,7 @@ import android.webkit.WebView; import java.io.File; import java.util.ArrayList; +import java.util.HashMap; import java.util.Vector; class TabControl { @@ -47,6 +48,8 @@ class TabControl { private final BrowserActivity mActivity; // Directory to store thumbnails for each WebView. private final File mThumbnailDir; + // Use on screen zoom buttons + private boolean mDisplayZoomControls; /** * Construct a new TabControl object that interfaces with the given @@ -57,6 +60,7 @@ class TabControl { TabControl(BrowserActivity activity) { mActivity = activity; mThumbnailDir = activity.getDir("thumbnails", 0); + mDisplayZoomControls = true; } File getThumbnailDir() { @@ -68,6 +72,14 @@ class TabControl { } /** + * Set if the webview should use the on screen zoom controls + * @param enabled + */ + void setDisplayZoomControls(boolean enabled) { + mDisplayZoomControls = enabled; + } + + /** * Return the current tab's main WebView. This will always return the main * WebView for a given tab and not a subwindow. * @return The current tab's WebView. @@ -132,7 +144,7 @@ class TabControl { int getCurrentIndex() { return mCurrentTab; } - + /** * Given a Tab, find it's index * @param Tab to find @@ -150,32 +162,49 @@ class TabControl { } /** + * Returns true if there are any incognito tabs open. + * @return True when any incognito tabs are open, false otherwise. + */ + boolean hasAnyOpenIncognitoTabs() { + for (Tab tab : mTabs) { + if (tab.getWebView() != null && tab.getWebView().isPrivateBrowsingEnabled()) { + return true; + } + } + return false; + } + + /** * Create a new tab. * @return The newly createTab or null if we have reached the maximum * number of open tabs. */ - Tab createNewTab(boolean closeOnExit, String appId, String url) { + Tab createNewTab(boolean closeOnExit, String appId, String url, + boolean privateBrowsing) { int size = mTabs.size(); // Return false if we have maxed out on tabs if (MAX_TABS == size) { return null; } - final WebView w = createNewWebView(); + final WebView w = createNewWebView(privateBrowsing); // Create a new tab and add it to the tab list Tab t = new Tab(mActivity, w, closeOnExit, appId, url); mTabs.add(t); // Initially put the tab in the background. t.putInBackground(); + if (mTabChangeListener != null) { + mTabChangeListener.onNewTab(t); + } return t; } /** * Create a new tab with default values for closeOnExit(false), - * appId(null), and url(null). + * appId(null), url(null), and privateBrowsing(false). */ Tab createNewTab() { - return createNewTab(false, null, null); + return createNewTab(false, null, null, false); } /** @@ -231,6 +260,9 @@ class TabControl { // Remove it from the queue of viewed tabs. mTabQueue.remove(t); + if (mTabChangeListener != null) { + mTabChangeListener.onRemoveTab(t); + } return true; } @@ -277,29 +309,56 @@ class TabControl { * @return True if there were previous tabs that were restored. False if * there was no saved state or restoring the state failed. */ - boolean restoreState(Bundle inState) { + boolean restoreState(Bundle inState, boolean dontRestoreIncognitoTabs) { final int numTabs = (inState == null) ? -1 : inState.getInt(Tab.NUMTABS, -1); if (numTabs == -1) { return false; } else { - final int currentTab = inState.getInt(Tab.CURRTAB, -1); + final int oldCurrentTab = inState.getInt(Tab.CURRTAB, -1); + + // Determine whether the saved current tab can be restored, and + // if not, which tab will take its place. + int currentTab = -1; + if (!dontRestoreIncognitoTabs + || !inState.getBundle(Tab.WEBVIEW + oldCurrentTab).getBoolean(Tab.INCOGNITO)) { + currentTab = oldCurrentTab; + } else { + for (int i = 0; i < numTabs; i++) { + if (!inState.getBundle(Tab.WEBVIEW + i).getBoolean(Tab.INCOGNITO)) { + currentTab = i; + break; + } + } + } + if (currentTab < 0) { + return false; + } + + // Map saved tab indices to new indices, in case any incognito tabs + // need to not be restored. + HashMap<Integer, Integer> originalTabIndices = new HashMap<Integer, Integer>(); + originalTabIndices.put(-1, -1); for (int i = 0; i < numTabs; i++) { - if (i == currentTab) { + Bundle state = inState.getBundle(Tab.WEBVIEW + i); + + if (dontRestoreIncognitoTabs && state != null && state.getBoolean(Tab.INCOGNITO)) { + originalTabIndices.put(i, -1); + } else if (i == currentTab) { Tab t = createNewTab(); // Me must set the current tab before restoring the state // so that all the client classes are set. setCurrentTab(t); - if (!t.restoreState(inState.getBundle(Tab.WEBVIEW + i))) { + if (!t.restoreState(state)) { Log.w(LOGTAG, "Fail in restoreState, load home page."); t.getWebView().loadUrl(BrowserSettings.getInstance() .getHomePage()); } + originalTabIndices.put(i, getTabCount() - 1); } else { // Create a new tab and don't restore the state yet, add it // to the tab list Tab t = new Tab(mActivity, null, false, null, null); - Bundle state = inState.getBundle(Tab.WEBVIEW + i); if (state != null) { t.setSavedState(state); t.populatePickerDataFromSavedState(); @@ -311,15 +370,17 @@ class TabControl { mTabs.add(t); // added the tab to the front as they are not current mTabQueue.add(0, t); + originalTabIndices.put(i, getTabCount() - 1); } } + // Rebuild the tree of tabs. Do this after all tabs have been // created/restored so that the parent tab exists. for (int i = 0; i < numTabs; i++) { final Bundle b = inState.getBundle(Tab.WEBVIEW + i); final Tab t = getTab(i); if (b != null && t != null) { - final int parentIndex = b.getInt(Tab.PARENTTAB, -1); + final Integer parentIndex = originalTabIndices.get(b.getInt(Tab.PARENTTAB, -1)); if (parentIndex != -1) { final Tab parent = getTab(parentIndex); if (parent != null) { @@ -529,13 +590,25 @@ class TabControl { * Creates a new WebView and registers it with the global settings. */ private WebView createNewWebView() { + return createNewWebView(false); + } + + /** + * Creates a new WebView and registers it with the global settings. + * @param privateBrowsing When true, enables private browsing in the new + * WebView. + */ + private WebView createNewWebView(boolean privateBrowsing) { // Create a new WebView - WebView w = new WebView(mActivity); + ScrollWebView w = new ScrollWebView(mActivity, null, + com.android.internal.R.attr.webViewStyle, privateBrowsing); + w.setScrollListener(mActivity.getScrollListener()); w.setScrollbarFadingEnabled(true); w.setScrollBarStyle(View.SCROLLBARS_OUTSIDE_OVERLAY); w.setMapTrackballToArrowKeys(false); // use trackball directly // Enable the built-in zoom w.getSettings().setBuiltInZoomControls(true); + w.getSettings().setDisplayZoomControls(mDisplayZoomControls); // Add this WebView to the settings observer list and update the // settings final BrowserSettings s = BrowserSettings.getInstance(); @@ -619,4 +692,42 @@ class TabControl { } return true; } + + interface TabChangeListener { + + public void onNewTab(Tab tab); + + public void onRemoveTab(Tab tab); + + public void onCurrentTab(Tab tab); + + public void onProgress(Tab tab, int progress); + + public void onUrlAndTitle(Tab tab, String url, String title); + + public void onFavicon(Tab tab, Bitmap favicon); + + public void onPageStarted(Tab tab); + + public void onPageFinished(Tab tab); + + } + + private TabChangeListener mTabChangeListener; + + /** + * register the TabChangeListener with the tab control + * @param listener + */ + void setOnTabChangeListener(TabChangeListener listener) { + mTabChangeListener = listener; + } + + /** + * get the current TabChangeListener (used by the tabs) + */ + TabChangeListener getTabChangeListener() { + return mTabChangeListener; + } + } diff --git a/src/com/android/browser/TabScrollView.java b/src/com/android/browser/TabScrollView.java new file mode 100644 index 000000000..b41141679 --- /dev/null +++ b/src/com/android/browser/TabScrollView.java @@ -0,0 +1,142 @@ +/* + * 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 android.content.Context; +import android.util.AttributeSet; +import android.view.View; +import android.widget.HorizontalScrollView; +import android.widget.LinearLayout; + +/** + * custom view for displaying tabs in the tabbed title bar + */ +public class TabScrollView extends HorizontalScrollView { + + private Context mContext; + + private LinearLayout mContentView; + + private int mSelected; + + /** + * @param context + * @param attrs + * @param defStyle + */ + public TabScrollView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(context); + } + + /** + * @param context + * @param attrs + */ + public TabScrollView(Context context, AttributeSet attrs) { + super(context, attrs); + init(context); + } + + /** + * @param context + */ + public TabScrollView(Context context) { + super(context); + init(context); + } + + private void init(Context ctx) { + mContext = ctx; + setHorizontalScrollBarEnabled(false); + mContentView = new LinearLayout(mContext); + mContentView.setOrientation(LinearLayout.HORIZONTAL); + mContentView.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, + LayoutParams.MATCH_PARENT)); + addView(mContentView); + mSelected = -1; + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + ensureChildVisible(getSelectedTab()); + } + + void setSelectedTab(int position) { + View v = getSelectedTab(); + if (v != null) { + v.setSelected(false); + } + mSelected = position; + v = getSelectedTab(); + if (v != null) { + v.setSelected(true); + } + requestLayout(); + } + + View getSelectedTab() { + if ((mSelected >= 0) && (mSelected < mContentView.getChildCount())) { + return mContentView.getChildAt(mSelected); + } else { + return null; + } + } + + void clearTabs() { + mContentView.removeAllViews(); + } + + void addTab(View tab) { + mContentView.addView(tab); + tab.setSelected(false); + } + + void addTab(View tab, int pos) { + mContentView.addView(tab, pos); + tab.setSelected(false); + } + + void removeTab(View tab) { + int ix = mContentView.indexOfChild(tab); + if (ix == mSelected) { + mSelected = -1; + } else if (ix < mSelected) { + mSelected--; + } + mContentView.removeView(tab); + } + + void ensureChildVisible(View child) { + if (child != null) { + int childl = child.getLeft(); + int childr = childl + child.getWidth(); + int viewl = getScrollX(); + int viewr = viewl + getWidth(); + if (childl < viewl) { + // need scrolling to left + scrollTo(childl, 0); + } else if (childr > viewr) { + // need scrolling to right + scrollTo(childr - viewr + viewl, 0); + } + } + } + + +} diff --git a/src/com/android/browser/TitleBar.java b/src/com/android/browser/TitleBar.java index dc4979bd3..bbb55ad0d 100644 --- a/src/com/android/browser/TitleBar.java +++ b/src/com/android/browser/TitleBar.java @@ -21,14 +21,8 @@ import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.res.Resources; -import android.graphics.Bitmap; -import android.graphics.Color; -import android.graphics.Rect; import android.graphics.drawable.Animatable; -import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; -import android.graphics.drawable.LayerDrawable; -import android.graphics.drawable.PaintDrawable; import android.os.Handler; import android.os.Message; import android.speech.RecognizerIntent; @@ -45,7 +39,6 @@ import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; import android.widget.ImageView; -import android.widget.LinearLayout; import android.widget.ProgressBar; import android.widget.TextView; @@ -55,21 +48,16 @@ import com.android.common.speech.LoggingEvents; * This class represents a title bar for a particular "tab" or "window" in the * browser. */ -public class TitleBar extends LinearLayout { +public class TitleBar extends TitleBarBase { private TextView mTitle; - private Drawable mCloseDrawable; private ImageView mRtButton; private Drawable mCircularProgress; private ProgressBar mHorizontalProgress; - private ImageView mFavicon; - private ImageView mLockIcon; private ImageView mStopButton; private Drawable mBookmarkDrawable; private Drawable mVoiceDrawable; private boolean mInLoad; private BrowserActivity mBrowserActivity; - private Drawable mGenericFavicon; - private int mIconDimension; private View mTitleBg; private MyHandler mHandler; private Intent mVoiceSearchIntent; @@ -84,7 +72,7 @@ public class TitleBar extends LinearLayout { private static int LONG_PRESS = 1; public TitleBar(BrowserActivity context) { - super(context, null); + super(context); mHandler = new MyHandler(); LayoutInflater factory = LayoutInflater.from(context); factory.inflate(R.layout.title_bar, this); @@ -107,13 +95,11 @@ public class TitleBar extends LinearLayout { TypedValue.COMPLEX_UNIT_DIP, 8f, metrics); mRightMargin = (int) TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, 6f, metrics); - mIconDimension = (int) TypedValue.applyDimension( + int iconDimension = (int) TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, 20f, metrics); - mCircularProgress.setBounds(0, 0, mIconDimension, mIconDimension); + mCircularProgress.setBounds(0, 0, iconDimension, iconDimension); mHorizontalProgress = (ProgressBar) findViewById( R.id.progress_horizontal); - mGenericFavicon = context.getResources().getDrawable( - R.drawable.app_web_browser_sm); mVoiceSearchIntent = new Intent(RecognizerIntent.ACTION_WEB_SEARCH); mVoiceSearchIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_WEB_SEARCH); @@ -219,7 +205,7 @@ public class TitleBar extends LinearLayout { } else if (mInLoad) { mBrowserActivity.stopLoading(); } else { - mBrowserActivity.bookmarksOrHistoryPicker(false); + mBrowserActivity.promptAddOrInstallBookmark(button); } button.setPressed(false); } else if (mTitleBg.isPressed()) { @@ -248,25 +234,6 @@ public class TitleBar extends LinearLayout { } /** - * Set a new Bitmap for the Favicon. - */ - /* package */ void setFavicon(Bitmap icon) { - Drawable[] array = new Drawable[3]; - array[0] = new PaintDrawable(Color.BLACK); - PaintDrawable p = new PaintDrawable(Color.WHITE); - array[1] = p; - if (icon == null) { - array[2] = mGenericFavicon; - } else { - array[2] = new BitmapDrawable(icon); - } - LayerDrawable d = new LayerDrawable(array); - d.setLayerInset(1, 1, 1, 1, 1); - d.setLayerInset(2, 2, 2, 2, 2); - mFavicon.setImageDrawable(d); - } - - /** * Change the TitleBar to or from voice mode. If there is no package to * handle voice search, the TitleBar cannot be set to voice mode. */ @@ -302,18 +269,6 @@ public class TitleBar extends LinearLayout { } /** - * Set the Drawable for the lock icon, or null to hide it. - */ - /* package */ void setLock(Drawable d) { - if (null == d) { - mLockIcon.setVisibility(View.GONE); - } else { - mLockIcon.setImageDrawable(d); - mLockIcon.setVisibility(View.VISIBLE); - } - } - - /** * Update the progress, from 0 to 100. */ /* package */ void setProgress(int newProgress) { @@ -374,11 +329,4 @@ public class TitleBar extends LinearLayout { } } } - - /* package */ void setToTabPicker() { - mTitle.setText(R.string.tab_picker_title); - setFavicon(null); - setLock(null); - mHorizontalProgress.setVisibility(View.GONE); - } } diff --git a/src/com/android/browser/TitleBarBase.java b/src/com/android/browser/TitleBarBase.java new file mode 100644 index 000000000..7016dc020 --- /dev/null +++ b/src/com/android/browser/TitleBarBase.java @@ -0,0 +1,78 @@ +/* + * 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 android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Color; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.LayerDrawable; +import android.graphics.drawable.PaintDrawable; +import android.view.View; +import android.widget.ImageView; +import android.widget.LinearLayout; + +/** + * Base class for a title bar used by the browser. + */ +public class TitleBarBase extends LinearLayout { + // These need to be set by the subclass. + protected ImageView mFavicon; + protected ImageView mLockIcon; + + protected Drawable mGenericFavicon; + + public TitleBarBase(Context context) { + super(context, null); + mGenericFavicon = context.getResources().getDrawable( + R.drawable.app_web_browser_sm); + } + + /* package */ void setProgress(int newProgress) {} + /* package */ void setDisplayTitle(String title) {} + + /* package */ void setLock(Drawable d) { + assert mLockIcon != null; + if (null == d) { + mLockIcon.setVisibility(View.GONE); + } else { + mLockIcon.setImageDrawable(d); + mLockIcon.setVisibility(View.VISIBLE); + } + } + + /* package */ void setFavicon(Bitmap icon) { + assert mFavicon != null; + Drawable[] array = new Drawable[3]; + array[0] = new PaintDrawable(Color.BLACK); + PaintDrawable p = new PaintDrawable(Color.WHITE); + array[1] = p; + if (icon == null) { + array[2] = mGenericFavicon; + } else { + array[2] = new BitmapDrawable(icon); + } + LayerDrawable d = new LayerDrawable(array); + d.setLayerInset(1, 1, 1, 1, 1); + d.setLayerInset(2, 2, 2, 2, 2); + mFavicon.setImageDrawable(d); + } + + /* package */ void setInVoiceMode(boolean inVoiceMode) {} + +} diff --git a/src/com/android/browser/TitleBarXLarge.java b/src/com/android/browser/TitleBarXLarge.java new file mode 100644 index 000000000..3408108e6 --- /dev/null +++ b/src/com/android/browser/TitleBarXLarge.java @@ -0,0 +1,222 @@ +/* + * 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 com.android.browser.UrlInputView.UrlInputListener; + +import android.app.SearchManager; +import android.content.Context; +import android.content.Intent; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.drawable.Drawable; +import android.util.Log; +import android.view.ContextMenu; +import android.view.LayoutInflater; +import android.view.MenuInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.View.OnFocusChangeListener; +import android.widget.ImageView; +import android.widget.TextView; + +/** + * tabbed title bar for xlarge screen browser + */ +public class TitleBarXLarge extends TitleBarBase + implements UrlInputListener, OnClickListener, OnFocusChangeListener { + + private static final int PROGRESS_MAX = 100; + + private BrowserActivity mBrowserActivity; + private Drawable mStopDrawable; + private Drawable mReloadDrawable; + + + private View mContainer; + private View mBackButton; + private View mForwardButton; + private View mStar; + private View mSearchButton; + private View mFocusContainer; + private View mUnfocusContainer; + private View mGoButton; + private ImageView mStopButton; + private View mAllButton; + private PageProgressView mProgressView; + private UrlInputView mUrlFocused; + private TextView mUrlUnfocused; + private boolean mInLoad; + + public TitleBarXLarge(BrowserActivity context) { + super(context); + mBrowserActivity = context; + Resources resources = context.getResources(); + mStopDrawable = resources.getDrawable(R.drawable.ic_stop_normal); + mReloadDrawable = resources.getDrawable(R.drawable.ic_refresh_normal); + rebuildLayout(context, true); + } + + private void rebuildLayout(Context context, boolean rebuildData) { + LayoutInflater factory = LayoutInflater.from(context); + factory.inflate(R.layout.url_bar, this); + + mContainer = findViewById(R.id.taburlbar); + mUrlFocused = (UrlInputView) findViewById(R.id.url_focused); + mUrlUnfocused = (TextView) findViewById(R.id.url_unfocused); + mAllButton = findViewById(R.id.all_btn); + // TODO: Change enabled states based on whether you can go + // back/forward. Probably should be done inside onPageStarted. + mBackButton = findViewById(R.id.back); + mForwardButton = findViewById(R.id.forward); + mStar = findViewById(R.id.star); + mStopButton = (ImageView) findViewById(R.id.stop); + mSearchButton = findViewById(R.id.search); + mLockIcon = (ImageView) findViewById(R.id.lock); + mGoButton = findViewById(R.id.go); + mProgressView = (PageProgressView) findViewById(R.id.progress); + mFocusContainer = findViewById(R.id.urlbar_focused); + mUnfocusContainer = findViewById(R.id.urlbar_unfocused); + + mBackButton.setOnClickListener(this); + mForwardButton.setOnClickListener(this); + mStar.setOnClickListener(this); + mAllButton.setOnClickListener(this); + mStopButton.setOnClickListener(this); + mSearchButton.setOnClickListener(this); + mGoButton.setOnClickListener(this); + mUrlFocused.setUrlInputListener(this); + mUrlUnfocused.setOnFocusChangeListener(this); + } + + public void onFocusChange(View v, boolean hasFocus) { + if (hasFocus) { + swapUrlContainer(true); + mUrlFocused.selectAll(); + mUrlFocused.requestFocus(); + mUrlFocused.setDropDownWidth(mUnfocusContainer.getWidth()); + mUrlFocused.setDropDownHorizontalOffset(-mUrlFocused.getLeft()); + } + } + + @Override + public void onClick(View v) { + if (mBackButton == v) { + mBrowserActivity.getTopWindow().goBack(); + } else if (mForwardButton == v) { + mBrowserActivity.getTopWindow().goForward(); + } else if (mStar == v) { + mBrowserActivity.promptAddOrInstallBookmark(mStar); + } else if (mAllButton == v) { + mBrowserActivity.bookmarksOrHistoryPicker(false, false); + } else if (mSearchButton == v) { + search(); + } else if (mStopButton == v) { + stopOrRefresh(); + } else if (mGoButton == v) { + onAction(mUrlFocused.getText().toString()); + } + } + + int getHeightWithoutProgress() { + return mContainer.getHeight(); + } + + @Override + void setFavicon(Bitmap icon) { } + + // UrlInputListener implementation + + @Override + public void onAction(String text) { + mBrowserActivity.getTabControl().getCurrentTopWebView().requestFocus(); + mBrowserActivity.hideFakeTitleBar(); + Intent i = new Intent(); + i.setAction(Intent.ACTION_SEARCH); + i.putExtra(SearchManager.QUERY, text); + mBrowserActivity.onNewIntent(i); + swapUrlContainer(false); + setDisplayTitle(text); + } + + @Override + public void onDismiss() { + mBrowserActivity.getTabControl().getCurrentTopWebView().requestFocus(); + mBrowserActivity.hideFakeTitleBar(); + setDisplayTitle(mBrowserActivity.getTabControl().getCurrentWebView().getUrl()); + swapUrlContainer(false); + } + + @Override + public void onEdit(String text) { + setDisplayTitle(text); + if (text != null) { + mUrlFocused.setSelection(text.length()); + } + } + + private void swapUrlContainer(boolean focus) { + mUnfocusContainer.setVisibility(focus ? View.GONE : View.VISIBLE); + mFocusContainer.setVisibility(focus ? View.VISIBLE : View.GONE); + } + + @Override + public void createContextMenu(ContextMenu menu) { + MenuInflater inflater = mBrowserActivity.getMenuInflater(); + inflater.inflate(R.menu.title_context, menu); + mBrowserActivity.onCreateContextMenu(menu, this, null); + } + + private void search() { + setDisplayTitle(""); + mUrlUnfocused.requestFocus(); + } + + private void stopOrRefresh() { + if (mInLoad) { + mBrowserActivity.stopLoading(); + } else { + mBrowserActivity.getTopWindow().reload(); + } + } + + /** + * Update the progress, from 0 to 100. + */ + @Override + void setProgress(int newProgress) { + if (newProgress >= PROGRESS_MAX) { + mProgressView.setVisibility(View.GONE); + mInLoad = false; + mStopButton.setImageDrawable(mReloadDrawable); + } else { + if (!mInLoad) { + mProgressView.setVisibility(View.VISIBLE); + mInLoad = true; + mStopButton.setImageDrawable(mStopDrawable); + } + mProgressView.setProgress(newProgress*10000/PROGRESS_MAX); + } + } + + @Override + /* package */ void setDisplayTitle(String title) { + mUrlFocused.setText(title); + mUrlUnfocused.setText(title); + } + +} diff --git a/src/com/android/browser/UrlInputView.java b/src/com/android/browser/UrlInputView.java new file mode 100644 index 000000000..96a598019 --- /dev/null +++ b/src/com/android/browser/UrlInputView.java @@ -0,0 +1,239 @@ +/* + * 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 android.app.SearchManager; +import android.content.ContentResolver; +import android.content.Context; +import android.database.Cursor; +import android.graphics.Color; +import android.graphics.drawable.Drawable; +import android.text.Editable; +import android.text.SpannableStringBuilder; +import android.text.TextWatcher; +import android.text.style.BackgroundColorSpan; +import android.util.AttributeSet; +import android.util.Log; +import android.view.ActionMode; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.View.OnFocusChangeListener; +import android.view.ViewGroup; +import android.view.inputmethod.InputMethodManager; +import android.widget.AutoCompleteTextView; +import android.widget.CursorAdapter; +import android.widget.Filterable; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.TextView; +import android.widget.TextView.OnEditorActionListener; + +/** + * url/search input view + * handling suggestions + */ +public class UrlInputView extends AutoCompleteTextView + implements OnFocusChangeListener, OnClickListener, OnEditorActionListener { + + private UrlInputListener mListener; + private InputMethodManager mInputManager; + private SuggestionsAdapter mAdapter; + + private OnFocusChangeListener mWrappedFocusListener; + + public UrlInputView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(context); + } + + public UrlInputView(Context context, AttributeSet attrs) { + super(context, attrs); + init(context); + } + + public UrlInputView(Context context) { + super(context); + init(context); + } + + private void init(Context ctx) { + mInputManager = (InputMethodManager) ctx.getSystemService(Context.INPUT_METHOD_SERVICE); + setOnEditorActionListener(this); + super.setOnFocusChangeListener(this); + final ContentResolver cr = mContext.getContentResolver(); + mAdapter = new SuggestionsAdapter(mContext, + BrowserProvider.getBookmarksSuggestions(cr, null)); + setAdapter(mAdapter); + setSelectAllOnFocus(false); + } + + @Override + public ActionMode startActionMode(ActionMode.Callback callback) { + // suppress selection action mode + return null; + } + + @Override + public void setOnFocusChangeListener(OnFocusChangeListener focusListener) { + mWrappedFocusListener = focusListener; + } + + @Override + public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { + finishInput(getText().toString()); + return true; + } + + @Override + public void onFocusChange(View v, boolean hasFocus) { + if (hasFocus) { + forceIme(); + } else { + finishInput(null); + } + if (mWrappedFocusListener != null) { + mWrappedFocusListener.onFocusChange(v, hasFocus); + } + } + + @Override + public void onClick(View view) { + if (view instanceof ImageButton) { + // user pressed edit search button + String text = mAdapter.getViewString((View)view.getParent()); + mListener.onEdit(text); + } else { + // user selected dropdown item + String url = mAdapter.getViewString(view); + finishInput(url); + } + } + + public void setUrlInputListener(UrlInputListener listener) { + mListener = listener; + } + + public void forceIme() { + mInputManager.showSoftInput(this, 0); + } + + private void finishInput(String url) { + this.dismissDropDown(); + this.setSelection(0,0); + mInputManager.hideSoftInputFromWindow(getWindowToken(), 0); + if (url == null) { + mListener.onDismiss(); + } else { + mListener.onAction(url); + } + } + + @Override + public boolean onKeyPreIme(int keyCode, KeyEvent evt) { + if (keyCode == KeyEvent.KEYCODE_BACK) { + // catch back key in order to do slightly more cleanup than usual + finishInput(null); + return true; + } + return super.onKeyPreIme(keyCode, evt); + } + + interface UrlInputListener { + public void onDismiss(); + public void onAction(String text); + public void onEdit(String text); + } + + /** + * adapter used by suggestion dropdown + */ + class SuggestionsAdapter extends CursorAdapter implements Filterable { + + private Cursor mLastCursor; + private ContentResolver mContent; + private int mIndexText1; + private int mIndexText2; + private int mIndexIcon; + + public SuggestionsAdapter(Context context, Cursor c) { + super(context, c); + mContent = context.getContentResolver(); + mIndexText1 = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_1); + mIndexText2 = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_2_URL); + mIndexIcon = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_1); + } + + public String getViewString(View view) { + TextView tv2 = (TextView) view.findViewById(android.R.id.text2); + if (tv2.getText().length() > 0) { + return tv2.getText().toString(); + } else { + TextView tv1 = (TextView) view.findViewById(android.R.id.text1); + return tv1.getText().toString(); + } + } + + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + final LayoutInflater inflater = LayoutInflater.from(context); + final View view = inflater.inflate( + R.layout.url_dropdown_item, parent, false); + bindView(view, context, cursor); + return view; + } + + @Override + public void bindView(View view, Context context, Cursor cursor) { + TextView tv1 = (TextView) view.findViewById(android.R.id.text1); + TextView tv2 = (TextView) view.findViewById(android.R.id.text2); + ImageView ic1 = (ImageView) view.findViewById(R.id.icon1); + View ic2 = view.findViewById(R.id.icon2); + tv1.setText(cursor.getString(mIndexText1)); + String url = cursor.getString(mIndexText2); + tv2.setText((url != null) ? url : ""); + ic2.setOnClickListener(UrlInputView.this); + // assume an id + try { + int id = Integer.parseInt(cursor.getString(mIndexIcon)); + Drawable d = context.getResources().getDrawable(id); + ic1.setImageDrawable(d); + ic2.setVisibility((id == R.drawable.ic_search_category_suggest)? View.VISIBLE : View.GONE); + } catch (NumberFormatException nfx) { + } + view.setOnClickListener(UrlInputView.this); + } + + @Override + public String convertToString(Cursor cursor) { + return cursor.getString(mIndexText1); + } + + @Override + public Cursor runQueryOnBackgroundThread(CharSequence constraint) { + if (getFilterQueryProvider() != null) { + return getFilterQueryProvider().runQuery(constraint); + } + mLastCursor = BrowserProvider.getBookmarksSuggestions(mContent, + (constraint != null) ? constraint.toString() : null); + return mLastCursor; + } + + } + +} diff --git a/src/com/android/browser/WebDialog.java b/src/com/android/browser/WebDialog.java deleted file mode 100644 index 9995e8f31..000000000 --- a/src/com/android/browser/WebDialog.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * 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 android.content.Context; -import android.view.View; -import android.view.animation.AnimationUtils; -import android.view.inputmethod.InputMethodManager; -import android.webkit.WebView; -import android.widget.LinearLayout; - -/* package */ class WebDialog extends LinearLayout { - protected WebView mWebView; - protected BrowserActivity mBrowserActivity; - private boolean mIsVisible; - - /* package */ WebDialog(BrowserActivity context) { - super(context); - mBrowserActivity = context; - } - - /* dialogs that have cancel buttons can optionally share code by including a - * view with an id of 'done'. - */ - protected void addCancel() { - View button = findViewById(R.id.done); - if (button != null) button.setOnClickListener(mCancelListener); - } - - private View.OnClickListener mCancelListener = new View.OnClickListener() { - public void onClick(View v) { - mBrowserActivity.closeDialogs(); - } - }; - - protected void dismiss() { - startAnimation(AnimationUtils.loadAnimation(mBrowserActivity, - R.anim.dialog_exit)); - mIsVisible = false; - } - - /* - * Remove the soft keyboard from the screen. - */ - protected void hideSoftInput() { - InputMethodManager imm = (InputMethodManager) - mBrowserActivity.getSystemService(Context.INPUT_METHOD_SERVICE); - imm.hideSoftInputFromWindow(mWebView.getWindowToken(), 0); - } - - protected boolean isVisible() { - return mIsVisible; - } - - /* package */ void setWebView(WebView webview) { - mWebView = webview; - } - - protected void show() { - startAnimation(AnimationUtils.loadAnimation(mBrowserActivity, - R.anim.dialog_enter)); - mIsVisible = true; - } - -} diff --git a/src/com/android/browser/provider/BrowserProvider2.java b/src/com/android/browser/provider/BrowserProvider2.java new file mode 100644 index 000000000..47d92de86 --- /dev/null +++ b/src/com/android/browser/provider/BrowserProvider2.java @@ -0,0 +1,1095 @@ +/* + * Copyright (C) 2010 he 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.provider; + +import com.android.browser.R; +import com.android.internal.content.SyncStateContentProviderHelper; + +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Context; +import android.content.UriMatcher; +import android.database.Cursor; +import android.database.DatabaseUtils; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.database.sqlite.SQLiteQueryBuilder; +import android.net.Uri; +import android.provider.BrowserContract; +import android.provider.BrowserContract.Accounts; +import android.provider.BrowserContract.Bookmarks; +import android.provider.BrowserContract.ChromeSyncColumns; +import android.provider.BrowserContract.Combined; +import android.provider.BrowserContract.History; +import android.provider.BrowserContract.Images; +import android.provider.BrowserContract.Searches; +import android.provider.BrowserContract.SyncState; +import android.provider.ContactsContract.RawContacts; +import android.provider.SyncStateContract; +import android.text.TextUtils; + +import java.util.HashMap; + +public class BrowserProvider2 extends SQLiteContentProvider { + + static final String LEGACY_AUTHORITY = "browser"; + static final Uri LEGACY_AUTHORITY_URI = new Uri.Builder().authority(LEGACY_AUTHORITY).build(); + + static final String TABLE_BOOKMARKS = "bookmarks"; + static final String TABLE_HISTORY = "history"; + static final String TABLE_IMAGES = "images"; + static final String TABLE_SEARCHES = "searches"; + static final String TABLE_SYNC_STATE = "syncstate"; + static final String VIEW_COMBINED = "combined"; + + static final String TABLE_BOOKMARKS_JOIN_IMAGES = "bookmarks LEFT OUTER JOIN images " + + "ON bookmarks.url = images." + Images.URL; + static final String TABLE_HISTORY_JOIN_IMAGES = "history LEFT OUTER JOIN images " + + "ON history.url = images." + Images.URL; + + static final String DEFAULT_SORT_HISTORY = History.DATE_LAST_VISITED + " DESC"; + + static final int BOOKMARKS = 1000; + static final int BOOKMARKS_ID = 1001; + static final int BOOKMARKS_FOLDER = 1002; + static final int BOOKMARKS_FOLDER_ID = 1003; + + static final int HISTORY = 2000; + static final int HISTORY_ID = 2001; + + static final int SEARCHES = 3000; + static final int SEARCHES_ID = 3001; + + static final int SYNCSTATE = 4000; + static final int SYNCSTATE_ID = 4001; + + static final int IMAGES = 5000; + + static final int COMBINED = 6000; + static final int COMBINED_ID = 6001; + + static final int ACCOUNTS = 7000; + + static final long FIXED_ID_CHROME_ROOT = 1; + static final long FIXED_ID_BOOKMARKS = 2; + static final long FIXED_ID_BOOKMARKS_BAR = 3; + static final long FIXED_ID_OTHER_BOOKMARKS = 4; + + static final String DEFAULT_BOOKMARKS_SORT_ORDER = "position ASC, _id ASC"; + + static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH); + + static final HashMap<String, String> ACCOUNTS_PROJECTION_MAP = new HashMap<String, String>(); + static final HashMap<String, String> BOOKMARKS_PROJECTION_MAP = new HashMap<String, String>(); + static final HashMap<String, String> OTHER_BOOKMARKS_PROJECTION_MAP = + new HashMap<String, String>(); + static final HashMap<String, String> HISTORY_PROJECTION_MAP = new HashMap<String, String>(); + static final HashMap<String, String> SYNC_STATE_PROJECTION_MAP = new HashMap<String, String>(); + static final HashMap<String, String> IMAGES_PROJECTION_MAP = new HashMap<String, String>(); + static final HashMap<String, String> COMBINED_PROJECTION_MAP = new HashMap<String, String>(); + + static { + final UriMatcher matcher = URI_MATCHER; + final String authority = BrowserContract.AUTHORITY; + matcher.addURI(authority, "accounts", ACCOUNTS); + matcher.addURI(authority, "bookmarks", BOOKMARKS); + matcher.addURI(authority, "bookmarks/#", BOOKMARKS_ID); + matcher.addURI(authority, "bookmarks/folder", BOOKMARKS_FOLDER); + matcher.addURI(authority, "bookmarks/folder/#", BOOKMARKS_FOLDER_ID); + matcher.addURI(authority, "history", HISTORY); + matcher.addURI(authority, "history/#", HISTORY_ID); + matcher.addURI(authority, "searches", SEARCHES); + matcher.addURI(authority, "searches/#", SEARCHES_ID); + matcher.addURI(authority, "syncstate", SYNCSTATE); + matcher.addURI(authority, "syncstate/#", SYNCSTATE_ID); + matcher.addURI(authority, "images", IMAGES); + matcher.addURI(authority, "combined", COMBINED); + matcher.addURI(authority, "combined/#", COMBINED_ID); + + // Projection maps + HashMap<String, String> map; + + // Accounts + map = ACCOUNTS_PROJECTION_MAP; + map.put(Accounts.ACCOUNT_TYPE, Accounts.ACCOUNT_TYPE); + map.put(Accounts.ACCOUNT_NAME, Accounts.ACCOUNT_NAME); + + // Bookmarks + map = BOOKMARKS_PROJECTION_MAP; + map.put(Bookmarks._ID, qualifyColumn(TABLE_BOOKMARKS, Bookmarks._ID)); + map.put(Bookmarks.TITLE, Bookmarks.TITLE); + map.put(Bookmarks.URL, Bookmarks.URL); + map.put(Bookmarks.FAVICON, Bookmarks.FAVICON); + map.put(Bookmarks.THUMBNAIL, Bookmarks.THUMBNAIL); + map.put(Bookmarks.TOUCH_ICON, Bookmarks.TOUCH_ICON); + map.put(Bookmarks.IS_FOLDER, Bookmarks.IS_FOLDER); + map.put(Bookmarks.PARENT, Bookmarks.PARENT); + map.put(Bookmarks.POSITION, Bookmarks.POSITION); + map.put(Bookmarks.INSERT_AFTER, Bookmarks.INSERT_AFTER); + map.put(Bookmarks.IS_DELETED, Bookmarks.IS_DELETED); + map.put(Bookmarks.ACCOUNT_NAME, Bookmarks.ACCOUNT_NAME); + map.put(Bookmarks.ACCOUNT_TYPE, Bookmarks.ACCOUNT_TYPE); + map.put(Bookmarks.SOURCE_ID, Bookmarks.SOURCE_ID); + map.put(Bookmarks.VERSION, Bookmarks.VERSION); + map.put(Bookmarks.DATE_CREATED, Bookmarks.DATE_CREATED); + map.put(Bookmarks.DATE_MODIFIED, Bookmarks.DATE_MODIFIED); + map.put(Bookmarks.DIRTY, Bookmarks.DIRTY); + map.put(Bookmarks.SYNC1, Bookmarks.SYNC1); + map.put(Bookmarks.SYNC2, Bookmarks.SYNC2); + map.put(Bookmarks.SYNC3, Bookmarks.SYNC3); + map.put(Bookmarks.SYNC4, Bookmarks.SYNC4); + map.put(Bookmarks.SYNC5, Bookmarks.SYNC5); + map.put(Bookmarks.PARENT_SOURCE_ID, "(SELECT " + Bookmarks.SOURCE_ID + + " FROM " + TABLE_BOOKMARKS + " A WHERE " + + "A." + Bookmarks._ID + "=" + TABLE_BOOKMARKS + "." + Bookmarks.PARENT + + ") AS " + Bookmarks.PARENT_SOURCE_ID); + map.put(Bookmarks.INSERT_AFTER_SOURCE_ID, "(SELECT " + Bookmarks.SOURCE_ID + + " FROM " + TABLE_BOOKMARKS + " A WHERE " + + "A." + Bookmarks._ID + "=" + TABLE_BOOKMARKS + "." + Bookmarks.INSERT_AFTER + + ") AS " + Bookmarks.INSERT_AFTER_SOURCE_ID); + + // Other bookmarks + OTHER_BOOKMARKS_PROJECTION_MAP.putAll(BOOKMARKS_PROJECTION_MAP); + OTHER_BOOKMARKS_PROJECTION_MAP.put(Bookmarks.POSITION, + Long.toString(Long.MAX_VALUE) + " AS " + Bookmarks.POSITION); + + // History + map = HISTORY_PROJECTION_MAP; + map.put(History._ID, qualifyColumn(TABLE_HISTORY, History._ID)); + map.put(History.TITLE, History.TITLE); + map.put(History.URL, History.URL); + map.put(History.FAVICON, History.FAVICON); + map.put(History.THUMBNAIL, History.THUMBNAIL); + map.put(History.TOUCH_ICON, History.TOUCH_ICON); + map.put(History.DATE_CREATED, History.DATE_CREATED); + map.put(History.DATE_LAST_VISITED, History.DATE_LAST_VISITED); + map.put(History.VISITS, History.VISITS); + map.put(History.USER_ENTERED, History.USER_ENTERED); + + // Sync state + map = SYNC_STATE_PROJECTION_MAP; + map.put(SyncState._ID, SyncState._ID); + map.put(SyncState.ACCOUNT_NAME, SyncState.ACCOUNT_NAME); + map.put(SyncState.ACCOUNT_TYPE, SyncState.ACCOUNT_TYPE); + map.put(SyncState.DATA, SyncState.DATA); + + // Images + map = IMAGES_PROJECTION_MAP; + map.put(Images.URL, Images.URL); + map.put(Images.FAVICON, Images.FAVICON); + map.put(Images.THUMBNAIL, Images.THUMBNAIL); + map.put(Images.TOUCH_ICON, Images.TOUCH_ICON); + + // Combined history half + map = COMBINED_PROJECTION_MAP; + map.put(Combined._ID, Combined._ID); + map.put(Combined.TITLE, Combined.TITLE); + map.put(Combined.URL, Combined.URL); + map.put(Combined.DATE_CREATED, Combined.DATE_CREATED); + map.put(Combined.DATE_LAST_VISITED, Combined.DATE_LAST_VISITED); + map.put(Combined.IS_BOOKMARK, Combined.IS_BOOKMARK); + map.put(Combined.VISITS, Combined.VISITS); + map.put(Combined.FAVICON, Combined.FAVICON); + map.put(Combined.THUMBNAIL, Combined.THUMBNAIL); + map.put(Combined.TOUCH_ICON, Combined.TOUCH_ICON); + map.put(Combined.USER_ENTERED, Combined.USER_ENTERED); + } + + static final String bookmarkOrHistoryColumn(String column) { + return "CASE WHEN bookmarks." + column + " IS NOT NULL THEN " + + "bookmarks." + column + " ELSE history." + column + " END AS " + column; + } + + static final String qualifyColumn(String table, String column) { + return table + "." + column + " AS " + column; + } + + DatabaseHelper mOpenHelper; + SyncStateContentProviderHelper mSyncHelper = new SyncStateContentProviderHelper(); + + final class DatabaseHelper extends SQLiteOpenHelper { + static final String DATABASE_NAME = "browser2.db"; + static final int DATABASE_VERSION = 22; + public DatabaseHelper(Context context) { + super(context, DATABASE_NAME, null, DATABASE_VERSION); + } + + @Override + public void onCreate(SQLiteDatabase db) { + db.execSQL("CREATE TABLE " + TABLE_BOOKMARKS + "(" + + Bookmarks._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + + Bookmarks.TITLE + " TEXT," + + Bookmarks.URL + " TEXT," + + Bookmarks.IS_FOLDER + " INTEGER NOT NULL DEFAULT 0," + + Bookmarks.PARENT + " INTEGER," + + Bookmarks.POSITION + " INTEGER NOT NULL," + + Bookmarks.INSERT_AFTER + " INTEGER," + + Bookmarks.IS_DELETED + " INTEGER NOT NULL DEFAULT 0," + + Bookmarks.ACCOUNT_NAME + " TEXT," + + Bookmarks.ACCOUNT_TYPE + " TEXT," + + Bookmarks.SOURCE_ID + " TEXT," + + Bookmarks.VERSION + " INTEGER NOT NULL DEFAULT 1," + + Bookmarks.DATE_CREATED + " INTEGER," + + Bookmarks.DATE_MODIFIED + " INTEGER," + + Bookmarks.DIRTY + " INTEGER NOT NULL DEFAULT 0," + + Bookmarks.SYNC1 + " TEXT," + + Bookmarks.SYNC2 + " TEXT," + + Bookmarks.SYNC3 + " TEXT," + + Bookmarks.SYNC4 + " TEXT," + + Bookmarks.SYNC5 + " TEXT" + + ");"); + + // TODO indices + + createDefaultBookmarks(db); + + db.execSQL("CREATE TABLE " + TABLE_HISTORY + "(" + + History._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + + History.TITLE + " TEXT," + + History.URL + " TEXT NOT NULL," + + History.DATE_CREATED + " INTEGER," + + History.DATE_LAST_VISITED + " INTEGER," + + History.VISITS + " INTEGER NOT NULL DEFAULT 0," + + History.USER_ENTERED + " INTEGER" + + ");"); + + db.execSQL("CREATE TABLE " + TABLE_IMAGES + " (" + + Images.URL + " TEXT UNIQUE NOT NULL," + + Images.FAVICON + " BLOB," + + Images.THUMBNAIL + " BLOB," + + Images.TOUCH_ICON + " BLOB" + + ");"); + db.execSQL("CREATE INDEX imagesUrlIndex ON " + TABLE_IMAGES + + "(" + Images.URL + ")"); + + db.execSQL("CREATE TABLE " + TABLE_SEARCHES + " (" + + Searches._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + + Searches.SEARCH + " TEXT," + + Searches.DATE + " LONG" + + ");"); + + db.execSQL("CREATE VIEW " + VIEW_COMBINED + " AS " + + "SELECT " + + bookmarkOrHistoryColumn(Combined._ID) + ", " + + bookmarkOrHistoryColumn(Combined.TITLE) + ", " + + qualifyColumn(TABLE_HISTORY, Combined.URL) + ", " + + qualifyColumn(TABLE_HISTORY, Combined.DATE_CREATED) + ", " + + Combined.DATE_LAST_VISITED + ", " + + "CASE WHEN bookmarks._id IS NOT NULL THEN 1 ELSE 0 END AS " + Combined.IS_BOOKMARK + ", " + + Combined.VISITS + ", " + + Combined.FAVICON + ", " + + Combined.THUMBNAIL + ", " + + Combined.TOUCH_ICON + ", " + + "NULL AS " + Combined.USER_ENTERED + " "+ + "FROM history LEFT OUTER JOIN bookmarks ON history.url = bookmarks.url LEFT OUTER JOIN images ON history.url = images.url_key " + + + "UNION ALL " + + + "SELECT " + + Combined._ID + ", " + + Combined.TITLE + ", " + + Combined.URL + ", " + + Combined.DATE_CREATED + ", " + + "NULL AS " + Combined.DATE_LAST_VISITED + ", "+ + "1 AS " + Combined.IS_BOOKMARK + ", " + + "0 AS " + Combined.VISITS + ", "+ + Combined.FAVICON + ", " + + Combined.THUMBNAIL + ", " + + Combined.TOUCH_ICON + ", " + + "NULL AS " + Combined.USER_ENTERED + " "+ + "FROM bookmarks LEFT OUTER JOIN images ON bookmarks.url = images.url_key WHERE url NOT IN (SELECT url FROM history)"); + + mSyncHelper.createDatabase(db); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + // TODO write upgrade logic + db.execSQL("DROP TABLE IF EXISTS " + TABLE_BOOKMARKS); + db.execSQL("DROP TABLE IF EXISTS " + TABLE_HISTORY); + db.execSQL("DROP TABLE IF EXISTS " + TABLE_SEARCHES); + db.execSQL("DROP TABLE IF EXISTS " + TABLE_IMAGES); + db.execSQL("DROP VIEW IF EXISTS " + VIEW_COMBINED); + onCreate(db); + } + + @Override + public void onOpen(SQLiteDatabase db) { + mSyncHelper.onDatabaseOpened(db); + } + + + private void createDefaultBookmarks(SQLiteDatabase db) { + ContentValues values = new ContentValues(); + // TODO figure out how to deal with localization for the defaults + + // Chrome sync root folder + values.put(Bookmarks._ID, FIXED_ID_CHROME_ROOT); + values.put(ChromeSyncColumns.SERVER_UNIQUE, ChromeSyncColumns.FOLDER_NAME_ROOT); + values.put(Bookmarks.TITLE, "Google Chrome"); + values.put(Bookmarks.PARENT, 0); + values.put(Bookmarks.POSITION, 0); + values.put(Bookmarks.IS_FOLDER, true); + values.put(Bookmarks.DIRTY, true); + db.insertOrThrow(TABLE_BOOKMARKS, null, values); + + // Bookmarks folder + values.put(Bookmarks._ID, FIXED_ID_BOOKMARKS); + values.put(ChromeSyncColumns.SERVER_UNIQUE, ChromeSyncColumns.FOLDER_NAME_BOOKMARKS); + values.put(Bookmarks.TITLE, "Bookmarks"); + values.put(Bookmarks.PARENT, FIXED_ID_CHROME_ROOT); + values.put(Bookmarks.POSITION, 0); + values.put(Bookmarks.IS_FOLDER, true); + values.put(Bookmarks.DIRTY, true); + db.insertOrThrow(TABLE_BOOKMARKS, null, values); + + // Bookmarks Bar folder + values.clear(); + values.put(Bookmarks._ID, FIXED_ID_BOOKMARKS_BAR); + values.put(ChromeSyncColumns.SERVER_UNIQUE, + ChromeSyncColumns.FOLDER_NAME_BOOKMARKS_BAR); + values.put(Bookmarks.TITLE, "Bookmarks Bar"); + values.put(Bookmarks.PARENT, FIXED_ID_BOOKMARKS); + values.put(Bookmarks.POSITION, 0); + values.put(Bookmarks.IS_FOLDER, true); + values.put(Bookmarks.DIRTY, true); + db.insertOrThrow(TABLE_BOOKMARKS, null, values); + + // Other Bookmarks folder + values.clear(); + values.put(Bookmarks._ID, FIXED_ID_OTHER_BOOKMARKS); + values.put(ChromeSyncColumns.SERVER_UNIQUE, + ChromeSyncColumns.FOLDER_NAME_OTHER_BOOKMARKS); + values.put(Bookmarks.TITLE, "Other Bookmarks"); + values.put(Bookmarks.PARENT, FIXED_ID_BOOKMARKS); + values.put(Bookmarks.POSITION, 1000); + values.put(Bookmarks.IS_FOLDER, true); + values.put(Bookmarks.DIRTY, true); + db.insertOrThrow(TABLE_BOOKMARKS, null, values); + + addDefaultBookmarks(db, FIXED_ID_BOOKMARKS_BAR); + + // TODO remove this testing code + db.execSQL("INSERT INTO bookmarks (" + + Bookmarks.TITLE + ", " + + Bookmarks.URL + ", " + + Bookmarks.IS_FOLDER + "," + + Bookmarks.PARENT + "," + + Bookmarks.POSITION + + ") VALUES (" + + "'Google Reader', " + + "'http://reader.google.com', " + + "0," + + Long.toString(FIXED_ID_OTHER_BOOKMARKS) + "," + + 0 + + ");"); + } + + private void addDefaultBookmarks(SQLiteDatabase db, long parentId) { + final CharSequence[] bookmarks = getContext().getResources().getTextArray( + R.array.bookmarks); + int size = bookmarks.length; + try { + String parent = Long.toString(parentId); + String now = Long.toString(System.currentTimeMillis()); + for (int i = 0; i < size; i = i + 2) { + CharSequence bookmarkDestination = replaceSystemPropertyInString(getContext(), + bookmarks[i + 1]); + db.execSQL("INSERT INTO bookmarks (" + + Bookmarks.TITLE + ", " + + Bookmarks.URL + ", " + + Bookmarks.IS_FOLDER + "," + + Bookmarks.PARENT + "," + + Bookmarks.POSITION + "," + + Bookmarks.DATE_CREATED + + ") VALUES (" + + "'" + bookmarks[i] + "', " + + "'" + bookmarkDestination + "', " + + "0," + + parent + "," + + Integer.toString(i) + "," + + now + + ");"); + } + } catch (ArrayIndexOutOfBoundsException e) { + } + } + + // XXX: This is a major hack to remove our dependency on gsf constants and + // its content provider. http://b/issue?id=2425179 + private String getClientId(ContentResolver cr) { + String ret = "android-google"; + Cursor c = null; + try { + c = cr.query(Uri.parse("content://com.google.settings/partner"), + new String[] { "value" }, "name='client_id'", null, null); + if (c != null && c.moveToNext()) { + ret = c.getString(0); + } + } catch (RuntimeException ex) { + // fall through to return the default + } finally { + if (c != null) { + c.close(); + } + } + return ret; + } + + private CharSequence replaceSystemPropertyInString(Context context, CharSequence srcString) { + StringBuffer sb = new StringBuffer(); + int lastCharLoc = 0; + + final String client_id = getClientId(context.getContentResolver()); + + for (int i = 0; i < srcString.length(); ++i) { + char c = srcString.charAt(i); + if (c == '{') { + sb.append(srcString.subSequence(lastCharLoc, i)); + lastCharLoc = i; + inner: + for (int j = i; j < srcString.length(); ++j) { + char k = srcString.charAt(j); + if (k == '}') { + String propertyKeyValue = srcString.subSequence(i + 1, j).toString(); + if (propertyKeyValue.equals("CLIENT_ID")) { + sb.append(client_id); + } else { + sb.append("unknown"); + } + lastCharLoc = j + 1; + i = j; + break inner; + } + } + } + } + if (srcString.length() - lastCharLoc > 0) { + // Put on the tail, if there is one + sb.append(srcString.subSequence(lastCharLoc, srcString.length())); + } + return sb; + } + } + + @Override + public SQLiteOpenHelper getDatabaseHelper(Context context) { + synchronized (this) { + if (mOpenHelper == null) { + mOpenHelper = new DatabaseHelper(context); + } + return mOpenHelper; + } + } + + @Override + public boolean isCallerSyncAdapter(Uri uri) { + return uri.getBooleanQueryParameter(BrowserContract.CALLER_IS_SYNCADAPTER, false); + } + + @Override + public void notifyChange(boolean callerIsSyncAdapter) { + ContentResolver resolver = getContext().getContentResolver(); + resolver.notifyChange(BrowserContract.AUTHORITY_URI, null, !callerIsSyncAdapter); + resolver.notifyChange(LEGACY_AUTHORITY_URI, null, !callerIsSyncAdapter); + } + + @Override + public String getType(Uri uri) { + final int match = URI_MATCHER.match(uri); + switch (match) { + case BOOKMARKS: + return Bookmarks.CONTENT_TYPE; + case BOOKMARKS_ID: + return Bookmarks.CONTENT_ITEM_TYPE; + case HISTORY: + return History.CONTENT_TYPE; + case HISTORY_ID: + return History.CONTENT_ITEM_TYPE; + case SEARCHES: + return Searches.CONTENT_TYPE; + case SEARCHES_ID: + return Searches.CONTENT_ITEM_TYPE; +// case SUGGEST: +// return SearchManager.SUGGEST_MIME_TYPE; + } + return null; + } + + @Override + public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, + String sortOrder) { + SQLiteDatabase db = mOpenHelper.getReadableDatabase(); + final int match = URI_MATCHER.match(uri); + SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); + String limit = uri.getQueryParameter(BrowserContract.PARAM_LIMIT); + switch (match) { + case ACCOUNTS: { + qb.setTables(TABLE_BOOKMARKS); + qb.setProjectionMap(ACCOUNTS_PROJECTION_MAP); + qb.setDistinct(true); + qb.appendWhere(Bookmarks.ACCOUNT_NAME + " IS NOT NULL"); + break; + } + + case BOOKMARKS_FOLDER_ID: + case BOOKMARKS_ID: + case BOOKMARKS: { + // Only show deleted bookmarks if requested to do so + if (!uri.getBooleanQueryParameter(Bookmarks.QUERY_PARAMETER_SHOW_DELETED, false)) { + selection = DatabaseUtils.concatenateWhere( + Bookmarks.IS_DELETED + "=0", selection); + } + + if (match == BOOKMARKS_ID) { + // Tack on the ID of the specific bookmark requested + selection = DatabaseUtils.concatenateWhere(selection, + TABLE_BOOKMARKS + "." + Bookmarks._ID + "=?"); + selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, + new String[] { Long.toString(ContentUris.parseId(uri)) }); + } else if (match == BOOKMARKS_FOLDER_ID) { + // Tack on the ID of the specific folder requested + selection = DatabaseUtils.concatenateWhere(selection, + TABLE_BOOKMARKS + "." + Bookmarks.PARENT + "=?"); + selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, + new String[] { Long.toString(ContentUris.parseId(uri)) }); + } + + // Look for account info + String accountType = uri.getQueryParameter(Bookmarks.PARAM_ACCOUNT_TYPE); + String accountName = uri.getQueryParameter(Bookmarks.PARAM_ACCOUNT_NAME); + if (!TextUtils.isEmpty(accountType) && !TextUtils.isEmpty(accountName)) { + selection = DatabaseUtils.concatenateWhere(selection, + Bookmarks.ACCOUNT_TYPE + "=? AND " + Bookmarks.ACCOUNT_NAME + "=? "); + selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, + new String[] { accountType, accountName }); + } + + // Set a default sort order if one isn't specified + if (TextUtils.isEmpty(sortOrder)) { + sortOrder = DEFAULT_BOOKMARKS_SORT_ORDER; + } + + qb.setProjectionMap(BOOKMARKS_PROJECTION_MAP); + qb.setTables(TABLE_BOOKMARKS_JOIN_IMAGES); + break; + } + + case BOOKMARKS_FOLDER: { + // Don't allow selections to be applied to the default folder + if (!TextUtils.isEmpty(selection) || selectionArgs != null) { + throw new UnsupportedOperationException( + "selections aren't supported on this URI"); + } + + // Look for an account + boolean useAccount = false; + String accountType = uri.getQueryParameter(Bookmarks.PARAM_ACCOUNT_TYPE); + String accountName = uri.getQueryParameter(Bookmarks.PARAM_ACCOUNT_NAME); + if (!TextUtils.isEmpty(accountType) && !TextUtils.isEmpty(accountName)) { + useAccount = true; + } + + qb.setTables(TABLE_BOOKMARKS_JOIN_IMAGES); + String bookmarksBarQuery; + String otherBookmarksQuery; + String[] args; + if (!useAccount) { + qb.setProjectionMap(BOOKMARKS_PROJECTION_MAP); + bookmarksBarQuery = qb.buildQuery(projection, + Bookmarks.PARENT + "=? AND " + Bookmarks.IS_DELETED + "=0", + null, null, null, null, null); + + qb.setProjectionMap(OTHER_BOOKMARKS_PROJECTION_MAP); + otherBookmarksQuery = qb.buildQuery(projection, + Bookmarks._ID + "=?", + null, null, null, null, null); + + args = new String[] { Long.toString(FIXED_ID_BOOKMARKS_BAR), + Long.toString(FIXED_ID_OTHER_BOOKMARKS) }; + } else { + qb.setProjectionMap(BOOKMARKS_PROJECTION_MAP); + bookmarksBarQuery = qb.buildQuery(projection, + Bookmarks.ACCOUNT_TYPE + "=? AND " + Bookmarks.ACCOUNT_NAME + "=? " + + "AND parent = " + + "(SELECT _id FROM " + TABLE_BOOKMARKS + " WHERE " + + ChromeSyncColumns.SERVER_UNIQUE + "=" + + "'" + ChromeSyncColumns.FOLDER_NAME_BOOKMARKS_BAR + "' " + + "AND account_type = ? AND account_name = ?) " + + "AND " + Bookmarks.IS_DELETED + "=0", + null, null, null, null, null); + + qb.setProjectionMap(OTHER_BOOKMARKS_PROJECTION_MAP); + otherBookmarksQuery = qb.buildQuery(projection, + Bookmarks.ACCOUNT_TYPE + "=? AND " + Bookmarks.ACCOUNT_NAME + "=?" + + " AND " + ChromeSyncColumns.SERVER_UNIQUE + "=?", + null, null, null, null, null); + + args = new String[] { + accountType, accountName, accountType, accountName, + accountType, accountName, ChromeSyncColumns.FOLDER_NAME_OTHER_BOOKMARKS, + }; + } + + String query = qb.buildUnionQuery( + new String[] { bookmarksBarQuery, otherBookmarksQuery }, + DEFAULT_BOOKMARKS_SORT_ORDER, limit); + return db.rawQuery(query, args); + } + + case HISTORY_ID: { + selection = DatabaseUtils.concatenateWhere(selection, TABLE_HISTORY + "._id=?"); + selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, + new String[] { Long.toString(ContentUris.parseId(uri)) }); + // fall through + } + case HISTORY: { + if (sortOrder == null) { + sortOrder = DEFAULT_SORT_HISTORY; + } + qb.setProjectionMap(HISTORY_PROJECTION_MAP); + qb.setTables(TABLE_HISTORY_JOIN_IMAGES); + break; + } + + case SYNCSTATE: { + return mSyncHelper.query(db, projection, selection, selectionArgs, sortOrder); + } + + case SYNCSTATE_ID: { + selection = appendAccountToSelection(uri, selection); + String selectionWithId = + (SyncStateContract.Columns._ID + "=" + ContentUris.parseId(uri) + " ") + + (selection == null ? "" : " AND (" + selection + ")"); + return mSyncHelper.query(db, projection, selectionWithId, selectionArgs, sortOrder); + } + + case IMAGES: { + qb.setTables(TABLE_IMAGES); + qb.setProjectionMap(IMAGES_PROJECTION_MAP); + break; + } + + case COMBINED_ID: { + selection = DatabaseUtils.concatenateWhere(selection, VIEW_COMBINED + "._id=?"); + selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, + new String[] { Long.toString(ContentUris.parseId(uri)) }); + // fall through + } + case COMBINED: { + qb.setTables(VIEW_COMBINED); + qb.setProjectionMap(COMBINED_PROJECTION_MAP); + break; + } + + default: { + throw new UnsupportedOperationException("Unknown URL " + uri.toString()); + } + } + + Cursor cursor = qb.query(db, projection, selection, selectionArgs, null, null, sortOrder, + limit); + cursor.setNotificationUri(getContext().getContentResolver(), BrowserContract.AUTHORITY_URI); + return cursor; + } + + @Override + public int deleteInTransaction(Uri uri, String selection, String[] selectionArgs, + boolean callerIsSyncAdapter) { + final int match = URI_MATCHER.match(uri); + final SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + switch (match) { + case BOOKMARKS_ID: + case BOOKMARKS: { + //TODO cascade deletes down from folders + if (!callerIsSyncAdapter) { + // If the caller isn't a sync adapter just go through and update all the + // bookmarks to have the deleted flag set. + ContentValues values = new ContentValues(); + values.put(Bookmarks.DATE_MODIFIED, System.currentTimeMillis()); + values.put(Bookmarks.IS_DELETED, 1); + return updateInTransaction(uri, values, selection, selectionArgs, + callerIsSyncAdapter); + } else { + // Sync adapters are allowed to actually delete things + if (match == BOOKMARKS_ID) { + selection = DatabaseUtils.concatenateWhere(selection, + TABLE_BOOKMARKS + "._id=?"); + selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, + new String[] { Long.toString(ContentUris.parseId(uri)) }); + } + return db.delete(TABLE_BOOKMARKS, selection, selectionArgs); + } + } + + case HISTORY_ID: { + selection = DatabaseUtils.concatenateWhere(selection, TABLE_HISTORY + "._id=?"); + selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, + new String[] { Long.toString(ContentUris.parseId(uri)) }); + // fall through + } + case HISTORY: { + return db.delete(TABLE_HISTORY, selection, selectionArgs); + } + + case SEARCHES_ID: { + selection = DatabaseUtils.concatenateWhere(selection, TABLE_SEARCHES + "._id=?"); + selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, + new String[] { Long.toString(ContentUris.parseId(uri)) }); + // fall through + } + case SEARCHES: { + return db.delete(TABLE_SEARCHES, selection, selectionArgs); + } + + case SYNCSTATE: { + return mSyncHelper.delete(db, selection, selectionArgs); + } + case SYNCSTATE_ID: { + String selectionWithId = + (SyncStateContract.Columns._ID + "=" + ContentUris.parseId(uri) + " ") + + (selection == null ? "" : " AND (" + selection + ")"); + return mSyncHelper.delete(db, selectionWithId, selectionArgs); + } + } + throw new UnsupportedOperationException("Unknown update URI " + uri); + } + + @Override + public Uri insertInTransaction(Uri uri, ContentValues values, boolean callerIsSyncAdapter) { + final int match = URI_MATCHER.match(uri); + final SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + long id = -1; + switch (match) { + case BOOKMARKS: { + // Mark rows dirty if they're not coming from a sync adapater + if (!callerIsSyncAdapter) { + long now = System.currentTimeMillis(); + values.put(Bookmarks.DATE_CREATED, now); + values.put(Bookmarks.DATE_MODIFIED, now); + values.put(Bookmarks.DIRTY, 1); + + // If no parent is set default to the "Bookmarks Bar" folder + if (!values.containsKey(Bookmarks.PARENT)) { + values.put(Bookmarks.PARENT, FIXED_ID_BOOKMARKS_BAR); + } + } + + // If no position is requested put the bookmark at the beginning of the list + if (!values.containsKey(Bookmarks.POSITION)) { + values.put(Bookmarks.POSITION, Long.toString(Long.MIN_VALUE)); + } + + // Extract out the image values so they can be inserted into the images table + String url = values.getAsString(Bookmarks.URL); + ContentValues imageValues = extractImageValues(values, url); + Boolean isFolder = values.getAsBoolean(Bookmarks.IS_FOLDER); + if ((isFolder == null || !isFolder) + && imageValues != null && !TextUtils.isEmpty(url)) { + int count = db.update(TABLE_IMAGES, imageValues, Images.URL + "=?", + new String[] { url }); + if (count == 0) { + db.insertOrThrow(TABLE_IMAGES, Images.FAVICON, imageValues); + } + } + + id = db.insertOrThrow(TABLE_BOOKMARKS, Bookmarks.DIRTY, values); + break; + } + + case HISTORY: { + // If no created time is specified set it to now + if (!values.containsKey(History.DATE_CREATED)) { + values.put(History.DATE_CREATED, System.currentTimeMillis()); + } + + // Extract out the image values so they can be inserted into the images table + ContentValues imageValues = extractImageValues(values, + values.getAsString(History.URL)); + if (imageValues != null) { + db.insertOrThrow(TABLE_IMAGES, Images.FAVICON, imageValues); + } + + id = db.insertOrThrow(TABLE_HISTORY, History.VISITS, values); + break; + } + + case SEARCHES: { + id = insertSearchesInTransaction(db, values); + break; + } + + case SYNCSTATE: { + id = mSyncHelper.insert(db, values); + break; + } + + default: { + throw new UnsupportedOperationException("Unknown insert URI " + uri); + } + } + + if (id >= 0) { + return ContentUris.withAppendedId(uri, id); + } else { + return null; + } + } + + /** + * Searches are unique, so perform an UPSERT manually since SQLite doesn't support them. + */ + private long insertSearchesInTransaction(SQLiteDatabase db, ContentValues values) { + String search = values.getAsString(Searches.SEARCH); + if (TextUtils.isEmpty(search)) { + throw new IllegalArgumentException("Must include the SEARCH field"); + } + Cursor cursor = null; + try { + cursor = db.query(TABLE_SEARCHES, new String[] { Searches._ID }, + Searches.SEARCH + "=?", new String[] { search }, null, null, null); + if (cursor.moveToNext()) { + long id = cursor.getLong(0); + db.update(TABLE_SEARCHES, values, Searches._ID + "=?", + new String[] { Long.toString(id) }); + return id; + } else { + return db.insertOrThrow(TABLE_SEARCHES, Searches.SEARCH, values); + } + } finally { + if (cursor != null) cursor.close(); + } + } + + @Override + public int updateInTransaction(Uri uri, ContentValues values, String selection, + String[] selectionArgs, boolean callerIsSyncAdapter) { + final int match = URI_MATCHER.match(uri); + final SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + switch (match) { + case BOOKMARKS_ID: { + selection = DatabaseUtils.concatenateWhere(selection, + TABLE_BOOKMARKS + "._id=?"); + selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, + new String[] { Long.toString(ContentUris.parseId(uri)) }); + // fall through + } + case BOOKMARKS: { + return updateBookmarksInTransaction(values, selection, selectionArgs, + callerIsSyncAdapter); + } + + case HISTORY_ID: { + selection = DatabaseUtils.concatenateWhere(selection, TABLE_HISTORY + "._id=?"); + selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, + new String[] { Long.toString(ContentUris.parseId(uri)) }); + // fall through + } + case HISTORY: { + return updateHistoryInTransaction(values, selection, selectionArgs); + } + + case SYNCSTATE: { + return mSyncHelper.update(mDb, values, + appendAccountToSelection(uri, selection), selectionArgs); + } + + case SYNCSTATE_ID: { + selection = appendAccountToSelection(uri, selection); + String selectionWithId = + (SyncStateContract.Columns._ID + "=" + ContentUris.parseId(uri) + " ") + + (selection == null ? "" : " AND (" + selection + ")"); + return mSyncHelper.update(mDb, values, + selectionWithId, selectionArgs); + } + + case IMAGES: { + String url = values.getAsString(Images.URL); + if (TextUtils.isEmpty(url)) { + throw new IllegalArgumentException("Images.URL is required"); + } + int count = db.update(TABLE_IMAGES, values, Images.URL + "=?", + new String[] { url }); + if (count == 0) { + db.insertOrThrow(TABLE_IMAGES, Images.FAVICON, values); + count = 1; + } + return count; + } + } + throw new UnsupportedOperationException("Unknown update URI " + uri); + } + + /** + * Does a query to find the matching bookmarks and updates each one with the provided values. + */ + int updateBookmarksInTransaction(ContentValues values, String selection, + String[] selectionArgs, boolean callerIsSyncAdapter) { + int count = 0; + final SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + Cursor cursor = query(Bookmarks.CONTENT_URI, + new String[] { Bookmarks._ID, Bookmarks.VERSION, Bookmarks.URL }, + selection, selectionArgs, null); + try { + String[] args = new String[1]; + // Mark the bookmark dirty if the caller isn't a sync adapter + if (!callerIsSyncAdapter) { + values.put(Bookmarks.DATE_MODIFIED, System.currentTimeMillis()); + values.put(Bookmarks.DIRTY, 1); + } + + boolean updatingUrl = values.containsKey(Bookmarks.URL); + String url = null; + if (updatingUrl) { + url = values.getAsString(Bookmarks.URL); + } + ContentValues imageValues = extractImageValues(values, url); + + while (cursor.moveToNext()) { + args[0] = cursor.getString(0); + if (!callerIsSyncAdapter) { + // increase the local version for non-sync changes + values.put(Bookmarks.VERSION, cursor.getLong(1) + 1); + } + count += db.update(TABLE_BOOKMARKS, values, "_id=?", args); + + // Update the images over in their table + if (imageValues != null) { + if (!updatingUrl) { + url = cursor.getString(2); + imageValues.put(Images.URL, url); + } + + if (!TextUtils.isEmpty(url)) { + args[0] = url; + if (db.update(TABLE_IMAGES, imageValues, Images.URL + "=?", args) == 0) { + db.insert(TABLE_IMAGES, Images.FAVICON, imageValues); + } + } + } + } + } finally { + if (cursor != null) cursor.close(); + } + return count; + } + + /** + * Does a query to find the matching bookmarks and updates each one with the provided values. + */ + int updateHistoryInTransaction(ContentValues values, String selection, String[] selectionArgs) { + int count = 0; + final SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + Cursor cursor = query(History.CONTENT_URI, + new String[] { History._ID, History.URL }, + selection, selectionArgs, null); + try { + String[] args = new String[1]; + + boolean updatingUrl = values.containsKey(History.URL); + String url = null; + if (updatingUrl) { + url = values.getAsString(History.URL); + } + ContentValues imageValues = extractImageValues(values, url); + + while (cursor.moveToNext()) { + args[0] = cursor.getString(0); + count += db.update(TABLE_HISTORY, values, "_id=?", args); + + // Update the images over in their table + if (imageValues != null) { + if (!updatingUrl) { + url = cursor.getString(1); + imageValues.put(Images.URL, url); + } + args[0] = url; + if (db.update(TABLE_IMAGES, imageValues, Images.URL + "=?", args) == 0) { + db.insert(TABLE_IMAGES, Images.FAVICON, imageValues); + } + } + } + } finally { + if (cursor != null) cursor.close(); + } + return count; + } + + String appendAccountToSelection(Uri uri, String selection) { + final String accountName = uri.getQueryParameter(RawContacts.ACCOUNT_NAME); + final String accountType = uri.getQueryParameter(RawContacts.ACCOUNT_TYPE); + + final boolean partialUri = TextUtils.isEmpty(accountName) ^ TextUtils.isEmpty(accountType); + if (partialUri) { + // Throw when either account is incomplete + throw new IllegalArgumentException( + "Must specify both or neither of ACCOUNT_NAME and ACCOUNT_TYPE for " + uri); + } + + // Accounts are valid by only checking one parameter, since we've + // already ruled out partial accounts. + final boolean validAccount = !TextUtils.isEmpty(accountName); + if (validAccount) { + StringBuilder selectionSb = new StringBuilder(RawContacts.ACCOUNT_NAME + "=" + + DatabaseUtils.sqlEscapeString(accountName) + " AND " + + RawContacts.ACCOUNT_TYPE + "=" + + DatabaseUtils.sqlEscapeString(accountType)); + if (!TextUtils.isEmpty(selection)) { + selectionSb.append(" AND ("); + selectionSb.append(selection); + selectionSb.append(')'); + } + return selectionSb.toString(); + } else { + return selection; + } + } + + ContentValues extractImageValues(ContentValues values, String url) { + ContentValues imageValues = null; + // favicon + if (values.containsKey(Bookmarks.FAVICON)) { + imageValues = new ContentValues(); + imageValues.put(Images.FAVICON, values.getAsByteArray(Bookmarks.FAVICON)); + values.remove(Bookmarks.FAVICON); + } + + // thumbnail + if (values.containsKey(Bookmarks.THUMBNAIL)) { + if (imageValues == null) { + imageValues = new ContentValues(); + } + imageValues.put(Images.THUMBNAIL, values.getAsByteArray(Bookmarks.THUMBNAIL)); + values.remove(Bookmarks.THUMBNAIL); + } + + // touch icon + if (values.containsKey(Bookmarks.TOUCH_ICON)) { + if (imageValues == null) { + imageValues = new ContentValues(); + } + imageValues.put(Images.TOUCH_ICON, values.getAsByteArray(Bookmarks.TOUCH_ICON)); + values.remove(Bookmarks.TOUCH_ICON); + } + + if (imageValues != null) { + imageValues.put(Images.URL, url); + } + return imageValues; + } +} diff --git a/src/com/android/browser/provider/SQLiteContentProvider.java b/src/com/android/browser/provider/SQLiteContentProvider.java new file mode 100644 index 000000000..a50894a41 --- /dev/null +++ b/src/com/android/browser/provider/SQLiteContentProvider.java @@ -0,0 +1,276 @@ +/* + * 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.browser.provider; + +import android.content.ContentProvider; +import android.content.ContentProviderOperation; +import android.content.ContentProviderResult; +import android.content.ContentValues; +import android.content.Context; +import android.content.OperationApplicationException; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.database.sqlite.SQLiteTransactionListener; +import android.net.Uri; + +import java.util.ArrayList; + +/** + * General purpose {@link ContentProvider} base class that uses SQLiteDatabase for storage. + */ +public abstract class SQLiteContentProvider extends ContentProvider + implements SQLiteTransactionListener { + + private static final String TAG = "SQLiteContentProvider"; + + private SQLiteOpenHelper mOpenHelper; + private volatile boolean mNotifyChange; + protected SQLiteDatabase mDb; + + private final ThreadLocal<Boolean> mApplyingBatch = new ThreadLocal<Boolean>(); + private static final int SLEEP_AFTER_YIELD_DELAY = 4000; + + /** + * Maximum number of operations allowed in a batch between yield points. + */ + private static final int MAX_OPERATIONS_PER_YIELD_POINT = 500; + + @Override + public boolean onCreate() { + Context context = getContext(); + mOpenHelper = getDatabaseHelper(context); + return true; + } + + /** + * Returns a {@link SQLiteOpenHelper} that can open the database. + */ + public abstract SQLiteOpenHelper getDatabaseHelper(Context context); + + /** + * The equivalent of the {@link #insert} method, but invoked within a transaction. + */ + public abstract Uri insertInTransaction(Uri uri, ContentValues values, + boolean callerIsSyncAdapter); + + /** + * The equivalent of the {@link #update} method, but invoked within a transaction. + */ + public abstract int updateInTransaction(Uri uri, ContentValues values, String selection, + String[] selectionArgs, boolean callerIsSyncAdapter); + + /** + * The equivalent of the {@link #delete} method, but invoked within a transaction. + */ + public abstract int deleteInTransaction(Uri uri, String selection, String[] selectionArgs, + boolean callerIsSyncAdapter); + + /** + * Called when the provider needs to notify the system of a change. + * @param callerIsSyncAdapter true if the caller that caused the change was a sync adapter. + */ + public abstract void notifyChange(boolean callerIsSyncAdapter); + + public boolean isCallerSyncAdapter(Uri uri) { + return false; + } + + public SQLiteOpenHelper getDatabaseHelper() { + return mOpenHelper; + } + + private boolean applyingBatch() { + return mApplyingBatch.get() != null && mApplyingBatch.get(); + } + + @Override + public Uri insert(Uri uri, ContentValues values) { + Uri result = null; + boolean callerIsSyncAdapter = isCallerSyncAdapter(uri); + boolean applyingBatch = applyingBatch(); + if (!applyingBatch) { + mDb = mOpenHelper.getWritableDatabase(); + mDb.beginTransactionWithListener(this); + try { + result = insertInTransaction(uri, values, callerIsSyncAdapter); + if (result != null) { + mNotifyChange = true; + } + mDb.setTransactionSuccessful(); + } finally { + mDb.endTransaction(); + } + + onEndTransaction(callerIsSyncAdapter); + } else { + result = insertInTransaction(uri, values, callerIsSyncAdapter); + if (result != null) { + mNotifyChange = true; + } + } + return result; + } + + @Override + public int bulkInsert(Uri uri, ContentValues[] values) { + int numValues = values.length; + boolean callerIsSyncAdapter = isCallerSyncAdapter(uri); + mDb = mOpenHelper.getWritableDatabase(); + mDb.beginTransactionWithListener(this); + try { + for (int i = 0; i < numValues; i++) { + Uri result = insertInTransaction(uri, values[i], callerIsSyncAdapter); + if (result != null) { + mNotifyChange = true; + } + mDb.yieldIfContendedSafely(); + } + mDb.setTransactionSuccessful(); + } finally { + mDb.endTransaction(); + } + + onEndTransaction(callerIsSyncAdapter); + return numValues; + } + + @Override + public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + int count = 0; + boolean callerIsSyncAdapter = isCallerSyncAdapter(uri); + boolean applyingBatch = applyingBatch(); + if (!applyingBatch) { + mDb = mOpenHelper.getWritableDatabase(); + mDb.beginTransactionWithListener(this); + try { + count = updateInTransaction(uri, values, selection, selectionArgs, + callerIsSyncAdapter); + if (count > 0) { + mNotifyChange = true; + } + mDb.setTransactionSuccessful(); + } finally { + mDb.endTransaction(); + } + + onEndTransaction(callerIsSyncAdapter); + } else { + count = updateInTransaction(uri, values, selection, selectionArgs, callerIsSyncAdapter); + if (count > 0) { + mNotifyChange = true; + } + } + + return count; + } + + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + int count = 0; + boolean callerIsSyncAdapter = isCallerSyncAdapter(uri); + boolean applyingBatch = applyingBatch(); + if (!applyingBatch) { + mDb = mOpenHelper.getWritableDatabase(); + mDb.beginTransactionWithListener(this); + try { + count = deleteInTransaction(uri, selection, selectionArgs, callerIsSyncAdapter); + if (count > 0) { + mNotifyChange = true; + } + mDb.setTransactionSuccessful(); + } finally { + mDb.endTransaction(); + } + + onEndTransaction(callerIsSyncAdapter); + } else { + count = deleteInTransaction(uri, selection, selectionArgs, callerIsSyncAdapter); + if (count > 0) { + mNotifyChange = true; + } + } + return count; + } + + @Override + public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) + throws OperationApplicationException { + int ypCount = 0; + int opCount = 0; + boolean callerIsSyncAdapter = false; + mDb = mOpenHelper.getWritableDatabase(); + mDb.beginTransactionWithListener(this); + try { + mApplyingBatch.set(true); + final int numOperations = operations.size(); + final ContentProviderResult[] results = new ContentProviderResult[numOperations]; + for (int i = 0; i < numOperations; i++) { + if (++opCount >= MAX_OPERATIONS_PER_YIELD_POINT) { + throw new OperationApplicationException( + "Too many content provider operations between yield points. " + + "The maximum number of operations per yield point is " + + MAX_OPERATIONS_PER_YIELD_POINT, ypCount); + } + final ContentProviderOperation operation = operations.get(i); + if (!callerIsSyncAdapter && isCallerSyncAdapter(operation.getUri())) { + callerIsSyncAdapter = true; + } + if (i > 0 && operation.isYieldAllowed()) { + opCount = 0; + if (mDb.yieldIfContendedSafely(SLEEP_AFTER_YIELD_DELAY)) { + ypCount++; + } + } + results[i] = operation.apply(this, results, i); + } + mDb.setTransactionSuccessful(); + return results; + } finally { + mApplyingBatch.set(false); + mDb.endTransaction(); + onEndTransaction(callerIsSyncAdapter); + } + } + + @Override + public void onBegin() { + onBeginTransaction(); + } + + @Override + public void onCommit() { + beforeTransactionCommit(); + } + + @Override + public void onRollback() { + // not used + } + + protected void onBeginTransaction() { + } + + protected void beforeTransactionCommit() { + } + + protected void onEndTransaction(boolean callerIsSyncAdapter) { + if (mNotifyChange) { + mNotifyChange = false; + notifyChange(callerIsSyncAdapter); + } + } +} diff --git a/src/com/android/browser/search/.DefaultSearchEngine.java.swp b/src/com/android/browser/search/.DefaultSearchEngine.java.swp Binary files differnew file mode 100644 index 000000000..441153c4a --- /dev/null +++ b/src/com/android/browser/search/.DefaultSearchEngine.java.swp diff --git a/src/com/android/browser/widget/BookmarkStackWidgetProvider.java b/src/com/android/browser/widget/BookmarkStackWidgetProvider.java new file mode 100644 index 000000000..0684c616c --- /dev/null +++ b/src/com/android/browser/widget/BookmarkStackWidgetProvider.java @@ -0,0 +1,45 @@ +/* + * 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.widget; + +import android.appwidget.AppWidgetManager; +import android.appwidget.AppWidgetProvider; +import android.content.Context; +import android.content.Intent; + +/** + * Widget that shows a preview of the user's bookmarks. + */ +public class BookmarkStackWidgetProvider extends AppWidgetProvider { + + @Override + public void onUpdate(Context context, AppWidgetManager mngr, int[] ids) { + context.startService(new Intent(BookmarkStackWidgetService.UPDATE, null, + context, BookmarkStackWidgetService.class)); + } + + @Override + public void onEnabled(Context context) { + context.startService(new Intent(context, BookmarkStackWidgetService.class)); + } + + @Override + public void onDisabled(Context context) { + context.stopService(new Intent(context, BookmarkStackWidgetService.class)); + } + +} diff --git a/src/com/android/browser/widget/BookmarkStackWidgetService.java b/src/com/android/browser/widget/BookmarkStackWidgetService.java new file mode 100644 index 000000000..f58cec1db --- /dev/null +++ b/src/com/android/browser/widget/BookmarkStackWidgetService.java @@ -0,0 +1,221 @@ +/* + * 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.widget; + +import com.android.browser.R; + +import android.app.PendingIntent; +import android.appwidget.AppWidgetManager; +import android.content.ComponentName; +import android.content.Intent; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.net.Uri; +import android.os.Handler; +import android.os.Message; +import android.provider.BrowserContract; +import android.text.TextUtils; +import android.util.Log; +import android.view.View; +import android.widget.RemoteViews; +import android.widget.RemoteViewsService; + +import java.util.ArrayList; +import java.util.List; + +public class BookmarkStackWidgetService extends RemoteViewsService { + + private static final String LOGTAG = "browserwidget"; + + /** Force the bookmarks to be re-renderer. */ + public static final String UPDATE = "com.android.browser.widget.UPDATE"; + + /** the adapter intent action */ + public static final String ADAPTER = "com.android.browser.widget.ADAPTER"; + + private static final String EXTRA_ID = "_id"; + private static final String EXTRA_URL = "_url"; + + private static final String[] PROJECTION = new String[] { + BrowserContract.Bookmarks._ID, + BrowserContract.Bookmarks.TITLE, + BrowserContract.Bookmarks.URL, + BrowserContract.Bookmarks.THUMBNAIL }; + + private static final String WHERE_CLAUSE = BrowserContract.Bookmarks.IS_FOLDER + + " == 0"; + + // No id specified. + private static final int NO_ID = -1; + + private static final int MSG_UPDATE = 0; + + List<RenderResult> mBookmarks; + + private final Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_UPDATE: + updateWidget(); + break; + default: + break; + } + } + }; + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + if ((intent == null) || (intent.getAction() != null) && UPDATE.equals(intent.getAction())) { + mHandler.sendEmptyMessage(MSG_UPDATE); + } + return START_STICKY; + } + + private void updateWidget() { + RemoteViews views = new RemoteViews(getPackageName(), + R.layout.bookmarkstackwidget); + Intent adapter = new Intent(BookmarkStackWidgetService.ADAPTER, null, + this, BookmarkStackWidgetService.class); + views.setRemoteAdapter(R.id.stackwidget_stack, adapter); + AppWidgetManager.getInstance(this).updateAppWidget( + new ComponentName(this, BookmarkStackWidgetProvider.class), + views); + } + + @Override + public RemoteViewsFactory onGetViewFactory(Intent intent) { + return mViewFactory; + } + + RemoteViewsService.RemoteViewsFactory mViewFactory = new RemoteViewsFactory () { + + @Override + public int getCount() { + return mBookmarks.size(); + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public RemoteViews getLoadingView() { + return null; + } + + @Override + public RemoteViews getViewAt(int position) { + RenderResult res = mBookmarks.get(position); + RemoteViews views = new RemoteViews(getPackageName(), + R.layout.bookmarkstackwidget_item); + views.setOnClickPendingIntent(R.id.stack_item, + getOpenUrlPendingIntent(res.mUrl)); + + // Set the title of the bookmark. Use the url as a backup. + String displayTitle = res.mTitle; + if (TextUtils.isEmpty(displayTitle)) { + displayTitle = res.mUrl; + } + views.setTextViewText(R.id.label, displayTitle); + if (res.mBitmap != null) { + views.setImageViewBitmap(R.id.thumb, res.mBitmap); + views.setViewVisibility(R.id.label, View.GONE); + } + return views; + } + + @Override + public int getViewTypeCount() { + return 1; + } + + @Override + public boolean hasStableIds() { + return false; + } + + @Override + public void onCreate() { + update(); + } + + @Override + public void onDestroy() { + } + + public void update() { + mBookmarks = new ArrayList<RenderResult>(); + // Look up all the bookmarks + Cursor c = null; + try { + c = getContentResolver().query(BrowserContract.Bookmarks.CONTENT_URI, + PROJECTION, WHERE_CLAUSE, null, null); + if (c != null) { + while (c.moveToNext()) { + int id = c.getInt(0); + String title = c.getString(1); + String url = c.getString(2); + RenderResult res = new RenderResult(id, title, url); + byte[] blob = c.getBlob(3); + if (blob != null) { + res.mBitmap = BitmapFactory.decodeByteArray(blob, 0, blob.length); + } + mBookmarks.add(res); + } + } + } catch (IllegalStateException e) { + Log.e(LOGTAG, "update bookmark widget", e); + } finally { + if (c != null) { + c.close(); + } + } + } + + @Override + public void onDataSetChanged() { + } + }; + + private PendingIntent getOpenUrlPendingIntent(String url) { + Intent vi = new Intent(Intent.ACTION_VIEW); + vi.setData(Uri.parse(url)); + return PendingIntent.getActivity(this, 0, vi, PendingIntent.FLAG_CANCEL_CURRENT); + } + + + // Class containing the rendering information for a specific bookmark. + private static class RenderResult { + final int mId; + final String mTitle; + final String mUrl; + Bitmap mBitmap; + + RenderResult(int id, String title, String url) { + mId = id; + mTitle = title; + mUrl = url; + } + + } + + +} |
