Code drop from //branches/cupcake/...@124589
diff --git a/src/com/android/browser/AddBookmarkPage.java b/src/com/android/browser/AddBookmarkPage.java
index 3fe38e8..ea65a46 100644
--- a/src/com/android/browser/AddBookmarkPage.java
+++ b/src/com/android/browser/AddBookmarkPage.java
@@ -114,7 +114,8 @@
*/
boolean save() {
String title = mTitle.getText().toString().trim();
- String unfilteredUrl = mAddress.getText().toString();
+ String unfilteredUrl =
+ BrowserActivity.fixUrl(mAddress.getText().toString());
boolean emptyTitle = title.length() == 0;
boolean emptyUrl = unfilteredUrl.trim().length() == 0;
Resources r = getResources();
diff --git a/src/com/android/browser/BrowserActivity.java b/src/com/android/browser/BrowserActivity.java
index fc4acfc..fe27f8d 100644
--- a/src/com/android/browser/BrowserActivity.java
+++ b/src/com/android/browser/BrowserActivity.java
@@ -62,6 +62,7 @@
import android.net.http.RequestQueue;
import android.net.http.SslCertificate;
import android.net.http.SslError;
+import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.Debug;
@@ -75,13 +76,16 @@
import android.os.ServiceManager;
import android.os.SystemClock;
import android.os.SystemProperties;
-import android.pim.DateFormat;
+import android.preference.PreferenceManager;
import android.provider.Browser;
import android.provider.Checkin;
import android.provider.Contacts.Intents.Insert;
import android.provider.Contacts;
import android.provider.Downloads;
+import android.provider.MediaStore;
import android.text.IClipboard;
+import android.text.TextUtils;
+import android.text.format.DateFormat;
import android.text.util.Regex;
import android.util.Config;
import android.util.Log;
@@ -94,7 +98,6 @@
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
-import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
@@ -142,6 +145,7 @@
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
+import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.Vector;
@@ -179,6 +183,8 @@
private void setupHomePage() {
final Runnable getAccount = new Runnable() {
public void run() {
+ // Lower priority
+ Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
// get the default home page
String homepage = mSettings.getHomePage();
@@ -242,7 +248,9 @@
mGls = IGoogleLoginService.Stub.asInterface(service);
if (done[0] == false) {
done[0] = true;
- new Thread(getAccount).start();
+ Thread account = new Thread(getAccount);
+ account.setName("GLSAccount");
+ account.start();
}
}
public void onServiceDisconnected(ComponentName className) {
@@ -513,6 +521,8 @@
* as there is a limit of 1Mb (see Asset.h)
*/
public void run() {
+ // Lower the priority
+ Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
try {
if (pluginsPath == null) {
Log.e(TAG, "No plugins path found!");
@@ -598,11 +608,24 @@
if (copyPluginsFromAssets.newSystemImage()) {
if (copyPluginsFromAssets.checkIsDifferentVersions()) {
copyPluginsFromAssets.cleanPluginsDirectory();
- new Thread(copyPluginsFromAssets).start();
+ Thread copyplugins = new Thread(copyPluginsFromAssets);
+ copyplugins.setName("CopyPlugins");
+ copyplugins.start();
}
}
}
+ private class ClearThumbnails extends AsyncTask<File, Void, Void> {
+ @Override
+ public Void doInBackground(File... files) {
+ if (files != null) {
+ for (File f : files) {
+ f.delete();
+ }
+ }
+ return null;
+ }
+ }
@Override public void onCreate(Bundle icicle) {
if (Config.LOGV) {
@@ -621,6 +644,9 @@
mResolver = getContentResolver();
+ setBaseSearchUrl(PreferenceManager.getDefaultSharedPreferences(this)
+ .getString("search_url", ""));
+
//
// start MASF proxy service
//
@@ -658,10 +684,19 @@
mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Browser");
if (!mTabControl.restoreState(icicle)) {
+ // clear up the thumbnail directory if we can't restore the state as
+ // none of the files in the directory are referenced any more.
+ new ClearThumbnails().execute(
+ mTabControl.getThumbnailDir().listFiles());
final Intent intent = getIntent();
final Bundle extra = intent.getExtras();
// Create an initial tab.
- final TabControl.Tab t = mTabControl.createNewTab();
+ // If the intent is ACTION_VIEW and data is not null, the Browser is
+ // invoked to view the content by another application. In this case,
+ // the tab will be close when exit.
+ final TabControl.Tab t = mTabControl.createNewTab(
+ Intent.ACTION_VIEW.equals(intent.getAction()) &&
+ intent.getData() != null);
mTabControl.setCurrentTab(t);
// This is one of the only places we call attachTabToContentView
// without animating from the tab picker.
@@ -722,6 +757,15 @@
@Override
protected void onNewIntent(Intent intent) {
+ // When a tab is closed on exit, the current tab index is set to -1.
+ // Reset before proceed as Browser requires the current tab to be set.
+ if (mTabControl.getCurrentIndex() == -1) {
+ TabControl.Tab current = mTabControl.getTab(0);
+ mTabControl.setCurrentTab(current);
+ attachTabToContentView(current);
+ mWebView = current.getWebView();
+ resetTitleAndIcon(mWebView);
+ }
if (mWebView == null) {
return;
}
@@ -734,6 +778,7 @@
}
if (Intent.ACTION_VIEW.equals(action)
|| Intent.ACTION_SEARCH.equals(action)
+ || MediaStore.INTENT_ACTION_MEDIA_SEARCH.equals(action)
|| Intent.ACTION_WEB_SEARCH.equals(action)) {
String url = getUrlFromIntent(intent);
if (url == null || url.length() == 0) {
@@ -742,9 +787,10 @@
if (Intent.ACTION_VIEW.equals(action) &&
(flags & Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT) != 0) {
// if FLAG_ACTIVITY_BROUGHT_TO_FRONT flag is on, the url will be
- // opened in a new tab unless we have reached MAX_TABS and the
- // url will be opened in the current tab
- openTabAndShow(url, null);
+ // 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(url, null, true);
} else {
if ("about:debug".equals(url)) {
mSettings.toggleDebugSettings();
@@ -779,24 +825,39 @@
}
}
} else if (Intent.ACTION_SEARCH.equals(action)
+ || MediaStore.INTENT_ACTION_MEDIA_SEARCH.equals(action)
|| Intent.ACTION_WEB_SEARCH.equals(action)) {
url = intent.getStringExtra(SearchManager.QUERY);
- mLastEnteredUrl = url;
- // Don't add Urls, just search terms.
- // Urls will get added when the page is loaded.
- if (!Regex.WEB_URL_PATTERN.matcher(url).matches()) {
- Browser.updateVisitedHistory(mResolver, url, false);
+ if (url != null) {
+ mLastEnteredUrl = url;
+ // Don't add Urls, just search terms.
+ // Urls will get added when the page is loaded.
+ if (!Regex.WEB_URL_PATTERN.matcher(url).matches()) {
+ Browser.updateVisitedHistory(mResolver, url, false);
+ }
+ // In general, we shouldn't modify URL from Intent.
+ // But currently, we get the user-typed URL from search box as well.
+ url = fixUrl(url);
+ url = smartUrlFilter(url);
+ String searchSource = "&source=android-" + GOOGLE_SEARCH_SOURCE_SUGGEST + "&";
+ if (url.contains(searchSource)) {
+ String source = null;
+ final Bundle appData = intent.getBundleExtra(SearchManager.APP_DATA);
+ if (appData != null) {
+ source = appData.getString(SearchManager.SOURCE);
+ }
+ if (TextUtils.isEmpty(source)) {
+ source = GOOGLE_SEARCH_SOURCE_UNKNOWN;
+ }
+ url = url.replace(searchSource, "&source=android-"+source+"&");
+ }
}
- // In general, we shouldn't modify URL from Intent.
- // But currently, we get the user-typed URL from search box as well.
- url = fixUrl(url);
- url = smartUrlFilter(url);
}
}
return url;
}
- private String fixUrl(String inUrl) {
+ /* package */ static String fixUrl(String inUrl) {
if (inUrl.startsWith("http://") || inUrl.startsWith("https://"))
return inUrl;
if (inUrl.startsWith("http:") ||
@@ -970,7 +1031,7 @@
}
mActivityInPause = true;
- if (!pauseWebView()) {
+ if (mTabControl.getCurrentIndex() >= 0 && !pauseWebView()) {
mWakeLock.acquire();
mHandler.sendMessageDelayed(mHandler
.obtainMessage(RELEASE_WAKELOCK), WAKELOCK_TIMEOUT);
@@ -1057,6 +1118,9 @@
showHttpAuthentication(mHttpAuthHandler, null, null, title,
name, password, focusId);
}
+ if (mFindDialog != null && mFindDialog.isShowing()) {
+ mFindDialog.onConfigurationChanged(newConfig);
+ }
}
@Override public void onLowMemory() {
@@ -1192,6 +1256,11 @@
// options selector, so set mCanChord to true so we can access them.
mCanChord = true;
int id = item.getItemId();
+ final WebView webView = getTopWindow();
+ final HashMap hrefMap = new HashMap();
+ hrefMap.put("webview", webView);
+ final Message msg = mHandler.obtainMessage(
+ FOCUS_NODE_HREF, id, 0, hrefMap);
switch (id) {
// -- Browser context menu
case R.id.open_context_menu_id:
@@ -1200,21 +1269,9 @@
case R.id.save_link_context_menu_id:
case R.id.share_link_context_menu_id:
case R.id.copy_link_context_menu_id:
- Message msg = mHandler.obtainMessage(
- FOCUS_NODE_HREF, id, 0);
- WebView webview = getTopWindow();
- msg.obj = webview;
- webview.requestFocusNodeHref(msg);
+ webView.requestFocusNodeHref(msg);
break;
- case R.id.download_context_menu_id:
- case R.id.view_image_context_menu_id:
- Message m = mHandler.obtainMessage(
- FOCUS_NODE_HREF, id, 0);
- WebView w = getTopWindow();
- m.obj = w;
- w.requestImageRef(m);
- break;
default:
// For other context menus
return onOptionsItemSelected(item);
@@ -1222,18 +1279,34 @@
mCanChord = false;
return true;
}
-
+
+ private Bundle createGoogleSearchSourceBundle(String source) {
+ Bundle bundle = new Bundle();
+ bundle.putString(SearchManager.SOURCE, source);
+ return bundle;
+ }
+
/**
* Overriding this forces the search key to launch global search. The difference
* is the final "true" which requests global search.
*/
@Override
public boolean onSearchRequested() {
- startSearch(null, false, null, true);
+ startSearch(null, false,
+ createGoogleSearchSourceBundle(GOOGLE_SEARCH_SOURCE_SEARCHKEY), true);
return true;
}
@Override
+ public void startSearch(String initialQuery, boolean selectInitialQuery,
+ Bundle appSearchData, boolean globalSearch) {
+ if (appSearchData == null) {
+ appSearchData = createGoogleSearchSourceBundle(GOOGLE_SEARCH_SOURCE_TYPE);
+ }
+ super.startSearch(initialQuery, selectInitialQuery, appSearchData, globalSearch);
+ }
+
+ @Override
public boolean onOptionsItemSelected(MenuItem item) {
if (!mCanChord) {
// The user has already fired a shortcut with this hold down of the
@@ -1244,23 +1317,27 @@
// -- Main menu
case R.id.goto_menu_id: {
String url = getTopWindow().getUrl();
- // TODO: Activities are requested to call onSearchRequested, and to override
- // that function in order to insert custom fields (e.g. the search query).
- startSearch(mSettings.getHomePage().equals(url) ? null : url, true, null, false);
+ startSearch(mSettings.getHomePage().equals(url) ? null : url, true,
+ createGoogleSearchSourceBundle(GOOGLE_SEARCH_SOURCE_GOTO), false);
}
break;
-
+
case R.id.search_menu_id:
// launch using "global" search, which will bring up the Google search box
- onSearchRequested();
+ startSearch(null, false,
+ createGoogleSearchSourceBundle(GOOGLE_SEARCH_SOURCE_SEARCHMENU), true);
break;
-
+
case R.id.bookmarks_menu_id:
bookmarksPicker();
break;
case R.id.windows_menu_id:
- tabPicker(true, mTabControl.getCurrentIndex(), false);
+ if (mTabControl.getTabCount() == 1) {
+ openTabAndShow(mSettings.getHomePage(), null, false);
+ } else {
+ tabPicker(true, mTabControl.getCurrentIndex(), false);
+ }
break;
case R.id.stop_reload_menu_id:
@@ -1299,7 +1376,7 @@
indexToShow--;
}
}
- removeTabAndShow(currentIndex, indexToShow);
+ switchTabs(currentIndex, indexToShow, true);
break;
case R.id.homepage_menu_id:
@@ -1313,39 +1390,21 @@
startActivityForResult(intent, PREFERENCES_PAGE);
break;
-/*
- Disable Find for version 1.0
case R.id.find_menu_id:
if (null == mFindDialog) {
mFindDialog = new FindDialog(this);
- FrameLayout.LayoutParams lp =
- new FrameLayout.LayoutParams(
- ViewGroup.LayoutParams.FILL_PARENT,
- ViewGroup.LayoutParams.WRAP_CONTENT,
- Gravity.BOTTOM);
- mFindDialog.setLayoutParams(lp);
}
mFindDialog.setWebView(getTopWindow());
- mContentView.addView(mFindDialog);
mFindDialog.show();
- Animation anim =AnimationUtils.loadAnimation(this,
- R.anim.find_dialog_enter);
- mFindDialog.startAnimation(anim);
mMenuState = EMPTY_MENU;
break;
-*/
case R.id.page_info_menu_id:
- showPageInfo(mWebView, false);
+ showPageInfo(mTabControl.getCurrentTab(), false);
break;
- case R.id.classic_history_menu_id: {
- Intent i = new Intent(this, BrowserHistoryPage.class);
- i.putExtra("maxTabsOpen",
- mTabControl.getTabCount() >=
- TabControl.MAX_TABS);
- startActivityForResult(i, CLASSIC_HISTORY_PAGE);
- }
+ case R.id.classic_history_menu_id:
+ loadHistory();
break;
case R.id.bookmark_page_menu_id:
@@ -1448,15 +1507,33 @@
case R.id.properties_tab_menu_id:
if (mTabListener != null && mTabOverview != null) {
int pos = mTabOverview.getContextMenuPosition(item);
- TabControl.Tab t = mTabControl.getTab(pos);
- // Use the tab's data for the page info dialog.
- if (t.getWebView() != null) {
- showPageInfo(t.getWebView(), false);
- }
- // FIXME: what should we display if the WebView is null?
+ showPageInfo(mTabControl.getTab(pos), false);
}
break;
+ case R.id.window_one_menu_id:
+ case R.id.window_two_menu_id:
+ case R.id.window_three_menu_id:
+ case R.id.window_four_menu_id:
+ case R.id.window_five_menu_id:
+ case R.id.window_six_menu_id:
+ case R.id.window_seven_menu_id:
+ case R.id.window_eight_menu_id:
+ {
+ int menuid = item.getItemId();
+ for (int id = 0; id < WINDOW_SHORTCUT_ID_ARRAY.length; id++) {
+ if (WINDOW_SHORTCUT_ID_ARRAY[id] == menuid) {
+ TabControl.Tab desiredTab = mTabControl.getTab(id);
+ if (desiredTab != null &&
+ desiredTab != mTabControl.getCurrentTab()) {
+ switchTabs(mTabControl.getCurrentIndex(), id, false);
+ }
+ break;
+ }
+ }
+ }
+ break;
+
default:
if (!super.onOptionsItemSelected(item)) {
return false;
@@ -1468,29 +1545,9 @@
}
public void closeFind() {
- Animation anim = AnimationUtils.loadAnimation(this,
- R.anim.find_dialog_exit);
- mFindDialog.startAnimation(anim);
- mContentView.removeView(mFindDialog);
- getTopWindow().requestFocus();
mMenuState = R.id.MAIN_MENU;
}
- @Override
- public boolean dispatchTouchEvent(MotionEvent event) {
- if (super.dispatchTouchEvent(event)) {
- return true;
- } else {
- // We do not use the Dialog class because it places dialogs in the
- // middle of the screen. It would take care of dismissing find if
- // were using it, but we are doing it manually since we are not.
- if (mFindDialog != null && mFindDialog.hasFocus()) {
- mFindDialog.dismiss();
- }
- return false;
- }
- }
-
@Override public boolean onPrepareOptionsMenu(Menu menu)
{
// This happens when the user begins to hold down the menu key, so
@@ -1533,14 +1590,11 @@
final MenuItem back = menu.findItem(R.id.back_menu_id);
back.setVisible(canGoBack);
back.setEnabled(canGoBack);
- final MenuItem close = menu.findItem(R.id.close_menu_id);
- close.setVisible(!canGoBack);
- close.setEnabled(!canGoBack);
final MenuItem flip =
menu.findItem(R.id.flip_orientation_menu_id);
boolean keyboardClosed =
- getResources().getConfiguration().keyboardHidden ==
- Configuration.KEYBOARDHIDDEN_YES;
+ getResources().getConfiguration().hardKeyboardHidden ==
+ Configuration.HARDKEYBOARDHIDDEN_YES;
flip.setEnabled(keyboardClosed);
boolean isHome = mSettings.getHomePage().equals(w.getUrl());
@@ -1562,6 +1616,19 @@
PackageManager.MATCH_DEFAULT_ONLY);
menu.findItem(R.id.share_page_menu_id).setVisible(
list.size() > 0);
+
+ // Hide the menu+<window number> items
+ // Can't set visibility in menu xml file b/c when a
+ // group is set visible, all items are set visible.
+ for (int i = 0; i < WINDOW_SHORTCUT_ID_ARRAY.length; i++) {
+ menu.findItem(WINDOW_SHORTCUT_ID_ARRAY[i]).setVisible(false);
+ }
+
+ // If there is only 1 window, the text will be "New window"
+ final MenuItem windows = menu.findItem(R.id.windows_menu_id);
+ windows.setTitleCondensed(mTabControl.getTabCount() > 1 ?
+ getString(R.string.view_tabs_condensed) :
+ getString(R.string.tab_picker_new_tab));
boolean isNavDump = mSettings.isNavDump();
final MenuItem nav = menu.findItem(R.id.dump_nav_menu_id);
@@ -1608,26 +1675,22 @@
menu.setGroupVisible(R.id.GEO_MENU,
type == WebView.HitTestResult.GEO_TYPE);
menu.setGroupVisible(R.id.IMAGE_MENU,
- type == WebView.HitTestResult.IMAGE_TYPE ||
- type == WebView.HitTestResult.IMAGE_ANCHOR_TYPE
+ type == WebView.HitTestResult.IMAGE_TYPE
|| type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE);
menu.setGroupVisible(R.id.ANCHOR_MENU,
- type == WebView.HitTestResult.ANCHOR_TYPE ||
- type == WebView.HitTestResult.IMAGE_ANCHOR_TYPE
- || type == WebView.HitTestResult.SRC_ANCHOR_TYPE
+ type == WebView.HitTestResult.SRC_ANCHOR_TYPE
|| type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE);
// Setup custom handling depending on the type
switch (type) {
case WebView.HitTestResult.PHONE_TYPE:
- menu.setHeaderTitle(extra);
+ menu.setHeaderTitle(Uri.decode(extra));
menu.findItem(R.id.dial_context_menu_id).setIntent(
new Intent(Intent.ACTION_VIEW, Uri
.parse(WebView.SCHEME_TEL + extra)));
- Intent addIntent = new Intent(Intent.ACTION_INSERT,
- Contacts.People.CONTENT_URI);
- addIntent.putExtra(Insert.FULL_MODE, true);
- addIntent.putExtra(Insert.PHONE, extra);
+ Intent addIntent = new Intent(Intent.ACTION_INSERT_OR_EDIT);
+ addIntent.putExtra(Insert.PHONE, Uri.decode(extra));
+ addIntent.setType(Contacts.People.CONTENT_ITEM_TYPE);
menu.findItem(R.id.add_contact_context_menu_id).setIntent(
addIntent);
menu.findItem(R.id.copy_phone_context_menu_id).setOnMenuItemClickListener(
@@ -1653,35 +1716,16 @@
new Copy(extra));
break;
- case WebView.HitTestResult.ANCHOR_TYPE:
- case WebView.HitTestResult.IMAGE_ANCHOR_TYPE:
case WebView.HitTestResult.SRC_ANCHOR_TYPE:
case WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE:
- mTitleView = (TextView) LayoutInflater.from(this)
+ TextView titleView = (TextView) LayoutInflater.from(this)
.inflate(android.R.layout.browser_link_context_header,
null);
- menu.setHeaderView(mTitleView);
+ titleView.setText(extra);
+ menu.setHeaderView(titleView);
// decide whether to show the open link in new tab option
menu.findItem(R.id.open_newtab_context_menu_id).setVisible(
mTabControl.getTabCount() < TabControl.MAX_TABS);
- if (type == WebView.HitTestResult.ANCHOR_TYPE
- || type == WebView.HitTestResult.IMAGE_ANCHOR_TYPE){
- menu.findItem(R.id.bookmark_context_menu_id).setVisible(
- false);
- menu.findItem(R.id.save_link_context_menu_id).setVisible(
- false);
- menu.findItem(R.id.copy_link_context_menu_id).setVisible(
- false);
- menu.findItem(R.id.share_link_context_menu_id).setVisible(
- false);
- mTitleView.setText(R.string.contextmenu_javascript);
- break;
- }
- Message headerMessage = mHandler.obtainMessage(FOCUS_NODE_HREF,
- HEADER_FLAG, 0);
- headerMessage.obj = webview;
- webview.requestFocusNodeHref(headerMessage);
- // decide whether to show the share link option
PackageManager pm = getPackageManager();
Intent send = new Intent(Intent.ACTION_SEND);
send.setType("text/plain");
@@ -1689,12 +1733,14 @@
PackageManager.MATCH_DEFAULT_ONLY);
menu.findItem(R.id.share_link_context_menu_id).setVisible(
list.size() > 0);
- if (type == WebView.HitTestResult.ANCHOR_TYPE) {
- break;
- }
- //fall through
+ break;
case WebView.HitTestResult.IMAGE_TYPE:
+ menu.setHeaderTitle(extra);
+ menu.findItem(R.id.view_image_context_menu_id).setIntent(
+ new Intent(Intent.ACTION_VIEW, Uri.parse(extra)));
+ menu.findItem(R.id.download_context_menu_id).
+ setOnMenuItemClickListener(new Download(extra));
break;
default:
@@ -1793,10 +1839,10 @@
// Increment the count to indicate that we are in an animation.
mAnimationCount++;
// Remove the listener so we don't get any more tab changes.
- if (mTabOverview != null) {
- mTabOverview.setListener(null);
- }
+ mTabOverview.setListener(null);
mTabListener = null;
+ // Make the menu empty until the animation completes.
+ mMenuState = EMPTY_MENU;
}
@@ -1828,7 +1874,8 @@
// the given Message. If the tab overview is already showing (i.e. this
// method is called from TabListener.onClick(), the method will animate
// away from the tab overview.
- private void openTabAndShow(String url, final Message msg) {
+ private void openTabAndShow(String url, final Message msg,
+ boolean closeOnExit) {
final boolean newTab = mTabControl.getTabCount() != TabControl.MAX_TABS;
final TabControl.Tab currentTab = mTabControl.getCurrentTab();
if (newTab) {
@@ -1856,7 +1903,7 @@
}
// Animate from the Tab overview after any animations have
// finished.
- sendAnimateFromOverview(mTabControl.createNewTab(),
+ sendAnimateFromOverview(mTabControl.createNewTab(closeOnExit),
true, url, delay, msg);
}
} else if (url != null) {
@@ -1900,10 +1947,6 @@
// be displayed to the user.
private void animateToTabOverview(final int newIndex, final boolean remove,
final AnimatingView view) {
- if (mTabOverview == null) {
- return;
- }
-
// Find the view in the ImageGrid allowing for the "New Tab" cell.
int position = mTabControl.getTabIndex(view.mTab);
if (!((ImageAdapter) mTabOverview.getAdapter()).maxedOut()) {
@@ -1920,12 +1963,10 @@
final Animation.AnimationListener l =
new Animation.AnimationListener() {
public void onAnimationStart(Animation a) {
- if (mTabOverview != null) {
- mTabOverview.requestFocus();
- // Clear the listener so we don't trigger a tab
- // selection.
- mTabOverview.setListener(null);
- }
+ mTabOverview.requestFocus();
+ // Clear the listener so we don't trigger a tab
+ // selection.
+ mTabOverview.setListener(null);
}
public void onAnimationRepeat(Animation a) {}
public void onAnimationEnd(Animation a) {
@@ -1940,17 +1981,15 @@
public void run() {
// Remove the AnimatingView.
mContentView.removeView(view);
- if (mTabOverview != null) {
- // Make newIndex visible.
- mTabOverview.setCurrentIndex(newIndex);
- // Restore the listener.
- mTabOverview.setListener(mTabListener);
- // Change the menu to TAB_MENU if the
- // ImageGrid is interactive.
- if (mTabOverview.isLive()) {
- mMenuState = R.id.TAB_MENU;
- mTabOverview.requestFocus();
- }
+ // Make newIndex visible.
+ mTabOverview.setCurrentIndex(newIndex);
+ // Restore the listener.
+ mTabOverview.setListener(mTabListener);
+ // Change the menu to TAB_MENU if the
+ // ImageGrid is interactive.
+ if (mTabOverview.isLive()) {
+ mMenuState = R.id.TAB_MENU;
+ mTabOverview.requestFocus();
}
// If a remove was requested, remove the tab.
if (remove) {
@@ -1968,12 +2007,10 @@
if (currentTab != tab) {
mTabControl.setCurrentTab(currentTab);
}
- if (mTabOverview != null) {
- mTabOverview.remove(newIndex);
- // Make the current tab visible.
- mTabOverview.setCurrentIndex(
- mTabControl.getCurrentIndex());
- }
+ mTabOverview.remove(newIndex);
+ // Make the current tab visible.
+ mTabOverview.setCurrentIndex(
+ mTabControl.getCurrentIndex());
}
}
});
@@ -1999,11 +2036,6 @@
// from.
private void animateFromTabOverview(final AnimatingView view,
final boolean newTab, final String url, final Message msg) {
- // mTabOverview may have been dismissed
- if (mTabOverview == null) {
- return;
- }
-
// firstVisible is the first visible tab on the screen. This helps
// to know which corner of the screen the selected tab is.
int firstVisible = mTabOverview.getFirstVisiblePosition();
@@ -2025,32 +2057,19 @@
// Find the view at this location.
final View v = mTabOverview.getChildAt(location);
- // Use a delay of 1 second in case we get a bad position
- long delay = 1000;
- boolean fade = false;
-
// Wait until the animation completes to load the url.
final Animation.AnimationListener l =
new Animation.AnimationListener() {
public void onAnimationStart(Animation a) {}
public void onAnimationRepeat(Animation a) {}
public void onAnimationEnd(Animation a) {
- // The animation is done so allow key events and other
- // animations to begin.
- mAnimationCount--;
mHandler.post(new Runnable() {
public void run() {
- if (v != null) {
- mContentView.removeView(view);
- mWebView.setVisibility(View.VISIBLE);
- // Make the sub window container visible if
- // there is one.
- if (mTabControl.getCurrentSubWindow() != null) {
- mTabControl.getCurrentTab()
- .getSubWebViewContainer()
- .setVisibility(View.VISIBLE);
- }
- }
+ mContentView.removeView(view);
+ // Dismiss the tab overview. If the cell at the
+ // given location is null, set the fade
+ // parameter to true.
+ dismissTabOverview(v == null);
if (url != null) {
// Dismiss the subwindow if one exists.
dismissSubWindow(
@@ -2065,6 +2084,12 @@
if (msg != null) {
msg.sendToTarget();
}
+ // The animation is done and the tab overview is
+ // gone so allow key events and other animations
+ // to begin.
+ mAnimationCount--;
+ // Reset all the title bar info.
+ resetTitle();
}
});
}
@@ -2077,32 +2102,42 @@
view.startAnimation(anim);
// Make the view VISIBLE during the animation.
view.setVisibility(View.VISIBLE);
- // Dismiss the tab overview after the animation completes.
- delay = anim.getDuration();
} else {
- // dismiss mTabOverview and have it fade out just in case we get a
- // bad location.
- fade = true;
// Go ahead and load the url.
l.onAnimationEnd(null);
}
- // Reset all the title bar info.
- resetTitle();
- // Dismiss the tab overview either after the animation or after a
- // second.
- mHandler.sendMessageDelayed(mHandler.obtainMessage(
- DISMISS_TAB_OVERVIEW, fade ? 1 : 0, 0), delay);
+ }
+
+ // Dismiss the tab overview applying a fade if needed.
+ private void dismissTabOverview(final boolean fade) {
+ if (fade) {
+ AlphaAnimation anim = new AlphaAnimation(1.0f, 0.0f);
+ anim.setDuration(500);
+ anim.startNow();
+ mTabOverview.startAnimation(anim);
+ }
+ // Just in case there was a problem with animating away from the tab
+ // overview
+ mWebView.setVisibility(View.VISIBLE);
+ // Make the sub window container visible.
+ if (mTabControl.getCurrentSubWindow() != null) {
+ mTabControl.getCurrentTab().getSubWebViewContainer()
+ .setVisibility(View.VISIBLE);
+ }
+ mContentView.removeView(mTabOverview);
+ mTabOverview.clear();
+ mTabOverview = null;
+ mTabListener = null;
}
private void openTab(String url) {
if (mSettings.openInBackground()) {
- TabControl.Tab t = mTabControl.createNewTab();
+ TabControl.Tab t = mTabControl.createNewTab(false);
if (t != null) {
- WebView w = t.getWebView();
- w.loadUrl(url);
+ t.getWebView().loadUrl(url);
}
} else {
- openTabAndShow(url, null);
+ openTabAndShow(url, null, false);
}
}
@@ -2118,6 +2153,19 @@
mText = toCopy;
}
}
+
+ private class Download implements OnMenuItemClickListener {
+ private String mText;
+
+ public boolean onMenuItemClick(MenuItem item) {
+ onDownloadStartNoStream(mText, null, null, null, -1);
+ return true;
+ }
+
+ public Download(String toDownload) {
+ mText = toDownload;
+ }
+ }
private void copy(CharSequence text) {
try {
@@ -2184,7 +2232,11 @@
mUrl = url;
mTitle = title;
- setTitle(buildUrlTitle(url, title));
+ // While the tab overview is animating or being shown, block changes
+ // to the title.
+ if (mAnimationCount == 0 && mTabOverview == null) {
+ setTitle(buildUrlTitle(url, title));
+ }
}
/**
@@ -2255,6 +2307,11 @@
// Set the favicon in the title bar.
private void setFavicon(Bitmap icon) {
+ // While the tab overview is animating or being shown, block changes to
+ // the favicon.
+ if (mAnimationCount > 0 || mTabOverview != null) {
+ return;
+ }
Drawable[] array = new Drawable[2];
PaintDrawable p = new PaintDrawable(Color.WHITE);
p.setCornerRadius(3f);
@@ -2294,11 +2351,11 @@
updateLockIconImage(mLockIconType);
}
- private void removeTabAndShow(int indexToRemove, int indexToShow) {
+ private void switchTabs(int indexFrom, int indexToShow, boolean remove) {
int delay = TAB_ANIMATION_DURATION + TAB_OVERVIEW_DELAY;
// Animate to the tab picker, remove the current tab, then
// animate away from the tab picker to the parent WebView.
- tabPicker(false, indexToRemove, true);
+ tabPicker(false, indexFrom, remove);
// Change to the parent tab
final TabControl.Tab tab = mTabControl.getTab(indexToShow);
if (tab != null) {
@@ -2315,13 +2372,25 @@
if (mWebView.canGoBack()) {
mWebView.goBack();
} else {
+ TabControl.Tab self = mTabControl.getCurrentTab();
// Check to see if we are closing a window that was created by
// another window. If so, we switch back to that window.
- TabControl.Tab parent = mTabControl.getCurrentTab().getParentTab();
+ TabControl.Tab parent = self.getParentTab();
if (parent != null) {
- removeTabAndShow(mTabControl.getCurrentIndex(),
- mTabControl.getTabIndex(parent));
+ switchTabs(mTabControl.getCurrentIndex(),
+ mTabControl.getTabIndex(parent), true);
} else {
+ if (self.closeOnExit()) {
+ if (mTabControl.getTabCount() == 1) {
+ finish();
+ return;
+ }
+ // call pauseWebView() now, we won't be able to call it in
+ // onPause() as the mWebView won't be valid.
+ pauseWebView();
+ removeTabFromContentView(self);
+ mTabControl.removeTab(self);
+ }
/*
* Instead of finishing the activity, simply push this to the back
* of the stack and let ActivityManager to choose the foreground
@@ -2352,7 +2421,10 @@
if (mAnimationCount > 0) {
return KeyTracker.State.DONE_TRACKING;
}
- if (stage == KeyTracker.Stage.UP) {
+ if (stage == KeyTracker.Stage.LONG_REPEAT) {
+ loadHistory();
+ return KeyTracker.State.DONE_TRACKING;
+ } else if (stage == KeyTracker.Stage.UP) {
// FIXME: Currently, we do not have a notion of the
// history picker for the subwindow, but maybe we
// should?
@@ -2381,14 +2453,12 @@
if (!handled) {
switch (keyCode) {
case KeyEvent.KEYCODE_SPACE:
- if (mMenuState == R.id.MAIN_MENU){
- if (event.isShiftPressed()) {
- getTopWindow().pageUp(false);
- } else {
- getTopWindow().pageDown(false);
- }
- handled = true;
+ if (event.isShiftPressed()) {
+ getTopWindow().pageUp(false);
+ } else {
+ getTopWindow().pageDown(false);
}
+ handled = true;
break;
default:
@@ -2424,6 +2494,13 @@
}
}
+ private void loadHistory() {
+ Intent intent = new Intent(this, BrowserHistoryPage.class);
+ intent.putExtra("maxTabsOpen",
+ mTabControl.getTabCount() >= TabControl.MAX_TABS);
+ startActivityForResult(intent, CLASSIC_HISTORY_PAGE);
+ }
+
// called by a non-UI thread to post the message
public void postMessage(int what, int arg1, int arg2, Object obj) {
mHandler.sendMessage(mHandler.obtainMessage(what, arg1, arg2, obj));
@@ -2436,13 +2513,12 @@
// Message Ids
private static final int JS_CONFIRM = 101;
private static final int FOCUS_NODE_HREF = 102;
- private static final int DISMISS_TAB_OVERVIEW = 103;
- private static final int CANCEL_CREDS_REQUEST = 104;
- private static final int ANIMATE_FROM_OVERVIEW = 105;
- private static final int ANIMATE_TO_OVERVIEW = 106;
- private static final int OPEN_TAB_AND_SHOW = 107;
- private static final int CHECK_MEMORY = 108;
- private static final int RELEASE_WAKELOCK = 109;
+ private static final int CANCEL_CREDS_REQUEST = 103;
+ private static final int ANIMATE_FROM_OVERVIEW = 104;
+ private static final int ANIMATE_TO_OVERVIEW = 105;
+ private static final int OPEN_TAB_AND_SHOW = 106;
+ private static final int CHECK_MEMORY = 107;
+ private static final int RELEASE_WAKELOCK = 108;
// Private handler for handling javascript and saving passwords
private Handler mHandler = new Handler() {
@@ -2458,37 +2534,6 @@
}
break;
- case DISMISS_TAB_OVERVIEW:
- if (mTabOverview != null) {
- if (msg.arg1 == 1) {
- AlphaAnimation anim =
- new AlphaAnimation(1.0f, 0.0f);
- anim.setDuration(500);
- anim.startNow();
- mTabOverview.startAnimation(anim);
- }
- // Just in case there was a problem with animating away
- // from the tab overview
- mWebView.setVisibility(View.VISIBLE);
- // Make the sub window container visible.
- if (mTabControl.getCurrentSubWindow() != null) {
- mTabControl.getCurrentTab().getSubWebViewContainer()
- .setVisibility(View.VISIBLE);
- }
- mContentView.removeView(mTabOverview);
- mTabOverview.clear();
- // XXX: There are checks for mTabOverview throughout
- // this file because this message can be received
- // before it is expected. This is because we are not
- // enforcing the order of animations properly. In order
- // to get this right, we would need to rewrite a lot of
- // the code to dispatch this messages after all
- // animations have completed.
- mTabOverview = null;
- mTabListener = null;
- }
- break;
-
case ANIMATE_FROM_OVERVIEW:
final HashMap map = (HashMap) msg.obj;
animateFromTabOverview((AnimatingView) map.get("view"),
@@ -2502,7 +2547,7 @@
break;
case OPEN_TAB_AND_SHOW:
- openTabAndShow((String) msg.obj, null);
+ openTabAndShow((String) msg.obj, null, false);
break;
case FOCUS_NODE_HREF:
@@ -2510,18 +2555,16 @@
if (url == null || url.length() == 0) {
break;
}
- WebView view = (WebView) msg.obj;
+ HashMap focusNodeMap = (HashMap) msg.obj;
+ WebView view = (WebView) focusNodeMap.get("webview");
// Only apply the action if the top window did not change.
if (getTopWindow() != view) {
break;
}
switch (msg.arg1) {
- case HEADER_FLAG:
- mTitleView.setText(url);
- break;
case R.id.open_context_menu_id:
case R.id.view_image_context_menu_id:
- loadURL(url);
+ loadURL(getTopWindow(), url);
break;
case R.id.open_newtab_context_menu_id:
openTab(url);
@@ -2546,7 +2589,7 @@
break;
case LOAD_URL:
- loadURL((String) msg.obj);
+ loadURL(getTopWindow(), (String) msg.obj);
break;
case STOP_LOAD:
@@ -2574,9 +2617,6 @@
}
};
- private static final int HEADER_FLAG = Integer.MIN_VALUE;
- private TextView mTitleView = null;
-
// -------------------------------------------------------------------------
// WebViewClient implementation.
//-------------------------------------------------------------------------
@@ -2591,14 +2631,22 @@
return mWebViewClient;
}
+ private void updateIcon(String url, Bitmap icon) {
+ if (icon != null) {
+ BrowserBookmarksAdapter.updateBookmarkFavicon(mResolver,
+ url, icon);
+ }
+ setFavicon(icon);
+ }
+
private final WebViewClient mWebViewClient = new WebViewClient() {
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
resetLockIcon(url);
setUrlTitle(url, null);
- // Call onReceivedIcon instead of setFavicon so the bookmark
+ // Call updateIcon instead of setFavicon so the bookmark
// database can be updated.
- mWebChromeClient.onReceivedIcon(view, favicon);
+ updateIcon(url, favicon);
if (mSettings.isTracing() == true) {
// FIXME: we should save the trace file somewhere other than data.
@@ -2664,8 +2712,6 @@
// Reset the title and icon in case we stopped a provisional
// load.
resetTitleAndIcon(view);
- // Make the progress full.
- getWindow().setFeatureInt(Window.FEATURE_PROGRESS, 10000);
// Update the lock icon image only once we are done loading
updateLockIconImage(mLockIconType);
@@ -2891,6 +2937,7 @@
if (errorCode != EventHandler.ERROR_LOOKUP &&
errorCode != EventHandler.ERROR_CONNECT &&
errorCode != EventHandler.ERROR_BAD_URL &&
+ errorCode != EventHandler.ERROR_UNSUPPORTED_SCHEME &&
errorCode != EventHandler.FILE_ERROR) {
new AlertDialog.Builder(BrowserActivity.this)
.setTitle((errorCode == EventHandler.FILE_NOT_FOUND_ERROR) ?
@@ -3106,7 +3153,7 @@
// openTabAndShow will dispatch the message after creating the
// new WebView. This will prevent another request from coming
// in during the animation.
- openTabAndShow(null, msg);
+ openTabAndShow(null, msg, false);
parent.addChildTab(mTabControl.getCurrentTab());
WebView.WebViewTransport transport =
(WebView.WebViewTransport) msg.obj;
@@ -3204,13 +3251,18 @@
mTabControl.getCurrentTab().getParentTab();
if (parent != null) {
// JavaScript can only close popup window.
- removeTabAndShow(currentIndex, mTabControl.getTabIndex(parent));
+ switchTabs(currentIndex, mTabControl.getTabIndex(parent), true);
}
}
@Override
public void onProgressChanged(WebView view, int newProgress) {
- getWindow().setFeatureInt(Window.FEATURE_PROGRESS, newProgress*100);
+ // 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 (newProgress == 100) {
// onProgressChanged() is called for sub-frame too while
@@ -3222,7 +3274,7 @@
@Override
public void onReceivedTitle(WebView view, String title) {
- String url = view.getUrl();
+ String url = view.getOriginalUrl();
// here, if url is null, we want to reset the title
setUrlTitle(url, title);
@@ -3245,7 +3297,9 @@
Cursor c = mResolver.query(Browser.BOOKMARKS_URI,
Browser.HISTORY_PROJECTION, where, selArgs, null);
if (c.moveToFirst()) {
- Log.d(LOGTAG, "updating cursor");
+ if (Config.LOGV) {
+ Log.v(LOGTAG, "updating cursor");
+ }
// Current implementation of database only has one entry per
// url.
int titleIndex =
@@ -3263,11 +3317,7 @@
@Override
public void onReceivedIcon(WebView view, Bitmap icon) {
- if (icon != null) {
- BrowserBookmarksAdapter.updateBookmarkFavicon(mResolver,
- view.getUrl(), icon);
- }
- setFavicon(icon);
+ updateIcon(view.getUrl(), icon);
}
//----------------------------------------------------------------------
@@ -3470,20 +3520,29 @@
* @param mimetype The mimetype of the content reported by the server
* @param contentLength The file size reported by the server
*/
- public void onDownloadStartNoStream(String url, String userAgent,
+ /*package */ void onDownloadStartNoStream(String url, String userAgent,
String contentDisposition, String mimetype, long contentLength) {
String filename = URLUtil.guessFileName(url,
contentDisposition, mimetype);
// Check to see if we have an SDCard
- if (!Environment.getExternalStorageState().
- equals(Environment.MEDIA_MOUNTED)) {
- String msg =
- getString(R.string.download_no_sdcard_dlg_msg, filename);
+ String status = Environment.getExternalStorageState();
+ if (!status.equals(Environment.MEDIA_MOUNTED)) {
+ int title;
+ String msg;
+
+ // Check to see if the SDCard is busy, same as the music app
+ if (status.equals(Environment.MEDIA_SHARED)) {
+ msg = getString(R.string.download_sdcard_busy_dlg_msg);
+ title = R.string.download_sdcard_busy_dlg_title;
+ } else {
+ msg = getString(R.string.download_no_sdcard_dlg_msg, filename);
+ title = R.string.download_no_sdcard_dlg_title;
+ }
new AlertDialog.Builder(this)
- .setTitle(R.string.download_no_sdcard_dlg_title)
+ .setTitle(title)
.setIcon(R.drawable.ic_dialog_alert)
.setMessage(msg)
.setPositiveButton(R.string.ok, null)
@@ -3508,9 +3567,15 @@
if (contentLength > 0) {
values.put(Downloads.TOTAL_BYTES, contentLength);
}
- final Uri contentUri =
- getContentResolver().insert(Downloads.CONTENT_URI, values);
- viewDownloads(contentUri);
+ if (mimetype == null) {
+ // We must have long pressed on a link or image to download it. We
+ // are not sure of the mimetype in this case, so do a head request
+ new FetchUrlMimeType(this).execute(values);
+ } else {
+ final Uri contentUri =
+ getContentResolver().insert(Downloads.CONTENT_URI, values);
+ viewDownloads(contentUri);
+ }
}
@@ -3563,29 +3628,38 @@
} else if (lockIconType == LOCK_ICON_MIXED) {
d = mMixLockIcon;
}
- getWindow().setFeatureDrawable(Window.FEATURE_RIGHT_ICON, d);
+ // 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);
+ }
}
/**
* Displays a page-info dialog.
- * @param view The target web-view.
+ * @param tab The tab to show info about
* @param fromShowSSLCertificateOnError The flag that indicates whether
* this dialog was opened from the SSL-certificate-on-error dialog or
* not. This is important, since we need to know whether to return to
* the parent dialog or simply dismiss.
*/
- private void showPageInfo(final WebView view,
+ private void showPageInfo(final TabControl.Tab tab,
final boolean fromShowSSLCertificateOnError) {
final LayoutInflater factory = LayoutInflater
.from(this);
final View pageInfoView = factory.inflate(R.layout.page_info, null);
+
+ final WebView view = tab.getWebView();
String url = null;
String title = null;
- // Use the cached title and url if this is the current WebView
- if (view == mWebView) {
+ if (view == null) {
+ url = tab.getUrl();
+ title = tab.getTitle();
+ }else if (view == mWebView) {
+ // Use the cached title and url if this is the current WebView
url = mUrl;
title = mTitle;
} else {
@@ -3603,7 +3677,7 @@
((TextView) pageInfoView.findViewById(R.id.address)).setText(url);
((TextView) pageInfoView.findViewById(R.id.title)).setText(title);
- mPageInfoView = view;
+ mPageInfoView = tab;
mPageInfoFromShowSSLCertificateOnError = new Boolean(fromShowSSLCertificateOnError);
AlertDialog.Builder alertDialogBuilder =
@@ -3649,7 +3723,8 @@
// if we have a main top-level page SSL certificate set or a certificate
// error
- if (fromShowSSLCertificateOnError || view.getCertificate() != null) {
+ if (fromShowSSLCertificateOnError ||
+ (view != null && view.getCertificate() != null)) {
// add a 'View Certificate' button
alertDialogBuilder.setNeutralButton(
R.string.view_certificate,
@@ -3671,7 +3746,7 @@
// otherwise, display the top-most certificate from
// the chain
if (view.getCertificate() != null) {
- showSSLCertificate(view);
+ showSSLCertificate(tab);
}
}
}
@@ -3684,9 +3759,9 @@
/**
* Displays the main top-level page SSL certificate dialog
* (accessible from the Page-Info dialog).
- * @param view The target web-view.
+ * @param tab The tab to show certificate for.
*/
- private void showSSLCertificate(final WebView view) {
+ private void showSSLCertificate(final TabControl.Tab tab) {
final View certificateView =
inflateCertificateView(mWebView.getCertificate());
if (certificateView == null) {
@@ -3703,7 +3778,7 @@
((TextView)ll.findViewById(R.id.success))
.setText(R.string.ssl_certificate_is_valid);
- mSSLCertificateView = view;
+ mSSLCertificateView = tab;
mSSLCertificateDialog =
new AlertDialog.Builder(this)
.setTitle(R.string.ssl_certificate).setIcon(
@@ -3716,7 +3791,7 @@
mSSLCertificateDialog = null;
mSSLCertificateView = null;
- showPageInfo(view, false);
+ showPageInfo(tab, false);
}
})
.setOnCancelListener(
@@ -3725,7 +3800,7 @@
mSSLCertificateDialog = null;
mSSLCertificateView = null;
- showPageInfo(view, false);
+ showPageInfo(tab, false);
}
})
.show();
@@ -3811,7 +3886,8 @@
// need to show the dialog again once the
// user is done exploring the page-info details
- showPageInfo(view, true);
+ showPageInfo(mTabControl.getTabFromView(view),
+ true);
}
})
.setOnCancelListener(
@@ -4016,6 +4092,7 @@
.show();
}
}
+ mTabControl.getCurrentWebView().setNetworkAvailable(up);
}
@Override
@@ -4030,14 +4107,19 @@
if (extras != null && extras.getBoolean("new_window", false)) {
openTab(data);
} else {
+ final TabControl.Tab currentTab =
+ mTabControl.getCurrentTab();
// If the Window overview is up and we are not in the
// middle of an animation, animate away from it to the
// current tab.
if (mTabOverview != null && mAnimationCount == 0) {
- sendAnimateFromOverview(mTabControl.getCurrentTab(),
- false, data, TAB_OVERVIEW_DELAY, null);
+ sendAnimateFromOverview(currentTab, false, data,
+ TAB_OVERVIEW_DELAY, null);
} else {
- loadURL(data);
+ dismissSubWindow(currentTab);
+ if (data != null && data.length() != 0) {
+ getTopWindow().loadUrl(data);
+ }
}
}
}
@@ -4053,7 +4135,7 @@
* menu to see the download window, or when a download changes state. It
* shows the download window ontop of the current window.
*/
- private void viewDownloads(Uri downloadRecord) {
+ /* package */ void viewDownloads(Uri downloadRecord) {
Intent intent = new Intent(this,
BrowserDownloadPage.class);
intent.setData(downloadRecord);
@@ -4071,30 +4153,41 @@
throw new AssertionError();
}
- mTabControl.removeTab(mTabControl.getTab(position));
+ // Remember the current tab.
+ TabControl.Tab current = mTabControl.getCurrentTab();
+ final TabControl.Tab remove = mTabControl.getTab(position);
+ mTabControl.removeTab(remove);
+ // If we removed the current tab, use the tab at position - 1 if
+ // possible.
+ if (current == remove) {
+ // If the user removes the last tab, act like the New Tab item
+ // was clicked on.
+ if (mTabControl.getTabCount() == 0) {
+ current = mTabControl.createNewTab(false);
+ sendAnimateFromOverview(current, true,
+ mSettings.getHomePage(), TAB_OVERVIEW_DELAY, null);
+ } else {
+ final int index = position > 0 ? (position - 1) : 0;
+ current = mTabControl.getTab(index);
+ }
+ }
+
// The tab overview could have been dismissed before this method is
// called.
if (mTabOverview != null) {
// Remove the tab and change the index.
- mTabOverview.remove(position--);
- mTabOverview.setCurrentIndex(position);
- } else {
- position--;
+ mTabOverview.remove(position);
+ mTabOverview.setCurrentIndex(mTabControl.getTabIndex(current));
}
// FIXME: This isn't really right. We don't have a current WebView
// since we are switching between tabs and haven't selected a new
// one. This just prevents a NPE in case the user hits home from the
// tab switcher.
- int index = position;
- if (index == ImageGrid.NEW_TAB) {
- index = 0;
- }
- final TabControl.Tab t = mTabControl.getTab(index);
// Only the current tab ensures its WebView is non-null. This
// implies that we are reloading the freed tab.
- mTabControl.setCurrentTab(t);
- mWebView = t.getWebView();
+ mTabControl.setCurrentTab(current);
+ mWebView = current.getWebView();
}
public void onClick(int index) {
// Change the tab if necessary.
@@ -4112,13 +4205,10 @@
// Clear all the data for tab picker so next time it will be
// recreated.
mTabControl.wipeAllPickerData();
- BrowserActivity.this.getWindow().setFeatureInt(
- Window.FEATURE_PROGRESS, Window.PROGRESS_VISIBILITY_ON);
- BrowserActivity.this.mMenuState = EMPTY_MENU;
// NEW_TAB means that the "New Tab" cell was clicked on.
if (index == ImageGrid.NEW_TAB) {
- openTabAndShow(mSettings.getHomePage(), null);
+ openTabAndShow(mSettings.getHomePage(), null, false);
} else {
sendAnimateFromOverview(mTabControl.getTab(index),
false, null, 0, null);
@@ -4156,11 +4246,13 @@
protected void onDraw(Canvas canvas) {
canvas.save();
canvas.drawColor(Color.WHITE);
- canvas.setDrawFilter(sZoomFilter);
- float scale = getWidth() * mScale;
- canvas.scale(scale, scale);
- canvas.translate(-mScrollX, -mScrollY);
- canvas.drawPicture(mPicture);
+ if (mPicture != null) {
+ canvas.setDrawFilter(sZoomFilter);
+ float scale = getWidth() * mScale;
+ canvas.scale(scale, scale);
+ canvas.translate(-mScrollX, -mScrollY);
+ canvas.drawPicture(mPicture);
+ }
canvas.restore();
}
}
@@ -4219,12 +4311,13 @@
// set it here to prevent another request to animate from coming in
// between now and when ANIMATE_TO_OVERVIEW is handled.
mAnimationCount++;
- if (stay) {
- getWindow().setFeatureDrawable(Window.FEATURE_LEFT_ICON, null);
- getWindow().setFeatureInt(Window.FEATURE_PROGRESS,
- Window.PROGRESS_VISIBILITY_OFF);
- setTitle(R.string.tab_picker_title);
- }
+ // 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);
// Make the menu empty until the animation completes.
mMenuState = EMPTY_MENU;
}
@@ -4254,14 +4347,13 @@
startActivityForResult(intent, BOOKMARKS_PAGE);
}
- // Called when loading from bookmarks or goto.
- private void loadURL(String url) {
+ // Called when loading from context menu or LOAD_URL message
+ private void loadURL(WebView view, String url) {
// In case the user enters nothing.
- if (url != null && url.length() != 0) {
+ if (url != null && url.length() != 0 && view != null) {
url = smartUrlFilter(url);
- WebView w = getTopWindow();
- if (!mWebViewClient.shouldOverrideUrlLoading(w, url)) {
- w.loadUrl(url);
+ if (!mWebViewClient.shouldOverrideUrlLoading(view, url)) {
+ view.loadUrl(url);
}
}
}
@@ -4316,21 +4408,23 @@
* @return Original or modified URL
*
*/
- String smartUrlFilter(String inUrl) {
+ String smartUrlFilter(String url) {
+ String inUrl = url.trim();
boolean hasSpace = inUrl.indexOf(' ') != -1;
- if (!hasSpace) {
- Matcher matcher = ACCEPTED_URI_SCHEMA.matcher(inUrl);
- if (matcher.matches()) {
- // force scheme to lowercase
- String scheme = matcher.group(1);
- String lcScheme = scheme.toLowerCase();
- if (!lcScheme.equals(scheme)) {
- return lcScheme + matcher.group(2);
- }
- return inUrl;
+ Matcher matcher = ACCEPTED_URI_SCHEMA.matcher(inUrl);
+ if (matcher.matches()) {
+ if (hasSpace) {
+ inUrl = inUrl.replace(" ", "%20");
}
+ // force scheme to lowercase
+ String scheme = matcher.group(1);
+ String lcScheme = scheme.toLowerCase();
+ if (!lcScheme.equals(scheme)) {
+ return lcScheme + matcher.group(2);
+ }
+ return inUrl;
}
if (hasSpace) {
// FIXME: quick search, need to be customized by setting
@@ -4378,6 +4472,33 @@
QUERY_PLACE_HOLDER);
}
+ /* package */void setBaseSearchUrl(String url) {
+ if (url == null || url.length() == 0) {
+ /*
+ * get the google search url based on the SIM. Default is US. NOTE:
+ * This code uses resources to optionally select the search Uri,
+ * based on the MCC value from the SIM. The default string will most
+ * likely be fine. It is parameterized to accept info from the
+ * Locale, the language code is the first parameter (%1$s) and the
+ * country code is the second (%2$s). This code must function in the
+ * same way as a similar lookup in
+ * com.android.googlesearch.SuggestionProvider#onCreate(). If you
+ * change either of these functions, change them both. (The same is
+ * true for the underlying resource strings, which are stored in
+ * mcc-specific xml files.)
+ */
+ Locale l = Locale.getDefault();
+ QuickSearch_G = getResources().getString(
+ R.string.google_search_base, l.getLanguage(),
+ l.getCountry().toLowerCase())
+ + "client=ms-"
+ + SystemProperties.get("ro.com.google.clientid", "unknown")
+ + "&source=android-" + GOOGLE_SEARCH_SOURCE_SUGGEST + "&q=%s";
+ } else {
+ QuickSearch_G = url;
+ }
+ }
+
private final static int LOCK_ICON_UNSECURE = 0;
private final static int LOCK_ICON_SECURE = 1;
private final static int LOCK_ICON_MIXED = 2;
@@ -4458,7 +4579,7 @@
// As PageInfo has different style for landscape / portrait, we have
// to re-open it when configuration changed
private AlertDialog mPageInfoDialog;
- private WebView mPageInfoView;
+ private TabControl.Tab mPageInfoView;
// If the Page-Info dialog is launched from the SSL-certificate-on-error
// dialog, we should not just dismiss it, but should get back to the
// SSL-certificate-on-error dialog. This flag is used to store this state
@@ -4474,7 +4595,7 @@
// as SSLCertificate has different style for landscape / portrait, we
// have to re-open it when configuration changed
private AlertDialog mSSLCertificateDialog;
- private WebView mSSLCertificateView;
+ private TabControl.Tab mSSLCertificateView;
// as HttpAuthentication has different style for landscape / portrait, we
// have to re-open it when configuration changed
@@ -4487,10 +4608,7 @@
ViewGroup.LayoutParams.FILL_PARENT);
// We may provide UI to customize these
// Google search from the browser
- final static String QuickSearch_G =
- "http://www.google.com/m?client=ms-"
- + SystemProperties.get("ro.com.google.clientid", "unknown")
- + "&source=android-chrome&q=%s";
+ static String QuickSearch_G;
// Wikipedia search
final static String QuickSearch_W = "http://en.wikipedia.org/w/index.php?search=%s&go=Go";
// Dictionary search
@@ -4498,7 +4616,20 @@
// Google Mobile Local search
final static String QuickSearch_L = "http://www.google.com/m/search?site=local&q=%s&near=mountain+view";
- private final static String QUERY_PLACE_HOLDER = "%s";
+ final static String QUERY_PLACE_HOLDER = "%s";
+
+ // "source" parameter for Google search through search key
+ final static String GOOGLE_SEARCH_SOURCE_SEARCHKEY = "browser-key";
+ // "source" parameter for Google search through search menu
+ final static String GOOGLE_SEARCH_SOURCE_SEARCHMENU = "browser-menu";
+ // "source" parameter for Google search through goto menu
+ final static String GOOGLE_SEARCH_SOURCE_GOTO = "browser-goto";
+ // "source" parameter for Google search through simplily type
+ final static String GOOGLE_SEARCH_SOURCE_TYPE = "browser-type";
+ // "source" parameter for Google search suggested by the browser
+ final static String GOOGLE_SEARCH_SOURCE_SUGGEST = "browser-suggest";
+ // "source" parameter for Google search from unknown source
+ final static String GOOGLE_SEARCH_SOURCE_UNKNOWN = "unknown";
private final static String LOGTAG = "browser";
@@ -4516,6 +4647,13 @@
// overlap. A count of 0 means no animation where a count of > 0 means
// there are animations in progress.
private int mAnimationCount;
+
+ // As the ids are dynamically created, we can't guarantee that they will
+ // be in sequence, so this static array maps ids to a window number.
+ final static private int[] WINDOW_SHORTCUT_ID_ARRAY =
+ { R.id.window_one_menu_id, R.id.window_two_menu_id, R.id.window_three_menu_id,
+ R.id.window_four_menu_id, R.id.window_five_menu_id, R.id.window_six_menu_id,
+ R.id.window_seven_menu_id, R.id.window_eight_menu_id };
// monitor platform changes
private IntentFilter mNetworkStateChangedFilter;
diff --git a/src/com/android/browser/BrowserBookmarksAdapter.java b/src/com/android/browser/BrowserBookmarksAdapter.java
index 3b76e75..479dc0e 100644
--- a/src/com/android/browser/BrowserBookmarksAdapter.java
+++ b/src/com/android/browser/BrowserBookmarksAdapter.java
@@ -312,8 +312,21 @@
if (url == null || favicon == null) {
return;
}
- final String[] selArgs = new String[] { url };
- final String where = Browser.BookmarkColumns.URL + " == ? AND "
+ // Strip the query.
+ int query = url.indexOf('?');
+ String noQuery = url;
+ if (query != -1) {
+ noQuery = url.substring(0, query);
+ }
+ url = noQuery + '?';
+ // Use noQuery to search for the base url (i.e. if the url is
+ // http://www.yahoo.com/?rs=1, search for http://www.yahoo.com)
+ // Use url to match the base url with other queries (i.e. if the url is
+ // http://www.google.com/m, search for
+ // http://www.google.com/m?some_query)
+ final String[] selArgs = new String[] { noQuery, url };
+ final String where = "(" + Browser.BookmarkColumns.URL + " == ? OR "
+ + Browser.BookmarkColumns.URL + " GLOB ? || '*') AND "
+ Browser.BookmarkColumns.BOOKMARK + " == 1";
final String[] projection = new String[] { Browser.BookmarkColumns._ID };
final Cursor c = cr.query(Browser.BOOKMARKS_URI, projection, where,
diff --git a/src/com/android/browser/BrowserBookmarksPage.java b/src/com/android/browser/BrowserBookmarksPage.java
index f938ff9..5c509a8 100644
--- a/src/com/android/browser/BrowserBookmarksPage.java
+++ b/src/com/android/browser/BrowserBookmarksPage.java
@@ -20,6 +20,7 @@
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.Intent;
+import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
@@ -52,6 +53,10 @@
private AddNewBookmark mAddHeader;
private boolean mCanceled = false;
private boolean mCreateShortcut;
+ // XXX: There is no public string defining this intent so if Home changes
+ // the value, we have to update this string.
+ private static final String INSTALL_SHORTCUT =
+ "com.android.launcher.action.INSTALL_SHORTCUT";
private final static String LOGTAG = "browser";
@@ -80,6 +85,12 @@
case R.id.edit_context_menu_id:
editBookmark(i.position);
break;
+ case R.id.shortcut_context_menu_id:
+ final Intent send = createShortcutIntent(getUrl(i.position),
+ getBookmarkTitle(i.position));
+ send.setAction(INSTALL_SHORTCUT);
+ sendBroadcast(send);
+ break;
case R.id.delete_context_menu_id:
displayRemoveBookmarkDialog(i.position);
break;
@@ -191,19 +202,27 @@
loadUrl(position);
}
} else {
- final Intent intent = new Intent();
- intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, new Intent(Intent.ACTION_VIEW,
- Uri.parse(getUrl(position))));
- intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, getBookmarkTitle(position));
- intent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE,
- Intent.ShortcutIconResource.fromContext(BrowserBookmarksPage.this,
- R.drawable.ic_launcher_browser));
+ final Intent intent = createShortcutIntent(getUrl(position),
+ getBookmarkTitle(position));
setResult(RESULT_OK, intent);
finish();
}
}
};
+ private Intent createShortcutIntent(String url, String title) {
+ final Intent i = new Intent();
+ i.putExtra(Intent.EXTRA_SHORTCUT_INTENT, new Intent(Intent.ACTION_VIEW,
+ Uri.parse(url)));
+ i.putExtra(Intent.EXTRA_SHORTCUT_NAME, title);
+ i.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE,
+ Intent.ShortcutIconResource.fromContext(BrowserBookmarksPage.this,
+ R.drawable.ic_launcher_browser));
+ // Do not allow duplicate items
+ i.putExtra("duplicate", false);
+ return i;
+ }
+
private void saveCurrentPage() {
Intent i = new Intent(BrowserBookmarksPage.this,
AddBookmarkPage.class);
@@ -290,7 +309,7 @@
final int deletePos = position;
new AlertDialog.Builder(this)
.setTitle(R.string.delete_bookmark)
- .setIcon(R.drawable.ssl_icon)
+ .setIcon(android.R.drawable.ic_dialog_alert)
.setMessage(getText(R.string.delete_bookmark_warning).toString().replace(
"%s", getBookmarkTitle(deletePos)))
.setPositiveButton(R.string.ok,
diff --git a/src/com/android/browser/BrowserDownloadAdapter.java b/src/com/android/browser/BrowserDownloadAdapter.java
index b3e08f5..0b509ef 100644
--- a/src/com/android/browser/BrowserDownloadAdapter.java
+++ b/src/com/android/browser/BrowserDownloadAdapter.java
@@ -20,7 +20,6 @@
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
-import android.content.Formatter;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
@@ -30,6 +29,7 @@
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.provider.Downloads;
+import android.text.format.Formatter;
import android.view.View;
import android.widget.ImageView;
import android.widget.ProgressBar;
@@ -59,7 +59,7 @@
public BrowserDownloadAdapter(Context context, int layout, Cursor c) {
super(context, layout, c);
- mFilenameColumnId = c.getColumnIndexOrThrow(Downloads.FILENAME);
+ mFilenameColumnId = c.getColumnIndexOrThrow(Downloads._DATA);
mTitleColumnId = c.getColumnIndexOrThrow(Downloads.TITLE);
mDescColumnId = c.getColumnIndexOrThrow(Downloads.DESCRIPTION);
mStatusColumnId = c.getColumnIndexOrThrow(Downloads.STATUS);
@@ -213,7 +213,7 @@
return R.string.download_file_error;
case Downloads.STATUS_BAD_REQUEST:
- case Downloads.STATUS_ERROR:
+ case Downloads.STATUS_UNKNOWN_ERROR:
default:
return R.string.download_error;
}
diff --git a/src/com/android/browser/BrowserDownloadPage.java b/src/com/android/browser/BrowserDownloadPage.java
index e2b11a6..4397337 100644
--- a/src/com/android/browser/BrowserDownloadPage.java
+++ b/src/com/android/browser/BrowserDownloadPage.java
@@ -75,7 +75,7 @@
mDownloadCursor = managedQuery(Downloads.CONTENT_URI,
new String [] {"_id", Downloads.TITLE, Downloads.STATUS,
Downloads.TOTAL_BYTES, Downloads.CURRENT_BYTES,
- Downloads.FILENAME, Downloads.DESCRIPTION,
+ Downloads._DATA, Downloads.DESCRIPTION,
Downloads.MIMETYPE, Downloads.LAST_MODIFICATION,
Downloads.VISIBILITY},
null, null);
@@ -170,6 +170,7 @@
(AdapterView.AdapterContextMenuInfo) menuInfo;
mDownloadCursor.moveToPosition(info.position);
mContextMenuPosition = info.position;
+ menu.setHeaderTitle(mDownloadCursor.getString(mTitleColumnId));
MenuInflater inflater = getMenuInflater();
int status = mDownloadCursor.getInt(mStatusColumnId);
@@ -242,10 +243,7 @@
* @param id Row id of the download to resume
*/
private void resumeDownload(final long id) {
- Uri record = ContentUris.withAppendedId(Downloads.CONTENT_URI, id);
- ContentValues values = new ContentValues();
- values.put(Downloads.CONTROL, Downloads.CONTROL_RUN);
- getContentResolver().update(record, values, null, null);
+ // the relevant functionality doesn't exist in the download manager
}
/**
@@ -327,7 +325,7 @@
*/
private void cancelAllDownloads() {
if (mDownloadCursor.moveToFirst()) {
- StringBuffer where = new StringBuffer();
+ StringBuilder where = new StringBuilder();
boolean firstTime = true;
while (!mDownloadCursor.isAfterLast()) {
int status = mDownloadCursor.getInt(mStatusColumnId);
@@ -339,9 +337,9 @@
}
where.append("( ");
where.append(Downloads._ID);
- where.append(" = ");
+ where.append(" = '");
where.append(mDownloadCursor.getLong(mIdColumnId));
- where.append(" )");
+ where.append("' )");
}
mDownloadCursor.moveToNext();
}
@@ -372,7 +370,7 @@
*/
private void clearAllDownloads() {
if (mDownloadCursor.moveToFirst()) {
- StringBuffer where = new StringBuffer();
+ StringBuilder where = new StringBuilder();
boolean firstTime = true;
while (!mDownloadCursor.isAfterLast()) {
int status = mDownloadCursor.getInt(mStatusColumnId);
@@ -384,9 +382,9 @@
}
where.append("( ");
where.append(Downloads._ID);
- where.append(" = ");
+ where.append(" = '");
where.append(mDownloadCursor.getLong(mIdColumnId));
- where.append(" )");
+ where.append("' )");
}
mDownloadCursor.moveToNext();
}
@@ -402,7 +400,7 @@
*/
private void openCurrentDownload() {
int filenameColumnId =
- mDownloadCursor.getColumnIndexOrThrow(Downloads.FILENAME);
+ mDownloadCursor.getColumnIndexOrThrow(Downloads._DATA);
String filename = mDownloadCursor.getString(filenameColumnId);
int mimetypeColumnId =
mDownloadCursor.getColumnIndexOrThrow(Downloads.MIMETYPE);
diff --git a/src/com/android/browser/BrowserHomepagePreference.java b/src/com/android/browser/BrowserHomepagePreference.java
new file mode 100644
index 0000000..bc21143
--- /dev/null
+++ b/src/com/android/browser/BrowserHomepagePreference.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2008 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.content.Context;
+import android.content.DialogInterface;
+import android.preference.EditTextPreference;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.text.util.Regex;
+import android.util.AttributeSet;
+
+public class BrowserHomepagePreference extends EditTextPreference implements
+ TextWatcher {
+
+ public BrowserHomepagePreference(Context context, AttributeSet attrs,
+ int defStyle) {
+ super(context, attrs, defStyle);
+ getEditText().addTextChangedListener(this);
+ }
+
+ public BrowserHomepagePreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ getEditText().addTextChangedListener(this);
+ }
+
+ public BrowserHomepagePreference(Context context) {
+ super(context);
+ getEditText().addTextChangedListener(this);
+ }
+
+ public void afterTextChanged(Editable s) {
+ AlertDialog dialog = (AlertDialog) getDialog();
+ // This callback is called before the dialog has been fully constructed
+ if (dialog != null) {
+ dialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(
+ Regex.WEB_URL_PATTERN.matcher(s.toString()).matches());
+ }
+ }
+
+ public void beforeTextChanged(CharSequence s, int start, int count,
+ int after) {
+ }
+
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ }
+}
diff --git a/src/com/android/browser/BrowserPreferencesPage.java b/src/com/android/browser/BrowserPreferencesPage.java
index b8bc495..b23f750 100644
--- a/src/com/android/browser/BrowserPreferencesPage.java
+++ b/src/com/android/browser/BrowserPreferencesPage.java
@@ -51,6 +51,9 @@
getPreferenceScreen().getSharedPreferences()
.getString(BrowserSettings.PREF_TEXT_SIZE, null)) );
+ e = findPreference(BrowserSettings.PREF_DEFAULT_TEXT_ENCODING);
+ e.setOnPreferenceChangeListener(this);
+
if (BrowserSettings.getInstance().showDebugSettings()) {
addPreferencesFromResource(R.xml.debug_preferences);
}
@@ -76,30 +79,36 @@
}
} else if (pref.getKey().equals(BrowserSettings.PREF_HOMEPAGE)) {
String value = (String) objValue;
-
- if (value.length() > 0) {
- Uri path = Uri.parse(value);
- if (path.getScheme() == null) {
- value = "http://"+value;
-
- pref.setSummary(value);
-
- // Update through the EditText control as it has a cached copy
- // of the string and it will handle persisting the value
- ((EditTextPreference)(pref)).setText(value);
-
- // as we update the value above, we need to return false
- // here so that setText() is not called by EditTextPref
- // with the old value.
- return false;
- }
+ boolean needUpdate = value.indexOf(' ') != -1;
+ if (needUpdate) {
+ value = value.trim().replace(" ", "%20");
}
-
+ Uri path = Uri.parse(value);
+ if (path.getScheme() == null) {
+ value = "http://" + value;
+ needUpdate = true;
+ }
+ // Set the summary value.
pref.setSummary(value);
- return true;
+ if (needUpdate) {
+ // Update through the EditText control as it has a cached copy
+ // of the string and it will handle persisting the value
+ ((EditTextPreference) pref).setText(value);
+
+ // as we update the value above, we need to return false
+ // here so that setText() is not called by EditTextPref
+ // with the old value.
+ return false;
+ } else {
+ return true;
+ }
} else if (pref.getKey().equals(BrowserSettings.PREF_TEXT_SIZE)) {
pref.setSummary(getVisualTextSizeName((String) objValue));
return true;
+ } else if (pref.getKey().equals(
+ BrowserSettings.PREF_DEFAULT_TEXT_ENCODING)) {
+ pref.setSummary((String) objValue);
+ return true;
}
return false;
diff --git a/src/com/android/browser/BrowserProvider.java b/src/com/android/browser/BrowserProvider.java
index 4f456e7..7aa5bb2 100644
--- a/src/com/android/browser/BrowserProvider.java
+++ b/src/com/android/browser/BrowserProvider.java
@@ -30,6 +30,7 @@
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
+import android.os.SystemProperties;
import android.provider.Browser;
import android.util.Log;
import android.text.util.Regex;
@@ -101,6 +102,45 @@
public BrowserProvider() {
}
+
+
+ private static CharSequence replaceSystemPropertyInString(CharSequence srcString) {
+ StringBuffer sb = new StringBuffer();
+ int lastCharLoc = 0;
+ for (int i = 0; i < srcString.length(); ++i) {
+ char c = srcString.charAt(i);
+ if (c == '{') {
+ sb.append(srcString.subSequence(lastCharLoc, i));
+ lastCharLoc = i;
+ inner:
+ for (int j = i; j < srcString.length(); ++j) {
+ char k = srcString.charAt(j);
+ if (k == '}') {
+ String propertyKeyValue = srcString.subSequence(i + 1, j).toString();
+ // See if the propertyKeyValue specifies a default value
+ int defaultOffset = propertyKeyValue.indexOf(':');
+ if (defaultOffset == -1) {
+ sb.append(SystemProperties.get(propertyKeyValue));
+ } else {
+ String propertyKey = propertyKeyValue.substring(0, defaultOffset);
+ String defaultValue =
+ propertyKeyValue.substring(defaultOffset + 1,
+ propertyKeyValue.length());
+ sb.append(SystemProperties.get(propertyKey, defaultValue));
+ }
+ lastCharLoc = j + 1;
+ i = j;
+ break inner;
+ }
+ }
+ }
+ }
+ if (srcString.length() - lastCharLoc > 0) {
+ // Put on the tail, if there is one
+ sb.append(srcString.subSequence(lastCharLoc, srcString.length()));
+ }
+ return sb;
+ }
private static class DatabaseHelper extends SQLiteOpenHelper {
private Context mContext;
@@ -129,9 +169,10 @@
int size = bookmarks.length;
try {
for (int i = 0; i < size; i = i + 2) {
+ CharSequence bookmarkDestination = replaceSystemPropertyInString(bookmarks[i + 1]);
db.execSQL("INSERT INTO bookmarks (title, url, visits, " +
"date, created, bookmark)" + " VALUES('" +
- bookmarks[i] + "', '" + bookmarks[i + 1] +
+ bookmarks[i] + "', '" + bookmarkDestination +
"', 0, 0, 0, 1);");
}
} catch (ArrayIndexOutOfBoundsException e) {
@@ -401,12 +442,18 @@
myArgs = null;
} else {
String like = selectionArgs[0] + "%";
- SUGGEST_ARGS[0] = "http://" + like;
- SUGGEST_ARGS[1] = "http://www." + like;
- SUGGEST_ARGS[2] = "https://" + like;
- SUGGEST_ARGS[3] = "https://www." + like;
- myArgs = SUGGEST_ARGS;
- suggestSelection = SUGGEST_SELECTION;
+ if (selectionArgs[0].startsWith("http")) {
+ myArgs = new String[1];
+ myArgs[0] = like;
+ suggestSelection = selection;
+ } else {
+ SUGGEST_ARGS[0] = "http://" + like;
+ SUGGEST_ARGS[1] = "http://www." + like;
+ SUGGEST_ARGS[2] = "https://" + like;
+ SUGGEST_ARGS[3] = "https://www." + like;
+ myArgs = SUGGEST_ARGS;
+ suggestSelection = SUGGEST_SELECTION;
+ }
}
// Suggestions are always performed with the default sort order:
// date ASC.
diff --git a/src/com/android/browser/BrowserSearchpagePreference.java b/src/com/android/browser/BrowserSearchpagePreference.java
new file mode 100644
index 0000000..09e8993
--- /dev/null
+++ b/src/com/android/browser/BrowserSearchpagePreference.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2008 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.content.Context;
+import android.content.DialogInterface;
+import android.preference.EditTextPreference;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.text.util.Regex;
+import android.util.AttributeSet;
+
+public class BrowserSearchpagePreference extends EditTextPreference implements
+ TextWatcher {
+
+ public BrowserSearchpagePreference(Context context, AttributeSet attrs,
+ int defStyle) {
+ super(context, attrs, defStyle);
+ getEditText().addTextChangedListener(this);
+ }
+
+ public BrowserSearchpagePreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ getEditText().addTextChangedListener(this);
+ }
+
+ public BrowserSearchpagePreference(Context context) {
+ super(context);
+ getEditText().addTextChangedListener(this);
+ }
+
+ public void afterTextChanged(Editable s) {
+ AlertDialog dialog = (AlertDialog) getDialog();
+ // This callback is called before the dialog has been fully constructed
+ if (dialog != null) {
+ String string = s.toString();
+ int length = string.length();
+ int first = length > 0 ? string
+ .indexOf(BrowserActivity.QUERY_PLACE_HOLDER) : -1;
+ int last = length > 0 ? string
+ .lastIndexOf(BrowserActivity.QUERY_PLACE_HOLDER) : -1;
+ dialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(
+ length == 0 || (first > 0 && first == last));
+ }
+ }
+
+ public void beforeTextChanged(CharSequence s, int start, int count,
+ int after) {
+ }
+
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ }
+}
diff --git a/src/com/android/browser/BrowserSettings.java b/src/com/android/browser/BrowserSettings.java
index b19c02e..6164e38 100644
--- a/src/com/android/browser/BrowserSettings.java
+++ b/src/com/android/browser/BrowserSettings.java
@@ -22,6 +22,7 @@
import android.content.pm.ActivityInfo;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
+import android.os.SystemProperties;
import android.view.WindowManager;
import android.webkit.CacheManager;
import android.webkit.CookieManager;
@@ -63,7 +64,8 @@
private boolean saveFormData = true;
private boolean openInBackground = false;
private String defaultTextEncodingName;
- private String homeUrl = "http://www.google.com/m";
+ private String homeUrl = "http://www.google.com/m?client=ms-" +
+ SystemProperties.get("ro.com.google.clientid", "unknown");
private boolean loginInitialized = false;
private int orientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
private boolean autoFitPage = true;
@@ -102,7 +104,17 @@
public final static String PREF_DEBUG_SETTINGS = "debug_menu";
public final static String PREF_GEARS_SETTINGS = "gears_settings";
public final static String PREF_TEXT_SIZE = "text_size";
-
+ public final static String PREF_DEFAULT_TEXT_ENCODING =
+ "default_text_encoding";
+
+ 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";
+
+ 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";
+
// Value to truncate strings when adding them to a TextView within
// a ListView
public final static int MAX_TEXTVIEW_LEN = 80;
@@ -134,7 +146,14 @@
WebSettings s = mSettings;
s.setLayoutAlgorithm(b.layoutAlgorithm);
- s.setUserAgent(b.userAgent);
+ if (b.userAgent == 0) {
+ // use the default ua string
+ s.setUserAgentString(null);
+ } else if (b.userAgent == 1) {
+ s.setUserAgentString(DESKTOP_USERAGENT);
+ } else if (b.userAgent == 2) {
+ s.setUserAgentString(IPHONE_USERAGENT);
+ }
s.setUseWideViewPort(b.useWideViewPort);
s.setLoadsImagesAutomatically(b.loadsImagesAutomatically);
s.setJavaScriptEnabled(b.javaScriptEnabled);
@@ -157,6 +176,8 @@
s.setNeedInitialFocus(false);
// Browser supports multiple windows
s.setSupportMultipleWindows(true);
+ // Turn off file access
+ s.setAllowFileAccess(false);
}
}
@@ -218,6 +239,9 @@
} else {
layoutAlgorithm = WebSettings.LayoutAlgorithm.NORMAL;
}
+ defaultTextEncodingName =
+ p.getString(PREF_DEFAULT_TEXT_ENCODING,
+ defaultTextEncodingName);
showDebugSettings =
p.getBoolean(PREF_DEBUG_SETTINGS, showDebugSettings);
@@ -244,6 +268,8 @@
navDump = p.getBoolean("enable_nav_dump", navDump);
doFlick = p.getBoolean("enable_flick", doFlick);
userAgent = Integer.parseInt(p.getString("user_agent", "0"));
+ mTabControl.getBrowserActivity().setBaseSearchUrl(
+ p.getString("search_url", ""));
}
update();
}
@@ -398,10 +424,6 @@
ContentResolver resolver = context.getContentResolver();
Browser.clearHistory(resolver);
Browser.clearSearches(resolver);
- // Delete back-forward list
- if (mTabControl != null) {
- mTabControl.clearHistory();
- }
}
/* package */ void clearFormData(Context context) {
diff --git a/src/com/android/browser/FakeWebView.java b/src/com/android/browser/FakeWebView.java
index 200f86a..7997672 100644
--- a/src/com/android/browser/FakeWebView.java
+++ b/src/com/android/browser/FakeWebView.java
@@ -70,12 +70,14 @@
final WebView w = mTab.getTopWindow();
if (w != null) {
Picture p = w.capturePicture();
- canvas.save();
- float scale = getWidth() * w.getScale() / w.getWidth();
- canvas.scale(scale, scale);
- canvas.translate(-w.getScrollX(), -w.getScrollY());
- canvas.drawPicture(p);
- canvas.restore();
+ if (p != null) {
+ canvas.save();
+ float scale = getWidth() * w.getScale() / w.getWidth();
+ canvas.scale(scale, scale);
+ canvas.translate(-w.getScrollX(), -w.getScrollY());
+ canvas.drawPicture(p);
+ canvas.restore();
+ }
}
}
}
diff --git a/src/com/android/browser/FetchUrlMimeType.java b/src/com/android/browser/FetchUrlMimeType.java
new file mode 100644
index 0000000..8578643
--- /dev/null
+++ b/src/com/android/browser/FetchUrlMimeType.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2008 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.ContentValues;
+import android.net.Uri;
+import android.net.http.AndroidHttpClient;
+
+import org.apache.http.HttpResponse;
+import org.apache.http.Header;
+import org.apache.http.client.methods.HttpHead;
+
+import java.io.IOException;
+
+import android.os.AsyncTask;
+import android.provider.Downloads;
+import android.webkit.MimeTypeMap;
+import android.webkit.URLUtil;
+
+/**
+ * This class is used to pull down the http headers of a given URL so that
+ * we can analyse the mimetype and make any correction needed before we give
+ * the URL to the download manager. The ContentValues class holds the
+ * content that would be provided to the download manager, so that on
+ * completion of checking the mimetype, we can issue the download to
+ * the download manager.
+ * This operation is needed when the user long-clicks on a link or image and
+ * we don't know the mimetype. If the user just clicks on the link, we will
+ * do the same steps of correcting the mimetype down in
+ * android.os.webkit.LoadListener rather than handling it here.
+ *
+ */
+class FetchUrlMimeType extends AsyncTask<ContentValues, String, String> {
+
+ BrowserActivity mActivity;
+ ContentValues mValues;
+
+ public FetchUrlMimeType(BrowserActivity activity) {
+ mActivity = activity;
+ }
+
+ @Override
+ public String doInBackground(ContentValues... values) {
+ mValues = values[0];
+
+ // Check to make sure we have a URI to download
+ String uri = mValues.getAsString(Downloads.URI);
+ if (uri == null || uri.length() == 0) {
+ return null;
+ }
+
+ // User agent is likely to be null, though the AndroidHttpClient
+ // seems ok with that.
+ AndroidHttpClient client = AndroidHttpClient.newInstance(
+ mValues.getAsString(Downloads.USER_AGENT));
+ HttpHead request = new HttpHead(uri);
+
+ String cookie = mValues.getAsString(Downloads.COOKIE_DATA);
+ if (cookie != null && cookie.length() > 0) {
+ request.addHeader("Cookie", cookie);
+ }
+
+ String referer = mValues.getAsString(Downloads.REFERER);
+ if (referer != null && referer.length() > 0) {
+ request.addHeader("Referer", referer);
+ }
+
+ HttpResponse response;
+ Boolean succeeded = true;
+ String mimeType = null;
+ try {
+ response = client.execute(request);
+ // We could get a redirect here, but if we do lets let
+ // the download manager take care of it, and thus trust that
+ // the server sends the right mimetype
+ if (response.getStatusLine().getStatusCode() == 200) {
+ Header header = response.getFirstHeader("Content-Type");
+ if (header != null) {
+ mimeType = header.getValue();
+ final int semicolonIndex = mimeType.indexOf(';');
+ if (semicolonIndex != -1) {
+ mimeType = mimeType.substring(0, semicolonIndex);
+ }
+ }
+ }
+ } catch (IllegalArgumentException ex) {
+ request.abort();
+ } catch (IOException ex) {
+ request.abort();
+ } finally {
+ client.close();
+ }
+
+ return mimeType;
+ }
+
+ @Override
+ public void onPostExecute(String mimeType) {
+ if (mimeType != null) {
+ String url = mValues.getAsString(Downloads.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);
+ }
+ }
+ String filename = URLUtil.guessFileName(url,
+ null, mimeType);
+ mValues.put(Downloads.FILENAME_HINT, filename);
+ }
+
+ // Start the download
+ final Uri contentUri =
+ mActivity.getContentResolver().insert(Downloads.CONTENT_URI, mValues);
+ mActivity.viewDownloads(contentUri);
+ }
+
+}
diff --git a/src/com/android/browser/FindDialog.java b/src/com/android/browser/FindDialog.java
index 42447e3..2b26784 100644
--- a/src/com/android/browser/FindDialog.java
+++ b/src/com/android/browser/FindDialog.java
@@ -16,22 +16,24 @@
package com.android.browser;
+import android.app.Dialog;
+import android.content.res.Configuration;
+import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.text.Editable;
import android.text.Spannable;
import android.text.TextWatcher;
+import android.view.Gravity;
import android.view.KeyEvent;
-import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
-import android.view.LayoutInflater;
+import android.view.Window;
import android.webkit.WebView;
import android.widget.EditText;
-import android.widget.LinearLayout;
import android.widget.TextView;
-/* package */ class FindDialog extends LinearLayout implements TextWatcher {
+/* package */ class FindDialog extends Dialog implements TextWatcher {
private WebView mWebView;
private TextView mMatches;
private BrowserActivity mBrowserActivity;
@@ -41,10 +43,7 @@
private EditText mEditText;
private View mNextButton;
private View mPrevButton;
-
- // Tags for messages to be sent to the handler.
- private final static int FIND_RESPONSE = 0;
- private final static int NUM_FOUND = 1;
+ private View mMatchesView;
private View.OnClickListener mFindListener = new View.OnClickListener() {
public void onClick(View v) {
@@ -65,26 +64,7 @@
if (mWebView == null) {
throw new AssertionError("No WebView for FindDialog::onClick");
}
- // Find is disabled for version 1.0, so find methods on WebView are
- // currently private.
- //mWebView.findPrevious(mEditText.getText().toString(),
- // mFindHandler.obtainMessage(FIND_RESPONSE));
- }
- };
-
- private Handler mFindHandler = new Handler() {
- public void handleMessage(Message msg) {
- if (NUM_FOUND == msg.what) {
- mMatches.setText(Integer.toString(msg.arg1));
- if (0 == msg.arg1) {
- disableButtons();
- } else {
- mPrevButton.setFocusable(true);
- mNextButton.setFocusable(true);
- mPrevButton.setEnabled(true);
- mNextButton.setEnabled(true);
- }
- }
+ mWebView.findNext(false);
}
};
@@ -95,20 +75,35 @@
mNextButton.setFocusable(false);
}
- public void setWebView(WebView webview) {
+ /* package */ void setWebView(WebView webview) {
mWebView = webview;
}
/* package */ FindDialog(BrowserActivity context) {
- super(context);
+ super(context, R.style.FindDialogTheme);
mBrowserActivity = context;
- LayoutInflater factory = LayoutInflater.from(context);
- factory.inflate(R.layout.browser_find, this);
-
- setLayoutParams(new ViewGroup.LayoutParams(
- ViewGroup.LayoutParams.FILL_PARENT,
- ViewGroup.LayoutParams.WRAP_CONTENT));
-
+ setCanceledOnTouchOutside(true);
+ }
+
+ /* package */ void onConfigurationChanged(Configuration newConfig) {
+ // FIXME: Would like to call mWebView.findAll again, so that the
+ // matches would refresh, but the new picture has not yet been
+ // created, so it is too soon.
+ mEditText.getText().clear();
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ Window theWindow = getWindow();
+ theWindow.setGravity(Gravity.BOTTOM|Gravity.FILL_HORIZONTAL);
+
+ setContentView(R.layout.browser_find);
+
+ theWindow.setLayout(ViewGroup.LayoutParams.FILL_PARENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT);
+
mEditText = (EditText) findViewById(R.id.edit);
View button = findViewById(R.id.next);
@@ -124,47 +119,21 @@
mOk = button;
mMatches = (TextView) findViewById(R.id.matches);
+ mMatchesView = findViewById(R.id.matches_view);
disableButtons();
}
public void dismiss() {
+ super.dismiss();
mBrowserActivity.closeFind();
- // If the nav buttons are highlighted, then there are matches
- // highlighted in the WebView, and they should be cleared.
- if (mPrevButton.isEnabled()) {
- // Find is disabled for version 1.0, so find methods on WebView are
- // currently private.
- //mWebView.clearMatches();
- }
+ mWebView.clearMatches();
}
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
- // Make up and down find previous/next
int code = event.getKeyCode();
boolean up = event.getAction() == KeyEvent.ACTION_UP;
switch (code) {
- case KeyEvent.KEYCODE_BACK:
- if (up) {
- dismiss();
- }
- return true;
- case KeyEvent.KEYCODE_DPAD_UP:
- if (event.getMetaState() != 0) {
- break;
- }
- if (up) {
- mFindPreviousListener.onClick(null);
- }
- return true;
- case KeyEvent.KEYCODE_DPAD_DOWN:
- if (event.getMetaState() != 0) {
- break;
- }
- if (up) {
- mFindListener.onClick(null);
- }
- return true;
case KeyEvent.KEYCODE_DPAD_CENTER:
case KeyEvent.KEYCODE_ENTER:
if (!mEditText.hasFocus()) {
@@ -179,26 +148,16 @@
}
return super.dispatchKeyEvent(event);
}
-
- @Override
- public boolean dispatchTouchEvent(MotionEvent ev) {
- super.dispatchTouchEvent(ev);
- // Return true so that BrowserActivity thinks we handled it and does
- // not dismiss us.
- return true;
- }
private void findNext() {
if (mWebView == null) {
throw new AssertionError("No WebView for FindDialog::findNext");
}
- // Find is disabled for version 1.0, so find methods on WebView are
- // currently private.
- //mWebView.findNext(mEditText.getText().toString(),
- // mFindHandler.obtainMessage(FIND_RESPONSE));
+ mWebView.findNext(true);
}
public void show() {
+ super.show();
mEditText.requestFocus();
mEditText.setText("");
Spannable span = (Spannable) mEditText.getText();
@@ -219,22 +178,30 @@
int start,
int before,
int count) {
+ if (mWebView == null) {
+ throw new AssertionError(
+ "No WebView for FindDialog::onTextChanged");
+ }
CharSequence find = mEditText.getText();
if (0 == find.length()) {
disableButtons();
- // Find is disabled for version 1.0, so find methods on WebView are
- // currently private.
- //mWebView.clearMatches();
- mMatches.setText(R.string.zero);
+ mWebView.clearMatches();
+ mMatchesView.setVisibility(View.INVISIBLE);
} else {
- if (mWebView == null) {
- throw new AssertionError(
- "No WebView for FindDialog::onTextChanged");
+ mMatchesView.setVisibility(View.VISIBLE);
+ int found = mWebView.findAll(find.toString());
+ mMatches.setText(Integer.toString(found));
+ if (found < 2) {
+ disableButtons();
+ if (found == 0) {
+ mMatches.setText(R.string.zero);
+ }
+ } else {
+ mPrevButton.setFocusable(true);
+ mNextButton.setFocusable(true);
+ mPrevButton.setEnabled(true);
+ mNextButton.setEnabled(true);
}
- // Find is disabled for version 1.0, so find methods on WebView are
- // currently private.
- //mWebView.findAll(find.toString(),
- // mFindHandler.obtainMessage(NUM_FOUND));
}
}
diff --git a/src/com/android/browser/GearsBaseDialog.java b/src/com/android/browser/GearsBaseDialog.java
new file mode 100644
index 0000000..c930dc8
--- /dev/null
+++ b/src/com/android/browser/GearsBaseDialog.java
@@ -0,0 +1,336 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.browser;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.os.Handler;
+import android.util.Log;
+import android.view.InflateException;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import java.io.InputStream;
+import java.io.IOException;
+import java.lang.ClassCastException;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * Base dialog class for gears
+ */
+class GearsBaseDialog {
+
+ private static final String TAG = "GearsNativeDialog";
+ protected Handler mHandler;
+ protected Activity mActivity;
+ protected String mDialogArguments;
+
+ private Bitmap mIcon;
+ private final int MAX_ICON_SIZE = 64;
+ protected int mChoosenIconSize;
+
+ // Dialog closing types
+ public static final int CANCEL = 0;
+ public static final int ALWAYS_DENY = 1;
+ public static final int ALLOW = 2;
+ public static final int DENY = 3;
+ public static final int NEW_ICON = 4;
+ public static final int UPDATE_ICON = 5;
+ public static final int REQUEST_ICON = 6;
+ public static final int PAUSE_REQUEST_ICON = 7;
+
+ protected final String LOCAL_DATA_STRING = "localData";
+ protected final String LOCAL_STORAGE_STRING = "localStorage";
+ protected final String LOCATION_DATA_STRING = "locationData";
+
+ protected String mGearsVersion = "UNDEFINED";
+ protected boolean mDebug = false;
+
+ public GearsBaseDialog(Activity activity, Handler handler, String arguments) {
+ mActivity = activity;
+ mHandler = handler;
+ mDialogArguments = arguments;
+ }
+
+ Resources getResources() {
+ return mActivity.getResources();
+ }
+
+ Object getSystemService(String name) {
+ return mActivity.getSystemService(name);
+ }
+
+ View findViewById(int id) {
+ return mActivity.findViewById(id);
+ }
+
+ private String getString(int id) {
+ return mActivity.getString(id);
+ }
+
+ public void setDebug(boolean debug) {
+ mDebug = debug;
+ }
+
+ public void setGearsVersion(String version) {
+ mGearsVersion = version;
+ }
+
+ public String closeDialog(int closingType) {
+ return null;
+ }
+
+ /*
+ * Utility methods for setting up the dialogs elements
+ */
+
+ /**
+ * Inflate a given layout in a view (which has to be
+ * a ViewGroup, e.g. LinearLayout).
+ * This is used to share the basic dialog outline among
+ * the different dialog types.
+ */
+ void inflate(int layout, int viewID) {
+ LayoutInflater inflater = (LayoutInflater) getSystemService(
+ Context.LAYOUT_INFLATER_SERVICE);
+ View view = findViewById(viewID);
+ if (view != null) {
+ try {
+ ViewGroup viewGroup = (ViewGroup) view;
+ inflater.inflate(layout, viewGroup);
+ } catch (ClassCastException e) {
+ String msg = "exception, the view (" + view + ")";
+ msg += " is not a ViewGroup";
+ Log.e(TAG, msg, e);
+ } catch (InflateException e) {
+ Log.e(TAG, "exception while inflating the layout", e);
+ }
+ } else {
+ String msg = "problem, trying to inflate a non-existent view";
+ msg += " (" + viewID + ")";
+ Log.e(TAG, msg);
+ }
+ }
+
+ /**
+ * Button setup.
+ * Set the button's text and its listener. If the text resource's id
+ * is 0, makes the button invisible.
+ */
+ void setupButton(int buttonRscID,
+ int rscString,
+ View.OnClickListener listener) {
+ View view = findViewById(buttonRscID);
+ if (view == null) {
+ return;
+ }
+ Button button = (Button) view;
+
+ if (rscString == 0) {
+ button.setVisibility(View.GONE);
+ } else {
+ CharSequence text = getString(rscString);
+ button.setText(text);
+ button.setOnClickListener(listener);
+ }
+ }
+
+ /**
+ * Utility method to setup the three dialog buttons.
+ */
+ void setupButtons(int alwaysDenyRsc, int allowRsc, int denyRsc) {
+ setupButton(R.id.button_alwaysdeny, alwaysDenyRsc,
+ new Button.OnClickListener() {
+ public void onClick(View v) {
+ mHandler.sendEmptyMessage(ALWAYS_DENY);
+ }
+ });
+
+ setupButton(R.id.button_allow, allowRsc,
+ new Button.OnClickListener() {
+ public void onClick(View v) {
+ mHandler.sendEmptyMessage(ALLOW);
+ }
+ });
+
+ setupButton(R.id.button_deny, denyRsc,
+ new Button.OnClickListener() {
+ public void onClick(View v) {
+ mHandler.sendEmptyMessage(DENY);
+ }
+ });
+ }
+
+ /**
+ * Utility method to set elements' text indicated in
+ * the dialogs' arguments.
+ */
+ void setLabel(JSONObject json, String name, int rsc) {
+ try {
+ if (json.has(name)) {
+ String text = json.getString(name);
+ View view = findViewById(rsc);
+ if (view != null && text != null) {
+ TextView textView = (TextView) view;
+ textView.setText(text);
+ textView.setVisibility(View.VISIBLE);
+ }
+ }
+ } catch (JSONException e) {
+ Log.e(TAG, "json exception", e);
+ }
+ }
+
+ /**
+ * Utility class to download an icon in the background.
+ * Once done ask the UI thread to update the icon.
+ */
+ class IconDownload implements Runnable {
+ private String mUrlString;
+
+ IconDownload(String url) {
+ mUrlString = url;
+ }
+
+ public void run() {
+ if (mUrlString == null) {
+ return;
+ }
+ try {
+ URL url = new URL(mUrlString);
+ HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+ connection.setDoInput(true);
+ connection.connect();
+ int length = connection.getContentLength();
+ InputStream is = connection.getInputStream();
+ Bitmap customIcon = BitmapFactory.decodeStream(is);
+ if (customIcon != null) {
+ mIcon = customIcon;
+ mHandler.sendEmptyMessage(UPDATE_ICON);
+ }
+ } catch (ClassCastException e) {
+ Log.e(TAG, "Class cast exception (" + mUrlString + ")", e);
+ } catch (MalformedURLException e) {
+ Log.e(TAG, "Malformed url (" + mUrlString + ") ", e);
+ } catch (IOException e) {
+ Log.e(TAG, "Exception downloading icon (" + mUrlString + ") ", e);
+ }
+ }
+ }
+
+ /**
+ * Utility method to update the icon.
+ * Called on the UI thread.
+ */
+ public void updateIcon() {
+ if (mIcon == null) {
+ return;
+ }
+ View view = findViewById(R.id.origin_icon);
+ if (view != null) {
+ ImageView imageView = (ImageView) view;
+ imageView.setMaxHeight(MAX_ICON_SIZE);
+ imageView.setMaxWidth(MAX_ICON_SIZE);
+ imageView.setScaleType(ImageView.ScaleType.FIT_XY);
+ imageView.setImageBitmap(mIcon);
+ imageView.setVisibility(View.VISIBLE);
+ }
+ }
+
+ /**
+ * Utility method to download an icon from a url and set
+ * it to the GUI element R.id.origin_icon.
+ * It is used both in the shortcut dialog and the
+ * permission dialog.
+ * The actual download is done in the background via
+ * IconDownload; once the icon is downlowded the UI is updated
+ * via updateIcon().
+ * The icon size is included in the layout with the choosen
+ * size, although not displayed, to limit text reflow once
+ * the icon is received.
+ */
+ void downloadIcon(String url) {
+ if (url == null) {
+ return;
+ }
+ View view = findViewById(R.id.origin_icon);
+ if (view != null) {
+ view.setMinimumWidth(mChoosenIconSize);
+ view.setMinimumHeight(mChoosenIconSize);
+ view.setVisibility(View.INVISIBLE);
+ }
+ Thread thread = new Thread(new IconDownload(url));
+ thread.start();
+ }
+
+ /**
+ * Utility method that get the dialogMessage
+ * and icon and ask the setupDialog(message,icon)
+ * method to set the values.
+ */
+ public void setupDialog() {
+ TextView dialogMessage = null;
+ ImageView icon = null;
+
+ View view = findViewById(R.id.dialog_message);
+ if (view != null) {
+ dialogMessage = (TextView) view;
+ }
+
+ View iconView = findViewById(R.id.icon);
+ if (iconView != null) {
+ icon = (ImageView) iconView;
+ }
+
+ if ((dialogMessage != null) && (icon != null)) {
+ setupDialog(dialogMessage, icon);
+ dialogMessage.setVisibility(View.VISIBLE);
+ }
+ }
+
+ /*
+ * Set the message and icon of the dialog
+ */
+ public void setupDialog(TextView message, ImageView icon) {
+ message.setText(R.string.unrecognized_dialog_message);
+ icon.setImageResource(R.drawable.gears_icon_48x48);
+ message.setVisibility(View.VISIBLE);
+ }
+
+ /**
+ * Setup the dialog
+ * By default, just display a simple message.
+ */
+ public void setup() {
+ setupButtons(0, 0, R.string.default_button);
+ setupDialog();
+ }
+
+
+}
diff --git a/src/com/android/browser/GearsDialog.java b/src/com/android/browser/GearsDialog.java
index fd9e762..62a8aaa 100644
--- a/src/com/android/browser/GearsDialog.java
+++ b/src/com/android/browser/GearsDialog.java
@@ -48,6 +48,8 @@
private String htmlContent;
private String dialogArguments;
+ private boolean dismissed = false;
+
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
@@ -66,14 +68,24 @@
}
@Override
+ public void onDestroy() {
+ super.onDestroy();
+ // In case we reach this point without
+ // notifying GearsDialogService, we do it now.
+ if (!dismissed) {
+ notifyEndOfDialog();
+ }
+ }
+
+ @Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
Intent i = getIntent();
boolean inSettings = i.getBooleanExtra("inSettings", false);
// If we are called from the settings, we
- // dismiss ourselve upon upon rotation
+ // dismiss ourselve upon rotation
if (inSettings) {
- GearsDialogService.signalFinishedDialog();
+ notifyEndOfDialog();
finish();
}
}
@@ -88,10 +100,22 @@
webview.loadDataWithBaseURL("", htmlContent, "text/html", "", "");
}
+ /**
+ * Signal to GearsDialogService that we are done.
+ */
+ private void notifyEndOfDialog() {
+ GearsDialogService.signalFinishedDialog();
+ dismissed = true;
+ }
+
+ /**
+ * Intercepts the back key to immediately notify
+ * GearsDialogService that we are done.
+ */
public boolean dispatchKeyEvent(KeyEvent event) {
if (event.getKeyCode() == KeyEvent.KEYCODE_BACK && event.isDown()) {
- GearsDialogService.signalFinishedDialog();
- }
+ notifyEndOfDialog();
+ }
return super.dispatchKeyEvent(event);
}
@@ -110,7 +134,7 @@
*/
public void closeDialog(String results) {
GearsDialogService.closeDialog(results);
- GearsDialogService.signalFinishedDialog();
+ notifyEndOfDialog();
finish();
}
diff --git a/src/com/android/browser/GearsFilePickerDialog.java b/src/com/android/browser/GearsFilePickerDialog.java
new file mode 100644
index 0000000..4a3c0ed
--- /dev/null
+++ b/src/com/android/browser/GearsFilePickerDialog.java
@@ -0,0 +1,629 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.browser;
+
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Color;
+import android.net.Uri;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.provider.MediaStore;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.BaseAdapter;
+import android.widget.GridView;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Vector;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+
+/**
+ * Gears FilePicker dialog
+ */
+class GearsFilePickerDialog extends GearsBaseDialog
+ implements View.OnTouchListener {
+
+ private static final String TAG = "Gears FilePicker";
+ private static Bitmap mDirectoryIcon;
+ private static Bitmap mDefaultIcon;
+ private static Bitmap mImageIcon;
+ private static Bitmap mBackIcon;
+ private static ImagesLoad mImagesLoader;
+ private FilePickerAdapter mAdapter;
+
+ public GearsFilePickerDialog(Activity activity,
+ Handler handler,
+ String arguments) {
+ super (activity, handler, arguments);
+ mAdapter = new FilePickerAdapter(activity);
+ }
+
+ public void setup() {
+ inflate(R.layout.gears_dialog_filepicker, R.id.panel_content);
+ setupButtons(0,
+ R.string.filepicker_button_allow,
+ R.string.filepicker_button_deny);
+ setupDialog();
+ GridView view = (GridView) findViewById(R.id.files_list);
+ view.setAdapter(mAdapter);
+ view.setOnTouchListener(this);
+
+ mImagesLoader = new ImagesLoad(mAdapter);
+ mImagesLoader.setAdapterView(view);
+ Thread thread = new Thread(mImagesLoader);
+ thread.start();
+ }
+
+ public void setupDialog(TextView message, ImageView icon) {
+ message.setText(R.string.filepicker_message);
+ message.setTextSize(24);
+ icon.setImageResource(R.drawable.gears_icon_48x48);
+ }
+
+ public boolean onTouch(View v, MotionEvent event) {
+ mImagesLoader.pauseIconRequest();
+ return false;
+ }
+
+ /**
+ * Utility class to load and generate thumbnails
+ * for image files
+ */
+ class ImagesLoad implements Runnable {
+ private Map mImagesMap;
+ private Vector mImagesPath;
+ private BaseAdapter mAdapter;
+ private AdapterView mAdapterView;
+ private Vector<FilePickerElement> mElements;
+ private Handler mLoaderHandler;
+
+ ImagesLoad(BaseAdapter adapter) {
+ mAdapter = adapter;
+ }
+
+ public void signalChanges() {
+ Message message = mHandler.obtainMessage(GearsBaseDialog.NEW_ICON,
+ mAdapter);
+ mHandler.sendMessage(message);
+ }
+
+ /**
+ * TODO: use the same thumbnails as the photo app
+ * (bug: http://b/issue?id=1497927)
+ */
+ public String getThumbnailPath(String path) {
+ File f = new File(path);
+ String myPath = f.getParent() + "/.thumbnails";
+ File d = new File(myPath);
+ if (!d.exists()) {
+ d.mkdirs();
+ }
+ return myPath + "/" + f.getName();
+ }
+
+ public boolean saveImage(String path, Bitmap image) {
+ boolean ret = false;
+ try {
+ FileOutputStream outStream = new FileOutputStream(path);
+ ret = image.compress(Bitmap.CompressFormat.JPEG, 100, outStream);
+ } catch (IOException e) {
+ Log.e(TAG, "IOException ", e);
+ }
+ return ret;
+ }
+
+ public Bitmap generateImage(FilePickerElement elem) {
+ String path = elem.getPath();
+ Bitmap finalImage = null;
+ try {
+ String thumbnailPath = getThumbnailPath(path);
+ File thumbnail = new File(thumbnailPath);
+ if (thumbnail.exists()) {
+ finalImage = BitmapFactory.decodeFile(thumbnailPath);
+ if (finalImage != null) {
+ return finalImage;
+ }
+ }
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeFile(path, options);
+
+ int width = options.outWidth;
+ int height = options.outHeight;
+ int size = 128;
+ int sampleSize = 1;
+ if (width > size || height > size) {
+ sampleSize = 2;
+ while ((width / sampleSize > size)
+ || (height / sampleSize > size)) {
+ sampleSize += 2;
+ }
+ }
+ options.inJustDecodeBounds = false;
+ options.inSampleSize = sampleSize;
+ Bitmap originalImage = BitmapFactory.decodeFile(path, options);
+ if (originalImage == null) {
+ return null;
+ }
+ finalImage = Bitmap.createScaledBitmap(originalImage, size, size, true);
+ if (saveImage(thumbnailPath, finalImage)) {
+ if (mDebug) {
+ Log.v(TAG, "Saved thumbnail for file " + path);
+ }
+ } else {
+ Log.e(TAG, "Could NOT Save thumbnail for file " + path);
+ }
+ originalImage.recycle();
+ } catch (java.lang.OutOfMemoryError e) {
+ Log.e(TAG, "Intercepted OOM ", e);
+ }
+ return finalImage;
+ }
+
+ public void pauseIconRequest() {
+ Message message = Message.obtain(mLoaderHandler,
+ GearsBaseDialog.PAUSE_REQUEST_ICON);
+ mLoaderHandler.sendMessageAtFrontOfQueue(message);
+ }
+ public void postIconRequest(FilePickerElement item, int position) {
+ if (item == null) {
+ return;
+ }
+ Message message = mLoaderHandler.obtainMessage(
+ GearsBaseDialog.REQUEST_ICON, position, 0, item);
+ mLoaderHandler.sendMessage(message);
+ }
+
+ public void generateIcon(FilePickerElement elem) {
+ if (elem.isImage()) {
+ if (elem.getThumbnail() == null) {
+ Bitmap image = generateImage(elem);
+ if (image != null) {
+ elem.setThumbnail(image);
+ }
+ }
+ }
+ }
+
+ public void setAdapterView(AdapterView view) {
+ mAdapterView = view;
+ }
+
+ public void run() {
+ Looper.prepare();
+ mLoaderHandler = new Handler() {
+ public void handleMessage(Message msg) {
+ int visibleElements = 10;
+ if (msg.what == GearsBaseDialog.PAUSE_REQUEST_ICON) {
+ try {
+ // We are busy (likely) scrolling the view,
+ // so we just pause the loading.
+ Thread.sleep(1000);
+ mLoaderHandler.removeMessages(
+ GearsBaseDialog.PAUSE_REQUEST_ICON);
+ } catch (InterruptedException e) {
+ Log.e(TAG, "InterruptedException ", e);
+ }
+ } else if (msg.what == GearsBaseDialog.REQUEST_ICON) {
+ try {
+ Thread.sleep(10);
+ } catch (InterruptedException e) {
+ Log.e(TAG, "InterruptedException ", e);
+ }
+ FilePickerElement elem = (FilePickerElement) msg.obj;
+ int firstVisiblePosition = mAdapterView.getFirstVisiblePosition();
+ // If the elements are not visible, we slow down the update
+ // TODO: replace this by a low-priority thread
+ if ((msg.arg1 < firstVisiblePosition - visibleElements)
+ && msg.arg1 > firstVisiblePosition + visibleElements) {
+ try {
+ Thread.sleep(100);
+ } catch (InterruptedException e) {
+ }
+ }
+ generateIcon(elem);
+ signalChanges();
+ }
+ }
+ };
+ Looper.loop();
+ }
+ }
+
+ /**
+ * Utility class representing an element displayed in the
+ * file picker, associated with an icon and/or thumbnail
+ */
+ class FilePickerElement {
+ private File mPath;
+ private String mName;
+ private Bitmap mIcon;
+ private boolean mIsSelected;
+ private Vector mChildren;
+ private FilePickerElement mParent;
+ private boolean mIsParent;
+ private BaseAdapter mAdapter;
+ private String mExtension;
+ private Bitmap mThumbnail;
+ private boolean mIsImage;
+
+ public FilePickerElement(String name, BaseAdapter adapter) {
+ this(name, adapter, null);
+ }
+
+ public FilePickerElement(String path, String name, BaseAdapter adapter) {
+ this(path, name, adapter, null);
+ }
+
+ public FilePickerElement(String name,
+ BaseAdapter adapter,
+ FilePickerElement parent) {
+ mName = name;
+ mAdapter = adapter;
+ mParent = parent;
+ mIsSelected = false;
+ mChildren = null;
+ }
+
+ public FilePickerElement(String path,
+ String name,
+ BaseAdapter adapter,
+ FilePickerElement parent) {
+ mPath = new File(path);
+ mName = name;
+ mIsSelected = false;
+ mChildren = null;
+ mParent = parent;
+ mAdapter = adapter;
+ mExtension = null;
+
+ setIcons();
+ }
+
+ public void setIcons() {
+ if (mPath.isDirectory()) {
+ if (mDirectoryIcon == null) {
+ mDirectoryIcon = BitmapFactory.decodeResource(
+ getResources(), R.drawable.gears_folder);
+ }
+ mIcon = mDirectoryIcon;
+
+ } else {
+ if (isImage()) {
+ if (mImageIcon == null) {
+ mImageIcon = BitmapFactory.decodeResource(
+ getResources(), R.drawable.gears_file_image);
+ }
+ mIcon = mImageIcon;
+ } else if (isAudio()) {
+ mIcon = BitmapFactory.decodeResource(
+ getResources(), R.drawable.gears_file_audio);
+ } else if (isVideo()) {
+ mIcon = BitmapFactory.decodeResource(
+ getResources(), R.drawable.gears_file_video);
+ } else {
+ if (mDefaultIcon == null) {
+ mDefaultIcon = BitmapFactory.decodeResource(
+ getResources(), R.drawable.gears_file_unknown);
+ }
+ mIcon = mDefaultIcon;
+ }
+ }
+ if (mBackIcon == null) {
+ mBackIcon = BitmapFactory.decodeResource(getResources(),
+ R.drawable.gears_back);
+ }
+ }
+
+ public boolean isImage() {
+ if (mIsImage) return mIsImage;
+ String extension = getExtension();
+ if (extension != null) {
+ if (extension.equalsIgnoreCase("jpg") ||
+ extension.equalsIgnoreCase("jpeg") ||
+ extension.equalsIgnoreCase("png") ||
+ extension.equalsIgnoreCase("gif")) {
+ mIsImage = true;
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public boolean isAudio() {
+ String extension = getExtension();
+ if (extension != null) {
+ if (extension.equalsIgnoreCase("mp3") ||
+ extension.equalsIgnoreCase("wav") ||
+ extension.equalsIgnoreCase("aac")) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public boolean isVideo() {
+ String extension = getExtension();
+ if (extension != null) {
+ if (extension.equalsIgnoreCase("mpg") ||
+ extension.equalsIgnoreCase("mpeg") ||
+ extension.equalsIgnoreCase("mpe") ||
+ extension.equalsIgnoreCase("divx") ||
+ extension.equalsIgnoreCase("3gpp") ||
+ extension.equalsIgnoreCase("avi")) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public void setParent(boolean isParent) {
+ mIsParent = isParent;
+ }
+
+ public boolean isDirectory() {
+ return mPath.isDirectory();
+ }
+
+ public String getExtension() {
+ if (isDirectory()) {
+ return null;
+ }
+ if (mExtension == null) {
+ String path = getPath();
+ int index = path.lastIndexOf(".");
+ if ((index != -1) && (index != path.length() - 1)){
+ // if we find a dot that is not the last character
+ mExtension = path.substring(index+1);
+ return mExtension;
+ }
+ }
+ return mExtension;
+ }
+
+ public void refresh() {
+ mChildren = null;
+ Vector children = getChildren();
+ for (int i = 0; i < children.size(); i++) {
+ FilePickerElement elem = (FilePickerElement) children.get(i);
+ mImagesLoader.postIconRequest(elem, i);
+ }
+ }
+
+ public Vector getChildren() {
+ if (isDirectory()) {
+ if (mChildren == null) {
+ mChildren = new Vector();
+ File[] files = mPath.listFiles();
+ if (mParent != null) {
+ mChildren.add(mParent);
+ mParent.setParent(true);
+ }
+ for (int i = 0; i < files.length; i++) {
+ String name = files[i].getName();
+ String fpath = files[i].getPath();
+ if (!name.startsWith(".")) { // hide dotfiles
+ FilePickerElement elem = new FilePickerElement(fpath, name,
+ mAdapter, this);
+ elem.setParent(false);
+ mChildren.add(elem);
+ }
+ }
+ }
+ }
+ return mChildren;
+ }
+
+ public FilePickerElement getChild(int position) {
+ Vector children = getChildren();
+ if (children != null) {
+ return (FilePickerElement) children.get(position);
+ }
+ return null;
+ }
+
+ public Bitmap getIcon(int position) {
+ if (mIsParent) {
+ return mBackIcon;
+ }
+ if (isImage()) {
+ if (mThumbnail != null) {
+ return mThumbnail;
+ } else {
+ mImagesLoader.postIconRequest(this, position);
+ }
+ }
+ return mIcon;
+ }
+
+ public Bitmap getThumbnail() {
+ return mThumbnail;
+ }
+
+ public void setThumbnail(Bitmap icon) {
+ mThumbnail = icon;
+ }
+
+ public String getName() {
+ return mName;
+ }
+
+ public String getPath() {
+ return mPath.getPath();
+ }
+
+ public void toggleSelection() {
+ mIsSelected = !mIsSelected;
+ }
+
+ public boolean isSelected() {
+ return mIsSelected;
+ }
+
+ }
+
+ /**
+ * Adapter for the GridView
+ */
+ class FilePickerAdapter extends BaseAdapter {
+ private Context mContext;
+ private Map mImagesMap;
+ private Map mImagesSelected;
+
+ private Vector mImages;
+ private Vector<FilePickerElement> mFiles;
+
+ private FilePickerElement mRootElement;
+ private FilePickerElement mCurrentElement;
+
+ public FilePickerAdapter(Context context) {
+ mContext = context;
+ mImages = new Vector();
+ mFiles = new Vector();
+
+ mImagesMap = Collections.synchronizedMap(new HashMap());
+ mImagesSelected = new HashMap();
+
+ Uri requests[] = { MediaStore.Images.Media.INTERNAL_CONTENT_URI,
+ MediaStore.Images.Media.EXTERNAL_CONTENT_URI };
+
+ String sdCardPath = Environment.getExternalStorageDirectory().getPath();
+ mRootElement = new FilePickerElement(sdCardPath, "SD Card", this);
+ mCurrentElement = mRootElement;
+ }
+
+ public void addImage(String path) {
+ mImages.add(path);
+ Bitmap image = BitmapFactory.decodeResource(
+ getResources(), R.drawable.gears_file_unknown);
+ mImagesMap.put(path, image);
+ mImagesSelected.put(path, Boolean.FALSE);
+ }
+
+ public int getCount() {
+ Vector elems = mCurrentElement.getChildren();
+ return elems.size();
+ }
+
+ public Object getItem(int position) {
+ return position;
+ }
+
+ public long getItemId(int position) {
+ return position;
+ }
+
+ public Vector selectedElements() {
+ if (mCurrentElement == null) {
+ return null;
+ }
+ Vector children = mCurrentElement.getChildren();
+ Vector ret = new Vector();
+ for (int i = 0; i < children.size(); i++) {
+ FilePickerElement elem = (FilePickerElement) children.get(i);
+ if (elem.isSelected()) {
+ ret.add(elem);
+ }
+ }
+ return ret;
+ }
+
+ public View getView(int position, View convertView, ViewGroup parent) {
+ View cell = convertView;
+ if (cell == null) {
+ LayoutInflater inflater = (LayoutInflater) getSystemService(
+ Context.LAYOUT_INFLATER_SERVICE);
+ cell = inflater.inflate(R.layout.gears_dialog_filepicker_cell, null);
+ }
+ ImageView imageView = (ImageView) cell.findViewById(R.id.icon);
+ TextView textView = (TextView) cell.findViewById(R.id.name);
+ FilePickerElement elem = mCurrentElement.getChild(position);
+ if (elem == null) {
+ String message = "Could not get elem " + position;
+ message += " for " + mCurrentElement.getPath();
+ Log.e(TAG, message);
+ return null;
+ }
+ String path = elem.getPath();
+ textView.setText(elem.getName());
+
+ View.OnClickListener listener = new View.OnClickListener() {
+ public void onClick(View view) {
+ int pos = (Integer) view.getTag();
+ FilePickerElement elem = mCurrentElement.getChild(pos);
+ if (elem.isDirectory()) {
+ mCurrentElement = elem;
+ mCurrentElement.refresh();
+ } else {
+ elem.toggleSelection();
+ }
+ notifyDataSetChanged();
+ }
+ };
+ imageView.setOnClickListener(listener);
+ cell.setLayoutParams(new GridView.LayoutParams(96, 96));
+
+ imageView.setTag(position);
+
+ if (elem.isSelected()) {
+ cell.setBackgroundColor(Color.LTGRAY);
+ } else {
+ cell.setBackgroundColor(Color.WHITE);
+ }
+ Bitmap bmp = elem.getIcon(position);
+ if (bmp != null) {
+ imageView.setImageBitmap(bmp);
+ }
+
+ return cell;
+ }
+ }
+
+ private String selectedFiles() {
+ Vector selection = mAdapter.selectedElements();
+ JSONArray jsonSelection = new JSONArray();
+ if (selection != null) {
+ for (int i = 0; i < selection.size(); i++) {
+ FilePickerElement elem = (FilePickerElement) selection.get(i);
+ jsonSelection.put(elem.getPath());
+ }
+ }
+ return jsonSelection.toString();
+ }
+
+ public String closeDialog(int closingType) {
+ return selectedFiles();
+ }
+}
diff --git a/src/com/android/browser/GearsNativeDialog.java b/src/com/android/browser/GearsNativeDialog.java
new file mode 100644
index 0000000..c8ae741
--- /dev/null
+++ b/src/com/android/browser/GearsNativeDialog.java
@@ -0,0 +1,274 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.browser;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.util.Config;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.Window;
+import android.widget.BaseAdapter;
+
+import android.webkit.gears.NativeDialog;
+
+import com.android.browser.GearsBaseDialog;
+import com.android.browser.GearsPermissionsDialog;
+import com.android.browser.GearsSettingsDialog;
+import com.android.browser.GearsShortcutDialog;
+import com.android.browser.GearsFilePickerDialog;
+
+/**
+ * Native dialog Activity used by gears
+ * TODO: rename in GearsNativeDialogActivity
+ * @hide
+ */
+public class GearsNativeDialog extends Activity {
+
+ private static final String TAG = "GearsNativeDialog";
+
+ private String mDialogArguments;
+
+ private String mGearsVersion = null;
+
+ private boolean mDebug = false;
+
+ private int mDialogType;
+ private final int SETTINGS_DIALOG = 1;
+ private final int PERMISSION_DIALOG = 2;
+ private final int SHORTCUT_DIALOG = 3;
+ private final int LOCATION_DIALOG = 4;
+ private final int FILEPICKER_DIALOG = 5;
+
+ private final String VERSION_STRING = "version";
+ private final String SETTINGS_DIALOG_STRING = "settings_dialog";
+ private final String PERMISSION_DIALOG_STRING = "permissions_dialog";
+ private final String SHORTCUT_DIALOG_STRING = "shortcuts_dialog";
+ private final String LOCATION_DIALOG_STRING = "locations_dialog";
+ private final String FILEPICKER_DIALOG_STRING = "filepicker_dialog";
+
+ private boolean mDialogDismissed = false;
+
+ GearsBaseDialog dialog;
+
+ // Handler for callbacks to the UI thread
+ final Handler mHandler = new Handler() {
+ public void handleMessage(Message msg) {
+ if (msg.what == GearsBaseDialog.NEW_ICON) {
+ BaseAdapter adapter = (BaseAdapter) msg.obj;
+ adapter.notifyDataSetChanged();
+ } else if (msg.what == GearsBaseDialog.UPDATE_ICON) {
+ dialog.updateIcon();
+ } else if (msg.what == GearsBaseDialog.ALWAYS_DENY) {
+ closeDialog(GearsBaseDialog.ALWAYS_DENY);
+ } else if (msg.what == GearsBaseDialog.ALLOW) {
+ closeDialog(GearsBaseDialog.ALLOW);
+ } else if (msg.what == GearsBaseDialog.DENY) {
+ closeDialog(GearsBaseDialog.DENY);
+ }
+ super.handleMessage(msg);
+ }
+ };
+
+ @Override
+ public void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+ requestWindowFeature(Window.FEATURE_NO_TITLE);
+ setContentView(R.layout.gears_dialog);
+ getArguments();
+
+ switch (mDialogType) {
+ case SETTINGS_DIALOG:
+ dialog = new GearsSettingsDialog(this, mHandler, mDialogArguments);
+ dialog.setGearsVersion(mGearsVersion);
+ break;
+ case PERMISSION_DIALOG:
+ dialog = new GearsPermissionsDialog(this, mHandler, mDialogArguments);
+ break;
+ case SHORTCUT_DIALOG:
+ dialog = new GearsShortcutDialog(this, mHandler, mDialogArguments);
+ break;
+ case LOCATION_DIALOG:
+ dialog = new GearsPermissionsDialog(this, mHandler, mDialogArguments);
+ break;
+ case FILEPICKER_DIALOG:
+ dialog = new GearsFilePickerDialog(this, mHandler, mDialogArguments);
+ break;
+ default:
+ dialog = new GearsBaseDialog(this, mHandler, mDialogArguments);
+ }
+ dialog.setDebug(mDebug);
+ dialog.setup();
+ }
+
+ /**
+ * Get the arguments for the dialog
+ *
+ * The dialog needs a json string as an argument, as
+ * well as a dialogType. In debug mode the arguments
+ * are mocked.
+ */
+ private void getArguments() {
+ if (mDebug) {
+ mDialogType = FILEPICKER_DIALOG +1;
+ mockArguments();
+
+ return;
+ }
+
+ Intent intent = getIntent();
+ mDialogArguments = intent.getStringExtra("dialogArguments");
+ String dialogTypeString = intent.getStringExtra("dialogType");
+ if (dialogTypeString == null) {
+ return;
+ }
+
+ if (Config.LOGV) {
+ Log.v(TAG, "dialogtype: " + dialogTypeString);
+ }
+
+ if (dialogTypeString.equalsIgnoreCase(SETTINGS_DIALOG_STRING)) {
+ mDialogType = SETTINGS_DIALOG;
+ mGearsVersion = intent.getStringExtra(VERSION_STRING);
+ } else if (dialogTypeString.equalsIgnoreCase(PERMISSION_DIALOG_STRING)) {
+ mDialogType = PERMISSION_DIALOG;
+ } else if (dialogTypeString.equalsIgnoreCase(SHORTCUT_DIALOG_STRING)) {
+ mDialogType = SHORTCUT_DIALOG;
+ } else if (dialogTypeString.equalsIgnoreCase(LOCATION_DIALOG_STRING)) {
+ mDialogType = LOCATION_DIALOG;
+ } else if (dialogTypeString.equalsIgnoreCase(FILEPICKER_DIALOG_STRING)) {
+ mDialogType = FILEPICKER_DIALOG;
+ }
+ }
+
+ /**
+ * Utility method for debugging the dialog.
+ *
+ * Set mock arguments.
+ */
+ private void mockArguments() {
+ String argumentsShortcuts = "{ locale: \"en-US\","
+ + "name: \"My Application\", link: \"http://www.google.com/\","
+ + "description: \"This application does things does things!\","
+ + "icon16x16: \"http://google-gears.googlecode.com/"
+ + "svn/trunk/gears/test/manual/shortcuts/16.png\","
+ + "icon32x32: \"http://google-gears.googlecode.com/"
+ + "svn/trunk/gears/test/manual/shortcuts/32.png\","
+ + "icon48x48: \"http://google-gears.googlecode.com/"
+ + "svn/trunk/gears/test/manual/shortcuts/48.png\","
+ + "icon128x128: \"http://google-gears.googlecode.com/"
+ + "svn/trunk/gears/test/manual/shortcuts/128.png\"}";
+
+ String argumentsPermissions = "{ locale: \"en-US\", "
+ + "origin: \"http://www.google.com\", dialogType: \"localData\","
+ + "customIcon: \"http://google-gears.googlecode.com/"
+ + "svn/trunk/gears/test/manual/shortcuts/32.png\","
+ + "customName: \"My Application\","
+ + "customMessage: \"Press the button to enable my "
+ + "application to run offline!\" };";
+
+ String argumentsLocation = "{ locale: \"en-US\", "
+ + "origin: \"http://www.google.com\", dialogType: \"locationData\","
+ + "customIcon: \"http://google-gears.googlecode.com/"
+ + "svn/trunk/gears/test/manual/shortcuts/32.png\","
+ + "customName: \"My Application\","
+ + "customMessage: \"Press the button to enable my "
+ + "application to run offline!\" };";
+
+ String argumentsSettings = "{ locale: \"en-US\", permissions: [ { "
+ + "name: \"http://www.google.com\", "
+ + "localStorage: { permissionState: 1 }, "
+ + "locationData: { permissionState: 0 } }, "
+ + "{ name: \"http://www.aaronboodman.com\", "
+ + "localStorage: { permissionState: 1 }, "
+ + "locationData: { permissionState: 2 } }, "
+ + "{ name: \"http://www.evil.org\", "
+ + "localStorage: { permissionState: 2 }, "
+ + "locationData: { permissionState: 2 } } ] }";
+
+ switch (mDialogType) {
+ case SHORTCUT_DIALOG:
+ mDialogArguments = argumentsShortcuts;
+ break;
+ case PERMISSION_DIALOG:
+ mDialogArguments = argumentsPermissions;
+ break;
+ case LOCATION_DIALOG:
+ mDialogArguments = argumentsLocation;
+ break;
+ case SETTINGS_DIALOG:
+ mDialogArguments = argumentsSettings;
+ }
+ }
+
+ /**
+ * Close the dialog and set the return string value.
+ */
+ private void closeDialog(int closingType) {
+ String ret = dialog.closeDialog(closingType);
+
+ if (mDebug) {
+ Log.v(TAG, "closeDialog ret value: " + ret);
+ }
+
+ NativeDialog.closeDialog(ret);
+ notifyEndOfDialog();
+ finish();
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ // In case we reach this point without
+ // notifying NativeDialog, we do it now.
+ if (!mDialogDismissed) {
+ notifyEndOfDialog();
+ }
+ }
+
+ @Override
+ public void onPause(){
+ super.onPause();
+ if (!mDialogDismissed) {
+ closeDialog(GearsBaseDialog.CANCEL);
+ }
+ }
+
+ /**
+ * Signal to NativeDialog that we are done.
+ */
+ private void notifyEndOfDialog() {
+ NativeDialog.signalFinishedDialog();
+ mDialogDismissed = true;
+ }
+
+ /**
+ * Intercepts the back key to immediately notify
+ * NativeDialog that we are done.
+ */
+ public boolean dispatchKeyEvent(KeyEvent event) {
+ if (event.getKeyCode() == KeyEvent.KEYCODE_BACK && event.isDown()) {
+ closeDialog(GearsBaseDialog.CANCEL);
+ }
+ return super.dispatchKeyEvent(event);
+ }
+
+}
diff --git a/src/com/android/browser/GearsPermissions.java b/src/com/android/browser/GearsPermissions.java
new file mode 100644
index 0000000..cd46324
--- /dev/null
+++ b/src/com/android/browser/GearsPermissions.java
@@ -0,0 +1,196 @@
+/*
+ * Copyright (C) 2008 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.util.Log;
+
+import java.util.HashMap;
+import java.util.Iterator;
+
+/**
+ * The permission mechanism works the following way:
+ *
+ * PermissionType allows to define a type of permission
+ * (e.g. localStorage/locationData), storing a name and a set of
+ * resource ids corresponding to the GUI resources.
+ *
+ * Permission defines an actual permission instance, with a type and a value.
+ *
+ * OriginPermissions holds an origin with a set of Permission objects
+ */
+class GearsPermissions {
+
+ private static final String TAG = "GearsPermissions";
+
+ /**
+ * Defines a type of permission
+ *
+ * Store the permission's name (used in the json result)
+ * Graphically, each permission is a label followed by two radio buttons.
+ * We store the resources ids here.
+ */
+ public static class PermissionType {
+ public static final int PERMISSION_NOT_SET = 0;
+ public static final int PERMISSION_ALLOWED = 1;
+ public static final int PERMISSION_DENIED = 2;
+
+ String mName;
+ int mRowRsc;
+ int mAllowedButtonRsc;
+ int mDeniedButtonRsc;
+
+ PermissionType(String name) {
+ mName = name;
+ }
+
+ public void setResources(int rowRsc, int allowedButtonRsc,
+ int deniedButtonRsc) {
+ mRowRsc = rowRsc;
+ mAllowedButtonRsc = allowedButtonRsc;
+ mDeniedButtonRsc = deniedButtonRsc;
+ }
+
+ public int getRowRsc() {
+ return mRowRsc;
+ }
+
+ public int getAllowedButtonRsc() {
+ return mAllowedButtonRsc;
+ }
+
+ public int getDeniedButtonRsc() {
+ return mDeniedButtonRsc;
+ }
+
+ public String getName() {
+ return mName;
+ }
+
+ }
+
+ /**
+ * Simple class to store an instance of a permission
+ *
+ * i.e. a permission type and a value
+ * Value can be either PERMISSION_NOT_SET,
+ * PERMISSION_ALLOWED or PERMISSION_DENIED
+ * (defined in PermissionType).
+ */
+ public static class Permission {
+ PermissionType mType;
+ int mValue;
+
+ Permission(PermissionType type, int value) {
+ mType = type;
+ mValue = value;
+ }
+
+ Permission(PermissionType type) {
+ mType = type;
+ mValue = 0;
+ }
+
+ public PermissionType getType() {
+ return mType;
+ }
+
+ public void setValue(int value) {
+ mValue = value;
+ }
+
+ public int getValue() {
+ return mValue;
+ }
+ }
+
+ /**
+ * Interface used by the GearsNativeDialog implementation
+ * to listen to changes in the permissions.
+ */
+ public interface PermissionsChangesListener {
+ public boolean setPermission(PermissionType type, int perm);
+ }
+
+ /**
+ * Holds the model for an origin -- each origin has a set of
+ * permissions.
+ */
+ public static class OriginPermissions {
+ HashMap<PermissionType, Permission> mPermissions;
+ String mOrigin;
+ public static PermissionsChangesListener mListener;
+
+ public static void setListener(PermissionsChangesListener listener) {
+ mListener = listener;
+ }
+
+ OriginPermissions(String anOrigin) {
+ mOrigin = anOrigin;
+ mPermissions = new HashMap<PermissionType, Permission>();
+ }
+
+ OriginPermissions(OriginPermissions perms) {
+ mOrigin = perms.getOrigin();
+ mPermissions = new HashMap<PermissionType, Permission>();
+ HashMap<PermissionType, Permission> permissions = perms.getPermissions();
+ Iterator<PermissionType> iterator = permissions.keySet().iterator();
+ while (iterator.hasNext()) {
+ Permission permission = permissions.get(iterator.next());
+ int value = permission.getValue();
+ setPermission(permission.getType(), value);
+ }
+ }
+
+ public String getOrigin() {
+ return mOrigin;
+ }
+
+ public HashMap<PermissionType, Permission> getPermissions() {
+ return mPermissions;
+ }
+
+ public int getPermission(PermissionType type) {
+ return mPermissions.get(type).getValue();
+ }
+
+ public void setPermission(PermissionType type, int perm) {
+ if (mPermissions.get(type) == null) {
+ Permission permission = new Permission(type, perm);
+ mPermissions.put(type, permission);
+ return;
+ }
+
+ if (mListener != null) {
+ mListener.setPermission(type, perm);
+ }
+
+ mPermissions.get(type).setValue(perm);
+ }
+
+ public void print() {
+ Log.v(TAG, "Permissions for " + mOrigin);
+ Iterator<PermissionType> iterator = mPermissions.keySet().iterator();
+ while (iterator.hasNext()) {
+ Permission permission = mPermissions.get(iterator.next());
+ String name = permission.getType().getName();
+ int value = permission.getValue();
+ Log.v(TAG, " " + name + ": " + value);
+ }
+ }
+ }
+
+}
diff --git a/src/com/android/browser/GearsPermissionsDialog.java b/src/com/android/browser/GearsPermissionsDialog.java
new file mode 100644
index 0000000..b57ab0b
--- /dev/null
+++ b/src/com/android/browser/GearsPermissionsDialog.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.browser;
+
+import android.app.Activity;
+import android.os.Handler;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * Gears permission dialog
+ */
+class GearsPermissionsDialog extends GearsBaseDialog {
+
+ private static final String TAG = "GearsPermissionsDialog";
+
+ private String mDialogType;
+
+ public GearsPermissionsDialog(Activity activity,
+ Handler handler,
+ String arguments) {
+ super (activity, handler, arguments);
+ }
+
+ public void setup() {
+ inflate(R.layout.gears_dialog_permission, R.id.panel_content);
+ setupButtons(R.string.permission_button_alwaysdeny,
+ R.string.permission_button_allow,
+ R.string.permission_button_deny);
+
+ View contentBorder = findViewById(R.id.content_border);
+ if (contentBorder != null) {
+ contentBorder.setBackgroundResource(R.color.permission_border);
+ }
+ View contentBackground = findViewById(R.id.content_background);
+ if (contentBackground != null) {
+ contentBackground.setBackgroundResource(R.color.permission_background);
+ }
+
+ try {
+ JSONObject json = new JSONObject(mDialogArguments);
+
+ if (json.has("dialogType")) {
+ mDialogType = json.getString("dialogType");
+ setupDialog();
+ }
+
+ if (!json.has("customName")) {
+ setLabel(json, "origin", R.id.origin_title);
+ View titleView = findViewById(R.id.origin_title);
+ if (titleView != null) {
+ TextView title = (TextView) titleView;
+ title.setGravity(Gravity.CENTER);
+ }
+ } else {
+ setLabel(json, "customName", R.id.origin_title);
+ setLabel(json, "origin", R.id.origin_subtitle);
+ setLabel(json, "customMessage", R.id.origin_message);
+ }
+
+ if (json.has("customIcon")) {
+ String iconUrl = json.getString("customIcon");
+ mChoosenIconSize = 32;
+ downloadIcon(iconUrl);
+ }
+
+ } catch (JSONException e) {
+ Log.e(TAG, "JSON exception ", e);
+ }
+ }
+
+ public void setupDialog(TextView message, ImageView icon) {
+ if (mDialogType.equalsIgnoreCase(LOCAL_DATA_STRING)) {
+ message.setText(R.string.query_data_message);
+ icon.setImageResource(R.drawable.gears_local_data);
+ } else if (mDialogType.equalsIgnoreCase(LOCATION_DATA_STRING)) {
+ message.setText(R.string.location_message);
+ icon.setImageResource(R.drawable.gears_location_data);
+ View privacyPolicyLabel = findViewById(R.id.privacy_policy_label);
+ if (privacyPolicyLabel != null) {
+ privacyPolicyLabel.setVisibility(View.VISIBLE);
+ }
+ }
+ }
+
+ public String closeDialog(int closingType) {
+ String ret = null;
+ switch (closingType) {
+ case ALWAYS_DENY:
+ ret = "{\"allow\": false, \"permanently\": true }";
+ break;
+ case ALLOW:
+ ret = "{\"allow\": true, \"permanently\": true }";
+ break;
+ case DENY:
+ ret = "{\"allow\": false, \"permanently\": false }";
+ break;
+ }
+ return ret;
+ }
+
+}
diff --git a/src/com/android/browser/GearsSettingsDialog.java b/src/com/android/browser/GearsSettingsDialog.java
new file mode 100644
index 0000000..dead4f2
--- /dev/null
+++ b/src/com/android/browser/GearsSettingsDialog.java
@@ -0,0 +1,500 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.browser;
+
+import android.app.Activity;
+import android.content.Context;
+import android.os.Handler;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.CompoundButton;
+import android.widget.ImageView;
+import android.widget.ListAdapter;
+import android.widget.ListView;
+import android.widget.RadioButton;
+import android.widget.TextView;
+
+import com.android.browser.GearsPermissions.OriginPermissions;
+import com.android.browser.GearsPermissions.PermissionsChangesListener;
+import com.android.browser.GearsPermissions.PermissionType;
+
+import java.util.Vector;
+import java.util.List;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * Gears Settings dialog
+ */
+class GearsSettingsDialog extends GearsBaseDialog
+ implements PermissionsChangesListener {
+
+ private static final String TAG = "GearsPermissionsDialog";
+ private Vector<OriginPermissions> mSitesPermissions = null;
+ private Vector<OriginPermissions> mOriginalPermissions = null;
+ private Vector<OriginPermissions> mCurrentPermissions = null;
+
+ private Vector<PermissionType> mPermissions;
+
+ // We declare the permissions globally to simplify the code
+ private final PermissionType LOCAL_STORAGE =
+ new PermissionType(LOCAL_STORAGE_STRING);
+ private final PermissionType LOCATION_DATA =
+ new PermissionType(LOCATION_DATA_STRING);
+
+ private boolean mChanges = false;
+
+
+ public GearsSettingsDialog(Activity activity,
+ Handler handler,
+ String arguments) {
+ super (activity, handler, arguments);
+ }
+
+ public void setup() {
+ // First let's add the permissions' resources
+ LOCAL_STORAGE.setResources(R.id.local_storage_choice,
+ R.id.local_storage_allowed,
+ R.id.local_storage_denied);
+
+ LOCATION_DATA.setResources(R.id.location_data_choice,
+ R.id.location_data_allowed,
+ R.id.location_data_denied);
+
+ // add the permissions to the list of permissions.
+ mPermissions = new Vector<PermissionType>();
+ mPermissions.add(LOCAL_STORAGE);
+ mPermissions.add(LOCATION_DATA);
+ OriginPermissions.setListener(this);
+
+
+ inflate(R.layout.gears_dialog_settings, R.id.panel_content);
+ setupDialog();
+ setupButtons(0,
+ R.string.settings_button_allow,
+ R.string.settings_button_deny);
+
+ // by default disable the allow button (it will get enabled if
+ // something is changed by the user)
+ View buttonView = findViewById(R.id.button_allow);
+ if (buttonView != null) {
+ Button button = (Button) buttonView;
+ button.setEnabled(false);
+ }
+
+ View gearsVersionView = findViewById(R.id.gears_version);
+ if (gearsVersionView != null) {
+ TextView gearsVersion = (TextView) gearsVersionView;
+ gearsVersion.setText(mGearsVersion);
+ }
+
+ // We manage the permissions using three vectors, mSitesPermissions,
+ // mOriginalPermissions and mCurrentPermissions.
+ // The dialog's arguments are parsed and a list of permissions is
+ // generated and stored in those three vectors.
+ // mOriginalPermissions is a separate copy and will not be modified;
+ // mSitesPermissions contains the current permissions _only_ --
+ // if an origin is removed, it is also removed from mSitesPermissions.
+ // Finally, mCurrentPermissions contains the current permissions and
+ // is a clone of mSitesPermissions, but removed sites aren't removed,
+ // their permissions are simply set to PERMISSION_NOT_SET. This
+ // allows us to easily generate the final difference between the
+ // original permissions and the final permissions, while directly
+ // using mSitesPermissions for the listView adapter (SettingsAdapter).
+
+ mSitesPermissions = new Vector<OriginPermissions>();
+ mOriginalPermissions = new Vector<OriginPermissions>();
+
+ try {
+ JSONObject json = new JSONObject(mDialogArguments);
+ if (json.has("permissions")) {
+ JSONArray jsonArray = json.getJSONArray("permissions");
+ for (int i = 0; i < jsonArray.length(); i++) {
+ JSONObject infos = jsonArray.getJSONObject(i);
+ String name = null;
+ int localStorage = PermissionType.PERMISSION_NOT_SET;
+ int locationData = PermissionType.PERMISSION_NOT_SET;
+ if (infos.has("name")) {
+ name = infos.getString("name");
+ }
+ if (infos.has(LOCAL_STORAGE_STRING)) {
+ JSONObject perm = infos.getJSONObject(LOCAL_STORAGE_STRING);
+ if (perm.has("permissionState")) {
+ localStorage = perm.getInt("permissionState");
+ }
+ }
+ if (infos.has(LOCATION_DATA_STRING)) {
+ JSONObject perm = infos.getJSONObject(LOCATION_DATA_STRING);
+ if (perm.has("permissionState")) {
+ locationData = perm.getInt("permissionState");
+ }
+ }
+ OriginPermissions perms = new OriginPermissions(name);
+ perms.setPermission(LOCAL_STORAGE, localStorage);
+ perms.setPermission(LOCATION_DATA, locationData);
+
+ mSitesPermissions.add(perms);
+ mOriginalPermissions.add(new OriginPermissions(perms));
+ }
+ }
+ } catch (JSONException e) {
+ Log.e(TAG, "JSON exception ", e);
+ }
+ mCurrentPermissions = (Vector<OriginPermissions>)mSitesPermissions.clone();
+
+ View listView = findViewById(R.id.sites_list);
+ if (listView != null) {
+ ListView list = (ListView) listView;
+ list.setAdapter(new SettingsAdapter(mActivity, mSitesPermissions));
+ }
+ if (mDebug) {
+ printPermissions();
+ }
+ }
+
+ public void setupDialog() {
+ View dialogTitleView = findViewById(R.id.dialog_title);
+ if (dialogTitleView != null) {
+ TextView dialogTitle = (TextView) dialogTitleView;
+ dialogTitle.setText(R.string.settings_title);
+ dialogTitle.setVisibility(View.VISIBLE);
+ }
+ View dialogSubtitleView = findViewById(R.id.dialog_subtitle);
+ if (dialogSubtitleView != null) {
+ TextView dialogSubtitle = (TextView) dialogSubtitleView;
+ dialogSubtitle.setText(R.string.settings_message);
+ dialogSubtitle.setVisibility(View.VISIBLE);
+ }
+ View iconView = findViewById(R.id.icon);
+ if (iconView != null) {
+ ImageView icon = (ImageView) iconView;
+ icon.setImageResource(R.drawable.gears_icon_32x32);
+ }
+ }
+
+ /**
+ * GearsPermissions.PermissionsChangesListener delegate
+ */
+ public boolean setPermission(PermissionType type, int perm) {
+ if (mChanges == false) {
+ signalChanges();
+ }
+ return mChanges;
+ }
+
+ /**
+ * Controller class for binding the model (OriginPermissions) with
+ * the UI.
+ */
+ class PermissionController {
+ final static int ALLOWED_BUTTON = 1;
+ final static int DENIED_BUTTON = 2;
+ private int mButtonType;
+ private PermissionType mPermissionType;
+ private OriginPermissions mPermissions;
+
+ PermissionController(PermissionType permissionType, int buttonType,
+ OriginPermissions permissions) {
+ mPermissionType = permissionType;
+ mButtonType = buttonType;
+ mPermissions = permissions;
+ }
+
+ public boolean isChecked() {
+ boolean checked = false;
+
+ switch (mButtonType) {
+ case ALLOWED_BUTTON:
+ if (mPermissions.getPermission(mPermissionType) ==
+ PermissionType.PERMISSION_ALLOWED) {
+ checked = true;
+ } break;
+ case DENIED_BUTTON:
+ if (mPermissions.getPermission(mPermissionType) ==
+ PermissionType.PERMISSION_DENIED) {
+ checked = true;
+ }
+ }
+ return checked;
+ }
+
+ public String print() {
+ return printType() + " for " + mPermissions.getOrigin();
+ }
+
+ private String printType() {
+ switch (mButtonType) {
+ case ALLOWED_BUTTON:
+ return "ALLOWED_BUTTON";
+ case DENIED_BUTTON:
+ return "DENIED_BUTTON";
+ }
+ return "UNKNOWN BUTTON";
+ }
+
+ public void changed(boolean isChecked) {
+ if (isChecked == isChecked()) {
+ return; // already set
+ }
+
+ switch (mButtonType) {
+ case ALLOWED_BUTTON:
+ mPermissions.setPermission(mPermissionType,
+ PermissionType.PERMISSION_ALLOWED);
+ break;
+ case DENIED_BUTTON:
+ mPermissions.setPermission(mPermissionType,
+ PermissionType.PERMISSION_DENIED);
+ break;
+ }
+ }
+ }
+
+
+
+ /**
+ * Adapter class for the list view in the settings dialog
+ *
+ * Every row in the settings dialog display the permissions
+ * for a given origin. For every type of permission
+ * (location, local data...) there is two radio buttons to
+ * authorize or deny the permission.
+ * A remove button is also present to let the user remove
+ * all the authorization of an origin in one step.
+ */
+ class SettingsAdapter extends ArrayAdapter {
+ private Activity mContext;
+ private List mItems;
+
+ SettingsAdapter(Activity context, List items) {
+ super(context, R.layout.gears_dialog_settings_row, items);
+ mContext = context;
+ mItems = items;
+ }
+
+ /*
+ * setup the necessary listeners for the radiobuttons
+ * When the buttons are clicked the permissions change.
+ */
+ private void createAndSetButtonListener(View buttonView,
+ OriginPermissions perms, PermissionType permissionType,
+ int buttonType) {
+ if (buttonView == null) {
+ return;
+ }
+ RadioButton button = (RadioButton) buttonView;
+
+ button.setOnCheckedChangeListener(null);
+ PermissionController p = new PermissionController(permissionType,
+ buttonType, perms);
+ button.setTag(p);
+
+ CompoundButton.OnCheckedChangeListener listener =
+ new CompoundButton.OnCheckedChangeListener() {
+ public void onCheckedChanged(CompoundButton buttonView,
+ boolean isChecked) {
+ PermissionController perm = (PermissionController)buttonView.getTag();
+ perm.changed(isChecked);
+ }
+ };
+
+ button.setOnCheckedChangeListener(listener);
+
+ if (p.isChecked() != button.isChecked()) {
+ button.setChecked(p.isChecked());
+ }
+ }
+
+ /*
+ * setup the remove button for an origin: each row has a global
+ * remove button in addition to the radio buttons controlling the
+ * permissions.
+ */
+ private void setRemoveButton(Button button, OriginPermissions perms) {
+ Button.OnClickListener listener = new Button.OnClickListener() {
+ public void onClick(View buttonView) {
+ if (mChanges == false) {
+ signalChanges();
+ }
+ OriginPermissions perm = (OriginPermissions) buttonView.getTag();
+ perm.setPermission(LOCAL_STORAGE, PermissionType.PERMISSION_NOT_SET);
+ perm.setPermission(LOCATION_DATA, PermissionType.PERMISSION_NOT_SET);
+ mSitesPermissions.remove(perm);
+
+ View view = findViewById(R.id.sites_list);
+ if (view != null) {
+ ListView listView = (ListView) view;
+ ListAdapter listAdapter = listView.getAdapter();
+ if (listAdapter != null) {
+ SettingsAdapter settingsAdapter = (SettingsAdapter) listAdapter;
+ settingsAdapter.notifyDataSetChanged();
+ }
+ }
+ }
+ };
+ button.setTag(perms);
+ button.setOnClickListener(listener);
+ }
+
+ public View getView(int position, View convertView, ViewGroup parent) {
+ View row = convertView;
+ if (row == null) { // no cached view, we create one
+ LayoutInflater inflater = (LayoutInflater) getSystemService(
+ Context.LAYOUT_INFLATER_SERVICE);
+ row = inflater.inflate(R.layout.gears_dialog_settings_row, null);
+ }
+
+ OriginPermissions perms = (OriginPermissions) mItems.get(position);
+
+ View nameView = row.findViewById(R.id.origin_name);
+ if (nameView != null) {
+ TextView originName = (TextView) nameView;
+ originName.setText(perms.getOrigin());
+ }
+
+ View removeButtonView = row.findViewById(R.id.origin_remove);
+ if (removeButtonView != null) {
+ Button removeButton = (Button) removeButtonView;
+ setRemoveButton(removeButton, perms);
+ }
+
+ for (int i = 0; i < mPermissions.size(); i++) {
+ PermissionType type = mPermissions.get(i);
+ int rowRsc = type.getRowRsc();
+ int allowedButtonRsc = type.getAllowedButtonRsc();
+ int deniedButtonRsc = type.getDeniedButtonRsc();
+
+ View rowView = row.findViewById(rowRsc);
+ if (rowView != null) {
+ int perm = perms.getPermission(type);
+ if (perm != PermissionType.PERMISSION_NOT_SET) {
+ createAndSetButtonListener(row.findViewById(allowedButtonRsc),
+ perms, type, PermissionController.ALLOWED_BUTTON);
+ createAndSetButtonListener(row.findViewById(deniedButtonRsc),
+ perms, type, PermissionController.DENIED_BUTTON);
+ rowView.setVisibility(View.VISIBLE);
+ } else {
+ rowView.setVisibility(View.GONE);
+ }
+ }
+ }
+
+ return row;
+ }
+ }
+
+ /**
+ * Utility method used in debug mode to print the list of
+ * permissions (original values and current values).
+ */
+ public void printPermissions() {
+ Log.v(TAG, "Original Permissions: ");
+ for (int i = 0; i < mOriginalPermissions.size(); i++) {
+ OriginPermissions p = mOriginalPermissions.get(i);
+ p.print();
+ }
+ Log.v(TAG, "Current Permissions: ");
+ for (int i = 0; i < mSitesPermissions.size(); i++) {
+ OriginPermissions p = mSitesPermissions.get(i);
+ p.print();
+ }
+ }
+
+ /**
+ * Utility method used by the settings dialog, signaling
+ * the user the settings have been modified.
+ * We reflect this by enabling the Allow button (disabled
+ * by default).
+ */
+ public void signalChanges() {
+ View view = findViewById(R.id.button_allow);
+ if (view != null) {
+ Button button = (Button) view;
+ button.setEnabled(true);
+ }
+ mChanges = true;
+ }
+
+ /**
+ * Computes the difference between the original permissions and the
+ * current ones. Returns a json-formatted string.
+ * It is used by the Settings dialog.
+ */
+ public String computeDiff(boolean modif) {
+ String ret = null;
+ try {
+ JSONObject results = new JSONObject();
+ JSONArray permissions = new JSONArray();
+
+ for (int i = 0; modif && i < mOriginalPermissions.size(); i++) {
+ OriginPermissions original = mOriginalPermissions.get(i);
+ OriginPermissions current = mCurrentPermissions.get(i);
+ JSONObject permission = new JSONObject();
+ boolean modifications = false;
+
+ for (int j = 0; j < mPermissions.size(); j++) {
+ PermissionType type = mPermissions.get(j);
+
+ if (current.getPermission(type) != original.getPermission(type)) {
+ JSONObject state = new JSONObject();
+ state.put("permissionState", current.getPermission(type));
+ permission.put(type.getName(), state);
+ modifications = true;
+ }
+ }
+
+ if (modifications) {
+ permission.put("name", current.getOrigin());
+ permissions.put(permission);
+ }
+ }
+ results.put("modifiedOrigins", permissions);
+ ret = results.toString();
+ } catch (JSONException e) {
+ Log.e(TAG, "JSON exception ", e);
+ }
+ return ret;
+ }
+
+ public String closeDialog(int closingType) {
+ String ret = null;
+ switch (closingType) {
+ case ALWAYS_DENY:
+ ret = "{\"allow\": false }";
+ break;
+ case ALLOW:
+ ret = computeDiff(true);
+ break;
+ case DENY:
+ ret = computeDiff(false);
+ break;
+ }
+
+ if (mDebug) {
+ printPermissions();
+ }
+
+ return ret;
+ }
+
+}
diff --git a/src/com/android/browser/GearsShortcutDialog.java b/src/com/android/browser/GearsShortcutDialog.java
new file mode 100644
index 0000000..deede12
--- /dev/null
+++ b/src/com/android/browser/GearsShortcutDialog.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.browser;
+
+import android.app.Activity;
+import android.os.Handler;
+import android.util.Log;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * Gears Shortcut dialog
+ */
+class GearsShortcutDialog extends GearsBaseDialog {
+
+ private static final String TAG = "GearsPermissionsDialog";
+
+ private final String ICON_16 = "icon16x16";
+ private final String ICON_32 = "icon32x32";
+ private final String ICON_48 = "icon48x48";
+ private final String ICON_128 = "icon128x128";
+
+ public GearsShortcutDialog(Activity activity,
+ Handler handler,
+ String arguments) {
+ super (activity, handler, arguments);
+ }
+
+ public void setup() {
+ inflate(R.layout.gears_dialog_permission, R.id.panel_content);
+ setupButtons(R.string.shortcut_button_alwaysdeny,
+ R.string.shortcut_button_allow,
+ R.string.shortcut_button_deny);
+
+ View contentBorder = findViewById(R.id.content_border);
+ if (contentBorder != null) {
+ contentBorder.setBackgroundResource(R.color.shortcut_border);
+ }
+ View contentBackground = findViewById(R.id.content_background);
+ if (contentBackground != null) {
+ contentBackground.setBackgroundResource(R.color.shortcut_background);
+ }
+
+ try {
+ JSONObject json = new JSONObject(mDialogArguments);
+
+ String iconUrl = pickIconToRender(json);
+ if (iconUrl != null) {
+ downloadIcon(iconUrl);
+ }
+
+ setupDialog();
+
+ setLabel(json, "name", R.id.origin_title);
+ setLabel(json, "link", R.id.origin_subtitle);
+ setLabel(json, "description", R.id.origin_message);
+ } catch (JSONException e) {
+ Log.e(TAG, "JSON exception", e);
+ }
+ }
+
+ public void setupDialog(TextView message, ImageView icon) {
+ message.setText(R.string.shortcut_message);
+ icon.setImageResource(R.drawable.gears_icon_48x48);
+ }
+
+ /**
+ * Utility method to validate an icon url. Used in the
+ * shortcut dialog.
+ */
+ boolean validIcon(JSONObject json, String name) {
+ try {
+ if (json.has(name)) {
+ String str = json.getString(name);
+ if (str.length() > 0) {
+ return true;
+ }
+ }
+ } catch (JSONException e) {
+ Log.e(TAG, "JSON exception", e);
+ }
+ return false;
+ }
+
+
+ /**
+ * Utility method to pick the best indicated icon
+ * from the dialogs' arguments. Used in the
+ * shortcut dialog.
+ */
+ String pickIconToRender(JSONObject json) {
+ try {
+ if (validIcon(json, ICON_48)) { // ideal size
+ mChoosenIconSize = 48;
+ return json.getString(ICON_48);
+ } else if (validIcon(json, ICON_32)) {
+ mChoosenIconSize = 32;
+ return json.getString(ICON_32);
+ } else if (validIcon(json, ICON_128)) {
+ mChoosenIconSize = 128;
+ return json.getString(ICON_128);
+ } else if (validIcon(json, ICON_16)) {
+ mChoosenIconSize = 16;
+ return json.getString(ICON_16);
+ }
+ } catch (JSONException e) {
+ Log.e(TAG, "JSON exception", e);
+ }
+ mChoosenIconSize = 0;
+ return null;
+ }
+
+ public String closeDialog(int closingType) {
+ String ret = null;
+ switch (closingType) {
+ case ALWAYS_DENY:
+ ret = "{\"allow\": false, \"permanently\": true }";
+ break;
+ case ALLOW:
+ ret = "{\"allow\": true, \"locations\": 0 }";
+ break;
+ case DENY:
+ ret = null;
+ break;
+ }
+ return ret;
+ }
+
+}
diff --git a/src/com/android/browser/ImageAdapter.java b/src/com/android/browser/ImageAdapter.java
index e6eaa75..b4c1209 100644
--- a/src/com/android/browser/ImageAdapter.java
+++ b/src/com/android/browser/ImageAdapter.java
@@ -17,18 +17,20 @@
package com.android.browser;
import android.app.AlertDialog;
+import android.content.Context;
import android.content.DialogInterface;
+import android.content.res.Configuration;
+import android.content.res.Resources;
import android.database.DataSetObserver;
import android.graphics.Color;
import android.view.KeyEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.LayoutInflater;
-import android.content.Context;
-import android.content.res.Configuration;
-import android.content.res.Resources;
import android.webkit.WebView;
-import android.widget.*;
+import android.widget.ImageView;
+import android.widget.ListAdapter;
+import android.widget.TextView;
import java.util.ArrayList;
@@ -38,31 +40,25 @@
public class ImageAdapter implements ListAdapter {
ArrayList<TabControl.Tab> mItems; // Items shown in the grid
- ArrayList<DataSetObserver> mDataObservers; // Data change listeners
- Context mContext; // Context to use to inflate views
- boolean mMaxedOut;
- boolean mLandScape;
- ImageGrid mImageGrid;
- boolean mIsLive;
+ private ArrayList<DataSetObserver> mDataObservers; // Data change listeners
+ private Context mContext; // Context to use to inflate views
+ private boolean mMaxedOut;
+ private ImageGrid mImageGrid;
+ private boolean mIsLive;
+ private int mTabHeight;
- ImageAdapter(Context context, ImageGrid grid,
- ArrayList<TabControl.Tab> items, boolean live) {
+ ImageAdapter(Context context, ImageGrid grid, boolean live) {
mContext = context;
mIsLive = live;
- if (items == null) {
- mItems = new ArrayList<TabControl.Tab>();
- } else {
- mItems = items;
- if (items.size() == TabControl.MAX_TABS) {
- mMaxedOut = true;
- }
- }
+ mItems = new ArrayList<TabControl.Tab>();
mImageGrid = grid;
mDataObservers = new ArrayList<DataSetObserver>();
- mLandScape = context.getResources().getConfiguration().orientation ==
- Configuration.ORIENTATION_LANDSCAPE;
}
-
+
+ void heightChanged(int newHeight) {
+ mTabHeight = newHeight;
+ }
+
/**
* Whether the adapter is at its limit, determined by TabControl.MAX_TABS
*
@@ -197,9 +193,9 @@
TabControl.Tab t = mItems.get(position);
img.setTab(t);
tv.setText(t.getTitle());
- // Do not put the 'X' for a single tab or if the tab picker isn't
- // "live" (meaning the user cannot click on a tab)
- if (mItems.size() == 1 || !mIsLive) {
+ // Do not put the 'X' if the tab picker isn't "live" (meaning the
+ // user cannot click on a tab)
+ if (!mIsLive) {
close.setVisibility(View.GONE);
} else {
close.setVisibility(View.VISIBLE);
@@ -218,10 +214,9 @@
tv.setText(R.string.new_window);
close.setVisibility(View.GONE);
}
- if (mLandScape) {
- ViewGroup.LayoutParams lp = img.getLayoutParams();
- lp.width = 225;
- lp.height = 120;
+ ViewGroup.LayoutParams lp = img.getLayoutParams();
+ if (lp.height != mTabHeight) {
+ lp.height = mTabHeight;
img.requestLayout();
}
return v;
@@ -245,7 +240,7 @@
};
new AlertDialog.Builder(mContext)
.setTitle(R.string.close)
- .setIcon(R.drawable.ssl_icon)
+ .setIcon(android.R.drawable.ic_dialog_alert)
.setMessage(R.string.close_window)
.setPositiveButton(R.string.ok, confirm)
.setNegativeButton(R.string.cancel, null)
@@ -257,7 +252,6 @@
*/
public void registerDataSetObserver(DataSetObserver observer) {
mDataObservers.add(observer);
-
}
/* (non-Javadoc)
@@ -272,9 +266,8 @@
*/
public void unregisterDataSetObserver(DataSetObserver observer) {
mDataObservers.remove(observer);
-
}
-
+
/**
* Notify all the observers that a change has happened.
*/
diff --git a/src/com/android/browser/ImageGrid.java b/src/com/android/browser/ImageGrid.java
index e0a5c89..9eccb16 100644
--- a/src/com/android/browser/ImageGrid.java
+++ b/src/com/android/browser/ImageGrid.java
@@ -39,6 +39,7 @@
private Listener mListener;
private ImageAdapter mAdapter;
private boolean mIsLive;
+ private static final int SPACING = 10;
public static final int CANCEL = -99;
public static final int NEW_TAB = -1;
@@ -58,23 +59,18 @@
setOnItemClickListener(this);
setOnCreateContextMenuListener(this);
}
- if (Config.DEBUG && l == null) {
- throw new AssertionError();
- }
mListener = l;
- mAdapter = new ImageAdapter(context, this, null, live);
+ mAdapter = new ImageAdapter(context, this, live);
setAdapter(mAdapter);
- // android.R.color.window_background seems to return transparent?
-// setBackgroundColor(android.R.color.window_background);
setBackgroundColor(0xFF000000);
- setPadding(0, 10, 0, 10);
- setVerticalSpacing(10);
- setHorizontalSpacing(10);
+ setVerticalSpacing(SPACING);
+ setHorizontalSpacing(SPACING);
setNumColumns(2);
setStretchMode(GridView.STRETCH_COLUMN_WIDTH);
+ setSelector(android.R.drawable.gallery_thumb);
}
@Override
@@ -189,11 +185,6 @@
position--;
}
menu.setHeaderTitle(mAdapter.mItems.get(position).getTitle());
-
- // If we only have one active tab left, don't add the remove option
- if (mAdapter.mItems.size() <= 1) {
- menu.findItem(R.id.remove_tab_menu_id).setVisible(false);
- }
}
}
@@ -212,10 +203,13 @@
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
- // Called when our orientation changes. Replace the adapter with one
- // that has the appropriate dimensions.
- mAdapter = new ImageAdapter(mContext, this, mAdapter.mItems, mIsLive);
- setAdapter(mAdapter);
+ // Called when our orientation changes. Tell the adapter about the new
+ // size. Compute the individual tab height by taking the grid height
+ // and subtracting the SPACING. Then subtract the list padding twice
+ // (once for each tab on screen) and divide the remaining height by 2.
+ int tabHeight = (h - SPACING
+ - 2 * (getListPaddingTop() + getListPaddingBottom())) / 2;
+ mAdapter.heightChanged(tabHeight);
super.onSizeChanged(w, h, oldw, oldh);
}
diff --git a/src/com/android/browser/TabControl.java b/src/com/android/browser/TabControl.java
index 66adf3c..01ef3a8 100644
--- a/src/com/android/browser/TabControl.java
+++ b/src/com/android/browser/TabControl.java
@@ -35,6 +35,7 @@
import android.widget.FrameLayout;
import android.widget.ImageButton;
+import java.io.File;
import java.util.ArrayList;
import java.util.Vector;
@@ -169,9 +170,12 @@
// children.
private Vector<Tab> mChildTabs;
+ private Boolean mCloseOnExit;
+
// Construct a new tab
- private Tab(WebView w) {
+ private Tab(WebView w, boolean closeOnExit) {
mMainView = w;
+ mCloseOnExit = closeOnExit;
}
/**
@@ -236,6 +240,17 @@
private void setParentTab(Tab parent) {
mParentTab = parent;
+ // This tab may have been freed due to low memory. If that is the
+ // case, the parent tab index is already saved. If we are changing
+ // that index (most likely due to removing the parent tab) we must
+ // update the parent tab index in the saved Bundle.
+ if (mSavedState != null) {
+ if (parent == null) {
+ mSavedState.remove(PARENTTAB);
+ } else {
+ mSavedState.putInt(PARENTTAB, getTabIndex(parent));
+ }
+ }
}
/**
@@ -273,8 +288,20 @@
public Tab getParentTab() {
return mParentTab;
}
+
+ /**
+ * Return whether this tab should be closed when it is backing out of
+ * the first page.
+ * @return TRUE if this tab should be closed when exit.
+ */
+ public boolean closeOnExit() {
+ return mCloseOnExit;
+ }
};
+ // Directory to store thumbnails for each WebView.
+ private final File mThumbnailDir;
+
/**
* Construct a new TabControl object that interfaces with the given
* BrowserActivity instance.
@@ -286,6 +313,15 @@
mInflateService =
((LayoutInflater) activity.getSystemService(
Context.LAYOUT_INFLATER_SERVICE));
+ mThumbnailDir = activity.getDir("thumbnails", 0);
+ }
+
+ File getThumbnailDir() {
+ return mThumbnailDir;
+ }
+
+ BrowserActivity getBrowserActivity() {
+ return mActivity;
}
/**
@@ -368,7 +404,7 @@
* @return The newly createTab or null if we have reached the maximum
* number of open tabs.
*/
- Tab createNewTab() {
+ Tab createNewTab(boolean closeOnExit) {
int size = mTabs.size();
// Return false if we have maxed out on tabs
if (MAX_TABS == size) {
@@ -382,7 +418,7 @@
final BrowserSettings s = BrowserSettings.getInstance();
s.addObserver(w.getSettings()).update(s, null);
// Create a new tab and add it to the tab list
- Tab t = new Tab(w);
+ Tab t = new Tab(w, closeOnExit);
mTabs.add(t);
return t;
}
@@ -418,6 +454,26 @@
// Remove it from our list of tabs.
mTabs.remove(t);
+
+ // The tab indices have shifted, update all the saved state so we point
+ // to the correct index.
+ for (Tab tab : mTabs) {
+ if (tab.mChildTabs != null) {
+ for (Tab child : tab.mChildTabs) {
+ child.setParentTab(tab);
+ }
+ }
+ }
+
+
+ // This tab may have been pushed in to the background and then closed.
+ // If the saved state contains a picture file, delete the file.
+ if (t.mSavedState != null) {
+ if (t.mSavedState.containsKey("picture")) {
+ new File(t.mSavedState.getString("picture")).delete();
+ }
+ }
+
// Remove it from the queue of viewed tabs.
mTabQueue.remove(t);
mCurrentTab = -1;
@@ -473,6 +529,8 @@
private static final String CURRTAB = "currentTab";
private static final String CURRURL = "currentUrl";
private static final String CURRTITLE = "currentTitle";
+ private static final String CLOSEONEXIT = "closeonexit";
+ private static final String PARENTTAB = "parentTab";
/**
* Save the state of all the Tabs.
@@ -506,7 +564,7 @@
final int currentTab = inState.getInt(CURRTAB, -1);
for (int i = 0; i < numTabs; i++) {
if (i == currentTab) {
- Tab t = createNewTab();
+ Tab t = createNewTab(false);
// Me must set the current tab before restoring the state
// so that all the client classes are set.
setCurrentTab(t);
@@ -518,7 +576,7 @@
} else {
// Create a new tab and don't restore the state yet, add it
// to the tab list
- Tab t = new Tab(null);
+ Tab t = new Tab(null, false);
t.mSavedState = inState.getBundle(WEBVIEW + i);
if (t.mSavedState != null) {
t.mUrl = t.mSavedState.getString(CURRURL);
@@ -528,6 +586,21 @@
mTabQueue.add(t);
}
}
+ // Rebuild the tree of tabs. Do this after all tabs have been
+ // created/restored so that the parent tab exists.
+ for (int i = 0; i < numTabs; i++) {
+ final Bundle b = inState.getBundle(WEBVIEW + i);
+ final Tab t = getTab(i);
+ if (b != null && t != null) {
+ final int parentIndex = b.getInt(PARENTTAB, -1);
+ if (parentIndex != -1) {
+ final Tab parent = getTab(parentIndex);
+ if (parent != null) {
+ parent.addChildTab(t);
+ }
+ }
+ }
+ }
}
return true;
}
@@ -801,9 +874,16 @@
}
final Bundle b = new Bundle();
final WebBackForwardList list = w.saveState(b);
+ if (list != null) {
+ final File f = new File(mThumbnailDir, w.hashCode()
+ + "_pic.save");
+ if (w.savePicture(b, f)) {
+ b.putString("picture", f.getPath());
+ }
+ }
// Store some extra info for displaying the tab in the picker.
- final WebHistoryItem item =
+ final WebHistoryItem item =
list != null ? list.getCurrentItem() : null;
populatePickerData(t, item);
if (t.mUrl != null) {
@@ -812,6 +892,12 @@
if (t.mTitle != null) {
b.putString(CURRTITLE, t.mTitle);
}
+ b.putBoolean(CLOSEONEXIT, t.mCloseOnExit);
+
+ // Remember the parent tab so the relationship can be restored.
+ if (t.mParentTab != null) {
+ b.putInt(PARENTTAB, getTabIndex(t.mParentTab));
+ }
// Remember the saved state.
t.mSavedState = b;
@@ -832,9 +918,15 @@
if (list == null) {
return false;
}
+ if (b.containsKey("picture")) {
+ final File f = new File(b.getString("picture"));
+ w.restorePicture(b, f);
+ f.delete();
+ }
t.mSavedState = null;
t.mUrl = null;
t.mTitle = null;
+ t.mCloseOnExit = b.getBoolean(CLOSEONEXIT);
return true;
}
}