| /* |
| * Copyright (C) 2006 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.browser; |
| |
| import com.google.android.googleapps.IGoogleLoginService; |
| import com.google.android.googlelogin.GoogleLoginServiceConstants; |
| |
| import android.app.Activity; |
| import android.app.AlertDialog; |
| import android.app.ProgressDialog; |
| import android.app.SearchManager; |
| import android.content.ActivityNotFoundException; |
| import android.content.BroadcastReceiver; |
| import android.content.ComponentName; |
| import android.content.ContentResolver; |
| import android.content.ContentUris; |
| import android.content.ContentValues; |
| import android.content.Context; |
| import android.content.DialogInterface; |
| import android.content.Intent; |
| import android.content.IntentFilter; |
| import android.content.ServiceConnection; |
| import android.content.DialogInterface.OnCancelListener; |
| import android.content.pm.PackageInfo; |
| import android.content.pm.PackageManager; |
| import android.content.pm.ResolveInfo; |
| import android.content.res.AssetManager; |
| import android.content.res.Configuration; |
| import android.content.res.Resources; |
| import android.database.Cursor; |
| import android.database.sqlite.SQLiteDatabase; |
| import android.database.sqlite.SQLiteException; |
| import android.graphics.Bitmap; |
| import android.graphics.BitmapFactory; |
| import android.graphics.Canvas; |
| import android.graphics.DrawFilter; |
| import android.graphics.Paint; |
| import android.graphics.PaintFlagsDrawFilter; |
| import android.graphics.Picture; |
| import android.graphics.PixelFormat; |
| import android.graphics.Rect; |
| import android.graphics.drawable.Drawable; |
| import android.hardware.SensorListener; |
| import android.hardware.SensorManager; |
| import android.net.ConnectivityManager; |
| import android.net.NetworkInfo; |
| import android.net.Uri; |
| import android.net.WebAddress; |
| import android.net.http.EventHandler; |
| import android.net.http.SslCertificate; |
| import android.net.http.SslError; |
| import android.os.AsyncTask; |
| import android.os.Bundle; |
| import android.os.Debug; |
| import android.os.Environment; |
| import android.os.Handler; |
| import android.os.IBinder; |
| import android.os.Message; |
| import android.os.PowerManager; |
| import android.os.Process; |
| import android.os.RemoteException; |
| import android.os.ServiceManager; |
| import android.os.SystemClock; |
| import android.provider.Browser; |
| import android.provider.ContactsContract; |
| import android.provider.ContactsContract.Intents.Insert; |
| 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.AttributeSet; |
| import android.util.Log; |
| import android.view.ContextMenu; |
| import android.view.Gravity; |
| import android.view.KeyEvent; |
| import android.view.LayoutInflater; |
| import android.view.Menu; |
| import android.view.MenuInflater; |
| import android.view.MenuItem; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.view.Window; |
| import android.view.WindowManager; |
| import android.view.ContextMenu.ContextMenuInfo; |
| import android.view.MenuItem.OnMenuItemClickListener; |
| import android.view.animation.AlphaAnimation; |
| import android.view.animation.Animation; |
| import android.view.animation.AnimationSet; |
| import android.view.animation.DecelerateInterpolator; |
| import android.view.animation.ScaleAnimation; |
| import android.view.animation.TranslateAnimation; |
| import android.webkit.CookieManager; |
| import android.webkit.CookieSyncManager; |
| import android.webkit.DownloadListener; |
| import android.webkit.GeolocationPermissions; |
| import android.webkit.HttpAuthHandler; |
| import android.webkit.PluginManager; |
| import android.webkit.SslErrorHandler; |
| import android.webkit.URLUtil; |
| import android.webkit.ValueCallback; |
| import android.webkit.WebChromeClient; |
| import android.webkit.WebChromeClient.CustomViewCallback; |
| import android.webkit.WebHistoryItem; |
| import android.webkit.WebIconDatabase; |
| import android.webkit.WebStorage; |
| import android.webkit.WebView; |
| import android.webkit.WebViewClient; |
| import android.widget.EditText; |
| import android.widget.FrameLayout; |
| import android.widget.LinearLayout; |
| import android.widget.TextView; |
| import android.widget.Toast; |
| |
| import java.io.BufferedOutputStream; |
| import java.io.ByteArrayOutputStream; |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.net.MalformedURLException; |
| import java.net.URI; |
| import java.net.URISyntaxException; |
| import java.net.URL; |
| import java.net.URLEncoder; |
| import java.text.ParseException; |
| import java.util.Date; |
| import java.util.Enumeration; |
| import java.util.HashMap; |
| import java.util.LinkedList; |
| import java.util.Vector; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| import java.util.zip.ZipEntry; |
| import java.util.zip.ZipFile; |
| |
| public class BrowserActivity extends Activity |
| implements View.OnCreateContextMenuListener, |
| DownloadListener { |
| |
| /* Define some aliases to make these debugging flags easier to refer to. |
| * This file imports android.provider.Browser, so we can't just refer to "Browser.DEBUG". |
| */ |
| private final static boolean DEBUG = com.android.browser.Browser.DEBUG; |
| private final static boolean LOGV_ENABLED = com.android.browser.Browser.LOGV_ENABLED; |
| private final static boolean LOGD_ENABLED = com.android.browser.Browser.LOGD_ENABLED; |
| |
| private IGoogleLoginService mGls = null; |
| private ServiceConnection mGlsConnection = null; |
| |
| private SensorManager mSensorManager = null; |
| |
| // These are single-character shortcuts for searching popular sources. |
| private static final int SHORTCUT_INVALID = 0; |
| private static final int SHORTCUT_GOOGLE_SEARCH = 1; |
| private static final int SHORTCUT_WIKIPEDIA_SEARCH = 2; |
| private static final int SHORTCUT_DICTIONARY_SEARCH = 3; |
| private static final int SHORTCUT_GOOGLE_MOBILE_LOCAL_SEARCH = 4; |
| |
| /* Whitelisted webpages |
| private static HashSet<String> sWhiteList; |
| |
| static { |
| sWhiteList = new HashSet<String>(); |
| sWhiteList.add("cnn.com/"); |
| sWhiteList.add("espn.go.com/"); |
| sWhiteList.add("nytimes.com/"); |
| sWhiteList.add("engadget.com/"); |
| sWhiteList.add("yahoo.com/"); |
| sWhiteList.add("msn.com/"); |
| sWhiteList.add("amazon.com/"); |
| sWhiteList.add("consumerist.com/"); |
| sWhiteList.add("google.com/m/news"); |
| } |
| */ |
| |
| 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(); |
| |
| try { |
| if (mGls == null) return; |
| |
| if (!homepage.startsWith("http://www.google.")) return; |
| if (homepage.indexOf('?') == -1) return; |
| |
| String hostedUser = mGls.getAccount(GoogleLoginServiceConstants.PREFER_HOSTED); |
| String googleUser = mGls.getAccount(GoogleLoginServiceConstants.REQUIRE_GOOGLE); |
| |
| // three cases: |
| // |
| // hostedUser == googleUser |
| // The device has only a google account |
| // |
| // hostedUser != googleUser |
| // The device has a hosted account and a google account |
| // |
| // hostedUser != null, googleUser == null |
| // The device has only a hosted account (so far) |
| |
| // developers might have no accounts at all |
| if (hostedUser == null) return; |
| |
| if (googleUser == null || !hostedUser.equals(googleUser)) { |
| String domain = hostedUser.substring(hostedUser.lastIndexOf('@')+1); |
| homepage = homepage.replace("?", "/a/" + domain + "?"); |
| } |
| } catch (RemoteException ignore) { |
| // Login service died; carry on |
| } catch (RuntimeException ignore) { |
| // Login service died; carry on |
| } finally { |
| finish(homepage); |
| } |
| } |
| |
| private void finish(final String homepage) { |
| mHandler.post(new Runnable() { |
| public void run() { |
| mSettings.setHomePage(BrowserActivity.this, homepage); |
| resumeAfterCredentials(); |
| |
| // as this is running in a separate thread, |
| // BrowserActivity's onDestroy() may have been called, |
| // which also calls unbindService(). |
| if (mGlsConnection != null) { |
| // we no longer need to keep GLS open |
| unbindService(mGlsConnection); |
| mGlsConnection = null; |
| } |
| } }); |
| } }; |
| |
| final boolean[] done = { false }; |
| |
| // Open a connection to the Google Login Service. The first |
| // time the connection is established, set up the homepage depending on |
| // the account in a background thread. |
| mGlsConnection = new ServiceConnection() { |
| public void onServiceConnected(ComponentName className, IBinder service) { |
| mGls = IGoogleLoginService.Stub.asInterface(service); |
| if (done[0] == false) { |
| done[0] = true; |
| Thread account = new Thread(getAccount); |
| account.setName("GLSAccount"); |
| account.start(); |
| } |
| } |
| public void onServiceDisconnected(ComponentName className) { |
| mGls = null; |
| } |
| }; |
| |
| bindService(GoogleLoginServiceConstants.SERVICE_INTENT, |
| mGlsConnection, Context.BIND_AUTO_CREATE); |
| } |
| |
| private static class ClearThumbnails extends AsyncTask<File, Void, Void> { |
| @Override |
| public Void doInBackground(File... files) { |
| if (files != null) { |
| for (File f : files) { |
| if (!f.delete()) { |
| Log.e(LOGTAG, f.getPath() + " was not deleted"); |
| } |
| } |
| } |
| return null; |
| } |
| } |
| |
| /** |
| * This layout holds everything you see below the status bar, including the |
| * error console, the custom view container, and the webviews. |
| */ |
| private FrameLayout mBrowserFrameLayout; |
| |
| @Override public void onCreate(Bundle icicle) { |
| if (LOGV_ENABLED) { |
| Log.v(LOGTAG, this + " onStart"); |
| } |
| super.onCreate(icicle); |
| // test the browser in OpenGL |
| // requestWindowFeature(Window.FEATURE_OPENGL); |
| |
| setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL); |
| |
| mResolver = getContentResolver(); |
| |
| // If this was a web search request, pass it on to the default web |
| // search provider and finish this activity. |
| if (handleWebSearchIntent(getIntent())) { |
| finish(); |
| return; |
| } |
| |
| // |
| // start MASF proxy service |
| // |
| //Intent proxyServiceIntent = new Intent(); |
| //proxyServiceIntent.setComponent |
| // (new ComponentName( |
| // "com.android.masfproxyservice", |
| // "com.android.masfproxyservice.MasfProxyService")); |
| //startService(proxyServiceIntent, null); |
| |
| mSecLockIcon = Resources.getSystem().getDrawable( |
| android.R.drawable.ic_secure); |
| mMixLockIcon = Resources.getSystem().getDrawable( |
| android.R.drawable.ic_partial_secure); |
| |
| FrameLayout frameLayout = (FrameLayout) getWindow().getDecorView() |
| .findViewById(com.android.internal.R.id.content); |
| mBrowserFrameLayout = (FrameLayout) LayoutInflater.from(this) |
| .inflate(R.layout.custom_screen, null); |
| mContentView = (FrameLayout) mBrowserFrameLayout.findViewById( |
| R.id.main_content); |
| mErrorConsoleContainer = (LinearLayout) mBrowserFrameLayout |
| .findViewById(R.id.error_console); |
| mCustomViewContainer = (FrameLayout) mBrowserFrameLayout |
| .findViewById(R.id.fullscreen_custom_content); |
| frameLayout.addView(mBrowserFrameLayout, COVER_SCREEN_PARAMS); |
| mTitleBar = new TitleBar(this); |
| |
| // Create the tab control and our initial tab |
| mTabControl = new TabControl(this); |
| |
| // Open the icon database and retain all the bookmark urls for favicons |
| retainIconsOnStartup(); |
| |
| // Keep a settings instance handy. |
| mSettings = BrowserSettings.getInstance(); |
| mSettings.setTabControl(mTabControl); |
| mSettings.loadFromDb(this); |
| |
| PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); |
| mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Browser"); |
| |
| /* enables registration for changes in network status from |
| http stack */ |
| mNetworkStateChangedFilter = new IntentFilter(); |
| mNetworkStateChangedFilter.addAction( |
| ConnectivityManager.CONNECTIVITY_ACTION); |
| mNetworkStateIntentReceiver = new BroadcastReceiver() { |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| if (intent.getAction().equals( |
| ConnectivityManager.CONNECTIVITY_ACTION)) { |
| boolean noConnectivity = intent.getBooleanExtra( |
| ConnectivityManager.EXTRA_NO_CONNECTIVITY, false); |
| onNetworkToggle(!noConnectivity); |
| } |
| } |
| }; |
| |
| IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED); |
| filter.addAction(Intent.ACTION_PACKAGE_REMOVED); |
| filter.addDataScheme("package"); |
| mPackageInstallationReceiver = new BroadcastReceiver() { |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| final String action = intent.getAction(); |
| final String packageName = intent.getData() |
| .getSchemeSpecificPart(); |
| final boolean replacing = intent.getBooleanExtra( |
| Intent.EXTRA_REPLACING, false); |
| if (Intent.ACTION_PACKAGE_REMOVED.equals(action) && replacing) { |
| // if it is replacing, refreshPlugins() when adding |
| return; |
| } |
| PackageManager pm = BrowserActivity.this.getPackageManager(); |
| PackageInfo pkgInfo = null; |
| try { |
| pkgInfo = pm.getPackageInfo(packageName, |
| PackageManager.GET_PERMISSIONS); |
| } catch (PackageManager.NameNotFoundException e) { |
| return; |
| } |
| if (pkgInfo != null) { |
| String permissions[] = pkgInfo.requestedPermissions; |
| if (permissions == null) { |
| return; |
| } |
| boolean permissionOk = false; |
| for (String permit : permissions) { |
| if (PluginManager.PLUGIN_PERMISSION.equals(permit)) { |
| permissionOk = true; |
| break; |
| } |
| } |
| if (permissionOk) { |
| PluginManager.getInstance(BrowserActivity.this) |
| .refreshPlugins( |
| Intent.ACTION_PACKAGE_ADDED |
| .equals(action)); |
| } |
| } |
| } |
| }; |
| registerReceiver(mPackageInstallationReceiver, filter); |
| |
| if (!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()); |
| // there is no quit on Android. But if we can't restore the state, |
| // we can treat it as a new Browser, remove the old session cookies. |
| CookieManager.getInstance().removeSessionCookie(); |
| final Intent intent = getIntent(); |
| final Bundle extra = intent.getExtras(); |
| // Create an initial tab. |
| // 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. |
| UrlData urlData = getUrlDataFromIntent(intent); |
| |
| final TabControl.Tab t = mTabControl.createNewTab( |
| Intent.ACTION_VIEW.equals(intent.getAction()) && |
| intent.getData() != null, |
| intent.getStringExtra(Browser.EXTRA_APPLICATION_ID), urlData.mUrl); |
| mTabControl.setCurrentTab(t); |
| attachTabToContentView(t); |
| WebView webView = t.getWebView(); |
| if (extra != null) { |
| int scale = extra.getInt(Browser.INITIAL_ZOOM_LEVEL, 0); |
| if (scale > 0 && scale <= 1000) { |
| webView.setInitialScale(scale); |
| } |
| } |
| // If we are not restoring from an icicle, then there is a high |
| // likely hood this is the first run. So, check to see if the |
| // homepage needs to be configured and copy any plugins from our |
| // asset directory to the data partition. |
| if ((extra == null || !extra.getBoolean("testing")) |
| && !mSettings.isLoginInitialized()) { |
| setupHomePage(); |
| } |
| |
| if (urlData.isEmpty()) { |
| if (mSettings.isLoginInitialized()) { |
| webView.loadUrl(mSettings.getHomePage()); |
| } else { |
| waitForCredentials(); |
| } |
| } else { |
| if (extra != null) { |
| urlData.setPostData(extra |
| .getByteArray(Browser.EXTRA_POST_DATA)); |
| } |
| urlData.loadIn(webView); |
| } |
| } else { |
| // TabControl.restoreState() will create a new tab even if |
| // restoring the state fails. |
| attachTabToContentView(mTabControl.getCurrentTab()); |
| } |
| |
| // Read JavaScript flags if it exists. |
| String jsFlags = mSettings.getJsFlags(); |
| if (jsFlags.trim().length() != 0) { |
| mTabControl.getCurrentWebView().setJsFlags(jsFlags); |
| } |
| } |
| |
| @Override |
| protected void onNewIntent(Intent intent) { |
| TabControl.Tab current = mTabControl.getCurrentTab(); |
| // 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 (current == null) { |
| // Try to reset the tab in case the index was incorrect. |
| current = mTabControl.getTab(0); |
| if (current == null) { |
| // No tabs at all so just ignore this intent. |
| return; |
| } |
| mTabControl.setCurrentTab(current); |
| attachTabToContentView(current); |
| resetTitleAndIcon(current.getWebView()); |
| } |
| final String action = intent.getAction(); |
| final int flags = intent.getFlags(); |
| if (Intent.ACTION_MAIN.equals(action) || |
| (flags & Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY) != 0) { |
| // just resume the browser |
| return; |
| } |
| if (Intent.ACTION_VIEW.equals(action) |
| || Intent.ACTION_SEARCH.equals(action) |
| || MediaStore.INTENT_ACTION_MEDIA_SEARCH.equals(action) |
| || Intent.ACTION_WEB_SEARCH.equals(action)) { |
| // If this was a search request (e.g. search query directly typed into the address bar), |
| // pass it on to the default web search provider. |
| if (handleWebSearchIntent(intent)) { |
| return; |
| } |
| |
| UrlData urlData = getUrlDataFromIntent(intent); |
| if (urlData.isEmpty()) { |
| urlData = new UrlData(mSettings.getHomePage()); |
| } |
| urlData.setPostData(intent |
| .getByteArrayExtra(Browser.EXTRA_POST_DATA)); |
| |
| final String appId = intent |
| .getStringExtra(Browser.EXTRA_APPLICATION_ID); |
| if (Intent.ACTION_VIEW.equals(action) |
| && !getPackageName().equals(appId) |
| && (flags & Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT) != 0) { |
| TabControl.Tab appTab = mTabControl.getTabFromId(appId); |
| if (appTab != null) { |
| Log.i(LOGTAG, "Reusing tab for " + appId); |
| // Dismiss the subwindow if applicable. |
| dismissSubWindow(appTab); |
| // Since we might kill the WebView, remove it from the |
| // content view first. |
| removeTabFromContentView(appTab); |
| // Recreate the main WebView after destroying the old one. |
| // If the WebView has the same original url and is on that |
| // page, it can be reused. |
| boolean needsLoad = |
| mTabControl.recreateWebView(appTab, urlData.mUrl); |
| |
| if (current != appTab) { |
| switchToTab(mTabControl.getTabIndex(appTab)); |
| if (needsLoad) { |
| urlData.loadIn(appTab.getWebView()); |
| } |
| } else { |
| // If the tab was the current tab, we have to attach |
| // it to the view system again. |
| attachTabToContentView(appTab); |
| if (needsLoad) { |
| urlData.loadIn(appTab.getWebView()); |
| } |
| } |
| return; |
| } else { |
| // No matching application tab, try to find a regular tab |
| // with a matching url. |
| appTab = mTabControl.findUnusedTabWithUrl(urlData.mUrl); |
| if (appTab != null) { |
| if (current != appTab) { |
| switchToTab(mTabControl.getTabIndex(appTab)); |
| } |
| // Otherwise, we are already viewing the correct tab. |
| } else { |
| // if FLAG_ACTIVITY_BROUGHT_TO_FRONT flag is on, the url |
| // will be opened in a new tab unless we have reached |
| // MAX_TABS. Then the url will be opened in the current |
| // tab. If a new tab is created, it will have "true" for |
| // exit on close. |
| openTabAndShow(urlData, true, appId); |
| } |
| } |
| } else { |
| if ("about:debug".equals(urlData.mUrl)) { |
| mSettings.toggleDebugSettings(); |
| return; |
| } |
| // Get rid of the subwindow if it exists |
| dismissSubWindow(current); |
| urlData.loadIn(current.getWebView()); |
| } |
| } |
| } |
| |
| private int parseUrlShortcut(String url) { |
| if (url == null) return SHORTCUT_INVALID; |
| |
| // FIXME: quick search, need to be customized by setting |
| if (url.length() > 2 && url.charAt(1) == ' ') { |
| switch (url.charAt(0)) { |
| case 'g': return SHORTCUT_GOOGLE_SEARCH; |
| case 'w': return SHORTCUT_WIKIPEDIA_SEARCH; |
| case 'd': return SHORTCUT_DICTIONARY_SEARCH; |
| case 'l': return SHORTCUT_GOOGLE_MOBILE_LOCAL_SEARCH; |
| } |
| } |
| return SHORTCUT_INVALID; |
| } |
| |
| /** |
| * Launches the default web search activity with the query parameters if the given intent's data |
| * are identified as plain search terms and not URLs/shortcuts. |
| * @return true if the intent was handled and web search activity was launched, false if not. |
| */ |
| private boolean handleWebSearchIntent(Intent intent) { |
| if (intent == null) return false; |
| |
| String url = null; |
| final String action = intent.getAction(); |
| if (Intent.ACTION_VIEW.equals(action)) { |
| Uri data = intent.getData(); |
| if (data != null) url = data.toString(); |
| } 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); |
| } |
| return handleWebSearchRequest(url, intent.getBundleExtra(SearchManager.APP_DATA), |
| intent.getStringExtra(SearchManager.EXTRA_DATA_KEY)); |
| } |
| |
| /** |
| * Launches the default web search activity with the query parameters if the given url string |
| * was identified as plain search terms and not URL/shortcut. |
| * @return true if the request was handled and web search activity was launched, false if not. |
| */ |
| private boolean handleWebSearchRequest(String inUrl, Bundle appData, String extraData) { |
| if (inUrl == null) return false; |
| |
| // In general, we shouldn't modify URL from Intent. |
| // But currently, we get the user-typed URL from search box as well. |
| String url = fixUrl(inUrl).trim(); |
| |
| // URLs and site specific search shortcuts are handled by the regular flow of control, so |
| // return early. |
| if (Regex.WEB_URL_PATTERN.matcher(url).matches() |
| || ACCEPTED_URI_SCHEMA.matcher(url).matches() |
| || parseUrlShortcut(url) != SHORTCUT_INVALID) { |
| return false; |
| } |
| |
| Browser.updateVisitedHistory(mResolver, url, false); |
| Browser.addSearchUrl(mResolver, url); |
| |
| Intent intent = new Intent(Intent.ACTION_WEB_SEARCH); |
| intent.addCategory(Intent.CATEGORY_DEFAULT); |
| intent.putExtra(SearchManager.QUERY, url); |
| if (appData != null) { |
| intent.putExtra(SearchManager.APP_DATA, appData); |
| } |
| if (extraData != null) { |
| intent.putExtra(SearchManager.EXTRA_DATA_KEY, extraData); |
| } |
| intent.putExtra(Browser.EXTRA_APPLICATION_ID, getPackageName()); |
| startActivity(intent); |
| |
| return true; |
| } |
| |
| private UrlData getUrlDataFromIntent(Intent intent) { |
| String url = null; |
| if (intent != null) { |
| final String action = intent.getAction(); |
| if (Intent.ACTION_VIEW.equals(action)) { |
| url = smartUrlFilter(intent.getData()); |
| if (url != null && url.startsWith("content:")) { |
| /* Append mimetype so webview knows how to display */ |
| String mimeType = intent.resolveType(getContentResolver()); |
| if (mimeType != null) { |
| url += "?" + mimeType; |
| } |
| } |
| if ("inline:".equals(url)) { |
| return new InlinedUrlData( |
| intent.getStringExtra(Browser.EXTRA_INLINE_CONTENT), |
| intent.getType(), |
| intent.getStringExtra(Browser.EXTRA_INLINE_ENCODING), |
| intent.getStringExtra(Browser.EXTRA_INLINE_FAILURL)); |
| } |
| } 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); |
| 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+"&"); |
| } |
| } |
| } |
| } |
| return new UrlData(url); |
| } |
| |
| /* package */ static String fixUrl(String inUrl) { |
| // FIXME: Converting the url to lower case |
| // duplicates functionality in smartUrlFilter(). |
| // However, changing all current callers of fixUrl to |
| // call smartUrlFilter in addition may have unwanted |
| // consequences, and is deferred for now. |
| int colon = inUrl.indexOf(':'); |
| boolean allLower = true; |
| for (int index = 0; index < colon; index++) { |
| char ch = inUrl.charAt(index); |
| if (!Character.isLetter(ch)) { |
| break; |
| } |
| allLower &= Character.isLowerCase(ch); |
| if (index == colon - 1 && !allLower) { |
| inUrl = inUrl.substring(0, colon).toLowerCase() |
| + inUrl.substring(colon); |
| } |
| } |
| if (inUrl.startsWith("http://") || inUrl.startsWith("https://")) |
| return inUrl; |
| if (inUrl.startsWith("http:") || |
| inUrl.startsWith("https:")) { |
| if (inUrl.startsWith("http:/") || inUrl.startsWith("https:/")) { |
| inUrl = inUrl.replaceFirst("/", "//"); |
| } else inUrl = inUrl.replaceFirst(":", "://"); |
| } |
| return inUrl; |
| } |
| |
| /** |
| * Looking for the pattern like this |
| * |
| * * |
| * * * |
| * *** * ******* |
| * * * |
| * * * |
| * * |
| */ |
| private final SensorListener mSensorListener = new SensorListener() { |
| private long mLastGestureTime; |
| private float[] mPrev = new float[3]; |
| private float[] mPrevDiff = new float[3]; |
| private float[] mDiff = new float[3]; |
| private float[] mRevertDiff = new float[3]; |
| |
| public void onSensorChanged(int sensor, float[] values) { |
| boolean show = false; |
| float[] diff = new float[3]; |
| |
| for (int i = 0; i < 3; i++) { |
| diff[i] = values[i] - mPrev[i]; |
| if (Math.abs(diff[i]) > 1) { |
| show = true; |
| } |
| if ((diff[i] > 1.0 && mDiff[i] < 0.2) |
| || (diff[i] < -1.0 && mDiff[i] > -0.2)) { |
| // start track when there is a big move, or revert |
| mRevertDiff[i] = mDiff[i]; |
| mDiff[i] = 0; |
| } else if (diff[i] > -0.2 && diff[i] < 0.2) { |
| // reset when it is flat |
| mDiff[i] = mRevertDiff[i] = 0; |
| } |
| mDiff[i] += diff[i]; |
| mPrevDiff[i] = diff[i]; |
| mPrev[i] = values[i]; |
| } |
| |
| if (false) { |
| // only shows if we think the delta is big enough, in an attempt |
| // to detect "serious" moves left/right or up/down |
| Log.d("BrowserSensorHack", "sensorChanged " + sensor + " (" |
| + values[0] + ", " + values[1] + ", " + values[2] + ")" |
| + " diff(" + diff[0] + " " + diff[1] + " " + diff[2] |
| + ")"); |
| Log.d("BrowserSensorHack", " mDiff(" + mDiff[0] + " " |
| + mDiff[1] + " " + mDiff[2] + ")" + " mRevertDiff(" |
| + mRevertDiff[0] + " " + mRevertDiff[1] + " " |
| + mRevertDiff[2] + ")"); |
| } |
| |
| long now = android.os.SystemClock.uptimeMillis(); |
| if (now - mLastGestureTime > 1000) { |
| mLastGestureTime = 0; |
| |
| float y = mDiff[1]; |
| float z = mDiff[2]; |
| float ay = Math.abs(y); |
| float az = Math.abs(z); |
| float ry = mRevertDiff[1]; |
| float rz = mRevertDiff[2]; |
| float ary = Math.abs(ry); |
| float arz = Math.abs(rz); |
| boolean gestY = ay > 2.5f && ary > 1.0f && ay > ary; |
| boolean gestZ = az > 3.5f && arz > 1.0f && az > arz; |
| |
| if ((gestY || gestZ) && !(gestY && gestZ)) { |
| WebView view = mTabControl.getCurrentWebView(); |
| |
| if (view != null) { |
| if (gestZ) { |
| if (z < 0) { |
| view.zoomOut(); |
| } else { |
| view.zoomIn(); |
| } |
| } else { |
| view.flingScroll(0, Math.round(y * 100)); |
| } |
| } |
| mLastGestureTime = now; |
| } |
| } |
| } |
| |
| public void onAccuracyChanged(int sensor, int accuracy) { |
| // TODO Auto-generated method stub |
| |
| } |
| }; |
| |
| @Override protected void onResume() { |
| super.onResume(); |
| if (LOGV_ENABLED) { |
| Log.v(LOGTAG, "BrowserActivity.onResume: this=" + this); |
| } |
| |
| if (!mActivityInPause) { |
| Log.e(LOGTAG, "BrowserActivity is already resumed."); |
| return; |
| } |
| |
| mTabControl.resumeCurrentTab(); |
| mActivityInPause = false; |
| resumeWebViewTimers(); |
| |
| if (mWakeLock.isHeld()) { |
| mHandler.removeMessages(RELEASE_WAKELOCK); |
| mWakeLock.release(); |
| } |
| |
| if (mCredsDlg != null) { |
| if (!mHandler.hasMessages(CANCEL_CREDS_REQUEST)) { |
| // In case credential request never comes back |
| mHandler.sendEmptyMessageDelayed(CANCEL_CREDS_REQUEST, 6000); |
| } |
| } |
| |
| registerReceiver(mNetworkStateIntentReceiver, |
| mNetworkStateChangedFilter); |
| WebView.enablePlatformNotifications(); |
| |
| if (mSettings.doFlick()) { |
| if (mSensorManager == null) { |
| mSensorManager = (SensorManager) getSystemService( |
| Context.SENSOR_SERVICE); |
| } |
| mSensorManager.registerListener(mSensorListener, |
| SensorManager.SENSOR_ACCELEROMETER, |
| SensorManager.SENSOR_DELAY_FASTEST); |
| } else { |
| mSensorManager = null; |
| } |
| } |
| |
| /** |
| * Since the actual title bar is embedded in the WebView, and removing it |
| * would change its appearance, create a temporary title bar to go at |
| * the top of the screen while the menu is open. |
| */ |
| private TitleBar mFakeTitleBar; |
| |
| /** |
| * Holder for the fake title bar. It will have a foreground shadow, as well |
| * as a white background, so the fake title bar looks like the real one. |
| */ |
| private ViewGroup mFakeTitleBarHolder; |
| |
| /** |
| * Layout parameters for the fake title bar within mFakeTitleBarHolder |
| */ |
| private FrameLayout.LayoutParams mFakeTitleBarParams |
| = new FrameLayout.LayoutParams( |
| ViewGroup.LayoutParams.FILL_PARENT, |
| ViewGroup.LayoutParams.WRAP_CONTENT); |
| /** |
| * Keeps track of whether the options menu is open. This is important in |
| * determining whether to show or hide the title bar overlay. |
| */ |
| private boolean mOptionsMenuOpen; |
| |
| /** |
| * Only meaningful when mOptionsMenuOpen is true. This variable keeps track |
| * of whether the configuration has changed. The first onMenuOpened call |
| * after a configuration change is simply a reopening of the same menu |
| * (i.e. mIconView did not change). |
| */ |
| private boolean mConfigChanged; |
| |
| /** |
| * Whether or not the options menu is in its smaller, icon menu form. When |
| * true, we want the title bar overlay to be up. When false, we do not. |
| * Only meaningful if mOptionsMenuOpen is true. |
| */ |
| private boolean mIconView; |
| |
| @Override |
| public boolean onMenuOpened(int featureId, Menu menu) { |
| if (Window.FEATURE_OPTIONS_PANEL == featureId) { |
| if (mOptionsMenuOpen) { |
| if (mConfigChanged) { |
| // We do not need to make any changes to the state of the |
| // title bar, since the only thing that happened was a |
| // change in orientation |
| mConfigChanged = false; |
| } else { |
| if (mIconView) { |
| // Switching the menu to expanded view, so hide the |
| // title bar. |
| hideFakeTitleBar(); |
| mIconView = false; |
| } else { |
| // Switching the menu back to icon view, so show the |
| // title bar once again. |
| showFakeTitleBar(); |
| mIconView = true; |
| } |
| } |
| } else { |
| // The options menu is closed, so open it, and show the title |
| showFakeTitleBar(); |
| mOptionsMenuOpen = true; |
| mConfigChanged = false; |
| mIconView = true; |
| } |
| } |
| return true; |
| } |
| |
| /** |
| * Special class used exclusively for the shadow drawn underneath the fake |
| * title bar. The shadow does not need to be drawn if the WebView |
| * underneath is scrolled to the top, because it will draw directly on top |
| * of the embedded shadow. |
| */ |
| private static class Shadow extends View { |
| private WebView mWebView; |
| |
| public Shadow(Context context, AttributeSet attrs) { |
| super(context, attrs); |
| } |
| |
| public void setWebView(WebView view) { |
| mWebView = view; |
| } |
| |
| @Override |
| public void draw(Canvas canvas) { |
| // In general onDraw is the method to override, but we care about |
| // whether or not the background gets drawn, which happens in draw() |
| if (mWebView == null || mWebView.getScrollY() > getHeight()) { |
| super.draw(canvas); |
| } |
| // Need to invalidate so that if the scroll position changes, we |
| // still draw as appropriate. |
| invalidate(); |
| } |
| } |
| |
| private void showFakeTitleBar() { |
| final View decor = getWindow().peekDecorView(); |
| if (mFakeTitleBar == null && mActiveTabsPage == null |
| && !mActivityInPause && decor != null |
| && decor.getWindowToken() != null) { |
| Rect visRect = new Rect(); |
| if (!mBrowserFrameLayout.getGlobalVisibleRect(visRect)) { |
| if (LOGD_ENABLED) { |
| Log.d(LOGTAG, "showFakeTitleBar visRect failed"); |
| } |
| return; |
| } |
| final WebView webView = getTopWindow(); |
| mFakeTitleBar = new TitleBar(this); |
| mFakeTitleBar.setTitleAndUrl(null, webView.getUrl()); |
| mFakeTitleBar.setProgress(webView.getProgress()); |
| mFakeTitleBar.setFavicon(webView.getFavicon()); |
| updateLockIconToLatest(); |
| |
| WindowManager manager |
| = (WindowManager) getSystemService(Context.WINDOW_SERVICE); |
| |
| // Add the title bar to the window manager so it can receive touches |
| // while the menu is up |
| WindowManager.LayoutParams params |
| = new WindowManager.LayoutParams( |
| ViewGroup.LayoutParams.FILL_PARENT, |
| ViewGroup.LayoutParams.WRAP_CONTENT, |
| WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL, |
| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, |
| PixelFormat.TRANSLUCENT); |
| params.gravity = Gravity.TOP; |
| WebView mainView = mTabControl.getCurrentWebView(); |
| boolean atTop = mainView != null && mainView.getScrollY() == 0; |
| params.windowAnimations = atTop ? 0 : R.style.TitleBar; |
| // XXX : Without providing an offset, the fake title bar will be |
| // placed underneath the status bar. Use the global visible rect |
| // of mBrowserFrameLayout to determine the bottom of the status bar |
| params.y = visRect.top; |
| // Add a holder for the title bar. It also holds a shadow to show |
| // below the title bar. |
| if (mFakeTitleBarHolder == null) { |
| mFakeTitleBarHolder = (ViewGroup) LayoutInflater.from(this) |
| .inflate(R.layout.title_bar_bg, null); |
| } |
| Shadow shadow = (Shadow) mFakeTitleBarHolder.findViewById( |
| R.id.shadow); |
| shadow.setWebView(mainView); |
| mFakeTitleBarHolder.addView(mFakeTitleBar, 0, mFakeTitleBarParams); |
| manager.addView(mFakeTitleBarHolder, params); |
| } |
| } |
| |
| @Override |
| public void onOptionsMenuClosed(Menu menu) { |
| mOptionsMenuOpen = false; |
| if (!mInLoad) { |
| hideFakeTitleBar(); |
| } else if (!mIconView) { |
| // The page is currently loading, and we are in expanded mode, so |
| // we were not showing the menu. Show it once again. It will be |
| // removed when the page finishes. |
| showFakeTitleBar(); |
| } |
| } |
| private void hideFakeTitleBar() { |
| if (mFakeTitleBar == null) return; |
| WindowManager.LayoutParams params = (WindowManager.LayoutParams) |
| mFakeTitleBarHolder.getLayoutParams(); |
| WebView mainView = mTabControl.getCurrentWebView(); |
| // Although we decided whether or not to animate based on the current |
| // scroll position, the scroll position may have changed since the |
| // fake title bar was displayed. Make sure it has the appropriate |
| // animation/lack thereof before removing. |
| params.windowAnimations = mainView != null && mainView.getScrollY() == 0 |
| ? 0 : R.style.TitleBar; |
| WindowManager manager |
| = (WindowManager) getSystemService(Context.WINDOW_SERVICE); |
| manager.updateViewLayout(mFakeTitleBarHolder, params); |
| mFakeTitleBarHolder.removeView(mFakeTitleBar); |
| manager.removeView(mFakeTitleBarHolder); |
| mFakeTitleBar = null; |
| } |
| |
| /** |
| * Special method for the fake title bar to call when displaying its context |
| * menu, since it is in its own Window, and its parent does not show a |
| * context menu. |
| */ |
| /* package */ void showTitleBarContextMenu() { |
| if (null == mTitleBar.getParent()) { |
| return; |
| } |
| openContextMenu(mTitleBar); |
| } |
| |
| /** |
| * onSaveInstanceState(Bundle map) |
| * onSaveInstanceState is called right before onStop(). The map contains |
| * the saved state. |
| */ |
| @Override protected void onSaveInstanceState(Bundle outState) { |
| if (LOGV_ENABLED) { |
| Log.v(LOGTAG, "BrowserActivity.onSaveInstanceState: this=" + this); |
| } |
| // the default implementation requires each view to have an id. As the |
| // browser handles the state itself and it doesn't use id for the views, |
| // don't call the default implementation. Otherwise it will trigger the |
| // warning like this, "couldn't save which view has focus because the |
| // focused view XXX has no id". |
| |
| // Save all the tabs |
| mTabControl.saveState(outState); |
| } |
| |
| @Override protected void onPause() { |
| super.onPause(); |
| |
| if (mActivityInPause) { |
| Log.e(LOGTAG, "BrowserActivity is already paused."); |
| return; |
| } |
| |
| mTabControl.pauseCurrentTab(); |
| mActivityInPause = true; |
| if (mTabControl.getCurrentIndex() >= 0 && !pauseWebViewTimers()) { |
| mWakeLock.acquire(); |
| mHandler.sendMessageDelayed(mHandler |
| .obtainMessage(RELEASE_WAKELOCK), WAKELOCK_TIMEOUT); |
| } |
| |
| // Clear the credentials toast if it is up |
| if (mCredsDlg != null && mCredsDlg.isShowing()) { |
| mCredsDlg.dismiss(); |
| } |
| mCredsDlg = null; |
| |
| // FIXME: This removes the active tabs page and resets the menu to |
| // MAIN_MENU. A better solution might be to do this work in onNewIntent |
| // but then we would need to save it in onSaveInstanceState and restore |
| // it in onCreate/onRestoreInstanceState |
| if (mActiveTabsPage != null) { |
| removeActiveTabPage(true); |
| } |
| |
| cancelStopToast(); |
| |
| // unregister network state listener |
| unregisterReceiver(mNetworkStateIntentReceiver); |
| WebView.disablePlatformNotifications(); |
| |
| if (mSensorManager != null) { |
| mSensorManager.unregisterListener(mSensorListener); |
| } |
| } |
| |
| @Override protected void onDestroy() { |
| if (LOGV_ENABLED) { |
| Log.v(LOGTAG, "BrowserActivity.onDestroy: this=" + this); |
| } |
| super.onDestroy(); |
| |
| if (mTabControl == null) return; |
| |
| // Remove the current tab and sub window |
| TabControl.Tab t = mTabControl.getCurrentTab(); |
| if (t != null) { |
| dismissSubWindow(t); |
| removeTabFromContentView(t); |
| } |
| // Destroy all the tabs |
| mTabControl.destroy(); |
| WebIconDatabase.getInstance().close(); |
| if (mGlsConnection != null) { |
| unbindService(mGlsConnection); |
| mGlsConnection = null; |
| } |
| |
| // |
| // stop MASF proxy service |
| // |
| //Intent proxyServiceIntent = new Intent(); |
| //proxyServiceIntent.setComponent |
| // (new ComponentName( |
| // "com.android.masfproxyservice", |
| // "com.android.masfproxyservice.MasfProxyService")); |
| //stopService(proxyServiceIntent); |
| |
| unregisterReceiver(mPackageInstallationReceiver); |
| } |
| |
| @Override |
| public void onConfigurationChanged(Configuration newConfig) { |
| mConfigChanged = true; |
| super.onConfigurationChanged(newConfig); |
| |
| if (mPageInfoDialog != null) { |
| mPageInfoDialog.dismiss(); |
| showPageInfo( |
| mPageInfoView, |
| mPageInfoFromShowSSLCertificateOnError.booleanValue()); |
| } |
| if (mSSLCertificateDialog != null) { |
| mSSLCertificateDialog.dismiss(); |
| showSSLCertificate( |
| mSSLCertificateView); |
| } |
| if (mSSLCertificateOnErrorDialog != null) { |
| mSSLCertificateOnErrorDialog.dismiss(); |
| showSSLCertificateOnError( |
| mSSLCertificateOnErrorView, |
| mSSLCertificateOnErrorHandler, |
| mSSLCertificateOnErrorError); |
| } |
| if (mHttpAuthenticationDialog != null) { |
| String title = ((TextView) mHttpAuthenticationDialog |
| .findViewById(com.android.internal.R.id.alertTitle)).getText() |
| .toString(); |
| String name = ((TextView) mHttpAuthenticationDialog |
| .findViewById(R.id.username_edit)).getText().toString(); |
| String password = ((TextView) mHttpAuthenticationDialog |
| .findViewById(R.id.password_edit)).getText().toString(); |
| int focusId = mHttpAuthenticationDialog.getCurrentFocus() |
| .getId(); |
| mHttpAuthenticationDialog.dismiss(); |
| showHttpAuthentication(mHttpAuthHandler, null, null, title, |
| name, password, focusId); |
| } |
| if (mFindDialog != null && mFindDialog.isShowing()) { |
| mFindDialog.onConfigurationChanged(newConfig); |
| } |
| } |
| |
| @Override public void onLowMemory() { |
| super.onLowMemory(); |
| mTabControl.freeMemory(); |
| } |
| |
| private boolean resumeWebViewTimers() { |
| if ((!mActivityInPause && !mPageStarted) || |
| (mActivityInPause && mPageStarted)) { |
| CookieSyncManager.getInstance().startSync(); |
| WebView w = mTabControl.getCurrentWebView(); |
| if (w != null) { |
| w.resumeTimers(); |
| } |
| return true; |
| } else { |
| return false; |
| } |
| } |
| |
| private boolean pauseWebViewTimers() { |
| if (mActivityInPause && !mPageStarted) { |
| CookieSyncManager.getInstance().stopSync(); |
| WebView w = mTabControl.getCurrentWebView(); |
| if (w != null) { |
| w.pauseTimers(); |
| } |
| return true; |
| } else { |
| return false; |
| } |
| } |
| |
| // FIXME: Do we want to call this when loading google for the first time? |
| /* |
| * This function is called when we are launching for the first time. We |
| * are waiting for the login credentials before loading Google home |
| * pages. This way the user will be logged in straight away. |
| */ |
| private void waitForCredentials() { |
| // Show a toast |
| mCredsDlg = new ProgressDialog(this); |
| mCredsDlg.setIndeterminate(true); |
| mCredsDlg.setMessage(getText(R.string.retrieving_creds_dlg_msg)); |
| // If the user cancels the operation, then cancel the Google |
| // Credentials request. |
| mCredsDlg.setCancelMessage(mHandler.obtainMessage(CANCEL_CREDS_REQUEST)); |
| mCredsDlg.show(); |
| |
| // We set a timeout for the retrieval of credentials in onResume() |
| // as that is when we have freed up some CPU time to get |
| // the login credentials. |
| } |
| |
| /* |
| * If we have received the credentials or we have timed out and we are |
| * showing the credentials dialog, then it is time to move on. |
| */ |
| private void resumeAfterCredentials() { |
| if (mCredsDlg == null) { |
| return; |
| } |
| |
| // Clear the toast |
| if (mCredsDlg.isShowing()) { |
| mCredsDlg.dismiss(); |
| } |
| mCredsDlg = null; |
| |
| // Clear any pending timeout |
| mHandler.removeMessages(CANCEL_CREDS_REQUEST); |
| |
| // Load the page |
| WebView w = mTabControl.getCurrentWebView(); |
| if (w != null) { |
| w.loadUrl(mSettings.getHomePage()); |
| } |
| |
| // Update the settings, need to do this last as it can take a moment |
| // to persist the settings. In the mean time we could be loading |
| // content. |
| mSettings.setLoginInitialized(this); |
| } |
| |
| // Open the icon database and retain all the icons for visited sites. |
| private void retainIconsOnStartup() { |
| final WebIconDatabase db = WebIconDatabase.getInstance(); |
| db.open(getDir("icons", 0).getPath()); |
| try { |
| Cursor c = Browser.getAllBookmarks(mResolver); |
| if (!c.moveToFirst()) { |
| c.deactivate(); |
| return; |
| } |
| int urlIndex = c.getColumnIndex(Browser.BookmarkColumns.URL); |
| do { |
| String url = c.getString(urlIndex); |
| db.retainIconForPageUrl(url); |
| } while (c.moveToNext()); |
| c.deactivate(); |
| } catch (IllegalStateException e) { |
| Log.e(LOGTAG, "retainIconsOnStartup", e); |
| } |
| } |
| |
| // Helper method for getting the top window. |
| WebView getTopWindow() { |
| return mTabControl.getCurrentTopWebView(); |
| } |
| |
| @Override |
| public boolean onCreateOptionsMenu(Menu menu) { |
| super.onCreateOptionsMenu(menu); |
| |
| MenuInflater inflater = getMenuInflater(); |
| inflater.inflate(R.menu.browser, menu); |
| mMenu = menu; |
| updateInLoadMenuItems(); |
| return true; |
| } |
| |
| /** |
| * As the menu can be open when loading state changes |
| * we must manually update the state of the stop/reload menu |
| * item |
| */ |
| private void updateInLoadMenuItems() { |
| if (mMenu == null) { |
| return; |
| } |
| MenuItem src = mInLoad ? |
| mMenu.findItem(R.id.stop_menu_id): |
| mMenu.findItem(R.id.reload_menu_id); |
| MenuItem dest = mMenu.findItem(R.id.stop_reload_menu_id); |
| dest.setIcon(src.getIcon()); |
| dest.setTitle(src.getTitle()); |
| } |
| |
| @Override |
| public boolean onContextItemSelected(MenuItem item) { |
| // chording is not an issue with context menus, but we use the same |
| // options selector, so set mCanChord to true so we can access them. |
| mCanChord = true; |
| int id = item.getItemId(); |
| switch (id) { |
| // For the context menu from the title bar |
| case R.id.title_bar_share_page_url: |
| case R.id.title_bar_copy_page_url: |
| WebView mainView = mTabControl.getCurrentWebView(); |
| if (null == mainView) { |
| return false; |
| } |
| if (id == R.id.title_bar_share_page_url) { |
| Browser.sendString(this, mainView.getUrl()); |
| } else { |
| copy(mainView.getUrl()); |
| } |
| break; |
| // -- Browser context menu |
| case R.id.open_context_menu_id: |
| case R.id.open_newtab_context_menu_id: |
| case R.id.bookmark_context_menu_id: |
| case R.id.save_link_context_menu_id: |
| case R.id.share_link_context_menu_id: |
| case R.id.copy_link_context_menu_id: |
| final WebView webView = getTopWindow(); |
| if (null == webView) { |
| return false; |
| } |
| final HashMap hrefMap = new HashMap(); |
| hrefMap.put("webview", webView); |
| final Message msg = mHandler.obtainMessage( |
| FOCUS_NODE_HREF, id, 0, hrefMap); |
| webView.requestFocusNodeHref(msg); |
| break; |
| |
| default: |
| // For other context menus |
| return onOptionsItemSelected(item); |
| } |
| mCanChord = false; |
| return true; |
| } |
| |
| private Bundle createGoogleSearchSourceBundle(String source) { |
| Bundle bundle = new Bundle(); |
| bundle.putString(SearchManager.SOURCE, source); |
| return bundle; |
| } |
| |
| /** |
| * Overriding this to insert a local information bundle |
| */ |
| @Override |
| public boolean onSearchRequested() { |
| if (mOptionsMenuOpen) closeOptionsMenu(); |
| String url = (getTopWindow() == null) ? null : getTopWindow().getUrl(); |
| startSearch(mSettings.getHomePage().equals(url) ? null : url, true, |
| createGoogleSearchSourceBundle(GOOGLE_SEARCH_SOURCE_SEARCHKEY), false); |
| 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); |
| } |
| |
| /** |
| * Switch tabs. Called by the TitleBarSet when sliding the title bar |
| * results in changing tabs. |
| * @param index Index of the tab to change to, as defined by |
| * mTabControl.getTabIndex(Tab t). |
| * @return boolean True if we successfully switched to a different tab. If |
| * the indexth tab is null, or if that tab is the same as |
| * the current one, return false. |
| */ |
| /* package */ boolean switchToTab(int index) { |
| TabControl.Tab tab = mTabControl.getTab(index); |
| TabControl.Tab currentTab = mTabControl.getCurrentTab(); |
| if (tab == null || tab == currentTab) { |
| return false; |
| } |
| if (currentTab != null) { |
| // currentTab may be null if it was just removed. In that case, |
| // we do not need to remove it |
| removeTabFromContentView(currentTab); |
| } |
| mTabControl.setCurrentTab(tab); |
| attachTabToContentView(tab); |
| resetTitleIconAndProgress(); |
| updateLockIconToLatest(); |
| return true; |
| } |
| |
| /* package */ TabControl.Tab openTabToHomePage() { |
| return openTabAndShow(mSettings.getHomePage(), false, null); |
| } |
| |
| /* package */ void closeCurrentWindow() { |
| final TabControl.Tab current = mTabControl.getCurrentTab(); |
| if (mTabControl.getTabCount() == 1) { |
| // This is the last tab. Open a new one, with the home |
| // page and close the current one. |
| TabControl.Tab newTab = openTabToHomePage(); |
| closeTab(current); |
| return; |
| } |
| final TabControl.Tab parent = current.getParentTab(); |
| int indexToShow = -1; |
| if (parent != null) { |
| indexToShow = mTabControl.getTabIndex(parent); |
| } else { |
| final int currentIndex = mTabControl.getCurrentIndex(); |
| // Try to move to the tab to the right |
| indexToShow = currentIndex + 1; |
| if (indexToShow > mTabControl.getTabCount() - 1) { |
| // Try to move to the tab to the left |
| indexToShow = currentIndex - 1; |
| } |
| } |
| if (switchToTab(indexToShow)) { |
| // Close window |
| closeTab(current); |
| } |
| } |
| |
| private ActiveTabsPage mActiveTabsPage; |
| |
| /** |
| * Remove the active tabs page. |
| * @param needToAttach If true, the active tabs page did not attach a tab |
| * to the content view, so we need to do that here. |
| */ |
| /* package */ void removeActiveTabPage(boolean needToAttach) { |
| mContentView.removeView(mActiveTabsPage); |
| mActiveTabsPage = null; |
| mMenuState = R.id.MAIN_MENU; |
| if (needToAttach) { |
| attachTabToContentView(mTabControl.getCurrentTab()); |
| } |
| getTopWindow().requestFocus(); |
| } |
| |
| @Override |
| public boolean onOptionsItemSelected(MenuItem item) { |
| if (!mCanChord) { |
| // The user has already fired a shortcut with this hold down of the |
| // menu key. |
| return false; |
| } |
| if (null == getTopWindow()) { |
| return false; |
| } |
| if (mMenuIsDown) { |
| // The shortcut action consumes the MENU. Even if it is still down, |
| // it won't trigger the next shortcut action. In the case of the |
| // shortcut action triggering a new activity, like Bookmarks, we |
| // won't get onKeyUp for MENU. So it is important to reset it here. |
| mMenuIsDown = false; |
| } |
| switch (item.getItemId()) { |
| // -- Main menu |
| case R.id.new_tab_menu_id: |
| openTabToHomePage(); |
| break; |
| |
| case R.id.goto_menu_id: |
| onSearchRequested(); |
| break; |
| |
| case R.id.bookmarks_menu_id: |
| bookmarksOrHistoryPicker(false); |
| break; |
| |
| case R.id.active_tabs_menu_id: |
| mActiveTabsPage = new ActiveTabsPage(this, mTabControl); |
| removeTabFromContentView(mTabControl.getCurrentTab()); |
| hideFakeTitleBar(); |
| mContentView.addView(mActiveTabsPage, COVER_SCREEN_PARAMS); |
| mActiveTabsPage.requestFocus(); |
| mMenuState = EMPTY_MENU; |
| break; |
| |
| case R.id.add_bookmark_menu_id: |
| Intent i = new Intent(BrowserActivity.this, |
| AddBookmarkPage.class); |
| WebView w = getTopWindow(); |
| i.putExtra("url", w.getUrl()); |
| i.putExtra("title", w.getTitle()); |
| i.putExtra("touch_icon_url", w.getTouchIconUrl()); |
| i.putExtra("thumbnail", createScreenshot(w)); |
| startActivity(i); |
| break; |
| |
| case R.id.stop_reload_menu_id: |
| if (mInLoad) { |
| stopLoading(); |
| } else { |
| getTopWindow().reload(); |
| } |
| break; |
| |
| case R.id.back_menu_id: |
| getTopWindow().goBack(); |
| break; |
| |
| case R.id.forward_menu_id: |
| getTopWindow().goForward(); |
| break; |
| |
| case R.id.close_menu_id: |
| // Close the subwindow if it exists. |
| if (mTabControl.getCurrentSubWindow() != null) { |
| dismissSubWindow(mTabControl.getCurrentTab()); |
| break; |
| } |
| closeCurrentWindow(); |
| break; |
| |
| case R.id.homepage_menu_id: |
| TabControl.Tab current = mTabControl.getCurrentTab(); |
| if (current != null) { |
| dismissSubWindow(current); |
| current.getWebView().loadUrl(mSettings.getHomePage()); |
| } |
| break; |
| |
| case R.id.preferences_menu_id: |
| Intent intent = new Intent(this, |
| BrowserPreferencesPage.class); |
| startActivityForResult(intent, PREFERENCES_PAGE); |
| break; |
| |
| case R.id.find_menu_id: |
| if (null == mFindDialog) { |
| mFindDialog = new FindDialog(this); |
| } |
| mFindDialog.setWebView(getTopWindow()); |
| mFindDialog.show(); |
| mMenuState = EMPTY_MENU; |
| break; |
| |
| case R.id.select_text_id: |
| getTopWindow().emulateShiftHeld(); |
| break; |
| case R.id.page_info_menu_id: |
| showPageInfo(mTabControl.getCurrentTab(), false); |
| break; |
| |
| case R.id.classic_history_menu_id: |
| bookmarksOrHistoryPicker(true); |
| break; |
| |
| case R.id.share_page_menu_id: |
| Browser.sendString(this, getTopWindow().getUrl(), |
| getText(R.string.choosertitle_sharevia).toString()); |
| break; |
| |
| case R.id.dump_nav_menu_id: |
| getTopWindow().debugDump(); |
| break; |
| |
| case R.id.zoom_in_menu_id: |
| getTopWindow().zoomIn(); |
| break; |
| |
| case R.id.zoom_out_menu_id: |
| getTopWindow().zoomOut(); |
| break; |
| |
| case R.id.view_downloads_menu_id: |
| viewDownloads(null); |
| 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()) { |
| switchToTab(id); |
| } |
| break; |
| } |
| } |
| } |
| break; |
| |
| default: |
| if (!super.onOptionsItemSelected(item)) { |
| return false; |
| } |
| // Otherwise fall through. |
| } |
| mCanChord = false; |
| return true; |
| } |
| |
| public void closeFind() { |
| mMenuState = R.id.MAIN_MENU; |
| } |
| |
| @Override public boolean onPrepareOptionsMenu(Menu menu) |
| { |
| // This happens when the user begins to hold down the menu key, so |
| // allow them to chord to get a shortcut. |
| mCanChord = true; |
| // Note: setVisible will decide whether an item is visible; while |
| // setEnabled() will decide whether an item is enabled, which also means |
| // whether the matching shortcut key will function. |
| super.onPrepareOptionsMenu(menu); |
| switch (mMenuState) { |
| case EMPTY_MENU: |
| if (mCurrentMenuState != mMenuState) { |
| menu.setGroupVisible(R.id.MAIN_MENU, false); |
| menu.setGroupEnabled(R.id.MAIN_MENU, false); |
| menu.setGroupEnabled(R.id.MAIN_SHORTCUT_MENU, false); |
| } |
| break; |
| default: |
| if (mCurrentMenuState != mMenuState) { |
| menu.setGroupVisible(R.id.MAIN_MENU, true); |
| menu.setGroupEnabled(R.id.MAIN_MENU, true); |
| menu.setGroupEnabled(R.id.MAIN_SHORTCUT_MENU, true); |
| } |
| final WebView w = getTopWindow(); |
| boolean canGoBack = false; |
| boolean canGoForward = false; |
| boolean isHome = false; |
| if (w != null) { |
| canGoBack = w.canGoBack(); |
| canGoForward = w.canGoForward(); |
| isHome = mSettings.getHomePage().equals(w.getUrl()); |
| } |
| final MenuItem back = menu.findItem(R.id.back_menu_id); |
| back.setEnabled(canGoBack); |
| |
| final MenuItem home = menu.findItem(R.id.homepage_menu_id); |
| home.setEnabled(!isHome); |
| |
| menu.findItem(R.id.forward_menu_id) |
| .setEnabled(canGoForward); |
| |
| menu.findItem(R.id.new_tab_menu_id).setEnabled( |
| mTabControl.getTabCount() < TabControl.MAX_TABS); |
| |
| // decide whether to show the share link option |
| PackageManager pm = getPackageManager(); |
| Intent send = new Intent(Intent.ACTION_SEND); |
| send.setType("text/plain"); |
| ResolveInfo ri = pm.resolveActivity(send, PackageManager.MATCH_DEFAULT_ONLY); |
| menu.findItem(R.id.share_page_menu_id).setVisible(ri != null); |
| |
| boolean isNavDump = mSettings.isNavDump(); |
| final MenuItem nav = menu.findItem(R.id.dump_nav_menu_id); |
| nav.setVisible(isNavDump); |
| nav.setEnabled(isNavDump); |
| break; |
| } |
| mCurrentMenuState = mMenuState; |
| return true; |
| } |
| |
| @Override |
| public void onCreateContextMenu(ContextMenu menu, View v, |
| ContextMenuInfo menuInfo) { |
| WebView webview = (WebView) v; |
| WebView.HitTestResult result = webview.getHitTestResult(); |
| if (result == null) { |
| return; |
| } |
| |
| int type = result.getType(); |
| if (type == WebView.HitTestResult.UNKNOWN_TYPE) { |
| Log.w(LOGTAG, |
| "We should not show context menu when nothing is touched"); |
| return; |
| } |
| if (type == WebView.HitTestResult.EDIT_TEXT_TYPE) { |
| // let TextView handles context menu |
| return; |
| } |
| |
| // Note, http://b/issue?id=1106666 is requesting that |
| // an inflated menu can be used again. This is not available |
| // yet, so inflate each time (yuk!) |
| MenuInflater inflater = getMenuInflater(); |
| inflater.inflate(R.menu.browsercontext, menu); |
| |
| // Show the correct menu group |
| String extra = result.getExtra(); |
| menu.setGroupVisible(R.id.PHONE_MENU, |
| type == WebView.HitTestResult.PHONE_TYPE); |
| menu.setGroupVisible(R.id.EMAIL_MENU, |
| type == WebView.HitTestResult.EMAIL_TYPE); |
| 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.SRC_IMAGE_ANCHOR_TYPE); |
| menu.setGroupVisible(R.id.ANCHOR_MENU, |
| 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(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_OR_EDIT); |
| addIntent.putExtra(Insert.PHONE, Uri.decode(extra)); |
| addIntent.setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE); |
| menu.findItem(R.id.add_contact_context_menu_id).setIntent( |
| addIntent); |
| menu.findItem(R.id.copy_phone_context_menu_id).setOnMenuItemClickListener( |
| new Copy(extra)); |
| break; |
| |
| case WebView.HitTestResult.EMAIL_TYPE: |
| menu.setHeaderTitle(extra); |
| menu.findItem(R.id.email_context_menu_id).setIntent( |
| new Intent(Intent.ACTION_VIEW, Uri |
| .parse(WebView.SCHEME_MAILTO + extra))); |
| menu.findItem(R.id.copy_mail_context_menu_id).setOnMenuItemClickListener( |
| new Copy(extra)); |
| break; |
| |
| case WebView.HitTestResult.GEO_TYPE: |
| menu.setHeaderTitle(extra); |
| menu.findItem(R.id.map_context_menu_id).setIntent( |
| new Intent(Intent.ACTION_VIEW, Uri |
| .parse(WebView.SCHEME_GEO |
| + URLEncoder.encode(extra)))); |
| menu.findItem(R.id.copy_geo_context_menu_id).setOnMenuItemClickListener( |
| new Copy(extra)); |
| break; |
| |
| case WebView.HitTestResult.SRC_ANCHOR_TYPE: |
| case WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE: |
| TextView titleView = (TextView) LayoutInflater.from(this) |
| .inflate(android.R.layout.browser_link_context_header, |
| null); |
| 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); |
| PackageManager pm = getPackageManager(); |
| Intent send = new Intent(Intent.ACTION_SEND); |
| send.setType("text/plain"); |
| ResolveInfo ri = pm.resolveActivity(send, PackageManager.MATCH_DEFAULT_ONLY); |
| menu.findItem(R.id.share_link_context_menu_id).setVisible(ri != null); |
| if (type == WebView.HitTestResult.SRC_ANCHOR_TYPE) { |
| break; |
| } |
| // otherwise fall through to handle image part |
| case WebView.HitTestResult.IMAGE_TYPE: |
| if (type == 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: |
| Log.w(LOGTAG, "We should not get here."); |
| break; |
| } |
| } |
| |
| // Attach the given tab to the content view. |
| // this should only be called for the current tab. |
| private void attachTabToContentView(TabControl.Tab t) { |
| // Attach the container that contains the main WebView and any other UI |
| // associated with the tab. |
| t.attachTabToContentView(mContentView); |
| |
| if (mShouldShowErrorConsole) { |
| ErrorConsoleView errorConsole = mTabControl.getCurrentErrorConsole(true); |
| if (errorConsole.numberOfErrors() == 0) { |
| errorConsole.showConsole(ErrorConsoleView.SHOW_NONE); |
| } else { |
| errorConsole.showConsole(ErrorConsoleView.SHOW_MINIMIZED); |
| } |
| |
| mErrorConsoleContainer.addView(errorConsole, |
| new LinearLayout.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT, |
| ViewGroup.LayoutParams.WRAP_CONTENT)); |
| } |
| |
| setLockIconType(t.getLockIconType()); |
| setPrevLockType(t.getPrevLockIconType()); |
| |
| // this is to match the code in removeTabFromContentView() |
| if (!mPageStarted && t.getTopWindow().getProgress() < 100) { |
| mPageStarted = true; |
| } |
| |
| WebView view = t.getWebView(); |
| view.setEmbeddedTitleBar(mTitleBar); |
| // Request focus on the top window. |
| t.getTopWindow().requestFocus(); |
| } |
| |
| // Attach a sub window to the main WebView of the given tab. |
| private void attachSubWindow(TabControl.Tab t) { |
| t.attachSubWindow(mContentView); |
| getTopWindow().requestFocus(); |
| } |
| |
| // Remove the given tab from the content view. |
| private void removeTabFromContentView(TabControl.Tab t) { |
| // Remove the container that contains the main WebView. |
| t.removeTabFromContentView(mContentView); |
| |
| if (mTabControl.getCurrentErrorConsole(false) != null) { |
| mErrorConsoleContainer.removeView(mTabControl.getCurrentErrorConsole(false)); |
| } |
| |
| WebView view = t.getWebView(); |
| if (view != null) { |
| view.setEmbeddedTitleBar(null); |
| } |
| |
| // unlike attachTabToContentView(), removeTabFromContentView() can be |
| // called for the non-current tab. Need to add the check. |
| if (t == mTabControl.getCurrentTab()) { |
| t.setLockIconType(getLockIconType()); |
| t.setPrevLockIconType(getPrevLockType()); |
| |
| // this is not a perfect solution. But currently there is one |
| // WebViewClient for all the WebView. if user switches from an |
| // in-load window to an already loaded window, mPageStarted will not |
| // be set to false. If user leaves the Browser, pauseWebViewTimers() |
| // won't do anything and leaves the timer running even Browser is in |
| // the background. |
| if (mPageStarted) { |
| mPageStarted = false; |
| } |
| } |
| } |
| |
| // Remove the sub window if it exists. Also called by TabControl when the |
| // user clicks the 'X' to dismiss a sub window. |
| /* package */ void dismissSubWindow(TabControl.Tab t) { |
| t.removeSubWindow(mContentView); |
| // Tell the TabControl to dismiss the subwindow. This will destroy |
| // the WebView. |
| mTabControl.dismissSubWindow(t); |
| getTopWindow().requestFocus(); |
| } |
| |
| // A wrapper function of {@link #openTabAndShow(UrlData, boolean, String)} |
| // that accepts url as string. |
| private TabControl.Tab openTabAndShow(String url, boolean closeOnExit, |
| String appId) { |
| return openTabAndShow(new UrlData(url), closeOnExit, appId); |
| } |
| |
| // This method does a ton of stuff. It will attempt to create a new tab |
| // if we haven't reached MAX_TABS. Otherwise it uses the current tab. If |
| // url isn't null, it will load the given url. |
| /* package */ TabControl.Tab openTabAndShow(UrlData urlData, |
| boolean closeOnExit, String appId) { |
| final boolean newTab = mTabControl.getTabCount() != TabControl.MAX_TABS; |
| final TabControl.Tab currentTab = mTabControl.getCurrentTab(); |
| if (newTab) { |
| final TabControl.Tab tab = mTabControl.createNewTab( |
| closeOnExit, appId, urlData.mUrl); |
| WebView webview = tab.getWebView(); |
| // If the last tab was removed from the active tabs page, currentTab |
| // will be null. |
| if (currentTab != null) { |
| removeTabFromContentView(currentTab); |
| } |
| // We must set the new tab as the current tab to reflect the old |
| // animation behavior. |
| mTabControl.setCurrentTab(tab); |
| attachTabToContentView(tab); |
| if (!urlData.isEmpty()) { |
| urlData.loadIn(webview); |
| } |
| return tab; |
| } else { |
| // Get rid of the subwindow if it exists |
| dismissSubWindow(currentTab); |
| if (!urlData.isEmpty()) { |
| // Load the given url. |
| urlData.loadIn(currentTab.getWebView()); |
| } |
| } |
| return currentTab; |
| } |
| |
| private TabControl.Tab openTab(String url) { |
| if (mSettings.openInBackground()) { |
| TabControl.Tab t = mTabControl.createNewTab(); |
| if (t != null) { |
| WebView view = t.getWebView(); |
| view.loadUrl(url); |
| } |
| return t; |
| } else { |
| return openTabAndShow(url, false, null); |
| } |
| } |
| |
| private class Copy implements OnMenuItemClickListener { |
| private CharSequence mText; |
| |
| public boolean onMenuItemClick(MenuItem item) { |
| copy(mText); |
| return true; |
| } |
| |
| public Copy(CharSequence toCopy) { |
| 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 { |
| IClipboard clip = IClipboard.Stub.asInterface(ServiceManager.getService("clipboard")); |
| if (clip != null) { |
| clip.setClipboardText(text); |
| } |
| } catch (android.os.RemoteException e) { |
| Log.e(LOGTAG, "Copy failed", e); |
| } |
| } |
| |
| /** |
| * Resets the browser title-view to whatever it must be |
| * (for example, if we had a loading error) |
| * When we have a new page, we call resetTitle, when we |
| * have to reset the titlebar to whatever it used to be |
| * (for example, if the user chose to stop loading), we |
| * call resetTitleAndRevertLockIcon. |
| */ |
| /* package */ void resetTitleAndRevertLockIcon() { |
| revertLockIcon(); |
| resetTitleIconAndProgress(); |
| } |
| |
| /** |
| * Reset the title, favicon, and progress. |
| */ |
| private void resetTitleIconAndProgress() { |
| WebView current = mTabControl.getCurrentWebView(); |
| if (current == null) { |
| return; |
| } |
| resetTitleAndIcon(current); |
| int progress = current.getProgress(); |
| mWebChromeClient.onProgressChanged(current, progress); |
| } |
| |
| // Reset the title and the icon based on the given item. |
| private void resetTitleAndIcon(WebView view) { |
| WebHistoryItem item = view.copyBackForwardList().getCurrentItem(); |
| if (item != null) { |
| setUrlTitle(item.getUrl(), item.getTitle()); |
| setFavicon(item.getFavicon()); |
| } else { |
| setUrlTitle(null, null); |
| setFavicon(null); |
| } |
| } |
| |
| /** |
| * Sets a title composed of the URL and the title string. |
| * @param url The URL of the site being loaded. |
| * @param title The title of the site being loaded. |
| */ |
| private void setUrlTitle(String url, String title) { |
| mUrl = url; |
| mTitle = title; |
| |
| mTitleBar.setTitleAndUrl(title, url); |
| if (mFakeTitleBar != null) { |
| mFakeTitleBar.setTitleAndUrl(title, url); |
| } |
| } |
| |
| /** |
| * @param url The URL to build a title version of the URL from. |
| * @return The title version of the URL or null if fails. |
| * The title version of the URL can be either the URL hostname, |
| * or the hostname with an "https://" prefix (for secure URLs), |
| * or an empty string if, for example, the URL in question is a |
| * file:// URL with no hostname. |
| */ |
| /* package */ static String buildTitleUrl(String url) { |
| String titleUrl = null; |
| |
| if (url != null) { |
| try { |
| // parse the url string |
| URL urlObj = new URL(url); |
| if (urlObj != null) { |
| titleUrl = ""; |
| |
| String protocol = urlObj.getProtocol(); |
| String host = urlObj.getHost(); |
| |
| if (host != null && 0 < host.length()) { |
| titleUrl = host; |
| if (protocol != null) { |
| // if a secure site, add an "https://" prefix! |
| if (protocol.equalsIgnoreCase("https")) { |
| titleUrl = protocol + "://" + host; |
| } |
| } |
| } |
| } |
| } catch (MalformedURLException e) {} |
| } |
| |
| return titleUrl; |
| } |
| |
| // Set the favicon in the title bar. |
| private void setFavicon(Bitmap icon) { |
| mTitleBar.setFavicon(icon); |
| if (mFakeTitleBar != null) { |
| mFakeTitleBar.setFavicon(icon); |
| } |
| } |
| |
| /** |
| * Saves the current lock-icon state before resetting |
| * the lock icon. If we have an error, we may need to |
| * roll back to the previous state. |
| */ |
| private void saveLockIcon() { |
| mPrevLockType = mLockIconType; |
| } |
| |
| /** |
| * Reverts the lock-icon state to the last saved state, |
| * for example, if we had an error, and need to cancel |
| * the load. |
| */ |
| private void revertLockIcon() { |
| mLockIconType = mPrevLockType; |
| |
| if (LOGV_ENABLED) { |
| Log.v(LOGTAG, "BrowserActivity.revertLockIcon:" + |
| " revert lock icon to " + mLockIconType); |
| } |
| |
| updateLockIconToLatest(); |
| } |
| |
| /** |
| * Close the tab, remove its associated title bar, and adjust mTabControl's |
| * current tab to a valid value. |
| */ |
| /* package */ void closeTab(TabControl.Tab t) { |
| int currentIndex = mTabControl.getCurrentIndex(); |
| int removeIndex = mTabControl.getTabIndex(t); |
| mTabControl.removeTab(t); |
| if (currentIndex >= removeIndex && currentIndex != 0) { |
| currentIndex--; |
| } |
| mTabControl.setCurrentTab(mTabControl.getTab(currentIndex)); |
| resetTitleIconAndProgress(); |
| } |
| |
| private void goBackOnePageOrQuit() { |
| TabControl.Tab current = mTabControl.getCurrentTab(); |
| if (current == null) { |
| /* |
| * Instead of finishing the activity, simply push this to the back |
| * of the stack and let ActivityManager to choose the foreground |
| * activity. As BrowserActivity is singleTask, it will be always the |
| * root of the task. So we can use either true or false for |
| * moveTaskToBack(). |
| */ |
| moveTaskToBack(true); |
| return; |
| } |
| WebView w = current.getWebView(); |
| if (w.canGoBack()) { |
| w.goBack(); |
| } else { |
| // 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 = current.getParentTab(); |
| if (parent != null) { |
| switchToTab(mTabControl.getTabIndex(parent)); |
| // Now we close the other tab |
| closeTab(current); |
| } else { |
| if (current.closeOnExit()) { |
| // force mPageStarted to be false as we are going to either |
| // finish the activity or remove the tab. This will ensure |
| // pauseWebView() taking action. |
| mPageStarted = false; |
| if (mTabControl.getTabCount() == 1) { |
| finish(); |
| return; |
| } |
| // call pauseWebViewTimers() now, we won't be able to call |
| // it in onPause() as the WebView won't be valid. |
| // Temporarily change mActivityInPause to be true as |
| // pauseWebViewTimers() will do nothing if mActivityInPause |
| // is false. |
| boolean savedState = mActivityInPause; |
| if (savedState) { |
| Log.e(LOGTAG, "BrowserActivity is already paused " |
| + "while handing goBackOnePageOrQuit."); |
| } |
| mActivityInPause = true; |
| pauseWebViewTimers(); |
| mActivityInPause = savedState; |
| removeTabFromContentView(current); |
| mTabControl.removeTab(current); |
| } |
| /* |
| * Instead of finishing the activity, simply push this to the back |
| * of the stack and let ActivityManager to choose the foreground |
| * activity. As BrowserActivity is singleTask, it will be always the |
| * root of the task. So we can use either true or false for |
| * moveTaskToBack(). |
| */ |
| moveTaskToBack(true); |
| } |
| } |
| } |
| |
| @Override |
| public boolean onKeyDown(int keyCode, KeyEvent event) { |
| // The default key mode is DEFAULT_KEYS_SEARCH_LOCAL. As the MENU is |
| // still down, we don't want to trigger the search. Pretend to consume |
| // the key and do nothing. |
| if (mMenuIsDown) return true; |
| |
| switch(keyCode) { |
| case KeyEvent.KEYCODE_MENU: |
| mMenuIsDown = true; |
| break; |
| case KeyEvent.KEYCODE_SPACE: |
| // WebView/WebTextView handle the keys in the KeyDown. As |
| // the Activity's shortcut keys are only handled when WebView |
| // doesn't, have to do it in onKeyDown instead of onKeyUp. |
| if (event.isShiftPressed()) { |
| getTopWindow().pageUp(false); |
| } else { |
| getTopWindow().pageDown(false); |
| } |
| return true; |
| case KeyEvent.KEYCODE_BACK: |
| if (event.getRepeatCount() == 0) { |
| event.startTracking(); |
| return true; |
| } else if (mCustomView == null && mActiveTabsPage == null |
| && event.isLongPress()) { |
| bookmarksOrHistoryPicker(true); |
| return true; |
| } |
| break; |
| } |
| return super.onKeyDown(keyCode, event); |
| } |
| |
| @Override |
| public boolean onKeyUp(int keyCode, KeyEvent event) { |
| switch(keyCode) { |
| case KeyEvent.KEYCODE_MENU: |
| mMenuIsDown = false; |
| break; |
| case KeyEvent.KEYCODE_BACK: |
| if (event.isTracking() && !event.isCanceled()) { |
| if (mCustomView != null) { |
| // if a custom view is showing, hide it |
| mWebChromeClient.onHideCustomView(); |
| } else if (mActiveTabsPage != null) { |
| // if tab page is showing, hide it |
| removeActiveTabPage(true); |
| } else { |
| WebView subwindow = mTabControl.getCurrentSubWindow(); |
| if (subwindow != null) { |
| if (subwindow.canGoBack()) { |
| subwindow.goBack(); |
| } else { |
| dismissSubWindow(mTabControl.getCurrentTab()); |
| } |
| } else { |
| goBackOnePageOrQuit(); |
| } |
| } |
| return true; |
| } |
| break; |
| } |
| return super.onKeyUp(keyCode, event); |
| } |
| |
| /* package */ void stopLoading() { |
| mDidStopLoad = true; |
| resetTitleAndRevertLockIcon(); |
| WebView w = getTopWindow(); |
| w.stopLoading(); |
| mWebViewClient.onPageFinished(w, w.getUrl()); |
| |
| cancelStopToast(); |
| mStopToast = Toast |
| .makeText(this, R.string.stopping, Toast.LENGTH_SHORT); |
| mStopToast.show(); |
| } |
| |
| private void cancelStopToast() { |
| if (mStopToast != null) { |
| mStopToast.cancel(); |
| mStopToast = null; |
| } |
| } |
| |
| // 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)); |
| } |
| |
| // public message ids |
| public final static int LOAD_URL = 1001; |
| public final static int STOP_LOAD = 1002; |
| |
| // Message Ids |
| private static final int FOCUS_NODE_HREF = 102; |
| private static final int CANCEL_CREDS_REQUEST = 103; |
| private static final int RELEASE_WAKELOCK = 107; |
| |
| private static final int UPDATE_BOOKMARK_THUMBNAIL = 108; |
| |
| // Private handler for handling javascript and saving passwords |
| private Handler mHandler = new Handler() { |
| |
| public void handleMessage(Message msg) { |
| switch (msg.what) { |
| case FOCUS_NODE_HREF: |
| { |
| String url = (String) msg.getData().get("url"); |
| if (url == null || url.length() == 0) { |
| break; |
| } |
| 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 R.id.open_context_menu_id: |
| case R.id.view_image_context_menu_id: |
| loadURL(getTopWindow(), url); |
| break; |
| case R.id.open_newtab_context_menu_id: |
| final TabControl.Tab parent = mTabControl |
| .getCurrentTab(); |
| final TabControl.Tab newTab = openTab(url); |
| if (newTab != parent) { |
| parent.addChildTab(newTab); |
| } |
| break; |
| case R.id.bookmark_context_menu_id: |
| Intent intent = new Intent(BrowserActivity.this, |
| AddBookmarkPage.class); |
| intent.putExtra("url", url); |
| startActivity(intent); |
| break; |
| case R.id.share_link_context_menu_id: |
| Browser.sendString(BrowserActivity.this, url, |
| getText(R.string.choosertitle_sharevia).toString()); |
| break; |
| case R.id.copy_link_context_menu_id: |
| copy(url); |
| break; |
| case R.id.save_link_context_menu_id: |
| case R.id.download_context_menu_id: |
| onDownloadStartNoStream(url, null, null, null, -1); |
| break; |
| } |
| break; |
| } |
| |
| case LOAD_URL: |
| loadURL(getTopWindow(), (String) msg.obj); |
| break; |
| |
| case STOP_LOAD: |
| stopLoading(); |
| break; |
| |
| case CANCEL_CREDS_REQUEST: |
| resumeAfterCredentials(); |
| break; |
| |
| case RELEASE_WAKELOCK: |
| if (mWakeLock.isHeld()) { |
| mWakeLock.release(); |
| } |
| break; |
| |
| case UPDATE_BOOKMARK_THUMBNAIL: |
| WebView view = (WebView) msg.obj; |
| if (view != null) { |
| updateScreenshot(view); |
| } |
| break; |
| } |
| } |
| }; |
| |
| private void updateScreenshot(WebView view) { |
| // If this is a bookmarked site, add a screenshot to the database. |
| // FIXME: When should we update? Every time? |
| // FIXME: Would like to make sure there is actually something to |
| // draw, but the API for that (WebViewCore.pictureReady()) is not |
| // currently accessible here. |
| |
| ContentResolver cr = getContentResolver(); |
| final Cursor c = BrowserBookmarksAdapter.queryBookmarksForUrl( |
| cr, view.getOriginalUrl(), view.getUrl(), true); |
| if (c != null) { |
| boolean succeed = c.moveToFirst(); |
| ContentValues values = null; |
| while (succeed) { |
| if (values == null) { |
| final ByteArrayOutputStream os |
| = new ByteArrayOutputStream(); |
| Bitmap bm = createScreenshot(view); |
| if (bm == null) { |
| c.close(); |
| return; |
| } |
| bm.compress(Bitmap.CompressFormat.PNG, 100, os); |
| values = new ContentValues(); |
| values.put(Browser.BookmarkColumns.THUMBNAIL, |
| os.toByteArray()); |
| } |
| cr.update(ContentUris.withAppendedId(Browser.BOOKMARKS_URI, |
| c.getInt(0)), values, null, null); |
| succeed = c.moveToNext(); |
| } |
| c.close(); |
| } |
| } |
| |
| /** |
| * Values for the size of the thumbnail created when taking a screenshot. |
| * Lazily initialized. Instead of using these directly, use |
| * getDesiredThumbnailWidth() or getDesiredThumbnailHeight(). |
| */ |
| private static int THUMBNAIL_WIDTH = 0; |
| private static int THUMBNAIL_HEIGHT = 0; |
| |
| /** |
| * Return the desired width for thumbnail screenshots, which are stored in |
| * the database, and used on the bookmarks screen. |
| * @param context Context for finding out the density of the screen. |
| * @return int desired width for thumbnail screenshot. |
| */ |
| /* package */ static int getDesiredThumbnailWidth(Context context) { |
| if (THUMBNAIL_WIDTH == 0) { |
| float density = context.getResources().getDisplayMetrics().density; |
| THUMBNAIL_WIDTH = (int) (90 * density); |
| THUMBNAIL_HEIGHT = (int) (80 * density); |
| } |
| return THUMBNAIL_WIDTH; |
| } |
| |
| /** |
| * Return the desired height for thumbnail screenshots, which are stored in |
| * the database, and used on the bookmarks screen. |
| * @param context Context for finding out the density of the screen. |
| * @return int desired height for thumbnail screenshot. |
| */ |
| /* package */ static int getDesiredThumbnailHeight(Context context) { |
| // To ensure that they are both initialized. |
| getDesiredThumbnailWidth(context); |
| return THUMBNAIL_HEIGHT; |
| } |
| |
| private Bitmap createScreenshot(WebView view) { |
| Picture thumbnail = view.capturePicture(); |
| if (thumbnail == null) { |
| return null; |
| } |
| Bitmap bm = Bitmap.createBitmap(getDesiredThumbnailWidth(this), |
| getDesiredThumbnailHeight(this), Bitmap.Config.ARGB_4444); |
| Canvas canvas = new Canvas(bm); |
| // May need to tweak these values to determine what is the |
| // best scale factor |
| int thumbnailWidth = thumbnail.getWidth(); |
| if (thumbnailWidth > 0) { |
| float scaleFactor = (float) getDesiredThumbnailWidth(this) / |
| (float)thumbnailWidth; |
| canvas.scale(scaleFactor, scaleFactor); |
| } |
| thumbnail.draw(canvas); |
| return bm; |
| } |
| |
| // ------------------------------------------------------------------------- |
| // WebViewClient implementation. |
| //------------------------------------------------------------------------- |
| |
| // Use in overrideUrlLoading |
| /* package */ final static String SCHEME_WTAI = "wtai://wp/"; |
| /* package */ final static String SCHEME_WTAI_MC = "wtai://wp/mc;"; |
| /* package */ final static String SCHEME_WTAI_SD = "wtai://wp/sd;"; |
| /* package */ final static String SCHEME_WTAI_AP = "wtai://wp/ap;"; |
| |
| /* package */ WebViewClient getWebViewClient() { |
| return mWebViewClient; |
| } |
| |
| private void updateIcon(WebView view, Bitmap icon) { |
| if (icon != null) { |
| BrowserBookmarksAdapter.updateBookmarkFavicon(mResolver, |
| view.getOriginalUrl(), view.getUrl(), icon); |
| } |
| setFavicon(icon); |
| } |
| |
| private void updateIcon(String url, Bitmap icon) { |
| if (icon != null) { |
| BrowserBookmarksAdapter.updateBookmarkFavicon(mResolver, |
| null, 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); |
| |
| // We've started to load a new page. If there was a pending message |
| // to save a screenshot then we will now take the new page and |
| // save an incorrect screenshot. Therefore, remove any pending |
| // thumbnail messages from the queue. |
| mHandler.removeMessages(UPDATE_BOOKMARK_THUMBNAIL); |
| |
| // If we start a touch icon load and then load a new page, we don't |
| // want to cancel the current touch icon loader. But, we do want to |
| // create a new one when the touch icon url is known. |
| if (mTouchIconLoader != null) { |
| mTouchIconLoader.mActivity = null; |
| mTouchIconLoader = null; |
| } |
| |
| ErrorConsoleView errorConsole = mTabControl.getCurrentErrorConsole(false); |
| if (errorConsole != null) { |
| errorConsole.clearErrorMessages(); |
| if (mShouldShowErrorConsole) { |
| errorConsole.showConsole(ErrorConsoleView.SHOW_NONE); |
| } |
| } |
| |
| // Call updateIcon instead of setFavicon so the bookmark |
| // database can be updated. |
| updateIcon(url, favicon); |
| |
| if (mSettings.isTracing()) { |
| String host; |
| try { |
| WebAddress uri = new WebAddress(url); |
| host = uri.mHost; |
| } catch (android.net.ParseException ex) { |
| host = "browser"; |
| } |
| host = host.replace('.', '_'); |
| host += ".trace"; |
| mInTrace = true; |
| Debug.startMethodTracing(host, 20 * 1024 * 1024); |
| } |
| |
| // Performance probe |
| if (false) { |
| mStart = SystemClock.uptimeMillis(); |
| mProcessStart = Process.getElapsedCpuTime(); |
| long[] sysCpu = new long[7]; |
| if (Process.readProcFile("/proc/stat", SYSTEM_CPU_FORMAT, null, |
| sysCpu, null)) { |
| mUserStart = sysCpu[0] + sysCpu[1]; |
| mSystemStart = sysCpu[2]; |
| mIdleStart = sysCpu[3]; |
| mIrqStart = sysCpu[4] + sysCpu[5] + sysCpu[6]; |
| } |
| mUiStart = SystemClock.currentThreadTimeMillis(); |
| } |
| |
| if (!mPageStarted) { |
| mPageStarted = true; |
| // if onResume() has been called, resumeWebViewTimers() does |
| // nothing. |
| resumeWebViewTimers(); |
| } |
| |
| // reset sync timer to avoid sync starts during loading a page |
| CookieSyncManager.getInstance().resetSync(); |
| |
| mInLoad = true; |
| mDidStopLoad = false; |
| showFakeTitleBar(); |
| updateInLoadMenuItems(); |
| if (!mIsNetworkUp) { |
| createAndShowNetworkDialog(); |
| if (view != null) { |
| view.setNetworkAvailable(false); |
| } |
| } |
| } |
| |
| @Override |
| public void onPageFinished(WebView view, String url) { |
| // Reset the title and icon in case we stopped a provisional |
| // load. |
| resetTitleAndIcon(view); |
| |
| if (!mDidStopLoad) { |
| // Only update the bookmark screenshot if the user did not |
| // cancel the load early. |
| Message updateScreenshot = Message.obtain(mHandler, UPDATE_BOOKMARK_THUMBNAIL, view); |
| mHandler.sendMessageDelayed(updateScreenshot, 500); |
| } |
| |
| // Update the lock icon image only once we are done loading |
| updateLockIconToLatest(); |
| |
| // Performance probe |
| if (false) { |
| long[] sysCpu = new long[7]; |
| if (Process.readProcFile("/proc/stat", SYSTEM_CPU_FORMAT, null, |
| sysCpu, null)) { |
| String uiInfo = "UI thread used " |
| + (SystemClock.currentThreadTimeMillis() - mUiStart) |
| + " ms"; |
| if (LOGD_ENABLED) { |
| Log.d(LOGTAG, uiInfo); |
| } |
| //The string that gets written to the log |
| String performanceString = "It took total " |
| + (SystemClock.uptimeMillis() - mStart) |
| + " ms clock time to load the page." |
| + "\nbrowser process used " |
| + (Process.getElapsedCpuTime() - mProcessStart) |
| + " ms, user processes used " |
| + (sysCpu[0] + sysCpu[1] - mUserStart) * 10 |
| + " ms, kernel used " |
| + (sysCpu[2] - mSystemStart) * 10 |
| + " ms, idle took " + (sysCpu[3] - mIdleStart) * 10 |
| + " ms and irq took " |
| + (sysCpu[4] + sysCpu[5] + sysCpu[6] - mIrqStart) |
| * 10 + " ms, " + uiInfo; |
| if (LOGD_ENABLED) { |
| Log.d(LOGTAG, performanceString + "\nWebpage: " + url); |
| } |
| if (url != null) { |
| // strip the url to maintain consistency |
| String newUrl = new String(url); |
| if (newUrl.startsWith("http://www.")) { |
| newUrl = newUrl.substring(11); |
| } else if (newUrl.startsWith("http://")) { |
| newUrl = newUrl.substring(7); |
| } else if (newUrl.startsWith("https://www.")) { |
| newUrl = newUrl.substring(12); |
| } else if (newUrl.startsWith("https://")) { |
| newUrl = newUrl.substring(8); |
| } |
| if (LOGD_ENABLED) { |
| Log.d(LOGTAG, newUrl + " loaded"); |
| } |
| /* |
| if (sWhiteList.contains(newUrl)) { |
| // The string that gets pushed to the statistcs |
| // service |
| performanceString = performanceString |
| + "\nWebpage: " |
| + newUrl |
| + "\nCarrier: " |
| + android.os.SystemProperties |
| .get("gsm.sim.operator.alpha"); |
| if (mWebView != null |
| && mWebView.getContext() != null |
| && mWebView.getContext().getSystemService( |
| Context.CONNECTIVITY_SERVICE) != null) { |
| ConnectivityManager cManager = |
| (ConnectivityManager) mWebView |
| .getContext().getSystemService( |
| Context.CONNECTIVITY_SERVICE); |
| NetworkInfo nInfo = cManager |
| .getActiveNetworkInfo(); |
| if (nInfo != null) { |
| performanceString = performanceString |
| + "\nNetwork Type: " |
| + nInfo.getType().toString(); |
| } |
| } |
| Checkin.logEvent(mResolver, |
| Checkin.Events.Tag.WEBPAGE_LOAD, |
| performanceString); |
| Log.w(LOGTAG, "pushed to the statistics service"); |
| } |
| */ |
| } |
| } |
| } |
| |
| if (mInTrace) { |
| mInTrace = false; |
| Debug.stopMethodTracing(); |
| } |
| |
| if (mPageStarted) { |
| mPageStarted = false; |
| // pauseWebViewTimers() will do nothing and return false if |
| // onPause() is not called yet. |
| if (pauseWebViewTimers()) { |
| if (mWakeLock.isHeld()) { |
| mHandler.removeMessages(RELEASE_WAKELOCK); |
| mWakeLock.release(); |
| } |
| } |
| } |
| } |
| |
| // return true if want to hijack the url to let another app to handle it |
| @Override |
| public boolean shouldOverrideUrlLoading(WebView view, String url) { |
| if (url.startsWith(SCHEME_WTAI)) { |
| // wtai://wp/mc;number |
| // number=string(phone-number) |
| if (url.startsWith(SCHEME_WTAI_MC)) { |
| Intent intent = new Intent(Intent.ACTION_VIEW, |
| Uri.parse(WebView.SCHEME_TEL + |
| url.substring(SCHEME_WTAI_MC.length()))); |
| startActivity(intent); |
| return true; |
| } |
| // wtai://wp/sd;dtmf |
| // dtmf=string(dialstring) |
| if (url.startsWith(SCHEME_WTAI_SD)) { |
| // TODO |
| // only send when there is active voice connection |
| return false; |
| } |
| // wtai://wp/ap;number;name |
| // number=string(phone-number) |
| // name=string |
| if (url.startsWith(SCHEME_WTAI_AP)) { |
| // TODO |
| return false; |
| } |
| } |
| |
| // The "about:" schemes are internal to the browser; don't |
| // want these to be dispatched to other apps. |
| if (url.startsWith("about:")) { |
| return false; |
| } |
| |
| Intent intent; |
| |
| // perform generic parsing of the URI to turn it into an Intent. |
| try { |
| intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME); |
| } catch (URISyntaxException ex) { |
| Log.w("Browser", "Bad URI " + url + ": " + ex.getMessage()); |
| return false; |
| } |
| |
| // check whether the intent can be resolved. If not, we will see |
| // whether we can download it from the Market. |
| if (getPackageManager().resolveActivity(intent, 0) == null) { |
| String packagename = intent.getPackage(); |
| if (packagename != null) { |
| intent = new Intent(Intent.ACTION_VIEW, Uri |
| .parse("market://search?q=pname:" + packagename)); |
| intent.addCategory(Intent.CATEGORY_BROWSABLE); |
| startActivity(intent); |
| return true; |
| } else { |
| return false; |
| } |
| } |
| |
| // sanitize the Intent, ensuring web pages can not bypass browser |
| // security (only access to BROWSABLE activities). |
| intent.addCategory(Intent.CATEGORY_BROWSABLE); |
| intent.setComponent(null); |
| try { |
| if (startActivityIfNeeded(intent, -1)) { |
| return true; |
| } |
| } catch (ActivityNotFoundException ex) { |
| // ignore the error. If no application can handle the URL, |
| // eg about:blank, assume the browser can handle it. |
| } |
| |
| if (mMenuIsDown) { |
| openTab(url); |
| closeOptionsMenu(); |
| return true; |
| } |
| |
| return false; |
| } |
| |
| /** |
| * Updates the lock icon. This method is called when we discover another |
| * resource to be loaded for this page (for example, javascript). While |
| * we update the icon type, we do not update the lock icon itself until |
| * we are done loading, it is slightly more secure this way. |
| */ |
| @Override |
| public void onLoadResource(WebView view, String url) { |
| if (url != null && url.length() > 0) { |
| // It is only if the page claims to be secure |
| // that we may have to update the lock: |
| if (mLockIconType == LOCK_ICON_SECURE) { |
| // If NOT a 'safe' url, change the lock to mixed content! |
| if (!(URLUtil.isHttpsUrl(url) || URLUtil.isDataUrl(url) || URLUtil.isAboutUrl(url))) { |
| mLockIconType = LOCK_ICON_MIXED; |
| if (LOGV_ENABLED) { |
| Log.v(LOGTAG, "BrowserActivity.updateLockIcon:" + |
| " updated lock icon to " + mLockIconType + " due to " + url); |
| } |
| } |
| } |
| } |
| } |
| |
| /** |
| * Show the dialog, asking the user if they would like to continue after |
| * an excessive number of HTTP redirects. |
| */ |
| @Override |
| public void onTooManyRedirects(WebView view, final Message cancelMsg, |
| final Message continueMsg) { |
| new AlertDialog.Builder(BrowserActivity.this) |
| .setTitle(R.string.browserFrameRedirect) |
| .setMessage(R.string.browserFrame307Post) |
| .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { |
| public void onClick(DialogInterface dialog, int which) { |
| continueMsg.sendToTarget(); |
| }}) |
| .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { |
| public void onClick(DialogInterface dialog, int which) { |
| cancelMsg.sendToTarget(); |
| }}) |
| .setOnCancelListener(new OnCancelListener() { |
| public void onCancel(DialogInterface dialog) { |
| cancelMsg.sendToTarget(); |
| }}) |
| .show(); |
| } |
| |
| // Container class for the next error dialog that needs to be |
| // displayed. |
| class ErrorDialog { |
| public final int mTitle; |
| public final String mDescription; |
| public final int mError; |
| ErrorDialog(int title, String desc, int error) { |
| mTitle = title; |
| mDescription = desc; |
| mError = error; |
| } |
| }; |
| |
| private void processNextError() { |
| if (mQueuedErrors == null) { |
| return; |
| } |
| // The first one is currently displayed so just remove it. |
| mQueuedErrors.removeFirst(); |
| if (mQueuedErrors.size() == 0) { |
| mQueuedErrors = null; |
| return; |
| } |
| showError(mQueuedErrors.getFirst()); |
| } |
| |
| private DialogInterface.OnDismissListener mDialogListener = |
| new DialogInterface.OnDismissListener() { |
| public void onDismiss(DialogInterface d) { |
| processNextError(); |
| } |
| }; |
| private LinkedList<ErrorDialog> mQueuedErrors; |
| |
| private void queueError(int err, String desc) { |
| if (mQueuedErrors == null) { |
| mQueuedErrors = new LinkedList<ErrorDialog>(); |
| } |
| for (ErrorDialog d : mQueuedErrors) { |
| if (d.mError == err) { |
| // Already saw a similar error, ignore the new one. |
| return; |
| } |
| } |
| ErrorDialog errDialog = new ErrorDialog( |
| err == WebViewClient.ERROR_FILE_NOT_FOUND ? |
| R.string.browserFrameFileErrorLabel : |
| R.string.browserFrameNetworkErrorLabel, |
| desc, err); |
| mQueuedErrors.addLast(errDialog); |
| |
| // Show the dialog now if the queue was empty. |
| if (mQueuedErrors.size() == 1) { |
| showError(errDialog); |
| } |
| } |
| |
| private void showError(ErrorDialog errDialog) { |
| AlertDialog d = new AlertDialog.Builder(BrowserActivity.this) |
| .setTitle(errDialog.mTitle) |
| .setMessage(errDialog.mDescription) |
| .setPositiveButton(R.string.ok, null) |
| .create(); |
| d.setOnDismissListener(mDialogListener); |
| d.show(); |
| } |
| |
| /** |
| * Show a dialog informing the user of the network error reported by |
| * WebCore. |
| */ |
| @Override |
| public void onReceivedError(WebView view, int errorCode, |
| String description, String failingUrl) { |
| if (errorCode != WebViewClient.ERROR_HOST_LOOKUP && |
| errorCode != WebViewClient.ERROR_CONNECT && |
| errorCode != WebViewClient.ERROR_BAD_URL && |
| errorCode != WebViewClient.ERROR_UNSUPPORTED_SCHEME && |
| errorCode != WebViewClient.ERROR_FILE) { |
| queueError(errorCode, description); |
| } |
| Log.e(LOGTAG, "onReceivedError " + errorCode + " " + failingUrl |
| + " " + description); |
| |
| // We need to reset the title after an error. |
| resetTitleAndRevertLockIcon(); |
| } |
| |
| /** |
| * Check with the user if it is ok to resend POST data as the page they |
| * are trying to navigate to is the result of a POST. |
| */ |
| @Override |
| public void onFormResubmission(WebView view, final Message dontResend, |
| final Message resend) { |
| new AlertDialog.Builder(BrowserActivity.this) |
| .setTitle(R.string.browserFrameFormResubmitLabel) |
| .setMessage(R.string.browserFrameFormResubmitMessage) |
| .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { |
| public void onClick(DialogInterface dialog, int which) { |
| resend.sendToTarget(); |
| }}) |
| .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { |
| public void onClick(DialogInterface dialog, int which) { |
| dontResend.sendToTarget(); |
| }}) |
| .setOnCancelListener(new OnCancelListener() { |
| public void onCancel(DialogInterface dialog) { |
| dontResend.sendToTarget(); |
| }}) |
| .show(); |
| } |
| |
| /** |
| * Insert the url into the visited history database. |
| * @param url The url to be inserted. |
| * @param isReload True if this url is being reloaded. |
| * FIXME: Not sure what to do when reloading the page. |
| */ |
| @Override |
| public void doUpdateVisitedHistory(WebView view, String url, |
| boolean isReload) { |
| if (url.regionMatches(true, 0, "about:", 0, 6)) { |
| return; |
| } |
| // remove "client" before updating it to the history so that it wont |
| // show up in the auto-complete list. |
| int index = url.indexOf("client=ms-"); |
| if (index > 0 && url.contains(".google.")) { |
| int end = url.indexOf('&', index); |
| if (end > 0) { |
| url = url.substring(0, index) |
| .concat(url.substring(end + 1)); |
| } else { |
| // the url.charAt(index-1) should be either '?' or '&' |
| url = url.substring(0, index-1); |
| } |
| } |
| Browser.updateVisitedHistory(mResolver, url, true); |
| WebIconDatabase.getInstance().retainIconForPageUrl(url); |
| } |
| |
| /** |
| * Displays SSL error(s) dialog to the user. |
| */ |
| @Override |
| public void onReceivedSslError( |
| final WebView view, final SslErrorHandler handler, final SslError error) { |
| |
| if (mSettings.showSecurityWarnings()) { |
| final LayoutInflater factory = |
| LayoutInflater.from(BrowserActivity.this); |
| final View warningsView = |
| factory.inflate(R.layout.ssl_warnings, null); |
| final LinearLayout placeholder = |
| (LinearLayout)warningsView.findViewById(R.id.placeholder); |
| |
| if (error.hasError(SslError.SSL_UNTRUSTED)) { |
| LinearLayout ll = (LinearLayout)factory |
| .inflate(R.layout.ssl_warning, null); |
| ((TextView)ll.findViewById(R.id.warning)) |
| .setText(R.string.ssl_untrusted); |
| placeholder.addView(ll); |
| } |
| |
| if (error.hasError(SslError.SSL_IDMISMATCH)) { |
| LinearLayout ll = (LinearLayout)factory |
| .inflate(R.layout.ssl_warning, null); |
| ((TextView)ll.findViewById(R.id.warning)) |
| .setText(R.string.ssl_mismatch); |
| placeholder.addView(ll); |
| } |
| |
| if (error.hasError(SslError.SSL_EXPIRED)) { |
| LinearLayout ll = (LinearLayout)factory |
| .inflate(R.layout.ssl_warning, null); |
| ((TextView)ll.findViewById(R.id.warning)) |
| .setText(R.string.ssl_expired); |
| placeholder.addView(ll); |
| } |
| |
| if (error.hasError(SslError.SSL_NOTYETVALID)) { |
| LinearLayout ll = (LinearLayout)factory |
| .inflate(R.layout.ssl_warning, null); |
| ((TextView)ll.findViewById(R.id.warning)) |
| .setText(R.string.ssl_not_yet_valid); |
| placeholder.addView(ll); |
| } |
| |
| new AlertDialog.Builder(BrowserActivity.this) |
| .setTitle(R.string.security_warning) |
| .setIcon(android.R.drawable.ic_dialog_alert) |
| .setView(warningsView) |
| .setPositiveButton(R.string.ssl_continue, |
| new DialogInterface.OnClickListener() { |
| public void onClick(DialogInterface dialog, int whichButton) { |
| handler.proceed(); |
| } |
| }) |
| .setNeutralButton(R.string.view_certificate, |
| new DialogInterface.OnClickListener() { |
| public void onClick(DialogInterface dialog, int whichButton) { |
| showSSLCertificateOnError(view, handler, error); |
| } |
| }) |
| .setNegativeButton(R.string.cancel, |
| new DialogInterface.OnClickListener() { |
| public void onClick(DialogInterface dialog, int whichButton) { |
| handler.cancel(); |
| BrowserActivity.this.resetTitleAndRevertLockIcon(); |
| } |
| }) |
| .setOnCancelListener( |
| new DialogInterface.OnCancelListener() { |
| public void onCancel(DialogInterface dialog) { |
| handler.cancel(); |
| BrowserActivity.this.resetTitleAndRevertLockIcon(); |
| } |
| }) |
| .show(); |
| } else { |
| handler.proceed(); |
| } |
| } |
| |
| /** |
| * Handles an HTTP authentication request. |
| * |
| * @param handler The authentication handler |
| * @param host The host |
| * @param realm The realm |
| */ |
| @Override |
| public void onReceivedHttpAuthRequest(WebView view, |
| final HttpAuthHandler handler, final String host, final String realm) { |
| String username = null; |
| String password = null; |
| |
| boolean reuseHttpAuthUsernamePassword = |
| handler.useHttpAuthUsernamePassword(); |
| |
| if (reuseHttpAuthUsernamePassword && |
| (mTabControl.getCurrentWebView() != null)) { |
| String[] credentials = |
| mTabControl.getCurrentWebView() |
| .getHttpAuthUsernamePassword(host, realm); |
| if (credentials != null && credentials.length == 2) { |
| username = credentials[0]; |
| password = credentials[1]; |
| } |
| } |
| |
| if (username != null && password != null) { |
| handler.proceed(username, password); |
| } else { |
| showHttpAuthentication(handler, host, realm, null, null, null, 0); |
| } |
| } |
| |
| @Override |
| public boolean shouldOverrideKeyEvent(WebView view, KeyEvent event) { |
| if (mMenuIsDown) { |
| // only check shortcut key when MENU is held |
| return getWindow().isShortcutKey(event.getKeyCode(), event); |
| } else { |
| return false; |
| } |
| } |
| |
| @Override |
| public void onUnhandledKeyEvent(WebView view, KeyEvent event) { |
| if (view != mTabControl.getCurrentTopWebView()) { |
| return; |
| } |
| if (event.isDown()) { |
| BrowserActivity.this.onKeyDown(event.getKeyCode(), event); |
| } else { |
| BrowserActivity.this.onKeyUp(event.getKeyCode(), event); |
| } |
| } |
| }; |
| |
| //-------------------------------------------------------------------------- |
| // WebChromeClient implementation |
| //-------------------------------------------------------------------------- |
| |
| /* package */ WebChromeClient getWebChromeClient() { |
| return mWebChromeClient; |
| } |
| |
| private final WebChromeClient mWebChromeClient = new WebChromeClient() { |
| // Helper method to create a new tab or sub window. |
| private void createWindow(final boolean dialog, final Message msg) { |
| if (dialog) { |
| mTabControl.createSubWindow(); |
| final TabControl.Tab t = mTabControl.getCurrentTab(); |
| attachSubWindow(t); |
| WebView.WebViewTransport transport = |
| (WebView.WebViewTransport) msg.obj; |
| transport.setWebView(t.getSubWebView()); |
| msg.sendToTarget(); |
| } else { |
| final TabControl.Tab parent = mTabControl.getCurrentTab(); |
| final TabControl.Tab newTab |
| = openTabAndShow(EMPTY_URL_DATA, false, null); |
| if (newTab != parent) { |
| parent.addChildTab(newTab); |
| } |
| WebView.WebViewTransport transport = |
| (WebView.WebViewTransport) msg.obj; |
| transport.setWebView(mTabControl.getCurrentWebView()); |
| msg.sendToTarget(); |
| } |
| } |
| |
| @Override |
| public boolean onCreateWindow(WebView view, final boolean dialog, |
| final boolean userGesture, final Message resultMsg) { |
| // Short-circuit if we can't create any more tabs or sub windows. |
| if (dialog && mTabControl.getCurrentSubWindow() != null) { |
| new AlertDialog.Builder(BrowserActivity.this) |
| .setTitle(R.string.too_many_subwindows_dialog_title) |
| .setIcon(android.R.drawable.ic_dialog_alert) |
| .setMessage(R.string.too_many_subwindows_dialog_message) |
| .setPositiveButton(R.string.ok, null) |
| .show(); |
| return false; |
| } else if (mTabControl.getTabCount() >= TabControl.MAX_TABS) { |
| new AlertDialog.Builder(BrowserActivity.this) |
| .setTitle(R.string.too_many_windows_dialog_title) |
| .setIcon(android.R.drawable.ic_dialog_alert) |
| .setMessage(R.string.too_many_windows_dialog_message) |
| .setPositiveButton(R.string.ok, null) |
| .show(); |
| return false; |
| } |
| |
| // Short-circuit if this was a user gesture. |
| if (userGesture) { |
| createWindow(dialog, resultMsg); |
| return true; |
| } |
| |
| // Allow the popup and create the appropriate window. |
| final AlertDialog.OnClickListener allowListener = |
| new AlertDialog.OnClickListener() { |
| public void onClick(DialogInterface d, |
| int which) { |
| createWindow(dialog, resultMsg); |
| } |
| }; |
| |
| // Block the popup by returning a null WebView. |
| final AlertDialog.OnClickListener blockListener = |
| new AlertDialog.OnClickListener() { |
| public void onClick(DialogInterface d, int which) { |
| resultMsg.sendToTarget(); |
| } |
| }; |
| |
| // Build a confirmation dialog to display to the user. |
| final AlertDialog d = |
| new AlertDialog.Builder(BrowserActivity.this) |
| .setTitle(R.string.attention) |
| .setIcon(android.R.drawable.ic_dialog_alert) |
| .setMessage(R.string.popup_window_attempt) |
| .setPositiveButton(R.string.allow, allowListener) |
| .setNegativeButton(R.string.block, blockListener) |
| .setCancelable(false) |
| .create(); |
| |
| // Show the confirmation dialog. |
| d.show(); |
| return true; |
| } |
| |
| @Override |
| public void onCloseWindow(WebView window) { |
| final TabControl.Tab current = mTabControl.getCurrentTab(); |
| final TabControl.Tab parent = current.getParentTab(); |
| if (parent != null) { |
| // JavaScript can only close popup window. |
| switchToTab(mTabControl.getTabIndex(parent)); |
| // Now we need to close the window |
| closeTab(current); |
| } |
| } |
| |
| @Override |
| public void onProgressChanged(WebView view, int newProgress) { |
| mTitleBar.setProgress(newProgress); |
| if (mFakeTitleBar != null) { |
| mFakeTitleBar.setProgress(newProgress); |
| } |
| |
| if (newProgress == 100) { |
| // onProgressChanged() may continue to be called after the main |
| // frame has finished loading, as any remaining sub frames |
| // continue to load. We'll only get called once though with |
| // newProgress as 100 when everything is loaded. |
| // (onPageFinished is called once when the main frame completes |
| // loading regardless of the state of any sub frames so calls |
| // to onProgressChanges may continue after onPageFinished has |
| // executed) |
| |
| // sync cookies and cache promptly here. |
| CookieSyncManager.getInstance().sync(); |
| if (mInLoad) { |
| mInLoad = false; |
| updateInLoadMenuItems(); |
| // If the options menu is open, leave the title bar |
| if (!mOptionsMenuOpen || !mIconView) { |
| hideFakeTitleBar(); |
| } |
| } |
| } else if (!mInLoad) { |
| // onPageFinished may have already been called but a subframe |
| // is still loading and updating the progress. Reset mInLoad |
| // and update the menu items. |
| mInLoad = true; |
| updateInLoadMenuItems(); |
| if (!mOptionsMenuOpen || mIconView) { |
| // This page has begun to load, so show the title bar |
| showFakeTitleBar(); |
| } |
| } |
| } |
| |
| @Override |
| public void onReceivedTitle(WebView view, String title) { |
| String url = view.getUrl(); |
| |
| // here, if url is null, we want to reset the title |
| setUrlTitle(url, title); |
| |
| if (url == null || |
| url.length() >= SQLiteDatabase.SQLITE_MAX_LIKE_PATTERN_LENGTH) { |
| return; |
| } |
| // See if we can find the current url in our history database and |
| // add the new title to it. |
| if (url.startsWith("http://www.")) { |
| url = url.substring(11); |
| } else if (url.startsWith("http://")) { |
| url = url.substring(4); |
| } |
| try { |
| url = "%" + url; |
| String [] selArgs = new String[] { url }; |
| |
| String where = Browser.BookmarkColumns.URL + " LIKE ? AND " |
| + Browser.BookmarkColumns.BOOKMARK + " = 0"; |
| Cursor c = mResolver.query(Browser.BOOKMARKS_URI, |
| Browser.HISTORY_PROJECTION, where, selArgs, null); |
| if (c.moveToFirst()) { |
| // Current implementation of database only has one entry per |
| // url. |
| ContentValues map = new ContentValues(); |
| map.put(Browser.BookmarkColumns.TITLE, title); |
| mResolver.update(Browser.BOOKMARKS_URI, map, |
| "_id = " + c.getInt(0), null); |
| } |
| c.close(); |
| } catch (IllegalStateException e) { |
| Log.e(LOGTAG, "BrowserActivity onReceived title", e); |
| } catch (SQLiteException ex) { |
| Log.e(LOGTAG, "onReceivedTitle() caught SQLiteException: ", ex); |
| } |
| } |
| |
| @Override |
| public void onReceivedIcon(WebView view, Bitmap icon) { |
| updateIcon(view, icon); |
| } |
| |
| @Override |
| public void onReceivedTouchIconUrl(WebView view, String url, |
| boolean precomposed) { |
| final ContentResolver cr = getContentResolver(); |
| final Cursor c = |
| BrowserBookmarksAdapter.queryBookmarksForUrl(cr, |
| view.getOriginalUrl(), view.getUrl(), true); |
| if (c != null) { |
| if (c.getCount() > 0) { |
| // Let precomposed icons take precedence over non-composed |
| // icons. |
| if (precomposed && mTouchIconLoader != null) { |
| mTouchIconLoader.cancel(false); |
| mTouchIconLoader = null; |
| } |
| // Have only one async task at a time. |
| if (mTouchIconLoader == null) { |
| mTouchIconLoader = new DownloadTouchIcon( |
| BrowserActivity.this, cr, c, view); |
| mTouchIconLoader.execute(url); |
| } |
| } else { |
| c.close(); |
| } |
| } |
| } |
| |
| @Override |
| public void onShowCustomView(View view, WebChromeClient.CustomViewCallback callback) { |
| if (mCustomView != null) |
| return; |
| |
| // Add the custom view to its container. |
| mCustomViewContainer.addView(view, COVER_SCREEN_GRAVITY_CENTER); |
| mCustomView = view; |
| mCustomViewCallback = callback; |
| // Save the menu state and set it to empty while the custom |
| // view is showing. |
| mOldMenuState = mMenuState; |
| mMenuState = EMPTY_MENU; |
| // Hide the content view. |
| mContentView.setVisibility(View.GONE); |
| // Finally show the custom view container. |
| mCustomViewContainer.setVisibility(View.VISIBLE); |
| mCustomViewContainer.bringToFront(); |
| } |
| |
| @Override |
| public void onHideCustomView() { |
| if (mCustomView == null) |
| return; |
| |
| // Hide the custom view. |
| mCustomView.setVisibility(View.GONE); |
| // Remove the custom view from its container. |
| mCustomViewContainer.removeView(mCustomView); |
| mCustomView = null; |
| // Reset the old menu state. |
| mMenuState = mOldMenuState; |
| mOldMenuState = EMPTY_MENU; |
| mCustomViewContainer.setVisibility(View.GONE); |
| mCustomViewCallback.onCustomViewHidden(); |
| // Show the content view. |
| mContentView.setVisibility(View.VISIBLE); |
| } |
| |
| /** |
| * The origin has exceeded its database quota. |
| * @param url the URL that exceeded the quota |
| * @param databaseIdentifier the identifier of the database on |
| * which the transaction that caused the quota overflow was run |
| * @param currentQuota the current quota for the origin. |
| * @param estimatedSize the estimated size of the database. |
| * @param totalUsedQuota is the sum of all origins' quota. |
| * @param quotaUpdater The callback to run when a decision to allow or |
| * deny quota has been made. Don't forget to call this! |
| */ |
| @Override |
| public void onExceededDatabaseQuota(String url, |
| String databaseIdentifier, long currentQuota, long estimatedSize, |
| long totalUsedQuota, WebStorage.QuotaUpdater quotaUpdater) { |
| mSettings.getWebStorageSizeManager().onExceededDatabaseQuota( |
| url, databaseIdentifier, currentQuota, estimatedSize, |
| totalUsedQuota, quotaUpdater); |
| } |
| |
| /** |
| * The Application Cache has exceeded its max size. |
| * @param spaceNeeded is the amount of disk space that would be needed |
| * in order for the last appcache operation to succeed. |
| * @param totalUsedQuota is the sum of all origins' quota. |
| * @param quotaUpdater A callback to inform the WebCore thread that a new |
| * app cache size is available. This callback must always be executed at |
| * some point to ensure that the sleeping WebCore thread is woken up. |
| */ |
| @Override |
| public void onReachedMaxAppCacheSize(long spaceNeeded, |
| long totalUsedQuota, WebStorage.QuotaUpdater quotaUpdater) { |
| mSettings.getWebStorageSizeManager().onReachedMaxAppCacheSize( |
| spaceNeeded, totalUsedQuota, quotaUpdater); |
| } |
| |
| /** |
| * Instructs the browser to show a prompt to ask the user to set the |
| * Geolocation permission state for the specified origin. |
| * @param origin The origin for which Geolocation permissions are |
| * requested. |
| * @param callback The callback to call once the user has set the |
| * Geolocation permission state. |
| */ |
| @Override |
| public void onGeolocationPermissionsShowPrompt(String origin, |
| GeolocationPermissions.Callback callback) { |
| mTabControl.getCurrentTab().getGeolocationPermissionsPrompt().show( |
| origin, callback); |
| } |
| |
| /** |
| * Instructs the browser to hide the Geolocation permissions prompt. |
| */ |
| @Override |
| public void onGeolocationPermissionsHidePrompt() { |
| mTabControl.getCurrentTab().getGeolocationPermissionsPrompt().hide(); |
| } |
| |
| /* Adds a JavaScript error message to the system log and if the JS |
| * console is enabled in the about:debug options, to that console |
| * also. |
| * @param message The error message to report. |
| * @param lineNumber The line number of the error. |
| * @param sourceID The name of the source file that caused the error. |
| */ |
| @Override |
| public void onConsoleMessage(String message, int lineNumber, String sourceID) { |
| ErrorConsoleView errorConsole = mTabControl.getCurrentErrorConsole(true); |
| errorConsole.addErrorMessage(message, sourceID, lineNumber); |
| if (mShouldShowErrorConsole && |
| errorConsole.getShowState() != ErrorConsoleView.SHOW_MAXIMIZED) { |
| errorConsole.showConsole(ErrorConsoleView.SHOW_MINIMIZED); |
| } |
| Log.w(LOGTAG, "Console: " + message + " " + sourceID + ":" + lineNumber); |
| } |
| |
| /** |
| * Ask the browser for an icon to represent a <video> element. |
| * This icon will be used if the Web page did not specify a poster attribute. |
| * |
| * @return Bitmap The icon or null if no such icon is available. |
| * @hide pending API Council approval |
| */ |
| @Override |
| public Bitmap getDefaultVideoPoster() { |
| if (mDefaultVideoPoster == null) { |
| mDefaultVideoPoster = BitmapFactory.decodeResource( |
| getResources(), R.drawable.default_video_poster); |
| } |
| return mDefaultVideoPoster; |
| } |
| |
| /** |
| * Ask the host application for a custom progress view to show while |
| * a <video> is loading. |
| * |
| * @return View The progress view. |
| * @hide pending API Council approval |
| */ |
| @Override |
| public View getVideoLoadingProgressView() { |
| if (mVideoProgressView == null) { |
| LayoutInflater inflater = LayoutInflater.from(BrowserActivity.this); |
| mVideoProgressView = inflater.inflate(R.layout.video_loading_progress, null); |
| } |
| return mVideoProgressView; |
| } |
| |
| /** |
| * Deliver a list of already-visited URLs |
| * @hide pending API Council approval |
| */ |
| @Override |
| public void getVisitedHistory(final ValueCallback<String[]> callback) { |
| AsyncTask<Void, Void, String[]> task = new AsyncTask<Void, Void, String[]>() { |
| public String[] doInBackground(Void... unused) { |
| return Browser.getVisitedHistory(getContentResolver()); |
| } |
| |
| public void onPostExecute(String[] result) { |
| callback.onReceiveValue(result); |
| |
| }; |
| }; |
| task.execute(); |
| }; |
| }; |
| |
| /** |
| * Notify the host application a download should be done, or that |
| * the data should be streamed if a streaming viewer is available. |
| * @param url The full url to the content that should be downloaded |
| * @param contentDisposition Content-disposition http header, if |
| * present. |
| * @param mimetype The mimetype of the content reported by the server |
| * @param contentLength The file size reported by the server |
| */ |
| public void onDownloadStart(String url, String userAgent, |
| String contentDisposition, String mimetype, long contentLength) { |
| // if we're dealing wih A/V content that's not explicitly marked |
| // for download, check if it's streamable. |
| if (contentDisposition == null |
| || !contentDisposition.regionMatches( |
| true, 0, "attachment", 0, 10)) { |
| // query the package manager to see if there's a registered handler |
| // that matches. |
| Intent intent = new Intent(Intent.ACTION_VIEW); |
| intent.setDataAndType(Uri.parse(url), mimetype); |
| ResolveInfo info = getPackageManager().resolveActivity(intent, |
| PackageManager.MATCH_DEFAULT_ONLY); |
| if (info != null) { |
| ComponentName myName = getComponentName(); |
| // If we resolved to ourselves, we don't want to attempt to |
| // load the url only to try and download it again. |
| if (!myName.getPackageName().equals( |
| info.activityInfo.packageName) |
| || !myName.getClassName().equals( |
| info.activityInfo.name)) { |
| // someone (other than us) knows how to handle this mime |
| // type with this scheme, don't download. |
| try { |
| startActivity(intent); |
| return; |
| } catch (ActivityNotFoundException ex) { |
| if (LOGD_ENABLED) { |
| Log.d(LOGTAG, "activity not found for " + mimetype |
| + " over " + Uri.parse(url).getScheme(), |
| ex); |
| } |
| // Best behavior is to fall back to a download in this |
| // case |
| } |
| } |
| } |
| } |
| onDownloadStartNoStream(url, userAgent, contentDisposition, mimetype, contentLength); |
| } |
| |
| /** |
| * Notify the host application a download should be done, even if there |
| * is a streaming viewer available for thise type. |
| * @param url The full url to the content that should be downloaded |
| * @param contentDisposition Content-disposition http header, if |
| * present. |
| * @param mimetype The mimetype of the content reported by the server |
| * @param contentLength The file size reported by the server |
| */ |
| /*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 |
| 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(title) |
| .setIcon(android.R.drawable.ic_dialog_alert) |
| .setMessage(msg) |
| .setPositiveButton(R.string.ok, null) |
| .show(); |
| return; |
| } |
| |
| // java.net.URI is a lot stricter than KURL so we have to undo |
| // KURL's percent-encoding and redo the encoding using java.net.URI. |
| URI uri = null; |
| try { |
| // Undo the percent-encoding that KURL may have done. |
| String newUrl = new String(URLUtil.decode(url.getBytes())); |
| // Parse the url into pieces |
| WebAddress w = new WebAddress(newUrl); |
| String frag = null; |
| String query = null; |
| String path = w.mPath; |
| // Break the path into path, query, and fragment |
| if (path.length() > 0) { |
| // Strip the fragment |
| int idx = path.lastIndexOf('#'); |
| if (idx != -1) { |
| frag = path.substring(idx + 1); |
| path = path.substring(0, idx); |
| } |
| idx = path.lastIndexOf('?'); |
| if (idx != -1) { |
| query = path.substring(idx + 1); |
| path = path.substring(0, idx); |
| } |
| } |
| uri = new URI(w.mScheme, w.mAuthInfo, w.mHost, w.mPort, path, |
| query, frag); |
| } catch (Exception e) { |
| Log.e(LOGTAG, "Could not parse url for download: " + url, e); |
| return; |
| } |
| |
| // XXX: Have to use the old url since the cookies were stored using the |
| // old percent-encoded url. |
| String cookies = CookieManager.getInstance().getCookie(url); |
| |
| ContentValues values = new ContentValues(); |
| values.put(Downloads.COLUMN_URI, uri.toString()); |
| values.put(Downloads.COLUMN_COOKIE_DATA, cookies); |
| values.put(Downloads.COLUMN_USER_AGENT, userAgent); |
| values.put(Downloads.COLUMN_NOTIFICATION_PACKAGE, |
| getPackageName()); |
| values.put(Downloads.COLUMN_NOTIFICATION_CLASS, |
| BrowserDownloadPage.class.getCanonicalName()); |
| values.put(Downloads.COLUMN_VISIBILITY, Downloads.VISIBILITY_VISIBLE_NOTIFY_COMPLETED); |
| values.put(Downloads.COLUMN_MIME_TYPE, mimetype); |
| values.put(Downloads.COLUMN_FILE_NAME_HINT, filename); |
| values.put(Downloads.COLUMN_DESCRIPTION, uri.getHost()); |
| if (contentLength > 0) { |
| values.put(Downloads.COLUMN_TOTAL_BYTES, contentLength); |
| } |
| 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); |
| } |
| |
| } |
| |
| /** |
| * Resets the lock icon. This method is called when we start a new load and |
| * know the url to be loaded. |
| */ |
| private void resetLockIcon(String url) { |
| // Save the lock-icon state (we revert to it if the load gets cancelled) |
| saveLockIcon(); |
| |
| mLockIconType = LOCK_ICON_UNSECURE; |
| if (URLUtil.isHttpsUrl(url)) { |
| mLockIconType = LOCK_ICON_SECURE; |
| if (LOGV_ENABLED) { |
| Log.v(LOGTAG, "BrowserActivity.resetLockIcon:" + |
| " reset lock icon to " + mLockIconType); |
| } |
| } |
| |
| updateLockIconImage(LOCK_ICON_UNSECURE); |
| } |
| |
| /* package */ void setLockIconType(int type) { |
| mLockIconType = type; |
| } |
| |
| /* package */ int getLockIconType() { |
| return mLockIconType; |
| } |
| |
| /* package */ void setPrevLockType(int type) { |
| mPrevLockType = type; |
| } |
| |
| /* package */ int getPrevLockType() { |
| return mPrevLockType; |
| } |
| |
| /** |
| * Update the lock icon to correspond to our latest state. |
| */ |
| /* package */ void updateLockIconToLatest() { |
| updateLockIconImage(mLockIconType); |
| } |
| |
| /** |
| * Updates the lock-icon image in the title-bar. |
| */ |
| private void updateLockIconImage(int lockIconType) { |
| Drawable d = null; |
| if (lockIconType == LOCK_ICON_SECURE) { |
| d = mSecLockIcon; |
| } else if (lockIconType == LOCK_ICON_MIXED) { |
| d = mMixLockIcon; |
| } |
| mTitleBar.setLock(d); |
| if (mFakeTitleBar != null) { |
| mFakeTitleBar.setLock(d); |
| } |
| } |
| |
| /** |
| * Displays a page-info dialog. |
| * @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 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; |
| |
| if (view == null) { |
| url = tab.getUrl(); |
| title = tab.getTitle(); |
| } else if (view == mTabControl.getCurrentWebView()) { |
| // Use the cached title and url if this is the current WebView |
| url = mUrl; |
| title = mTitle; |
| } else { |
| url = view.getUrl(); |
| title = view.getTitle(); |
| } |
| |
| if (url == null) { |
| url = ""; |
| } |
| if (title == null) { |
| title = ""; |
| } |
| |
| ((TextView) pageInfoView.findViewById(R.id.address)).setText(url); |
| ((TextView) pageInfoView.findViewById(R.id.title)).setText(title); |
| |
| mPageInfoView = tab; |
| mPageInfoFromShowSSLCertificateOnError = new Boolean(fromShowSSLCertificateOnError); |
| |
| AlertDialog.Builder alertDialogBuilder = |
| new AlertDialog.Builder(this) |
| .setTitle(R.string.page_info).setIcon(android.R.drawable.ic_dialog_info) |
| .setView(pageInfoView) |
| .setPositiveButton( |
| R.string.ok, |
| new DialogInterface.OnClickListener() { |
| public void onClick(DialogInterface dialog, |
| int whichButton) { |
| mPageInfoDialog = null; |
| mPageInfoView = null; |
| mPageInfoFromShowSSLCertificateOnError = null; |
| |
| // if we came here from the SSL error dialog |
| if (fromShowSSLCertificateOnError) { |
| // go back to the SSL error dialog |
| showSSLCertificateOnError( |
| mSSLCertificateOnErrorView, |
| mSSLCertificateOnErrorHandler, |
| mSSLCertificateOnErrorError); |
| } |
| } |
| }) |
| .setOnCancelListener( |
| new DialogInterface.OnCancelListener() { |
| public void onCancel(DialogInterface dialog) { |
| mPageInfoDialog = null; |
| mPageInfoView = null; |
| mPageInfoFromShowSSLCertificateOnError = null; |
| |
| // if we came here from the SSL error dialog |
| if (fromShowSSLCertificateOnError) { |
| // go back to the SSL error dialog |
| showSSLCertificateOnError( |
| mSSLCertificateOnErrorView, |
| mSSLCertificateOnErrorHandler, |
| mSSLCertificateOnErrorError); |
| } |
| } |
| }); |
| |
| // if we have a main top-level page SSL certificate set or a certificate |
| // error |
| if (fromShowSSLCertificateOnError || |
| (view != null && view.getCertificate() != null)) { |
| // add a 'View Certificate' button |
| alertDialogBuilder.setNeutralButton( |
| R.string.view_certificate, |
| new DialogInterface.OnClickListener() { |
| public void onClick(DialogInterface dialog, |
| int whichButton) { |
| mPageInfoDialog = null; |
| mPageInfoView = null; |
| mPageInfoFromShowSSLCertificateOnError = null; |
| |
| // if we came here from the SSL error dialog |
| if (fromShowSSLCertificateOnError) { |
| // go back to the SSL error dialog |
| showSSLCertificateOnError( |
| mSSLCertificateOnErrorView, |
| mSSLCertificateOnErrorHandler, |
| mSSLCertificateOnErrorError); |
| } else { |
| // otherwise, display the top-most certificate from |
| // the chain |
| if (view.getCertificate() != null) { |
| showSSLCertificate(tab); |
| } |
| } |
| } |
| }); |
| } |
| |
| mPageInfoDialog = alertDialogBuilder.show(); |
| } |
| |
| /** |
| * Displays the main top-level page SSL certificate dialog |
| * (accessible from the Page-Info dialog). |
| * @param tab The tab to show certificate for. |
| */ |
| private void showSSLCertificate(final TabControl.Tab tab) { |
| final View certificateView = |
| inflateCertificateView(tab.getWebView().getCertificate()); |
| if (certificateView == null) { |
| return; |
| } |
| |
| LayoutInflater factory = LayoutInflater.from(this); |
| |
| final LinearLayout placeholder = |
| (LinearLayout)certificateView.findViewById(R.id.placeholder); |
| |
| LinearLayout ll = (LinearLayout) factory.inflate( |
| R.layout.ssl_success, placeholder); |
| ((TextView)ll.findViewById(R.id.success)) |
| .setText(R.string.ssl_certificate_is_valid); |
| |
| mSSLCertificateView = tab; |
| mSSLCertificateDialog = |
| new AlertDialog.Builder(this) |
| .setTitle(R.string.ssl_certificate).setIcon( |
| R.drawable.ic_dialog_browser_certificate_secure) |
| .setView(certificateView) |
| .setPositiveButton(R.string.ok, |
| new DialogInterface.OnClickListener() { |
| public void onClick(DialogInterface dialog, |
| int whichButton) { |
| mSSLCertificateDialog = null; |
| mSSLCertificateView = null; |
| |
| showPageInfo(tab, false); |
| } |
| }) |
| .setOnCancelListener( |
| new DialogInterface.OnCancelListener() { |
| public void onCancel(DialogInterface dialog) { |
| mSSLCertificateDialog = null; |
| mSSLCertificateView = null; |
| |
| showPageInfo(tab, false); |
| } |
| }) |
| .show(); |
| } |
| |
| /** |
| * Displays the SSL error certificate dialog. |
| * @param view The target web-view. |
| * @param handler The SSL error handler responsible for cancelling the |
| * connection that resulted in an SSL error or proceeding per user request. |
| * @param error The SSL error object. |
| */ |
| private void showSSLCertificateOnError( |
| final WebView view, final SslErrorHandler handler, final SslError error) { |
| |
| final View certificateView = |
| inflateCertificateView(error.getCertificate()); |
| if (certificateView == null) { |
| return; |
| } |
| |
| LayoutInflater factory = LayoutInflater.from(this); |
| |
| final LinearLayout placeholder = |
| (LinearLayout)certificateView.findViewById(R.id.placeholder); |
| |
| if (error.hasError(SslError.SSL_UNTRUSTED)) { |
| LinearLayout ll = (LinearLayout)factory |
| .inflate(R.layout.ssl_warning, placeholder); |
| ((TextView)ll.findViewById(R.id.warning)) |
| .setText(R.string.ssl_untrusted); |
| } |
| |
| if (error.hasError(SslError.SSL_IDMISMATCH)) { |
| LinearLayout ll = (LinearLayout)factory |
| .inflate(R.layout.ssl_warning, placeholder); |
| ((TextView)ll.findViewById(R.id.warning)) |
| .setText(R.string.ssl_mismatch); |
| } |
| |
| if (error.hasError(SslError.SSL_EXPIRED)) { |
| LinearLayout ll = (LinearLayout)factory |
| .inflate(R.layout.ssl_warning, placeholder); |
| ((TextView)ll.findViewById(R.id.warning)) |
| .setText(R.string.ssl_expired); |
| } |
| |
| if (error.hasError(SslError.SSL_NOTYETVALID)) { |
| LinearLayout ll = (LinearLayout)factory |
| .inflate(R.layout.ssl_warning, placeholder); |
| ((TextView)ll.findViewById(R.id.warning)) |
| .setText(R.string.ssl_not_yet_valid); |
| } |
| |
| mSSLCertificateOnErrorHandler = handler; |
| mSSLCertificateOnErrorView = view; |
| mSSLCertificateOnErrorError = error; |
| mSSLCertificateOnErrorDialog = |
| new AlertDialog.Builder(this) |
| .setTitle(R.string.ssl_certificate).setIcon( |
| R.drawable.ic_dialog_browser_certificate_partially_secure) |
| .setView(certificateView) |
| .setPositiveButton(R.string.ok, |
| new DialogInterface.OnClickListener() { |
| public void onClick(DialogInterface dialog, |
| int whichButton) { |
| mSSLCertificateOnErrorDialog = null; |
| mSSLCertificateOnErrorView = null; |
| mSSLCertificateOnErrorHandler = null; |
| mSSLCertificateOnErrorError = null; |
| |
| mWebViewClient.onReceivedSslError( |
| view, handler, error); |
| } |
| }) |
| .setNeutralButton(R.string.page_info_view, |
| new DialogInterface.OnClickListener() { |
| public void onClick(DialogInterface dialog, |
| int whichButton) { |
| mSSLCertificateOnErrorDialog = null; |
| |
| // do not clear the dialog state: we will |
| // need to show the dialog again once the |
| // user is done exploring the page-info details |
| |
| showPageInfo(mTabControl.getTabFromView(view), |
| true); |
| } |
| }) |
| .setOnCancelListener( |
| new DialogInterface.OnCancelListener() { |
| public void onCancel(DialogInterface dialog) { |
| mSSLCertificateOnErrorDialog = null; |
| mSSLCertificateOnErrorView = null; |
| mSSLCertificateOnErrorHandler = null; |
| mSSLCertificateOnErrorError = null; |
| |
| mWebViewClient.onReceivedSslError( |
| view, handler, error); |
| } |
| }) |
| .show(); |
| } |
| |
| /** |
| * Inflates the SSL certificate view (helper method). |
| * @param certificate The SSL certificate. |
| * @return The resultant certificate view with issued-to, issued-by, |
| * issued-on, expires-on, and possibly other fields set. |
| * If the input certificate is null, returns null. |
| */ |
| private View inflateCertificateView(SslCertificate certificate) { |
| if (certificate == null) { |
| return null; |
| } |
| |
| LayoutInflater factory = LayoutInflater.from(this); |
| |
| View certificateView = factory.inflate( |
| R.layout.ssl_certificate, null); |
| |
| // issued to: |
| SslCertificate.DName issuedTo = certificate.getIssuedTo(); |
| if (issuedTo != null) { |
| ((TextView) certificateView.findViewById(R.id.to_common)) |
| .setText(issuedTo.getCName()); |
| ((TextView) certificateView.findViewById(R.id.to_org)) |
| .setText(issuedTo.getOName()); |
| ((TextView) certificateView.findViewById(R.id.to_org_unit)) |
| .setText(issuedTo.getUName()); |
| } |
| |
| // issued by: |
| SslCertificate.DName issuedBy = certificate.getIssuedBy(); |
| if (issuedBy != null) { |
| ((TextView) certificateView.findViewById(R.id.by_common)) |
| .setText(issuedBy.getCName()); |
| ((TextView) certificateView.findViewById(R.id.by_org)) |
| .setText(issuedBy.getOName()); |
| ((TextView) certificateView.findViewById(R.id.by_org_unit)) |
| .setText(issuedBy.getUName()); |
| } |
| |
| // issued on: |
| String issuedOn = reformatCertificateDate( |
| certificate.getValidNotBefore()); |
| ((TextView) certificateView.findViewById(R.id.issued_on)) |
| .setText(issuedOn); |
| |
| // expires on: |
| String expiresOn = reformatCertificateDate( |
| certificate.getValidNotAfter()); |
| ((TextView) certificateView.findViewById(R.id.expires_on)) |
| .setText(expiresOn); |
| |
| return certificateView; |
| } |
| |
| /** |
| * Re-formats the certificate date (Date.toString()) string to |
| * a properly localized date string. |
| * @return Properly localized version of the certificate date string and |
| * the original certificate date string if fails to localize. |
| * If the original string is null, returns an empty string "". |
| */ |
| private String reformatCertificateDate(String certificateDate) { |
| String reformattedDate = null; |
| |
| if (certificateDate != null) { |
| Date date = null; |
| try { |
| date = java.text.DateFormat.getInstance().parse(certificateDate); |
| } catch (ParseException e) { |
| date = null; |
| } |
| |
| if (date != null) { |
| reformattedDate = |
| DateFormat.getDateFormat(this).format(date); |
| } |
| } |
| |
| return reformattedDate != null ? reformattedDate : |
| (certificateDate != null ? certificateDate : ""); |
| } |
| |
| /** |
| * Displays an http-authentication dialog. |
| */ |
| private void showHttpAuthentication(final HttpAuthHandler handler, |
| final String host, final String realm, final String title, |
| final String name, final String password, int focusId) { |
| LayoutInflater factory = LayoutInflater.from(this); |
| final View v = factory |
| .inflate(R.layout.http_authentication, null); |
| if (name != null) { |
| ((EditText) v.findViewById(R.id.username_edit)).setText(name); |
| } |
| if (password != null) { |
| ((EditText) v.findViewById(R.id.password_edit)).setText(password); |
| } |
| |
| String titleText = title; |
| if (titleText == null) { |
| titleText = getText(R.string.sign_in_to).toString().replace( |
| "%s1", host).replace("%s2", realm); |
| } |
| |
| mHttpAuthHandler = handler; |
| AlertDialog dialog = new AlertDialog.Builder(this) |
| .setTitle(titleText) |
| .setIcon(android.R.drawable.ic_dialog_alert) |
| .setView(v) |
| .setPositiveButton(R.string.action, |
| new DialogInterface.OnClickListener() { |
| public void onClick(DialogInterface dialog, |
| int whichButton) { |
| String nm = ((EditText) v |
| .findViewById(R.id.username_edit)) |
| .getText().toString(); |
| String pw = ((EditText) v |
| .findViewById(R.id.password_edit)) |
| .getText().toString(); |
| BrowserActivity.this.setHttpAuthUsernamePassword |
| (host, realm, nm, pw); |
| handler.proceed(nm, pw); |
| mHttpAuthenticationDialog = null; |
| mHttpAuthHandler = null; |
| }}) |
| .setNegativeButton(R.string.cancel, |
| new DialogInterface.OnClickListener() { |
| public void onClick(DialogInterface dialog, |
| int whichButton) { |
| handler.cancel(); |
| BrowserActivity.this.resetTitleAndRevertLockIcon(); |
| mHttpAuthenticationDialog = null; |
| mHttpAuthHandler = null; |
| }}) |
| .setOnCancelListener(new DialogInterface.OnCancelListener() { |
| public void onCancel(DialogInterface dialog) { |
| handler.cancel(); |
| BrowserActivity.this.resetTitleAndRevertLockIcon(); |
| mHttpAuthenticationDialog = null; |
| mHttpAuthHandler = null; |
| }}) |
| .create(); |
| // Make the IME appear when the dialog is displayed if applicable. |
| dialog.getWindow().setSoftInputMode( |
| WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE); |
| dialog.show(); |
| if (focusId != 0) { |
| dialog.findViewById(focusId).requestFocus(); |
| } else { |
| v.findViewById(R.id.username_edit).requestFocus(); |
| } |
| mHttpAuthenticationDialog = dialog; |
| } |
| |
| public int getProgress() { |
| WebView w = mTabControl.getCurrentWebView(); |
| if (w != null) { |
| return w.getProgress(); |
| } else { |
| return 100; |
| } |
| } |
| |
| /** |
| * Set HTTP authentication password. |
| * |
| * @param host The host for the password |
| * @param realm The realm for the password |
| * @param username The username for the password. If it is null, it means |
| * password can't be saved. |
| * @param password The password |
| */ |
| public void setHttpAuthUsernamePassword(String host, String realm, |
| String username, |
| String password) { |
| WebView w = mTabControl.getCurrentWebView(); |
| if (w != null) { |
| w.setHttpAuthUsernamePassword(host, realm, username, password); |
| } |
| } |
| |
| /** |
| * connectivity manager says net has come or gone... inform the user |
| * @param up true if net has come up, false if net has gone down |
| */ |
| public void onNetworkToggle(boolean up) { |
| if (up == mIsNetworkUp) { |
| return; |
| } else if (up) { |
| mIsNetworkUp = true; |
| if (mAlertDialog != null) { |
| mAlertDialog.cancel(); |
| mAlertDialog = null; |
| } |
| } else { |
| mIsNetworkUp = false; |
| if (mInLoad) { |
| createAndShowNetworkDialog(); |
| } |
| } |
| WebView w = mTabControl.getCurrentWebView(); |
| if (w != null) { |
| w.setNetworkAvailable(up); |
| } |
| } |
| |
| // This method shows the network dialog alerting the user that the net is |
| // down. It will only show the dialog if mAlertDialog is null. |
| private void createAndShowNetworkDialog() { |
| if (mAlertDialog == null) { |
| mAlertDialog = new AlertDialog.Builder(this) |
| .setTitle(R.string.loadSuspendedTitle) |
| .setMessage(R.string.loadSuspended) |
| .setPositiveButton(R.string.ok, null) |
| .show(); |
| } |
| } |
| |
| @Override |
| protected void onActivityResult(int requestCode, int resultCode, |
| Intent intent) { |
| switch (requestCode) { |
| case COMBO_PAGE: |
| if (resultCode == RESULT_OK && intent != null) { |
| String data = intent.getAction(); |
| Bundle extras = intent.getExtras(); |
| if (extras != null && extras.getBoolean("new_window", false)) { |
| openTab(data); |
| } else { |
| final TabControl.Tab currentTab = |
| mTabControl.getCurrentTab(); |
| dismissSubWindow(currentTab); |
| if (data != null && data.length() != 0) { |
| getTopWindow().loadUrl(data); |
| } |
| } |
| } |
| break; |
| default: |
| break; |
| } |
| getTopWindow().requestFocus(); |
| } |
| |
| /* |
| * This method is called as a result of the user selecting the options |
| * menu to see the download window, or when a download changes state. It |
| * shows the download window ontop of the current window. |
| */ |
| /* package */ void viewDownloads(Uri downloadRecord) { |
| Intent intent = new Intent(this, |
| BrowserDownloadPage.class); |
| intent.setData(downloadRecord); |
| startActivityForResult(intent, this.DOWNLOAD_PAGE); |
| |
| } |
| |
| /** |
| * Open the Go page. |
| * @param startWithHistory If true, open starting on the history tab. |
| * Otherwise, start with the bookmarks tab. |
| */ |
| /* package */ void bookmarksOrHistoryPicker(boolean startWithHistory) { |
| WebView current = mTabControl.getCurrentWebView(); |
| if (current == null) { |
| return; |
| } |
| Intent intent = new Intent(this, |
| CombinedBookmarkHistoryActivity.class); |
| String title = current.getTitle(); |
| String url = current.getUrl(); |
| Bitmap thumbnail = createScreenshot(current); |
| |
| // Just in case the user opens bookmarks before a page finishes loading |
| // so the current history item, and therefore the page, is null. |
| if (null == url) { |
| url = mLastEnteredUrl; |
| // This can happen. |
| if (null == url) { |
| url = mSettings.getHomePage(); |
| } |
| } |
| // In case the web page has not yet received its associated title. |
| if (title == null) { |
| title = url; |
| } |
| intent.putExtra("title", title); |
| intent.putExtra("url", url); |
| intent.putExtra("thumbnail", thumbnail); |
| // Disable opening in a new window if we have maxed out the windows |
| intent.putExtra("disable_new_window", mTabControl.getTabCount() |
| >= TabControl.MAX_TABS); |
| intent.putExtra("touch_icon_url", current.getTouchIconUrl()); |
| if (startWithHistory) { |
| intent.putExtra(CombinedBookmarkHistoryActivity.STARTING_TAB, |
| CombinedBookmarkHistoryActivity.HISTORY_TAB); |
| } |
| startActivityForResult(intent, COMBO_PAGE); |
| } |
| |
| // 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 && view != null) { |
| url = smartUrlFilter(url); |
| if (!mWebViewClient.shouldOverrideUrlLoading(view, url)) { |
| view.loadUrl(url); |
| } |
| } |
| } |
| |
| private String smartUrlFilter(Uri inUri) { |
| if (inUri != null) { |
| return smartUrlFilter(inUri.toString()); |
| } |
| return null; |
| } |
| |
| |
| // get window count |
| |
| int getWindowCount(){ |
| if(mTabControl != null){ |
| return mTabControl.getTabCount(); |
| } |
| return 0; |
| } |
| |
| protected static final Pattern ACCEPTED_URI_SCHEMA = Pattern.compile( |
| "(?i)" + // switch on case insensitive matching |
| "(" + // begin group for schema |
| "(?:http|https|file):\\/\\/" + |
| "|(?:inline|data|about|content|javascript):" + |
| ")" + |
| "(.*)" ); |
| |
| /** |
| * Attempts to determine whether user input is a URL or search |
| * terms. Anything with a space is passed to search. |
| * |
| * Converts to lowercase any mistakenly uppercased schema (i.e., |
| * "Http://" converts to "http://" |
| * |
| * @return Original or modified URL |
| * |
| */ |
| String smartUrlFilter(String url) { |
| |
| String inUrl = url.trim(); |
| boolean hasSpace = inUrl.indexOf(' ') != -1; |
| |
| 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)) { |
| inUrl = lcScheme + matcher.group(2); |
| } |
| if (hasSpace) { |
| inUrl = inUrl.replace(" ", "%20"); |
| } |
| return inUrl; |
| } |
| if (hasSpace) { |
| // FIXME: Is this the correct place to add to searches? |
| // what if someone else calls this function? |
| int shortcut = parseUrlShortcut(inUrl); |
| if (shortcut != SHORTCUT_INVALID) { |
| Browser.addSearchUrl(mResolver, inUrl); |
| String query = inUrl.substring(2); |
| switch (shortcut) { |
| case SHORTCUT_GOOGLE_SEARCH: |
| return URLUtil.composeSearchUrl(query, QuickSearch_G, QUERY_PLACE_HOLDER); |
| case SHORTCUT_WIKIPEDIA_SEARCH: |
| return URLUtil.composeSearchUrl(query, QuickSearch_W, QUERY_PLACE_HOLDER); |
| case SHORTCUT_DICTIONARY_SEARCH: |
| return URLUtil.composeSearchUrl(query, QuickSearch_D, QUERY_PLACE_HOLDER); |
| case SHORTCUT_GOOGLE_MOBILE_LOCAL_SEARCH: |
| // FIXME: we need location in this case |
| return URLUtil.composeSearchUrl(query, QuickSearch_L, QUERY_PLACE_HOLDER); |
| } |
| } |
| } else { |
| if (Regex.WEB_URL_PATTERN.matcher(inUrl).matches()) { |
| return URLUtil.guessUrl(inUrl); |
| } |
| } |
| |
| Browser.addSearchUrl(mResolver, inUrl); |
| return URLUtil.composeSearchUrl(inUrl, QuickSearch_G, QUERY_PLACE_HOLDER); |
| } |
| |
| /* package */ void setShouldShowErrorConsole(boolean flag) { |
| if (flag == mShouldShowErrorConsole) { |
| // Nothing to do. |
| return; |
| } |
| |
| mShouldShowErrorConsole = flag; |
| |
| ErrorConsoleView errorConsole = mTabControl.getCurrentErrorConsole(true); |
| |
| if (flag) { |
| // Setting the show state of the console will cause it's the layout to be inflated. |
| if (errorConsole.numberOfErrors() > 0) { |
| errorConsole.showConsole(ErrorConsoleView.SHOW_MINIMIZED); |
| } else { |
| errorConsole.showConsole(ErrorConsoleView.SHOW_NONE); |
| } |
| |
| // Now we can add it to the main view. |
| mErrorConsoleContainer.addView(errorConsole, |
| new LinearLayout.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT, |
| ViewGroup.LayoutParams.WRAP_CONTENT)); |
| } else { |
| mErrorConsoleContainer.removeView(errorConsole); |
| } |
| |
| } |
| |
| final static int LOCK_ICON_UNSECURE = 0; |
| final static int LOCK_ICON_SECURE = 1; |
| final static int LOCK_ICON_MIXED = 2; |
| |
| private int mLockIconType = LOCK_ICON_UNSECURE; |
| private int mPrevLockType = LOCK_ICON_UNSECURE; |
| |
| private BrowserSettings mSettings; |
| private TabControl mTabControl; |
| private ContentResolver mResolver; |
| private FrameLayout mContentView; |
| private View mCustomView; |
| private FrameLayout mCustomViewContainer; |
| private WebChromeClient.CustomViewCallback mCustomViewCallback; |
| |
| // FIXME, temp address onPrepareMenu performance problem. When we move everything out of |
| // view, we should rewrite this. |
| private int mCurrentMenuState = 0; |
| private int mMenuState = R.id.MAIN_MENU; |
| private int mOldMenuState = EMPTY_MENU; |
| private static final int EMPTY_MENU = -1; |
| private Menu mMenu; |
| |
| private FindDialog mFindDialog; |
| // Used to prevent chording to result in firing two shortcuts immediately |
| // one after another. Fixes bug 1211714. |
| boolean mCanChord; |
| |
| private boolean mInLoad; |
| private boolean mIsNetworkUp; |
| private boolean mDidStopLoad; |
| |
| private boolean mPageStarted; |
| private boolean mActivityInPause = true; |
| |
| private boolean mMenuIsDown; |
| |
| private static boolean mInTrace; |
| |
| // Performance probe |
| private static final int[] SYSTEM_CPU_FORMAT = new int[] { |
| Process.PROC_SPACE_TERM | Process.PROC_COMBINE, |
| Process.PROC_SPACE_TERM | Process.PROC_OUT_LONG, // 1: user time |
| Process.PROC_SPACE_TERM | Process.PROC_OUT_LONG, // 2: nice time |
| Process.PROC_SPACE_TERM | Process.PROC_OUT_LONG, // 3: sys time |
| Process.PROC_SPACE_TERM | Process.PROC_OUT_LONG, // 4: idle time |
| Process.PROC_SPACE_TERM | Process.PROC_OUT_LONG, // 5: iowait time |
| Process.PROC_SPACE_TERM | Process.PROC_OUT_LONG, // 6: irq time |
| Process.PROC_SPACE_TERM | Process.PROC_OUT_LONG // 7: softirq time |
| }; |
| |
| private long mStart; |
| private long mProcessStart; |
| private long mUserStart; |
| private long mSystemStart; |
| private long mIdleStart; |
| private long mIrqStart; |
| |
| private long mUiStart; |
| |
| private Drawable mMixLockIcon; |
| private Drawable mSecLockIcon; |
| |
| /* hold a ref so we can auto-cancel if necessary */ |
| private AlertDialog mAlertDialog; |
| |
| // Wait for credentials before loading google.com |
| private ProgressDialog mCredsDlg; |
| |
| // The up-to-date URL and title (these can be different from those stored |
| // in WebView, since it takes some time for the information in WebView to |
| // get updated) |
| private String mUrl; |
| private String mTitle; |
| |
| // As PageInfo has different style for landscape / portrait, we have |
| // to re-open it when configuration changed |
| private AlertDialog mPageInfoDialog; |
| 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 |
| private Boolean mPageInfoFromShowSSLCertificateOnError; |
| |
| // as SSLCertificateOnError has different style for landscape / portrait, |
| // we have to re-open it when configuration changed |
| private AlertDialog mSSLCertificateOnErrorDialog; |
| private WebView mSSLCertificateOnErrorView; |
| private SslErrorHandler mSSLCertificateOnErrorHandler; |
| private SslError mSSLCertificateOnErrorError; |
| |
| // as SSLCertificate has different style for landscape / portrait, we |
| // have to re-open it when configuration changed |
| private AlertDialog mSSLCertificateDialog; |
| private TabControl.Tab mSSLCertificateView; |
| |
| // as HttpAuthentication has different style for landscape / portrait, we |
| // have to re-open it when configuration changed |
| private AlertDialog mHttpAuthenticationDialog; |
| private HttpAuthHandler mHttpAuthHandler; |
| |
| /*package*/ static final FrameLayout.LayoutParams COVER_SCREEN_PARAMS = |
| new FrameLayout.LayoutParams( |
| ViewGroup.LayoutParams.FILL_PARENT, |
| ViewGroup.LayoutParams.FILL_PARENT); |
| /*package*/ static final FrameLayout.LayoutParams COVER_SCREEN_GRAVITY_CENTER = |
| new FrameLayout.LayoutParams( |
| ViewGroup.LayoutParams.FILL_PARENT, |
| ViewGroup.LayoutParams.FILL_PARENT, |
| Gravity.CENTER); |
| // Google search |
| final static String QuickSearch_G = "http://www.google.com/m?q=%s"; |
| // Wikipedia search |
| final static String QuickSearch_W = "http://en.wikipedia.org/w/index.php?search=%s&go=Go"; |
| // Dictionary search |
| final static String QuickSearch_D = "http://dictionary.reference.com/search?q=%s"; |
| // Google Mobile Local search |
| final static String QuickSearch_L = "http://www.google.com/m/search?site=local&q=%s&near=mountain+view"; |
| |
| 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 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"; |
| |
| private String mLastEnteredUrl; |
| |
| private PowerManager.WakeLock mWakeLock; |
| private final static int WAKELOCK_TIMEOUT = 5 * 60 * 1000; // 5 minutes |
| |
| private Toast mStopToast; |
| |
| private TitleBar mTitleBar; |
| |
| private LinearLayout mErrorConsoleContainer = null; |
| private boolean mShouldShowErrorConsole = false; |
| |
| // 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; |
| private BroadcastReceiver mNetworkStateIntentReceiver; |
| |
| private BroadcastReceiver mPackageInstallationReceiver; |
| |
| // AsyncTask for downloading touch icons |
| /* package */ DownloadTouchIcon mTouchIconLoader; |
| |
| // activity requestCode |
| final static int COMBO_PAGE = 1; |
| final static int DOWNLOAD_PAGE = 2; |
| final static int PREFERENCES_PAGE = 3; |
| |
| // the default <video> poster |
| private Bitmap mDefaultVideoPoster; |
| // the video progress view |
| private View mVideoProgressView; |
| |
| /** |
| * A UrlData class to abstract how the content will be set to WebView. |
| * This base class uses loadUrl to show the content. |
| */ |
| private static class UrlData { |
| String mUrl; |
| byte[] mPostData; |
| |
| UrlData(String url) { |
| this.mUrl = url; |
| } |
| |
| void setPostData(byte[] postData) { |
| mPostData = postData; |
| } |
| |
| boolean isEmpty() { |
| return mUrl == null || mUrl.length() == 0; |
| } |
| |
| public void loadIn(WebView webView) { |
| if (mPostData != null) { |
| webView.postUrl(mUrl, mPostData); |
| } else { |
| webView.loadUrl(mUrl); |
| } |
| } |
| }; |
| |
| /** |
| * A subclass of UrlData class that can display inlined content using |
| * {@link WebView#loadDataWithBaseURL(String, String, String, String, String)}. |
| */ |
| private static class InlinedUrlData extends UrlData { |
| InlinedUrlData(String inlined, String mimeType, String encoding, String failUrl) { |
| super(failUrl); |
| mInlined = inlined; |
| mMimeType = mimeType; |
| mEncoding = encoding; |
| } |
| String mMimeType; |
| String mInlined; |
| String mEncoding; |
| @Override |
| boolean isEmpty() { |
| return mInlined == null || mInlined.length() == 0 || super.isEmpty(); |
| } |
| |
| @Override |
| public void loadIn(WebView webView) { |
| webView.loadDataWithBaseURL(null, mInlined, mMimeType, mEncoding, mUrl); |
| } |
| } |
| |
| /* package */ static final UrlData EMPTY_URL_DATA = new UrlData(null); |
| } |