/* * 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 com.google.android.providers.GoogleSettings.Partner; import android.app.Activity; import android.app.ActivityManager; 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.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.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.Canvas; import android.graphics.Color; import android.graphics.DrawFilter; import android.graphics.Paint; import android.graphics.PaintFlagsDrawFilter; import android.graphics.Picture; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.LayerDrawable; import android.graphics.drawable.PaintDrawable; import android.hardware.SensorListener; import android.hardware.SensorManager; import android.net.ConnectivityManager; 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.os.SystemProperties; import android.preference.PreferenceManager; import android.provider.Browser; import android.provider.Contacts; import android.provider.Downloads; import android.provider.MediaStore; import android.provider.Contacts.Intents.Insert; import android.text.IClipboard; import android.text.TextUtils; import android.text.format.DateFormat; import android.text.util.Regex; import android.util.Config; import android.util.Log; 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.HttpAuthHandler; import android.webkit.SslErrorHandler; import android.webkit.URLUtil; import android.webkit.WebChromeClient; import android.webkit.WebHistoryItem; import android.webkit.WebIconDatabase; 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.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.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.Locale; 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 KeyTracker.OnKeyTracker, View.OnCreateContextMenuListener, DownloadListener { private IGoogleLoginService mGls = null; private ServiceConnection mGlsConnection = null; private SensorManager mSensorManager = null; /* Whitelisted webpages private static HashSet sWhiteList; static { sWhiteList = new HashSet(); 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; 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 = "http://www.google.com/m/a/" + domain + "?client=ms-" + Partner.getString(BrowserActivity.this.getContentResolver(), Partner.CLIENT_ID); } } 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); } /** * This class is in charge of installing pre-packaged plugins * from the Browser assets directory to the user's data partition. * Plugins are loaded from the "plugins" directory in the assets; * Anything that is in this directory will be copied over to the * user data partition in app_plugins. */ private class CopyPlugins implements Runnable { final static String TAG = "PluginsInstaller"; final static String ZIP_FILTER = "assets/plugins/"; final static String APK_PATH = "/system/app/Browser.apk"; final static String PLUGIN_EXTENSION = ".so"; final static String TEMPORARY_EXTENSION = "_temp"; final static String BUILD_INFOS_FILE = "build.prop"; final static String SYSTEM_BUILD_INFOS_FILE = "/system/" + BUILD_INFOS_FILE; final int BUFSIZE = 4096; boolean mDoOverwrite = false; String pluginsPath; Context mContext; File pluginsDir; AssetManager manager; public CopyPlugins (boolean overwrite, Context context) { mDoOverwrite = overwrite; mContext = context; } /** * Returned a filtered list of ZipEntry. * We list all the files contained in the zip and * only returns the ones starting with the ZIP_FILTER * path. * * @param zip the zip file used. */ public Vector pluginsFilesFromZip(ZipFile zip) { Vector list = new Vector(); Enumeration entries = zip.entries(); while (entries.hasMoreElements()) { ZipEntry entry = (ZipEntry) entries.nextElement(); if (entry.getName().startsWith(ZIP_FILTER)) { list.add(entry); } } return list; } /** * Utility method to copy the content from an inputstream * to a file output stream. */ public void copyStreams(InputStream is, FileOutputStream fos) { BufferedOutputStream os = null; try { byte data[] = new byte[BUFSIZE]; int count; os = new BufferedOutputStream(fos, BUFSIZE); while ((count = is.read(data, 0, BUFSIZE)) != -1) { os.write(data, 0, count); } os.flush(); } catch (IOException e) { Log.e(TAG, "Exception while copying: " + e); } finally { try { if (os != null) { os.close(); } } catch (IOException e2) { Log.e(TAG, "Exception while closing the stream: " + e2); } } } /** * Returns a string containing the contents of a file * * @param file the target file */ private String contentsOfFile(File file) { String ret = null; FileInputStream is = null; try { byte[] buffer = new byte[BUFSIZE]; int count; is = new FileInputStream(file); StringBuffer out = new StringBuffer(); while ((count = is.read(buffer, 0, BUFSIZE)) != -1) { out.append(new String(buffer, 0, count)); } ret = out.toString(); } catch (IOException e) { Log.e(TAG, "Exception getting contents of file " + e); } finally { if (is != null) { try { is.close(); } catch (IOException e2) { Log.e(TAG, "Exception while closing the file: " + e2); } } } return ret; } /** * Utility method to initialize the user data plugins path. */ public void initPluginsPath() { BrowserSettings s = BrowserSettings.getInstance(); pluginsPath = s.getPluginsPath(); if (pluginsPath == null) { s.loadFromDb(mContext); pluginsPath = s.getPluginsPath(); } if (Config.LOGV) { Log.v(TAG, "Plugin path: " + pluginsPath); } } /** * Utility method to delete a file or a directory * * @param file the File to delete */ public void deleteFile(File file) { File[] files = file.listFiles(); if ((files != null) && files.length > 0) { for (int i=0; i< files.length; i++) { deleteFile(files[i]); } } if (!file.delete()) { Log.e(TAG, file.getPath() + " could not get deleted"); } } /** * Clean the content of the plugins directory. * We delete the directory, then recreate it. */ public void cleanPluginsDirectory() { if (Config.LOGV) { Log.v(TAG, "delete plugins directory: " + pluginsPath); } File pluginsDirectory = new File(pluginsPath); deleteFile(pluginsDirectory); pluginsDirectory.mkdir(); } /** * Copy the SYSTEM_BUILD_INFOS_FILE file containing the * informations about the system build to the * BUILD_INFOS_FILE in the plugins directory. */ public void copyBuildInfos() { try { if (Config.LOGV) { Log.v(TAG, "Copy build infos to the plugins directory"); } File buildInfoFile = new File(SYSTEM_BUILD_INFOS_FILE); File buildInfoPlugins = new File(pluginsPath, BUILD_INFOS_FILE); copyStreams(new FileInputStream(buildInfoFile), new FileOutputStream(buildInfoPlugins)); } catch (IOException e) { Log.e(TAG, "Exception while copying the build infos: " + e); } } /** * Returns true if the current system is newer than the * system that installed the plugins. * We determinate this by checking the build number of the system. * * At the end of the plugins copy operation, we copy the * SYSTEM_BUILD_INFOS_FILE to the BUILD_INFOS_FILE. * We then just have to load both and compare them -- if they * are different the current system is newer. * * Loading and comparing the strings should be faster than * creating a hash, the files being rather small. Extracting the * version number would require some parsing which may be more * brittle. */ public boolean newSystemImage() { try { File buildInfoFile = new File(SYSTEM_BUILD_INFOS_FILE); File buildInfoPlugins = new File(pluginsPath, BUILD_INFOS_FILE); if (!buildInfoPlugins.exists()) { if (Config.LOGV) { Log.v(TAG, "build.prop in plugins directory " + pluginsPath + " does not exist, therefore it's a new system image"); } return true; } else { String buildInfo = contentsOfFile(buildInfoFile); String buildInfoPlugin = contentsOfFile(buildInfoPlugins); if (buildInfo == null || buildInfoPlugin == null || buildInfo.compareTo(buildInfoPlugin) != 0) { if (Config.LOGV) { Log.v(TAG, "build.prop are different, " + " therefore it's a new system image"); } return true; } } } catch (Exception e) { Log.e(TAG, "Exc in newSystemImage(): " + e); } return false; } /** * Check if the version of the plugins contained in the * Browser assets is the same as the version of the plugins * in the plugins directory. * We simply iterate on every file in the assets/plugins * and return false if a file listed in the assets does * not exist in the plugins directory. */ private boolean checkIsDifferentVersions() { try { ZipFile zip = new ZipFile(APK_PATH); Vector files = pluginsFilesFromZip(zip); int zipFilterLength = ZIP_FILTER.length(); Enumeration entries = files.elements(); while (entries.hasMoreElements()) { ZipEntry entry = (ZipEntry) entries.nextElement(); String path = entry.getName().substring(zipFilterLength); File outputFile = new File(pluginsPath, path); if (!outputFile.exists()) { if (Config.LOGV) { Log.v(TAG, "checkIsDifferentVersions(): extracted file " + path + " does not exist, we have a different version"); } return true; } } } catch (IOException e) { Log.e(TAG, "Exception in checkDifferentVersions(): " + e); } return false; } /** * Copy every files from the assets/plugins directory * to the app_plugins directory in the data partition. * Once copied, we copy over the SYSTEM_BUILD_INFOS file * in the plugins directory. * * NOTE: we directly access the content from the Browser * package (it's a zip file) and do not use AssetManager * as there is a limit of 1Mb (see Asset.h) */ public void run() { // Lower the priority Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); try { if (pluginsPath == null) { Log.e(TAG, "No plugins path found!"); return; } ZipFile zip = new ZipFile(APK_PATH); Vector files = pluginsFilesFromZip(zip); Vector plugins = new Vector(); int zipFilterLength = ZIP_FILTER.length(); Enumeration entries = files.elements(); while (entries.hasMoreElements()) { ZipEntry entry = (ZipEntry) entries.nextElement(); String path = entry.getName().substring(zipFilterLength); File outputFile = new File(pluginsPath, path); outputFile.getParentFile().mkdirs(); if (outputFile.exists() && !mDoOverwrite) { if (Config.LOGV) { Log.v(TAG, path + " already extracted."); } } else { if (path.endsWith(PLUGIN_EXTENSION)) { // We rename plugins to be sure a half-copied // plugin is not loaded by the browser. plugins.add(outputFile); outputFile = new File(pluginsPath, path + TEMPORARY_EXTENSION); } FileOutputStream fos = new FileOutputStream(outputFile); if (Config.LOGV) { Log.v(TAG, "copy " + entry + " to " + pluginsPath + "/" + path); } copyStreams(zip.getInputStream(entry), fos); } } // We now rename the .so we copied, once all their resources // are safely copied over to the user data partition. Enumeration elems = plugins.elements(); while (elems.hasMoreElements()) { File renamedFile = (File) elems.nextElement(); File sourceFile = new File(renamedFile.getPath() + TEMPORARY_EXTENSION); if (Config.LOGV) { Log.v(TAG, "rename " + sourceFile.getPath() + " to " + renamedFile.getPath()); } sourceFile.renameTo(renamedFile); } copyBuildInfos(); // Refresh the plugin list. if (mTabControl.getCurrentWebView() != null) { mTabControl.getCurrentWebView().refreshPlugins(false); } } catch (IOException e) { Log.e(TAG, "IO Exception: " + e); } } }; /** * Copy the content of assets/plugins/ to the app_plugins directory * in the data partition. * * This function is called every time the browser is started. * We first check if the system image is newer than the one that * copied the plugins (if there's plugins in the data partition). * If this is the case, we then check if the versions are different. * If they are different, we clean the plugins directory in the * data partition, then start a thread to copy the plugins while * the browser continue to load. * * @param overwrite if true overwrite the files even if they are * already present (to let the user "reset" the plugins if needed). */ private void copyPlugins(boolean overwrite) { CopyPlugins copyPluginsFromAssets = new CopyPlugins(overwrite, this); copyPluginsFromAssets.initPluginsPath(); if (copyPluginsFromAssets.newSystemImage()) { if (copyPluginsFromAssets.checkIsDifferentVersions()) { copyPluginsFromAssets.cleanPluginsDirectory(); Thread copyplugins = new Thread(copyPluginsFromAssets); copyplugins.setName("CopyPlugins"); copyplugins.start(); } } } private class ClearThumbnails extends AsyncTask { @Override public Void doInBackground(File... files) { if (files != null) { for (File f : files) { f.delete(); } } return null; } } @Override public void onCreate(Bundle icicle) { if (Config.LOGV) { Log.v(LOGTAG, this + " onStart"); } super.onCreate(icicle); this.requestWindowFeature(Window.FEATURE_LEFT_ICON); this.requestWindowFeature(Window.FEATURE_RIGHT_ICON); this.requestWindowFeature(Window.FEATURE_PROGRESS); this.requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); // test the browser in OpenGL // requestWindowFeature(Window.FEATURE_OPENGL); setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL); mResolver = getContentResolver(); setBaseSearchUrl(PreferenceManager.getDefaultSharedPreferences(this) .getString("search_url", "")); // // 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); mGenericFavicon = getResources().getDrawable( R.drawable.app_web_browser_sm); mContentView = (FrameLayout) getWindow().getDecorView().findViewById( com.android.internal.R.id.content); // 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"); if (!mTabControl.restoreState(icicle)) { // clear up the thumbnail directory if we can't restore the state as // none of the files in the directory are referenced any more. new ClearThumbnails().execute( mTabControl.getThumbnailDir().listFiles()); final Intent intent = getIntent(); final Bundle extra = intent.getExtras(); // Create an initial tab. // 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. String url = getUrlFromIntent(intent); final TabControl.Tab t = mTabControl.createNewTab( Intent.ACTION_VIEW.equals(intent.getAction()) && intent.getData() != null, intent.getStringExtra(Browser.EXTRA_APPLICATION_ID), url); mTabControl.setCurrentTab(t); // This is one of the only places we call attachTabToContentView // without animating from the tab picker. 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(); } copyPlugins(true); if (url == null || url.length() == 0) { if (mSettings.isLoginInitialized()) { webView.loadUrl(mSettings.getHomePage()); } else { waitForCredentials(); } } else { webView.loadUrl(url); } } else { // TabControl.restoreState() will create a new tab even if // restoring the state fails. Attach it to the view here since we // are not animating from the tab picker. attachTabToContentView(mTabControl.getCurrentTab()); } /* 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 down = intent.getBooleanExtra( ConnectivityManager.EXTRA_NO_CONNECTIVITY, false); onNetworkToggle(!down); } } }; } @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)) { String url = getUrlFromIntent(intent); if (url == null || url.length() == 0) { url = mSettings.getHomePage(); } if (Intent.ACTION_VIEW.equals(action) && (flags & Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT) != 0) { final String appId = intent.getStringExtra(Browser.EXTRA_APPLICATION_ID); final 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, url); if (current != appTab) { showTab(appTab, needsLoad ? url : null); } else { if (mTabOverview != null && mAnimationCount == 0) { sendAnimateFromOverview(appTab, false, needsLoad ? url : null, TAB_OVERVIEW_DELAY, null); } else { // If the tab was the current tab, we have to attach // it to the view system again. attachTabToContentView(appTab); if (needsLoad) { appTab.getWebView().loadUrl(url); } } } return; } // 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(url, null, true, appId); } else { if ("about:debug".equals(url)) { mSettings.toggleDebugSettings(); return; } // If the Window overview is up and we are not in the midst of // an animation, animate away from the Window overview. if (mTabOverview != null && mAnimationCount == 0) { sendAnimateFromOverview(current, false, url, TAB_OVERVIEW_DELAY, null); } else { // Get rid of the subwindow if it exists dismissSubWindow(current); current.getWebView().loadUrl(url); } } } } private String getUrlFromIntent(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; } } } 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 url; } /* package */ static String fixUrl(String inUrl) { 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 (Config.LOGV) { Log.v(LOGTAG, "BrowserActivity.onResume: this=" + this); } if (!mActivityInPause) { Log.e(LOGTAG, "BrowserActivity is already resumed."); return; } mActivityInPause = false; resumeWebView(); 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; } } /** * onSaveInstanceState(Bundle map) * onSaveInstanceState is called right before onStop(). The map contains * the saved state. */ @Override protected void onSaveInstanceState(Bundle outState) { if (Config.LOGV) { 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; } mActivityInPause = true; if (mTabControl.getCurrentIndex() >= 0 && !pauseWebView()) { 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; cancelStopToast(); // unregister network state listener unregisterReceiver(mNetworkStateIntentReceiver); WebView.disablePlatformNotifications(); if (mSensorManager != null) { mSensorManager.unregisterListener(mSensorListener); } } @Override protected void onDestroy() { if (Config.LOGV) { Log.v(LOGTAG, "BrowserActivity.onDestroy: this=" + this); } super.onDestroy(); // 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); } @Override public void onConfigurationChanged(Configuration newConfig) { 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 resumeWebView() { if ((!mActivityInPause && !mPageStarted) || (mActivityInPause && mPageStarted)) { CookieSyncManager.getInstance().startSync(); WebView w = mTabControl.getCurrentWebView(); if (w != null) { w.resumeTimers(); } return true; } else { return false; } } private boolean pauseWebView() { if (mActivityInPause && !mPageStarted) { CookieSyncManager.getInstance().stopSync(); WebView w = mTabControl.getCurrentWebView(); if (w != null) { w.pauseTimers(); } return true; } else { return false; } } /* * 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(); final WebView webView = getTopWindow(); final HashMap hrefMap = new HashMap(); hrefMap.put("webview", webView); final Message msg = mHandler.obtainMessage( FOCUS_NODE_HREF, id, 0, hrefMap); switch (id) { // -- Browser context menu case R.id.open_context_menu_id: 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: 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() { startSearch(null, false, 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); } @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; } switch (item.getItemId()) { // -- Main menu case R.id.goto_menu_id: { String url = getTopWindow().getUrl(); startSearch(mSettings.getHomePage().equals(url) ? null : url, true, createGoogleSearchSourceBundle(GOOGLE_SEARCH_SOURCE_GOTO), false); } break; case R.id.bookmarks_menu_id: bookmarksOrHistoryPicker(false); break; case R.id.windows_menu_id: if (mTabControl.getTabCount() == 1) { openTabAndShow(mSettings.getHomePage(), null, false, null); } else { tabPicker(true, mTabControl.getCurrentIndex(), false); } 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; } final int currentIndex = mTabControl.getCurrentIndex(); final TabControl.Tab parent = mTabControl.getCurrentTab().getParentTab(); int indexToShow = -1; if (parent != null) { indexToShow = mTabControl.getTabIndex(parent); } else { // Get the last tab in the list. If it is the current tab, // subtract 1 more. indexToShow = mTabControl.getTabCount() - 1; if (currentIndex == indexToShow) { indexToShow--; } } switchTabs(currentIndex, indexToShow, true); 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()); 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; // -- Tab menu case R.id.view_tab_menu_id: if (mTabListener != null && mTabOverview != null) { int pos = mTabOverview.getContextMenuPosition(item); mTabOverview.setCurrentIndex(pos); mTabListener.onClick(pos); } break; case R.id.remove_tab_menu_id: if (mTabListener != null && mTabOverview != null) { int pos = mTabOverview.getContextMenuPosition(item); mTabListener.remove(pos); } break; case R.id.new_tab_menu_id: // No need to check for mTabOverview here since we are not // dependent on it for a position. if (mTabListener != null) { // If the overview happens to be non-null, make the "New // Tab" cell visible. if (mTabOverview != null) { mTabOverview.setCurrentIndex(ImageGrid.NEW_TAB); } mTabListener.onClick(ImageGrid.NEW_TAB); } break; case R.id.bookmark_tab_menu_id: if (mTabListener != null && mTabOverview != null) { int pos = mTabOverview.getContextMenuPosition(item); TabControl.Tab t = mTabControl.getTab(pos); // Since we called populatePickerData for all of the // tabs, getTitle and getUrl will return appropriate // values. Browser.saveBookmark(BrowserActivity.this, t.getTitle(), t.getUrl()); } break; case R.id.history_tab_menu_id: bookmarksOrHistoryPicker(true); break; case R.id.bookmarks_tab_menu_id: bookmarksOrHistoryPicker(false); break; case R.id.properties_tab_menu_id: if (mTabListener != null && mTabOverview != null) { int pos = mTabOverview.getContextMenuPosition(item); showPageInfo(mTabControl.getTab(pos), false); } break; case R.id.window_one_menu_id: case R.id.window_two_menu_id: case R.id.window_three_menu_id: case R.id.window_four_menu_id: case R.id.window_five_menu_id: case R.id.window_six_menu_id: case R.id.window_seven_menu_id: case R.id.window_eight_menu_id: { int menuid = item.getItemId(); for (int id = 0; id < WINDOW_SHORTCUT_ID_ARRAY.length; id++) { if (WINDOW_SHORTCUT_ID_ARRAY[id] == menuid) { TabControl.Tab desiredTab = mTabControl.getTab(id); if (desiredTab != null && desiredTab != mTabControl.getCurrentTab()) { switchTabs(mTabControl.getCurrentIndex(), id, false); } break; } } } break; default: if (!super.onOptionsItemSelected(item)) { return false; } // 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 R.id.TAB_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); menu.setGroupVisible(R.id.TAB_MENU, true); menu.setGroupEnabled(R.id.TAB_MENU, true); } boolean newT = mTabControl.getTabCount() < TabControl.MAX_TABS; final MenuItem tab = menu.findItem(R.id.new_tab_menu_id); tab.setVisible(newT); tab.setEnabled(newT); break; 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); menu.setGroupVisible(R.id.TAB_MENU, false); menu.setGroupEnabled(R.id.TAB_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); menu.setGroupVisible(R.id.TAB_MENU, false); menu.setGroupEnabled(R.id.TAB_MENU, false); } 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); // 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); // If there is only 1 window, the text will be "New window" final MenuItem windows = menu.findItem(R.id.windows_menu_id); windows.setTitleCondensed(mTabControl.getTabCount() > 1 ? getString(R.string.view_tabs_condensed) : getString(R.string.tab_picker_new_tab)); boolean isNavDump = mSettings.isNavDump(); final MenuItem nav = menu.findItem(R.id.dump_nav_menu_id); 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(Contacts.People.CONTENT_ITEM_TYPE); menu.findItem(R.id.add_contact_context_menu_id).setIntent( addIntent); menu.findItem(R.id.copy_phone_context_menu_id).setOnMenuItemClickListener( 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. private void attachTabToContentView(TabControl.Tab t) { final WebView main = t.getWebView(); // Attach the main WebView. mContentView.addView(main, COVER_SCREEN_PARAMS); // Attach the sub window if necessary attachSubWindow(t); // 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) { // If a sub window exists, attach it to the content view. final WebView subView = t.getSubWebView(); if (subView != null) { final View container = t.getSubWebViewContainer(); mContentView.addView(container, COVER_SCREEN_PARAMS); subView.requestFocus(); } } // Remove the given tab from the content view. private void removeTabFromContentView(TabControl.Tab t) { // Remove the main WebView. mContentView.removeView(t.getWebView()); // Remove the sub window if it exists. if (t.getSubWebView() != null) { mContentView.removeView(t.getSubWebViewContainer()); } } // 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) { final WebView mainView = t.getWebView(); if (t.getSubWebView() != null) { // Remove the container view and request focus on the main WebView. mContentView.removeView(t.getSubWebViewContainer()); mainView.requestFocus(); // Tell the TabControl to dismiss the subwindow. This will destroy // the WebView. mTabControl.dismissSubWindow(t); } } // Send the ANIMTE_FROM_OVERVIEW message after changing the current tab. private void sendAnimateFromOverview(final TabControl.Tab tab, final boolean newTab, final String url, final int delay, final Message msg) { // Set the current tab. mTabControl.setCurrentTab(tab); // Attach the WebView so it will layout. attachTabToContentView(tab); // Set the view to invisibile for now. tab.getWebView().setVisibility(View.INVISIBLE); // If there is a sub window, make it invisible too. if (tab.getSubWebView() != null) { tab.getSubWebViewContainer().setVisibility(View.INVISIBLE); } // Create our fake animating view. final AnimatingView view = new AnimatingView(this, tab); // Attach it to the view system and make in invisible so it will // layout but not flash white on the screen. mContentView.addView(view, COVER_SCREEN_PARAMS); view.setVisibility(View.INVISIBLE); // Send the animate message. final HashMap map = new HashMap(); map.put("view", view); // Load the url after the AnimatingView has captured the picture. This // prevents any bad layout or bad scale from being used during // animation. if (url != null) { dismissSubWindow(tab); tab.getWebView().loadUrl(url); } map.put("msg", msg); mHandler.sendMessageDelayed(mHandler.obtainMessage( ANIMATE_FROM_OVERVIEW, newTab ? 1 : 0, 0, map), delay); // Increment the count to indicate that we are in an animation. mAnimationCount++; // Remove the listener so we don't get any more tab changes. mTabOverview.setListener(null); mTabListener = null; // Make the menu empty until the animation completes. mMenuState = EMPTY_MENU; } // 500ms animation with 800ms delay private static final int TAB_ANIMATION_DURATION = 500; private static final int TAB_OVERVIEW_DELAY = 800; // Called by TabControl when a tab is requesting focus /* package */ void showTab(TabControl.Tab t) { showTab(t, null); } private void showTab(TabControl.Tab t, String url) { // Disallow focus change during a tab animation. if (mAnimationCount > 0) { return; } int delay = 0; if (mTabOverview == null) { // Add a delay so the tab overview can be shown before the second // animation begins. delay = TAB_ANIMATION_DURATION + TAB_OVERVIEW_DELAY; tabPicker(false, mTabControl.getTabIndex(t), false); } sendAnimateFromOverview(t, false, url, delay, null); } // 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. If the tab overview is not // showing, it will animate to the tab overview, create a new tab and // animate away from it. After the animation completes, it will dispatch // the given Message. If the tab overview is already showing (i.e. this // method is called from TabListener.onClick(), the method will animate // away from the tab overview. private TabControl.Tab openTabAndShow(String url, final Message msg, boolean closeOnExit, String appId) { final boolean newTab = mTabControl.getTabCount() != TabControl.MAX_TABS; final TabControl.Tab currentTab = mTabControl.getCurrentTab(); if (newTab) { int delay = 0; // If the tab overview is up and there are animations, just load // the url. if (mTabOverview != null && mAnimationCount > 0) { if (url != null) { // We should not have a msg here since onCreateWindow // checks the animation count and every other caller passes // null. assert msg == null; // just dismiss the subwindow and load the given url. dismissSubWindow(currentTab); currentTab.getWebView().loadUrl(url); } } else { // show mTabOverview if it is not there. if (mTabOverview == null) { // We have to delay the animation from the tab picker by the // length of the tab animation. Add a delay so the tab // overview can be shown before the second animation begins. delay = TAB_ANIMATION_DURATION + TAB_OVERVIEW_DELAY; tabPicker(false, ImageGrid.NEW_TAB, false); } // Animate from the Tab overview after any animations have // finished. final TabControl.Tab tab = mTabControl.createNewTab( closeOnExit, appId, url); sendAnimateFromOverview(tab, true, url, delay, msg); return tab; } } else if (url != null) { // We should not have a msg here. assert msg == null; if (mTabOverview != null && mAnimationCount == 0) { sendAnimateFromOverview(currentTab, false, url, TAB_OVERVIEW_DELAY, null); } else { // Get rid of the subwindow if it exists dismissSubWindow(currentTab); // Load the given url. currentTab.getWebView().loadUrl(url); } } return currentTab; } private Animation createTabAnimation(final AnimatingView view, final View cell, boolean scaleDown) { final AnimationSet set = new AnimationSet(true); final float scaleX = (float) cell.getWidth() / view.getWidth(); final float scaleY = (float) cell.getHeight() / view.getHeight(); if (scaleDown) { set.addAnimation(new ScaleAnimation(1.0f, scaleX, 1.0f, scaleY)); set.addAnimation(new TranslateAnimation(0, cell.getLeft(), 0, cell.getTop())); } else { set.addAnimation(new ScaleAnimation(scaleX, 1.0f, scaleY, 1.0f)); set.addAnimation(new TranslateAnimation(cell.getLeft(), 0, cell.getTop(), 0)); } set.setDuration(TAB_ANIMATION_DURATION); set.setInterpolator(new DecelerateInterpolator()); return set; } // Animate to the tab overview. currentIndex tells us which position to // animate to and newIndex is the position that should be selected after // the animation completes. // If remove is true, after the animation stops, a confirmation dialog will // be displayed to the user. private void animateToTabOverview(final int newIndex, final boolean remove, final AnimatingView view) { // Find the view in the ImageGrid allowing for the "New Tab" cell. int position = mTabControl.getTabIndex(view.mTab); if (!((ImageAdapter) mTabOverview.getAdapter()).maxedOut()) { position++; } // Offset the tab position with the first visible position to get a // number between 0 and 3. position -= mTabOverview.getFirstVisiblePosition(); // Grab the view that we are going to animate to. final View v = mTabOverview.getChildAt(position); final Animation.AnimationListener l = new Animation.AnimationListener() { public void onAnimationStart(Animation a) { mTabOverview.requestFocus(); // Clear the listener so we don't trigger a tab // selection. mTabOverview.setListener(null); } public void onAnimationRepeat(Animation a) {} public void onAnimationEnd(Animation a) { // We are no longer animating so decrement the count. mAnimationCount--; // Make the view GONE so that it will not draw between // now and when the Runnable is handled. view.setVisibility(View.GONE); // Post a runnable since we can't modify the view // hierarchy during this callback. mHandler.post(new Runnable() { public void run() { // Remove the AnimatingView. mContentView.removeView(view); if (mTabOverview != null) { // Make newIndex visible. mTabOverview.setCurrentIndex(newIndex); // Restore the listener. mTabOverview.setListener(mTabListener); // Change the menu to TAB_MENU if the // ImageGrid is interactive. if (mTabOverview.isLive()) { mMenuState = R.id.TAB_MENU; mTabOverview.requestFocus(); } } // If a remove was requested, remove the tab. if (remove) { // During a remove, the current tab has // already changed. Remember the current one // here. final TabControl.Tab currentTab = mTabControl.getCurrentTab(); // Remove the tab at newIndex from // TabControl and the tab overview. final TabControl.Tab tab = mTabControl.getTab(newIndex); mTabControl.removeTab(tab); // Restore the current tab. if (currentTab != tab) { mTabControl.setCurrentTab(currentTab); } if (mTabOverview != null) { mTabOverview.remove(newIndex); // Make the current tab visible. mTabOverview.setCurrentIndex( mTabControl.getCurrentIndex()); } } } }); } }; // Do an animation if there is a view to animate to. if (v != null) { // Create our animation final Animation anim = createTabAnimation(view, v, true); anim.setAnimationListener(l); // Start animating view.startAnimation(anim); } else { // If something goes wrong and we didn't find a view to animate to, // just do everything here. l.onAnimationStart(null); l.onAnimationEnd(null); } } // Animate from the tab picker. The index supplied is the index to animate // from. private void animateFromTabOverview(final AnimatingView view, final boolean newTab, final Message msg) { // firstVisible is the first visible tab on the screen. This helps // to know which corner of the screen the selected tab is. int firstVisible = mTabOverview.getFirstVisiblePosition(); // tabPosition is the 0-based index of of the tab being opened int tabPosition = mTabControl.getTabIndex(view.mTab); if (!((ImageAdapter) mTabOverview.getAdapter()).maxedOut()) { // Add one to make room for the "New Tab" cell. tabPosition++; } // If this is a new tab, animate from the "New Tab" cell. if (newTab) { tabPosition = 0; } // Location corresponds to the four corners of the screen. // A new tab or 0 is upper left, 0 for an old tab is upper // right, 1 is lower left, and 2 is lower right int location = tabPosition - firstVisible; // Find the view at this location. final View v = mTabOverview.getChildAt(location); // Wait until the animation completes to replace the AnimatingView. final Animation.AnimationListener l = new Animation.AnimationListener() { public void onAnimationStart(Animation a) {} public void onAnimationRepeat(Animation a) {} public void onAnimationEnd(Animation a) { mHandler.post(new Runnable() { public void run() { mContentView.removeView(view); // Dismiss the tab overview. If the cell at the // given location is null, set the fade // parameter to true. dismissTabOverview(v == null); TabControl.Tab t = mTabControl.getCurrentTab(); mMenuState = R.id.MAIN_MENU; // Resume regular updates. t.getWebView().resumeTimers(); // Dispatch the message after the animation // completes. if (msg != null) { msg.sendToTarget(); } // The animation is done and the tab overview is // gone so allow key events and other animations // to begin. mAnimationCount--; // Reset all the title bar info. resetTitle(); } }); } }; if (v != null) { final Animation anim = createTabAnimation(view, v, false); // Set the listener and start animating anim.setAnimationListener(l); view.startAnimation(anim); // Make the view VISIBLE during the animation. view.setVisibility(View.VISIBLE); } else { // Go ahead and do all the cleanup. l.onAnimationEnd(null); } } // Dismiss the tab overview applying a fade if needed. private void dismissTabOverview(final boolean fade) { if (fade) { AlphaAnimation anim = new AlphaAnimation(1.0f, 0.0f); anim.setDuration(500); anim.startNow(); mTabOverview.startAnimation(anim); } // Just in case there was a problem with animating away from the tab // overview WebView current = mTabControl.getCurrentWebView(); if (current != null) { current.setVisibility(View.VISIBLE); } else { Log.e(LOGTAG, "No current WebView in dismissTabOverview"); } // Make the sub window container visible. if (mTabControl.getCurrentSubWindow() != null) { mTabControl.getCurrentTab().getSubWebViewContainer() .setVisibility(View.VISIBLE); } mContentView.removeView(mTabOverview); mTabOverview.clear(); mTabOverview = null; mTabListener = null; } private TabControl.Tab openTab(String url) { if (mSettings.openInBackground()) { TabControl.Tab t = mTabControl.createNewTab(); if (t != null) { t.getWebView().loadUrl(url); } return t; } else { return openTabAndShow(url, null, 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 * load a page from history). */ private void resetTitle() { resetLockIcon(); resetTitleIconAndProgress(); } /** * 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; // While the tab overview is animating or being shown, block changes // to the title. if (mAnimationCount == 0 && mTabOverview == null) { setTitle(buildUrlTitle(url, title)); } } /** * Builds and returns the page title, which is some * combination of the page URL and title. * @param url The URL of the site being loaded. * @param title The title of the site being loaded. * @return The page title. */ private String buildUrlTitle(String url, String title) { String urlTitle = ""; if (url != null) { String titleUrl = buildTitleUrl(url); if (title != null && 0 < title.length()) { if (titleUrl != null && 0 < titleUrl.length()) { urlTitle = titleUrl + ": " + title; } else { urlTitle = title; } } else { if (titleUrl != null) { urlTitle = titleUrl; } } } return urlTitle; } /** * @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. */ private 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) { // While the tab overview is animating or being shown, block changes to // the favicon. if (mAnimationCount > 0 || mTabOverview != null) { return; } Drawable[] array = new Drawable[2]; PaintDrawable p = new PaintDrawable(Color.WHITE); p.setCornerRadius(3f); array[0] = p; if (icon == null) { array[1] = mGenericFavicon; } else { array[1] = new BitmapDrawable(icon); } LayerDrawable d = new LayerDrawable(array); d.setLayerInset(1, 2, 2, 2, 2); getWindow().setFeatureDrawable(Window.FEATURE_LEFT_ICON, d); } /** * 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 (Config.LOGV) { Log.v(LOGTAG, "BrowserActivity.revertLockIcon:" + " revert lock icon to " + mLockIconType); } updateLockIconImage(mLockIconType); } private void switchTabs(int indexFrom, int indexToShow, boolean remove) { int delay = TAB_ANIMATION_DURATION + TAB_OVERVIEW_DELAY; // Animate to the tab picker, remove the current tab, then // animate away from the tab picker to the parent WebView. tabPicker(false, indexFrom, remove); // Change to the parent tab final TabControl.Tab tab = mTabControl.getTab(indexToShow); if (tab != null) { sendAnimateFromOverview(tab, false, null, delay, null); } else { // Increment this here so that no other animations can happen in // between the end of the tab picker transition and the beginning // of openTabAndShow. This has a matching decrement in the handler // of OPEN_TAB_AND_SHOW. mAnimationCount++; // Send a message to open a new tab. mHandler.sendMessageDelayed( mHandler.obtainMessage(OPEN_TAB_AND_SHOW, mSettings.getHomePage()), delay); } } 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); } 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) { switchTabs(mTabControl.getCurrentIndex(), mTabControl.getTabIndex(parent), true); } else { if (current.closeOnExit()) { if (mTabControl.getTabCount() == 1) { finish(); return; } // call pauseWebView() now, we won't be able to call it in // onPause() as the WebView won't be valid. pauseWebView(); 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); } } } public KeyTracker.State onKeyTracker(int keyCode, KeyEvent event, KeyTracker.Stage stage, int duration) { // if onKeyTracker() is called after activity onStop() // because of accumulated key events, // we should ignore it as browser is not active any more. WebView topWindow = getTopWindow(); if (topWindow == null) return KeyTracker.State.NOT_TRACKING; if (keyCode == KeyEvent.KEYCODE_BACK) { // During animations, block the back key so that other animations // are not triggered and so that we don't end up destroying all the // WebViews before finishing the animation. if (mAnimationCount > 0) { return KeyTracker.State.DONE_TRACKING; } if (stage == KeyTracker.Stage.LONG_REPEAT) { bookmarksOrHistoryPicker(true); return KeyTracker.State.DONE_TRACKING; } else if (stage == KeyTracker.Stage.UP) { // FIXME: Currently, we do not have a notion of the // history picker for the subwindow, but maybe we // should? WebView subwindow = mTabControl.getCurrentSubWindow(); if (subwindow != null) { if (subwindow.canGoBack()) { subwindow.goBack(); } else { dismissSubWindow(mTabControl.getCurrentTab()); } } else { goBackOnePageOrQuit(); } return KeyTracker.State.DONE_TRACKING; } return KeyTracker.State.KEEP_TRACKING; } return KeyTracker.State.NOT_TRACKING; } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_MENU) { mMenuIsDown = true; } boolean handled = mKeyTracker.doKeyDown(keyCode, event); if (!handled) { switch (keyCode) { case KeyEvent.KEYCODE_SPACE: if (event.isShiftPressed()) { getTopWindow().pageUp(false); } else { getTopWindow().pageDown(false); } handled = true; break; default: break; } } return handled || super.onKeyDown(keyCode, event); } @Override public boolean onKeyUp(int keyCode, KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_MENU) { mMenuIsDown = false; } return mKeyTracker.doKeyUp(keyCode, event) || super.onKeyUp(keyCode, event); } private void stopLoading() { 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 ANIMATE_FROM_OVERVIEW = 104; private static final int ANIMATE_TO_OVERVIEW = 105; private static final int OPEN_TAB_AND_SHOW = 106; private static final int CHECK_MEMORY = 107; private static final int RELEASE_WAKELOCK = 108; // Private handler for handling javascript and saving passwords private Handler mHandler = new Handler() { public void handleMessage(Message msg) { switch (msg.what) { case ANIMATE_FROM_OVERVIEW: final HashMap map = (HashMap) msg.obj; animateFromTabOverview((AnimatingView) map.get("view"), msg.arg1 == 1, (Message) map.get("msg")); break; case ANIMATE_TO_OVERVIEW: animateToTabOverview(msg.arg1, msg.arg2 == 1, (AnimatingView) msg.obj); break; case OPEN_TAB_AND_SHOW: // Decrement mAnimationCount before openTabAndShow because // the method relies on the value being 0 to start the next // animation. mAnimationCount--; openTabAndShow((String) msg.obj, null, false, null); break; 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); 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 CHECK_MEMORY: // reschedule to check memory condition mHandler.removeMessages(CHECK_MEMORY); mHandler.sendMessageDelayed(mHandler.obtainMessage (CHECK_MEMORY), CHECK_MEMORY_INTERVAL); checkMemory(); break; case RELEASE_WAKELOCK: if (mWakeLock.isHeld()) { mWakeLock.release(); } break; } } }; // ------------------------------------------------------------------------- // 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(String url, Bitmap icon) { if (icon != null) { BrowserBookmarksAdapter.updateBookmarkFavicon(mResolver, url, icon); } setFavicon(icon); } private final WebViewClient mWebViewClient = new WebViewClient() { @Override public void onPageStarted(WebView view, String url, Bitmap favicon) { resetLockIcon(url); setUrlTitle(url, null); // Call updateIcon instead of setFavicon so the bookmark // database can be updated. updateIcon(url, favicon); if (mSettings.isTracing() == true) { // FIXME: we should save the trace file somewhere other than data. // I can't use "/tmp" as it competes for system memory. File file = getDir("browserTrace", 0); String baseDir = file.getPath(); if (!baseDir.endsWith(File.separator)) baseDir += File.separator; String host; try { WebAddress uri = new WebAddress(url); host = uri.mHost; } catch (android.net.ParseException ex) { host = "unknown_host"; } host = host.replace('.', '_'); baseDir = baseDir + host; file = new File(baseDir+".data"); if (file.exists() == true) { file.delete(); } file = new File(baseDir+".key"); if (file.exists() == true) { file.delete(); } mInTrace = true; Debug.startMethodTracing(baseDir, 8 * 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, resumeWebView() does nothing. resumeWebView(); } // reset sync timer to avoid sync starts during loading a page CookieSyncManager.getInstance().resetSync(); mInLoad = true; updateInLoadMenuItems(); if (!mIsNetworkUp) { if ( mAlertDialog == null) { mAlertDialog = new AlertDialog.Builder(BrowserActivity.this) .setTitle(R.string.loadSuspendedTitle) .setMessage(R.string.loadSuspended) .setPositiveButton(R.string.ok, null) .show(); } if (view != null) { view.setNetworkAvailable(false); } } // schedule to check memory condition mHandler.sendMessageDelayed(mHandler.obtainMessage(CHECK_MEMORY), CHECK_MEMORY_INTERVAL); } @Override public void onPageFinished(WebView view, String url) { // Reset the title and icon in case we stopped a provisional // load. resetTitleAndIcon(view); // Update the lock icon image only once we are done loading updateLockIconImage(mLockIconType); // 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 (Config.LOGD) { 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 (Config.LOGD) { 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 (Config.LOGD) { 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; // pauseWebView() will do nothing and return false if onPause() // is not called yet. if (pauseWebView()) { if (mWakeLock.isHeld()) { mHandler.removeMessages(RELEASE_WAKELOCK); mWakeLock.release(); } } } mHandler.removeMessages(CHECK_MEMORY); checkMemory(); } // 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; } } Uri uri; try { uri = Uri.parse(url); } catch (IllegalArgumentException ex) { return false; } // check whether other activities want to handle this url Intent intent = new Intent(Intent.ACTION_VIEW, uri); intent.addCategory(Intent.CATEGORY_BROWSABLE); 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 (Config.LOGV) { 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 mQueuedErrors; private void queueError(int err, String desc) { if (mQueuedErrors == null) { mQueuedErrors = new LinkedList(); } for (ErrorDialog d : mQueuedErrors) { if (d.mError == err) { // Already saw a similar error, ignore the new one. return; } } ErrorDialog errDialog = new ErrorDialog( err == EventHandler.FILE_NOT_FOUND_ERROR ? 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 != EventHandler.ERROR_LOOKUP && errorCode != EventHandler.ERROR_CONNECT && errorCode != EventHandler.ERROR_BAD_URL && errorCode != EventHandler.ERROR_UNSUPPORTED_SCHEME && errorCode != EventHandler.FILE_ERROR) { 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; } 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(); // openTabAndShow will dispatch the message after creating the // new WebView. This will prevent another request from coming // in during the animation. final TabControl.Tab newTab = openTabAndShow(null, msg, false, null); if (newTab != parent) { parent.addChildTab(newTab); } WebView.WebViewTransport transport = (WebView.WebViewTransport) msg.obj; transport.setWebView(mTabControl.getCurrentWebView()); } } @Override public boolean onCreateWindow(WebView view, final boolean dialog, final boolean userGesture, final Message resultMsg) { // Ignore these requests during tab animations or if the tab // overview is showing. if (mAnimationCount > 0 || mTabOverview != null) { return false; } // 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 will call openTabAndShow for new Windows and // that will call tabPicker which will increment // mAnimationCount. 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) { // Same comment as above for setting // mAnimationCount. createWindow(dialog, resultMsg); // Since we incremented mAnimationCount while the // dialog was up, we have to decrement it here. mAnimationCount--; } }; // Block the popup by returning a null WebView. final AlertDialog.OnClickListener blockListener = new AlertDialog.OnClickListener() { public void onClick(DialogInterface d, int which) { resultMsg.sendToTarget(); // We are not going to trigger an animation so // unblock keys and animation requests. mAnimationCount--; } }; // 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(); // We want to increment mAnimationCount here to prevent a // potential race condition. If the user allows a pop-up from a // site and that pop-up then triggers another pop-up, it is // possible to get the BACK key between here and when the dialog // appears. mAnimationCount++; return true; } @Override public void onCloseWindow(WebView window) { final int currentIndex = mTabControl.getCurrentIndex(); final TabControl.Tab parent = mTabControl.getCurrentTab().getParentTab(); if (parent != null) { // JavaScript can only close popup window. switchTabs(currentIndex, mTabControl.getTabIndex(parent), true); } } @Override public void onProgressChanged(WebView view, int newProgress) { // Block progress updates to the title bar while the tab overview // is animating or being displayed. if (mAnimationCount == 0 && mTabOverview == null) { getWindow().setFeatureInt(Window.FEATURE_PROGRESS, newProgress * 100); } if (newProgress == 100) { // onProgressChanged() is called for sub-frame too while // onPageFinished() is only called for the main frame. sync // cookie and cache promptly here. CookieSyncManager.getInstance().sync(); if (mInLoad) { mInLoad = false; updateInLoadMenuItems(); } } else { // onPageFinished may have already been called but a subframe // is still loading and updating the progress. Reset mInLoad // and update the menu items. if (!mInLoad) { mInLoad = true; updateInLoadMenuItems(); } } } @Override public void onReceivedTitle(WebView view, String title) { String url = view.getOriginalUrl(); // 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; } 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()) { if (Config.LOGV) { Log.v(LOGTAG, "updating cursor"); } // Current implementation of database only has one entry per // url. int titleIndex = c.getColumnIndex(Browser.BookmarkColumns.TITLE); c.updateString(titleIndex, title); c.commitUpdates(); } 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.getUrl(), icon); } }; /** * 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); if (getPackageManager().resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY) != null) { // someone knows how to handle this mime type with this scheme, don't download. try { startActivity(intent); return; } catch (ActivityNotFoundException ex) { if (Config.LOGD) { 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.URI, uri.toString()); values.put(Downloads.COOKIE_DATA, cookies); values.put(Downloads.USER_AGENT, userAgent); values.put(Downloads.NOTIFICATION_PACKAGE, getPackageName()); values.put(Downloads.NOTIFICATION_CLASS, BrowserDownloadPage.class.getCanonicalName()); values.put(Downloads.VISIBILITY, Downloads.VISIBILITY_VISIBLE_NOTIFY_COMPLETED); values.put(Downloads.MIMETYPE, mimetype); values.put(Downloads.FILENAME_HINT, filename); values.put(Downloads.DESCRIPTION, uri.getHost()); if (contentLength > 0) { values.put(Downloads.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 (Config.LOGV) { Log.v(LOGTAG, "BrowserActivity.resetLockIcon:" + " reset lock icon to " + mLockIconType); } } updateLockIconImage(LOCK_ICON_UNSECURE); } /** * Resets the lock icon. This method is called when the icon needs to be * reset but we do not know whether we are loading a secure or not secure * page. */ private void resetLockIcon() { // Save the lock-icon state (we revert to it if the load gets cancelled) saveLockIcon(); mLockIconType = LOCK_ICON_UNSECURE; if (Config.LOGV) { Log.v(LOGTAG, "BrowserActivity.resetLockIcon:" + " reset lock icon to " + mLockIconType); } updateLockIconImage(LOCK_ICON_UNSECURE); } /** * 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; } // If the tab overview is animating or being shown, do not update the // lock icon. if (mAnimationCount == 0 && mTabOverview == null) { getWindow().setFeatureDrawable(Window.FEATURE_RIGHT_ICON, d); } } /** * Displays a page-info dialog. * @param 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 && mAlertDialog == null) { mAlertDialog = new AlertDialog.Builder(this) .setTitle(R.string.loadSuspendedTitle) .setMessage(R.string.loadSuspended) .setPositiveButton(R.string.ok, null) .show(); } } WebView w = mTabControl.getCurrentWebView(); if (w != null) { w.setNetworkAvailable(up); } } @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(); // If the Window overview is up and we are not in the // middle of an animation, animate away from it to the // current tab. if (mTabOverview != null && mAnimationCount == 0) { sendAnimateFromOverview(currentTab, false, data, TAB_OVERVIEW_DELAY, null); } else { 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); } /** * Handle results from Tab Switcher mTabOverview tool */ private class TabListener implements ImageGrid.Listener { public void remove(int position) { // Note: Remove is not enabled if we have only one tab. if (Config.DEBUG && mTabControl.getTabCount() == 1) { throw new AssertionError(); } // Remember the current tab. TabControl.Tab current = mTabControl.getCurrentTab(); final TabControl.Tab remove = mTabControl.getTab(position); mTabControl.removeTab(remove); // If we removed the current tab, use the tab at position - 1 if // possible. if (current == remove) { // If the user removes the last tab, act like the New Tab item // was clicked on. if (mTabControl.getTabCount() == 0) { current = mTabControl.createNewTab(); sendAnimateFromOverview(current, true, mSettings.getHomePage(), TAB_OVERVIEW_DELAY, null); } else { final int index = position > 0 ? (position - 1) : 0; current = mTabControl.getTab(index); } } // The tab overview could have been dismissed before this method is // called. if (mTabOverview != null) { // Remove the tab and change the index. mTabOverview.remove(position); mTabOverview.setCurrentIndex(mTabControl.getTabIndex(current)); } // Only the current tab ensures its WebView is non-null. This // implies that we are reloading the freed tab. mTabControl.setCurrentTab(current); } public void onClick(int index) { // Change the tab if necessary. // Index equals ImageGrid.CANCEL when pressing back from the tab // overview. if (index == ImageGrid.CANCEL) { index = mTabControl.getCurrentIndex(); // The current index is -1 if the current tab was removed. if (index == -1) { // Take the last tab as a fallback. index = mTabControl.getTabCount() - 1; } } // Clear all the data for tab picker so next time it will be // recreated. mTabControl.wipeAllPickerData(); // NEW_TAB means that the "New Tab" cell was clicked on. if (index == ImageGrid.NEW_TAB) { openTabAndShow(mSettings.getHomePage(), null, false, null); } else { sendAnimateFromOverview(mTabControl.getTab(index), false, null, 0, null); } } } // A fake View that draws the WebView's picture with a fast zoom filter. // The View is used in case the tab is freed during the animation because // of low memory. private static class AnimatingView extends View { private static final int ZOOM_BITS = Paint.FILTER_BITMAP_FLAG | Paint.DITHER_FLAG | Paint.SUBPIXEL_TEXT_FLAG; private static final DrawFilter sZoomFilter = new PaintFlagsDrawFilter(ZOOM_BITS, Paint.LINEAR_TEXT_FLAG); private final Picture mPicture; private final float mScale; private final int mScrollX; private final int mScrollY; final TabControl.Tab mTab; AnimatingView(Context ctxt, TabControl.Tab t) { super(ctxt); mTab = t; // Use the top window in the animation since the tab overview will // display the top window in each cell. final WebView w = t.getTopWindow(); mPicture = w.capturePicture(); mScale = w.getScale() / w.getWidth(); mScrollX = w.getScrollX(); mScrollY = w.getScrollY(); } @Override protected void onDraw(Canvas canvas) { canvas.save(); canvas.drawColor(Color.WHITE); if (mPicture != null) { canvas.setDrawFilter(sZoomFilter); float scale = getWidth() * mScale; canvas.scale(scale, scale); canvas.translate(-mScrollX, -mScrollY); canvas.drawPicture(mPicture); } canvas.restore(); } } /** * Open the tab picker. This function will always use the current tab in * its animation. * @param stay boolean stating whether the tab picker is to remain open * (in which case it needs a listener and its menu) or not. * @param index The index of the tab to show as the selection in the tab * overview. * @param remove If true, the tab at index will be removed after the * animation completes. */ private void tabPicker(final boolean stay, final int index, final boolean remove) { if (mTabOverview != null) { return; } int size = mTabControl.getTabCount(); TabListener l = null; if (stay) { l = mTabListener = new TabListener(); } mTabOverview = new ImageGrid(this, stay, l); for (int i = 0; i < size; i++) { final TabControl.Tab t = mTabControl.getTab(i); mTabControl.populatePickerData(t); mTabOverview.add(t); } // Tell the tab overview to show the current tab, the tab overview will // handle the "New Tab" case. int currentIndex = mTabControl.getCurrentIndex(); mTabOverview.setCurrentIndex(currentIndex); // Attach the tab overview. mContentView.addView(mTabOverview, COVER_SCREEN_PARAMS); // Create a fake AnimatingView to animate the WebView's picture. final TabControl.Tab current = mTabControl.getCurrentTab(); final AnimatingView v = new AnimatingView(this, current); mContentView.addView(v, COVER_SCREEN_PARAMS); removeTabFromContentView(current); // Pause timers to get the animation smoother. current.getWebView().pauseTimers(); // Send a message so the tab picker has a chance to layout and get // positions for all the cells. mHandler.sendMessage(mHandler.obtainMessage(ANIMATE_TO_OVERVIEW, index, remove ? 1 : 0, v)); // Setting this will indicate that we are animating to the overview. We // set it here to prevent another request to animate from coming in // between now and when ANIMATE_TO_OVERVIEW is handled. mAnimationCount++; // Always change the title bar to the window overview title while // animating. getWindow().setFeatureDrawable(Window.FEATURE_LEFT_ICON, null); getWindow().setFeatureDrawable(Window.FEATURE_RIGHT_ICON, null); getWindow().setFeatureInt(Window.FEATURE_PROGRESS, Window.PROGRESS_VISIBILITY_OFF); setTitle(R.string.tab_picker_title); // Make the menu empty until the animation completes. mMenuState = EMPTY_MENU; } private 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(); // 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("maxTabsOpen", mTabControl.getTabCount() >= TabControl.MAX_TABS); 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 void checkMemory() { ActivityManager.MemoryInfo mi = new ActivityManager.MemoryInfo(); ((ActivityManager) getSystemService(ACTIVITY_SERVICE)) .getMemoryInfo(mi); // FIXME: mi.lowMemory is too aggressive, use (mi.availMem < // mi.threshold) for now // if (mi.lowMemory) { if (mi.availMem < mi.threshold) { Log.w(LOGTAG, "Browser is freeing memory now because: available=" + (mi.availMem / 1024) + "K threshold=" + (mi.threshold / 1024) + "K"); mTabControl.freeMemory(); } } 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):\\/\\/" + "|(?: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()) { if (hasSpace) { inUrl = inUrl.replace(" ", "%20"); } // force scheme to lowercase String scheme = matcher.group(1); String lcScheme = scheme.toLowerCase(); if (!lcScheme.equals(scheme)) { return lcScheme + matcher.group(2); } return inUrl; } if (hasSpace) { // FIXME: quick search, need to be customized by setting if (inUrl.length() > 2 && inUrl.charAt(1) == ' ') { // FIXME: Is this the correct place to add to searches? // what if someone else calls this function? char char0 = inUrl.charAt(0); if (char0 == 'g') { Browser.addSearchUrl(mResolver, inUrl); return composeSearchUrl(inUrl.substring(2)); } else if (char0 == 'w') { Browser.addSearchUrl(mResolver, inUrl); return URLUtil.composeSearchUrl(inUrl.substring(2), QuickSearch_W, QUERY_PLACE_HOLDER); } else if (char0 == 'd') { Browser.addSearchUrl(mResolver, inUrl); return URLUtil.composeSearchUrl(inUrl.substring(2), QuickSearch_D, QUERY_PLACE_HOLDER); } else if (char0 == 'l') { Browser.addSearchUrl(mResolver, inUrl); // FIXME: we need location in this case return URLUtil.composeSearchUrl(inUrl.substring(2), QuickSearch_L, QUERY_PLACE_HOLDER); } } } else { if (Regex.WEB_URL_PATTERN.matcher(inUrl).matches()) { return URLUtil.guessUrl(inUrl); } } Browser.addSearchUrl(mResolver, inUrl); return composeSearchUrl(inUrl); } /* package */ String composeSearchUrl(String search) { return URLUtil.composeSearchUrl(search, QuickSearch_G, QUERY_PLACE_HOLDER); } /* package */void setBaseSearchUrl(String url) { if (url == null || url.length() == 0) { /* * get the google search url based on the SIM. Default is US. NOTE: * This code uses resources to optionally select the search Uri, * based on the MCC value from the SIM. The default string will most * likely be fine. It is parameterized to accept info from the * Locale, the language code is the first parameter (%1$s) and the * country code is the second (%2$s). This code must function in the * same way as a similar lookup in * com.android.googlesearch.SuggestionProvider#onCreate(). If you * change either of these functions, change them both. (The same is * true for the underlying resource strings, which are stored in * mcc-specific xml files.) */ Locale l = Locale.getDefault(); QuickSearch_G = getResources().getString( R.string.google_search_base, l.getLanguage(), l.getCountry().toLowerCase()) + "client=ms-" + Partner.getString(this.getContentResolver(), Partner.CLIENT_ID) + "&source=android-" + GOOGLE_SEARCH_SOURCE_SUGGEST + "&q=%s"; } else { QuickSearch_G = url; } } private final static int LOCK_ICON_UNSECURE = 0; private final static int LOCK_ICON_SECURE = 1; private final static int LOCK_ICON_MIXED = 2; 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 ImageGrid mTabOverview; // 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 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 mPageStarted; private boolean mActivityInPause = true; private boolean mMenuIsDown; private final KeyTracker mKeyTracker = new KeyTracker(this); // As trackball doesn't send repeat down, we have to track it ourselves private boolean mTrackTrackball; 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; private Drawable mGenericFavicon; /* 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); // We may provide UI to customize these // Google search from the browser static String QuickSearch_G; // 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 TabListener mTabListener; private String mLastEnteredUrl; private PowerManager.WakeLock mWakeLock; private final static int WAKELOCK_TIMEOUT = 5 * 60 * 1000; // 5 minutes private Toast mStopToast; // Used during animations to prevent other animations from being triggered. // A count is used since the animation to and from the Window overview can // overlap. A count of 0 means no animation where a count of > 0 means // there are animations in progress. private int mAnimationCount; // As the ids are dynamically created, we can't guarantee that they will // be in sequence, so this static array maps ids to a window number. final static private int[] WINDOW_SHORTCUT_ID_ARRAY = { R.id.window_one_menu_id, R.id.window_two_menu_id, R.id.window_three_menu_id, R.id.window_four_menu_id, R.id.window_five_menu_id, R.id.window_six_menu_id, R.id.window_seven_menu_id, R.id.window_eight_menu_id }; // monitor platform changes private IntentFilter mNetworkStateChangedFilter; private BroadcastReceiver mNetworkStateIntentReceiver; // activity requestCode final static int COMBO_PAGE = 1; final static int DOWNLOAD_PAGE = 2; final static int PREFERENCES_PAGE = 3; // the frenquency of checking whether system memory is low final static int CHECK_MEMORY_INTERVAL = 30000; // 30 seconds }