diff options
| author | The Android Open Source Project <initial-contribution@android.com> | 2008-12-17 18:05:52 -0800 |
|---|---|---|
| committer | The Android Open Source Project <initial-contribution@android.com> | 2008-12-17 18:05:52 -0800 |
| commit | ed217d91fb3f1a8f4e75ab36ef81d72ef9f4e6d6 (patch) | |
| tree | f4f3c7225a6ed8d870f32cedbbc8f6e41b5633e7 /src/com/android | |
| parent | ba6d7b853c32ad6c3be26c443daa61f322bcfdc2 (diff) | |
| download | packages_apps_Browser-ed217d91fb3f1a8f4e75ab36ef81d72ef9f4e6d6.tar.gz packages_apps_Browser-ed217d91fb3f1a8f4e75ab36ef81d72ef9f4e6d6.tar.bz2 packages_apps_Browser-ed217d91fb3f1a8f4e75ab36ef81d72ef9f4e6d6.zip | |
Code drop from //branches/cupcake/...@124589
Diffstat (limited to 'src/com/android')
25 files changed, 3359 insertions, 571 deletions
diff --git a/src/com/android/browser/AddBookmarkPage.java b/src/com/android/browser/AddBookmarkPage.java index 3fe38e827..ea65a46ca 100644 --- a/src/com/android/browser/AddBookmarkPage.java +++ b/src/com/android/browser/AddBookmarkPage.java @@ -114,7 +114,8 @@ public class AddBookmarkPage extends Activity { */ 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 fc4acfc9c..fe27f8da5 100644 --- a/src/com/android/browser/BrowserActivity.java +++ b/src/com/android/browser/BrowserActivity.java @@ -62,6 +62,7 @@ import android.net.http.EventHandler; 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.RemoteException; 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.LayoutInflater; 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.Enumeration; 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 @@ public class BrowserActivity extends Activity 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 @@ public class BrowserActivity extends Activity 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 @@ public class BrowserActivity extends Activity * 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 @@ public class BrowserActivity extends Activity 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 @@ public class BrowserActivity extends Activity mResolver = getContentResolver(); + setBaseSearchUrl(PreferenceManager.getDefaultSharedPreferences(this) + .getString("search_url", "")); + // // start MASF proxy service // @@ -658,10 +684,19 @@ public class BrowserActivity extends Activity 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 @@ public class BrowserActivity extends Activity @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 @@ public class BrowserActivity extends Activity } 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 @@ public class BrowserActivity extends Activity 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 @@ public class BrowserActivity extends Activity } } } 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 @@ public class BrowserActivity extends Activity } mActivityInPause = true; - if (!pauseWebView()) { + if (mTabControl.getCurrentIndex() >= 0 && !pauseWebView()) { mWakeLock.acquire(); mHandler.sendMessageDelayed(mHandler .obtainMessage(RELEASE_WAKELOCK), WAKELOCK_TIMEOUT); @@ -1057,6 +1118,9 @@ public class BrowserActivity extends Activity showHttpAuthentication(mHttpAuthHandler, null, null, title, name, password, focusId); } + if (mFindDialog != null && mFindDialog.isShowing()) { + mFindDialog.onConfigurationChanged(newConfig); + } } @Override public void onLowMemory() { @@ -1192,6 +1256,11 @@ public class BrowserActivity extends Activity // 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 @@ public class BrowserActivity extends Activity 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 @@ public class BrowserActivity extends Activity 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 @@ public class BrowserActivity extends Activity // -- 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 @@ public class BrowserActivity extends Activity indexToShow--; } } - removeTabAndShow(currentIndex, indexToShow); + switchTabs(currentIndex, indexToShow, true); break; case R.id.homepage_menu_id: @@ -1313,39 +1390,21 @@ public class BrowserActivity extends Activity 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 @@ public class BrowserActivity extends Activity 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 class BrowserActivity extends Activity } 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 @@ public class BrowserActivity extends Activity 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 @@ public class BrowserActivity extends Activity 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 @@ public class BrowserActivity extends Activity 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 @@ public class BrowserActivity extends Activity 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 @@ public class BrowserActivity extends Activity 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 @@ public class BrowserActivity extends Activity // 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 @@ public class BrowserActivity extends Activity // 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 @@ public class BrowserActivity extends Activity } // 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 @@ public class BrowserActivity extends Activity // 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 @@ public class BrowserActivity extends Activity 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 class BrowserActivity extends Activity 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 @@ public class BrowserActivity extends Activity 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 @@ public class BrowserActivity extends Activity // 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 @@ public class BrowserActivity extends Activity // 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 @@ public class BrowserActivity extends Activity 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 @@ public class BrowserActivity extends Activity 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 @@ public class BrowserActivity extends Activity 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 @@ public class BrowserActivity extends Activity 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 @@ public class BrowserActivity extends Activity // 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 @@ public class BrowserActivity extends Activity 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 @@ public class BrowserActivity extends Activity 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 @@ public class BrowserActivity extends Activity 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 @@ public class BrowserActivity extends Activity 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 @@ public class BrowserActivity extends Activity } } + 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 @@ public class BrowserActivity extends Activity // 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 @@ public class BrowserActivity extends Activity } 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 @@ public class BrowserActivity extends Activity 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 @@ public class BrowserActivity extends Activity 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 @@ public class BrowserActivity extends Activity break; case LOAD_URL: - loadURL((String) msg.obj); + loadURL(getTopWindow(), (String) msg.obj); break; case STOP_LOAD: @@ -2574,9 +2617,6 @@ public class BrowserActivity extends Activity } }; - private static final int HEADER_FLAG = Integer.MIN_VALUE; - private TextView mTitleView = null; - // ------------------------------------------------------------------------- // WebViewClient implementation. //------------------------------------------------------------------------- @@ -2591,14 +2631,22 @@ public class BrowserActivity extends Activity 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 @@ public class BrowserActivity extends Activity // 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 @@ public class BrowserActivity extends Activity 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 @@ public class BrowserActivity extends Activity // 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 @@ public class BrowserActivity extends Activity 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 @@ public class BrowserActivity extends Activity @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 @@ public class BrowserActivity extends Activity 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 @@ public class BrowserActivity extends Activity @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 @@ public class BrowserActivity extends Activity * @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 @@ public class BrowserActivity extends Activity 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 @@ public class BrowserActivity extends Activity } 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 @@ public class BrowserActivity extends Activity ((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 @@ public class BrowserActivity extends Activity // 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 @@ public class BrowserActivity extends Activity // otherwise, display the top-most certificate from // the chain if (view.getCertificate() != null) { - showSSLCertificate(view); + showSSLCertificate(tab); } } } @@ -3684,9 +3759,9 @@ public class BrowserActivity extends Activity /** * 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 @@ public class BrowserActivity extends Activity ((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 @@ public class BrowserActivity extends Activity mSSLCertificateDialog = null; mSSLCertificateView = null; - showPageInfo(view, false); + showPageInfo(tab, false); } }) .setOnCancelListener( @@ -3725,7 +3800,7 @@ public class BrowserActivity extends Activity mSSLCertificateDialog = null; mSSLCertificateView = null; - showPageInfo(view, false); + showPageInfo(tab, false); } }) .show(); @@ -3811,7 +3886,8 @@ public class BrowserActivity extends Activity // 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 @@ public class BrowserActivity extends Activity .show(); } } + mTabControl.getCurrentWebView().setNetworkAvailable(up); } @Override @@ -4030,14 +4107,19 @@ public class BrowserActivity extends Activity 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 @@ public class BrowserActivity extends Activity * 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 @@ public class BrowserActivity extends Activity 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 @@ public class BrowserActivity extends Activity // 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 @@ public class BrowserActivity extends Activity 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 @@ public class BrowserActivity extends Activity // 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 @@ public class BrowserActivity extends Activity 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 @@ public class BrowserActivity extends Activity * @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 @@ public class BrowserActivity extends Activity 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 @@ public class BrowserActivity extends Activity // 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 @@ public class BrowserActivity extends Activity // 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 @@ public class BrowserActivity extends Activity 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 @@ public class BrowserActivity extends Activity // 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 @@ public class BrowserActivity extends Activity // 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 3b76e757c..479dc0efb 100644 --- a/src/com/android/browser/BrowserBookmarksAdapter.java +++ b/src/com/android/browser/BrowserBookmarksAdapter.java @@ -312,8 +312,21 @@ class BrowserBookmarksAdapter extends BaseAdapter { 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 f938ff9a2..5c509a8a1 100644 --- a/src/com/android/browser/BrowserBookmarksPage.java +++ b/src/com/android/browser/BrowserBookmarksPage.java @@ -20,6 +20,7 @@ import android.app.Activity; 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 @@ public class BrowserBookmarksPage extends Activity implements 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 @@ public class BrowserBookmarksPage extends Activity implements 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 @@ public class BrowserBookmarksPage extends Activity implements 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 @@ public class BrowserBookmarksPage extends Activity implements 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 b3e08f550..0b509efd5 100644 --- a/src/com/android/browser/BrowserDownloadAdapter.java +++ b/src/com/android/browser/BrowserDownloadAdapter.java @@ -20,7 +20,6 @@ package com.android.browser; 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.drm.mobile1.DrmRawContent; 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 class BrowserDownloadAdapter extends ResourceCursorAdapter { 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 @@ public class BrowserDownloadAdapter extends ResourceCursorAdapter { 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 e2b11a69b..4397337b0 100644 --- a/src/com/android/browser/BrowserDownloadPage.java +++ b/src/com/android/browser/BrowserDownloadPage.java @@ -75,7 +75,7 @@ public class BrowserDownloadPage extends Activity 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 @@ public class BrowserDownloadPage extends Activity (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 @@ public class BrowserDownloadPage extends Activity * @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 @@ public class BrowserDownloadPage extends Activity */ 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 @@ public class BrowserDownloadPage extends Activity } 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 @@ public class BrowserDownloadPage extends Activity */ 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 @@ public class BrowserDownloadPage extends Activity } 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 @@ public class BrowserDownloadPage extends Activity */ 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 000000000..bc2114334 --- /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 b8bc495c9..b23f750b0 100644 --- a/src/com/android/browser/BrowserPreferencesPage.java +++ b/src/com/android/browser/BrowserPreferencesPage.java @@ -51,6 +51,9 @@ public class BrowserPreferencesPage extends PreferenceActivity 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 @@ public class BrowserPreferencesPage extends PreferenceActivity } } 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 4f456e712..7aa5bb255 100644 --- a/src/com/android/browser/BrowserProvider.java +++ b/src/com/android/browser/BrowserProvider.java @@ -30,6 +30,7 @@ import android.database.DataSetObserver; 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 class BrowserProvider extends ContentProvider { 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 @@ public class BrowserProvider extends ContentProvider { 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 @@ public class BrowserProvider extends ContentProvider { 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 000000000..09e89933b --- /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 b19c02e5c..6164e388c 100644 --- a/src/com/android/browser/BrowserSettings.java +++ b/src/com/android/browser/BrowserSettings.java @@ -22,6 +22,7 @@ import android.content.Context; 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 @@ class BrowserSettings extends Observable { 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 @@ class BrowserSettings extends Observable { 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 @@ class BrowserSettings extends Observable { 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 @@ class BrowserSettings extends Observable { s.setNeedInitialFocus(false); // Browser supports multiple windows s.setSupportMultipleWindows(true); + // Turn off file access + s.setAllowFileAccess(false); } } @@ -218,6 +239,9 @@ class BrowserSettings extends Observable { } else { layoutAlgorithm = WebSettings.LayoutAlgorithm.NORMAL; } + defaultTextEncodingName = + p.getString(PREF_DEFAULT_TEXT_ENCODING, + defaultTextEncodingName); showDebugSettings = p.getBoolean(PREF_DEBUG_SETTINGS, showDebugSettings); @@ -244,6 +268,8 @@ class BrowserSettings extends Observable { 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 @@ class BrowserSettings extends Observable { 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 200f86a0d..79976720c 100644 --- a/src/com/android/browser/FakeWebView.java +++ b/src/com/android/browser/FakeWebView.java @@ -70,12 +70,14 @@ public class FakeWebView extends ImageView { 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 000000000..8578643bf --- /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 42447e39d..2b2678488 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 @@ import android.widget.TextView; 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 @@ import android.widget.TextView; 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 @@ import android.widget.TextView; 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 @@ import android.widget.TextView; 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 @@ import android.widget.TextView; } 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 @@ import android.widget.TextView; 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 000000000..c930dc862 --- /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 fd9e76246..62a8aaa99 100644 --- a/src/com/android/browser/GearsDialog.java +++ b/src/com/android/browser/GearsDialog.java @@ -48,6 +48,8 @@ public class GearsDialog extends Activity { private String htmlContent; private String dialogArguments; + private boolean dismissed = false; + @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); @@ -66,14 +68,24 @@ public class GearsDialog extends Activity { } @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 @@ public class GearsDialog extends Activity { 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 class GearsDialog extends Activity { */ 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 000000000..4a3c0edb6 --- /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 000000000..c8ae74181 --- /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 000000000..cd4632400 --- /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 000000000..b57ab0b19 --- /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 000000000..dead4f247 --- /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 000000000..deede1259 --- /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 e6eaa75ad..b4c1209db 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 @@ import java.util.ArrayList; 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 @@ public class ImageAdapter implements ListAdapter { 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 @@ public class ImageAdapter implements ListAdapter { 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 @@ public class ImageAdapter implements ListAdapter { }; 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 class ImageAdapter implements ListAdapter { */ public void registerDataSetObserver(DataSetObserver observer) { mDataObservers.add(observer); - } /* (non-Javadoc) @@ -272,9 +266,8 @@ public class ImageAdapter implements ListAdapter { */ 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 e0a5c8987..9eccb1620 100644 --- a/src/com/android/browser/ImageGrid.java +++ b/src/com/android/browser/ImageGrid.java @@ -39,6 +39,7 @@ class ImageGrid extends GridView implements OnItemClickListener, 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 @@ class ImageGrid extends GridView implements OnItemClickListener, 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 @@ class ImageGrid extends GridView implements OnItemClickListener, 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 @@ class ImageGrid extends GridView implements OnItemClickListener, @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 66adf3c01..01ef3a84e 100644 --- a/src/com/android/browser/TabControl.java +++ b/src/com/android/browser/TabControl.java @@ -35,6 +35,7 @@ import android.webkit.WebViewClient; import android.widget.FrameLayout; import android.widget.ImageButton; +import java.io.File; import java.util.ArrayList; import java.util.Vector; @@ -169,9 +170,12 @@ class TabControl { // 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 @@ class TabControl { 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 @@ class TabControl { 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 @@ class TabControl { 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 @@ class TabControl { * @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 @@ class TabControl { 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 @@ class TabControl { // 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 @@ class TabControl { 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 @@ class TabControl { 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 @@ class TabControl { } 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 @@ class TabControl { 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 @@ class TabControl { } 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 @@ class TabControl { 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 @@ class TabControl { 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; } } |
