BrowserActivity.java revision fde9746ac3055848e110c35f19ec5893c621f766
1/* 2 * Copyright (C) 2006 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.browser; 18 19import android.app.Activity; 20import android.app.AlertDialog; 21import android.app.ProgressDialog; 22import android.app.SearchManager; 23import android.content.ActivityNotFoundException; 24import android.content.BroadcastReceiver; 25import android.content.ComponentName; 26import android.content.ContentResolver; 27import android.content.ContentUris; 28import android.content.ContentValues; 29import android.content.Context; 30import android.content.DialogInterface; 31import android.content.Intent; 32import android.content.IntentFilter; 33import android.content.ServiceConnection; 34import android.content.pm.PackageInfo; 35import android.content.pm.PackageManager; 36import android.content.pm.ResolveInfo; 37import android.content.res.Configuration; 38import android.content.res.Resources; 39import android.database.Cursor; 40import android.graphics.Bitmap; 41import android.graphics.BitmapFactory; 42import android.graphics.Canvas; 43import android.graphics.Picture; 44import android.graphics.PixelFormat; 45import android.graphics.Rect; 46import android.graphics.drawable.Drawable; 47import android.net.ConnectivityManager; 48import android.net.NetworkInfo; 49import android.net.Uri; 50import android.net.WebAddress; 51import android.net.http.SslCertificate; 52import android.net.http.SslError; 53import android.os.AsyncTask; 54import android.os.Bundle; 55import android.os.Debug; 56import android.os.Environment; 57import android.os.Handler; 58import android.os.IBinder; 59import android.os.Message; 60import android.os.PowerManager; 61import android.os.Process; 62import android.os.RemoteException; 63import android.os.ServiceManager; 64import android.os.SystemClock; 65import android.provider.Browser; 66import android.provider.ContactsContract; 67import android.provider.ContactsContract.Intents.Insert; 68import android.provider.Downloads; 69import android.provider.MediaStore; 70import android.text.IClipboard; 71import android.text.TextUtils; 72import android.text.format.DateFormat; 73import android.util.AttributeSet; 74import android.util.Log; 75import android.view.ContextMenu; 76import android.view.Gravity; 77import android.view.KeyEvent; 78import android.view.LayoutInflater; 79import android.view.Menu; 80import android.view.MenuInflater; 81import android.view.MenuItem; 82import android.view.View; 83import android.view.ViewGroup; 84import android.view.Window; 85import android.view.WindowManager; 86import android.view.ContextMenu.ContextMenuInfo; 87import android.view.MenuItem.OnMenuItemClickListener; 88import android.webkit.CookieManager; 89import android.webkit.CookieSyncManager; 90import android.webkit.DownloadListener; 91import android.webkit.HttpAuthHandler; 92import android.webkit.PluginManager; 93import android.webkit.SslErrorHandler; 94import android.webkit.URLUtil; 95import android.webkit.ValueCallback; 96import android.webkit.WebChromeClient; 97import android.webkit.WebHistoryItem; 98import android.webkit.WebIconDatabase; 99import android.webkit.WebView; 100import android.widget.EditText; 101import android.widget.FrameLayout; 102import android.widget.LinearLayout; 103import android.widget.TextView; 104import android.widget.Toast; 105 106import com.android.common.Patterns; 107 108import com.google.android.googleapps.IGoogleLoginService; 109import com.google.android.googlelogin.GoogleLoginServiceConstants; 110 111import java.io.ByteArrayOutputStream; 112import java.io.File; 113import java.net.MalformedURLException; 114import java.net.URI; 115import java.net.URISyntaxException; 116import java.net.URL; 117import java.net.URLEncoder; 118import java.text.ParseException; 119import java.util.Date; 120import java.util.HashMap; 121import java.util.regex.Matcher; 122import java.util.regex.Pattern; 123 124public class BrowserActivity extends Activity 125 implements View.OnCreateContextMenuListener, 126 DownloadListener { 127 128 /* Define some aliases to make these debugging flags easier to refer to. 129 * This file imports android.provider.Browser, so we can't just refer to "Browser.DEBUG". 130 */ 131 private final static boolean DEBUG = com.android.browser.Browser.DEBUG; 132 private final static boolean LOGV_ENABLED = com.android.browser.Browser.LOGV_ENABLED; 133 private final static boolean LOGD_ENABLED = com.android.browser.Browser.LOGD_ENABLED; 134 135 private IGoogleLoginService mGls = null; 136 private ServiceConnection mGlsConnection = null; 137 138 // These are single-character shortcuts for searching popular sources. 139 private static final int SHORTCUT_INVALID = 0; 140 private static final int SHORTCUT_GOOGLE_SEARCH = 1; 141 private static final int SHORTCUT_WIKIPEDIA_SEARCH = 2; 142 private static final int SHORTCUT_DICTIONARY_SEARCH = 3; 143 private static final int SHORTCUT_GOOGLE_MOBILE_LOCAL_SEARCH = 4; 144 145 private void setupHomePage() { 146 final Runnable getAccount = new Runnable() { 147 public void run() { 148 // Lower priority 149 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); 150 // get the default home page 151 String homepage = mSettings.getHomePage(); 152 153 try { 154 if (mGls == null) return; 155 156 if (!homepage.startsWith("http://www.google.")) return; 157 if (homepage.indexOf('?') == -1) return; 158 159 String hostedUser = mGls.getAccount(GoogleLoginServiceConstants.PREFER_HOSTED); 160 String googleUser = mGls.getAccount(GoogleLoginServiceConstants.REQUIRE_GOOGLE); 161 162 // three cases: 163 // 164 // hostedUser == googleUser 165 // The device has only a google account 166 // 167 // hostedUser != googleUser 168 // The device has a hosted account and a google account 169 // 170 // hostedUser != null, googleUser == null 171 // The device has only a hosted account (so far) 172 173 // developers might have no accounts at all 174 if (hostedUser == null) return; 175 176 if (googleUser == null || !hostedUser.equals(googleUser)) { 177 String domain = hostedUser.substring(hostedUser.lastIndexOf('@')+1); 178 homepage = homepage.replace("?", "/a/" + domain + "?"); 179 } 180 } catch (RemoteException ignore) { 181 // Login service died; carry on 182 } catch (RuntimeException ignore) { 183 // Login service died; carry on 184 } finally { 185 finish(homepage); 186 } 187 } 188 189 private void finish(final String homepage) { 190 mHandler.post(new Runnable() { 191 public void run() { 192 mSettings.setHomePage(BrowserActivity.this, homepage); 193 resumeAfterCredentials(); 194 195 // as this is running in a separate thread, 196 // BrowserActivity's onDestroy() may have been called, 197 // which also calls unbindService(). 198 if (mGlsConnection != null) { 199 // we no longer need to keep GLS open 200 unbindService(mGlsConnection); 201 mGlsConnection = null; 202 } 203 } }); 204 } }; 205 206 final boolean[] done = { false }; 207 208 // Open a connection to the Google Login Service. The first 209 // time the connection is established, set up the homepage depending on 210 // the account in a background thread. 211 mGlsConnection = new ServiceConnection() { 212 public void onServiceConnected(ComponentName className, IBinder service) { 213 mGls = IGoogleLoginService.Stub.asInterface(service); 214 if (done[0] == false) { 215 done[0] = true; 216 Thread account = new Thread(getAccount); 217 account.setName("GLSAccount"); 218 account.start(); 219 } 220 } 221 public void onServiceDisconnected(ComponentName className) { 222 mGls = null; 223 } 224 }; 225 226 bindService(GoogleLoginServiceConstants.SERVICE_INTENT, 227 mGlsConnection, Context.BIND_AUTO_CREATE); 228 } 229 230 private static class ClearThumbnails extends AsyncTask<File, Void, Void> { 231 @Override 232 public Void doInBackground(File... files) { 233 if (files != null) { 234 for (File f : files) { 235 if (!f.delete()) { 236 Log.e(LOGTAG, f.getPath() + " was not deleted"); 237 } 238 } 239 } 240 return null; 241 } 242 } 243 244 /** 245 * This layout holds everything you see below the status bar, including the 246 * error console, the custom view container, and the webviews. 247 */ 248 private FrameLayout mBrowserFrameLayout; 249 250 @Override 251 public void onCreate(Bundle icicle) { 252 if (LOGV_ENABLED) { 253 Log.v(LOGTAG, this + " onStart"); 254 } 255 super.onCreate(icicle); 256 // test the browser in OpenGL 257 // requestWindowFeature(Window.FEATURE_OPENGL); 258 259 setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL); 260 261 mResolver = getContentResolver(); 262 263 // If this was a web search request, pass it on to the default web 264 // search provider and finish this activity. 265 if (handleWebSearchIntent(getIntent())) { 266 finish(); 267 return; 268 } 269 270 mSecLockIcon = Resources.getSystem().getDrawable( 271 android.R.drawable.ic_secure); 272 mMixLockIcon = Resources.getSystem().getDrawable( 273 android.R.drawable.ic_partial_secure); 274 275 FrameLayout frameLayout = (FrameLayout) getWindow().getDecorView() 276 .findViewById(com.android.internal.R.id.content); 277 mBrowserFrameLayout = (FrameLayout) LayoutInflater.from(this) 278 .inflate(R.layout.custom_screen, null); 279 mContentView = (FrameLayout) mBrowserFrameLayout.findViewById( 280 R.id.main_content); 281 mErrorConsoleContainer = (LinearLayout) mBrowserFrameLayout 282 .findViewById(R.id.error_console); 283 mCustomViewContainer = (FrameLayout) mBrowserFrameLayout 284 .findViewById(R.id.fullscreen_custom_content); 285 frameLayout.addView(mBrowserFrameLayout, COVER_SCREEN_PARAMS); 286 mTitleBar = new TitleBar(this); 287 mFakeTitleBar = new TitleBar(this); 288 289 // Create the tab control and our initial tab 290 mTabControl = new TabControl(this); 291 292 // Open the icon database and retain all the bookmark urls for favicons 293 retainIconsOnStartup(); 294 295 // Keep a settings instance handy. 296 mSettings = BrowserSettings.getInstance(); 297 mSettings.setTabControl(mTabControl); 298 mSettings.loadFromDb(this); 299 300 PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); 301 mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Browser"); 302 303 /* enables registration for changes in network status from 304 http stack */ 305 mNetworkStateChangedFilter = new IntentFilter(); 306 mNetworkStateChangedFilter.addAction( 307 ConnectivityManager.CONNECTIVITY_ACTION); 308 mNetworkStateIntentReceiver = new BroadcastReceiver() { 309 @Override 310 public void onReceive(Context context, Intent intent) { 311 if (intent.getAction().equals( 312 ConnectivityManager.CONNECTIVITY_ACTION)) { 313 boolean noConnectivity = intent.getBooleanExtra( 314 ConnectivityManager.EXTRA_NO_CONNECTIVITY, false); 315 onNetworkToggle(!noConnectivity); 316 } 317 } 318 }; 319 320 IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED); 321 filter.addAction(Intent.ACTION_PACKAGE_REMOVED); 322 filter.addDataScheme("package"); 323 mPackageInstallationReceiver = new BroadcastReceiver() { 324 @Override 325 public void onReceive(Context context, Intent intent) { 326 final String action = intent.getAction(); 327 final String packageName = intent.getData() 328 .getSchemeSpecificPart(); 329 final boolean replacing = intent.getBooleanExtra( 330 Intent.EXTRA_REPLACING, false); 331 if (Intent.ACTION_PACKAGE_REMOVED.equals(action) && replacing) { 332 // if it is replacing, refreshPlugins() when adding 333 return; 334 } 335 PackageManager pm = BrowserActivity.this.getPackageManager(); 336 PackageInfo pkgInfo = null; 337 try { 338 pkgInfo = pm.getPackageInfo(packageName, 339 PackageManager.GET_PERMISSIONS); 340 } catch (PackageManager.NameNotFoundException e) { 341 return; 342 } 343 if (pkgInfo != null) { 344 String permissions[] = pkgInfo.requestedPermissions; 345 if (permissions == null) { 346 return; 347 } 348 boolean permissionOk = false; 349 for (String permit : permissions) { 350 if (PluginManager.PLUGIN_PERMISSION.equals(permit)) { 351 permissionOk = true; 352 break; 353 } 354 } 355 if (permissionOk) { 356 PluginManager.getInstance(BrowserActivity.this) 357 .refreshPlugins( 358 Intent.ACTION_PACKAGE_ADDED 359 .equals(action)); 360 } 361 } 362 } 363 }; 364 registerReceiver(mPackageInstallationReceiver, filter); 365 366 if (!mTabControl.restoreState(icicle)) { 367 // clear up the thumbnail directory if we can't restore the state as 368 // none of the files in the directory are referenced any more. 369 new ClearThumbnails().execute( 370 mTabControl.getThumbnailDir().listFiles()); 371 // there is no quit on Android. But if we can't restore the state, 372 // we can treat it as a new Browser, remove the old session cookies. 373 CookieManager.getInstance().removeSessionCookie(); 374 final Intent intent = getIntent(); 375 final Bundle extra = intent.getExtras(); 376 // Create an initial tab. 377 // If the intent is ACTION_VIEW and data is not null, the Browser is 378 // invoked to view the content by another application. In this case, 379 // the tab will be close when exit. 380 UrlData urlData = getUrlDataFromIntent(intent); 381 382 final Tab t = mTabControl.createNewTab( 383 Intent.ACTION_VIEW.equals(intent.getAction()) && 384 intent.getData() != null, 385 intent.getStringExtra(Browser.EXTRA_APPLICATION_ID), urlData.mUrl); 386 mTabControl.setCurrentTab(t); 387 attachTabToContentView(t); 388 WebView webView = t.getWebView(); 389 if (extra != null) { 390 int scale = extra.getInt(Browser.INITIAL_ZOOM_LEVEL, 0); 391 if (scale > 0 && scale <= 1000) { 392 webView.setInitialScale(scale); 393 } 394 } 395 // If we are not restoring from an icicle, then there is a high 396 // likely hood this is the first run. So, check to see if the 397 // homepage needs to be configured and copy any plugins from our 398 // asset directory to the data partition. 399 if ((extra == null || !extra.getBoolean("testing")) 400 && !mSettings.isLoginInitialized()) { 401 setupHomePage(); 402 } 403 404 if (urlData.isEmpty()) { 405 if (mSettings.isLoginInitialized()) { 406 webView.loadUrl(mSettings.getHomePage()); 407 } else { 408 waitForCredentials(); 409 } 410 } else { 411 if (extra != null) { 412 urlData.setPostData(extra 413 .getByteArray(Browser.EXTRA_POST_DATA)); 414 } 415 urlData.loadIn(webView); 416 } 417 } else { 418 // TabControl.restoreState() will create a new tab even if 419 // restoring the state fails. 420 attachTabToContentView(mTabControl.getCurrentTab()); 421 } 422 423 // Read JavaScript flags if it exists. 424 String jsFlags = mSettings.getJsFlags(); 425 if (jsFlags.trim().length() != 0) { 426 mTabControl.getCurrentWebView().setJsFlags(jsFlags); 427 } 428 } 429 430 @Override 431 protected void onNewIntent(Intent intent) { 432 Tab current = mTabControl.getCurrentTab(); 433 // When a tab is closed on exit, the current tab index is set to -1. 434 // Reset before proceed as Browser requires the current tab to be set. 435 if (current == null) { 436 // Try to reset the tab in case the index was incorrect. 437 current = mTabControl.getTab(0); 438 if (current == null) { 439 // No tabs at all so just ignore this intent. 440 return; 441 } 442 mTabControl.setCurrentTab(current); 443 attachTabToContentView(current); 444 resetTitleAndIcon(current.getWebView()); 445 } 446 final String action = intent.getAction(); 447 final int flags = intent.getFlags(); 448 if (Intent.ACTION_MAIN.equals(action) || 449 (flags & Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY) != 0) { 450 // just resume the browser 451 return; 452 } 453 if (Intent.ACTION_VIEW.equals(action) 454 || Intent.ACTION_SEARCH.equals(action) 455 || MediaStore.INTENT_ACTION_MEDIA_SEARCH.equals(action) 456 || Intent.ACTION_WEB_SEARCH.equals(action)) { 457 // If this was a search request (e.g. search query directly typed into the address bar), 458 // pass it on to the default web search provider. 459 if (handleWebSearchIntent(intent)) { 460 return; 461 } 462 463 UrlData urlData = getUrlDataFromIntent(intent); 464 if (urlData.isEmpty()) { 465 urlData = new UrlData(mSettings.getHomePage()); 466 } 467 urlData.setPostData(intent 468 .getByteArrayExtra(Browser.EXTRA_POST_DATA)); 469 470 final String appId = intent 471 .getStringExtra(Browser.EXTRA_APPLICATION_ID); 472 if (Intent.ACTION_VIEW.equals(action) 473 && !getPackageName().equals(appId) 474 && (flags & Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT) != 0) { 475 Tab appTab = mTabControl.getTabFromId(appId); 476 if (appTab != null) { 477 Log.i(LOGTAG, "Reusing tab for " + appId); 478 // Dismiss the subwindow if applicable. 479 dismissSubWindow(appTab); 480 // Since we might kill the WebView, remove it from the 481 // content view first. 482 removeTabFromContentView(appTab); 483 // Recreate the main WebView after destroying the old one. 484 // If the WebView has the same original url and is on that 485 // page, it can be reused. 486 boolean needsLoad = 487 mTabControl.recreateWebView(appTab, urlData.mUrl); 488 489 if (current != appTab) { 490 switchToTab(mTabControl.getTabIndex(appTab)); 491 if (needsLoad) { 492 urlData.loadIn(appTab.getWebView()); 493 } 494 } else { 495 // If the tab was the current tab, we have to attach 496 // it to the view system again. 497 attachTabToContentView(appTab); 498 if (needsLoad) { 499 urlData.loadIn(appTab.getWebView()); 500 } 501 } 502 return; 503 } else { 504 // No matching application tab, try to find a regular tab 505 // with a matching url. 506 appTab = mTabControl.findUnusedTabWithUrl(urlData.mUrl); 507 if (appTab != null) { 508 if (current != appTab) { 509 switchToTab(mTabControl.getTabIndex(appTab)); 510 } 511 // Otherwise, we are already viewing the correct tab. 512 } else { 513 // if FLAG_ACTIVITY_BROUGHT_TO_FRONT flag is on, the url 514 // will be opened in a new tab unless we have reached 515 // MAX_TABS. Then the url will be opened in the current 516 // tab. If a new tab is created, it will have "true" for 517 // exit on close. 518 openTabAndShow(urlData, true, appId); 519 } 520 } 521 } else { 522 if (!urlData.isEmpty() 523 && urlData.mUrl.startsWith("about:debug")) { 524 if ("about:debug.dom".equals(urlData.mUrl)) { 525 current.getWebView().dumpDomTree(false); 526 } else if ("about:debug.dom.file".equals(urlData.mUrl)) { 527 current.getWebView().dumpDomTree(true); 528 } else if ("about:debug.render".equals(urlData.mUrl)) { 529 current.getWebView().dumpRenderTree(false); 530 } else if ("about:debug.render.file".equals(urlData.mUrl)) { 531 current.getWebView().dumpRenderTree(true); 532 } else if ("about:debug.display".equals(urlData.mUrl)) { 533 current.getWebView().dumpDisplayTree(); 534 } else { 535 mSettings.toggleDebugSettings(); 536 } 537 return; 538 } 539 // Get rid of the subwindow if it exists 540 dismissSubWindow(current); 541 urlData.loadIn(current.getWebView()); 542 } 543 } 544 } 545 546 private int parseUrlShortcut(String url) { 547 if (url == null) return SHORTCUT_INVALID; 548 549 // FIXME: quick search, need to be customized by setting 550 if (url.length() > 2 && url.charAt(1) == ' ') { 551 switch (url.charAt(0)) { 552 case 'g': return SHORTCUT_GOOGLE_SEARCH; 553 case 'w': return SHORTCUT_WIKIPEDIA_SEARCH; 554 case 'd': return SHORTCUT_DICTIONARY_SEARCH; 555 case 'l': return SHORTCUT_GOOGLE_MOBILE_LOCAL_SEARCH; 556 } 557 } 558 return SHORTCUT_INVALID; 559 } 560 561 /** 562 * Launches the default web search activity with the query parameters if the given intent's data 563 * are identified as plain search terms and not URLs/shortcuts. 564 * @return true if the intent was handled and web search activity was launched, false if not. 565 */ 566 private boolean handleWebSearchIntent(Intent intent) { 567 if (intent == null) return false; 568 569 String url = null; 570 final String action = intent.getAction(); 571 if (Intent.ACTION_VIEW.equals(action)) { 572 Uri data = intent.getData(); 573 if (data != null) url = data.toString(); 574 } else if (Intent.ACTION_SEARCH.equals(action) 575 || MediaStore.INTENT_ACTION_MEDIA_SEARCH.equals(action) 576 || Intent.ACTION_WEB_SEARCH.equals(action)) { 577 url = intent.getStringExtra(SearchManager.QUERY); 578 } 579 return handleWebSearchRequest(url, intent.getBundleExtra(SearchManager.APP_DATA), 580 intent.getStringExtra(SearchManager.EXTRA_DATA_KEY)); 581 } 582 583 /** 584 * Launches the default web search activity with the query parameters if the given url string 585 * was identified as plain search terms and not URL/shortcut. 586 * @return true if the request was handled and web search activity was launched, false if not. 587 */ 588 private boolean handleWebSearchRequest(String inUrl, Bundle appData, String extraData) { 589 if (inUrl == null) return false; 590 591 // In general, we shouldn't modify URL from Intent. 592 // But currently, we get the user-typed URL from search box as well. 593 String url = fixUrl(inUrl).trim(); 594 595 // URLs and site specific search shortcuts are handled by the regular flow of control, so 596 // return early. 597 if (Patterns.WEB_URL.matcher(url).matches() 598 || ACCEPTED_URI_SCHEMA.matcher(url).matches() 599 || parseUrlShortcut(url) != SHORTCUT_INVALID) { 600 return false; 601 } 602 603 Browser.updateVisitedHistory(mResolver, url, false); 604 Browser.addSearchUrl(mResolver, url); 605 606 Intent intent = new Intent(Intent.ACTION_WEB_SEARCH); 607 intent.addCategory(Intent.CATEGORY_DEFAULT); 608 intent.putExtra(SearchManager.QUERY, url); 609 if (appData != null) { 610 intent.putExtra(SearchManager.APP_DATA, appData); 611 } 612 if (extraData != null) { 613 intent.putExtra(SearchManager.EXTRA_DATA_KEY, extraData); 614 } 615 intent.putExtra(Browser.EXTRA_APPLICATION_ID, getPackageName()); 616 startActivity(intent); 617 618 return true; 619 } 620 621 private UrlData getUrlDataFromIntent(Intent intent) { 622 String url = null; 623 if (intent != null) { 624 final String action = intent.getAction(); 625 if (Intent.ACTION_VIEW.equals(action)) { 626 url = smartUrlFilter(intent.getData()); 627 if (url != null && url.startsWith("content:")) { 628 /* Append mimetype so webview knows how to display */ 629 String mimeType = intent.resolveType(getContentResolver()); 630 if (mimeType != null) { 631 url += "?" + mimeType; 632 } 633 } 634 if ("inline:".equals(url)) { 635 return new InlinedUrlData( 636 intent.getStringExtra(Browser.EXTRA_INLINE_CONTENT), 637 intent.getType(), 638 intent.getStringExtra(Browser.EXTRA_INLINE_ENCODING), 639 intent.getStringExtra(Browser.EXTRA_INLINE_FAILURL)); 640 } 641 } else if (Intent.ACTION_SEARCH.equals(action) 642 || MediaStore.INTENT_ACTION_MEDIA_SEARCH.equals(action) 643 || Intent.ACTION_WEB_SEARCH.equals(action)) { 644 url = intent.getStringExtra(SearchManager.QUERY); 645 if (url != null) { 646 mLastEnteredUrl = url; 647 Browser.updateVisitedHistory(mResolver, url, false); 648 // In general, we shouldn't modify URL from Intent. 649 // But currently, we get the user-typed URL from search box as well. 650 url = fixUrl(url); 651 url = smartUrlFilter(url); 652 String searchSource = "&source=android-" + GOOGLE_SEARCH_SOURCE_SUGGEST + "&"; 653 if (url.contains(searchSource)) { 654 String source = null; 655 final Bundle appData = intent.getBundleExtra(SearchManager.APP_DATA); 656 if (appData != null) { 657 source = appData.getString(SearchManager.SOURCE); 658 } 659 if (TextUtils.isEmpty(source)) { 660 source = GOOGLE_SEARCH_SOURCE_UNKNOWN; 661 } 662 url = url.replace(searchSource, "&source=android-"+source+"&"); 663 } 664 } 665 } 666 } 667 return new UrlData(url); 668 } 669 670 /* package */ static String fixUrl(String inUrl) { 671 // FIXME: Converting the url to lower case 672 // duplicates functionality in smartUrlFilter(). 673 // However, changing all current callers of fixUrl to 674 // call smartUrlFilter in addition may have unwanted 675 // consequences, and is deferred for now. 676 int colon = inUrl.indexOf(':'); 677 boolean allLower = true; 678 for (int index = 0; index < colon; index++) { 679 char ch = inUrl.charAt(index); 680 if (!Character.isLetter(ch)) { 681 break; 682 } 683 allLower &= Character.isLowerCase(ch); 684 if (index == colon - 1 && !allLower) { 685 inUrl = inUrl.substring(0, colon).toLowerCase() 686 + inUrl.substring(colon); 687 } 688 } 689 if (inUrl.startsWith("http://") || inUrl.startsWith("https://")) 690 return inUrl; 691 if (inUrl.startsWith("http:") || 692 inUrl.startsWith("https:")) { 693 if (inUrl.startsWith("http:/") || inUrl.startsWith("https:/")) { 694 inUrl = inUrl.replaceFirst("/", "//"); 695 } else inUrl = inUrl.replaceFirst(":", "://"); 696 } 697 return inUrl; 698 } 699 700 @Override 701 protected void onResume() { 702 super.onResume(); 703 if (LOGV_ENABLED) { 704 Log.v(LOGTAG, "BrowserActivity.onResume: this=" + this); 705 } 706 707 if (!mActivityInPause) { 708 Log.e(LOGTAG, "BrowserActivity is already resumed."); 709 return; 710 } 711 712 mTabControl.resumeCurrentTab(); 713 mActivityInPause = false; 714 resumeWebViewTimers(); 715 716 if (mWakeLock.isHeld()) { 717 mHandler.removeMessages(RELEASE_WAKELOCK); 718 mWakeLock.release(); 719 } 720 721 if (mCredsDlg != null) { 722 if (!mHandler.hasMessages(CANCEL_CREDS_REQUEST)) { 723 // In case credential request never comes back 724 mHandler.sendEmptyMessageDelayed(CANCEL_CREDS_REQUEST, 6000); 725 } 726 } 727 728 registerReceiver(mNetworkStateIntentReceiver, 729 mNetworkStateChangedFilter); 730 WebView.enablePlatformNotifications(); 731 } 732 733 /** 734 * Since the actual title bar is embedded in the WebView, and removing it 735 * would change its appearance, use a different TitleBar to show overlayed 736 * at the top of the screen, when the menu is open or the page is loading. 737 */ 738 private TitleBar mFakeTitleBar; 739 740 /** 741 * Holder for the fake title bar. It will have a foreground shadow, as well 742 * as a white background, so the fake title bar looks like the real one. 743 */ 744 private ViewGroup mFakeTitleBarHolder; 745 746 /** 747 * Layout parameters for the fake title bar within mFakeTitleBarHolder 748 */ 749 private FrameLayout.LayoutParams mFakeTitleBarParams 750 = new FrameLayout.LayoutParams( 751 ViewGroup.LayoutParams.FILL_PARENT, 752 ViewGroup.LayoutParams.WRAP_CONTENT); 753 /** 754 * Keeps track of whether the options menu is open. This is important in 755 * determining whether to show or hide the title bar overlay. 756 */ 757 private boolean mOptionsMenuOpen; 758 759 /** 760 * Only meaningful when mOptionsMenuOpen is true. This variable keeps track 761 * of whether the configuration has changed. The first onMenuOpened call 762 * after a configuration change is simply a reopening of the same menu 763 * (i.e. mIconView did not change). 764 */ 765 private boolean mConfigChanged; 766 767 /** 768 * Whether or not the options menu is in its smaller, icon menu form. When 769 * true, we want the title bar overlay to be up. When false, we do not. 770 * Only meaningful if mOptionsMenuOpen is true. 771 */ 772 private boolean mIconView; 773 774 @Override 775 public boolean onMenuOpened(int featureId, Menu menu) { 776 if (Window.FEATURE_OPTIONS_PANEL == featureId) { 777 if (mOptionsMenuOpen) { 778 if (mConfigChanged) { 779 // We do not need to make any changes to the state of the 780 // title bar, since the only thing that happened was a 781 // change in orientation 782 mConfigChanged = false; 783 } else { 784 if (mIconView) { 785 // Switching the menu to expanded view, so hide the 786 // title bar. 787 hideFakeTitleBar(); 788 mIconView = false; 789 } else { 790 // Switching the menu back to icon view, so show the 791 // title bar once again. 792 showFakeTitleBar(); 793 mIconView = true; 794 } 795 } 796 } else { 797 // The options menu is closed, so open it, and show the title 798 showFakeTitleBar(); 799 mOptionsMenuOpen = true; 800 mConfigChanged = false; 801 mIconView = true; 802 } 803 } 804 return true; 805 } 806 807 /** 808 * Special class used exclusively for the shadow drawn underneath the fake 809 * title bar. The shadow does not need to be drawn if the WebView 810 * underneath is scrolled to the top, because it will draw directly on top 811 * of the embedded shadow. 812 */ 813 private static class Shadow extends View { 814 private WebView mWebView; 815 816 public Shadow(Context context, AttributeSet attrs) { 817 super(context, attrs); 818 } 819 820 public void setWebView(WebView view) { 821 mWebView = view; 822 } 823 824 @Override 825 public void draw(Canvas canvas) { 826 // In general onDraw is the method to override, but we care about 827 // whether or not the background gets drawn, which happens in draw() 828 if (mWebView == null || mWebView.getScrollY() > getHeight()) { 829 super.draw(canvas); 830 } 831 // Need to invalidate so that if the scroll position changes, we 832 // still draw as appropriate. 833 invalidate(); 834 } 835 } 836 837 private void showFakeTitleBar() { 838 final View decor = getWindow().peekDecorView(); 839 if (mFakeTitleBar.getParent() == null && mActiveTabsPage == null 840 && !mActivityInPause && decor != null 841 && decor.getWindowToken() != null) { 842 Rect visRect = new Rect(); 843 if (!mBrowserFrameLayout.getGlobalVisibleRect(visRect)) { 844 if (LOGD_ENABLED) { 845 Log.d(LOGTAG, "showFakeTitleBar visRect failed"); 846 } 847 return; 848 } 849 850 WindowManager manager 851 = (WindowManager) getSystemService(Context.WINDOW_SERVICE); 852 853 // Add the title bar to the window manager so it can receive touches 854 // while the menu is up 855 WindowManager.LayoutParams params 856 = new WindowManager.LayoutParams( 857 ViewGroup.LayoutParams.FILL_PARENT, 858 ViewGroup.LayoutParams.WRAP_CONTENT, 859 WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL, 860 WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, 861 PixelFormat.TRANSLUCENT); 862 params.gravity = Gravity.TOP; 863 WebView mainView = mTabControl.getCurrentWebView(); 864 boolean atTop = mainView != null && mainView.getScrollY() == 0; 865 params.windowAnimations = atTop ? 0 : R.style.TitleBar; 866 // XXX : Without providing an offset, the fake title bar will be 867 // placed underneath the status bar. Use the global visible rect 868 // of mBrowserFrameLayout to determine the bottom of the status bar 869 params.y = visRect.top; 870 // Add a holder for the title bar. It also holds a shadow to show 871 // below the title bar. 872 if (mFakeTitleBarHolder == null) { 873 mFakeTitleBarHolder = (ViewGroup) LayoutInflater.from(this) 874 .inflate(R.layout.title_bar_bg, null); 875 } 876 Shadow shadow = (Shadow) mFakeTitleBarHolder.findViewById( 877 R.id.shadow); 878 shadow.setWebView(mainView); 879 mFakeTitleBarHolder.addView(mFakeTitleBar, 0, mFakeTitleBarParams); 880 manager.addView(mFakeTitleBarHolder, params); 881 } 882 } 883 884 @Override 885 public void onOptionsMenuClosed(Menu menu) { 886 mOptionsMenuOpen = false; 887 if (!mInLoad) { 888 hideFakeTitleBar(); 889 } else if (!mIconView) { 890 // The page is currently loading, and we are in expanded mode, so 891 // we were not showing the menu. Show it once again. It will be 892 // removed when the page finishes. 893 showFakeTitleBar(); 894 } 895 } 896 897 private void hideFakeTitleBar() { 898 if (mFakeTitleBar.getParent() == null) return; 899 WindowManager.LayoutParams params = (WindowManager.LayoutParams) 900 mFakeTitleBarHolder.getLayoutParams(); 901 WebView mainView = mTabControl.getCurrentWebView(); 902 // Although we decided whether or not to animate based on the current 903 // scroll position, the scroll position may have changed since the 904 // fake title bar was displayed. Make sure it has the appropriate 905 // animation/lack thereof before removing. 906 params.windowAnimations = mainView != null && mainView.getScrollY() == 0 907 ? 0 : R.style.TitleBar; 908 WindowManager manager 909 = (WindowManager) getSystemService(Context.WINDOW_SERVICE); 910 manager.updateViewLayout(mFakeTitleBarHolder, params); 911 mFakeTitleBarHolder.removeView(mFakeTitleBar); 912 manager.removeView(mFakeTitleBarHolder); 913 } 914 915 /** 916 * Special method for the fake title bar to call when displaying its context 917 * menu, since it is in its own Window, and its parent does not show a 918 * context menu. 919 */ 920 /* package */ void showTitleBarContextMenu() { 921 if (null == mTitleBar.getParent()) { 922 return; 923 } 924 openContextMenu(mTitleBar); 925 } 926 927 @Override 928 public void onContextMenuClosed(Menu menu) { 929 super.onContextMenuClosed(menu); 930 if (mInLoad) { 931 showFakeTitleBar(); 932 } 933 } 934 935 /** 936 * onSaveInstanceState(Bundle map) 937 * onSaveInstanceState is called right before onStop(). The map contains 938 * the saved state. 939 */ 940 @Override 941 protected void onSaveInstanceState(Bundle outState) { 942 if (LOGV_ENABLED) { 943 Log.v(LOGTAG, "BrowserActivity.onSaveInstanceState: this=" + this); 944 } 945 // the default implementation requires each view to have an id. As the 946 // browser handles the state itself and it doesn't use id for the views, 947 // don't call the default implementation. Otherwise it will trigger the 948 // warning like this, "couldn't save which view has focus because the 949 // focused view XXX has no id". 950 951 // Save all the tabs 952 mTabControl.saveState(outState); 953 } 954 955 @Override 956 protected void onPause() { 957 super.onPause(); 958 959 if (mActivityInPause) { 960 Log.e(LOGTAG, "BrowserActivity is already paused."); 961 return; 962 } 963 964 mTabControl.pauseCurrentTab(); 965 mActivityInPause = true; 966 if (mTabControl.getCurrentIndex() >= 0 && !pauseWebViewTimers()) { 967 mWakeLock.acquire(); 968 mHandler.sendMessageDelayed(mHandler 969 .obtainMessage(RELEASE_WAKELOCK), WAKELOCK_TIMEOUT); 970 } 971 972 // Clear the credentials toast if it is up 973 if (mCredsDlg != null && mCredsDlg.isShowing()) { 974 mCredsDlg.dismiss(); 975 } 976 mCredsDlg = null; 977 978 // FIXME: This removes the active tabs page and resets the menu to 979 // MAIN_MENU. A better solution might be to do this work in onNewIntent 980 // but then we would need to save it in onSaveInstanceState and restore 981 // it in onCreate/onRestoreInstanceState 982 if (mActiveTabsPage != null) { 983 removeActiveTabPage(true); 984 } 985 986 cancelStopToast(); 987 988 // unregister network state listener 989 unregisterReceiver(mNetworkStateIntentReceiver); 990 WebView.disablePlatformNotifications(); 991 } 992 993 @Override 994 protected void onDestroy() { 995 if (LOGV_ENABLED) { 996 Log.v(LOGTAG, "BrowserActivity.onDestroy: this=" + this); 997 } 998 super.onDestroy(); 999 1000 if (mUploadMessage != null) { 1001 mUploadMessage.onReceiveValue(null); 1002 mUploadMessage = null; 1003 } 1004 1005 if (mTabControl == null) return; 1006 1007 // Remove the fake title bar if it is there 1008 hideFakeTitleBar(); 1009 1010 // Remove the current tab and sub window 1011 Tab t = mTabControl.getCurrentTab(); 1012 if (t != null) { 1013 dismissSubWindow(t); 1014 removeTabFromContentView(t); 1015 } 1016 // Destroy all the tabs 1017 mTabControl.destroy(); 1018 WebIconDatabase.getInstance().close(); 1019 if (mGlsConnection != null) { 1020 unbindService(mGlsConnection); 1021 mGlsConnection = null; 1022 } 1023 1024 unregisterReceiver(mPackageInstallationReceiver); 1025 } 1026 1027 @Override 1028 public void onConfigurationChanged(Configuration newConfig) { 1029 mConfigChanged = true; 1030 super.onConfigurationChanged(newConfig); 1031 1032 if (mPageInfoDialog != null) { 1033 mPageInfoDialog.dismiss(); 1034 showPageInfo( 1035 mPageInfoView, 1036 mPageInfoFromShowSSLCertificateOnError.booleanValue()); 1037 } 1038 if (mSSLCertificateDialog != null) { 1039 mSSLCertificateDialog.dismiss(); 1040 showSSLCertificate( 1041 mSSLCertificateView); 1042 } 1043 if (mSSLCertificateOnErrorDialog != null) { 1044 mSSLCertificateOnErrorDialog.dismiss(); 1045 showSSLCertificateOnError( 1046 mSSLCertificateOnErrorView, 1047 mSSLCertificateOnErrorHandler, 1048 mSSLCertificateOnErrorError); 1049 } 1050 if (mHttpAuthenticationDialog != null) { 1051 String title = ((TextView) mHttpAuthenticationDialog 1052 .findViewById(com.android.internal.R.id.alertTitle)).getText() 1053 .toString(); 1054 String name = ((TextView) mHttpAuthenticationDialog 1055 .findViewById(R.id.username_edit)).getText().toString(); 1056 String password = ((TextView) mHttpAuthenticationDialog 1057 .findViewById(R.id.password_edit)).getText().toString(); 1058 int focusId = mHttpAuthenticationDialog.getCurrentFocus() 1059 .getId(); 1060 mHttpAuthenticationDialog.dismiss(); 1061 showHttpAuthentication(mHttpAuthHandler, null, null, title, 1062 name, password, focusId); 1063 } 1064 } 1065 1066 @Override 1067 public void onLowMemory() { 1068 super.onLowMemory(); 1069 mTabControl.freeMemory(); 1070 } 1071 1072 private boolean resumeWebViewTimers() { 1073 Tab tab = mTabControl.getCurrentTab(); 1074 boolean inLoad = tab.inLoad(); 1075 if ((!mActivityInPause && !inLoad) || (mActivityInPause && inLoad)) { 1076 CookieSyncManager.getInstance().startSync(); 1077 WebView w = tab.getWebView(); 1078 if (w != null) { 1079 w.resumeTimers(); 1080 } 1081 return true; 1082 } else { 1083 return false; 1084 } 1085 } 1086 1087 private boolean pauseWebViewTimers() { 1088 Tab tab = mTabControl.getCurrentTab(); 1089 boolean inLoad = tab.inLoad(); 1090 if (mActivityInPause && !inLoad) { 1091 CookieSyncManager.getInstance().stopSync(); 1092 WebView w = mTabControl.getCurrentWebView(); 1093 if (w != null) { 1094 w.pauseTimers(); 1095 } 1096 return true; 1097 } else { 1098 return false; 1099 } 1100 } 1101 1102 // FIXME: Do we want to call this when loading google for the first time? 1103 /* 1104 * This function is called when we are launching for the first time. We 1105 * are waiting for the login credentials before loading Google home 1106 * pages. This way the user will be logged in straight away. 1107 */ 1108 private void waitForCredentials() { 1109 // Show a toast 1110 mCredsDlg = new ProgressDialog(this); 1111 mCredsDlg.setIndeterminate(true); 1112 mCredsDlg.setMessage(getText(R.string.retrieving_creds_dlg_msg)); 1113 // If the user cancels the operation, then cancel the Google 1114 // Credentials request. 1115 mCredsDlg.setCancelMessage(mHandler.obtainMessage(CANCEL_CREDS_REQUEST)); 1116 mCredsDlg.show(); 1117 1118 // We set a timeout for the retrieval of credentials in onResume() 1119 // as that is when we have freed up some CPU time to get 1120 // the login credentials. 1121 } 1122 1123 /* 1124 * If we have received the credentials or we have timed out and we are 1125 * showing the credentials dialog, then it is time to move on. 1126 */ 1127 private void resumeAfterCredentials() { 1128 if (mCredsDlg == null) { 1129 return; 1130 } 1131 1132 // Clear the toast 1133 if (mCredsDlg.isShowing()) { 1134 mCredsDlg.dismiss(); 1135 } 1136 mCredsDlg = null; 1137 1138 // Clear any pending timeout 1139 mHandler.removeMessages(CANCEL_CREDS_REQUEST); 1140 1141 // Load the page 1142 WebView w = mTabControl.getCurrentWebView(); 1143 if (w != null) { 1144 w.loadUrl(mSettings.getHomePage()); 1145 } 1146 1147 // Update the settings, need to do this last as it can take a moment 1148 // to persist the settings. In the mean time we could be loading 1149 // content. 1150 mSettings.setLoginInitialized(this); 1151 } 1152 1153 // Open the icon database and retain all the icons for visited sites. 1154 private void retainIconsOnStartup() { 1155 final WebIconDatabase db = WebIconDatabase.getInstance(); 1156 db.open(getDir("icons", 0).getPath()); 1157 try { 1158 Cursor c = Browser.getAllBookmarks(mResolver); 1159 if (!c.moveToFirst()) { 1160 c.deactivate(); 1161 return; 1162 } 1163 int urlIndex = c.getColumnIndex(Browser.BookmarkColumns.URL); 1164 do { 1165 String url = c.getString(urlIndex); 1166 db.retainIconForPageUrl(url); 1167 } while (c.moveToNext()); 1168 c.deactivate(); 1169 } catch (IllegalStateException e) { 1170 Log.e(LOGTAG, "retainIconsOnStartup", e); 1171 } 1172 } 1173 1174 // Helper method for getting the top window. 1175 WebView getTopWindow() { 1176 return mTabControl.getCurrentTopWebView(); 1177 } 1178 1179 TabControl getTabControl() { 1180 return mTabControl; 1181 } 1182 1183 @Override 1184 public boolean onCreateOptionsMenu(Menu menu) { 1185 super.onCreateOptionsMenu(menu); 1186 1187 MenuInflater inflater = getMenuInflater(); 1188 inflater.inflate(R.menu.browser, menu); 1189 mMenu = menu; 1190 updateInLoadMenuItems(); 1191 return true; 1192 } 1193 1194 /** 1195 * As the menu can be open when loading state changes 1196 * we must manually update the state of the stop/reload menu 1197 * item 1198 */ 1199 private void updateInLoadMenuItems() { 1200 if (mMenu == null) { 1201 return; 1202 } 1203 MenuItem src = mInLoad ? 1204 mMenu.findItem(R.id.stop_menu_id): 1205 mMenu.findItem(R.id.reload_menu_id); 1206 MenuItem dest = mMenu.findItem(R.id.stop_reload_menu_id); 1207 dest.setIcon(src.getIcon()); 1208 dest.setTitle(src.getTitle()); 1209 } 1210 1211 @Override 1212 public boolean onContextItemSelected(MenuItem item) { 1213 // chording is not an issue with context menus, but we use the same 1214 // options selector, so set mCanChord to true so we can access them. 1215 mCanChord = true; 1216 int id = item.getItemId(); 1217 switch (id) { 1218 // For the context menu from the title bar 1219 case R.id.title_bar_share_page_url: 1220 case R.id.title_bar_copy_page_url: 1221 WebView mainView = mTabControl.getCurrentWebView(); 1222 if (null == mainView) { 1223 return false; 1224 } 1225 if (id == R.id.title_bar_share_page_url) { 1226 Browser.sendString(this, mainView.getUrl()); 1227 } else { 1228 copy(mainView.getUrl()); 1229 } 1230 break; 1231 // -- Browser context menu 1232 case R.id.open_context_menu_id: 1233 case R.id.open_newtab_context_menu_id: 1234 case R.id.bookmark_context_menu_id: 1235 case R.id.save_link_context_menu_id: 1236 case R.id.share_link_context_menu_id: 1237 case R.id.copy_link_context_menu_id: 1238 final WebView webView = getTopWindow(); 1239 if (null == webView) { 1240 return false; 1241 } 1242 final HashMap hrefMap = new HashMap(); 1243 hrefMap.put("webview", webView); 1244 final Message msg = mHandler.obtainMessage( 1245 FOCUS_NODE_HREF, id, 0, hrefMap); 1246 webView.requestFocusNodeHref(msg); 1247 break; 1248 1249 default: 1250 // For other context menus 1251 return onOptionsItemSelected(item); 1252 } 1253 mCanChord = false; 1254 return true; 1255 } 1256 1257 private Bundle createGoogleSearchSourceBundle(String source) { 1258 Bundle bundle = new Bundle(); 1259 bundle.putString(SearchManager.SOURCE, source); 1260 return bundle; 1261 } 1262 1263 /** 1264 * Overriding this to insert a local information bundle 1265 */ 1266 @Override 1267 public boolean onSearchRequested() { 1268 if (mOptionsMenuOpen) closeOptionsMenu(); 1269 String url = (getTopWindow() == null) ? null : getTopWindow().getUrl(); 1270 startSearch(mSettings.getHomePage().equals(url) ? null : url, true, 1271 createGoogleSearchSourceBundle(GOOGLE_SEARCH_SOURCE_SEARCHKEY), false); 1272 return true; 1273 } 1274 1275 @Override 1276 public void startSearch(String initialQuery, boolean selectInitialQuery, 1277 Bundle appSearchData, boolean globalSearch) { 1278 if (appSearchData == null) { 1279 appSearchData = createGoogleSearchSourceBundle(GOOGLE_SEARCH_SOURCE_TYPE); 1280 } 1281 super.startSearch(initialQuery, selectInitialQuery, appSearchData, globalSearch); 1282 } 1283 1284 /** 1285 * Switch tabs. Called by the TitleBarSet when sliding the title bar 1286 * results in changing tabs. 1287 * @param index Index of the tab to change to, as defined by 1288 * mTabControl.getTabIndex(Tab t). 1289 * @return boolean True if we successfully switched to a different tab. If 1290 * the indexth tab is null, or if that tab is the same as 1291 * the current one, return false. 1292 */ 1293 /* package */ boolean switchToTab(int index) { 1294 Tab tab = mTabControl.getTab(index); 1295 Tab currentTab = mTabControl.getCurrentTab(); 1296 if (tab == null || tab == currentTab) { 1297 return false; 1298 } 1299 if (currentTab != null) { 1300 // currentTab may be null if it was just removed. In that case, 1301 // we do not need to remove it 1302 removeTabFromContentView(currentTab); 1303 } 1304 mTabControl.setCurrentTab(tab); 1305 attachTabToContentView(tab); 1306 resetTitleIconAndProgress(); 1307 updateLockIconToLatest(); 1308 return true; 1309 } 1310 1311 /* package */ Tab openTabToHomePage() { 1312 return openTabAndShow(mSettings.getHomePage(), false, null); 1313 } 1314 1315 /* package */ void closeCurrentWindow() { 1316 final Tab current = mTabControl.getCurrentTab(); 1317 if (mTabControl.getTabCount() == 1) { 1318 // This is the last tab. Open a new one, with the home 1319 // page and close the current one. 1320 openTabToHomePage(); 1321 closeTab(current); 1322 return; 1323 } 1324 final Tab parent = current.getParentTab(); 1325 int indexToShow = -1; 1326 if (parent != null) { 1327 indexToShow = mTabControl.getTabIndex(parent); 1328 } else { 1329 final int currentIndex = mTabControl.getCurrentIndex(); 1330 // Try to move to the tab to the right 1331 indexToShow = currentIndex + 1; 1332 if (indexToShow > mTabControl.getTabCount() - 1) { 1333 // Try to move to the tab to the left 1334 indexToShow = currentIndex - 1; 1335 } 1336 } 1337 if (switchToTab(indexToShow)) { 1338 // Close window 1339 closeTab(current); 1340 } 1341 } 1342 1343 private ActiveTabsPage mActiveTabsPage; 1344 1345 /** 1346 * Remove the active tabs page. 1347 * @param needToAttach If true, the active tabs page did not attach a tab 1348 * to the content view, so we need to do that here. 1349 */ 1350 /* package */ void removeActiveTabPage(boolean needToAttach) { 1351 mContentView.removeView(mActiveTabsPage); 1352 mActiveTabsPage = null; 1353 mMenuState = R.id.MAIN_MENU; 1354 if (needToAttach) { 1355 attachTabToContentView(mTabControl.getCurrentTab()); 1356 } 1357 getTopWindow().requestFocus(); 1358 } 1359 1360 @Override 1361 public boolean onOptionsItemSelected(MenuItem item) { 1362 if (!mCanChord) { 1363 // The user has already fired a shortcut with this hold down of the 1364 // menu key. 1365 return false; 1366 } 1367 if (null == getTopWindow()) { 1368 return false; 1369 } 1370 if (mMenuIsDown) { 1371 // The shortcut action consumes the MENU. Even if it is still down, 1372 // it won't trigger the next shortcut action. In the case of the 1373 // shortcut action triggering a new activity, like Bookmarks, we 1374 // won't get onKeyUp for MENU. So it is important to reset it here. 1375 mMenuIsDown = false; 1376 } 1377 switch (item.getItemId()) { 1378 // -- Main menu 1379 case R.id.new_tab_menu_id: 1380 openTabToHomePage(); 1381 break; 1382 1383 case R.id.goto_menu_id: 1384 onSearchRequested(); 1385 break; 1386 1387 case R.id.bookmarks_menu_id: 1388 bookmarksOrHistoryPicker(false); 1389 break; 1390 1391 case R.id.active_tabs_menu_id: 1392 mActiveTabsPage = new ActiveTabsPage(this, mTabControl); 1393 removeTabFromContentView(mTabControl.getCurrentTab()); 1394 hideFakeTitleBar(); 1395 mContentView.addView(mActiveTabsPage, COVER_SCREEN_PARAMS); 1396 mActiveTabsPage.requestFocus(); 1397 mMenuState = EMPTY_MENU; 1398 break; 1399 1400 case R.id.add_bookmark_menu_id: 1401 Intent i = new Intent(BrowserActivity.this, 1402 AddBookmarkPage.class); 1403 WebView w = getTopWindow(); 1404 i.putExtra("url", w.getUrl()); 1405 i.putExtra("title", w.getTitle()); 1406 i.putExtra("touch_icon_url", w.getTouchIconUrl()); 1407 i.putExtra("thumbnail", createScreenshot(w)); 1408 startActivity(i); 1409 break; 1410 1411 case R.id.stop_reload_menu_id: 1412 if (mInLoad) { 1413 stopLoading(); 1414 } else { 1415 getTopWindow().reload(); 1416 } 1417 break; 1418 1419 case R.id.back_menu_id: 1420 getTopWindow().goBack(); 1421 break; 1422 1423 case R.id.forward_menu_id: 1424 getTopWindow().goForward(); 1425 break; 1426 1427 case R.id.close_menu_id: 1428 // Close the subwindow if it exists. 1429 if (mTabControl.getCurrentSubWindow() != null) { 1430 dismissSubWindow(mTabControl.getCurrentTab()); 1431 break; 1432 } 1433 closeCurrentWindow(); 1434 break; 1435 1436 case R.id.homepage_menu_id: 1437 Tab current = mTabControl.getCurrentTab(); 1438 if (current != null) { 1439 dismissSubWindow(current); 1440 current.getWebView().loadUrl(mSettings.getHomePage()); 1441 } 1442 break; 1443 1444 case R.id.preferences_menu_id: 1445 Intent intent = new Intent(this, 1446 BrowserPreferencesPage.class); 1447 intent.putExtra(BrowserPreferencesPage.CURRENT_PAGE, 1448 getTopWindow().getUrl()); 1449 startActivityForResult(intent, PREFERENCES_PAGE); 1450 break; 1451 1452 case R.id.find_menu_id: 1453 if (null == mFindDialog) { 1454 mFindDialog = new FindDialog(this); 1455 } 1456 mFindDialog.setWebView(getTopWindow()); 1457 mFindDialog.show(); 1458 mMenuState = EMPTY_MENU; 1459 break; 1460 1461 case R.id.select_text_id: 1462 getTopWindow().emulateShiftHeld(); 1463 break; 1464 case R.id.page_info_menu_id: 1465 showPageInfo(mTabControl.getCurrentTab(), false); 1466 break; 1467 1468 case R.id.classic_history_menu_id: 1469 bookmarksOrHistoryPicker(true); 1470 break; 1471 1472 case R.id.share_page_menu_id: 1473 Browser.sendString(this, getTopWindow().getUrl(), 1474 getText(R.string.choosertitle_sharevia).toString()); 1475 break; 1476 1477 case R.id.dump_nav_menu_id: 1478 getTopWindow().debugDump(); 1479 break; 1480 1481 case R.id.zoom_in_menu_id: 1482 getTopWindow().zoomIn(); 1483 break; 1484 1485 case R.id.zoom_out_menu_id: 1486 getTopWindow().zoomOut(); 1487 break; 1488 1489 case R.id.view_downloads_menu_id: 1490 viewDownloads(null); 1491 break; 1492 1493 case R.id.window_one_menu_id: 1494 case R.id.window_two_menu_id: 1495 case R.id.window_three_menu_id: 1496 case R.id.window_four_menu_id: 1497 case R.id.window_five_menu_id: 1498 case R.id.window_six_menu_id: 1499 case R.id.window_seven_menu_id: 1500 case R.id.window_eight_menu_id: 1501 { 1502 int menuid = item.getItemId(); 1503 for (int id = 0; id < WINDOW_SHORTCUT_ID_ARRAY.length; id++) { 1504 if (WINDOW_SHORTCUT_ID_ARRAY[id] == menuid) { 1505 Tab desiredTab = mTabControl.getTab(id); 1506 if (desiredTab != null && 1507 desiredTab != mTabControl.getCurrentTab()) { 1508 switchToTab(id); 1509 } 1510 break; 1511 } 1512 } 1513 } 1514 break; 1515 1516 default: 1517 if (!super.onOptionsItemSelected(item)) { 1518 return false; 1519 } 1520 // Otherwise fall through. 1521 } 1522 mCanChord = false; 1523 return true; 1524 } 1525 1526 public void closeFind() { 1527 mMenuState = R.id.MAIN_MENU; 1528 } 1529 1530 @Override 1531 public boolean onPrepareOptionsMenu(Menu menu) { 1532 // This happens when the user begins to hold down the menu key, so 1533 // allow them to chord to get a shortcut. 1534 mCanChord = true; 1535 // Note: setVisible will decide whether an item is visible; while 1536 // setEnabled() will decide whether an item is enabled, which also means 1537 // whether the matching shortcut key will function. 1538 super.onPrepareOptionsMenu(menu); 1539 switch (mMenuState) { 1540 case EMPTY_MENU: 1541 if (mCurrentMenuState != mMenuState) { 1542 menu.setGroupVisible(R.id.MAIN_MENU, false); 1543 menu.setGroupEnabled(R.id.MAIN_MENU, false); 1544 menu.setGroupEnabled(R.id.MAIN_SHORTCUT_MENU, false); 1545 } 1546 break; 1547 default: 1548 if (mCurrentMenuState != mMenuState) { 1549 menu.setGroupVisible(R.id.MAIN_MENU, true); 1550 menu.setGroupEnabled(R.id.MAIN_MENU, true); 1551 menu.setGroupEnabled(R.id.MAIN_SHORTCUT_MENU, true); 1552 } 1553 final WebView w = getTopWindow(); 1554 boolean canGoBack = false; 1555 boolean canGoForward = false; 1556 boolean isHome = false; 1557 if (w != null) { 1558 canGoBack = w.canGoBack(); 1559 canGoForward = w.canGoForward(); 1560 isHome = mSettings.getHomePage().equals(w.getUrl()); 1561 } 1562 final MenuItem back = menu.findItem(R.id.back_menu_id); 1563 back.setEnabled(canGoBack); 1564 1565 final MenuItem home = menu.findItem(R.id.homepage_menu_id); 1566 home.setEnabled(!isHome); 1567 1568 menu.findItem(R.id.forward_menu_id) 1569 .setEnabled(canGoForward); 1570 1571 menu.findItem(R.id.new_tab_menu_id).setEnabled( 1572 mTabControl.canCreateNewTab()); 1573 1574 // decide whether to show the share link option 1575 PackageManager pm = getPackageManager(); 1576 Intent send = new Intent(Intent.ACTION_SEND); 1577 send.setType("text/plain"); 1578 ResolveInfo ri = pm.resolveActivity(send, PackageManager.MATCH_DEFAULT_ONLY); 1579 menu.findItem(R.id.share_page_menu_id).setVisible(ri != null); 1580 1581 boolean isNavDump = mSettings.isNavDump(); 1582 final MenuItem nav = menu.findItem(R.id.dump_nav_menu_id); 1583 nav.setVisible(isNavDump); 1584 nav.setEnabled(isNavDump); 1585 break; 1586 } 1587 mCurrentMenuState = mMenuState; 1588 return true; 1589 } 1590 1591 @Override 1592 public void onCreateContextMenu(ContextMenu menu, View v, 1593 ContextMenuInfo menuInfo) { 1594 WebView webview = (WebView) v; 1595 WebView.HitTestResult result = webview.getHitTestResult(); 1596 if (result == null) { 1597 return; 1598 } 1599 1600 int type = result.getType(); 1601 if (type == WebView.HitTestResult.UNKNOWN_TYPE) { 1602 Log.w(LOGTAG, 1603 "We should not show context menu when nothing is touched"); 1604 return; 1605 } 1606 if (type == WebView.HitTestResult.EDIT_TEXT_TYPE) { 1607 // let TextView handles context menu 1608 return; 1609 } 1610 1611 // Note, http://b/issue?id=1106666 is requesting that 1612 // an inflated menu can be used again. This is not available 1613 // yet, so inflate each time (yuk!) 1614 MenuInflater inflater = getMenuInflater(); 1615 inflater.inflate(R.menu.browsercontext, menu); 1616 1617 // Show the correct menu group 1618 String extra = result.getExtra(); 1619 menu.setGroupVisible(R.id.PHONE_MENU, 1620 type == WebView.HitTestResult.PHONE_TYPE); 1621 menu.setGroupVisible(R.id.EMAIL_MENU, 1622 type == WebView.HitTestResult.EMAIL_TYPE); 1623 menu.setGroupVisible(R.id.GEO_MENU, 1624 type == WebView.HitTestResult.GEO_TYPE); 1625 menu.setGroupVisible(R.id.IMAGE_MENU, 1626 type == WebView.HitTestResult.IMAGE_TYPE 1627 || type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE); 1628 menu.setGroupVisible(R.id.ANCHOR_MENU, 1629 type == WebView.HitTestResult.SRC_ANCHOR_TYPE 1630 || type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE); 1631 1632 // Setup custom handling depending on the type 1633 switch (type) { 1634 case WebView.HitTestResult.PHONE_TYPE: 1635 menu.setHeaderTitle(Uri.decode(extra)); 1636 menu.findItem(R.id.dial_context_menu_id).setIntent( 1637 new Intent(Intent.ACTION_VIEW, Uri 1638 .parse(WebView.SCHEME_TEL + extra))); 1639 Intent addIntent = new Intent(Intent.ACTION_INSERT_OR_EDIT); 1640 addIntent.putExtra(Insert.PHONE, Uri.decode(extra)); 1641 addIntent.setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE); 1642 menu.findItem(R.id.add_contact_context_menu_id).setIntent( 1643 addIntent); 1644 menu.findItem(R.id.copy_phone_context_menu_id).setOnMenuItemClickListener( 1645 new Copy(extra)); 1646 break; 1647 1648 case WebView.HitTestResult.EMAIL_TYPE: 1649 menu.setHeaderTitle(extra); 1650 menu.findItem(R.id.email_context_menu_id).setIntent( 1651 new Intent(Intent.ACTION_VIEW, Uri 1652 .parse(WebView.SCHEME_MAILTO + extra))); 1653 menu.findItem(R.id.copy_mail_context_menu_id).setOnMenuItemClickListener( 1654 new Copy(extra)); 1655 break; 1656 1657 case WebView.HitTestResult.GEO_TYPE: 1658 menu.setHeaderTitle(extra); 1659 menu.findItem(R.id.map_context_menu_id).setIntent( 1660 new Intent(Intent.ACTION_VIEW, Uri 1661 .parse(WebView.SCHEME_GEO 1662 + URLEncoder.encode(extra)))); 1663 menu.findItem(R.id.copy_geo_context_menu_id).setOnMenuItemClickListener( 1664 new Copy(extra)); 1665 break; 1666 1667 case WebView.HitTestResult.SRC_ANCHOR_TYPE: 1668 case WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE: 1669 TextView titleView = (TextView) LayoutInflater.from(this) 1670 .inflate(android.R.layout.browser_link_context_header, 1671 null); 1672 titleView.setText(extra); 1673 menu.setHeaderView(titleView); 1674 // decide whether to show the open link in new tab option 1675 menu.findItem(R.id.open_newtab_context_menu_id).setVisible( 1676 mTabControl.canCreateNewTab()); 1677 menu.findItem(R.id.bookmark_context_menu_id).setVisible( 1678 Bookmarks.urlHasAcceptableScheme(extra)); 1679 PackageManager pm = getPackageManager(); 1680 Intent send = new Intent(Intent.ACTION_SEND); 1681 send.setType("text/plain"); 1682 ResolveInfo ri = pm.resolveActivity(send, PackageManager.MATCH_DEFAULT_ONLY); 1683 menu.findItem(R.id.share_link_context_menu_id).setVisible(ri != null); 1684 if (type == WebView.HitTestResult.SRC_ANCHOR_TYPE) { 1685 break; 1686 } 1687 // otherwise fall through to handle image part 1688 case WebView.HitTestResult.IMAGE_TYPE: 1689 if (type == WebView.HitTestResult.IMAGE_TYPE) { 1690 menu.setHeaderTitle(extra); 1691 } 1692 menu.findItem(R.id.view_image_context_menu_id).setIntent( 1693 new Intent(Intent.ACTION_VIEW, Uri.parse(extra))); 1694 menu.findItem(R.id.download_context_menu_id). 1695 setOnMenuItemClickListener(new Download(extra)); 1696 break; 1697 1698 default: 1699 Log.w(LOGTAG, "We should not get here."); 1700 break; 1701 } 1702 hideFakeTitleBar(); 1703 } 1704 1705 // Attach the given tab to the content view. 1706 // this should only be called for the current tab. 1707 private void attachTabToContentView(Tab t) { 1708 // Attach the container that contains the main WebView and any other UI 1709 // associated with the tab. 1710 t.attachTabToContentView(mContentView); 1711 1712 if (mShouldShowErrorConsole) { 1713 ErrorConsoleView errorConsole = t.getErrorConsole(true); 1714 if (errorConsole.numberOfErrors() == 0) { 1715 errorConsole.showConsole(ErrorConsoleView.SHOW_NONE); 1716 } else { 1717 errorConsole.showConsole(ErrorConsoleView.SHOW_MINIMIZED); 1718 } 1719 1720 mErrorConsoleContainer.addView(errorConsole, 1721 new LinearLayout.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT, 1722 ViewGroup.LayoutParams.WRAP_CONTENT)); 1723 } 1724 1725 WebView view = t.getWebView(); 1726 view.setEmbeddedTitleBar(mTitleBar); 1727 // Request focus on the top window. 1728 t.getTopWindow().requestFocus(); 1729 } 1730 1731 // Attach a sub window to the main WebView of the given tab. 1732 void attachSubWindow(Tab t) { 1733 t.attachSubWindow(mContentView); 1734 getTopWindow().requestFocus(); 1735 } 1736 1737 // Remove the given tab from the content view. 1738 private void removeTabFromContentView(Tab t) { 1739 // Remove the container that contains the main WebView. 1740 t.removeTabFromContentView(mContentView); 1741 1742 ErrorConsoleView errorConsole = t.getErrorConsole(false); 1743 if (errorConsole != null) { 1744 mErrorConsoleContainer.removeView(errorConsole); 1745 } 1746 1747 WebView view = t.getWebView(); 1748 if (view != null) { 1749 view.setEmbeddedTitleBar(null); 1750 } 1751 } 1752 1753 // Remove the sub window if it exists. Also called by TabControl when the 1754 // user clicks the 'X' to dismiss a sub window. 1755 /* package */ void dismissSubWindow(Tab t) { 1756 t.removeSubWindow(mContentView); 1757 // dismiss the subwindow. This will destroy the WebView. 1758 t.dismissSubWindow(); 1759 getTopWindow().requestFocus(); 1760 } 1761 1762 // A wrapper function of {@link #openTabAndShow(UrlData, boolean, String)} 1763 // that accepts url as string. 1764 private Tab openTabAndShow(String url, boolean closeOnExit, String appId) { 1765 return openTabAndShow(new UrlData(url), closeOnExit, appId); 1766 } 1767 1768 // This method does a ton of stuff. It will attempt to create a new tab 1769 // if we haven't reached MAX_TABS. Otherwise it uses the current tab. If 1770 // url isn't null, it will load the given url. 1771 /* package */Tab openTabAndShow(UrlData urlData, boolean closeOnExit, 1772 String appId) { 1773 final Tab currentTab = mTabControl.getCurrentTab(); 1774 if (mTabControl.canCreateNewTab()) { 1775 final Tab tab = mTabControl.createNewTab(closeOnExit, appId, 1776 urlData.mUrl); 1777 WebView webview = tab.getWebView(); 1778 // If the last tab was removed from the active tabs page, currentTab 1779 // will be null. 1780 if (currentTab != null) { 1781 removeTabFromContentView(currentTab); 1782 } 1783 // We must set the new tab as the current tab to reflect the old 1784 // animation behavior. 1785 mTabControl.setCurrentTab(tab); 1786 attachTabToContentView(tab); 1787 if (!urlData.isEmpty()) { 1788 urlData.loadIn(webview); 1789 } 1790 return tab; 1791 } else { 1792 // Get rid of the subwindow if it exists 1793 dismissSubWindow(currentTab); 1794 if (!urlData.isEmpty()) { 1795 // Load the given url. 1796 urlData.loadIn(currentTab.getWebView()); 1797 } 1798 } 1799 return currentTab; 1800 } 1801 1802 private Tab openTab(String url) { 1803 if (mSettings.openInBackground()) { 1804 Tab t = mTabControl.createNewTab(); 1805 if (t != null) { 1806 WebView view = t.getWebView(); 1807 view.loadUrl(url); 1808 } 1809 return t; 1810 } else { 1811 return openTabAndShow(url, false, null); 1812 } 1813 } 1814 1815 private class Copy implements OnMenuItemClickListener { 1816 private CharSequence mText; 1817 1818 public boolean onMenuItemClick(MenuItem item) { 1819 copy(mText); 1820 return true; 1821 } 1822 1823 public Copy(CharSequence toCopy) { 1824 mText = toCopy; 1825 } 1826 } 1827 1828 private class Download implements OnMenuItemClickListener { 1829 private String mText; 1830 1831 public boolean onMenuItemClick(MenuItem item) { 1832 onDownloadStartNoStream(mText, null, null, null, -1); 1833 return true; 1834 } 1835 1836 public Download(String toDownload) { 1837 mText = toDownload; 1838 } 1839 } 1840 1841 private void copy(CharSequence text) { 1842 try { 1843 IClipboard clip = IClipboard.Stub.asInterface(ServiceManager.getService("clipboard")); 1844 if (clip != null) { 1845 clip.setClipboardText(text); 1846 } 1847 } catch (android.os.RemoteException e) { 1848 Log.e(LOGTAG, "Copy failed", e); 1849 } 1850 } 1851 1852 /** 1853 * Resets the browser title-view to whatever it must be 1854 * (for example, if we had a loading error) 1855 * When we have a new page, we call resetTitle, when we 1856 * have to reset the titlebar to whatever it used to be 1857 * (for example, if the user chose to stop loading), we 1858 * call resetTitleAndRevertLockIcon. 1859 */ 1860 /* package */ void resetTitleAndRevertLockIcon() { 1861 mTabControl.getCurrentTab().revertLockIcon(); 1862 updateLockIconToLatest(); 1863 resetTitleIconAndProgress(); 1864 } 1865 1866 /** 1867 * Reset the title, favicon, and progress. 1868 */ 1869 private void resetTitleIconAndProgress() { 1870 WebView current = mTabControl.getCurrentWebView(); 1871 if (current == null) { 1872 return; 1873 } 1874 resetTitleAndIcon(current); 1875 int progress = current.getProgress(); 1876 current.getWebChromeClient().onProgressChanged(current, progress); 1877 } 1878 1879 // Reset the title and the icon based on the given item. 1880 private void resetTitleAndIcon(WebView view) { 1881 WebHistoryItem item = view.copyBackForwardList().getCurrentItem(); 1882 if (item != null) { 1883 setUrlTitle(item.getUrl(), item.getTitle()); 1884 setFavicon(item.getFavicon()); 1885 } else { 1886 setUrlTitle(null, null); 1887 setFavicon(null); 1888 } 1889 } 1890 1891 /** 1892 * Sets a title composed of the URL and the title string. 1893 * @param url The URL of the site being loaded. 1894 * @param title The title of the site being loaded. 1895 */ 1896 void setUrlTitle(String url, String title) { 1897 mUrl = url; 1898 mTitle = title; 1899 1900 mTitleBar.setTitleAndUrl(title, url); 1901 mFakeTitleBar.setTitleAndUrl(title, url); 1902 } 1903 1904 /** 1905 * @param url The URL to build a title version of the URL from. 1906 * @return The title version of the URL or null if fails. 1907 * The title version of the URL can be either the URL hostname, 1908 * or the hostname with an "https://" prefix (for secure URLs), 1909 * or an empty string if, for example, the URL in question is a 1910 * file:// URL with no hostname. 1911 */ 1912 /* package */ static String buildTitleUrl(String url) { 1913 String titleUrl = null; 1914 1915 if (url != null) { 1916 try { 1917 // parse the url string 1918 URL urlObj = new URL(url); 1919 if (urlObj != null) { 1920 titleUrl = ""; 1921 1922 String protocol = urlObj.getProtocol(); 1923 String host = urlObj.getHost(); 1924 1925 if (host != null && 0 < host.length()) { 1926 titleUrl = host; 1927 if (protocol != null) { 1928 // if a secure site, add an "https://" prefix! 1929 if (protocol.equalsIgnoreCase("https")) { 1930 titleUrl = protocol + "://" + host; 1931 } 1932 } 1933 } 1934 } 1935 } catch (MalformedURLException e) {} 1936 } 1937 1938 return titleUrl; 1939 } 1940 1941 // Set the favicon in the title bar. 1942 void setFavicon(Bitmap icon) { 1943 mTitleBar.setFavicon(icon); 1944 mFakeTitleBar.setFavicon(icon); 1945 } 1946 1947 /** 1948 * Close the tab, remove its associated title bar, and adjust mTabControl's 1949 * current tab to a valid value. 1950 */ 1951 /* package */ void closeTab(Tab t) { 1952 int currentIndex = mTabControl.getCurrentIndex(); 1953 int removeIndex = mTabControl.getTabIndex(t); 1954 mTabControl.removeTab(t); 1955 if (currentIndex >= removeIndex && currentIndex != 0) { 1956 currentIndex--; 1957 } 1958 mTabControl.setCurrentTab(mTabControl.getTab(currentIndex)); 1959 resetTitleIconAndProgress(); 1960 } 1961 1962 private void goBackOnePageOrQuit() { 1963 Tab current = mTabControl.getCurrentTab(); 1964 if (current == null) { 1965 /* 1966 * Instead of finishing the activity, simply push this to the back 1967 * of the stack and let ActivityManager to choose the foreground 1968 * activity. As BrowserActivity is singleTask, it will be always the 1969 * root of the task. So we can use either true or false for 1970 * moveTaskToBack(). 1971 */ 1972 moveTaskToBack(true); 1973 return; 1974 } 1975 WebView w = current.getWebView(); 1976 if (w.canGoBack()) { 1977 w.goBack(); 1978 } else { 1979 // Check to see if we are closing a window that was created by 1980 // another window. If so, we switch back to that window. 1981 Tab parent = current.getParentTab(); 1982 if (parent != null) { 1983 switchToTab(mTabControl.getTabIndex(parent)); 1984 // Now we close the other tab 1985 closeTab(current); 1986 } else { 1987 if (current.closeOnExit()) { 1988 // force the tab's inLoad() to be false as we are going to 1989 // either finish the activity or remove the tab. This will 1990 // ensure pauseWebViewTimers() taking action. 1991 mTabControl.getCurrentTab().clearInLoad(); 1992 if (mTabControl.getTabCount() == 1) { 1993 finish(); 1994 return; 1995 } 1996 // call pauseWebViewTimers() now, we won't be able to call 1997 // it in onPause() as the WebView won't be valid. 1998 // Temporarily change mActivityInPause to be true as 1999 // pauseWebViewTimers() will do nothing if mActivityInPause 2000 // is false. 2001 boolean savedState = mActivityInPause; 2002 if (savedState) { 2003 Log.e(LOGTAG, "BrowserActivity is already paused " 2004 + "while handing goBackOnePageOrQuit."); 2005 } 2006 mActivityInPause = true; 2007 pauseWebViewTimers(); 2008 mActivityInPause = savedState; 2009 removeTabFromContentView(current); 2010 mTabControl.removeTab(current); 2011 } 2012 /* 2013 * Instead of finishing the activity, simply push this to the back 2014 * of the stack and let ActivityManager to choose the foreground 2015 * activity. As BrowserActivity is singleTask, it will be always the 2016 * root of the task. So we can use either true or false for 2017 * moveTaskToBack(). 2018 */ 2019 moveTaskToBack(true); 2020 } 2021 } 2022 } 2023 2024 boolean isMenuDown() { 2025 return mMenuIsDown; 2026 } 2027 2028 @Override 2029 public boolean onKeyDown(int keyCode, KeyEvent event) { 2030 // Even if MENU is already held down, we need to call to super to open 2031 // the IME on long press. 2032 if (KeyEvent.KEYCODE_MENU == keyCode) { 2033 mMenuIsDown = true; 2034 return super.onKeyDown(keyCode, event); 2035 } 2036 // The default key mode is DEFAULT_KEYS_SEARCH_LOCAL. As the MENU is 2037 // still down, we don't want to trigger the search. Pretend to consume 2038 // the key and do nothing. 2039 if (mMenuIsDown) return true; 2040 2041 switch(keyCode) { 2042 case KeyEvent.KEYCODE_SPACE: 2043 // WebView/WebTextView handle the keys in the KeyDown. As 2044 // the Activity's shortcut keys are only handled when WebView 2045 // doesn't, have to do it in onKeyDown instead of onKeyUp. 2046 if (event.isShiftPressed()) { 2047 getTopWindow().pageUp(false); 2048 } else { 2049 getTopWindow().pageDown(false); 2050 } 2051 return true; 2052 case KeyEvent.KEYCODE_BACK: 2053 if (event.getRepeatCount() == 0) { 2054 event.startTracking(); 2055 return true; 2056 } else if (mCustomView == null && mActiveTabsPage == null 2057 && event.isLongPress()) { 2058 bookmarksOrHistoryPicker(true); 2059 return true; 2060 } 2061 break; 2062 } 2063 return super.onKeyDown(keyCode, event); 2064 } 2065 2066 @Override 2067 public boolean onKeyUp(int keyCode, KeyEvent event) { 2068 switch(keyCode) { 2069 case KeyEvent.KEYCODE_MENU: 2070 mMenuIsDown = false; 2071 break; 2072 case KeyEvent.KEYCODE_BACK: 2073 if (event.isTracking() && !event.isCanceled()) { 2074 if (mCustomView != null) { 2075 // if a custom view is showing, hide it 2076 mTabControl.getCurrentWebView().getWebChromeClient() 2077 .onHideCustomView(); 2078 } else if (mActiveTabsPage != null) { 2079 // if tab page is showing, hide it 2080 removeActiveTabPage(true); 2081 } else { 2082 WebView subwindow = mTabControl.getCurrentSubWindow(); 2083 if (subwindow != null) { 2084 if (subwindow.canGoBack()) { 2085 subwindow.goBack(); 2086 } else { 2087 dismissSubWindow(mTabControl.getCurrentTab()); 2088 } 2089 } else { 2090 goBackOnePageOrQuit(); 2091 } 2092 } 2093 return true; 2094 } 2095 break; 2096 } 2097 return super.onKeyUp(keyCode, event); 2098 } 2099 2100 /* package */ void stopLoading() { 2101 mDidStopLoad = true; 2102 resetTitleAndRevertLockIcon(); 2103 WebView w = getTopWindow(); 2104 w.stopLoading(); 2105 // FIXME: before refactor, it is using mWebViewClient. So I keep the 2106 // same logic here. But for subwindow case, should we call into the main 2107 // WebView's onPageFinished as we never call its onPageStarted and if 2108 // the page finishes itself, we don't call onPageFinished. 2109 mTabControl.getCurrentWebView().getWebViewClient().onPageFinished(w, 2110 w.getUrl()); 2111 2112 cancelStopToast(); 2113 mStopToast = Toast 2114 .makeText(this, R.string.stopping, Toast.LENGTH_SHORT); 2115 mStopToast.show(); 2116 } 2117 2118 boolean didUserStopLoading() { 2119 return mDidStopLoad; 2120 } 2121 2122 private void cancelStopToast() { 2123 if (mStopToast != null) { 2124 mStopToast.cancel(); 2125 mStopToast = null; 2126 } 2127 } 2128 2129 // called by a UI or non-UI thread to post the message 2130 public void postMessage(int what, int arg1, int arg2, Object obj, 2131 long delayMillis) { 2132 mHandler.sendMessageDelayed(mHandler.obtainMessage(what, arg1, arg2, 2133 obj), delayMillis); 2134 } 2135 2136 // called by a UI or non-UI thread to remove the message 2137 void removeMessages(int what, Object object) { 2138 mHandler.removeMessages(what, object); 2139 } 2140 2141 // public message ids 2142 public final static int LOAD_URL = 1001; 2143 public final static int STOP_LOAD = 1002; 2144 2145 // Message Ids 2146 private static final int FOCUS_NODE_HREF = 102; 2147 private static final int CANCEL_CREDS_REQUEST = 103; 2148 private static final int RELEASE_WAKELOCK = 107; 2149 2150 static final int UPDATE_BOOKMARK_THUMBNAIL = 108; 2151 2152 // Private handler for handling javascript and saving passwords 2153 private Handler mHandler = new Handler() { 2154 2155 public void handleMessage(Message msg) { 2156 switch (msg.what) { 2157 case FOCUS_NODE_HREF: 2158 { 2159 String url = (String) msg.getData().get("url"); 2160 String title = (String) msg.getData().get("title"); 2161 if (url == null || url.length() == 0) { 2162 break; 2163 } 2164 HashMap focusNodeMap = (HashMap) msg.obj; 2165 WebView view = (WebView) focusNodeMap.get("webview"); 2166 // Only apply the action if the top window did not change. 2167 if (getTopWindow() != view) { 2168 break; 2169 } 2170 switch (msg.arg1) { 2171 case R.id.open_context_menu_id: 2172 case R.id.view_image_context_menu_id: 2173 loadURL(getTopWindow(), url); 2174 break; 2175 case R.id.open_newtab_context_menu_id: 2176 final Tab parent = mTabControl.getCurrentTab(); 2177 final Tab newTab = openTab(url); 2178 if (newTab != parent) { 2179 parent.addChildTab(newTab); 2180 } 2181 break; 2182 case R.id.bookmark_context_menu_id: 2183 Intent intent = new Intent(BrowserActivity.this, 2184 AddBookmarkPage.class); 2185 intent.putExtra("url", url); 2186 intent.putExtra("title", title); 2187 startActivity(intent); 2188 break; 2189 case R.id.share_link_context_menu_id: 2190 Browser.sendString(BrowserActivity.this, url, 2191 getText(R.string.choosertitle_sharevia).toString()); 2192 break; 2193 case R.id.copy_link_context_menu_id: 2194 copy(url); 2195 break; 2196 case R.id.save_link_context_menu_id: 2197 case R.id.download_context_menu_id: 2198 onDownloadStartNoStream(url, null, null, null, -1); 2199 break; 2200 } 2201 break; 2202 } 2203 2204 case LOAD_URL: 2205 loadURL(getTopWindow(), (String) msg.obj); 2206 break; 2207 2208 case STOP_LOAD: 2209 stopLoading(); 2210 break; 2211 2212 case CANCEL_CREDS_REQUEST: 2213 resumeAfterCredentials(); 2214 break; 2215 2216 case RELEASE_WAKELOCK: 2217 if (mWakeLock.isHeld()) { 2218 mWakeLock.release(); 2219 // if we reach here, Browser should be still in the 2220 // background loading after WAKELOCK_TIMEOUT (5-min). 2221 // To avoid burning the battery, stop loading. 2222 mTabControl.stopAllLoading(); 2223 } 2224 break; 2225 2226 case UPDATE_BOOKMARK_THUMBNAIL: 2227 WebView view = (WebView) msg.obj; 2228 if (view != null) { 2229 updateScreenshot(view); 2230 } 2231 break; 2232 } 2233 } 2234 }; 2235 2236 private void updateScreenshot(WebView view) { 2237 // If this is a bookmarked site, add a screenshot to the database. 2238 // FIXME: When should we update? Every time? 2239 // FIXME: Would like to make sure there is actually something to 2240 // draw, but the API for that (WebViewCore.pictureReady()) is not 2241 // currently accessible here. 2242 2243 ContentResolver cr = getContentResolver(); 2244 final Cursor c = BrowserBookmarksAdapter.queryBookmarksForUrl( 2245 cr, view.getOriginalUrl(), view.getUrl(), true); 2246 if (c != null) { 2247 boolean succeed = c.moveToFirst(); 2248 ContentValues values = null; 2249 while (succeed) { 2250 if (values == null) { 2251 final ByteArrayOutputStream os 2252 = new ByteArrayOutputStream(); 2253 Bitmap bm = createScreenshot(view); 2254 if (bm == null) { 2255 c.close(); 2256 return; 2257 } 2258 bm.compress(Bitmap.CompressFormat.PNG, 100, os); 2259 values = new ContentValues(); 2260 values.put(Browser.BookmarkColumns.THUMBNAIL, 2261 os.toByteArray()); 2262 } 2263 cr.update(ContentUris.withAppendedId(Browser.BOOKMARKS_URI, 2264 c.getInt(0)), values, null, null); 2265 succeed = c.moveToNext(); 2266 } 2267 c.close(); 2268 } 2269 } 2270 2271 /** 2272 * Values for the size of the thumbnail created when taking a screenshot. 2273 * Lazily initialized. Instead of using these directly, use 2274 * getDesiredThumbnailWidth() or getDesiredThumbnailHeight(). 2275 */ 2276 private static int THUMBNAIL_WIDTH = 0; 2277 private static int THUMBNAIL_HEIGHT = 0; 2278 2279 /** 2280 * Return the desired width for thumbnail screenshots, which are stored in 2281 * the database, and used on the bookmarks screen. 2282 * @param context Context for finding out the density of the screen. 2283 * @return int desired width for thumbnail screenshot. 2284 */ 2285 /* package */ static int getDesiredThumbnailWidth(Context context) { 2286 if (THUMBNAIL_WIDTH == 0) { 2287 float density = context.getResources().getDisplayMetrics().density; 2288 THUMBNAIL_WIDTH = (int) (90 * density); 2289 THUMBNAIL_HEIGHT = (int) (80 * density); 2290 } 2291 return THUMBNAIL_WIDTH; 2292 } 2293 2294 /** 2295 * Return the desired height for thumbnail screenshots, which are stored in 2296 * the database, and used on the bookmarks screen. 2297 * @param context Context for finding out the density of the screen. 2298 * @return int desired height for thumbnail screenshot. 2299 */ 2300 /* package */ static int getDesiredThumbnailHeight(Context context) { 2301 // To ensure that they are both initialized. 2302 getDesiredThumbnailWidth(context); 2303 return THUMBNAIL_HEIGHT; 2304 } 2305 2306 private Bitmap createScreenshot(WebView view) { 2307 Picture thumbnail = view.capturePicture(); 2308 if (thumbnail == null) { 2309 return null; 2310 } 2311 Bitmap bm = Bitmap.createBitmap(getDesiredThumbnailWidth(this), 2312 getDesiredThumbnailHeight(this), Bitmap.Config.ARGB_4444); 2313 Canvas canvas = new Canvas(bm); 2314 // May need to tweak these values to determine what is the 2315 // best scale factor 2316 int thumbnailWidth = thumbnail.getWidth(); 2317 int thumbnailHeight = thumbnail.getHeight(); 2318 float scaleFactorX = 1.0f; 2319 float scaleFactorY = 1.0f; 2320 if (thumbnailWidth > 0) { 2321 scaleFactorX = (float) getDesiredThumbnailWidth(this) / 2322 (float)thumbnailWidth; 2323 } else { 2324 return null; 2325 } 2326 2327 if (view.getWidth() > view.getHeight() && 2328 thumbnailHeight < view.getHeight() && thumbnailHeight > 0) { 2329 // If the device is in landscape and the page is shorter 2330 // than the height of the view, stretch the thumbnail to fill the 2331 // space. 2332 scaleFactorY = (float) getDesiredThumbnailHeight(this) / 2333 (float)thumbnailHeight; 2334 } else { 2335 // In the portrait case, this looks nice. 2336 scaleFactorY = scaleFactorX; 2337 } 2338 2339 canvas.scale(scaleFactorX, scaleFactorY); 2340 2341 thumbnail.draw(canvas); 2342 return bm; 2343 } 2344 2345 // ------------------------------------------------------------------------- 2346 // Helper function for WebViewClient. 2347 //------------------------------------------------------------------------- 2348 2349 // Use in overrideUrlLoading 2350 /* package */ final static String SCHEME_WTAI = "wtai://wp/"; 2351 /* package */ final static String SCHEME_WTAI_MC = "wtai://wp/mc;"; 2352 /* package */ final static String SCHEME_WTAI_SD = "wtai://wp/sd;"; 2353 /* package */ final static String SCHEME_WTAI_AP = "wtai://wp/ap;"; 2354 2355 void onPageStarted(WebView view, String url, Bitmap favicon) { 2356 // when BrowserActivity just starts, onPageStarted may be called before 2357 // onResume as it is triggered from onCreate. Call resumeWebViewTimers 2358 // to start the timer. As we won't switch tabs while an activity is in 2359 // pause state, we can ensure calling resume and pause in pair. 2360 if (mActivityInPause) resumeWebViewTimers(); 2361 2362 resetLockIcon(url); 2363 setUrlTitle(url, null); 2364 setFavicon(favicon); 2365 // Keep this initial progress in sync with initialProgressValue (* 100) 2366 // in ProgressTracker.cpp 2367 // Show some progress so that the user knows the page is beginning to 2368 // load 2369 onProgressChanged(view, 10); 2370 mDidStopLoad = false; 2371 if (!mIsNetworkUp) createAndShowNetworkDialog(); 2372 2373 if (mSettings.isTracing()) { 2374 String host; 2375 try { 2376 WebAddress uri = new WebAddress(url); 2377 host = uri.mHost; 2378 } catch (android.net.ParseException ex) { 2379 host = "browser"; 2380 } 2381 host = host.replace('.', '_'); 2382 host += ".trace"; 2383 mInTrace = true; 2384 Debug.startMethodTracing(host, 20 * 1024 * 1024); 2385 } 2386 2387 // Performance probe 2388 if (false) { 2389 mStart = SystemClock.uptimeMillis(); 2390 mProcessStart = Process.getElapsedCpuTime(); 2391 long[] sysCpu = new long[7]; 2392 if (Process.readProcFile("/proc/stat", SYSTEM_CPU_FORMAT, null, 2393 sysCpu, null)) { 2394 mUserStart = sysCpu[0] + sysCpu[1]; 2395 mSystemStart = sysCpu[2]; 2396 mIdleStart = sysCpu[3]; 2397 mIrqStart = sysCpu[4] + sysCpu[5] + sysCpu[6]; 2398 } 2399 mUiStart = SystemClock.currentThreadTimeMillis(); 2400 } 2401 } 2402 2403 void onPageFinished(WebView view, String url) { 2404 // Reset the title and icon in case we stopped a provisional load. 2405 resetTitleAndIcon(view); 2406 // Update the lock icon image only once we are done loading 2407 updateLockIconToLatest(); 2408 // pause the WebView timer and release the wake lock if it is finished 2409 // while BrowserActivity is in pause state. 2410 if (mActivityInPause && pauseWebViewTimers()) { 2411 if (mWakeLock.isHeld()) { 2412 mHandler.removeMessages(RELEASE_WAKELOCK); 2413 mWakeLock.release(); 2414 } 2415 } 2416 2417 // Performance probe 2418 if (false) { 2419 long[] sysCpu = new long[7]; 2420 if (Process.readProcFile("/proc/stat", SYSTEM_CPU_FORMAT, null, 2421 sysCpu, null)) { 2422 String uiInfo = "UI thread used " 2423 + (SystemClock.currentThreadTimeMillis() - mUiStart) 2424 + " ms"; 2425 if (LOGD_ENABLED) { 2426 Log.d(LOGTAG, uiInfo); 2427 } 2428 //The string that gets written to the log 2429 String performanceString = "It took total " 2430 + (SystemClock.uptimeMillis() - mStart) 2431 + " ms clock time to load the page." 2432 + "\nbrowser process used " 2433 + (Process.getElapsedCpuTime() - mProcessStart) 2434 + " ms, user processes used " 2435 + (sysCpu[0] + sysCpu[1] - mUserStart) * 10 2436 + " ms, kernel used " 2437 + (sysCpu[2] - mSystemStart) * 10 2438 + " ms, idle took " + (sysCpu[3] - mIdleStart) * 10 2439 + " ms and irq took " 2440 + (sysCpu[4] + sysCpu[5] + sysCpu[6] - mIrqStart) 2441 * 10 + " ms, " + uiInfo; 2442 if (LOGD_ENABLED) { 2443 Log.d(LOGTAG, performanceString + "\nWebpage: " + url); 2444 } 2445 if (url != null) { 2446 // strip the url to maintain consistency 2447 String newUrl = new String(url); 2448 if (newUrl.startsWith("http://www.")) { 2449 newUrl = newUrl.substring(11); 2450 } else if (newUrl.startsWith("http://")) { 2451 newUrl = newUrl.substring(7); 2452 } else if (newUrl.startsWith("https://www.")) { 2453 newUrl = newUrl.substring(12); 2454 } else if (newUrl.startsWith("https://")) { 2455 newUrl = newUrl.substring(8); 2456 } 2457 if (LOGD_ENABLED) { 2458 Log.d(LOGTAG, newUrl + " loaded"); 2459 } 2460 } 2461 } 2462 } 2463 2464 if (mInTrace) { 2465 mInTrace = false; 2466 Debug.stopMethodTracing(); 2467 } 2468 } 2469 2470 boolean shouldOverrideUrlLoading(WebView view, String url) { 2471 if (url.startsWith(SCHEME_WTAI)) { 2472 // wtai://wp/mc;number 2473 // number=string(phone-number) 2474 if (url.startsWith(SCHEME_WTAI_MC)) { 2475 Intent intent = new Intent(Intent.ACTION_VIEW, 2476 Uri.parse(WebView.SCHEME_TEL + 2477 url.substring(SCHEME_WTAI_MC.length()))); 2478 startActivity(intent); 2479 return true; 2480 } 2481 // wtai://wp/sd;dtmf 2482 // dtmf=string(dialstring) 2483 if (url.startsWith(SCHEME_WTAI_SD)) { 2484 // TODO: only send when there is active voice connection 2485 return false; 2486 } 2487 // wtai://wp/ap;number;name 2488 // number=string(phone-number) 2489 // name=string 2490 if (url.startsWith(SCHEME_WTAI_AP)) { 2491 // TODO 2492 return false; 2493 } 2494 } 2495 2496 // The "about:" schemes are internal to the browser; don't want these to 2497 // be dispatched to other apps. 2498 if (url.startsWith("about:")) { 2499 return false; 2500 } 2501 2502 Intent intent; 2503 // perform generic parsing of the URI to turn it into an Intent. 2504 try { 2505 intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME); 2506 } catch (URISyntaxException ex) { 2507 Log.w("Browser", "Bad URI " + url + ": " + ex.getMessage()); 2508 return false; 2509 } 2510 2511 // check whether the intent can be resolved. If not, we will see 2512 // whether we can download it from the Market. 2513 if (getPackageManager().resolveActivity(intent, 0) == null) { 2514 String packagename = intent.getPackage(); 2515 if (packagename != null) { 2516 intent = new Intent(Intent.ACTION_VIEW, Uri 2517 .parse("market://search?q=pname:" + packagename)); 2518 intent.addCategory(Intent.CATEGORY_BROWSABLE); 2519 startActivity(intent); 2520 return true; 2521 } else { 2522 return false; 2523 } 2524 } 2525 2526 // sanitize the Intent, ensuring web pages can not bypass browser 2527 // security (only access to BROWSABLE activities). 2528 intent.addCategory(Intent.CATEGORY_BROWSABLE); 2529 intent.setComponent(null); 2530 try { 2531 if (startActivityIfNeeded(intent, -1)) { 2532 return true; 2533 } 2534 } catch (ActivityNotFoundException ex) { 2535 // ignore the error. If no application can handle the URL, 2536 // eg about:blank, assume the browser can handle it. 2537 } 2538 2539 if (mMenuIsDown) { 2540 openTab(url); 2541 closeOptionsMenu(); 2542 return true; 2543 } 2544 return false; 2545 } 2546 2547 // ------------------------------------------------------------------------- 2548 // Helper function for WebChromeClient 2549 // ------------------------------------------------------------------------- 2550 2551 void onProgressChanged(WebView view, int newProgress) { 2552 mTitleBar.setProgress(newProgress); 2553 mFakeTitleBar.setProgress(newProgress); 2554 2555 if (newProgress == 100) { 2556 // onProgressChanged() may continue to be called after the main 2557 // frame has finished loading, as any remaining sub frames continue 2558 // to load. We'll only get called once though with newProgress as 2559 // 100 when everything is loaded. (onPageFinished is called once 2560 // when the main frame completes loading regardless of the state of 2561 // any sub frames so calls to onProgressChanges may continue after 2562 // onPageFinished has executed) 2563 if (mInLoad) { 2564 mInLoad = false; 2565 updateInLoadMenuItems(); 2566 // If the options menu is open, leave the title bar 2567 if (!mOptionsMenuOpen || !mIconView) { 2568 hideFakeTitleBar(); 2569 } 2570 } 2571 } else if (!mInLoad) { 2572 // onPageFinished may have already been called but a subframe is 2573 // still loading and updating the progress. Reset mInLoad and update 2574 // the menu items. 2575 mInLoad = true; 2576 updateInLoadMenuItems(); 2577 if (!mOptionsMenuOpen || mIconView) { 2578 // This page has begun to load, so show the title bar 2579 showFakeTitleBar(); 2580 } 2581 } 2582 } 2583 2584 void onShowCustomView(View view, WebChromeClient.CustomViewCallback callback) { 2585 // if a view already exists then immediately terminate the new one 2586 if (mCustomView != null) { 2587 callback.onCustomViewHidden(); 2588 return; 2589 } 2590 2591 // Add the custom view to its container. 2592 mCustomViewContainer.addView(view, COVER_SCREEN_GRAVITY_CENTER); 2593 mCustomView = view; 2594 mCustomViewCallback = callback; 2595 // Save the menu state and set it to empty while the custom 2596 // view is showing. 2597 mOldMenuState = mMenuState; 2598 mMenuState = EMPTY_MENU; 2599 // Hide the content view. 2600 mContentView.setVisibility(View.GONE); 2601 // Finally show the custom view container. 2602 setStatusBarVisibility(false); 2603 mCustomViewContainer.setVisibility(View.VISIBLE); 2604 mCustomViewContainer.bringToFront(); 2605 } 2606 2607 void onHideCustomView() { 2608 if (mCustomView == null) 2609 return; 2610 2611 // Hide the custom view. 2612 mCustomView.setVisibility(View.GONE); 2613 // Remove the custom view from its container. 2614 mCustomViewContainer.removeView(mCustomView); 2615 mCustomView = null; 2616 // Reset the old menu state. 2617 mMenuState = mOldMenuState; 2618 mOldMenuState = EMPTY_MENU; 2619 mCustomViewContainer.setVisibility(View.GONE); 2620 mCustomViewCallback.onCustomViewHidden(); 2621 // Show the content view. 2622 setStatusBarVisibility(true); 2623 mContentView.setVisibility(View.VISIBLE); 2624 } 2625 2626 Bitmap getDefaultVideoPoster() { 2627 if (mDefaultVideoPoster == null) { 2628 mDefaultVideoPoster = BitmapFactory.decodeResource( 2629 getResources(), R.drawable.default_video_poster); 2630 } 2631 return mDefaultVideoPoster; 2632 } 2633 2634 View getVideoLoadingProgressView() { 2635 if (mVideoProgressView == null) { 2636 LayoutInflater inflater = LayoutInflater.from(BrowserActivity.this); 2637 mVideoProgressView = inflater.inflate( 2638 R.layout.video_loading_progress, null); 2639 } 2640 return mVideoProgressView; 2641 } 2642 2643 /* 2644 * The Object used to inform the WebView of the file to upload. 2645 */ 2646 private ValueCallback<Uri> mUploadMessage; 2647 2648 void openFileChooser(ValueCallback<Uri> uploadMsg) { 2649 if (mUploadMessage != null) return; 2650 mUploadMessage = uploadMsg; 2651 Intent i = new Intent(Intent.ACTION_GET_CONTENT); 2652 i.addCategory(Intent.CATEGORY_OPENABLE); 2653 i.setType("*/*"); 2654 BrowserActivity.this.startActivityForResult(Intent.createChooser(i, 2655 getString(R.string.choose_upload)), FILE_SELECTED); 2656 } 2657 2658 // ------------------------------------------------------------------------- 2659 // Implement functions for DownloadListener 2660 // ------------------------------------------------------------------------- 2661 2662 /** 2663 * Notify the host application a download should be done, or that 2664 * the data should be streamed if a streaming viewer is available. 2665 * @param url The full url to the content that should be downloaded 2666 * @param contentDisposition Content-disposition http header, if 2667 * present. 2668 * @param mimetype The mimetype of the content reported by the server 2669 * @param contentLength The file size reported by the server 2670 */ 2671 public void onDownloadStart(String url, String userAgent, 2672 String contentDisposition, String mimetype, long contentLength) { 2673 // if we're dealing wih A/V content that's not explicitly marked 2674 // for download, check if it's streamable. 2675 if (contentDisposition == null 2676 || !contentDisposition.regionMatches( 2677 true, 0, "attachment", 0, 10)) { 2678 // query the package manager to see if there's a registered handler 2679 // that matches. 2680 Intent intent = new Intent(Intent.ACTION_VIEW); 2681 intent.setDataAndType(Uri.parse(url), mimetype); 2682 ResolveInfo info = getPackageManager().resolveActivity(intent, 2683 PackageManager.MATCH_DEFAULT_ONLY); 2684 if (info != null) { 2685 ComponentName myName = getComponentName(); 2686 // If we resolved to ourselves, we don't want to attempt to 2687 // load the url only to try and download it again. 2688 if (!myName.getPackageName().equals( 2689 info.activityInfo.packageName) 2690 || !myName.getClassName().equals( 2691 info.activityInfo.name)) { 2692 // someone (other than us) knows how to handle this mime 2693 // type with this scheme, don't download. 2694 try { 2695 startActivity(intent); 2696 return; 2697 } catch (ActivityNotFoundException ex) { 2698 if (LOGD_ENABLED) { 2699 Log.d(LOGTAG, "activity not found for " + mimetype 2700 + " over " + Uri.parse(url).getScheme(), 2701 ex); 2702 } 2703 // Best behavior is to fall back to a download in this 2704 // case 2705 } 2706 } 2707 } 2708 } 2709 onDownloadStartNoStream(url, userAgent, contentDisposition, mimetype, contentLength); 2710 } 2711 2712 /** 2713 * Notify the host application a download should be done, even if there 2714 * is a streaming viewer available for thise type. 2715 * @param url The full url to the content that should be downloaded 2716 * @param contentDisposition Content-disposition http header, if 2717 * present. 2718 * @param mimetype The mimetype of the content reported by the server 2719 * @param contentLength The file size reported by the server 2720 */ 2721 /*package */ void onDownloadStartNoStream(String url, String userAgent, 2722 String contentDisposition, String mimetype, long contentLength) { 2723 2724 String filename = URLUtil.guessFileName(url, 2725 contentDisposition, mimetype); 2726 2727 // Check to see if we have an SDCard 2728 String status = Environment.getExternalStorageState(); 2729 if (!status.equals(Environment.MEDIA_MOUNTED)) { 2730 int title; 2731 String msg; 2732 2733 // Check to see if the SDCard is busy, same as the music app 2734 if (status.equals(Environment.MEDIA_SHARED)) { 2735 msg = getString(R.string.download_sdcard_busy_dlg_msg); 2736 title = R.string.download_sdcard_busy_dlg_title; 2737 } else { 2738 msg = getString(R.string.download_no_sdcard_dlg_msg, filename); 2739 title = R.string.download_no_sdcard_dlg_title; 2740 } 2741 2742 new AlertDialog.Builder(this) 2743 .setTitle(title) 2744 .setIcon(android.R.drawable.ic_dialog_alert) 2745 .setMessage(msg) 2746 .setPositiveButton(R.string.ok, null) 2747 .show(); 2748 return; 2749 } 2750 2751 // java.net.URI is a lot stricter than KURL so we have to undo 2752 // KURL's percent-encoding and redo the encoding using java.net.URI. 2753 URI uri = null; 2754 try { 2755 // Undo the percent-encoding that KURL may have done. 2756 String newUrl = new String(URLUtil.decode(url.getBytes())); 2757 // Parse the url into pieces 2758 WebAddress w = new WebAddress(newUrl); 2759 String frag = null; 2760 String query = null; 2761 String path = w.mPath; 2762 // Break the path into path, query, and fragment 2763 if (path.length() > 0) { 2764 // Strip the fragment 2765 int idx = path.lastIndexOf('#'); 2766 if (idx != -1) { 2767 frag = path.substring(idx + 1); 2768 path = path.substring(0, idx); 2769 } 2770 idx = path.lastIndexOf('?'); 2771 if (idx != -1) { 2772 query = path.substring(idx + 1); 2773 path = path.substring(0, idx); 2774 } 2775 } 2776 uri = new URI(w.mScheme, w.mAuthInfo, w.mHost, w.mPort, path, 2777 query, frag); 2778 } catch (Exception e) { 2779 Log.e(LOGTAG, "Could not parse url for download: " + url, e); 2780 return; 2781 } 2782 2783 // XXX: Have to use the old url since the cookies were stored using the 2784 // old percent-encoded url. 2785 String cookies = CookieManager.getInstance().getCookie(url); 2786 2787 ContentValues values = new ContentValues(); 2788 values.put(Downloads.COLUMN_URI, uri.toString()); 2789 values.put(Downloads.COLUMN_COOKIE_DATA, cookies); 2790 values.put(Downloads.COLUMN_USER_AGENT, userAgent); 2791 values.put(Downloads.COLUMN_NOTIFICATION_PACKAGE, 2792 getPackageName()); 2793 values.put(Downloads.COLUMN_NOTIFICATION_CLASS, 2794 BrowserDownloadPage.class.getCanonicalName()); 2795 values.put(Downloads.COLUMN_VISIBILITY, Downloads.VISIBILITY_VISIBLE_NOTIFY_COMPLETED); 2796 values.put(Downloads.COLUMN_MIME_TYPE, mimetype); 2797 values.put(Downloads.COLUMN_FILE_NAME_HINT, filename); 2798 values.put(Downloads.COLUMN_DESCRIPTION, uri.getHost()); 2799 if (contentLength > 0) { 2800 values.put(Downloads.COLUMN_TOTAL_BYTES, contentLength); 2801 } 2802 if (mimetype == null) { 2803 // We must have long pressed on a link or image to download it. We 2804 // are not sure of the mimetype in this case, so do a head request 2805 new FetchUrlMimeType(this).execute(values); 2806 } else { 2807 final Uri contentUri = 2808 getContentResolver().insert(Downloads.CONTENT_URI, values); 2809 viewDownloads(contentUri); 2810 } 2811 2812 } 2813 2814 // ------------------------------------------------------------------------- 2815 2816 /** 2817 * Resets the lock icon. This method is called when we start a new load and 2818 * know the url to be loaded. 2819 */ 2820 private void resetLockIcon(String url) { 2821 // Save the lock-icon state (we revert to it if the load gets cancelled) 2822 mTabControl.getCurrentTab().resetLockIcon(url); 2823 updateLockIconImage(LOCK_ICON_UNSECURE); 2824 } 2825 2826 /** 2827 * Update the lock icon to correspond to our latest state. 2828 */ 2829 private void updateLockIconToLatest() { 2830 updateLockIconImage(mTabControl.getCurrentTab().getLockIconType()); 2831 } 2832 2833 /** 2834 * Updates the lock-icon image in the title-bar. 2835 */ 2836 private void updateLockIconImage(int lockIconType) { 2837 Drawable d = null; 2838 if (lockIconType == LOCK_ICON_SECURE) { 2839 d = mSecLockIcon; 2840 } else if (lockIconType == LOCK_ICON_MIXED) { 2841 d = mMixLockIcon; 2842 } 2843 mTitleBar.setLock(d); 2844 mFakeTitleBar.setLock(d); 2845 } 2846 2847 /** 2848 * Displays a page-info dialog. 2849 * @param tab The tab to show info about 2850 * @param fromShowSSLCertificateOnError The flag that indicates whether 2851 * this dialog was opened from the SSL-certificate-on-error dialog or 2852 * not. This is important, since we need to know whether to return to 2853 * the parent dialog or simply dismiss. 2854 */ 2855 private void showPageInfo(final Tab tab, 2856 final boolean fromShowSSLCertificateOnError) { 2857 final LayoutInflater factory = LayoutInflater 2858 .from(this); 2859 2860 final View pageInfoView = factory.inflate(R.layout.page_info, null); 2861 2862 final WebView view = tab.getWebView(); 2863 2864 String url = null; 2865 String title = null; 2866 2867 if (view == null) { 2868 url = tab.getUrl(); 2869 title = tab.getTitle(); 2870 } else if (view == mTabControl.getCurrentWebView()) { 2871 // Use the cached title and url if this is the current WebView 2872 url = mUrl; 2873 title = mTitle; 2874 } else { 2875 url = view.getUrl(); 2876 title = view.getTitle(); 2877 } 2878 2879 if (url == null) { 2880 url = ""; 2881 } 2882 if (title == null) { 2883 title = ""; 2884 } 2885 2886 ((TextView) pageInfoView.findViewById(R.id.address)).setText(url); 2887 ((TextView) pageInfoView.findViewById(R.id.title)).setText(title); 2888 2889 mPageInfoView = tab; 2890 mPageInfoFromShowSSLCertificateOnError = new Boolean(fromShowSSLCertificateOnError); 2891 2892 AlertDialog.Builder alertDialogBuilder = 2893 new AlertDialog.Builder(this) 2894 .setTitle(R.string.page_info).setIcon(android.R.drawable.ic_dialog_info) 2895 .setView(pageInfoView) 2896 .setPositiveButton( 2897 R.string.ok, 2898 new DialogInterface.OnClickListener() { 2899 public void onClick(DialogInterface dialog, 2900 int whichButton) { 2901 mPageInfoDialog = null; 2902 mPageInfoView = null; 2903 mPageInfoFromShowSSLCertificateOnError = null; 2904 2905 // if we came here from the SSL error dialog 2906 if (fromShowSSLCertificateOnError) { 2907 // go back to the SSL error dialog 2908 showSSLCertificateOnError( 2909 mSSLCertificateOnErrorView, 2910 mSSLCertificateOnErrorHandler, 2911 mSSLCertificateOnErrorError); 2912 } 2913 } 2914 }) 2915 .setOnCancelListener( 2916 new DialogInterface.OnCancelListener() { 2917 public void onCancel(DialogInterface dialog) { 2918 mPageInfoDialog = null; 2919 mPageInfoView = null; 2920 mPageInfoFromShowSSLCertificateOnError = null; 2921 2922 // if we came here from the SSL error dialog 2923 if (fromShowSSLCertificateOnError) { 2924 // go back to the SSL error dialog 2925 showSSLCertificateOnError( 2926 mSSLCertificateOnErrorView, 2927 mSSLCertificateOnErrorHandler, 2928 mSSLCertificateOnErrorError); 2929 } 2930 } 2931 }); 2932 2933 // if we have a main top-level page SSL certificate set or a certificate 2934 // error 2935 if (fromShowSSLCertificateOnError || 2936 (view != null && view.getCertificate() != null)) { 2937 // add a 'View Certificate' button 2938 alertDialogBuilder.setNeutralButton( 2939 R.string.view_certificate, 2940 new DialogInterface.OnClickListener() { 2941 public void onClick(DialogInterface dialog, 2942 int whichButton) { 2943 mPageInfoDialog = null; 2944 mPageInfoView = null; 2945 mPageInfoFromShowSSLCertificateOnError = null; 2946 2947 // if we came here from the SSL error dialog 2948 if (fromShowSSLCertificateOnError) { 2949 // go back to the SSL error dialog 2950 showSSLCertificateOnError( 2951 mSSLCertificateOnErrorView, 2952 mSSLCertificateOnErrorHandler, 2953 mSSLCertificateOnErrorError); 2954 } else { 2955 // otherwise, display the top-most certificate from 2956 // the chain 2957 if (view.getCertificate() != null) { 2958 showSSLCertificate(tab); 2959 } 2960 } 2961 } 2962 }); 2963 } 2964 2965 mPageInfoDialog = alertDialogBuilder.show(); 2966 } 2967 2968 /** 2969 * Displays the main top-level page SSL certificate dialog 2970 * (accessible from the Page-Info dialog). 2971 * @param tab The tab to show certificate for. 2972 */ 2973 private void showSSLCertificate(final Tab tab) { 2974 final View certificateView = 2975 inflateCertificateView(tab.getWebView().getCertificate()); 2976 if (certificateView == null) { 2977 return; 2978 } 2979 2980 LayoutInflater factory = LayoutInflater.from(this); 2981 2982 final LinearLayout placeholder = 2983 (LinearLayout)certificateView.findViewById(R.id.placeholder); 2984 2985 LinearLayout ll = (LinearLayout) factory.inflate( 2986 R.layout.ssl_success, placeholder); 2987 ((TextView)ll.findViewById(R.id.success)) 2988 .setText(R.string.ssl_certificate_is_valid); 2989 2990 mSSLCertificateView = tab; 2991 mSSLCertificateDialog = 2992 new AlertDialog.Builder(this) 2993 .setTitle(R.string.ssl_certificate).setIcon( 2994 R.drawable.ic_dialog_browser_certificate_secure) 2995 .setView(certificateView) 2996 .setPositiveButton(R.string.ok, 2997 new DialogInterface.OnClickListener() { 2998 public void onClick(DialogInterface dialog, 2999 int whichButton) { 3000 mSSLCertificateDialog = null; 3001 mSSLCertificateView = null; 3002 3003 showPageInfo(tab, false); 3004 } 3005 }) 3006 .setOnCancelListener( 3007 new DialogInterface.OnCancelListener() { 3008 public void onCancel(DialogInterface dialog) { 3009 mSSLCertificateDialog = null; 3010 mSSLCertificateView = null; 3011 3012 showPageInfo(tab, false); 3013 } 3014 }) 3015 .show(); 3016 } 3017 3018 /** 3019 * Displays the SSL error certificate dialog. 3020 * @param view The target web-view. 3021 * @param handler The SSL error handler responsible for cancelling the 3022 * connection that resulted in an SSL error or proceeding per user request. 3023 * @param error The SSL error object. 3024 */ 3025 void showSSLCertificateOnError( 3026 final WebView view, final SslErrorHandler handler, final SslError error) { 3027 3028 final View certificateView = 3029 inflateCertificateView(error.getCertificate()); 3030 if (certificateView == null) { 3031 return; 3032 } 3033 3034 LayoutInflater factory = LayoutInflater.from(this); 3035 3036 final LinearLayout placeholder = 3037 (LinearLayout)certificateView.findViewById(R.id.placeholder); 3038 3039 if (error.hasError(SslError.SSL_UNTRUSTED)) { 3040 LinearLayout ll = (LinearLayout)factory 3041 .inflate(R.layout.ssl_warning, placeholder); 3042 ((TextView)ll.findViewById(R.id.warning)) 3043 .setText(R.string.ssl_untrusted); 3044 } 3045 3046 if (error.hasError(SslError.SSL_IDMISMATCH)) { 3047 LinearLayout ll = (LinearLayout)factory 3048 .inflate(R.layout.ssl_warning, placeholder); 3049 ((TextView)ll.findViewById(R.id.warning)) 3050 .setText(R.string.ssl_mismatch); 3051 } 3052 3053 if (error.hasError(SslError.SSL_EXPIRED)) { 3054 LinearLayout ll = (LinearLayout)factory 3055 .inflate(R.layout.ssl_warning, placeholder); 3056 ((TextView)ll.findViewById(R.id.warning)) 3057 .setText(R.string.ssl_expired); 3058 } 3059 3060 if (error.hasError(SslError.SSL_NOTYETVALID)) { 3061 LinearLayout ll = (LinearLayout)factory 3062 .inflate(R.layout.ssl_warning, placeholder); 3063 ((TextView)ll.findViewById(R.id.warning)) 3064 .setText(R.string.ssl_not_yet_valid); 3065 } 3066 3067 mSSLCertificateOnErrorHandler = handler; 3068 mSSLCertificateOnErrorView = view; 3069 mSSLCertificateOnErrorError = error; 3070 mSSLCertificateOnErrorDialog = 3071 new AlertDialog.Builder(this) 3072 .setTitle(R.string.ssl_certificate).setIcon( 3073 R.drawable.ic_dialog_browser_certificate_partially_secure) 3074 .setView(certificateView) 3075 .setPositiveButton(R.string.ok, 3076 new DialogInterface.OnClickListener() { 3077 public void onClick(DialogInterface dialog, 3078 int whichButton) { 3079 mSSLCertificateOnErrorDialog = null; 3080 mSSLCertificateOnErrorView = null; 3081 mSSLCertificateOnErrorHandler = null; 3082 mSSLCertificateOnErrorError = null; 3083 3084 view.getWebViewClient().onReceivedSslError( 3085 view, handler, error); 3086 } 3087 }) 3088 .setNeutralButton(R.string.page_info_view, 3089 new DialogInterface.OnClickListener() { 3090 public void onClick(DialogInterface dialog, 3091 int whichButton) { 3092 mSSLCertificateOnErrorDialog = null; 3093 3094 // do not clear the dialog state: we will 3095 // need to show the dialog again once the 3096 // user is done exploring the page-info details 3097 3098 showPageInfo(mTabControl.getTabFromView(view), 3099 true); 3100 } 3101 }) 3102 .setOnCancelListener( 3103 new DialogInterface.OnCancelListener() { 3104 public void onCancel(DialogInterface dialog) { 3105 mSSLCertificateOnErrorDialog = null; 3106 mSSLCertificateOnErrorView = null; 3107 mSSLCertificateOnErrorHandler = null; 3108 mSSLCertificateOnErrorError = null; 3109 3110 view.getWebViewClient().onReceivedSslError( 3111 view, handler, error); 3112 } 3113 }) 3114 .show(); 3115 } 3116 3117 /** 3118 * Inflates the SSL certificate view (helper method). 3119 * @param certificate The SSL certificate. 3120 * @return The resultant certificate view with issued-to, issued-by, 3121 * issued-on, expires-on, and possibly other fields set. 3122 * If the input certificate is null, returns null. 3123 */ 3124 private View inflateCertificateView(SslCertificate certificate) { 3125 if (certificate == null) { 3126 return null; 3127 } 3128 3129 LayoutInflater factory = LayoutInflater.from(this); 3130 3131 View certificateView = factory.inflate( 3132 R.layout.ssl_certificate, null); 3133 3134 // issued to: 3135 SslCertificate.DName issuedTo = certificate.getIssuedTo(); 3136 if (issuedTo != null) { 3137 ((TextView) certificateView.findViewById(R.id.to_common)) 3138 .setText(issuedTo.getCName()); 3139 ((TextView) certificateView.findViewById(R.id.to_org)) 3140 .setText(issuedTo.getOName()); 3141 ((TextView) certificateView.findViewById(R.id.to_org_unit)) 3142 .setText(issuedTo.getUName()); 3143 } 3144 3145 // issued by: 3146 SslCertificate.DName issuedBy = certificate.getIssuedBy(); 3147 if (issuedBy != null) { 3148 ((TextView) certificateView.findViewById(R.id.by_common)) 3149 .setText(issuedBy.getCName()); 3150 ((TextView) certificateView.findViewById(R.id.by_org)) 3151 .setText(issuedBy.getOName()); 3152 ((TextView) certificateView.findViewById(R.id.by_org_unit)) 3153 .setText(issuedBy.getUName()); 3154 } 3155 3156 // issued on: 3157 String issuedOn = reformatCertificateDate( 3158 certificate.getValidNotBefore()); 3159 ((TextView) certificateView.findViewById(R.id.issued_on)) 3160 .setText(issuedOn); 3161 3162 // expires on: 3163 String expiresOn = reformatCertificateDate( 3164 certificate.getValidNotAfter()); 3165 ((TextView) certificateView.findViewById(R.id.expires_on)) 3166 .setText(expiresOn); 3167 3168 return certificateView; 3169 } 3170 3171 /** 3172 * Re-formats the certificate date (Date.toString()) string to 3173 * a properly localized date string. 3174 * @return Properly localized version of the certificate date string and 3175 * the original certificate date string if fails to localize. 3176 * If the original string is null, returns an empty string "". 3177 */ 3178 private String reformatCertificateDate(String certificateDate) { 3179 String reformattedDate = null; 3180 3181 if (certificateDate != null) { 3182 Date date = null; 3183 try { 3184 date = java.text.DateFormat.getInstance().parse(certificateDate); 3185 } catch (ParseException e) { 3186 date = null; 3187 } 3188 3189 if (date != null) { 3190 reformattedDate = 3191 DateFormat.getDateFormat(this).format(date); 3192 } 3193 } 3194 3195 return reformattedDate != null ? reformattedDate : 3196 (certificateDate != null ? certificateDate : ""); 3197 } 3198 3199 /** 3200 * Displays an http-authentication dialog. 3201 */ 3202 void showHttpAuthentication(final HttpAuthHandler handler, 3203 final String host, final String realm, final String title, 3204 final String name, final String password, int focusId) { 3205 LayoutInflater factory = LayoutInflater.from(this); 3206 final View v = factory 3207 .inflate(R.layout.http_authentication, null); 3208 if (name != null) { 3209 ((EditText) v.findViewById(R.id.username_edit)).setText(name); 3210 } 3211 if (password != null) { 3212 ((EditText) v.findViewById(R.id.password_edit)).setText(password); 3213 } 3214 3215 String titleText = title; 3216 if (titleText == null) { 3217 titleText = getText(R.string.sign_in_to).toString().replace( 3218 "%s1", host).replace("%s2", realm); 3219 } 3220 3221 mHttpAuthHandler = handler; 3222 AlertDialog dialog = new AlertDialog.Builder(this) 3223 .setTitle(titleText) 3224 .setIcon(android.R.drawable.ic_dialog_alert) 3225 .setView(v) 3226 .setPositiveButton(R.string.action, 3227 new DialogInterface.OnClickListener() { 3228 public void onClick(DialogInterface dialog, 3229 int whichButton) { 3230 String nm = ((EditText) v 3231 .findViewById(R.id.username_edit)) 3232 .getText().toString(); 3233 String pw = ((EditText) v 3234 .findViewById(R.id.password_edit)) 3235 .getText().toString(); 3236 BrowserActivity.this.setHttpAuthUsernamePassword 3237 (host, realm, nm, pw); 3238 handler.proceed(nm, pw); 3239 mHttpAuthenticationDialog = null; 3240 mHttpAuthHandler = null; 3241 }}) 3242 .setNegativeButton(R.string.cancel, 3243 new DialogInterface.OnClickListener() { 3244 public void onClick(DialogInterface dialog, 3245 int whichButton) { 3246 handler.cancel(); 3247 BrowserActivity.this.resetTitleAndRevertLockIcon(); 3248 mHttpAuthenticationDialog = null; 3249 mHttpAuthHandler = null; 3250 }}) 3251 .setOnCancelListener(new DialogInterface.OnCancelListener() { 3252 public void onCancel(DialogInterface dialog) { 3253 handler.cancel(); 3254 BrowserActivity.this.resetTitleAndRevertLockIcon(); 3255 mHttpAuthenticationDialog = null; 3256 mHttpAuthHandler = null; 3257 }}) 3258 .create(); 3259 // Make the IME appear when the dialog is displayed if applicable. 3260 dialog.getWindow().setSoftInputMode( 3261 WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE); 3262 dialog.show(); 3263 if (focusId != 0) { 3264 dialog.findViewById(focusId).requestFocus(); 3265 } else { 3266 v.findViewById(R.id.username_edit).requestFocus(); 3267 } 3268 mHttpAuthenticationDialog = dialog; 3269 } 3270 3271 public int getProgress() { 3272 WebView w = mTabControl.getCurrentWebView(); 3273 if (w != null) { 3274 return w.getProgress(); 3275 } else { 3276 return 100; 3277 } 3278 } 3279 3280 /** 3281 * Set HTTP authentication password. 3282 * 3283 * @param host The host for the password 3284 * @param realm The realm for the password 3285 * @param username The username for the password. If it is null, it means 3286 * password can't be saved. 3287 * @param password The password 3288 */ 3289 public void setHttpAuthUsernamePassword(String host, String realm, 3290 String username, 3291 String password) { 3292 WebView w = mTabControl.getCurrentWebView(); 3293 if (w != null) { 3294 w.setHttpAuthUsernamePassword(host, realm, username, password); 3295 } 3296 } 3297 3298 /** 3299 * connectivity manager says net has come or gone... inform the user 3300 * @param up true if net has come up, false if net has gone down 3301 */ 3302 public void onNetworkToggle(boolean up) { 3303 if (up == mIsNetworkUp) { 3304 return; 3305 } else if (up) { 3306 mIsNetworkUp = true; 3307 if (mAlertDialog != null) { 3308 mAlertDialog.cancel(); 3309 mAlertDialog = null; 3310 } 3311 } else { 3312 mIsNetworkUp = false; 3313 if (mInLoad) { 3314 createAndShowNetworkDialog(); 3315 } 3316 } 3317 WebView w = mTabControl.getCurrentWebView(); 3318 if (w != null) { 3319 w.setNetworkAvailable(up); 3320 } 3321 } 3322 3323 boolean isNetworkUp() { 3324 return mIsNetworkUp; 3325 } 3326 3327 // This method shows the network dialog alerting the user that the net is 3328 // down. It will only show the dialog if mAlertDialog is null. 3329 private void createAndShowNetworkDialog() { 3330 if (mAlertDialog == null) { 3331 mAlertDialog = new AlertDialog.Builder(this) 3332 .setTitle(R.string.loadSuspendedTitle) 3333 .setMessage(R.string.loadSuspended) 3334 .setPositiveButton(R.string.ok, null) 3335 .show(); 3336 } 3337 } 3338 3339 @Override 3340 protected void onActivityResult(int requestCode, int resultCode, 3341 Intent intent) { 3342 if (getTopWindow() == null) return; 3343 3344 switch (requestCode) { 3345 case COMBO_PAGE: 3346 if (resultCode == RESULT_OK && intent != null) { 3347 String data = intent.getAction(); 3348 Bundle extras = intent.getExtras(); 3349 if (extras != null && extras.getBoolean("new_window", false)) { 3350 openTab(data); 3351 } else { 3352 final Tab currentTab = 3353 mTabControl.getCurrentTab(); 3354 dismissSubWindow(currentTab); 3355 if (data != null && data.length() != 0) { 3356 getTopWindow().loadUrl(data); 3357 } 3358 } 3359 } 3360 // Deliberately fall through to PREFERENCES_PAGE, since the 3361 // same extra may be attached to the COMBO_PAGE 3362 case PREFERENCES_PAGE: 3363 if (resultCode == RESULT_OK && intent != null) { 3364 String action = intent.getStringExtra(Intent.EXTRA_TEXT); 3365 if (BrowserSettings.PREF_CLEAR_HISTORY.equals(action)) { 3366 mTabControl.removeParentChildRelationShips(); 3367 } 3368 } 3369 break; 3370 // Choose a file from the file picker. 3371 case FILE_SELECTED: 3372 if (null == mUploadMessage) break; 3373 Uri result = intent == null || resultCode != RESULT_OK ? null 3374 : intent.getData(); 3375 mUploadMessage.onReceiveValue(result); 3376 mUploadMessage = null; 3377 break; 3378 default: 3379 break; 3380 } 3381 getTopWindow().requestFocus(); 3382 } 3383 3384 /* 3385 * This method is called as a result of the user selecting the options 3386 * menu to see the download window, or when a download changes state. It 3387 * shows the download window ontop of the current window. 3388 */ 3389 /* package */ void viewDownloads(Uri downloadRecord) { 3390 Intent intent = new Intent(this, 3391 BrowserDownloadPage.class); 3392 intent.setData(downloadRecord); 3393 startActivityForResult(intent, BrowserActivity.DOWNLOAD_PAGE); 3394 3395 } 3396 3397 /** 3398 * Open the Go page. 3399 * @param startWithHistory If true, open starting on the history tab. 3400 * Otherwise, start with the bookmarks tab. 3401 */ 3402 /* package */ void bookmarksOrHistoryPicker(boolean startWithHistory) { 3403 WebView current = mTabControl.getCurrentWebView(); 3404 if (current == null) { 3405 return; 3406 } 3407 Intent intent = new Intent(this, 3408 CombinedBookmarkHistoryActivity.class); 3409 String title = current.getTitle(); 3410 String url = current.getUrl(); 3411 Bitmap thumbnail = createScreenshot(current); 3412 3413 // Just in case the user opens bookmarks before a page finishes loading 3414 // so the current history item, and therefore the page, is null. 3415 if (null == url) { 3416 url = mLastEnteredUrl; 3417 // This can happen. 3418 if (null == url) { 3419 url = mSettings.getHomePage(); 3420 } 3421 } 3422 // In case the web page has not yet received its associated title. 3423 if (title == null) { 3424 title = url; 3425 } 3426 intent.putExtra("title", title); 3427 intent.putExtra("url", url); 3428 intent.putExtra("thumbnail", thumbnail); 3429 // Disable opening in a new window if we have maxed out the windows 3430 intent.putExtra("disable_new_window", !mTabControl.canCreateNewTab()); 3431 intent.putExtra("touch_icon_url", current.getTouchIconUrl()); 3432 if (startWithHistory) { 3433 intent.putExtra(CombinedBookmarkHistoryActivity.STARTING_TAB, 3434 CombinedBookmarkHistoryActivity.HISTORY_TAB); 3435 } 3436 startActivityForResult(intent, COMBO_PAGE); 3437 } 3438 3439 // Called when loading from context menu or LOAD_URL message 3440 private void loadURL(WebView view, String url) { 3441 // In case the user enters nothing. 3442 if (url != null && url.length() != 0 && view != null) { 3443 url = smartUrlFilter(url); 3444 if (!view.getWebViewClient().shouldOverrideUrlLoading(view, url)) { 3445 view.loadUrl(url); 3446 } 3447 } 3448 } 3449 3450 private String smartUrlFilter(Uri inUri) { 3451 if (inUri != null) { 3452 return smartUrlFilter(inUri.toString()); 3453 } 3454 return null; 3455 } 3456 3457 protected static final Pattern ACCEPTED_URI_SCHEMA = Pattern.compile( 3458 "(?i)" + // switch on case insensitive matching 3459 "(" + // begin group for schema 3460 "(?:http|https|file):\\/\\/" + 3461 "|(?:inline|data|about|content|javascript):" + 3462 ")" + 3463 "(.*)" ); 3464 3465 /** 3466 * Attempts to determine whether user input is a URL or search 3467 * terms. Anything with a space is passed to search. 3468 * 3469 * Converts to lowercase any mistakenly uppercased schema (i.e., 3470 * "Http://" converts to "http://" 3471 * 3472 * @return Original or modified URL 3473 * 3474 */ 3475 String smartUrlFilter(String url) { 3476 3477 String inUrl = url.trim(); 3478 boolean hasSpace = inUrl.indexOf(' ') != -1; 3479 3480 Matcher matcher = ACCEPTED_URI_SCHEMA.matcher(inUrl); 3481 if (matcher.matches()) { 3482 // force scheme to lowercase 3483 String scheme = matcher.group(1); 3484 String lcScheme = scheme.toLowerCase(); 3485 if (!lcScheme.equals(scheme)) { 3486 inUrl = lcScheme + matcher.group(2); 3487 } 3488 if (hasSpace) { 3489 inUrl = inUrl.replace(" ", "%20"); 3490 } 3491 return inUrl; 3492 } 3493 if (hasSpace) { 3494 // FIXME: Is this the correct place to add to searches? 3495 // what if someone else calls this function? 3496 int shortcut = parseUrlShortcut(inUrl); 3497 if (shortcut != SHORTCUT_INVALID) { 3498 Browser.addSearchUrl(mResolver, inUrl); 3499 String query = inUrl.substring(2); 3500 switch (shortcut) { 3501 case SHORTCUT_GOOGLE_SEARCH: 3502 return URLUtil.composeSearchUrl(query, QuickSearch_G, QUERY_PLACE_HOLDER); 3503 case SHORTCUT_WIKIPEDIA_SEARCH: 3504 return URLUtil.composeSearchUrl(query, QuickSearch_W, QUERY_PLACE_HOLDER); 3505 case SHORTCUT_DICTIONARY_SEARCH: 3506 return URLUtil.composeSearchUrl(query, QuickSearch_D, QUERY_PLACE_HOLDER); 3507 case SHORTCUT_GOOGLE_MOBILE_LOCAL_SEARCH: 3508 // FIXME: we need location in this case 3509 return URLUtil.composeSearchUrl(query, QuickSearch_L, QUERY_PLACE_HOLDER); 3510 } 3511 } 3512 } else { 3513 if (Patterns.WEB_URL.matcher(inUrl).matches()) { 3514 return URLUtil.guessUrl(inUrl); 3515 } 3516 } 3517 3518 Browser.addSearchUrl(mResolver, inUrl); 3519 return URLUtil.composeSearchUrl(inUrl, QuickSearch_G, QUERY_PLACE_HOLDER); 3520 } 3521 3522 /* package */ void setShouldShowErrorConsole(boolean flag) { 3523 if (flag == mShouldShowErrorConsole) { 3524 // Nothing to do. 3525 return; 3526 } 3527 3528 mShouldShowErrorConsole = flag; 3529 3530 ErrorConsoleView errorConsole = mTabControl.getCurrentTab() 3531 .getErrorConsole(true); 3532 3533 if (flag) { 3534 // Setting the show state of the console will cause it's the layout to be inflated. 3535 if (errorConsole.numberOfErrors() > 0) { 3536 errorConsole.showConsole(ErrorConsoleView.SHOW_MINIMIZED); 3537 } else { 3538 errorConsole.showConsole(ErrorConsoleView.SHOW_NONE); 3539 } 3540 3541 // Now we can add it to the main view. 3542 mErrorConsoleContainer.addView(errorConsole, 3543 new LinearLayout.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT, 3544 ViewGroup.LayoutParams.WRAP_CONTENT)); 3545 } else { 3546 mErrorConsoleContainer.removeView(errorConsole); 3547 } 3548 3549 } 3550 3551 boolean shouldShowErrorConsole() { 3552 return mShouldShowErrorConsole; 3553 } 3554 3555 private void setStatusBarVisibility(boolean visible) { 3556 int flag = visible ? 0 : WindowManager.LayoutParams.FLAG_FULLSCREEN; 3557 getWindow().setFlags(flag, WindowManager.LayoutParams.FLAG_FULLSCREEN); 3558 } 3559 3560 final static int LOCK_ICON_UNSECURE = 0; 3561 final static int LOCK_ICON_SECURE = 1; 3562 final static int LOCK_ICON_MIXED = 2; 3563 3564 private BrowserSettings mSettings; 3565 private TabControl mTabControl; 3566 private ContentResolver mResolver; 3567 private FrameLayout mContentView; 3568 private View mCustomView; 3569 private FrameLayout mCustomViewContainer; 3570 private WebChromeClient.CustomViewCallback mCustomViewCallback; 3571 3572 // FIXME, temp address onPrepareMenu performance problem. When we move everything out of 3573 // view, we should rewrite this. 3574 private int mCurrentMenuState = 0; 3575 private int mMenuState = R.id.MAIN_MENU; 3576 private int mOldMenuState = EMPTY_MENU; 3577 private static final int EMPTY_MENU = -1; 3578 private Menu mMenu; 3579 3580 private FindDialog mFindDialog; 3581 // Used to prevent chording to result in firing two shortcuts immediately 3582 // one after another. Fixes bug 1211714. 3583 boolean mCanChord; 3584 3585 private boolean mInLoad; 3586 private boolean mIsNetworkUp; 3587 private boolean mDidStopLoad; 3588 3589 private boolean mActivityInPause = true; 3590 3591 private boolean mMenuIsDown; 3592 3593 private static boolean mInTrace; 3594 3595 // Performance probe 3596 private static final int[] SYSTEM_CPU_FORMAT = new int[] { 3597 Process.PROC_SPACE_TERM | Process.PROC_COMBINE, 3598 Process.PROC_SPACE_TERM | Process.PROC_OUT_LONG, // 1: user time 3599 Process.PROC_SPACE_TERM | Process.PROC_OUT_LONG, // 2: nice time 3600 Process.PROC_SPACE_TERM | Process.PROC_OUT_LONG, // 3: sys time 3601 Process.PROC_SPACE_TERM | Process.PROC_OUT_LONG, // 4: idle time 3602 Process.PROC_SPACE_TERM | Process.PROC_OUT_LONG, // 5: iowait time 3603 Process.PROC_SPACE_TERM | Process.PROC_OUT_LONG, // 6: irq time 3604 Process.PROC_SPACE_TERM | Process.PROC_OUT_LONG // 7: softirq time 3605 }; 3606 3607 private long mStart; 3608 private long mProcessStart; 3609 private long mUserStart; 3610 private long mSystemStart; 3611 private long mIdleStart; 3612 private long mIrqStart; 3613 3614 private long mUiStart; 3615 3616 private Drawable mMixLockIcon; 3617 private Drawable mSecLockIcon; 3618 3619 /* hold a ref so we can auto-cancel if necessary */ 3620 private AlertDialog mAlertDialog; 3621 3622 // Wait for credentials before loading google.com 3623 private ProgressDialog mCredsDlg; 3624 3625 // The up-to-date URL and title (these can be different from those stored 3626 // in WebView, since it takes some time for the information in WebView to 3627 // get updated) 3628 private String mUrl; 3629 private String mTitle; 3630 3631 // As PageInfo has different style for landscape / portrait, we have 3632 // to re-open it when configuration changed 3633 private AlertDialog mPageInfoDialog; 3634 private Tab mPageInfoView; 3635 // If the Page-Info dialog is launched from the SSL-certificate-on-error 3636 // dialog, we should not just dismiss it, but should get back to the 3637 // SSL-certificate-on-error dialog. This flag is used to store this state 3638 private Boolean mPageInfoFromShowSSLCertificateOnError; 3639 3640 // as SSLCertificateOnError has different style for landscape / portrait, 3641 // we have to re-open it when configuration changed 3642 private AlertDialog mSSLCertificateOnErrorDialog; 3643 private WebView mSSLCertificateOnErrorView; 3644 private SslErrorHandler mSSLCertificateOnErrorHandler; 3645 private SslError mSSLCertificateOnErrorError; 3646 3647 // as SSLCertificate has different style for landscape / portrait, we 3648 // have to re-open it when configuration changed 3649 private AlertDialog mSSLCertificateDialog; 3650 private Tab mSSLCertificateView; 3651 3652 // as HttpAuthentication has different style for landscape / portrait, we 3653 // have to re-open it when configuration changed 3654 private AlertDialog mHttpAuthenticationDialog; 3655 private HttpAuthHandler mHttpAuthHandler; 3656 3657 /*package*/ static final FrameLayout.LayoutParams COVER_SCREEN_PARAMS = 3658 new FrameLayout.LayoutParams( 3659 ViewGroup.LayoutParams.FILL_PARENT, 3660 ViewGroup.LayoutParams.FILL_PARENT); 3661 /*package*/ static final FrameLayout.LayoutParams COVER_SCREEN_GRAVITY_CENTER = 3662 new FrameLayout.LayoutParams( 3663 ViewGroup.LayoutParams.FILL_PARENT, 3664 ViewGroup.LayoutParams.FILL_PARENT, 3665 Gravity.CENTER); 3666 // Google search 3667 final static String QuickSearch_G = "http://www.google.com/m?q=%s"; 3668 // Wikipedia search 3669 final static String QuickSearch_W = "http://en.wikipedia.org/w/index.php?search=%s&go=Go"; 3670 // Dictionary search 3671 final static String QuickSearch_D = "http://dictionary.reference.com/search?q=%s"; 3672 // Google Mobile Local search 3673 final static String QuickSearch_L = "http://www.google.com/m/search?site=local&q=%s&near=mountain+view"; 3674 3675 final static String QUERY_PLACE_HOLDER = "%s"; 3676 3677 // "source" parameter for Google search through search key 3678 final static String GOOGLE_SEARCH_SOURCE_SEARCHKEY = "browser-key"; 3679 // "source" parameter for Google search through goto menu 3680 final static String GOOGLE_SEARCH_SOURCE_GOTO = "browser-goto"; 3681 // "source" parameter for Google search through simplily type 3682 final static String GOOGLE_SEARCH_SOURCE_TYPE = "browser-type"; 3683 // "source" parameter for Google search suggested by the browser 3684 final static String GOOGLE_SEARCH_SOURCE_SUGGEST = "browser-suggest"; 3685 // "source" parameter for Google search from unknown source 3686 final static String GOOGLE_SEARCH_SOURCE_UNKNOWN = "unknown"; 3687 3688 private final static String LOGTAG = "browser"; 3689 3690 private String mLastEnteredUrl; 3691 3692 private PowerManager.WakeLock mWakeLock; 3693 private final static int WAKELOCK_TIMEOUT = 5 * 60 * 1000; // 5 minutes 3694 3695 private Toast mStopToast; 3696 3697 private TitleBar mTitleBar; 3698 3699 private LinearLayout mErrorConsoleContainer = null; 3700 private boolean mShouldShowErrorConsole = false; 3701 3702 // As the ids are dynamically created, we can't guarantee that they will 3703 // be in sequence, so this static array maps ids to a window number. 3704 final static private int[] WINDOW_SHORTCUT_ID_ARRAY = 3705 { R.id.window_one_menu_id, R.id.window_two_menu_id, R.id.window_three_menu_id, 3706 R.id.window_four_menu_id, R.id.window_five_menu_id, R.id.window_six_menu_id, 3707 R.id.window_seven_menu_id, R.id.window_eight_menu_id }; 3708 3709 // monitor platform changes 3710 private IntentFilter mNetworkStateChangedFilter; 3711 private BroadcastReceiver mNetworkStateIntentReceiver; 3712 3713 private BroadcastReceiver mPackageInstallationReceiver; 3714 3715 // activity requestCode 3716 final static int COMBO_PAGE = 1; 3717 final static int DOWNLOAD_PAGE = 2; 3718 final static int PREFERENCES_PAGE = 3; 3719 final static int FILE_SELECTED = 4; 3720 3721 // the default <video> poster 3722 private Bitmap mDefaultVideoPoster; 3723 // the video progress view 3724 private View mVideoProgressView; 3725 3726 /** 3727 * A UrlData class to abstract how the content will be set to WebView. 3728 * This base class uses loadUrl to show the content. 3729 */ 3730 private static class UrlData { 3731 String mUrl; 3732 byte[] mPostData; 3733 3734 UrlData(String url) { 3735 this.mUrl = url; 3736 } 3737 3738 void setPostData(byte[] postData) { 3739 mPostData = postData; 3740 } 3741 3742 boolean isEmpty() { 3743 return mUrl == null || mUrl.length() == 0; 3744 } 3745 3746 public void loadIn(WebView webView) { 3747 if (mPostData != null) { 3748 webView.postUrl(mUrl, mPostData); 3749 } else { 3750 webView.loadUrl(mUrl); 3751 } 3752 } 3753 }; 3754 3755 /** 3756 * A subclass of UrlData class that can display inlined content using 3757 * {@link WebView#loadDataWithBaseURL(String, String, String, String, String)}. 3758 */ 3759 private static class InlinedUrlData extends UrlData { 3760 InlinedUrlData(String inlined, String mimeType, String encoding, String failUrl) { 3761 super(failUrl); 3762 mInlined = inlined; 3763 mMimeType = mimeType; 3764 mEncoding = encoding; 3765 } 3766 String mMimeType; 3767 String mInlined; 3768 String mEncoding; 3769 @Override 3770 boolean isEmpty() { 3771 return mInlined == null || mInlined.length() == 0 || super.isEmpty(); 3772 } 3773 3774 @Override 3775 public void loadIn(WebView webView) { 3776 webView.loadDataWithBaseURL(null, mInlined, mMimeType, mEncoding, mUrl); 3777 } 3778 } 3779 3780 /* package */ static final UrlData EMPTY_URL_DATA = new UrlData(null); 3781} 3782