diff options
Diffstat (limited to 'src')
24 files changed, 2966 insertions, 514 deletions
diff --git a/src/com/android/browser/AddBookmarkPage.java b/src/com/android/browser/AddBookmarkPage.java index cf3fe70..191659a 100644 --- a/src/com/android/browser/AddBookmarkPage.java +++ b/src/com/android/browser/AddBookmarkPage.java @@ -18,18 +18,14 @@ package com.android.browser; import android.app.Activity; import android.content.ContentResolver; -import android.content.ContentUris; -import android.content.ContentValues; import android.content.Intent; import android.content.res.Resources; -import android.database.Cursor; import android.net.ParseException; import android.net.WebAddress; import android.os.Bundle; import android.provider.Browser; import android.view.View; import android.view.Window; -import android.webkit.WebIconDatabase; import android.widget.EditText; import android.widget.TextView; import android.widget.Toast; @@ -46,11 +42,6 @@ public class AddBookmarkPage extends Activity { private View mCancelButton; private boolean mEditingExisting; private Bundle mMap; - - private static final String[] mProjection = - { "_id", "url", "bookmark", "created", "title", "visits" }; - private static final String WHERE_CLAUSE = "url = ?"; - private final String[] SELECTION_ARGS = new String[1]; private View.OnClickListener mSaveBookmark = new View.OnClickListener() { public void onClick(View v) { @@ -151,70 +142,7 @@ public class AddBookmarkPage extends Activity { setResult(RESULT_OK, (new Intent()).setAction( getIntent().toString()).putExtras(mMap)); } else { - // Want to append to the beginning of the list - long creationTime = new Date().getTime(); - SELECTION_ARGS[0] = url; - ContentResolver cr = getContentResolver(); - Cursor c = cr.query(Browser.BOOKMARKS_URI, - mProjection, - WHERE_CLAUSE, - SELECTION_ARGS, - null); - ContentValues map = new ContentValues(); - if (c.moveToFirst() && c.getInt(c.getColumnIndexOrThrow( - Browser.BookmarkColumns.BOOKMARK)) == 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, title); - map.put(Browser.BookmarkColumns.BOOKMARK, 1); - cr.update(Browser.BOOKMARKS_URI, map, - "_id = " + c.getInt(0), null); - } else { - int count = c.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 - c.moveToPosition(i); - if (c.getString(c.getColumnIndexOrThrow( - Browser.BookmarkColumns.TITLE)).equals(title)) { - // The old bookmark has the same name. - // Update its creation time. - map.put(Browser.BookmarkColumns.CREATED, - creationTime); - cr.update(Browser.BOOKMARKS_URI, map, - "_id = " + c.getInt(0), null); - matchedTitle = true; - } - } - 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, title); - map.put(Browser.BookmarkColumns.URL, url); - map.put(Browser.BookmarkColumns.CREATED, creationTime); - map.put(Browser.BookmarkColumns.BOOKMARK, 1); - map.put(Browser.BookmarkColumns.DATE, 0); - 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 = c.getInt(c.getColumnIndexOrThrow( - Browser.BookmarkColumns.VISITS)); - } - // 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); - } - } - WebIconDatabase.getInstance().retainIconForPageUrl(url); - c.deactivate(); + Bookmarks.addBookmark(null, getContentResolver(), url, title, true); setResult(RESULT_OK); } } catch (IllegalStateException e) { diff --git a/src/com/android/browser/AddNewBookmark.java b/src/com/android/browser/AddNewBookmark.java index a75d002..5308f6b 100644 --- a/src/com/android/browser/AddNewBookmark.java +++ b/src/com/android/browser/AddNewBookmark.java @@ -47,17 +47,7 @@ class AddNewBookmark extends LinearLayout { mUrlText = (TextView) findViewById(R.id.url); mImageView = (ImageView) findViewById(R.id.favicon); } - - /** - * Copy this BookmarkItem to item. - * @param item BookmarkItem to receive the info from this BookmarkItem. - */ - /* package */ void copyTo(AddNewBookmark item) { - item.mTextView.setText(mTextView.getText()); - item.mUrlText.setText(mUrlText.getText()); - item.mImageView.setImageDrawable(mImageView.getDrawable()); - } - + /** * Set the new url for the bookmark item. * @param url The new url for the bookmark item. diff --git a/src/com/android/browser/Bookmarks.java b/src/com/android/browser/Bookmarks.java new file mode 100644 index 0000000..a3dc919 --- /dev/null +++ b/src/com/android/browser/Bookmarks.java @@ -0,0 +1,202 @@ +/* + * 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; + +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.provider.Browser; +import android.util.Log; +import android.webkit.WebIconDatabase; +import android.widget.Toast; + +import java.util.Date; + +/** + * This class is purely to have a common place for adding/deleting bookmarks. + */ +/* package */ class Bookmarks { + private static final String WHERE_CLAUSE + = "url = ? OR url = ? OR url = ? OR url = ?"; + private static final String WHERE_CLAUSE_SECURE = "url = ? OR url = ?"; + + private static String[] SELECTION_ARGS; + + /** + * Add a bookmark to the database. + * @param context Context of the calling Activity. This is used to make + * Toast confirming that the bookmark has been added. If the + * caller provides null, the Toast will not be shown. + * @param cr The ContentResolver being used to add the bookmark to the db. + * @param url URL of the website to be bookmarked. + * @param name Provided name for the bookmark. + * @param retainIcon Whether to retain the page's icon in the icon database. + * 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, + boolean retainIcon) { + // Want to append to the beginning of the list + long creationTime = new Date().getTime(); + // First we check to see if the user has already visited this + // site. They may have bookmarked it in a different way from + // how it's stored in the database, so allow different combos + // to map to the same url. + boolean secure = false; + String compareString = url; + if (compareString.startsWith("http://")) { + compareString = compareString.substring(7); + } else if (compareString.startsWith("https://")) { + compareString = compareString.substring(8); + secure = true; + } + if (compareString.startsWith("www.")) { + compareString = compareString.substring(4); + } + if (secure) { + SELECTION_ARGS = new String[2]; + SELECTION_ARGS[0] = "https://" + compareString; + SELECTION_ARGS[1] = "https://www." + compareString; + } else { + SELECTION_ARGS = new String[4]; + SELECTION_ARGS[0] = compareString; + SELECTION_ARGS[1] = "www." + compareString; + SELECTION_ARGS[2] = "http://" + compareString; + SELECTION_ARGS[3] = "http://" + SELECTION_ARGS[1]; + } + Cursor cursor = cr.query(Browser.BOOKMARKS_URI, + Browser.HISTORY_PROJECTION, + secure ? WHERE_CLAUSE_SECURE : WHERE_CLAUSE, + SELECTION_ARGS, + null); + ContentValues map = new ContentValues(); + 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); + 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); + 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); + } + } + if (retainIcon) { + WebIconDatabase.getInstance().retainIconForPageUrl(url); + } + cursor.deactivate(); + if (context != null) { + Toast.makeText(context, R.string.added_to_bookmarks, + Toast.LENGTH_LONG).show(); + } + } + + /** + * Remove a bookmark from the database. If the url is a visited site, it + * will remain in the database, but only as a history item, and not as a + * bookmarked site. + * @param context Context of the calling Activity. This is used to make + * Toast confirming that the bookmark has been removed. If the + * caller provides null, the Toast will not be shown. + * @param cr The ContentResolver being used to remove the bookmark. + * @param url URL of the website to be removed. + */ + /* package */ static void removeFromBookmarks(Context context, + ContentResolver cr, String url) { + Cursor cursor = cr.query( + Browser.BOOKMARKS_URI, + Browser.HISTORY_PROJECTION, + "url = ?", + new String[] { url }, + null); + boolean first = cursor.moveToFirst(); + // Should be in the database no matter what + if (!first) { + throw new AssertionError("URL is not in the database!"); + } + // 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!"); + } + } + if (context != null) { + Toast.makeText(context, R.string.removed_from_bookmarks, + Toast.LENGTH_LONG).show(); + } + cursor.deactivate(); + } +}
\ No newline at end of file diff --git a/src/com/android/browser/BrowserActivity.java b/src/com/android/browser/BrowserActivity.java index 93ba1b1..5134cf8 100644 --- a/src/com/android/browser/BrowserActivity.java +++ b/src/com/android/browser/BrowserActivity.java @@ -28,6 +28,7 @@ import android.content.ActivityNotFoundException; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.ContentResolver; +import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; import android.content.DialogInterface; @@ -35,6 +36,7 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.ServiceConnection; import android.content.DialogInterface.OnCancelListener; +import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.res.AssetManager; @@ -107,11 +109,14 @@ 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.WebChromeClient; +import android.webkit.WebChromeClient.CustomViewCallback; import android.webkit.WebHistoryItem; import android.webkit.WebIconDatabase; +import android.webkit.WebStorage; import android.webkit.WebView; import android.webkit.WebViewClient; import android.widget.EditText; @@ -121,6 +126,7 @@ import android.widget.TextView; import android.widget.Toast; import java.io.BufferedOutputStream; +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; @@ -583,11 +589,6 @@ public class BrowserActivity extends Activity } copyBuildInfos(); - - // Refresh the plugin list. - if (mTabControl.getCurrentWebView() != null) { - mTabControl.getCurrentWebView().refreshPlugins(false); - } } catch (IOException e) { Log.e(TAG, "IO Exception: " + e); } @@ -634,16 +635,22 @@ public class BrowserActivity extends Activity } } + // Flag to enable the touchable browser bar with buttons + private final boolean CUSTOM_BROWSER_BAR = true; + @Override public void onCreate(Bundle icicle) { if (LOGV_ENABLED) { Log.v(LOGTAG, this + " onStart"); } super.onCreate(icicle); - this.requestWindowFeature(Window.FEATURE_LEFT_ICON); - this.requestWindowFeature(Window.FEATURE_RIGHT_ICON); - this.requestWindowFeature(Window.FEATURE_PROGRESS); - this.requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); - + if (CUSTOM_BROWSER_BAR) { + this.requestWindowFeature(Window.FEATURE_NO_TITLE); + } else { + this.requestWindowFeature(Window.FEATURE_LEFT_ICON); + this.requestWindowFeature(Window.FEATURE_RIGHT_ICON); + this.requestWindowFeature(Window.FEATURE_PROGRESS); + this.requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); + } // test the browser in OpenGL // requestWindowFeature(Window.FEATURE_OPENGL); @@ -668,8 +675,37 @@ public class BrowserActivity extends Activity mGenericFavicon = getResources().getDrawable( R.drawable.app_web_browser_sm); - mContentView = (FrameLayout) getWindow().getDecorView().findViewById( - com.android.internal.R.id.content); + FrameLayout frameLayout = (FrameLayout) getWindow().getDecorView() + .findViewById(com.android.internal.R.id.content); + if (CUSTOM_BROWSER_BAR) { + // This FrameLayout will hold the custom FrameLayout and a LinearLayout + // that contains the title bar and a FrameLayout, which + // holds everything else. + FrameLayout browserFrameLayout = (FrameLayout) LayoutInflater.from(this) + .inflate(R.layout.custom_screen, null); + mTitleBar = (TitleBar) browserFrameLayout.findViewById(R.id.title_bar); + mTitleBar.setBrowserActivity(this); + mContentView = (FrameLayout) browserFrameLayout.findViewById( + R.id.main_content); + mErrorConsoleContainer = (LinearLayout) browserFrameLayout.findViewById( + R.id.error_console); + mCustomViewContainer = (FrameLayout) browserFrameLayout + .findViewById(R.id.fullscreen_custom_content); + frameLayout.addView(browserFrameLayout, COVER_SCREEN_PARAMS); + } else { + mCustomViewContainer = new FrameLayout(this); + mCustomViewContainer.setBackgroundColor(Color.BLACK); + mContentView = new FrameLayout(this); + + LinearLayout linearLayout = new LinearLayout(this); + linearLayout.setOrientation(LinearLayout.VERTICAL); + mErrorConsoleContainer = new LinearLayout(this); + linearLayout.addView(mErrorConsoleContainer, new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + linearLayout.addView(mContentView, COVER_SCREEN_PARAMS); + frameLayout.addView(mCustomViewContainer, COVER_SCREEN_PARAMS); + frameLayout.addView(linearLayout, COVER_SCREEN_PARAMS); + } // Create the tab control and our initial tab mTabControl = new TabControl(this); @@ -702,6 +738,52 @@ 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; + } + 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); + // If this was a web search request, pass it on to the default web search provider. if (handleWebSearchIntent(getIntent())) { moveTaskToBack(true); @@ -713,6 +795,9 @@ public class BrowserActivity extends Activity // none of the files in the directory are referenced any more. new ClearThumbnails().execute( mTabControl.getThumbnailDir().listFiles()); + // 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(); final Intent intent = getIntent(); final Bundle extra = intent.getExtras(); // Create an initial tab. @@ -765,6 +850,12 @@ public class BrowserActivity extends Activity // are not animating from the tab picker. attachTabToContentView(mTabControl.getCurrentTab()); } + + // Read JavaScript flags if it exists. + String jsFlags = mSettings.getJsFlags(); + if (jsFlags.trim().length() != 0) { + mTabControl.getCurrentWebView().setJsFlags(jsFlags); + } } @Override @@ -812,7 +903,7 @@ public class BrowserActivity extends Activity if (Intent.ACTION_VIEW.equals(action) && !getPackageName().equals(appId) && (flags & Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT) != 0) { - final TabControl.Tab appTab = mTabControl.getTabFromId(appId); + TabControl.Tab appTab = mTabControl.getTabFromId(appId); if (appTab != null) { Log.i(LOGTAG, "Reusing tab for " + appId); // Dismiss the subwindow if applicable. @@ -825,14 +916,14 @@ public class BrowserActivity extends Activity // page, it can be reused. boolean needsLoad = mTabControl.recreateWebView(appTab, urlData.mUrl); - + if (current != appTab) { showTab(appTab, needsLoad ? urlData : EMPTY_URL_DATA); } else { if (mTabOverview != null && mAnimationCount == 0) { sendAnimateFromOverview(appTab, false, - needsLoad ? urlData : EMPTY_URL_DATA, TAB_OVERVIEW_DELAY, - null); + needsLoad ? urlData : EMPTY_URL_DATA, + TAB_OVERVIEW_DELAY, null); } else { // If the tab was the current tab, we have to attach // it to the view system again. @@ -843,12 +934,32 @@ public class BrowserActivity extends Activity } } return; + } else { + // No matching application tab, try to find a regular tab + // with a matching url. + appTab = mTabControl.findUnusedTabWithUrl(urlData.mUrl); + if (appTab != null) { + if (current != appTab) { + // Use EMPTY_URL_DATA so we do not reload the page + showTab(appTab, EMPTY_URL_DATA); + } else { + if (mTabOverview != null && mAnimationCount == 0) { + sendAnimateFromOverview(appTab, false, + EMPTY_URL_DATA, TAB_OVERVIEW_DELAY, + null); + } + // Don't do anything here since we are on the + // correct page. + } + } else { + // if FLAG_ACTIVITY_BROUGHT_TO_FRONT flag is on, the url + // will be opened in a new tab unless we have reached + // MAX_TABS. Then the url will be opened in the current + // tab. If a new tab is created, it will have "true" for + // exit on close. + openTabAndShow(urlData, null, true, appId); + } } - // if FLAG_ACTIVITY_BROUGHT_TO_FRONT flag is on, the url will be - // opened in a new tab unless we have reached MAX_TABS. Then the - // url will be opened in the current tab. If a new tab is - // created, it will have "true" for exit on close. - openTabAndShow(urlData, null, true, appId); } else { if ("about:debug".equals(urlData.mUrl)) { mSettings.toggleDebugSettings(); @@ -1107,8 +1218,9 @@ public class BrowserActivity extends Activity return; } + mTabControl.resumeCurrentTab(); mActivityInPause = false; - resumeWebView(); + resumeWebViewTimers(); if (mWakeLock.isHeld()) { mHandler.removeMessages(RELEASE_WAKELOCK); @@ -1166,8 +1278,9 @@ public class BrowserActivity extends Activity return; } + mTabControl.pauseCurrentTab(); mActivityInPause = true; - if (mTabControl.getCurrentIndex() >= 0 && !pauseWebView()) { + if (mTabControl.getCurrentIndex() >= 0 && !pauseWebViewTimers()) { mWakeLock.acquire(); mHandler.sendMessageDelayed(mHandler .obtainMessage(RELEASE_WAKELOCK), WAKELOCK_TIMEOUT); @@ -1218,6 +1331,8 @@ public class BrowserActivity extends Activity // "com.android.masfproxyservice", // "com.android.masfproxyservice.MasfProxyService")); //stopService(proxyServiceIntent); + + unregisterReceiver(mPackageInstallationReceiver); } @Override @@ -1266,7 +1381,7 @@ public class BrowserActivity extends Activity mTabControl.freeMemory(); } - private boolean resumeWebView() { + private boolean resumeWebViewTimers() { if ((!mActivityInPause && !mPageStarted) || (mActivityInPause && mPageStarted)) { CookieSyncManager.getInstance().startSync(); @@ -1280,7 +1395,7 @@ public class BrowserActivity extends Activity } } - private boolean pauseWebView() { + private boolean pauseWebViewTimers() { if (mActivityInPause && !mPageStarted) { CookieSyncManager.getInstance().stopSync(); WebView w = mTabControl.getCurrentWebView(); @@ -1441,7 +1556,7 @@ public class BrowserActivity extends Activity */ @Override public boolean onSearchRequested() { - String url = getTopWindow().getUrl(); + String url = (getTopWindow() == null) ? null : getTopWindow().getUrl(); startSearch(mSettings.getHomePage().equals(url) ? null : url, true, createGoogleSearchSourceBundle(GOOGLE_SEARCH_SOURCE_SEARCHKEY), false); return true; @@ -1877,6 +1992,20 @@ public class BrowserActivity extends Activity final WebView main = t.getWebView(); // Attach the main WebView. mContentView.addView(main, COVER_SCREEN_PARAMS); + + if (mShouldShowErrorConsole) { + ErrorConsoleView errorConsole = mTabControl.getCurrentErrorConsole(true); + if (errorConsole.numberOfErrors() == 0) { + errorConsole.showConsole(ErrorConsoleView.SHOW_NONE); + } else { + errorConsole.showConsole(ErrorConsoleView.SHOW_MINIMIZED); + } + + mErrorConsoleContainer.addView(errorConsole, + new LinearLayout.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT)); + } + // Attach the sub window if necessary attachSubWindow(t); // Request focus on the top window. @@ -1898,6 +2027,11 @@ public class BrowserActivity extends Activity private void removeTabFromContentView(TabControl.Tab t) { // Remove the main WebView. mContentView.removeView(t.getWebView()); + + if (mTabControl.getCurrentErrorConsole(false) != null) { + mErrorConsoleContainer.removeView(mTabControl.getCurrentErrorConsole(false)); + } + // Remove the sub window if it exists. if (t.getSubWebView() != null) { mContentView.removeView(t.getSubWebViewContainer()); @@ -2377,7 +2511,11 @@ public class BrowserActivity extends Activity // While the tab overview is animating or being shown, block changes // to the title. if (mAnimationCount == 0 && mTabOverview == null) { - setTitle(buildUrlTitle(url, title)); + if (CUSTOM_BROWSER_BAR) { + mTitleBar.setTitleAndUrl(title, url); + } else { + setTitle(buildUrlTitle(url, title)); + } } } @@ -2418,7 +2556,7 @@ public class BrowserActivity extends Activity * or an empty string if, for example, the URL in question is a * file:// URL with no hostname. */ - private static String buildTitleUrl(String url) { + /* package */ static String buildTitleUrl(String url) { String titleUrl = null; if (url != null) { @@ -2454,18 +2592,34 @@ public class BrowserActivity extends Activity if (mAnimationCount > 0 || mTabOverview != null) { return; } - Drawable[] array = new Drawable[2]; - PaintDrawable p = new PaintDrawable(Color.WHITE); - p.setCornerRadius(3f); - array[0] = p; - if (icon == null) { - array[1] = mGenericFavicon; + if (CUSTOM_BROWSER_BAR) { + 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); + mTitleBar.setFavicon(d); } else { - array[1] = new BitmapDrawable(icon); + Drawable[] array = new Drawable[2]; + PaintDrawable p = new PaintDrawable(Color.WHITE); + p.setCornerRadius(3f); + array[0] = p; + if (icon == null) { + array[1] = mGenericFavicon; + } else { + array[1] = new BitmapDrawable(icon); + } + LayerDrawable d = new LayerDrawable(array); + d.setLayerInset(1, 2, 2, 2, 2); + getWindow().setFeatureDrawable(Window.FEATURE_LEFT_ICON, d); } - LayerDrawable d = new LayerDrawable(array); - d.setLayerInset(1, 2, 2, 2, 2); - getWindow().setFeatureDrawable(Window.FEATURE_LEFT_ICON, d); } /** @@ -2543,9 +2697,9 @@ public class BrowserActivity extends Activity finish(); return; } - // call pauseWebView() now, we won't be able to call it in - // onPause() as the WebView won't be valid. - pauseWebView(); + // call pauseWebViewTimers() now, we won't be able to call + // it in onPause() as the WebView won't be valid. + pauseWebViewTimers(); removeTabFromContentView(current); mTabControl.removeTab(current); } @@ -2569,10 +2723,15 @@ public class BrowserActivity extends Activity // because of accumulated key events, // we should ignore it as browser is not active any more. WebView topWindow = getTopWindow(); - if (topWindow == null) + if (topWindow == null && mCustomView == null) return KeyTracker.State.NOT_TRACKING; if (keyCode == KeyEvent.KEYCODE_BACK) { + // Check if a custom view is currently showing and, if it is, hide it. + if (mCustomView != null) { + mWebChromeClient.onHideCustomView(); + return KeyTracker.State.DONE_TRACKING; + } // During animations, block the back key so that other animations // are not triggered and so that we don't end up destroying all the // WebViews before finishing the animation. @@ -2771,6 +2930,58 @@ public class BrowserActivity extends Activity } }; + private void updateScreenshot(WebView view) { + // If this is a bookmarked site, add a screenshot to the database. + // FIXME: When should we update? Every time? + // FIXME: Would like to make sure there is actually something to + // draw, but the API for that (WebViewCore.pictureReady()) is not + // currently accessible here. + String original = view.getOriginalUrl(); + if (original != null) { + // copied from BrowserBookmarksAdapter + int query = original.indexOf('?'); + String noQuery = original; + if (query != -1) { + noQuery = original.substring(0, query); + } + String URL = noQuery + '?'; + String[] selArgs = new String[] { noQuery, URL }; + final String where + = "(url == ? OR url GLOB ? || '*') AND bookmark == 1"; + final String[] projection + = new String[] { Browser.BookmarkColumns._ID }; + ContentResolver cr = getContentResolver(); + final Cursor c = cr.query(Browser.BOOKMARKS_URI, projection, + where, selArgs, null); + boolean succeed = c.moveToFirst(); + ContentValues values = null; + while (succeed) { + if (values == null) { + final ByteArrayOutputStream os + = new ByteArrayOutputStream(); + Picture thumbnail = view.capturePicture(); + // Keep width and height in sync with BrowserBookmarksPage + // and bookmark_thumb + Bitmap bm = Bitmap.createBitmap(100, 80, + Bitmap.Config.ARGB_4444); + Canvas canvas = new Canvas(bm); + // May need to tweak these values to determine what is the + // best scale factor + canvas.scale(.5f, .5f); + thumbnail.draw(canvas); + bm.compress(Bitmap.CompressFormat.PNG, 100, os); + values = new ContentValues(); + values.put(Browser.BookmarkColumns.THUMBNAIL, + os.toByteArray()); + } + cr.update(ContentUris.withAppendedId(Browser.BOOKMARKS_URI, + c.getInt(0)), values, null, null); + succeed = c.moveToNext(); + } + c.close(); + } + } + // ------------------------------------------------------------------------- // WebViewClient implementation. //------------------------------------------------------------------------- @@ -2798,6 +3009,15 @@ public class BrowserActivity extends Activity public void onPageStarted(WebView view, String url, Bitmap favicon) { resetLockIcon(url); setUrlTitle(url, null); + + ErrorConsoleView errorConsole = mTabControl.getCurrentErrorConsole(false); + if (errorConsole != null) { + errorConsole.clearErrorMessages(); + if (mShouldShowErrorConsole) { + errorConsole.showConsole(ErrorConsoleView.SHOW_NONE); + } + } + // Call updateIcon instead of setFavicon so the bookmark // database can be updated. updateIcon(url, favicon); @@ -2846,8 +3066,9 @@ public class BrowserActivity extends Activity if (!mPageStarted) { mPageStarted = true; - // if onResume() has been called, resumeWebView() does nothing. - resumeWebView(); + // if onResume() has been called, resumeWebViewTimers() does + // nothing. + resumeWebViewTimers(); } // reset sync timer to avoid sync starts during loading a page @@ -2881,6 +3102,7 @@ public class BrowserActivity extends Activity // Update the lock icon image only once we are done loading updateLockIconImage(mLockIconType); + updateScreenshot(view); // Performance probe if (false) { @@ -2968,9 +3190,9 @@ public class BrowserActivity extends Activity if (mPageStarted) { mPageStarted = false; - // pauseWebView() will do nothing and return false if onPause() - // is not called yet. - if (pauseWebView()) { + // pauseWebViewTimers() will do nothing and return false if + // onPause() is not called yet. + if (pauseWebViewTimers()) { if (mWakeLock.isHeld()) { mHandler.removeMessages(RELEASE_WAKELOCK); mWakeLock.release(); @@ -3016,9 +3238,9 @@ public class BrowserActivity extends Activity if (url.startsWith("about:")) { return false; } - + Intent intent; - + // perform generic parsing of the URI to turn it into an Intent. try { intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME); @@ -3516,8 +3738,13 @@ public class BrowserActivity extends Activity // Block progress updates to the title bar while the tab overview // is animating or being displayed. if (mAnimationCount == 0 && mTabOverview == null) { - getWindow().setFeatureInt(Window.FEATURE_PROGRESS, - newProgress * 100); + if (CUSTOM_BROWSER_BAR) { + mTitleBar.setProgress(newProgress); + } else { + getWindow().setFeatureInt(Window.FEATURE_PROGRESS, + newProgress * 100); + + } } if (newProgress == 100) { @@ -3551,6 +3778,8 @@ public class BrowserActivity extends Activity url.length() >= SQLiteDatabase.SQLITE_MAX_LIKE_PATTERN_LENGTH) { return; } + // See if we can find the current url in our history database and + // add the new title to it. if (url.startsWith("http://www.")) { url = url.substring(11); } else if (url.startsWith("http://")) { @@ -3565,9 +3794,6 @@ public class BrowserActivity extends Activity Cursor c = mResolver.query(Browser.BOOKMARKS_URI, Browser.HISTORY_PROJECTION, where, selArgs, null); if (c.moveToFirst()) { - if (LOGV_ENABLED) { - Log.v(LOGTAG, "updating cursor"); - } // Current implementation of database only has one entry per // url. ContentValues map = new ContentValues(); @@ -3587,6 +3813,96 @@ public class BrowserActivity extends Activity public void onReceivedIcon(WebView view, Bitmap icon) { updateIcon(view.getUrl(), icon); } + + @Override + public void onShowCustomView(View view, WebChromeClient.CustomViewCallback callback) { + if (mCustomView != null) + return; + + // Add the custom view to its container. + mCustomViewContainer.addView(view, COVER_SCREEN_GRAVITY_CENTER); + mCustomView = view; + mCustomViewCallback = callback; + // Save the menu state and set it to empty while the custom + // view is showing. + mOldMenuState = mMenuState; + mMenuState = EMPTY_MENU; + // Hide the content view. + mContentView.setVisibility(View.GONE); + // Finally show the custom view container. + mCustomViewContainer.setVisibility(View.VISIBLE); + mCustomViewContainer.bringToFront(); + } + + @Override + public void onHideCustomView() { + if (mCustomView == null) + return; + + // Hide the custom view. + mCustomView.setVisibility(View.GONE); + // Remove the custom view from its container. + mCustomViewContainer.removeView(mCustomView); + mCustomView = null; + // Reset the old menu state. + mMenuState = mOldMenuState; + mOldMenuState = EMPTY_MENU; + mCustomViewContainer.setVisibility(View.GONE); + mCustomViewCallback.onCustomViewHidden(); + // Show the content view. + mContentView.setVisibility(View.VISIBLE); + } + + /** + * The origin has exceeded its database quota. + * @param url the URL that exceeded the quota + * @param databaseIdentifier the identifier of the database on + * which the transaction that caused the quota overflow was run + * @param currentQuota the current quota for the origin. + * @param totalUsedQuota is the sum of all origins' quota. + * @param quotaUpdater The callback to run when a decision to allow or + * deny quota has been made. Don't forget to call this! + */ + @Override + public void onExceededDatabaseQuota(String url, + String databaseIdentifier, long currentQuota, long totalUsedQuota, + WebStorage.QuotaUpdater quotaUpdater) { + mSettings.getWebStorageSizeManager().onExceededDatabaseQuota( + url, databaseIdentifier, currentQuota, totalUsedQuota, + quotaUpdater); + } + + /** + * The Application Cache has exceeded its max size. + * @param spaceNeeded is the amount of disk space that would be needed + * in order for the last appcache operation to succeed. + * @param totalUsedQuota is the sum of all origins' quota. + * @param quotaUpdater A callback to inform the WebCore thread that a new + * app cache size is available. This callback must always be executed at + * some point to ensure that the sleeping WebCore thread is woken up. + */ + @Override + public void onReachedMaxAppCacheSize(long spaceNeeded, + long totalUsedQuota, WebStorage.QuotaUpdater quotaUpdater) { + mSettings.getWebStorageSizeManager().onReachedMaxAppCacheSize( + spaceNeeded, totalUsedQuota, quotaUpdater); + } + + /* Adds a JavaScript error message to the system log. + * @param message The error message to report. + * @param lineNumber The line number of the error. + * @param sourceID The name of the source file that caused the error. + */ + @Override + public void addMessageToConsole(String message, int lineNumber, String sourceID) { + ErrorConsoleView errorConsole = mTabControl.getCurrentErrorConsole(true); + errorConsole.addErrorMessage(message, sourceID, lineNumber); + if (mShouldShowErrorConsole && + errorConsole.getShowState() != ErrorConsoleView.SHOW_MAXIMIZED) { + errorConsole.showConsole(ErrorConsoleView.SHOW_MINIMIZED); + } + Log.w(LOGTAG, "Console: " + message + " " + sourceID + ":" + lineNumber); + } }; /** @@ -3702,19 +4018,19 @@ public class BrowserActivity extends Activity String cookies = CookieManager.getInstance().getCookie(url); ContentValues values = new ContentValues(); - values.put(Downloads.URI, uri.toString()); - values.put(Downloads.COOKIE_DATA, cookies); - values.put(Downloads.USER_AGENT, userAgent); - values.put(Downloads.NOTIFICATION_PACKAGE, + values.put(Downloads.COLUMN_URI, uri.toString()); + values.put(Downloads.COLUMN_COOKIE_DATA, cookies); + values.put(Downloads.COLUMN_USER_AGENT, userAgent); + values.put(Downloads.COLUMN_NOTIFICATION_PACKAGE, getPackageName()); - values.put(Downloads.NOTIFICATION_CLASS, + values.put(Downloads.COLUMN_NOTIFICATION_CLASS, BrowserDownloadPage.class.getCanonicalName()); - values.put(Downloads.VISIBILITY, Downloads.VISIBILITY_VISIBLE_NOTIFY_COMPLETED); - values.put(Downloads.MIMETYPE, mimetype); - values.put(Downloads.FILENAME_HINT, filename); - values.put(Downloads.DESCRIPTION, uri.getHost()); + values.put(Downloads.COLUMN_VISIBILITY, Downloads.VISIBILITY_VISIBLE_NOTIFY_COMPLETED); + values.put(Downloads.COLUMN_MIME_TYPE, mimetype); + values.put(Downloads.COLUMN_FILE_NAME_HINT, filename); + values.put(Downloads.COLUMN_DESCRIPTION, uri.getHost()); if (contentLength > 0) { - values.put(Downloads.TOTAL_BYTES, contentLength); + values.put(Downloads.COLUMN_TOTAL_BYTES, contentLength); } if (mimetype == null) { // We must have long pressed on a link or image to download it. We @@ -3780,7 +4096,11 @@ public class BrowserActivity extends Activity // If the tab overview is animating or being shown, do not update the // lock icon. if (mAnimationCount == 0 && mTabOverview == null) { - getWindow().setFeatureDrawable(Window.FEATURE_RIGHT_ICON, d); + if (CUSTOM_BROWSER_BAR) { + mTitleBar.setLock(d); + } else { + getWindow().setFeatureDrawable(Window.FEATURE_RIGHT_ICON, d); + } } } @@ -4273,7 +4593,17 @@ public class BrowserActivity extends Activity String data = intent.getAction(); Bundle extras = intent.getExtras(); if (extras != null && extras.getBoolean("new_window", false)) { - openTab(data); + final TabControl.Tab newTab = openTab(data); + if (mSettings.openInBackground() && + newTab != null && mTabOverview != null) { + mTabControl.populatePickerData(newTab); + mTabControl.setCurrentTab(newTab); + mTabOverview.add(newTab); + mTabOverview.setCurrentIndex( + mTabControl.getCurrentIndex()); + sendAnimateFromOverview(newTab, false, + EMPTY_URL_DATA, TAB_OVERVIEW_DELAY, null); + } } else { final TabControl.Tab currentTab = mTabControl.getCurrentTab(); @@ -4281,8 +4611,8 @@ public class BrowserActivity extends Activity // middle of an animation, animate away from it to the // current tab. if (mTabOverview != null && mAnimationCount == 0) { - sendAnimateFromOverview(currentTab, false, new UrlData(data), - TAB_OVERVIEW_DELAY, null); + sendAnimateFromOverview(currentTab, false, + new UrlData(data), TAB_OVERVIEW_DELAY, null); } else { dismissSubWindow(currentTab); if (data != null && data.length() != 0) { @@ -4332,8 +4662,8 @@ public class BrowserActivity extends Activity // was clicked on. if (mTabControl.getTabCount() == 0) { current = mTabControl.createNewTab(); - sendAnimateFromOverview(current, true, - new UrlData(mSettings.getHomePage()), TAB_OVERVIEW_DELAY, null); + sendAnimateFromOverview(current, true, new UrlData( + mSettings.getHomePage()), TAB_OVERVIEW_DELAY, null); } else { final int index = position > 0 ? (position - 1) : 0; current = mTabControl.getTab(index); @@ -4369,8 +4699,8 @@ public class BrowserActivity extends Activity if (index == ImageGrid.NEW_TAB) { openTabAndShow(mSettings.getHomePage(), null, false, null); } else { - sendAnimateFromOverview(mTabControl.getTab(index), - false, EMPTY_URL_DATA, 0, null); + sendAnimateFromOverview(mTabControl.getTab(index), false, + EMPTY_URL_DATA, 0, null); } } } @@ -4392,13 +4722,19 @@ public class BrowserActivity extends Activity AnimatingView(Context ctxt, TabControl.Tab t) { super(ctxt); mTab = t; - // Use the top window in the animation since the tab overview will - // display the top window in each cell. - final WebView w = t.getTopWindow(); - mPicture = w.capturePicture(); - mScale = w.getScale() / w.getWidth(); - mScrollX = w.getScrollX(); - mScrollY = w.getScrollY(); + if (t != null && t.getTopWindow() != null) { + // Use the top window in the animation since the tab overview + // will display the top window in each cell. + final WebView w = t.getTopWindow(); + mPicture = w.capturePicture(); + mScale = w.getScale() / w.getWidth(); + mScrollX = w.getScrollX(); + mScrollY = w.getScrollY(); + } else { + mPicture = null; + mScale = 1.0f; + mScrollX = mScrollY = 0; + } } @Override @@ -4472,16 +4808,20 @@ public class BrowserActivity extends Activity mAnimationCount++; // Always change the title bar to the window overview title while // animating. - getWindow().setFeatureDrawable(Window.FEATURE_LEFT_ICON, null); - getWindow().setFeatureDrawable(Window.FEATURE_RIGHT_ICON, null); - getWindow().setFeatureInt(Window.FEATURE_PROGRESS, - Window.PROGRESS_VISIBILITY_OFF); - setTitle(R.string.tab_picker_title); + if (CUSTOM_BROWSER_BAR) { + mTitleBar.setToTabPicker(); + } else { + getWindow().setFeatureDrawable(Window.FEATURE_LEFT_ICON, null); + getWindow().setFeatureDrawable(Window.FEATURE_RIGHT_ICON, null); + getWindow().setFeatureInt(Window.FEATURE_PROGRESS, + Window.PROGRESS_VISIBILITY_OFF); + setTitle(R.string.tab_picker_title); + } // Make the menu empty until the animation completes. mMenuState = EMPTY_MENU; } - private void bookmarksOrHistoryPicker(boolean startWithHistory) { + /* package */ void bookmarksOrHistoryPicker(boolean startWithHistory) { WebView current = mTabControl.getCurrentWebView(); if (current == null) { return; @@ -4557,7 +4897,7 @@ public class BrowserActivity extends Activity return 0; } - static final Pattern ACCEPTED_URI_SCHEMA = Pattern.compile( + protected static final Pattern ACCEPTED_URI_SCHEMA = Pattern.compile( "(?i)" + // switch on case insensitive matching "(" + // begin group for schema "(?:http|https|file):\\/\\/" + @@ -4622,6 +4962,34 @@ public class BrowserActivity extends Activity return URLUtil.composeSearchUrl(inUrl, QuickSearch_G, QUERY_PLACE_HOLDER); } + /* package */ void setShouldShowErrorConsole(boolean flag) { + if (flag == mShouldShowErrorConsole) { + // Nothing to do. + return; + } + + mShouldShowErrorConsole = flag; + + ErrorConsoleView errorConsole = mTabControl.getCurrentErrorConsole(true); + + if (flag) { + // Setting the show state of the console will cause it's the layout to be inflated. + if (errorConsole.numberOfErrors() > 0) { + errorConsole.showConsole(ErrorConsoleView.SHOW_MINIMIZED); + } else { + errorConsole.showConsole(ErrorConsoleView.SHOW_NONE); + } + + // Now we can add it to the main view. + mErrorConsoleContainer.addView(errorConsole, + new LinearLayout.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT)); + } else { + mErrorConsoleContainer.removeView(errorConsole); + } + + } + private final static int LOCK_ICON_UNSECURE = 0; private final static int LOCK_ICON_SECURE = 1; private final static int LOCK_ICON_MIXED = 2; @@ -4634,11 +5002,15 @@ public class BrowserActivity extends Activity private ContentResolver mResolver; private FrameLayout mContentView; private ImageGrid mTabOverview; + private View mCustomView; + private FrameLayout mCustomViewContainer; + private WebChromeClient.CustomViewCallback mCustomViewCallback; // FIXME, temp address onPrepareMenu performance problem. When we move everything out of // view, we should rewrite this. private int mCurrentMenuState = 0; private int mMenuState = R.id.MAIN_MENU; + private int mOldMenuState = EMPTY_MENU; private static final int EMPTY_MENU = -1; private Menu mMenu; @@ -4729,6 +5101,11 @@ public class BrowserActivity extends Activity new FrameLayout.LayoutParams( ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.FILL_PARENT); + /*package*/ static final FrameLayout.LayoutParams COVER_SCREEN_GRAVITY_CENTER = + new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.FILL_PARENT, + ViewGroup.LayoutParams.FILL_PARENT, + Gravity.CENTER); // Google search final static String QuickSearch_G = "http://www.google.com/m?q=%s"; // Wikipedia search @@ -4762,6 +5139,11 @@ public class BrowserActivity extends Activity private Toast mStopToast; + private TitleBar mTitleBar; + + private LinearLayout mErrorConsoleContainer = null; + private boolean mShouldShowErrorConsole = false; + // Used during animations to prevent other animations from being triggered. // A count is used since the animation to and from the Window overview can // overlap. A count of 0 means no animation where a count of > 0 means @@ -4779,10 +5161,12 @@ public class BrowserActivity extends Activity private IntentFilter mNetworkStateChangedFilter; private BroadcastReceiver mNetworkStateIntentReceiver; + private BroadcastReceiver mPackageInstallationReceiver; + // activity requestCode - final static int COMBO_PAGE = 1; - final static int DOWNLOAD_PAGE = 2; - final static int PREFERENCES_PAGE = 3; + final static int COMBO_PAGE = 1; + final static int DOWNLOAD_PAGE = 2; + final static int PREFERENCES_PAGE = 3; // the frenquency of checking whether system memory is low final static int CHECK_MEMORY_INTERVAL = 30000; // 30 seconds @@ -4832,7 +5216,7 @@ public class BrowserActivity extends Activity String mEncoding; @Override boolean isEmpty() { - return mInlined == null || mInlined.length() == 0 || super.isEmpty(); + return mInlined == null || mInlined.length() == 0 || super.isEmpty(); } @Override diff --git a/src/com/android/browser/BrowserBackupAgent.java b/src/com/android/browser/BrowserBackupAgent.java new file mode 100644 index 0000000..c239b12 --- /dev/null +++ b/src/com/android/browser/BrowserBackupAgent.java @@ -0,0 +1,295 @@ +/* + * 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; + +import java.io.IOException; + +import android.app.BackupAgent; +import android.backup.BackupDataInput; +import android.backup.BackupDataOutput; +import android.database.Cursor; +import android.os.ParcelFileDescriptor; +import android.provider.Browser; +import android.provider.Browser.BookmarkColumns; +import android.util.Log; + +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.EOFException; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.util.ArrayList; +import java.util.zip.CRC32; + +/** + * Settings backup agent for the Android browser. Currently the only thing + * stored is the set of bookmarks. It's okay if I/O exceptions are thrown + * out of the agent; the calling code handles it and the backup operation + * simply fails. + * + * @hide + */ +public class BrowserBackupAgent extends BackupAgent { + static final String TAG = "BrowserBookmarkAgent"; + static final boolean DEBUG = true; + + static final String BOOKMARK_KEY = "_bookmarks_"; + /** this version num MUST be incremented if the flattened-file schema ever changes */ + static final int BACKUP_AGENT_VERSION = 0; + + /** + * In order to determine whether the bookmark set has changed since the + * last time we did a backup, we store the following bits of info in the + * state file after a backup: + * + * 1. the size of the flattened bookmark file + * 2. the CRC32 of that file + * 3. the agent version number [relevant following an OTA] + * + * After we flatten the bookmarks file here in onBackup, we compare its + * metrics with the values from the saved state. If they match, it means + * the bookmarks didn't really change and we don't need to send the data. + * (If they don't match, of course, then they've changed and we do indeed + * send the new flattened file to be backed up.) + */ + @Override + public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data, + ParcelFileDescriptor newState) throws IOException { + long savedFileSize = -1; + long savedCrc = -1; + int savedVersion = -1; + + // Extract the previous bookmark file size & CRC from the saved state + DataInputStream in = new DataInputStream( + new FileInputStream(oldState.getFileDescriptor())); + try { + savedFileSize = in.readLong(); + savedCrc = in.readLong(); + savedVersion = in.readInt(); + } catch (EOFException e) { + // It means we had no previous state; that's fine + } + + // Build a flattened representation of the bookmarks table + File tmpfile = File.createTempFile("bkp", null, getCacheDir()); + try { + FileOutputStream outfstream = new FileOutputStream(tmpfile); + long newCrc = buildBookmarkFile(outfstream); + outfstream.close(); + + // Any changes since the last backup? + if ((savedVersion != BACKUP_AGENT_VERSION) + || (newCrc != savedCrc) + || (tmpfile.length() != savedFileSize)) { + // Different checksum or different size, so we need to back it up + copyFileToBackup(BOOKMARK_KEY, tmpfile, data); + } + + // Record our backup state and we're done + writeBackupState(tmpfile.length(), newCrc, newState); + } finally { + // Make sure to tidy up when we're done + tmpfile.delete(); + } + } + + /** + * Restore from backup -- reads in the flattened bookmark file as supplied from + * the backup service, parses that out, and rebuilds the bookmarks table in the + * browser database from it. + */ + @Override + public void onRestore(BackupDataInput data, int appVersionCode, + ParcelFileDescriptor newState) throws IOException { + long crc = -1; + File tmpfile = File.createTempFile("rst", null, getFilesDir()); + try { + while (data.readNextHeader()) { + if (BOOKMARK_KEY.equals(data.getKey())) { + // Read the flattened bookmark data into a temp file + crc = copyBackupToFile(data, tmpfile, data.getDataSize()); + + FileInputStream infstream = new FileInputStream(tmpfile); + DataInputStream in = new DataInputStream(infstream); + + try { + int count = in.readInt(); + ArrayList<Bookmark> bookmarks = new ArrayList<Bookmark>(count); + + // Read all the bookmarks, then process later -- if we can't read + // all the data successfully, we don't touch the bookmarks table + for (int i = 0; i < count; i++) { + Bookmark mark = new Bookmark(); + mark.url = in.readUTF(); + mark.visits = in.readInt(); + mark.date = in.readLong(); + mark.created = in.readLong(); + mark.title = in.readUTF(); + bookmarks.add(mark); + } + + // Okay, we have all the bookmarks -- now see if we need to add + // them to the browser's database + int N = bookmarks.size(); + if (DEBUG) Log.v(TAG, "Restoring " + N + " bookmarks"); + String[] urlCol = new String[] { BookmarkColumns.URL }; + for (int i = 0; i < N; i++) { + Bookmark mark = bookmarks.get(i); + + // Does this URL exist in the bookmark table? + Cursor cursor = getContentResolver().query(Browser.BOOKMARKS_URI, + urlCol, BookmarkColumns.URL + " == '" + mark.url + "' AND " + + BookmarkColumns.BOOKMARK + " == 1 ", null, null); + // if not, insert it + if (cursor.getCount() <= 0) { + 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(), + mark.url, mark.title, false); + } else { + Log.v(TAG, "Skipping extant url: " + mark.url); + } + cursor.close(); + } + } catch (IOException ioe) { + Log.w(TAG, "Bad backup data; not restoring"); + crc = -1; + } + } + + // Last, write the state we just restored from so we can discern + // changes whenever we get invoked for backup in the future + writeBackupState(tmpfile.length(), crc, newState); + } + } finally { + // Whatever happens, delete the temp file + tmpfile.delete(); + } + } + + class Bookmark { + public String url; + public int visits; + public long date; + public long created; + public String title; + } + /* + * Utility functions + */ + + // Flatten the bookmarks table into the given file, calculating its CRC in the process + private long buildBookmarkFile(FileOutputStream outfstream) throws IOException { + CRC32 crc = new CRC32(); + ByteArrayOutputStream bufstream = new ByteArrayOutputStream(512); + DataOutputStream bout = new DataOutputStream(bufstream); + + Cursor cursor = getContentResolver().query(Browser.BOOKMARKS_URI, + new String[] { BookmarkColumns.URL, BookmarkColumns.VISITS, + BookmarkColumns.DATE, BookmarkColumns.CREATED, + BookmarkColumns.TITLE }, + BookmarkColumns.BOOKMARK + " == 1 ", null, null); + + // The first thing in the file is the row count... + int count = cursor.getCount(); + if (DEBUG) Log.v(TAG, "Backing up " + count + " bookmarks"); + bout.writeInt(count); + byte[] record = bufstream.toByteArray(); + crc.update(record); + outfstream.write(record); + + // ... followed by the data for each row + for (int i = 0; i < count; i++) { + cursor.moveToNext(); + + String url = cursor.getString(0); + int visits = cursor.getInt(1); + long date = cursor.getLong(2); + long created = cursor.getLong(3); + String title = cursor.getString(4); + + // construct the flattened record in a byte array + bufstream.reset(); + bout.writeUTF(url); + bout.writeInt(visits); + bout.writeLong(date); + bout.writeLong(created); + bout.writeUTF(title); + + // Update the CRC and write the record to the temp file + record = bufstream.toByteArray(); + crc.update(record); + outfstream.write(record); + + if (DEBUG) Log.v(TAG, " wrote url " + url); + } + + cursor.close(); + return crc.getValue(); + } + + // Write the file to backup as a single record under the given key + private void copyFileToBackup(String key, File file, BackupDataOutput data) + throws IOException { + final int CHUNK = 8192; + byte[] buf = new byte[CHUNK]; + + int toCopy = (int) file.length(); + data.writeEntityHeader(key, toCopy); + + FileInputStream in = new FileInputStream(file); + int nRead; + while (toCopy > 0) { + nRead = in.read(buf, 0, CHUNK); + data.writeEntityData(buf, nRead); + toCopy -= nRead; + } + in.close(); + } + + // Read the given file from backup to a file, calculating a CRC32 along the way + private long copyBackupToFile(BackupDataInput data, File file, int toRead) + throws IOException { + final int CHUNK = 8192; + byte[] buf = new byte[CHUNK]; + CRC32 crc = new CRC32(); + FileOutputStream out = new FileOutputStream(file); + + while (toRead > 0) { + int numRead = data.readEntityData(buf, 0, CHUNK); + crc.update(buf, 0, numRead); + out.write(buf, 0, numRead); + toRead -= numRead; + } + + out.close(); + return crc.getValue(); + } + + // Write the given metrics to the new state file + private void writeBackupState(long fileSize, long crc, ParcelFileDescriptor stateFile) + throws IOException { + DataOutputStream out = new DataOutputStream( + new FileOutputStream(stateFile.getFileDescriptor())); + out.writeLong(fileSize); + out.writeLong(crc); + out.writeInt(BACKUP_AGENT_VERSION); + } +} diff --git a/src/com/android/browser/BrowserBookmarksAdapter.java b/src/com/android/browser/BrowserBookmarksAdapter.java index 27782e0..75be45b 100644 --- a/src/com/android/browser/BrowserBookmarksAdapter.java +++ b/src/com/android/browser/BrowserBookmarksAdapter.java @@ -30,29 +30,27 @@ import android.os.Handler; 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.WebIconDatabase.IconListener; import android.widget.BaseAdapter; +import android.widget.ImageView; +import android.widget.TextView; import java.io.ByteArrayOutputStream; class BrowserBookmarksAdapter extends BaseAdapter { - private final String LOGTAG = "Bookmarks"; - private String mCurrentPage; + private String mCurrentTitle; private Cursor mCursor; private int mCount; - private String mLastWhereClause; - private String[] mLastSelectionArgs; - private String mLastOrderBy; private BrowserBookmarksPage mBookmarksPage; private ContentResolver mContentResolver; - private ChangeObserver mChangeObserver; - private DataSetObserver mDataSetObserver; private boolean mDataValid; + private boolean mGridMode; // When true, this adapter is used to pick a bookmark to create a shortcut private boolean mCreateShortcut; @@ -70,36 +68,37 @@ class BrowserBookmarksAdapter extends BaseAdapter { /** * 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) { - this(b, curPage, false); - } - - /** - * 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, - boolean createShortcut) { + String curTitle, boolean createShortcut) { mDataValid = false; mCreateShortcut = createShortcut; mExtraOffset = createShortcut ? 0 : 1; mBookmarksPage = b; - mCurrentPage = b.getResources().getString(R.string.current_page) + - curPage; + mCurrentPage = b.getResources().getString(R.string.current_page) + + curPage; + mCurrentTitle = curTitle; mContentResolver = b.getContentResolver(); - mLastOrderBy = Browser.BookmarkColumns.CREATED + " DESC"; - mChangeObserver = new ChangeObserver(); - mDataSetObserver = new MyDataSetObserver(); + mGridMode = false; + // FIXME: Should have a default sort order that the user selects. - search(null); + String whereClause = Browser.BookmarkColumns.BOOKMARK + " != 0"; + String orderBy = Browser.BookmarkColumns.VISITS + " DESC"; + 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; + // FIXME: This requires another query of the database after the - // initial search(null). Can we optimize this? + // managedQuery. Can we optimize this? Browser.requestAllIcons(mContentResolver, Browser.BookmarkColumns.FAVICON + " is NULL AND " + Browser.BookmarkColumns.BOOKMARK + " == 1", mIconReceiver); @@ -181,18 +180,7 @@ class BrowserBookmarksAdapter extends BaseAdapter { } mCursor.moveToPosition(position- mExtraOffset); String url = mCursor.getString(Browser.HISTORY_PROJECTION_URL_INDEX); - WebIconDatabase.getInstance().releaseIconForPageUrl(url); - Uri uri = ContentUris.withAppendedId(Browser.BOOKMARKS_URI, mCursor - .getInt(Browser.HISTORY_PROJECTION_ID_INDEX)); - int numVisits = mCursor.getInt(Browser.HISTORY_PROJECTION_VISITS_INDEX); - if (0 == numVisits) { - mContentResolver.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); - mContentResolver.update(uri, values, null, null); - } + Bookmarks.removeFromBookmarks(null, mContentResolver, url); refreshList(); } @@ -253,52 +241,9 @@ class BrowserBookmarksAdapter extends BaseAdapter { * Refresh list to recognize a change in the database. */ public void refreshList() { - // FIXME: consider using requery(). - // Need to do more work to get it to function though. - searchInternal(mLastWhereClause, mLastSelectionArgs, mLastOrderBy); - } - - /** - * Search the database for bookmarks that match the input string. - * @param like String to use to search the database. Strings with spaces - * are treated as having multiple search terms using the - * OR operator. Search both the title and url. - */ - public void search(String like) { - String whereClause = Browser.BookmarkColumns.BOOKMARK + " == 1"; - String[] selectionArgs = null; - if (like != null) { - String[] likes = like.split(" "); - int count = 0; - boolean firstTerm = true; - StringBuilder andClause = new StringBuilder(256); - for (int j = 0; j < likes.length; j++) { - if (likes[j].length() > 0) { - if (firstTerm) { - firstTerm = false; - } else { - andClause.append(" OR "); - } - andClause.append(Browser.BookmarkColumns.TITLE - + " LIKE ? OR " + Browser.BookmarkColumns.URL - + " LIKE ? "); - count += 2; - } - } - if (count > 0) { - selectionArgs = new String[count]; - count = 0; - for (int j = 0; j < likes.length; j++) { - if (likes[j].length() > 0) { - like = "%" + likes[j] + "%"; - selectionArgs[count++] = like; - selectionArgs[count++] = like; - } - } - whereClause += " AND (" + andClause + ")"; - } - } - searchInternal(whereClause, selectionArgs, mLastOrderBy); + mCursor.requery(); + mCount = mCursor.getCount() + mExtraOffset; + notifyDataSetChanged(); } /** @@ -348,46 +293,6 @@ class BrowserBookmarksAdapter extends BaseAdapter { } /** - * This sorts alphabetically, with non-capitalized titles before - * capitalized. - */ - public void sortAlphabetical() { - searchInternal(mLastWhereClause, mLastSelectionArgs, - Browser.BookmarkColumns.TITLE + " COLLATE UNICODE ASC"); - } - - /** - * Internal function used in search, sort, and refreshList. - */ - private void searchInternal(String whereClause, String[] selectionArgs, - String orderBy) { - if (mCursor != null) { - mCursor.unregisterContentObserver(mChangeObserver); - mCursor.unregisterDataSetObserver(mDataSetObserver); - mBookmarksPage.stopManagingCursor(mCursor); - mCursor.deactivate(); - } - - mLastWhereClause = whereClause; - mLastSelectionArgs = selectionArgs; - mLastOrderBy = orderBy; - mCursor = mContentResolver.query( - Browser.BOOKMARKS_URI, - Browser.HISTORY_PROJECTION, - whereClause, - selectionArgs, - orderBy); - mCursor.registerContentObserver(mChangeObserver); - mCursor.registerDataSetObserver(mDataSetObserver); - mBookmarksPage.startManagingCursor(mCursor); - - mDataValid = true; - notifyDataSetChanged(); - - mCount = mCursor.getCount() + mExtraOffset; - } - - /** * How many items should be displayed in the list. * @return Count of items. */ @@ -425,6 +330,20 @@ class BrowserBookmarksAdapter extends BaseAdapter { return position; } + /* package */ void switchViewMode(boolean toGrid) { + mGridMode = toGrid; + } + + /* package */ void populateBookmarkItem(BookmarkItem b, int position) { + mCursor.moveToPosition(position - mExtraOffset); + b.setUrl(mCursor.getString(Browser.HISTORY_PROJECTION_URL_INDEX)); + b.setName(mCursor.getString(Browser.HISTORY_PROJECTION_TITLE_INDEX)); + byte[] data = mCursor.getBlob(Browser.HISTORY_PROJECTION_FAVICON_INDEX); + Bitmap bitmap = (null == data) ? null : + BitmapFactory.decodeByteArray(data, 0, data.length); + b.setFavicon(bitmap); + } + /** * Get a View that displays the data at the specified position * in the list. @@ -440,6 +359,53 @@ class BrowserBookmarksAdapter extends BaseAdapter { throw new AssertionError( "BrowserBookmarksAdapter tried to get a view out of range"); } + if (mGridMode) { + 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); + ImageView fav = (ImageView) convertView.findViewById(R.id.fav); + TextView tv = (TextView) convertView.findViewById(R.id.label); + + if (0 == position && !mCreateShortcut) { + // This is to create a bookmark for the current page. + holder.setVisibility(View.VISIBLE); + fav.setVisibility(View.GONE); + tv.setText(mCurrentTitle); + // FIXME: Want to show the screenshot of the current page + thumb.setImageResource(R.drawable.blank); + return convertView; + } + holder.setVisibility(View.GONE); + mCursor.moveToPosition(position - mExtraOffset); + tv.setText(mCursor.getString( + Browser.HISTORY_PROJECTION_TITLE_INDEX)); + byte[] data = mCursor.getBlob( + Browser.HISTORY_PROJECTION_THUMBNAIL_INDEX); + if (data == null) { + // Backup is to just show white + thumb.setImageResource(R.drawable.blank); + } else { + thumb.setImageBitmap( + BitmapFactory.decodeByteArray(data, 0, data.length)); + } + // Now show the favicon + data = mCursor.getBlob(Browser.HISTORY_PROJECTION_FAVICON_INDEX); + if (data == null) { + fav.setVisibility(View.GONE); + } else { + fav.setVisibility(View.VISIBLE); + fav.setImageBitmap( + BitmapFactory.decodeByteArray(data, 0, data.length)); + } + + return convertView; + + } if (position == 0 && !mCreateShortcut) { AddNewBookmark b; if (convertView instanceof AddNewBookmark) { @@ -450,7 +416,7 @@ class BrowserBookmarksAdapter extends BaseAdapter { b.setUrl(mCurrentPage); return b; } - if (convertView == null || convertView instanceof AddNewBookmark) { + if (convertView == null || !(convertView instanceof BookmarkItem)) { convertView = new BookmarkItem(mBookmarksPage); } bind((BookmarkItem)convertView, position); diff --git a/src/com/android/browser/BrowserBookmarksPage.java b/src/com/android/browser/BrowserBookmarksPage.java index dd34c14..428aa92 100644 --- a/src/com/android/browser/BrowserBookmarksPage.java +++ b/src/com/android/browser/BrowserBookmarksPage.java @@ -36,6 +36,7 @@ import android.text.IClipboard; import android.util.Log; import android.view.ContextMenu; import android.view.KeyEvent; +import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; @@ -43,7 +44,9 @@ import android.view.View; import android.view.ViewGroup; import android.view.ContextMenu.ContextMenuInfo; import android.widget.AdapterView; +import android.widget.GridView; import android.widget.ListView; +import android.widget.Toast; /** * View showing the user's bookmarks in the browser. @@ -51,6 +54,9 @@ import android.widget.ListView; public class BrowserBookmarksPage extends Activity implements View.OnCreateContextMenuListener { + private boolean mGridMode; + private GridView mGridPage; + private View mVerticalList; private BrowserBookmarksAdapter mBookmarksAdapter; private static final int BOOKMARKS_SAVE = 1; private boolean mMaxTabsOpen; @@ -107,7 +113,13 @@ public class BrowserBookmarksPage extends Activity implements 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(); + break; default: return super.onContextItemSelected(item); } @@ -131,25 +143,29 @@ public class BrowserBookmarksPage extends Activity implements ((ViewGroup) mAddHeader.getParent()). removeView(mAddHeader); } - ((AddNewBookmark) i.targetView).copyTo(mAddHeader); + mAddHeader.setUrl(getIntent().getStringExtra("url")); menu.setHeaderView(mAddHeader); return; } menu.setGroupVisible(R.id.ADD_MENU, false); - BookmarkItem b = (BookmarkItem) i.targetView; + if (mMaxTabsOpen) { + 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); } - b.copyTo(mContextHeader); - menu.setHeaderView(mContextHeader); - - if (mMaxTabsOpen) { - menu.findItem(R.id.new_window_context_menu_id).setVisible( - false); + if (mGridMode) { + mBookmarksAdapter.populateBookmarkItem(mContextHeader, + i.position); + } else { + BookmarkItem b = (BookmarkItem) i.targetView; + b.copyTo(mContextHeader); } + menu.setHeaderView(mContextHeader); } /** @@ -159,25 +175,61 @@ public class BrowserBookmarksPage extends Activity implements protected void onCreate(Bundle icicle) { super.onCreate(icicle); - setContentView(R.layout.browser_bookmarks_page); - setTitle(R.string.browser_bookmarks_page_bookmarks_text); - if (Intent.ACTION_CREATE_SHORTCUT.equals(getIntent().getAction())) { mCreateShortcut = true; } - - mBookmarksAdapter = new BrowserBookmarksAdapter(this, - getIntent().getStringExtra("url"), mCreateShortcut); mMaxTabsOpen = getIntent().getBooleanExtra("maxTabsOpen", false); - ListView listView = (ListView) findViewById(R.id.list); - listView.setAdapter(mBookmarksAdapter); - listView.setDrawSelectorOnTop(false); - listView.setVerticalScrollBarEnabled(true); - listView.setOnItemClickListener(mListener); + setTitle(R.string.browser_bookmarks_page_bookmarks_text); + mBookmarksAdapter = new BrowserBookmarksAdapter(this, + getIntent().getStringExtra("url"), + getIntent().getStringExtra("title"), mCreateShortcut); + switchViewMode(true); + } - if (!mCreateShortcut) { - listView.setOnCreateContextMenuListener(this); + /** + * Set the ContentView to be either the grid of thumbnails or the vertical + * list. Pass true to set it to the grid. + */ + private void switchViewMode(boolean gridMode) { + mGridMode = gridMode; + mBookmarksAdapter.switchViewMode(gridMode); + if (mGridMode) { + if (mGridPage == null) { + mGridPage = new GridView(this); + mGridPage.setAdapter(mBookmarksAdapter); + mGridPage.setOnItemClickListener(mListener); + mGridPage.setNumColumns(GridView.AUTO_FIT); + // Keep this in sync with bookmark_thumb and + // BrowserActivity.updateScreenshot + mGridPage.setColumnWidth(100); + mGridPage.setFocusable(true); + mGridPage.setFocusableInTouchMode(true); + mGridPage.setSelector(android.R.drawable.gallery_thumb); + mGridPage.setVerticalSpacing(10); + if (!mCreateShortcut) { + mGridPage.setOnCreateContextMenuListener(this); + } + } + setContentView(mGridPage); + } else { + if (null == mVerticalList) { + LayoutInflater factory = LayoutInflater.from(this); + mVerticalList = factory.inflate(R.layout.browser_bookmarks_page, + null); + + ListView listView + = (ListView) mVerticalList.findViewById(R.id.list); + listView.setAdapter(mBookmarksAdapter); + listView.setDrawSelectorOnTop(false); + listView.setVerticalScrollBarEnabled(true); + listView.setOnItemClickListener(mListener); + + if (!mCreateShortcut) { + listView.setOnCreateContextMenuListener(this); + } + } + setContentView(mVerticalList); } } @@ -196,7 +248,7 @@ public class BrowserBookmarksPage extends Activity implements // 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("browser", "item clicked when dismising"); + android.util.Log.e(LOGTAG, "item clicked when dismissing"); return; } if (!mCreateShortcut) { @@ -294,12 +346,16 @@ public class BrowserBookmarksPage extends Activity implements @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { - case R.id.new_context_menu_id: - saveCurrentPage(); - break; - - default: - return super.onOptionsItemSelected(item); + case R.id.new_context_menu_id: + saveCurrentPage(); + break; + + case R.id.switch_mode_menu_id: + switchViewMode(!mGridMode); + break; + + default: + return super.onOptionsItemSelected(item); } return true; } @@ -370,7 +426,7 @@ public class BrowserBookmarksPage extends Activity implements /** * Refresh the shown list after the database has changed. */ - public void refreshList() { + private void refreshList() { mBookmarksAdapter.refreshList(); } diff --git a/src/com/android/browser/BrowserDownloadAdapter.java b/src/com/android/browser/BrowserDownloadAdapter.java index 38b83fe..16cb982 100644 --- a/src/com/android/browser/BrowserDownloadAdapter.java +++ b/src/com/android/browser/BrowserDownloadAdapter.java @@ -60,14 +60,14 @@ public class BrowserDownloadAdapter extends ResourceCursorAdapter { public BrowserDownloadAdapter(Context context, int layout, Cursor c) { super(context, layout, c); mFilenameColumnId = c.getColumnIndexOrThrow(Downloads._DATA); - mTitleColumnId = c.getColumnIndexOrThrow(Downloads.TITLE); - mDescColumnId = c.getColumnIndexOrThrow(Downloads.DESCRIPTION); - mStatusColumnId = c.getColumnIndexOrThrow(Downloads.STATUS); - mTotalBytesColumnId = c.getColumnIndexOrThrow(Downloads.TOTAL_BYTES); + mTitleColumnId = c.getColumnIndexOrThrow(Downloads.COLUMN_TITLE); + mDescColumnId = c.getColumnIndexOrThrow(Downloads.COLUMN_DESCRIPTION); + mStatusColumnId = c.getColumnIndexOrThrow(Downloads.COLUMN_STATUS); + mTotalBytesColumnId = c.getColumnIndexOrThrow(Downloads.COLUMN_TOTAL_BYTES); mCurrentBytesColumnId = - c.getColumnIndexOrThrow(Downloads.CURRENT_BYTES); - mMimetypeColumnId = c.getColumnIndexOrThrow(Downloads.MIMETYPE); - mDateColumnId = c.getColumnIndexOrThrow(Downloads.LAST_MODIFICATION); + c.getColumnIndexOrThrow(Downloads.COLUMN_CURRENT_BYTES); + mMimetypeColumnId = c.getColumnIndexOrThrow(Downloads.COLUMN_MIME_TYPE); + mDateColumnId = c.getColumnIndexOrThrow(Downloads.COLUMN_LAST_MODIFICATION); } @Override @@ -106,7 +106,7 @@ public class BrowserDownloadAdapter extends ResourceCursorAdapter { // We have a filename, so we can build a title from that title = new File(fullFilename).getName(); ContentValues values = new ContentValues(); - values.put(Downloads.TITLE, title); + values.put(Downloads.COLUMN_TITLE, title); // assume "_id" is the first column for the cursor context.getContentResolver().update( ContentUris.withAppendedId(Downloads.CONTENT_URI, diff --git a/src/com/android/browser/BrowserDownloadPage.java b/src/com/android/browser/BrowserDownloadPage.java index 4397337..22e0e65 100644 --- a/src/com/android/browser/BrowserDownloadPage.java +++ b/src/com/android/browser/BrowserDownloadPage.java @@ -66,34 +66,30 @@ public class BrowserDownloadPage extends Activity setTitle(getText(R.string.download_title)); mListView = (ListView) findViewById(R.id.list); - LayoutInflater factory = LayoutInflater.from(this); - View v = factory.inflate(R.layout.no_downloads, null); - addContentView(v, new LayoutParams(LayoutParams.FILL_PARENT, - LayoutParams.FILL_PARENT)); - mListView.setEmptyView(v); + mListView.setEmptyView(findViewById(R.id.empty)); mDownloadCursor = managedQuery(Downloads.CONTENT_URI, - new String [] {"_id", Downloads.TITLE, Downloads.STATUS, - Downloads.TOTAL_BYTES, Downloads.CURRENT_BYTES, - Downloads._DATA, Downloads.DESCRIPTION, - Downloads.MIMETYPE, Downloads.LAST_MODIFICATION, - Downloads.VISIBILITY}, + new String [] {"_id", Downloads.COLUMN_TITLE, Downloads.COLUMN_STATUS, + Downloads.COLUMN_TOTAL_BYTES, Downloads.COLUMN_CURRENT_BYTES, + Downloads._DATA, Downloads.COLUMN_DESCRIPTION, + Downloads.COLUMN_MIME_TYPE, Downloads.COLUMN_LAST_MODIFICATION, + Downloads.COLUMN_VISIBILITY}, null, null); // only attach everything to the listbox if we can access // the download database. Otherwise, just show it empty if (mDownloadCursor != null) { mStatusColumnId = - mDownloadCursor.getColumnIndexOrThrow(Downloads.STATUS); + mDownloadCursor.getColumnIndexOrThrow(Downloads.COLUMN_STATUS); mIdColumnId = mDownloadCursor.getColumnIndexOrThrow(Downloads._ID); mTitleColumnId = - mDownloadCursor.getColumnIndexOrThrow(Downloads.TITLE); + mDownloadCursor.getColumnIndexOrThrow(Downloads.COLUMN_TITLE); // Create a list "controller" for the data mDownloadAdapter = new BrowserDownloadAdapter(this, R.layout.browser_download_item, mDownloadCursor); - + mListView.setAdapter(mDownloadAdapter); mListView.setScrollBarStyle(View.SCROLLBARS_INSIDE_INSET); mListView.setOnCreateContextMenuListener(this); @@ -403,7 +399,7 @@ public class BrowserDownloadPage extends Activity mDownloadCursor.getColumnIndexOrThrow(Downloads._DATA); String filename = mDownloadCursor.getString(filenameColumnId); int mimetypeColumnId = - mDownloadCursor.getColumnIndexOrThrow(Downloads.MIMETYPE); + mDownloadCursor.getColumnIndexOrThrow(Downloads.COLUMN_MIME_TYPE); String mimetype = mDownloadCursor.getString(mimetypeColumnId); Uri path = Uri.parse(filename); // If there is no scheme, then it must be a file @@ -453,13 +449,13 @@ public class BrowserDownloadPage extends Activity private void hideCompletedDownload() { int status = mDownloadCursor.getInt(mStatusColumnId); - int visibilityColumn = mDownloadCursor.getColumnIndexOrThrow(Downloads.VISIBILITY); + int visibilityColumn = mDownloadCursor.getColumnIndexOrThrow(Downloads.COLUMN_VISIBILITY); int visibility = mDownloadCursor.getInt(visibilityColumn); if (Downloads.isStatusCompleted(status) && visibility == Downloads.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) { ContentValues values = new ContentValues(); - values.put(Downloads.VISIBILITY, Downloads.VISIBILITY_VISIBLE); + values.put(Downloads.COLUMN_VISIBILITY, Downloads.VISIBILITY_VISIBLE); getContentResolver().update( ContentUris.withAppendedId(Downloads.CONTENT_URI, mDownloadCursor.getLong(mIdColumnId)), values, null, null); diff --git a/src/com/android/browser/BrowserHistoryPage.java b/src/com/android/browser/BrowserHistoryPage.java index 42ca848..335d8fe 100644 --- a/src/com/android/browser/BrowserHistoryPage.java +++ b/src/com/android/browser/BrowserHistoryPage.java @@ -41,6 +41,7 @@ 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.DateSorter; import android.webkit.WebIconDatabase.IconListener; import android.widget.AdapterView; @@ -48,6 +49,7 @@ import android.widget.ExpandableListAdapter; import android.widget.ExpandableListView; import android.widget.ExpandableListView.ExpandableListContextMenuInfo; import android.widget.TextView; +import android.widget.Toast; import java.util.List; import java.util.Vector; @@ -60,6 +62,7 @@ public class BrowserHistoryPage extends ExpandableListActivity { private HistoryAdapter mAdapter; private DateSorter mDateSorter; private boolean mMaxTabsOpen; + private HistoryItem mContextHeader; private final static String LOGTAG = "browser"; @@ -110,8 +113,7 @@ public class BrowserHistoryPage extends ExpandableListActivity { setListAdapter(mAdapter); final ExpandableListView list = getExpandableListView(); list.setOnCreateContextMenuListener(this); - LayoutInflater factory = LayoutInflater.from(this); - View v = factory.inflate(R.layout.empty_history, null); + View v = new ViewStub(this, R.layout.empty_history); addContentView(v, new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT)); list.setEmptyView(v); @@ -165,7 +167,7 @@ public class BrowserHistoryPage extends ExpandableListActivity { } return super.onOptionsItemSelected(item); } - + @Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { @@ -180,12 +182,25 @@ public class BrowserHistoryPage extends ExpandableListActivity { MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.historycontext, menu); + HistoryItem historyItem = (HistoryItem) i.targetView; + // Setup the header - menu.setHeaderTitle(((HistoryItem)i.targetView).getUrl()); + if (mContextHeader == null) { + mContextHeader = new HistoryItem(this); + } else if (mContextHeader.getParent() != null) { + ((ViewGroup) mContextHeader.getParent()).removeView(mContextHeader); + } + historyItem.copyTo(mContextHeader); + menu.setHeaderView(mContextHeader); // Only show open in new tab if we have not maxed out available tabs menu.findItem(R.id.new_window_context_menu_id).setVisible(!mMaxTabsOpen); + // For a bookmark, provide the option to remove it from bookmarks + if (historyItem.isBookmark()) { + MenuItem item = menu.findItem(R.id.save_to_bookmarks_menu_id); + item.setTitle(R.string.remove_from_bookmarks); + } // decide whether to show the share link option PackageManager pm = getPackageManager(); Intent send = new Intent(Intent.ACTION_SEND); @@ -200,8 +215,9 @@ public class BrowserHistoryPage extends ExpandableListActivity { public boolean onContextItemSelected(MenuItem item) { ExpandableListContextMenuInfo i = (ExpandableListContextMenuInfo) item.getMenuInfo(); - String url = ((HistoryItem)i.targetView).getUrl(); - String title = ((HistoryItem)i.targetView).getName(); + HistoryItem historyItem = (HistoryItem) i.targetView; + String url = historyItem.getUrl(); + String title = historyItem.getName(); switch (item.getItemId()) { case R.id.open_context_menu_id: loadUrl(url, false); @@ -210,7 +226,12 @@ public class BrowserHistoryPage extends ExpandableListActivity { loadUrl(url, true); return true; case R.id.save_to_bookmarks_menu_id: - Browser.saveBookmark(this, title, url); + if (historyItem.isBookmark()) { + Bookmarks.removeFromBookmarks(this, getContentResolver(), + url); + } else { + Browser.saveBookmark(this, title, url); + } return true; case R.id.share_link_context_menu_id: Browser.sendString(this, url); @@ -222,6 +243,11 @@ public class BrowserHistoryPage extends ExpandableListActivity { Browser.deleteFromHistory(getContentResolver(), url); mAdapter.refreshData(); 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(); + return true; default: break; } @@ -267,18 +293,25 @@ public class BrowserHistoryPage extends ExpandableListActivity { // Array for each of our bins. Each entry represents how many items are // in that bin. - int mItemMap[]; + 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. - int mNumberOfBins; - Vector<DataSetObserver> mObservers; - Cursor mCursor; + private int mNumberOfBins; + private Vector<DataSetObserver> mObservers; + private Cursor mCursor; HistoryAdapter() { mObservers = new Vector<DataSetObserver>(); - String whereClause = Browser.BookmarkColumns.VISITS + " > 0 "; - String orderBy = Browser.BookmarkColumns.DATE + " DESC"; + 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"; mCursor = managedQuery( Browser.BOOKMARKS_URI, @@ -290,6 +323,9 @@ public class BrowserHistoryPage extends ExpandableListActivity { } void refreshData() { + if (mCursor.isClosed()) { + return; + } mCursor.requery(); buildMap(); for (DataSetObserver o : mObservers) { diff --git a/src/com/android/browser/BrowserHomepagePreference.java b/src/com/android/browser/BrowserHomepagePreference.java index d4708c3..7324f24 100644 --- a/src/com/android/browser/BrowserHomepagePreference.java +++ b/src/com/android/browser/BrowserHomepagePreference.java @@ -50,8 +50,8 @@ public class BrowserHomepagePreference extends EditTextPreference implements if (dialog != null) { String url = s.toString(); dialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled( - url.length() == 0 || url.equals("about:blank") || - Regex.WEB_URL_PATTERN.matcher(url).matches()); + url.length() == 0 || + BrowserActivity.ACCEPTED_URI_SCHEMA.matcher(url).matches()); } } diff --git a/src/com/android/browser/BrowserPreferencesPage.java b/src/com/android/browser/BrowserPreferencesPage.java index 3b747d1..2348af0 100644 --- a/src/com/android/browser/BrowserPreferencesPage.java +++ b/src/com/android/browser/BrowserPreferencesPage.java @@ -17,19 +17,29 @@ package com.android.browser; import java.util.List; +import java.util.Vector; +import java.util.Set; +import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.preference.EditTextPreference; +import android.preference.ListPreference; import android.preference.Preference; import android.preference.PreferenceActivity; -import android.webkit.WebView; +import android.preference.PreferenceScreen; +import android.util.Log; +import android.webkit.GeolocationPermissions; import android.webkit.Plugin; +import android.webkit.WebStorage; +import android.webkit.WebView; public class BrowserPreferencesPage extends PreferenceActivity implements Preference.OnPreferenceChangeListener, Preference.OnPreferenceClickListener { + String TAG = "BrowserPreferencesPage"; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -59,13 +69,36 @@ public class BrowserPreferencesPage extends PreferenceActivity e = findPreference(BrowserSettings.PREF_DEFAULT_TEXT_ENCODING); e.setOnPreferenceChangeListener(this); - + if (BrowserSettings.getInstance().showDebugSettings()) { addPreferencesFromResource(R.xml.debug_preferences); } - + e = findPreference(BrowserSettings.PREF_GEARS_SETTINGS); e.setOnPreferenceClickListener(this); + + PreferenceScreen websiteSettings = (PreferenceScreen) + findPreference(BrowserSettings.PREF_WEBSITE_SETTINGS); + Intent intent = new Intent(this, WebsiteSettingsActivity.class); + websiteSettings.setIntent(intent); + } + + /* + * We need to set the PreferenceScreen state in onResume(), as the number of + * origins with active features (WebStorage, Geolocation etc) could have + * changed after calling the WebsiteSettingsActivity. + */ + @Override + protected void onResume() { + super.onResume(); + PreferenceScreen websiteSettings = (PreferenceScreen) + findPreference(BrowserSettings.PREF_WEBSITE_SETTINGS); + Set webStorageOrigins = WebStorage.getInstance().getOrigins(); + Set geolocationOrigins = + GeolocationPermissions.getInstance().getOrigins(); + websiteSettings.setEnabled( + ((webStorageOrigins != null) && !webStorageOrigins.isEmpty()) || + ((geolocationOrigins != null) && !geolocationOrigins.isEmpty())); } @Override diff --git a/src/com/android/browser/BrowserProvider.java b/src/com/android/browser/BrowserProvider.java index a3ccf04..cdab3a3 100644 --- a/src/com/android/browser/BrowserProvider.java +++ b/src/com/android/browser/BrowserProvider.java @@ -19,8 +19,10 @@ package com.android.browser; import com.google.android.providers.GoogleSettings.Partner; import android.app.SearchManager; +import android.backup.BackupManager; import android.content.ComponentName; import android.content.ContentProvider; +import android.content.ContentResolver; import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; @@ -40,6 +42,7 @@ import android.os.Handler; import android.preference.PreferenceManager; import android.provider.Browser; import android.provider.Settings; +import android.provider.Browser.BookmarkColumns; import android.server.search.SearchableInfo; import android.text.TextUtils; import android.text.util.Regex; @@ -54,6 +57,7 @@ import java.util.regex.Pattern; public class BrowserProvider extends ContentProvider { private SQLiteOpenHelper mOpenHelper; + private BackupManager mBackupManager; private static final String sDatabaseName = "browser.db"; private static final String TAG = "BrowserProvider"; private static final String ORDER_BY = "visits DESC, date DESC"; @@ -96,6 +100,8 @@ public class BrowserProvider extends ContentProvider { 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; @@ -143,7 +149,8 @@ public class BrowserProvider extends ContentProvider { // 15 -> 17 Set it up for the SearchManager // 17 -> 18 Added favicon in bookmarks table for Home shortcuts // 18 -> 19 Remove labels table - private static final int DATABASE_VERSION = 19; + // 19 -> 20 Added thumbnail + private static final int DATABASE_VERSION = 20; // Regular expression which matches http://, followed by some stuff, followed by // optionally a trailing slash, all matched as separate groups. @@ -215,7 +222,8 @@ public class BrowserProvider extends ContentProvider { "created LONG," + "description TEXT," + "bookmark INTEGER," + - "favicon BLOB DEFAULT NULL" + + "favicon BLOB DEFAULT NULL," + + "thumbnail BLOB DEFAULT NULL" + ");"); final CharSequence[] bookmarks = mContext.getResources() @@ -242,9 +250,12 @@ public class BrowserProvider extends ContentProvider { @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { Log.w(TAG, "Upgrading database from version " + oldVersion + " to " - + newVersion + ", which will destroy all old data"); + + newVersion); if (oldVersion == 18) { db.execSQL("DROP TABLE IF EXISTS labels"); + } + if (oldVersion <= 19) { + db.execSQL("ALTER TABLE bookmarks ADD COLUMN thumbnail BLOB DEFAULT NULL;"); } else { db.execSQL("DROP TABLE IF EXISTS bookmarks"); db.execSQL("DROP TABLE IF EXISTS searches"); @@ -257,6 +268,7 @@ public class BrowserProvider extends ContentProvider { public boolean onCreate() { final Context context = getContext(); mOpenHelper = new DatabaseHelper(context); + mBackupManager = new BackupManager(context); // we added "picasa web album" into default bookmarks for version 19. // To avoid erasing the bookmark table, we added it explicitly for // version 18 and 19 as in the other cases, we will erase the table. @@ -471,22 +483,22 @@ public class BrowserProvider extends ContentProvider { case SUGGEST_COLUMN_ICON_1_ID: if (mHistoryCount > mPos) { if (mHistoryCursor.getInt(3) == 1) { - return new Integer( + return Integer.valueOf( R.drawable.ic_search_category_bookmark) .toString(); } else { - return new Integer( + return Integer.valueOf( R.drawable.ic_search_category_history) .toString(); } } else { - return new Integer( + return Integer.valueOf( R.drawable.ic_search_category_suggest) .toString(); } case SUGGEST_COLUMN_ICON_2_ID: - return new String("0"); + return "0"; case SUGGEST_COLUMN_QUERY_ID: if (mHistoryCount > mPos) { @@ -637,7 +649,8 @@ public class BrowserProvider extends ContentProvider { myArgs = null; } else { String like = selectionArgs[0] + "%"; - if (selectionArgs[0].startsWith("http")) { + if (selectionArgs[0].startsWith("http") + || selectionArgs[0].startsWith("file")) { myArgs = new String[1]; myArgs[0] = like; suggestSelection = selection; @@ -655,8 +668,7 @@ public class BrowserProvider extends ContentProvider { Cursor c = db.query(TABLE_NAMES[URI_MATCH_BOOKMARKS], SUGGEST_PROJECTION, suggestSelection, myArgs, null, null, - ORDER_BY, - (new Integer(MAX_SUGGESTION_LONG_ENTRIES)).toString()); + ORDER_BY, MAX_SUGGESTION_LONG_ENTRIES_STRING); if (match == URI_MATCH_BOOKMARKS_SUGGEST || Regex.WEB_URL_PATTERN.matcher(selectionArgs[0]).matches()) { @@ -729,6 +741,7 @@ public class BrowserProvider extends ContentProvider { @Override public Uri insert(Uri url, ContentValues initialValues) { + boolean isBookmarkTable = false; SQLiteDatabase db = mOpenHelper.getWritableDatabase(); int match = URI_MATCHER.match(url); @@ -742,6 +755,7 @@ public class BrowserProvider extends ContentProvider { uri = ContentUris.withAppendedId(Browser.BOOKMARKS_URI, rowID); } + isBookmarkTable = true; break; } @@ -764,6 +778,15 @@ public class BrowserProvider extends ContentProvider { throw new IllegalArgumentException("Unknown URL"); } getContext().getContentResolver().notifyChange(uri, null); + + // Back up the new bookmark set if we just inserted one. + // A row created when bookmarks are added from scratch will have + // bookmark=1 in the initial value set. + if (isBookmarkTable + && initialValues.containsKey(BookmarkColumns.BOOKMARK) + && initialValues.getAsInteger(BookmarkColumns.BOOKMARK) != 0) { + mBackupManager.dataChanged(); + } return uri; } @@ -776,20 +799,41 @@ public class BrowserProvider extends ContentProvider { throw new IllegalArgumentException("Unknown URL"); } - if (match == URI_MATCH_BOOKMARKS_ID || match == URI_MATCH_SEARCHES_ID) { + // need to know whether it's the bookmarks table for a couple of reasons + boolean isBookmarkTable = (match == URI_MATCH_BOOKMARKS_ID); + String id = null; + + if (isBookmarkTable || match == URI_MATCH_SEARCHES_ID) { StringBuilder sb = new StringBuilder(); if (where != null && where.length() > 0) { sb.append("( "); sb.append(where); sb.append(" ) AND "); } + id = url.getPathSegments().get(1); sb.append("_id = "); - sb.append(url.getPathSegments().get(1)); + sb.append(id); where = sb.toString(); } + ContentResolver cr = getContext().getContentResolver(); + + // we'lll need to back up the bookmark set if we are about to delete one + if (isBookmarkTable) { + Cursor cursor = cr.query(Browser.BOOKMARKS_URI, + new String[] { BookmarkColumns.BOOKMARK }, + "_id = " + id, null, null); + if (cursor.moveToNext()) { + if (cursor.getInt(0) != 0) { + // yep, this record is a bookmark + mBackupManager.dataChanged(); + } + } + cursor.close(); + } + int count = db.delete(TABLE_NAMES[match % 10], where, whereArgs); - getContext().getContentResolver().notifyChange(url, null); + cr.notifyChange(url, null); return count; } @@ -803,20 +847,59 @@ public class BrowserProvider extends ContentProvider { throw new IllegalArgumentException("Unknown URL"); } - if (match == URI_MATCH_BOOKMARKS_ID || match == URI_MATCH_SEARCHES_ID) { + String id = null; + boolean isBookmarkTable = (match == URI_MATCH_BOOKMARKS_ID); + boolean changingBookmarks = false; + + if (isBookmarkTable || match == URI_MATCH_SEARCHES_ID) { StringBuilder sb = new StringBuilder(); if (where != null && where.length() > 0) { sb.append("( "); sb.append(where); sb.append(" ) AND "); } + id = url.getPathSegments().get(1); sb.append("_id = "); - sb.append(url.getPathSegments().get(1)); + sb.append(id); where = sb.toString(); } + ContentResolver cr = getContext().getContentResolver(); + + // Not all bookmark-table updates should be backed up. Look to see + // whether we changed the title, url, or "is a bookmark" state, and + // request a backup if so. + if (isBookmarkTable) { + // Alterations to the bookmark field inherently change the bookmark + // set, so we don't need to query the record; we know a priori that + // we will need to back up this change. + if (values.containsKey(BookmarkColumns.BOOKMARK)) { + changingBookmarks = true; + } + // changing the title or URL of a bookmark record requires a backup, + // but we don't know wether such an update is on a bookmark without + // querying the record + if (!changingBookmarks && + (values.containsKey(BookmarkColumns.TITLE) + || values.containsKey(BookmarkColumns.URL))) { + // when isBookmarkTable is true, the 'id' var was assigned above + Cursor cursor = cr.query(Browser.BOOKMARKS_URI, + new String[] { BookmarkColumns.BOOKMARK }, + "_id = " + id, null, null); + if (cursor.moveToNext()) { + changingBookmarks = (cursor.getInt(0) != 0); + } + cursor.close(); + } + + // if this *is* a bookmark row we're altering, we need to back it up. + if (changingBookmarks) { + mBackupManager.dataChanged(); + } + } + int ret = db.update(TABLE_NAMES[match % 10], values, where, whereArgs); - getContext().getContentResolver().notifyChange(url, null); + cr.notifyChange(url, null); return ret; } diff --git a/src/com/android/browser/BrowserSettings.java b/src/com/android/browser/BrowserSettings.java index a5e23c9..3ed6cf0 100644 --- a/src/com/android/browser/BrowserSettings.java +++ b/src/com/android/browser/BrowserSettings.java @@ -20,16 +20,22 @@ import com.google.android.providers.GoogleSettings.Partner; import android.content.ContentResolver; import android.content.Context; +import android.content.pm.ActivityInfo; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; +import android.preference.PreferenceActivity; +import android.preference.PreferenceScreen; import android.webkit.CookieManager; +import android.webkit.GeolocationPermissions; import android.webkit.WebView; import android.webkit.WebViewDatabase; import android.webkit.WebIconDatabase; import android.webkit.WebSettings; +import android.webkit.WebStorage; import android.preference.PreferenceManager; import android.provider.Browser; +import java.util.Set; import java.util.HashMap; import java.util.Observable; @@ -48,7 +54,7 @@ import java.util.Observable; */ class BrowserSettings extends Observable { - // Public variables for settings + // Private variables for settings // NOTE: these defaults need to be kept in sync with the XML // until the performance of PreferenceManager.setDefaultValues() // is improved. @@ -65,7 +71,21 @@ class BrowserSettings extends Observable { private String homeUrl = ""; private boolean loginInitialized = false; private boolean autoFitPage = true; + private boolean landscapeOnly = false; private boolean showDebugSettings = false; + private String databasePath; // default value set in loadFromDb() + private boolean databaseEnabled = true; + private long webStorageDefaultQuota = 5 * 1024 * 1024; + // The Browser always enables Application Caches. + private boolean appCacheEnabled = true; + private String appCachePath; // default value set in loadFromDb(). + private long appCacheMaxSize = Long.MAX_VALUE; + private WebStorageSizeManager webStorageSizeManager; + private boolean domStorageEnabled = true; + private String jsFlags = ""; + private boolean geolocationEnabled = true; + + private final static String TAG = "BrowserSettings"; // Development settings public WebSettings.LayoutAlgorithm layoutAlgorithm = @@ -75,6 +95,11 @@ class BrowserSettings extends Observable { private boolean tracing = false; private boolean lightTouch = false; private boolean navDump = false; + + // By default the error console is shown once the user navigates to about:debug. + // The setting can be then toggled from the settings menu. + private boolean showConsole = true; + // Browser only settings private boolean doFlick = false; @@ -97,22 +122,27 @@ class BrowserSettings extends Observable { "privacy_clear_form_data"; public final static String PREF_CLEAR_PASSWORDS = "privacy_clear_passwords"; + public final static String PREF_DEFAULT_QUOTA = + "webstorage_default_quota"; public final static String PREF_EXTRAS_RESET_DEFAULTS = "reset_default_preferences"; public final static String PREF_DEBUG_SETTINGS = "debug_menu"; public final static String PREF_GEARS_SETTINGS = "gears_settings"; + public final static String PREF_WEBSITE_SETTINGS = "website_settings"; public final static String PREF_TEXT_SIZE = "text_size"; public final static String PREF_DEFAULT_ZOOM = "default_zoom"; public final static String PREF_DEFAULT_TEXT_ENCODING = "default_text_encoding"; + public final static String PREF_CLEAR_LOCATION_ACCESS = + "privacy_clear_location_access"; private static final String DESKTOP_USERAGENT = "Mozilla/5.0 (Macintosh; " + - "U; Intel Mac OS X 10_5_5; en-us) AppleWebKit/525.18 (KHTML, " + - "like Gecko) Version/3.1.2 Safari/525.20.1"; + "U; Intel Mac OS X 10_5_7; en-us) AppleWebKit/530.17 (KHTML, " + + "like Gecko) Version/4.0 Safari/530.17"; private static final String IPHONE_USERAGENT = "Mozilla/5.0 (iPhone; U; " + - "CPU iPhone OS 2_2 like Mac OS X; en-us) AppleWebKit/525.18.1 " + - "(KHTML, like Gecko) Version/3.1.1 Mobile/5G77 Safari/525.20"; + "CPU iPhone OS 3_0 like Mac OS X; en-us) AppleWebKit/528.18 " + + "(KHTML, like Gecko) Version/4.0 Mobile/7A341 Safari/528.16"; // Value to truncate strings when adding them to a TextView within // a ListView @@ -157,7 +187,6 @@ class BrowserSettings extends Observable { s.setLoadsImagesAutomatically(b.loadsImagesAutomatically); s.setJavaScriptEnabled(b.javaScriptEnabled); s.setPluginsEnabled(b.pluginsEnabled); - s.setPluginsPath(b.pluginsPath); s.setJavaScriptCanOpenWindowsAutomatically( b.javaScriptCanOpenWindowsAutomatically); s.setDefaultTextEncodingName(b.defaultTextEncodingName); @@ -178,6 +207,24 @@ class BrowserSettings extends Observable { s.setSupportMultipleWindows(true); // Turn off file access s.setAllowFileAccess(false); + + s.setDatabasePath(b.databasePath); + s.setDatabaseEnabled(b.databaseEnabled); + s.setDomStorageEnabled(b.domStorageEnabled); + s.setWebStorageDefaultQuota(b.webStorageDefaultQuota); + + // Turn on Application Caches. + s.setAppCachePath(b.appCachePath); + s.setAppCacheEnabled(b.appCacheEnabled); + s.setAppCacheMaxSize(b.appCacheMaxSize); + + // Enable/Disable the error console. + b.mTabControl.getBrowserActivity().setShouldShowErrorConsole( + b.showDebugSettings && b.showConsole); + + // Configure the Geolocation permissions manager to deny all + // permission requests if Geolocation is disabled in the browser. + // TODO(steveblock): Implement } } @@ -197,6 +244,16 @@ class BrowserSettings extends Observable { // Set the default value for the plugins path to the application's // local directory. pluginsPath = ctx.getDir("plugins", 0).getPath(); + // Set the default value for the Application Caches path. + appCachePath = ctx.getDir("appcache", 0).getPath(); + // Determine the maximum size of the application cache. + webStorageSizeManager = new WebStorageSizeManager( + ctx, + new WebStorageSizeManager.StatFsDiskInfo(appCachePath), + new WebStorageSizeManager.WebKitAppCacheInfo(appCachePath)); + appCacheMaxSize = webStorageSizeManager.getAppCacheMaxSize(); + // Set the default value for the Database path. + databasePath = ctx.getDir("databases", 0).getPath(); homeUrl = getFactoryResetHomeUrl(ctx); @@ -219,6 +276,15 @@ class BrowserSettings extends Observable { pluginsEnabled = p.getBoolean("enable_plugins", pluginsEnabled); pluginsPath = p.getString("plugins_path", pluginsPath); + databasePath = p.getString("database_path", databasePath); + databaseEnabled = p.getBoolean("enable_database", databaseEnabled); + webStorageDefaultQuota = Long.parseLong(p.getString(PREF_DEFAULT_QUOTA, + String.valueOf(webStorageDefaultQuota))); + appCacheEnabled = p.getBoolean("enable_appcache", + appCacheEnabled); + domStorageEnabled = p.getBoolean("enable_domstorage", + domStorageEnabled); + appCachePath = p.getString("appcache_path", appCachePath); javaScriptCanOpenWindowsAutomatically = !p.getBoolean( "block_popup_windows", !javaScriptCanOpenWindowsAutomatically); @@ -238,6 +304,14 @@ class BrowserSettings extends Observable { zoomDensity = WebSettings.ZoomDensity.valueOf( p.getString(PREF_DEFAULT_ZOOM, zoomDensity.name())); autoFitPage = p.getBoolean("autofit_pages", autoFitPage); + boolean landscapeOnlyTemp = + p.getBoolean("landscape_only", landscapeOnly); + if (landscapeOnlyTemp != landscapeOnly) { + landscapeOnly = landscapeOnlyTemp; + mTabControl.getBrowserActivity().setRequestedOrientation( + landscapeOnly ? ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE + : ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED); + } useWideViewPort = true; // use wide view port for either setting if (autoFitPage) { layoutAlgorithm = WebSettings.LayoutAlgorithm.NARROW_COLUMNS; @@ -274,6 +348,21 @@ class BrowserSettings extends Observable { doFlick = p.getBoolean("enable_flick", doFlick); userAgent = Integer.parseInt(p.getString("user_agent", "0")); } + // JS flags is loaded from DB even if showDebugSettings is false, + // so that it can be set once and be effective all the time. + jsFlags = p.getString("js_engine_flags", ""); + + // Read the setting for showing/hiding the JS Console always so that should the + // user enable debug settings, we already know if we should show the console. + // The user will never see the console unless they navigate to about:debug, + // regardless of the setting we read here. This setting is only used after debug + // is enabled. + showConsole = p.getBoolean("javascript_console", showConsole); + mTabControl.getBrowserActivity().setShouldShowErrorConsole( + showDebugSettings && showConsole); + + geolocationEnabled = p.getBoolean("enable_geolocation", geolocationEnabled); + update(); } @@ -285,6 +374,14 @@ class BrowserSettings extends Observable { return homeUrl; } + public String getJsFlags() { + return jsFlags; + } + + public WebStorageSizeManager getWebStorageSizeManager() { + return webStorageSizeManager; + } + public void setHomePage(Context context, String url) { Editor ed = PreferenceManager. getDefaultSharedPreferences(context).edit(); @@ -436,14 +533,39 @@ class BrowserSettings extends Observable { db.clearHttpAuthUsernamePassword(); } - /*package*/ void resetDefaultPreferences(Context context) { + private void maybeDisableWebsiteSettings(Context context) { + Set webStorageOrigins = WebStorage.getInstance().getOrigins(); + Set geolocationOrigins = + GeolocationPermissions.getInstance().getOrigins(); + if (((webStorageOrigins == null) || webStorageOrigins.isEmpty()) && + ((geolocationOrigins == null) || geolocationOrigins.isEmpty())) { + PreferenceActivity activity = (PreferenceActivity) context; + PreferenceScreen screen = (PreferenceScreen) + activity.findPreference(BrowserSettings.PREF_WEBSITE_SETTINGS); + screen.setEnabled(false); + } + } + + /*package*/ void clearDatabases(Context context) { + WebStorage.getInstance().deleteAllData(); + maybeDisableWebsiteSettings(context); + } + + /*package*/ void clearLocationAccess(Context context) { + GeolocationPermissions.getInstance().clearAll(); + maybeDisableWebsiteSettings(context); + } + + /*package*/ void resetDefaultPreferences(Context ctx) { SharedPreferences p = - PreferenceManager.getDefaultSharedPreferences(context); + PreferenceManager.getDefaultSharedPreferences(ctx); p.edit().clear().commit(); - PreferenceManager.setDefaultValues(context, R.xml.browser_preferences, + PreferenceManager.setDefaultValues(ctx, R.xml.browser_preferences, true); // reset homeUrl - setHomePage(context, getFactoryResetHomeUrl(context)); + setHomePage(ctx, getFactoryResetHomeUrl(ctx)); + // reset appcache max size + appCacheMaxSize = webStorageSizeManager.getAppCacheMaxSize(); } private String getFactoryResetHomeUrl(Context context) { diff --git a/src/com/android/browser/BrowserYesNoPreference.java b/src/com/android/browser/BrowserYesNoPreference.java index 65cde71..e380e57 100644 --- a/src/com/android/browser/BrowserYesNoPreference.java +++ b/src/com/android/browser/BrowserYesNoPreference.java @@ -38,6 +38,7 @@ class BrowserYesNoPreference extends YesNoPreference { Context context = getContext(); if (BrowserSettings.PREF_CLEAR_CACHE.equals(getKey())) { BrowserSettings.getInstance().clearCache(context); + BrowserSettings.getInstance().clearDatabases(context); } else if (BrowserSettings.PREF_CLEAR_COOKIES.equals(getKey())) { BrowserSettings.getInstance().clearCookies(context); } else if (BrowserSettings.PREF_CLEAR_HISTORY.equals(getKey())) { @@ -50,6 +51,9 @@ class BrowserYesNoPreference extends YesNoPreference { getKey())) { BrowserSettings.getInstance().resetDefaultPreferences(context); setEnabled(true); + } else if (BrowserSettings.PREF_CLEAR_LOCATION_ACCESS.equals( + getKey())) { + BrowserSettings.getInstance().clearLocationAccess(context); } } } diff --git a/src/com/android/browser/ErrorConsoleView.java b/src/com/android/browser/ErrorConsoleView.java new file mode 100644 index 0000000..56f663b --- /dev/null +++ b/src/com/android/browser/ErrorConsoleView.java @@ -0,0 +1,339 @@ +/* + * 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; + +import android.content.Context; +import android.database.DataSetObserver; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.View.OnClickListener; +import android.webkit.WebView; +import android.widget.Button; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.ListView; +import android.widget.TextView; +import android.widget.TwoLineListItem; + +import java.util.Vector; + +/* package */ class ErrorConsoleView extends LinearLayout { + + /** + * Define some constants to describe the visibility of the error console. + */ + public static final int SHOW_MINIMIZED = 0; + public static final int SHOW_MAXIMIZED = 1; + public static final int SHOW_NONE = 2; + + private TextView mConsoleHeader; + private ErrorConsoleListView mErrorList; + private LinearLayout mEvalJsViewGroup; + private EditText mEvalEditText; + private Button mEvalButton; + private WebView mWebView; + private int mCurrentShowState = SHOW_NONE; + + private boolean mSetupComplete = false; + + // Before we've been asked to display the console, cache any messages that should + // be added to the console. Then when we do display the console, add them to the view + // then. + private Vector<ErrorConsoleMessage> mErrorMessageCache; + + public ErrorConsoleView(Context context) { + super(context); + } + + public ErrorConsoleView(Context context, AttributeSet attributes) { + super(context, attributes); + } + + private void commonSetupIfNeeded() { + if (mSetupComplete) { + return; + } + + LayoutInflater inflater = (LayoutInflater) getContext().getSystemService( + Context.LAYOUT_INFLATER_SERVICE); + inflater.inflate(R.layout.error_console, this); + + // Get references to each ui element. + mConsoleHeader = (TextView) findViewById(R.id.error_console_header_id); + mErrorList = (ErrorConsoleListView) findViewById(R.id.error_console_list_id); + mEvalJsViewGroup = (LinearLayout) findViewById(R.id.error_console_eval_view_group_id); + mEvalEditText = (EditText) findViewById(R.id.error_console_eval_text_id); + mEvalButton = (Button) findViewById(R.id.error_console_eval_button_id); + + mEvalButton.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + // Send the javascript to be evaluated to webkit as a javascript: url + // TODO: Can we expose access to webkit's JS interpreter here and evaluate it that + // way? Note that this is called on the UI thread so we will need to post a message + // to the WebCore thread to implement this. + if (mWebView != null) { + mWebView.loadUrl("javascript:" + mEvalEditText.getText()); + } + + mEvalEditText.setText(""); + } + }); + + // Make clicking on the console title bar min/maximse it. + mConsoleHeader.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + if (mCurrentShowState == SHOW_MINIMIZED) { + showConsole(SHOW_MAXIMIZED); + } else { + showConsole(SHOW_MINIMIZED); + } + } + }); + + // Add any cached messages to the list now that we've assembled the view. + if (mErrorMessageCache != null) { + for (ErrorConsoleMessage msg : mErrorMessageCache) { + mErrorList.addErrorMessage(msg.getMessage(), msg.getSourceID(), msg.getLineNumber()); + } + mErrorMessageCache.clear(); + } + + mSetupComplete = true; + } + + /** + * Adds a message to the set of messages the console uses. + */ + public void addErrorMessage(String msg, String sourceId, int lineNumber) { + if (mSetupComplete) { + mErrorList.addErrorMessage(msg, sourceId, lineNumber); + } else { + if (mErrorMessageCache == null) { + mErrorMessageCache = new Vector<ErrorConsoleMessage>(); + } + mErrorMessageCache.add(new ErrorConsoleMessage(msg, sourceId, lineNumber)); + } + } + + /** + * Removes all error messages from the console. + */ + public void clearErrorMessages() { + if (mSetupComplete) { + mErrorList.clearErrorMessages(); + } else if (mErrorMessageCache != null) { + mErrorMessageCache.clear(); + } + } + + /** + * Returns the current number of errors displayed in the console. + */ + public int numberOfErrors() { + if (mSetupComplete) { + return mErrorList.getCount(); + } else { + return (mErrorMessageCache == null) ? 0 : mErrorMessageCache.size(); + } + } + + /** + * Sets the webview that this console is associated with. Currently this is used so + * we can call into webkit to evaluate JS expressions in the console. + */ + public void setWebView(WebView webview) { + mWebView = webview; + } + + /** + * Sets the visibility state of the console. + */ + public void showConsole(int show_state) { + commonSetupIfNeeded(); + switch (show_state) { + case SHOW_MINIMIZED: + mConsoleHeader.setVisibility(View.VISIBLE); + mConsoleHeader.setText(R.string.error_console_header_text_minimized); + mErrorList.setVisibility(View.GONE); + mEvalJsViewGroup.setVisibility(View.GONE); + break; + + case SHOW_MAXIMIZED: + mConsoleHeader.setVisibility(View.VISIBLE); + mConsoleHeader.setText(R.string.error_console_header_text_maximized); + mErrorList.setVisibility(View.VISIBLE); + mEvalJsViewGroup.setVisibility(View.VISIBLE); + break; + + case SHOW_NONE: + mConsoleHeader.setVisibility(View.GONE); + mErrorList.setVisibility(View.GONE); + mEvalJsViewGroup.setVisibility(View.GONE); + break; + } + mCurrentShowState = show_state; + } + + /** + * Returns the current visibility state of the console. + */ + public int getShowState() { + if (mSetupComplete) { + return mCurrentShowState; + } else { + return SHOW_NONE; + } + } + + /** + * This class extends ListView to implement the View that will actually display the set of + * errors encountered on the current page. + */ + private static class ErrorConsoleListView extends ListView { + // An adapter for this View that contains a list of error messages. + private ErrorConsoleMessageList mConsoleMessages; + + public ErrorConsoleListView(Context context, AttributeSet attributes) { + super(context, attributes); + mConsoleMessages = new ErrorConsoleMessageList(context); + setAdapter(mConsoleMessages); + } + + public void addErrorMessage(String msg, String sourceId, int lineNumber) { + mConsoleMessages.add(msg, sourceId, lineNumber); + setSelection(mConsoleMessages.getCount()); + } + + public void clearErrorMessages() { + mConsoleMessages.clear(); + } + + /** + * This class is an adapter for ErrorConsoleListView that contains the error console + * message data. + */ + private class ErrorConsoleMessageList extends android.widget.BaseAdapter + implements android.widget.ListAdapter { + + private Vector<ErrorConsoleMessage> mMessages; + private LayoutInflater mInflater; + + public ErrorConsoleMessageList(Context context) { + mMessages = new Vector<ErrorConsoleMessage>(); + mInflater = (LayoutInflater)context.getSystemService( + Context.LAYOUT_INFLATER_SERVICE); + } + + /** + * Add a new message to the list and update the View. + */ + public void add(String msg, String sourceID, int lineNumber) { + mMessages.add(new ErrorConsoleMessage(msg, sourceID, lineNumber)); + notifyDataSetChanged(); + } + + /** + * Remove all messages from the list and update the view. + */ + public void clear() { + mMessages.clear(); + notifyDataSetChanged(); + } + + @Override + public boolean areAllItemsEnabled() { + return false; + } + + @Override + public boolean isEnabled(int position) { + return false; + } + + public long getItemId(int position) { + return position; + } + + public Object getItem(int position) { + return mMessages.get(position); + } + + public int getCount() { + return mMessages.size(); + } + + @Override + public boolean hasStableIds() { + return true; + } + + /** + * Constructs a TwoLineListItem for the error at position. + */ + public View getView(int position, View convertView, ViewGroup parent) { + View view; + ErrorConsoleMessage error = mMessages.get(position); + + if (error == null) { + return null; + } + + if (convertView == null) { + view = mInflater.inflate(android.R.layout.two_line_list_item, parent, false); + } else { + view = convertView; + } + + TextView headline = (TextView) view.findViewById(android.R.id.text1); + TextView subText = (TextView) view.findViewById(android.R.id.text2); + headline.setText(error.getSourceID() + ":" + error.getLineNumber()); + subText.setText(error.getMessage()); + return view; + } + + } + } + + /** + * This class holds the data for a single error message in the console. + */ + private static class ErrorConsoleMessage { + private String mMessage; + private String mSourceID; + private int mLineNumber; + + public ErrorConsoleMessage(String msg, String sourceID, int lineNumber) { + mMessage = msg; + mSourceID = sourceID; + mLineNumber = lineNumber; + } + + public String getMessage() { + return mMessage; + } + + public String getSourceID() { + return mSourceID; + } + + public int getLineNumber() { + return mLineNumber; + } + } +} diff --git a/src/com/android/browser/FetchUrlMimeType.java b/src/com/android/browser/FetchUrlMimeType.java index 8578643..c585dbb 100644 --- a/src/com/android/browser/FetchUrlMimeType.java +++ b/src/com/android/browser/FetchUrlMimeType.java @@ -58,7 +58,7 @@ class FetchUrlMimeType extends AsyncTask<ContentValues, String, String> { mValues = values[0]; // Check to make sure we have a URI to download - String uri = mValues.getAsString(Downloads.URI); + String uri = mValues.getAsString(Downloads.COLUMN_URI); if (uri == null || uri.length() == 0) { return null; } @@ -66,15 +66,15 @@ class FetchUrlMimeType extends AsyncTask<ContentValues, String, String> { // User agent is likely to be null, though the AndroidHttpClient // seems ok with that. AndroidHttpClient client = AndroidHttpClient.newInstance( - mValues.getAsString(Downloads.USER_AGENT)); + mValues.getAsString(Downloads.COLUMN_USER_AGENT)); HttpHead request = new HttpHead(uri); - String cookie = mValues.getAsString(Downloads.COOKIE_DATA); + String cookie = mValues.getAsString(Downloads.COLUMN_COOKIE_DATA); if (cookie != null && cookie.length() > 0) { request.addHeader("Cookie", cookie); } - String referer = mValues.getAsString(Downloads.REFERER); + String referer = mValues.getAsString(Downloads.COLUMN_REFERER); if (referer != null && referer.length() > 0) { request.addHeader("Referer", referer); } @@ -111,19 +111,19 @@ class FetchUrlMimeType extends AsyncTask<ContentValues, String, String> { @Override public void onPostExecute(String mimeType) { if (mimeType != null) { - String url = mValues.getAsString(Downloads.URI); + String url = mValues.getAsString(Downloads.COLUMN_URI); if (mimeType.equalsIgnoreCase("text/plain") || mimeType.equalsIgnoreCase("application/octet-stream")) { String newMimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension( MimeTypeMap.getFileExtensionFromUrl(url)); if (newMimeType != null) { - mValues.put(Downloads.MIMETYPE, newMimeType); + mValues.put(Downloads.COLUMN_MIME_TYPE, newMimeType); } } String filename = URLUtil.guessFileName(url, null, mimeType); - mValues.put(Downloads.FILENAME_HINT, filename); + mValues.put(Downloads.COLUMN_FILE_NAME_HINT, filename); } // Start the download diff --git a/src/com/android/browser/FindDialog.java b/src/com/android/browser/FindDialog.java index 6e9574c..2049bd0 100644 --- a/src/com/android/browser/FindDialog.java +++ b/src/com/android/browser/FindDialog.java @@ -42,7 +42,6 @@ import android.widget.TextView; private BrowserActivity mBrowserActivity; // Views with which the user can interact. - private View mOk; private EditText mEditText; private View mNextButton; private View mPrevButton; @@ -129,7 +128,6 @@ import android.widget.TextView; button = findViewById(R.id.done); button.setOnClickListener(mFindCancelListener); - mOk = button; mMatches = (TextView) findViewById(R.id.matches); mMatchesView = findViewById(R.id.matches_view); @@ -143,23 +141,14 @@ import android.widget.TextView; mBrowserActivity.closeFind(); mWebView.clearMatches(); } - + @Override public boolean dispatchKeyEvent(KeyEvent event) { - int code = event.getKeyCode(); - boolean up = event.getAction() == KeyEvent.ACTION_UP; - switch (code) { - case KeyEvent.KEYCODE_DPAD_CENTER: - case KeyEvent.KEYCODE_ENTER: - if (!mEditText.hasFocus()) { - break; - } - if (up) { - findNext(); - } - return true; - default: - break; + if (event.getKeyCode() == KeyEvent.KEYCODE_ENTER + && event.getAction() == KeyEvent.ACTION_UP + && mEditText.hasFocus()) { + findNext(); + return true; } return super.dispatchKeyEvent(event); } diff --git a/src/com/android/browser/HistoryItem.java b/src/com/android/browser/HistoryItem.java index 55e43f0..e8f15b1 100644 --- a/src/com/android/browser/HistoryItem.java +++ b/src/com/android/browser/HistoryItem.java @@ -17,23 +17,13 @@ package com.android.browser; -import android.content.ContentResolver; -import android.content.ContentUris; -import android.content.ContentValues; import android.content.Context; -import android.database.Cursor; import android.graphics.Bitmap; -import android.net.Uri; import android.provider.Browser; -import android.util.Log; import android.view.View; -import android.webkit.WebIconDatabase; import android.widget.CompoundButton; import android.widget.ImageView; import android.widget.TextView; -import android.widget.Toast; - -import java.util.Date; /** * Layout representing a history item in the classic history viewer. @@ -54,61 +44,18 @@ import java.util.Date; mListener = new CompoundButton.OnCheckedChangeListener() { public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { - ContentResolver cr = mContext.getContentResolver(); - Cursor cursor = cr.query( - Browser.BOOKMARKS_URI, - Browser.HISTORY_PROJECTION, - "url = ?", - new String[] { mUrl }, - null); - boolean first = cursor.moveToFirst(); - // Should be in the database no matter what - if (!first) { - throw new AssertionError("URL is not in the database!"); - } if (isChecked) { - // Add to bookmarks - // FIXME: Share code with AddBookmarkPage.java - ContentValues map = new ContentValues(); - map.put(Browser.BookmarkColumns.CREATED, - new Date().getTime()); - map.put(Browser.BookmarkColumns.TITLE, getName()); - map.put(Browser.BookmarkColumns.BOOKMARK, 1); - try { - cr.update(Browser.BOOKMARKS_URI, map, - "_id = " + cursor.getInt(0), null); - } catch (IllegalStateException e) { - Log.e("HistoryItem", "no database!"); - } - WebIconDatabase.getInstance().retainIconForPageUrl(mUrl); - // catch IllegalStateException? - Toast.makeText(mContext, R.string.added_to_bookmarks, - Toast.LENGTH_LONG).show(); + Bookmarks.addBookmark(mContext, + mContext.getContentResolver(), mUrl, getName(), true); } else { - // Remove from bookmarks - // FIXME: This code should be shared with - // BrowserBookmarksAdapter.java - WebIconDatabase.getInstance().releaseIconForPageUrl(mUrl); - Uri uri = ContentUris.withAppendedId(Browser.BOOKMARKS_URI, - cursor.getInt(Browser.HISTORY_PROJECTION_ID_INDEX)); - // 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("HistoryItem", "no database!"); - } - Toast.makeText(mContext, R.string.removed_from_bookmarks, - Toast.LENGTH_LONG).show(); + Bookmarks.removeFromBookmarks(mContext, + mContext.getContentResolver(), mUrl); } - cursor.deactivate(); } }; } - void copyTo(HistoryItem item) { + /* package */ void copyTo(HistoryItem item) { item.mTextView.setText(mTextView.getText()); item.mUrlText.setText(mUrlText.getText()); item.setIsBookmark(mStar.isChecked()); @@ -116,10 +63,17 @@ import java.util.Date; } /** + * Whether or not this item represents a bookmarked site + */ + /* package */ boolean isBookmark() { + return mStar.isChecked(); + } + + /** * Set whether or not this represents a bookmark, and make sure the star * behaves appropriately. */ - void setIsBookmark(boolean isBookmark) { + /* package */ void setIsBookmark(boolean isBookmark) { mStar.setOnCheckedChangeListener(null); mStar.setChecked(isBookmark); mStar.setOnCheckedChangeListener(mListener); diff --git a/src/com/android/browser/MostVisitedActivity.java b/src/com/android/browser/MostVisitedActivity.java index 704ee27..90052d3 100644 --- a/src/com/android/browser/MostVisitedActivity.java +++ b/src/com/android/browser/MostVisitedActivity.java @@ -35,6 +35,7 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.ViewGroup.LayoutParams; +import android.view.ViewStub; import java.util.Vector; @@ -50,8 +51,7 @@ public class MostVisitedActivity extends ListActivity { .addListener(new IconReceiver()); setListAdapter(mAdapter); ListView list = getListView(); - LayoutInflater factory = LayoutInflater.from(this); - View v = factory.inflate(R.layout.empty_history, null); + View v = new ViewStub(this, R.layout.empty_history); addContentView(v, new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT)); list.setEmptyView(v); @@ -84,9 +84,9 @@ public class MostVisitedActivity extends ListActivity { private Vector<DataSetObserver> mObservers; private Cursor mCursor; // These correspond with projection below. - private final int mUrlIndex = 0; - private final int mTitleIndex = 1; - private final int mBookmarkIndex = 2; + private static final int mUrlIndex = 0; + private static final int mTitleIndex = 1; + private static final int mBookmarkIndex = 2; MyAdapter() { mObservers = new Vector<DataSetObserver>(); diff --git a/src/com/android/browser/TabControl.java b/src/com/android/browser/TabControl.java index 575be8d..c7c3e3f 100644 --- a/src/com/android/browser/TabControl.java +++ b/src/com/android/browser/TabControl.java @@ -195,6 +195,8 @@ class TabControl { // url has not changed. private String mOriginalUrl; + private ErrorConsoleView mErrorConsole; + // Construct a new tab private Tab(WebView w, boolean closeOnExit, String appId, String url) { mMainView = w; @@ -388,6 +390,28 @@ class TabControl { } /** + * Return the current tab's error console. Creates the console if createIfNEcessary + * is true and we haven't already created the console. + * @param createIfNecessary Flag to indicate if the console should be created if it has + * not been already. + * @return The current tab's error console, or null if one has not been created and + * createIfNecessary is false. + */ + ErrorConsoleView getCurrentErrorConsole(boolean createIfNecessary) { + Tab t = getTab(mCurrentTab); + if (t == null) { + return null; + } + + if (createIfNecessary && t.mErrorConsole == null) { + t.mErrorConsole = new ErrorConsoleView(mActivity); + t.mErrorConsole.setWebView(t.mMainView); + } + + return t.mErrorConsole; + } + + /** * Return the current tab's top-level WebView. This can return a subwindow * if one exists. * @return The top-level WebView of the current tab. @@ -446,6 +470,9 @@ class TabControl { * @return index of Tab or -1 if not found */ int getTabIndex(Tab tab) { + if (tab == null) { + return -1; + } return mTabs.indexOf(tab); } @@ -681,11 +708,11 @@ class TabControl { return; } - // free the WebView cache - Log.w(LOGTAG, "Free WebView cache"); + // free the WebView's unused memory (this includes the cache) + Log.w(LOGTAG, "Free WebView's unused memory and cache"); WebView view = getCurrentWebView(); if (view != null) { - view.clearCache(false); + view.freeMemory(); } // force a gc System.gc(); @@ -801,6 +828,45 @@ class TabControl { return null; } + // This method checks if a non-app tab (one created within the browser) + // matches the given url. + private boolean tabMatchesUrl(Tab t, String url) { + if (t.mAppId != null) { + return false; + } else if (t.mMainView == null) { + return false; + } else if (url.equals(t.mMainView.getUrl()) || + url.equals(t.mMainView.getOriginalUrl())) { + return true; + } + return false; + } + + /** + * Return the tab that has no app id associated with it and the url of the + * tab matches the given url. + * @param url The url to search for. + */ + Tab findUnusedTabWithUrl(String url) { + if (url == null) { + return null; + } + // Check the current tab first. + Tab t = getCurrentTab(); + if (t != null && tabMatchesUrl(t, url)) { + return t; + } + // Now check all the rest. + final int size = getTabCount(); + for (int i = 0; i < size; i++) { + t = getTab(i); + if (tabMatchesUrl(t, url)) { + return t; + } + } + return null; + } + /** * Recreate the main WebView of the given tab. Returns true if the WebView * was deleted. @@ -865,6 +931,48 @@ class TabControl { return setCurrentTab(newTab, false); } + /*package*/ void pauseCurrentTab() { + Tab t = getCurrentTab(); + if (t != null) { + t.mMainView.onPause(); + if (t.mSubView != null) { + t.mSubView.onPause(); + } + } + } + + /*package*/ void resumeCurrentTab() { + Tab t = getCurrentTab(); + if (t != null) { + t.mMainView.onResume(); + if (t.mSubView != null) { + t.mSubView.onResume(); + } + } + } + + private void putViewInForeground(WebView v, WebViewClient vc, + WebChromeClient cc) { + v.setWebViewClient(vc); + v.setWebChromeClient(cc); + v.setOnCreateContextMenuListener(mActivity); + v.setDownloadListener(mActivity); + v.onResume(); + } + + private void putViewInBackground(WebView v) { + // Set an empty callback so that default actions are not triggered. + v.setWebViewClient(mEmptyClient); + v.setWebChromeClient(mBackgroundChromeClient); + v.setOnCreateContextMenuListener(null); + // Leave the DownloadManager attached so that downloads can start in + // a non-active window. This can happen when going to a site that does + // a redirect after a period of time. The user could have switched to + // another tab while waiting for the download to start. + v.setDownloadListener(mActivity); + v.onPause(); + } + /** * If force is true, this method skips the check for newTab == current. */ @@ -890,7 +998,6 @@ class TabControl { mTabQueue.add(newTab); WebView mainView; - WebView subView; // Display the new current tab mCurrentTab = mTabs.indexOf(newTab); @@ -900,17 +1007,12 @@ class TabControl { // Same work as in createNewTab() except don't do new Tab() newTab.mMainView = mainView = createNewWebView(); } - mainView.setWebViewClient(mActivity.getWebViewClient()); - mainView.setWebChromeClient(mActivity.getWebChromeClient()); - mainView.setOnCreateContextMenuListener(mActivity); - mainView.setDownloadListener(mActivity); + putViewInForeground(mainView, mActivity.getWebViewClient(), + mActivity.getWebChromeClient()); // Add the subwindow if it exists if (newTab.mSubViewContainer != null) { - subView = newTab.mSubView; - subView.setWebViewClient(newTab.mSubViewClient); - subView.setWebChromeClient(newTab.mSubViewChromeClient); - subView.setOnCreateContextMenuListener(mActivity); - subView.setDownloadListener(mActivity); + putViewInForeground(newTab.mSubView, newTab.mSubViewClient, + newTab.mSubViewChromeClient); } if (needRestore) { // Have to finish setCurrentTab work before calling restoreState @@ -925,23 +1027,9 @@ class TabControl { * Put the tab in the background using all the empty/background clients. */ private void putTabInBackground(Tab t) { - WebView mainView = t.mMainView; - // Set an empty callback so that default actions are not triggered. - mainView.setWebViewClient(mEmptyClient); - mainView.setWebChromeClient(mBackgroundChromeClient); - mainView.setOnCreateContextMenuListener(null); - // Leave the DownloadManager attached so that downloads can start in - // a non-active window. This can happen when going to a site that does - // a redirect after a period of time. The user could have switched to - // another tab while waiting for the download to start. - mainView.setDownloadListener(mActivity); - WebView subView = t.mSubView; - if (subView != null) { - // Set an empty callback so that default actions are not triggered. - subView.setWebViewClient(mEmptyClient); - subView.setWebChromeClient(mBackgroundChromeClient); - subView.setOnCreateContextMenuListener(null); - subView.setDownloadListener(mActivity); + putViewInBackground(t.mMainView); + if (t.mSubView != null) { + putViewInBackground(t.mSubView); } } @@ -1041,6 +1129,11 @@ class TabControl { data.mScale = w.getScale(); data.mScrollX = w.getScrollX(); data.mScrollY = w.getScrollY(); + + // Remember the old picture if possible. + if (t.mPickerData != null) { + data.mPicture = t.mPickerData.mPicture; + } t.mPickerData = data; } diff --git a/src/com/android/browser/TitleBar.java b/src/com/android/browser/TitleBar.java new file mode 100644 index 0000000..f534a03 --- /dev/null +++ b/src/com/android/browser/TitleBar.java @@ -0,0 +1,170 @@ +/* + * 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; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.webkit.WebView; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.ProgressBar; +import android.widget.TextView; + +public class TitleBar extends LinearLayout { + private TextView mTitle; + private TextView mUrl; + private ImageView mLftButton; + private Drawable mBookmarkDrawable; + private View mRtButton; + private View mDivider; + private ProgressBar mCircularProgress; + private ProgressBar mHorizontalProgress; + private ImageView mFavicon; + private ImageView mLockIcon; + private boolean mInLoad; + private boolean mTitleSet; + + public TitleBar(Context context) { + this(context, null); + } + + public TitleBar(Context context, AttributeSet attrs) { + super(context, attrs); + LayoutInflater factory = LayoutInflater.from(context); + factory.inflate(R.layout.title_bar, this); + + mTitle = (TextView) findViewById(R.id.title); + mUrl = (TextView) findViewById(R.id.url); + + mLftButton = (ImageView) findViewById(R.id.lft_button); + mRtButton = findViewById(R.id.rt_button); + + mCircularProgress = (ProgressBar) findViewById(R.id.progress_circular); + mHorizontalProgress = (ProgressBar) findViewById( + R.id.progress_horizontal); + mFavicon = (ImageView) findViewById(R.id.favicon); + mLockIcon = (ImageView) findViewById(R.id.lock_icon); + mDivider = findViewById(R.id.divider); + } + + /* package */ void setBrowserActivity(final BrowserActivity activity) { + mLftButton.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + if (mInLoad) { + WebView webView = activity.getTopWindow(); + if (webView != null) { + webView.stopLoading(); + } + } else { + activity.bookmarksOrHistoryPicker(false); + } + } + }); + mRtButton.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + WebView webView = activity.getTopWindow(); + if (webView != null) { + webView.zoomScrollOut(); + } + } + }); + setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + activity.onSearchRequested(); + } + }); + } + + /* package */ void setFavicon(Drawable d) { + mFavicon.setImageDrawable(d); + } + + /* package */ void setLock(Drawable d) { + if (d == null) { + mLockIcon.setVisibility(View.GONE); + } else { + mLockIcon.setImageDrawable(d); + mLockIcon.setVisibility(View.VISIBLE); + } + } + + /* package */ void setProgress(int newProgress) { + if (newProgress == mCircularProgress.getMax()) { + mCircularProgress.setVisibility(View.GONE); + mHorizontalProgress.setVisibility(View.GONE); + mDivider.setVisibility(View.VISIBLE); + mRtButton.setVisibility(View.VISIBLE); + mLftButton.setImageDrawable(mBookmarkDrawable); + mInLoad = false; + if (!mTitleSet) { + mTitle.setText(mUrl.getText()); + mUrl.setText(null); + mTitleSet = true; + } + } else { + mCircularProgress.setProgress(newProgress); + mHorizontalProgress.setProgress(newProgress); + mCircularProgress.setVisibility(View.VISIBLE); + mHorizontalProgress.setVisibility(View.VISIBLE); + mDivider.setVisibility(View.GONE); + mRtButton.setVisibility(View.GONE); + if (mBookmarkDrawable == null) { + // The drawable was assigned in the xml file, so it already + // exists. Keep a pointer to it when we switch to the resource + // so we can easily switch back. + mBookmarkDrawable = mLftButton.getDrawable(); + } + mLftButton.setImageResource( + com.android.internal.R.drawable.ic_menu_stop); + mInLoad = true; + } + } + + /* package */ void setTitleAndUrl(CharSequence title, CharSequence url) { + if (url != null) { + url = BrowserActivity.buildTitleUrl(url.toString()); + } + if (null == title) { + if (mInLoad) { + mTitleSet = false; + mTitle.setText(R.string.title_bar_loading); + } else { + // If the page has no title, put the url in the title space + // and leave the url blank. + mTitle.setText(url); + mUrl.setText(null); + mTitleSet = true; + return; + } + } else { + mTitle.setText(title); + mTitleSet = true; + } + mUrl.setText(url); + } + + /* package */ void setToTabPicker() { + mTitle.setText(R.string.tab_picker_title); + setFavicon(null); + setLock(null); + mCircularProgress.setVisibility(View.GONE); + mHorizontalProgress.setVisibility(View.GONE); + } +} diff --git a/src/com/android/browser/WebStorageSizeManager.java b/src/com/android/browser/WebStorageSizeManager.java new file mode 100644 index 0000000..40d30a2 --- /dev/null +++ b/src/com/android/browser/WebStorageSizeManager.java @@ -0,0 +1,382 @@ +/* + * 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; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.os.StatFs; +import android.util.Log; +import android.webkit.WebStorage; + +import java.io.File; +import java.util.Set; + + +/** + * Package level class for managing the disk size consumed by the WebDatabase + * and ApplicationCaches APIs (henceforth called Web storage). + * + * Currently, the situation on the WebKit side is as follows: + * - WebDatabase enforces a quota for each origin. + * - Session/LocalStorage do not enforce any disk limits. + * - ApplicationCaches enforces a maximum size for all origins. + * + * The WebStorageSizeManager maintains a global limit for the disk space + * consumed by the WebDatabase and ApplicationCaches. As soon as WebKit will + * have a limit for Session/LocalStorage, this class will manage the space used + * by those APIs as well. + * + * The global limit is computed as a function of the size of the partition where + * these APIs store their data (they must store it on the same partition for + * this to work) and the size of the available space on that partition. + * The global limit is not subject to user configuration but we do provide + * a debug-only setting. + * TODO(andreip): implement the debug setting. + * + * The size of the disk space used for Web storage is initially divided between + * WebDatabase and ApplicationCaches as follows: + * + * 75% for WebDatabase + * 25% for ApplicationCaches + * + * When an origin's database usage reaches its current quota, WebKit invokes + * the following callback function: + * - exceededDatabaseQuota(Frame* frame, const String& database_name); + * Note that the default quota for a new origin is 0, so we will receive the + * 'exceededDatabaseQuota' callback before a new origin gets the chance to + * create its first database. + * + * When the total ApplicationCaches usage reaches its current quota, WebKit + * invokes the following callback function: + * - void reachedMaxAppCacheSize(int64_t spaceNeeded); + * + * The WebStorageSizeManager's main job is to respond to the above two callbacks + * by inspecting the amount of unused Web storage quota (i.e. global limit - + * sum of all other origins' quota) and deciding if a quota increase for the + * out-of-space origin is allowed or not. + * + * The default quota for an origin is min(ORIGIN_DEFAULT_QUOTA, unused_quota). + * Quota increases are done in steps, where the increase step is + * min(QUOTA_INCREASE_STEP, unused_quota). + * + * This approach has the drawback that space may remain unused if there + * are many websites that store a lot less content than ORIGIN_DEFAULT_QUOTA. + * We deal with this by picking a value for ORIGIN_DEFAULT_QUOTA that is smaller + * than what the HTML 5 spec recommends. At the same time, picking a very small + * value for ORIGIN_DEFAULT_QUOTA may create performance problems since it's + * more likely for origins to have to rollback and restart transactions as a + * result of reaching the quota more often. + * + * When all the Web storage space is used, the WebStorageSizeManager creates + * a system notification that will guide the user to the WebSettings UI. There, + * the user can free some of the Web storage space by deleting all the data used + * by an origin. + */ +class WebStorageSizeManager { + // Logging flags. + private final static boolean LOGV_ENABLED = com.android.browser.Browser.LOGV_ENABLED; + private final static boolean LOGD_ENABLED = com.android.browser.Browser.LOGD_ENABLED; + private final static String LOGTAG = "browser"; + // The default quota value for an origin. + public final static long ORIGIN_DEFAULT_QUOTA = 3 * 1024 * 1024; // 3MB + // The default value for quota increases. + public final static long QUOTA_INCREASE_STEP = 1 * 1024 * 1024; // 1MB + // Extra padding space for appcache maximum size increases. This is needed + // because WebKit sends us an estimate of the amount of space needed + // but this estimate may, currently, be slightly less than what is actually + // needed. We therefore add some 'padding'. + // TODO(andreip): fix this in WebKit. + public final static long APPCACHE_MAXSIZE_PADDING = 512 * 1024; // 512KB + // The system status bar notification id. + private final static int OUT_OF_SPACE_ID = 1; + // The application context. + private final Context mContext; + // The global Web storage limit. + private final long mGlobalLimit; + // The maximum size of the application cache file. + private long mAppCacheMaxSize; + + /** + * Interface used by the WebStorageSizeManager to obtain information + * about the underlying file system. This functionality is separated + * into its own interface mainly for testing purposes. + */ + public interface DiskInfo { + /** + * @return the size of the free space in the file system. + */ + public long getFreeSpaceSizeBytes(); + + /** + * @return the total size of the file system. + */ + public long getTotalSizeBytes(); + }; + + private DiskInfo mDiskInfo; + // For convenience, we provide a DiskInfo implementation that uses StatFs. + public static class StatFsDiskInfo implements DiskInfo { + private StatFs mFs; + + public StatFsDiskInfo(String path) { + mFs = new StatFs(path); + } + + public long getFreeSpaceSizeBytes() { + return mFs.getAvailableBlocks() * mFs.getBlockSize(); + } + + public long getTotalSizeBytes() { + return mFs.getBlockCount() * mFs.getBlockSize(); + } + }; + + /** + * Interface used by the WebStorageSizeManager to obtain information + * about the appcache file. This functionality is separated into its own + * interface mainly for testing purposes. + */ + public interface AppCacheInfo { + /** + * @return the current size of the appcache file. + */ + public long getAppCacheSizeBytes(); + }; + + // For convenience, we provide an AppCacheInfo implementation. + public static class WebKitAppCacheInfo implements AppCacheInfo { + // The name of the application cache file. Keep in sync with + // WebCore/loader/appcache/ApplicationCacheStorage.cpp + private final static String APPCACHE_FILE = "ApplicationCache.db"; + private String mAppCachePath; + + public WebKitAppCacheInfo(String path) { + mAppCachePath = path; + } + + public long getAppCacheSizeBytes() { + File file = new File(mAppCachePath + + File.separator + + APPCACHE_FILE); + return file.length(); + } + }; + + /** + * Public ctor + * @param ctx is the application context + * @param diskInfo is the DiskInfo instance used to query the file system. + * @param appCacheInfo is the AppCacheInfo used to query info about the + * appcache file. + */ + public WebStorageSizeManager(Context ctx, DiskInfo diskInfo, + AppCacheInfo appCacheInfo) { + mContext = ctx; + mDiskInfo = diskInfo; + mGlobalLimit = getGlobalLimit(); + // The initial max size of the app cache is either 25% of the global + // limit or the current size of the app cache file, whichever is bigger. + mAppCacheMaxSize = Math.max(mGlobalLimit / 4, + appCacheInfo.getAppCacheSizeBytes()); + } + + /** + * Returns the maximum size of the application cache. + */ + public long getAppCacheMaxSize() { + return mAppCacheMaxSize; + } + + /** + * The origin has exceeded its database quota. + * @param url the URL that exceeded the quota + * @param databaseIdentifier the identifier of the database on + * which the transaction that caused the quota overflow was run + * @param currentQuota the current quota for the origin. + * @param totalUsedQuota is the sum of all origins' quota. + * @param quotaUpdater The callback to run when a decision to allow or + * deny quota has been made. Don't forget to call this! + */ + public void onExceededDatabaseQuota(String url, + String databaseIdentifier, long currentQuota, long totalUsedQuota, + WebStorage.QuotaUpdater quotaUpdater) { + if(LOGV_ENABLED) { + Log.v(LOGTAG, + "Received onExceededDatabaseQuota for " + + url + + ":" + + databaseIdentifier + + "(current quota: " + + currentQuota + + ", total used quota: " + + totalUsedQuota + + ")"); + } + long totalUnusedQuota = mGlobalLimit - totalUsedQuota - mAppCacheMaxSize; + + if (totalUnusedQuota <= 0) { + // There definitely isn't any more space. Fire notifications + // if needed and exit. + if (totalUsedQuota > 0) { + // We only fire the notification if there are some other websites + // using some of the quota. This avoids the degenerate case where + // the first ever website to use Web storage tries to use more + // data than it is actually available. In such a case, showing + // the notification would not help at all since there is nothing + // the user can do. + scheduleOutOfSpaceNotification(); + } + quotaUpdater.updateQuota(currentQuota); + if(LOGV_ENABLED) { + Log.v(LOGTAG, "onExceededDatabaseQuota: out of space."); + } + return; + } + // We have enough space inside mGlobalLimit. + long newOriginQuota = currentQuota; + if (newOriginQuota == 0) { + // This is a new origin. It wants an initial quota. + newOriginQuota = + Math.min(ORIGIN_DEFAULT_QUOTA, totalUnusedQuota); + } else { + // This is an origin we have seen before. It wants a quota + // increase. + newOriginQuota += + Math.min(QUOTA_INCREASE_STEP, totalUnusedQuota); + } + quotaUpdater.updateQuota(newOriginQuota); + + if(LOGV_ENABLED) { + Log.v(LOGTAG, "onExceededDatabaseQuota set new quota to " + + newOriginQuota); + } + } + + /** + * The Application Cache has exceeded its max size. + * @param spaceNeeded is the amount of disk space that would be needed + * in order for the last appcache operation to succeed. + * @param totalUsedQuota is the sum of all origins' quota. + * @param quotaUpdater A callback to inform the WebCore thread that a new + * app cache size is available. This callback must always be executed at + * some point to ensure that the sleeping WebCore thread is woken up. + */ + public void onReachedMaxAppCacheSize(long spaceNeeded, long totalUsedQuota, + WebStorage.QuotaUpdater quotaUpdater) { + if(LOGV_ENABLED) { + Log.v(LOGTAG, "Received onReachedMaxAppCacheSize with spaceNeeded " + + spaceNeeded + " bytes."); + } + + long totalUnusedQuota = mGlobalLimit - totalUsedQuota - mAppCacheMaxSize; + + if (totalUnusedQuota < spaceNeeded + APPCACHE_MAXSIZE_PADDING) { + // There definitely isn't any more space. Fire notifications + // if needed and exit. + if (totalUsedQuota > 0) { + // We only fire the notification if there are some other websites + // using some of the quota. This avoids the degenerate case where + // the first ever website to use Web storage tries to use more + // data than it is actually available. In such a case, showing + // the notification would not help at all since there is nothing + // the user can do. + scheduleOutOfSpaceNotification(); + } + quotaUpdater.updateQuota(0); + if(LOGV_ENABLED) { + Log.v(LOGTAG, "onReachedMaxAppCacheSize: out of space."); + } + return; + } + // There is enough space to accommodate spaceNeeded bytes. + mAppCacheMaxSize += spaceNeeded + APPCACHE_MAXSIZE_PADDING; + quotaUpdater.updateQuota(mAppCacheMaxSize); + + if(LOGV_ENABLED) { + Log.v(LOGTAG, "onReachedMaxAppCacheSize set new max size to " + + mAppCacheMaxSize); + } + } + + // Computes the global limit as a function of the size of the data + // partition and the amount of free space on that partition. + private long getGlobalLimit() { + long freeSpace = mDiskInfo.getFreeSpaceSizeBytes(); + long fileSystemSize = mDiskInfo.getTotalSizeBytes(); + return calculateGlobalLimit(fileSystemSize, freeSpace); + } + + /*package*/ static long calculateGlobalLimit(long fileSystemSizeBytes, + long freeSpaceBytes) { + if (fileSystemSizeBytes <= 0 + || freeSpaceBytes <= 0 + || freeSpaceBytes > fileSystemSizeBytes) { + return 0; + } + + long fileSystemSizeRatio = + 2 << ((int) Math.floor(Math.log10( + fileSystemSizeBytes / (1024 * 1024)))); + long maxSizeBytes = (long) Math.min(Math.floor( + fileSystemSizeBytes / fileSystemSizeRatio), + Math.floor(freeSpaceBytes / 2)); + // Round maxSizeBytes up to a multiple of 1024KB (but only if + // maxSizeBytes > 1MB). + long maxSizeStepBytes = 1024 * 1024; + if (maxSizeBytes < maxSizeStepBytes) { + return 0; + } + long roundingExtra = maxSizeBytes % maxSizeStepBytes == 0 ? 0 : 1; + return (maxSizeStepBytes + * ((maxSizeBytes / maxSizeStepBytes) + roundingExtra)); + } + + // Schedules a system notification that takes the user to the WebSettings + // activity when clicked. + private void scheduleOutOfSpaceNotification() { + if(LOGV_ENABLED) { + Log.v(LOGTAG, "scheduleOutOfSpaceNotification called."); + } + if (mContext == null) { + // mContext can be null if we're running unit tests. + return; + } + // setup the notification boilerplate. + int icon = R.drawable.ic_launcher_browser; + CharSequence title = mContext.getString( + R.string.webstorage_outofspace_notification_title); + CharSequence text = mContext.getString( + R.string.webstorage_outofspace_notification_text); + long when = System.currentTimeMillis(); + Intent intent = new Intent(mContext, WebsiteSettingsActivity.class); + PendingIntent contentIntent = + PendingIntent.getActivity(mContext, 0, intent, 0); + Notification notification = new Notification(icon, title, when); + notification.setLatestEventInfo(mContext, title, text, contentIntent); + notification.flags |= Notification.FLAG_AUTO_CANCEL; + // Fire away. + String ns = Context.NOTIFICATION_SERVICE; + NotificationManager mgr = + (NotificationManager) mContext.getSystemService(ns); + if (mgr != null) { + mgr.notify(OUT_OF_SPACE_ID, notification); + } + } +}
\ No newline at end of file diff --git a/src/com/android/browser/WebsiteSettingsActivity.java b/src/com/android/browser/WebsiteSettingsActivity.java new file mode 100644 index 0000000..89e5963 --- /dev/null +++ b/src/com/android/browser/WebsiteSettingsActivity.java @@ -0,0 +1,430 @@ +/* + * 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; + +import android.app.AlertDialog; +import android.app.ListActivity; +import android.content.Context; +import android.content.DialogInterface; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.net.Uri; +import android.os.Bundle; +import android.provider.Browser; +import android.util.Log; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.webkit.GeolocationPermissions; +import android.webkit.WebIconDatabase; +import android.webkit.WebStorage; +import android.widget.ArrayAdapter; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.ImageView; +import android.widget.TextView; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; +import java.util.Vector; + +/** + * Manage the settings for an origin. + * We use it to keep track of the 'HTML5' settings, i.e. database (webstorage) + * and Geolocation. + */ +public class WebsiteSettingsActivity extends ListActivity { + + private String LOGTAG = "WebsiteSettingsActivity"; + private static String sMBStored = null; + private SiteAdapter mAdapter = null; + + class Site { + private String mOrigin; + private String mTitle; + private Bitmap mIcon; + private int mFeatures; + + // These constants provide the set of features that a site may support + // They must be consecutive. To add a new feature, add a new FEATURE_XXX + // variable with value equal to the current value of FEATURE_COUNT, then + // increment FEATURE_COUNT. + private final static int FEATURE_WEB_STORAGE = 0; + private final static int FEATURE_GEOLOCATION = 1; + // The number of features available. + private final static int FEATURE_COUNT = 2; + + public Site(String origin) { + mOrigin = origin; + mTitle = null; + mIcon = null; + mFeatures = 0; + } + + public void addFeature(int feature) { + mFeatures |= (1 << feature); + } + + public boolean hasFeature(int feature) { + return (mFeatures & (1 << feature)) != 0; + } + + /** + * Gets the number of features supported by this site. + */ + public int getFeatureCount() { + int count = 0; + for (int i = 0; i < FEATURE_COUNT; ++i) { + count += hasFeature(i) ? 1 : 0; + } + return count; + } + + /** + * Gets the ID of the nth (zero-based) feature supported by this site. + * The return value is a feature ID - one of the FEATURE_XXX values. + * This is required to determine which feature is displayed at a given + * position in the list of features for this site. This is used both + * when populating the view and when responding to clicks on the list. + */ + public int getFeatureByIndex(int n) { + int j = -1; + for (int i = 0; i < FEATURE_COUNT; ++i) { + j += hasFeature(i) ? 1 : 0; + if (j == n) { + return i; + } + } + return -1; + } + + public String getOrigin() { + return mOrigin; + } + + public void setTitle(String title) { + mTitle = title; + } + + public void setIcon(Bitmap icon) { + mIcon = icon; + } + + public Bitmap getIcon() { + return mIcon; + } + + public String getPrettyOrigin() { + return mTitle == null ? null : hideHttp(mOrigin); + } + + public String getPrettyTitle() { + return mTitle == null ? hideHttp(mOrigin) : mTitle; + } + + private String hideHttp(String str) { + Uri uri = Uri.parse(str); + return "http".equals(uri.getScheme()) ? str.substring(7) : str; + } + } + + class SiteAdapter extends ArrayAdapter<Site> + implements AdapterView.OnItemClickListener { + private int mResource; + private LayoutInflater mInflater; + private Bitmap mDefaultIcon; + private Site mCurrentSite; + + public SiteAdapter(Context context, int rsc) { + super(context, rsc); + mResource = rsc; + mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + mDefaultIcon = BitmapFactory.decodeResource(getResources(), + R.drawable.ic_launcher_shortcut_browser_bookmark); + populateOrigins(); + } + + /** + * Adds the specified feature to the site corresponding to supplied + * origin in the map. Creates the site if it does not already exist. + */ + private void addFeatureToSite(Map sites, String origin, int feature) { + Site site = null; + if (sites.containsKey(origin)) { + site = (Site) sites.get(origin); + } else { + site = new Site(origin); + sites.put(origin, site); + } + site.addFeature(feature); + } + + public void populateOrigins() { + clear(); + + // Get the list of origins we want to display. + // All 'HTML 5 modules' (Database, Geolocation etc) form these + // origin strings using WebCore::SecurityOrigin::toString(), so it's + // safe to group origins here. Note that WebCore::SecurityOrigin + // uses 0 (which is not printed) for the port if the port is the + // default for the protocol. Eg http://www.google.com and + // http://www.google.com:80 both record a port of 0 and hence + // toString() == 'http://www.google.com' for both. + Set origins = WebStorage.getInstance().getOrigins(); + Map sites = new HashMap<String, Site>(); + if (origins != null) { + Iterator<String> iter = origins.iterator(); + while (iter.hasNext()) { + addFeatureToSite(sites, iter.next(), Site.FEATURE_WEB_STORAGE); + } + } + origins = GeolocationPermissions.getInstance().getOrigins(); + if (origins != null) { + Iterator<String> iter = origins.iterator(); + while (iter.hasNext()) { + addFeatureToSite(sites, iter.next(), Site.FEATURE_GEOLOCATION); + } + } + + // Create a map from host to origin. This is used to add metadata + // (title, icon) for this origin from the bookmarks DB. + HashMap hosts = new HashMap<String, Set<Site> >(); + Set keys = sites.keySet(); + Iterator<String> originIter = keys.iterator(); + while (originIter.hasNext()) { + String origin = originIter.next(); + Site site = (Site) sites.get(origin); + String host = Uri.parse(origin).getHost(); + Set hostSites = null; + if (hosts.containsKey(host)) { + hostSites = (Set) hosts.get(host); + } else { + hostSites = new HashSet<Site>(); + hosts.put(host, hostSites); + } + hostSites.add(site); + } + + // Check the bookmark DB. If we have data for a host used by any of + // our origins, use it to set their title and favicon + Cursor c = getContext().getContentResolver().query(Browser.BOOKMARKS_URI, + new String[] { Browser.BookmarkColumns.URL, Browser.BookmarkColumns.TITLE, + Browser.BookmarkColumns.FAVICON }, "bookmark = 1", null, null); + + if ((c != null) && c.moveToFirst()) { + int urlIndex = c.getColumnIndex(Browser.BookmarkColumns.URL); + int titleIndex = c.getColumnIndex(Browser.BookmarkColumns.TITLE); + int faviconIndex = c.getColumnIndex(Browser.BookmarkColumns.FAVICON); + do { + String url = c.getString(urlIndex); + String host = Uri.parse(url).getHost(); + if (hosts.containsKey(host)) { + String title = c.getString(titleIndex); + Bitmap bmp = null; + byte[] data = c.getBlob(faviconIndex); + if (data != null) { + bmp = BitmapFactory.decodeByteArray(data, 0, data.length); + } + Set matchingSites = (Set) hosts.get(host); + Iterator<Site> sitesIter = matchingSites.iterator(); + while (sitesIter.hasNext()) { + Site site = sitesIter.next(); + site.setTitle(title); + if (bmp != null) { + site.setIcon(bmp); + } + } + } + } while (c.moveToNext()); + } + + // We can now simply populate our array with Site instances + keys = sites.keySet(); + originIter = keys.iterator(); + while (originIter.hasNext()) { + String origin = originIter.next(); + Site site = (Site) sites.get(origin); + add(site); + } + + if (getCount() == 0) { + finish(); // we close the screen + } + } + + public int getCount() { + if (mCurrentSite == null) { + return super.getCount(); + } + return mCurrentSite.getFeatureCount(); + } + + public String sizeValueToString(long value) { + float mb = (float) value / (1024.0F * 1024.0F); + int val = (int) (mb * 10); + float ret = (float) (val / 10.0F); + if (ret <= 0) { + return "0"; + } + return String.valueOf(ret); + } + + /* + * If we receive the back event and are displaying + * site's settings, we want to go back to the main + * list view. If not, we just do nothing (see + * dispatchKeyEvent() below). + */ + public boolean backKeyPressed() { + if (mCurrentSite != null) { + mCurrentSite = null; + populateOrigins(); + notifyDataSetChanged(); + return true; + } + return false; + } + + public View getView(int position, View convertView, ViewGroup parent) { + View view; + TextView title; + TextView subtitle; + ImageView icon; + + if (convertView == null) { + view = mInflater.inflate(mResource, parent, false); + } else { + view = convertView; + } + + title = (TextView) view.findViewById(R.id.title); + subtitle = (TextView) view.findViewById(R.id.subtitle); + icon = (ImageView) view.findViewById(R.id.icon); + + if (mCurrentSite == null) { + Site site = getItem(position); + title.setText(site.getPrettyTitle()); + subtitle.setText(site.getPrettyOrigin()); + icon.setVisibility(View.VISIBLE); + Bitmap bmp = site.getIcon(); + if (bmp == null) { + bmp = mDefaultIcon; + } + icon.setImageBitmap(bmp); + // We set the site as the view's tag, + // so that we can get it in onItemClick() + view.setTag(site); + } else { + icon.setVisibility(View.GONE); + String origin = mCurrentSite.getOrigin(); + switch (mCurrentSite.getFeatureByIndex(position)) { + case Site.FEATURE_WEB_STORAGE: + long usageValue = WebStorage.getInstance().getUsageForOrigin(origin); + String usage = sizeValueToString(usageValue) + " " + sMBStored; + + title.setText(R.string.webstorage_clear_data_title); + subtitle.setText(usage); + break; + case Site.FEATURE_GEOLOCATION: + title.setText(R.string.geolocation_settings_page_title); + boolean allowed = GeolocationPermissions.getInstance().getAllowed(origin); + subtitle.setText(allowed ? + R.string.geolocation_settings_page_summary_allowed : + R.string.geolocation_settings_page_summary_not_allowed); + break; + } + } + + return view; + } + + public void onItemClick(AdapterView<?> parent, + View view, + int position, + long id) { + if (mCurrentSite != null) { + switch (mCurrentSite.getFeatureByIndex(position)) { + case Site.FEATURE_WEB_STORAGE: + new AlertDialog.Builder(getContext()) + .setTitle(R.string.webstorage_clear_data_dialog_title) + .setMessage(R.string.webstorage_clear_data_dialog_message) + .setPositiveButton(R.string.webstorage_clear_data_dialog_ok_button, + new AlertDialog.OnClickListener() { + public void onClick(DialogInterface dlg, int which) { + WebStorage.getInstance().deleteOrigin(mCurrentSite.getOrigin()); + mCurrentSite = null; + populateOrigins(); + notifyDataSetChanged(); + }}) + .setNegativeButton(R.string.webstorage_clear_data_dialog_cancel_button, null) + .setIcon(android.R.drawable.ic_dialog_alert) + .show(); + break; + case Site.FEATURE_GEOLOCATION: + new AlertDialog.Builder(getContext()) + .setTitle(R.string.geolocation_settings_page_dialog_title) + .setMessage(R.string.geolocation_settings_page_dialog_message) + .setPositiveButton(R.string.geolocation_settings_page_dialog_ok_button, + new AlertDialog.OnClickListener() { + public void onClick(DialogInterface dlg, int which) { + GeolocationPermissions.getInstance().clear(mCurrentSite.getOrigin()); + mCurrentSite = null; + populateOrigins(); + notifyDataSetChanged(); + }}) + .setNegativeButton(R.string.geolocation_settings_page_dialog_cancel_button, null) + .setIcon(android.R.drawable.ic_dialog_alert) + .show(); + break; + } + } else { + mCurrentSite = (Site) view.getTag(); + notifyDataSetChanged(); + } + } + } + + /** + * Intercepts the back key to immediately notify + * NativeDialog that we are done. + */ + public boolean dispatchKeyEvent(KeyEvent event) { + if ((event.getKeyCode() == KeyEvent.KEYCODE_BACK) + && (event.getAction() == KeyEvent.ACTION_DOWN)) { + if ((mAdapter != null) && (mAdapter.backKeyPressed())){ + return true; // event consumed + } + } + return super.dispatchKeyEvent(event); + } + + @Override + protected void onCreate(Bundle icicle) { + super.onCreate(icicle); + if (sMBStored == null) { + sMBStored = getString(R.string.webstorage_origin_summary_mb_stored); + } + mAdapter = new SiteAdapter(this, R.layout.application); + setListAdapter(mAdapter); + getListView().setOnItemClickListener(mAdapter); + } +} |