blob: fc4acfc9cf8883c4683ee8d39bd4186e952ff47a [file] [log] [blame]
The Android Open Source Projectba6d7b82008-10-21 07:00:00 -07001/*
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.ActivityManager;
21import android.app.AlertDialog;
22import android.app.SearchManager;
23import android.app.ProgressDialog;
24import android.content.ActivityNotFoundException;
25import android.content.res.AssetManager;
26import android.content.BroadcastReceiver;
27import android.content.ComponentName;
28import android.content.ContentResolver;
29import android.content.ContentValues;
30import android.content.Context;
31import android.content.DialogInterface.OnCancelListener;
32import android.content.DialogInterface;
33import android.content.Intent;
34import android.content.IntentFilter;
35import android.content.ServiceConnection;
36import android.content.pm.ActivityInfo;
37import android.content.pm.PackageManager;
38import android.content.pm.ResolveInfo;
39import android.content.res.Configuration;
40import android.content.res.Resources;
41import android.database.Cursor;
42import android.database.sqlite.SQLiteDatabase;
43import android.database.sqlite.SQLiteException;
44import android.graphics.Bitmap;
45import android.graphics.Canvas;
46import android.graphics.Color;
47import android.graphics.DrawFilter;
48import android.graphics.Paint;
49import android.graphics.PaintFlagsDrawFilter;
50import android.graphics.Picture;
51import android.graphics.drawable.BitmapDrawable;
52import android.graphics.drawable.Drawable;
53import android.graphics.drawable.LayerDrawable;
54import android.graphics.drawable.PaintDrawable;
55import android.hardware.SensorListener;
56import android.hardware.SensorManager;
57import android.net.ConnectivityManager;
58import android.net.NetworkInfo;
59import android.net.Uri;
60import android.net.WebAddress;
61import android.net.http.EventHandler;
62import android.net.http.RequestQueue;
63import android.net.http.SslCertificate;
64import android.net.http.SslError;
65import android.os.Build;
66import android.os.Bundle;
67import android.os.Debug;
68import android.os.Environment;
69import android.os.Handler;
70import android.os.IBinder;
71import android.os.Message;
72import android.os.PowerManager;
73import android.os.Process;
74import android.os.RemoteException;
75import android.os.ServiceManager;
76import android.os.SystemClock;
77import android.os.SystemProperties;
78import android.pim.DateFormat;
79import android.provider.Browser;
80import android.provider.Checkin;
81import android.provider.Contacts.Intents.Insert;
82import android.provider.Contacts;
83import android.provider.Downloads;
84import android.text.IClipboard;
85import android.text.util.Regex;
86import android.util.Config;
87import android.util.Log;
88import android.view.ContextMenu;
89import android.view.ContextMenu.ContextMenuInfo;
90import android.view.MenuItem.OnMenuItemClickListener;
91import android.view.Gravity;
92import android.view.KeyEvent;
93import android.view.LayoutInflater;
94import android.view.Menu;
95import android.view.MenuInflater;
96import android.view.MenuItem;
97import android.view.MotionEvent;
98import android.view.View;
99import android.view.ViewGroup;
100import android.view.Window;
101import android.view.animation.AlphaAnimation;
102import android.view.animation.Animation;
103import android.view.animation.AnimationSet;
104import android.view.animation.AnimationUtils;
105import android.view.animation.DecelerateInterpolator;
106import android.view.animation.ScaleAnimation;
107import android.view.animation.TranslateAnimation;
108import android.webkit.CookieManager;
109import android.webkit.CookieSyncManager;
110import android.webkit.DownloadListener;
111import android.webkit.HttpAuthHandler;
112import android.webkit.JsPromptResult;
113import android.webkit.JsResult;
114import android.webkit.SslErrorHandler;
115import android.webkit.URLUtil;
116import android.webkit.WebChromeClient;
117import android.webkit.WebHistoryItem;
118import android.webkit.WebIconDatabase;
119import android.webkit.WebView;
120import android.webkit.WebViewClient;
121import android.widget.EditText;
122import android.widget.FrameLayout;
123import android.widget.LinearLayout;
124import android.widget.TextView;
125import android.widget.Toast;
126
127import com.google.android.googleapps.IGoogleLoginService;
128import com.google.android.googlelogin.GoogleLoginServiceConstants;
129
130import java.io.BufferedOutputStream;
131import java.io.File;
132import java.io.FileInputStream;
133import java.io.FileOutputStream;
134import java.io.InputStream;
135import java.io.IOException;
136import java.net.MalformedURLException;
137import java.net.URL;
138import java.net.URLEncoder;
139import java.text.ParseException;
140import java.util.Date;
141import java.util.Enumeration;
142import java.util.HashMap;
143import java.util.HashSet;
144import java.util.List;
145import java.util.regex.Matcher;
146import java.util.regex.Pattern;
147import java.util.Vector;
148import java.util.zip.ZipEntry;
149import java.util.zip.ZipFile;
150import java.util.zip.ZipInputStream;
151
152public class BrowserActivity extends Activity
153 implements KeyTracker.OnKeyTracker,
154 View.OnCreateContextMenuListener,
155 DownloadListener {
156
157 private IGoogleLoginService mGls = null;
158 private ServiceConnection mGlsConnection = null;
159
160 private SensorManager mSensorManager = null;
161
162 /* Whitelisted webpages
163 private static HashSet<String> sWhiteList;
164
165 static {
166 sWhiteList = new HashSet<String>();
167 sWhiteList.add("cnn.com/");
168 sWhiteList.add("espn.go.com/");
169 sWhiteList.add("nytimes.com/");
170 sWhiteList.add("engadget.com/");
171 sWhiteList.add("yahoo.com/");
172 sWhiteList.add("msn.com/");
173 sWhiteList.add("amazon.com/");
174 sWhiteList.add("consumerist.com/");
175 sWhiteList.add("google.com/m/news");
176 }
177 */
178
179 private void setupHomePage() {
180 final Runnable getAccount = new Runnable() {
181 public void run() {
182 // get the default home page
183 String homepage = mSettings.getHomePage();
184
185 try {
186 if (mGls == null) return;
187
188 String hostedUser = mGls.getAccount(GoogleLoginServiceConstants.PREFER_HOSTED);
189 String googleUser = mGls.getAccount(GoogleLoginServiceConstants.REQUIRE_GOOGLE);
190
191 // three cases:
192 //
193 // hostedUser == googleUser
194 // The device has only a google account
195 //
196 // hostedUser != googleUser
197 // The device has a hosted account and a google account
198 //
199 // hostedUser != null, googleUser == null
200 // The device has only a hosted account (so far)
201
202 // developers might have no accounts at all
203 if (hostedUser == null) return;
204
205 if (googleUser == null || !hostedUser.equals(googleUser)) {
206 String domain = hostedUser.substring(hostedUser.lastIndexOf('@')+1);
207 homepage = "http://www.google.com/m/a/" + domain;
208 }
209 } catch (RemoteException ignore) {
210 // Login service died; carry on
211 } catch (RuntimeException ignore) {
212 // Login service died; carry on
213 } finally {
214 finish(homepage);
215 }
216 }
217
218 private void finish(final String homepage) {
219 mHandler.post(new Runnable() {
220 public void run() {
221 mSettings.setHomePage(BrowserActivity.this, homepage);
222 resumeAfterCredentials();
223
224 // as this is running in a separate thread,
225 // BrowserActivity's onDestroy() may have been called,
226 // which also calls unbindService().
227 if (mGlsConnection != null) {
228 // we no longer need to keep GLS open
229 unbindService(mGlsConnection);
230 mGlsConnection = null;
231 }
232 } });
233 } };
234
235 final boolean[] done = { false };
236
237 // Open a connection to the Google Login Service. The first
238 // time the connection is established, set up the homepage depending on
239 // the account in a background thread.
240 mGlsConnection = new ServiceConnection() {
241 public void onServiceConnected(ComponentName className, IBinder service) {
242 mGls = IGoogleLoginService.Stub.asInterface(service);
243 if (done[0] == false) {
244 done[0] = true;
245 new Thread(getAccount).start();
246 }
247 }
248 public void onServiceDisconnected(ComponentName className) {
249 mGls = null;
250 }
251 };
252
253 bindService(GoogleLoginServiceConstants.SERVICE_INTENT,
254 mGlsConnection, Context.BIND_AUTO_CREATE);
255 }
256
257 /**
258 * This class is in charge of installing pre-packaged plugins
259 * from the Browser assets directory to the user's data partition.
260 * Plugins are loaded from the "plugins" directory in the assets;
261 * Anything that is in this directory will be copied over to the
262 * user data partition in app_plugins.
263 */
264 private class CopyPlugins implements Runnable {
265 final static String TAG = "PluginsInstaller";
266 final static String ZIP_FILTER = "assets/plugins/";
267 final static String APK_PATH = "/system/app/Browser.apk";
268 final static String PLUGIN_EXTENSION = ".so";
269 final static String TEMPORARY_EXTENSION = "_temp";
270 final static String BUILD_INFOS_FILE = "build.prop";
271 final static String SYSTEM_BUILD_INFOS_FILE = "/system/"
272 + BUILD_INFOS_FILE;
273 final int BUFSIZE = 4096;
274 boolean mDoOverwrite = false;
275 String pluginsPath;
276 Context mContext;
277 File pluginsDir;
278 AssetManager manager;
279
280 public CopyPlugins (boolean overwrite, Context context) {
281 mDoOverwrite = overwrite;
282 mContext = context;
283 }
284
285 /**
286 * Returned a filtered list of ZipEntry.
287 * We list all the files contained in the zip and
288 * only returns the ones starting with the ZIP_FILTER
289 * path.
290 *
291 * @param zip the zip file used.
292 */
293 public Vector<ZipEntry> pluginsFilesFromZip(ZipFile zip) {
294 Vector<ZipEntry> list = new Vector<ZipEntry>();
295 Enumeration entries = zip.entries();
296 while (entries.hasMoreElements()) {
297 ZipEntry entry = (ZipEntry) entries.nextElement();
298 if (entry.getName().startsWith(ZIP_FILTER)) {
299 list.add(entry);
300 }
301 }
302 return list;
303 }
304
305 /**
306 * Utility method to copy the content from an inputstream
307 * to a file output stream.
308 */
309 public void copyStreams(InputStream is, FileOutputStream fos) {
310 BufferedOutputStream os = null;
311 try {
312 byte data[] = new byte[BUFSIZE];
313 int count;
314 os = new BufferedOutputStream(fos, BUFSIZE);
315 while ((count = is.read(data, 0, BUFSIZE)) != -1) {
316 os.write(data, 0, count);
317 }
318 os.flush();
319 } catch (IOException e) {
320 Log.e(TAG, "Exception while copying: " + e);
321 } finally {
322 try {
323 if (os != null) {
324 os.close();
325 }
326 } catch (IOException e2) {
327 Log.e(TAG, "Exception while closing the stream: " + e2);
328 }
329 }
330 }
331
332 /**
333 * Returns a string containing the contents of a file
334 *
335 * @param file the target file
336 */
337 private String contentsOfFile(File file) {
338 String ret = null;
339 FileInputStream is = null;
340 try {
341 byte[] buffer = new byte[BUFSIZE];
342 int count;
343 is = new FileInputStream(file);
344 StringBuffer out = new StringBuffer();
345
346 while ((count = is.read(buffer, 0, BUFSIZE)) != -1) {
347 out.append(new String(buffer, 0, count));
348 }
349 ret = out.toString();
350 } catch (IOException e) {
351 Log.e(TAG, "Exception getting contents of file " + e);
352 } finally {
353 if (is != null) {
354 try {
355 is.close();
356 } catch (IOException e2) {
357 Log.e(TAG, "Exception while closing the file: " + e2);
358 }
359 }
360 }
361 return ret;
362 }
363
364 /**
365 * Utility method to initialize the user data plugins path.
366 */
367 public void initPluginsPath() {
368 BrowserSettings s = BrowserSettings.getInstance();
369 pluginsPath = s.getPluginsPath();
370 if (pluginsPath == null) {
371 s.loadFromDb(mContext);
372 pluginsPath = s.getPluginsPath();
373 }
374 if (Config.LOGV) {
375 Log.v(TAG, "Plugin path: " + pluginsPath);
376 }
377 }
378
379 /**
380 * Utility method to delete a file or a directory
381 *
382 * @param file the File to delete
383 */
384 public void deleteFile(File file) {
385 File[] files = file.listFiles();
386 if ((files != null) && files.length > 0) {
387 for (int i=0; i< files.length; i++) {
388 deleteFile(files[i]);
389 }
390 }
391 if (!file.delete()) {
392 Log.e(TAG, file.getPath() + " could not get deleted");
393 }
394 }
395
396 /**
397 * Clean the content of the plugins directory.
398 * We delete the directory, then recreate it.
399 */
400 public void cleanPluginsDirectory() {
401 if (Config.LOGV) {
402 Log.v(TAG, "delete plugins directory: " + pluginsPath);
403 }
404 File pluginsDirectory = new File(pluginsPath);
405 deleteFile(pluginsDirectory);
406 pluginsDirectory.mkdir();
407 }
408
409
410 /**
411 * Copy the SYSTEM_BUILD_INFOS_FILE file containing the
412 * informations about the system build to the
413 * BUILD_INFOS_FILE in the plugins directory.
414 */
415 public void copyBuildInfos() {
416 try {
417 if (Config.LOGV) {
418 Log.v(TAG, "Copy build infos to the plugins directory");
419 }
420 File buildInfoFile = new File(SYSTEM_BUILD_INFOS_FILE);
421 File buildInfoPlugins = new File(pluginsPath, BUILD_INFOS_FILE);
422 copyStreams(new FileInputStream(buildInfoFile),
423 new FileOutputStream(buildInfoPlugins));
424 } catch (IOException e) {
425 Log.e(TAG, "Exception while copying the build infos: " + e);
426 }
427 }
428
429 /**
430 * Returns true if the current system is newer than the
431 * system that installed the plugins.
432 * We determinate this by checking the build number of the system.
433 *
434 * At the end of the plugins copy operation, we copy the
435 * SYSTEM_BUILD_INFOS_FILE to the BUILD_INFOS_FILE.
436 * We then just have to load both and compare them -- if they
437 * are different the current system is newer.
438 *
439 * Loading and comparing the strings should be faster than
440 * creating a hash, the files being rather small. Extracting the
441 * version number would require some parsing which may be more
442 * brittle.
443 */
444 public boolean newSystemImage() {
445 try {
446 File buildInfoFile = new File(SYSTEM_BUILD_INFOS_FILE);
447 File buildInfoPlugins = new File(pluginsPath, BUILD_INFOS_FILE);
448 if (!buildInfoPlugins.exists()) {
449 if (Config.LOGV) {
450 Log.v(TAG, "build.prop in plugins directory " + pluginsPath
451 + " does not exist, therefore it's a new system image");
452 }
453 return true;
454 } else {
455 String buildInfo = contentsOfFile(buildInfoFile);
456 String buildInfoPlugin = contentsOfFile(buildInfoPlugins);
457 if (buildInfo == null || buildInfoPlugin == null
458 || buildInfo.compareTo(buildInfoPlugin) != 0) {
459 if (Config.LOGV) {
460 Log.v(TAG, "build.prop are different, "
461 + " therefore it's a new system image");
462 }
463 return true;
464 }
465 }
466 } catch (Exception e) {
467 Log.e(TAG, "Exc in newSystemImage(): " + e);
468 }
469 return false;
470 }
471
472 /**
473 * Check if the version of the plugins contained in the
474 * Browser assets is the same as the version of the plugins
475 * in the plugins directory.
476 * We simply iterate on every file in the assets/plugins
477 * and return false if a file listed in the assets does
478 * not exist in the plugins directory.
479 */
480 private boolean checkIsDifferentVersions() {
481 try {
482 ZipFile zip = new ZipFile(APK_PATH);
483 Vector<ZipEntry> files = pluginsFilesFromZip(zip);
484 int zipFilterLength = ZIP_FILTER.length();
485
486 Enumeration entries = files.elements();
487 while (entries.hasMoreElements()) {
488 ZipEntry entry = (ZipEntry) entries.nextElement();
489 String path = entry.getName().substring(zipFilterLength);
490 File outputFile = new File(pluginsPath, path);
491 if (!outputFile.exists()) {
492 if (Config.LOGV) {
493 Log.v(TAG, "checkIsDifferentVersions(): extracted file "
494 + path + " does not exist, we have a different version");
495 }
496 return true;
497 }
498 }
499 } catch (IOException e) {
500 Log.e(TAG, "Exception in checkDifferentVersions(): " + e);
501 }
502 return false;
503 }
504
505 /**
506 * Copy every files from the assets/plugins directory
507 * to the app_plugins directory in the data partition.
508 * Once copied, we copy over the SYSTEM_BUILD_INFOS file
509 * in the plugins directory.
510 *
511 * NOTE: we directly access the content from the Browser
512 * package (it's a zip file) and do not use AssetManager
513 * as there is a limit of 1Mb (see Asset.h)
514 */
515 public void run() {
516 try {
517 if (pluginsPath == null) {
518 Log.e(TAG, "No plugins path found!");
519 return;
520 }
521
522 ZipFile zip = new ZipFile(APK_PATH);
523 Vector<ZipEntry> files = pluginsFilesFromZip(zip);
524 Vector<File> plugins = new Vector<File>();
525 int zipFilterLength = ZIP_FILTER.length();
526
527 Enumeration entries = files.elements();
528 while (entries.hasMoreElements()) {
529 ZipEntry entry = (ZipEntry) entries.nextElement();
530 String path = entry.getName().substring(zipFilterLength);
531 File outputFile = new File(pluginsPath, path);
532 outputFile.getParentFile().mkdirs();
533
534 if (outputFile.exists() && !mDoOverwrite) {
535 if (Config.LOGV) {
536 Log.v(TAG, path + " already extracted.");
537 }
538 } else {
539 if (path.endsWith(PLUGIN_EXTENSION)) {
540 // We rename plugins to be sure a half-copied
541 // plugin is not loaded by the browser.
542 plugins.add(outputFile);
543 outputFile = new File(pluginsPath,
544 path + TEMPORARY_EXTENSION);
545 }
546 FileOutputStream fos = new FileOutputStream(outputFile);
547 if (Config.LOGV) {
548 Log.v(TAG, "copy " + entry + " to "
549 + pluginsPath + "/" + path);
550 }
551 copyStreams(zip.getInputStream(entry), fos);
552 }
553 }
554
555 // We now rename the .so we copied, once all their resources
556 // are safely copied over to the user data partition.
557 Enumeration elems = plugins.elements();
558 while (elems.hasMoreElements()) {
559 File renamedFile = (File) elems.nextElement();
560 File sourceFile = new File(renamedFile.getPath()
561 + TEMPORARY_EXTENSION);
562 if (Config.LOGV) {
563 Log.v(TAG, "rename " + sourceFile.getPath()
564 + " to " + renamedFile.getPath());
565 }
566 sourceFile.renameTo(renamedFile);
567 }
568
569 copyBuildInfos();
570
571 // Refresh the plugin list.
572 if (mWebView != null)
573 mWebView.refreshPlugins(false);
574 } catch (IOException e) {
575 Log.e(TAG, "IO Exception: " + e);
576 }
577 }
578 };
579
580 /**
581 * Copy the content of assets/plugins/ to the app_plugins directory
582 * in the data partition.
583 *
584 * This function is called every time the browser is started.
585 * We first check if the system image is newer than the one that
586 * copied the plugins (if there's plugins in the data partition).
587 * If this is the case, we then check if the versions are different.
588 * If they are different, we clean the plugins directory in the
589 * data partition, then start a thread to copy the plugins while
590 * the browser continue to load.
591 *
592 * @param overwrite if true overwrite the files even if they are
593 * already present (to let the user "reset" the plugins if needed).
594 */
595 private void copyPlugins(boolean overwrite) {
596 CopyPlugins copyPluginsFromAssets = new CopyPlugins(overwrite, this);
597 copyPluginsFromAssets.initPluginsPath();
598 if (copyPluginsFromAssets.newSystemImage()) {
599 if (copyPluginsFromAssets.checkIsDifferentVersions()) {
600 copyPluginsFromAssets.cleanPluginsDirectory();
601 new Thread(copyPluginsFromAssets).start();
602 }
603 }
604 }
605
606
607 @Override public void onCreate(Bundle icicle) {
608 if (Config.LOGV) {
609 Log.v(LOGTAG, this + " onStart");
610 }
611 super.onCreate(icicle);
612 this.requestWindowFeature(Window.FEATURE_LEFT_ICON);
613 this.requestWindowFeature(Window.FEATURE_RIGHT_ICON);
614 this.requestWindowFeature(Window.FEATURE_PROGRESS);
615 this.requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
616
617 // test the browser in OpenGL
618 // requestWindowFeature(Window.FEATURE_OPENGL);
619
620 setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
621
622 mResolver = getContentResolver();
623
624 //
625 // start MASF proxy service
626 //
627 //Intent proxyServiceIntent = new Intent();
628 //proxyServiceIntent.setComponent
629 // (new ComponentName(
630 // "com.android.masfproxyservice",
631 // "com.android.masfproxyservice.MasfProxyService"));
632 //startService(proxyServiceIntent, null);
633
634 mSecLockIcon = Resources.getSystem().getDrawable(
635 android.R.drawable.ic_secure);
636 mMixLockIcon = Resources.getSystem().getDrawable(
637 android.R.drawable.ic_partial_secure);
638 mGenericFavicon = getResources().getDrawable(
639 R.drawable.app_web_browser_sm);
640
641
642 mContentView = new FrameLayout(this);
643
644 setContentView(mContentView);
645
646 // Create the tab control and our initial tab
647 mTabControl = new TabControl(this);
648
649 // Open the icon database and retain all the bookmark urls for favicons
650 retainIconsOnStartup();
651
652 // Keep a settings instance handy.
653 mSettings = BrowserSettings.getInstance();
654 mSettings.setTabControl(mTabControl);
655 mSettings.loadFromDb(this);
656
657 PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
658 mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Browser");
659
660 if (!mTabControl.restoreState(icicle)) {
661 final Intent intent = getIntent();
662 final Bundle extra = intent.getExtras();
663 // Create an initial tab.
664 final TabControl.Tab t = mTabControl.createNewTab();
665 mTabControl.setCurrentTab(t);
666 // This is one of the only places we call attachTabToContentView
667 // without animating from the tab picker.
668 attachTabToContentView(t);
669 mWebView = t.getWebView();
670 if (extra != null) {
671 int scale = extra.getInt(Browser.INITIAL_ZOOM_LEVEL, 0);
672 if (scale > 0 && scale <= 1000) {
673 mWebView.setInitialScale(scale);
674 }
675 }
676 // If we are not restoring from an icicle, then there is a high
677 // likely hood this is the first run. So, check to see if the
678 // homepage needs to be configured and copy any plugins from our
679 // asset directory to the data partition.
680 if ((extra == null || !extra.getBoolean("testing"))
681 && !mSettings.isLoginInitialized()) {
682 setupHomePage();
683 }
684 copyPlugins(true);
685
686 String url = getUrlFromIntent(intent);
687 if (url == null || url.length() == 0) {
688 if (mSettings.isLoginInitialized()) {
689 mWebView.loadUrl(mSettings.getHomePage());
690 } else {
691 waitForCredentials();
692 }
693 } else {
694 mWebView.loadUrl(url);
695 }
696 } else {
697 // TabControl.restoreState() will create a new tab even if
698 // restoring the state fails. Attach it to the view here since we
699 // are not animating from the tab picker.
700 attachTabToContentView(mTabControl.getCurrentTab());
701 mWebView = mTabControl.getCurrentWebView();
702 }
703
704 /* enables registration for changes in network status from
705 http stack */
706 mNetworkStateChangedFilter = new IntentFilter();
707 mNetworkStateChangedFilter.addAction(
708 RequestQueue.HTTP_NETWORK_STATE_CHANGED_INTENT);
709 mNetworkStateIntentReceiver = new BroadcastReceiver() {
710 @Override
711 public void onReceive(Context context, Intent intent) {
712 if (intent.getAction().equals(
713 RequestQueue.HTTP_NETWORK_STATE_CHANGED_INTENT)) {
714 Boolean up = (Boolean)intent.getExtra(
715 RequestQueue.HTTP_NETWORK_STATE_UP);
716 onNetworkToggle(up);
717 }
718 }
719 };
720 setRequestedOrientation(mSettings.getOrientation());
721 }
722
723 @Override
724 protected void onNewIntent(Intent intent) {
725 if (mWebView == null) {
726 return;
727 }
728 final String action = intent.getAction();
729 final int flags = intent.getFlags();
730 if (Intent.ACTION_MAIN.equals(action) ||
731 (flags & Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY) != 0) {
732 // just resume the browser
733 return;
734 }
735 if (Intent.ACTION_VIEW.equals(action)
736 || Intent.ACTION_SEARCH.equals(action)
737 || Intent.ACTION_WEB_SEARCH.equals(action)) {
738 String url = getUrlFromIntent(intent);
739 if (url == null || url.length() == 0) {
740 url = mSettings.getHomePage();
741 }
742 if (Intent.ACTION_VIEW.equals(action) &&
743 (flags & Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT) != 0) {
744 // if FLAG_ACTIVITY_BROUGHT_TO_FRONT flag is on, the url will be
745 // opened in a new tab unless we have reached MAX_TABS and the
746 // url will be opened in the current tab
747 openTabAndShow(url, null);
748 } else {
749 if ("about:debug".equals(url)) {
750 mSettings.toggleDebugSettings();
751 return;
752 }
753 final TabControl.Tab currentTab = mTabControl.getCurrentTab();
754 // If the Window overview is up and we are not in the midst of
755 // an animation, animate away from the Window overview.
756 if (mTabOverview != null && mAnimationCount == 0) {
757 sendAnimateFromOverview(currentTab, false, url,
758 TAB_OVERVIEW_DELAY, null);
759 } else {
760 // Get rid of the subwindow if it exists
761 dismissSubWindow(currentTab);
762 mWebView.loadUrl(url);
763 }
764 }
765 }
766 }
767
768 private String getUrlFromIntent(Intent intent) {
769 String url = null;
770 if (intent != null) {
771 final String action = intent.getAction();
772 if (Intent.ACTION_VIEW.equals(action)) {
773 url = smartUrlFilter(intent.getData());
774 if (url != null && url.startsWith("content:")) {
775 /* Append mimetype so webview knows how to display */
776 String mimeType = intent.resolveType(getContentResolver());
777 if (mimeType != null) {
778 url += "?" + mimeType;
779 }
780 }
781 } else if (Intent.ACTION_SEARCH.equals(action)
782 || Intent.ACTION_WEB_SEARCH.equals(action)) {
783 url = intent.getStringExtra(SearchManager.QUERY);
784 mLastEnteredUrl = url;
785 // Don't add Urls, just search terms.
786 // Urls will get added when the page is loaded.
787 if (!Regex.WEB_URL_PATTERN.matcher(url).matches()) {
788 Browser.updateVisitedHistory(mResolver, url, false);
789 }
790 // In general, we shouldn't modify URL from Intent.
791 // But currently, we get the user-typed URL from search box as well.
792 url = fixUrl(url);
793 url = smartUrlFilter(url);
794 }
795 }
796 return url;
797 }
798
799 private String fixUrl(String inUrl) {
800 if (inUrl.startsWith("http://") || inUrl.startsWith("https://"))
801 return inUrl;
802 if (inUrl.startsWith("http:") ||
803 inUrl.startsWith("https:")) {
804 if (inUrl.startsWith("http:/") || inUrl.startsWith("https:/")) {
805 inUrl = inUrl.replaceFirst("/", "//");
806 } else inUrl = inUrl.replaceFirst(":", "://");
807 }
808 return inUrl;
809 }
810
811 /**
812 * Looking for the pattern like this
813 *
814 * *
815 * * *
816 * *** * *******
817 * * *
818 * * *
819 * *
820 */
821 private final SensorListener mSensorListener = new SensorListener() {
822 private long mLastGestureTime;
823 private float[] mPrev = new float[3];
824 private float[] mPrevDiff = new float[3];
825 private float[] mDiff = new float[3];
826 private float[] mRevertDiff = new float[3];
827
828 public void onSensorChanged(int sensor, float[] values) {
829 boolean show = false;
830 float[] diff = new float[3];
831
832 for (int i = 0; i < 3; i++) {
833 diff[i] = values[i] - mPrev[i];
834 if (Math.abs(diff[i]) > 1) {
835 show = true;
836 }
837 if ((diff[i] > 1.0 && mDiff[i] < 0.2)
838 || (diff[i] < -1.0 && mDiff[i] > -0.2)) {
839 // start track when there is a big move, or revert
840 mRevertDiff[i] = mDiff[i];
841 mDiff[i] = 0;
842 } else if (diff[i] > -0.2 && diff[i] < 0.2) {
843 // reset when it is flat
844 mDiff[i] = mRevertDiff[i] = 0;
845 }
846 mDiff[i] += diff[i];
847 mPrevDiff[i] = diff[i];
848 mPrev[i] = values[i];
849 }
850
851 if (false) {
852 // only shows if we think the delta is big enough, in an attempt
853 // to detect "serious" moves left/right or up/down
854 Log.d("BrowserSensorHack", "sensorChanged " + sensor + " ("
855 + values[0] + ", " + values[1] + ", " + values[2] + ")"
856 + " diff(" + diff[0] + " " + diff[1] + " " + diff[2]
857 + ")");
858 Log.d("BrowserSensorHack", " mDiff(" + mDiff[0] + " "
859 + mDiff[1] + " " + mDiff[2] + ")" + " mRevertDiff("
860 + mRevertDiff[0] + " " + mRevertDiff[1] + " "
861 + mRevertDiff[2] + ")");
862 }
863
864 long now = android.os.SystemClock.uptimeMillis();
865 if (now - mLastGestureTime > 1000) {
866 mLastGestureTime = 0;
867
868 float y = mDiff[1];
869 float z = mDiff[2];
870 float ay = Math.abs(y);
871 float az = Math.abs(z);
872 float ry = mRevertDiff[1];
873 float rz = mRevertDiff[2];
874 float ary = Math.abs(ry);
875 float arz = Math.abs(rz);
876 boolean gestY = ay > 2.5f && ary > 1.0f && ay > ary;
877 boolean gestZ = az > 3.5f && arz > 1.0f && az > arz;
878
879 if ((gestY || gestZ) && !(gestY && gestZ)) {
880 WebView view = mWebView;
881
882 if (gestZ) {
883 if (z < 0) {
884 view.zoomOut();
885 } else {
886 view.zoomIn();
887 }
888 } else {
889 view.flingScroll(0, Math.round(y * 100));
890 }
891 mLastGestureTime = now;
892 }
893 }
894 }
895
896 public void onAccuracyChanged(int sensor, int accuracy) {
897 // TODO Auto-generated method stub
898
899 }
900 };
901
902 @Override protected void onResume() {
903 super.onResume();
904 if (Config.LOGV) {
905 Log.v(LOGTAG, "BrowserActivity.onResume: this=" + this);
906 }
907
908 if (!mActivityInPause) {
909 Log.e(LOGTAG, "BrowserActivity is already resumed.");
910 return;
911 }
912
913 mActivityInPause = false;
914 resumeWebView();
915
916 if (mWakeLock.isHeld()) {
917 mHandler.removeMessages(RELEASE_WAKELOCK);
918 mWakeLock.release();
919 }
920
921 if (mCredsDlg != null) {
922 if (!mHandler.hasMessages(CANCEL_CREDS_REQUEST)) {
923 // In case credential request never comes back
924 mHandler.sendEmptyMessageDelayed(CANCEL_CREDS_REQUEST, 6000);
925 }
926 }
927
928 registerReceiver(mNetworkStateIntentReceiver,
929 mNetworkStateChangedFilter);
930 WebView.enablePlatformNotifications();
931
932 if (mSettings.doFlick()) {
933 if (mSensorManager == null) {
934 mSensorManager = (SensorManager) getSystemService(
935 Context.SENSOR_SERVICE);
936 }
937 mSensorManager.registerListener(mSensorListener,
938 SensorManager.SENSOR_ACCELEROMETER,
939 SensorManager.SENSOR_DELAY_FASTEST);
940 } else {
941 mSensorManager = null;
942 }
943 }
944
945 /**
946 * onSaveInstanceState(Bundle map)
947 * onSaveInstanceState is called right before onStop(). The map contains
948 * the saved state.
949 */
950 @Override protected void onSaveInstanceState(Bundle outState) {
951 if (Config.LOGV) {
952 Log.v(LOGTAG, "BrowserActivity.onSaveInstanceState: this=" + this);
953 }
954 // the default implementation requires each view to have an id. As the
955 // browser handles the state itself and it doesn't use id for the views,
956 // don't call the default implementation. Otherwise it will trigger the
957 // warning like this, "couldn't save which view has focus because the
958 // focused view XXX has no id".
959
960 // Save all the tabs
961 mTabControl.saveState(outState);
962 }
963
964 @Override protected void onPause() {
965 super.onPause();
966
967 if (mActivityInPause) {
968 Log.e(LOGTAG, "BrowserActivity is already paused.");
969 return;
970 }
971
972 mActivityInPause = true;
973 if (!pauseWebView()) {
974 mWakeLock.acquire();
975 mHandler.sendMessageDelayed(mHandler
976 .obtainMessage(RELEASE_WAKELOCK), WAKELOCK_TIMEOUT);
977 }
978
979 // Clear the credentials toast if it is up
980 if (mCredsDlg != null && mCredsDlg.isShowing()) {
981 mCredsDlg.dismiss();
982 }
983 mCredsDlg = null;
984
985 cancelStopToast();
986
987 // unregister network state listener
988 unregisterReceiver(mNetworkStateIntentReceiver);
989 WebView.disablePlatformNotifications();
990
991 if (mSensorManager != null) {
992 mSensorManager.unregisterListener(mSensorListener);
993 }
994 }
995
996 @Override protected void onDestroy() {
997 if (Config.LOGV) {
998 Log.v(LOGTAG, "BrowserActivity.onDestroy: this=" + this);
999 }
1000 super.onDestroy();
1001 // Remove the current tab and sub window
1002 TabControl.Tab t = mTabControl.getCurrentTab();
1003 dismissSubWindow(t);
1004 removeTabFromContentView(t);
1005 // Destroy all the tabs
1006 mTabControl.destroy();
1007 WebIconDatabase.getInstance().close();
1008 if (mGlsConnection != null) {
1009 unbindService(mGlsConnection);
1010 mGlsConnection = null;
1011 }
1012
1013 //
1014 // stop MASF proxy service
1015 //
1016 //Intent proxyServiceIntent = new Intent();
1017 //proxyServiceIntent.setComponent
1018 // (new ComponentName(
1019 // "com.android.masfproxyservice",
1020 // "com.android.masfproxyservice.MasfProxyService"));
1021 //stopService(proxyServiceIntent);
1022 }
1023
1024 @Override
1025 public void onConfigurationChanged(Configuration newConfig) {
1026 super.onConfigurationChanged(newConfig);
1027
1028 if (mPageInfoDialog != null) {
1029 mPageInfoDialog.dismiss();
1030 showPageInfo(
1031 mPageInfoView,
1032 mPageInfoFromShowSSLCertificateOnError.booleanValue());
1033 }
1034 if (mSSLCertificateDialog != null) {
1035 mSSLCertificateDialog.dismiss();
1036 showSSLCertificate(
1037 mSSLCertificateView);
1038 }
1039 if (mSSLCertificateOnErrorDialog != null) {
1040 mSSLCertificateOnErrorDialog.dismiss();
1041 showSSLCertificateOnError(
1042 mSSLCertificateOnErrorView,
1043 mSSLCertificateOnErrorHandler,
1044 mSSLCertificateOnErrorError);
1045 }
1046 if (mHttpAuthenticationDialog != null) {
1047 String title = ((TextView) mHttpAuthenticationDialog
1048 .findViewById(com.android.internal.R.id.alertTitle)).getText()
1049 .toString();
1050 String name = ((TextView) mHttpAuthenticationDialog
1051 .findViewById(R.id.username_edit)).getText().toString();
1052 String password = ((TextView) mHttpAuthenticationDialog
1053 .findViewById(R.id.password_edit)).getText().toString();
1054 int focusId = mHttpAuthenticationDialog.getCurrentFocus()
1055 .getId();
1056 mHttpAuthenticationDialog.dismiss();
1057 showHttpAuthentication(mHttpAuthHandler, null, null, title,
1058 name, password, focusId);
1059 }
1060 }
1061
1062 @Override public void onLowMemory() {
1063 super.onLowMemory();
1064 mTabControl.freeMemory();
1065 }
1066
1067 private boolean resumeWebView() {
1068 if ((!mActivityInPause && !mPageStarted) ||
1069 (mActivityInPause && mPageStarted)) {
1070 CookieSyncManager.getInstance().startSync();
1071 mWebView.resumeTimers();
1072 return true;
1073 } else {
1074 return false;
1075 }
1076 }
1077
1078 private boolean pauseWebView() {
1079 if (mActivityInPause && !mPageStarted) {
1080 CookieSyncManager.getInstance().stopSync();
1081 mWebView.pauseTimers();
1082 return true;
1083 } else {
1084 return false;
1085 }
1086 }
1087
1088 /*
1089 * This function is called when we are launching for the first time. We
1090 * are waiting for the login credentials before loading Google home
1091 * pages. This way the user will be logged in straight away.
1092 */
1093 private void waitForCredentials() {
1094 // Show a toast
1095 mCredsDlg = new ProgressDialog(this);
1096 mCredsDlg.setIndeterminate(true);
1097 mCredsDlg.setMessage(getText(R.string.retrieving_creds_dlg_msg));
1098 // If the user cancels the operation, then cancel the Google
1099 // Credentials request.
1100 mCredsDlg.setCancelMessage(mHandler.obtainMessage(CANCEL_CREDS_REQUEST));
1101 mCredsDlg.show();
1102
1103 // We set a timeout for the retrieval of credentials in onResume()
1104 // as that is when we have freed up some CPU time to get
1105 // the login credentials.
1106 }
1107
1108 /*
1109 * If we have received the credentials or we have timed out and we are
1110 * showing the credentials dialog, then it is time to move on.
1111 */
1112 private void resumeAfterCredentials() {
1113 if (mCredsDlg == null) {
1114 return;
1115 }
1116
1117 // Clear the toast
1118 if (mCredsDlg.isShowing()) {
1119 mCredsDlg.dismiss();
1120 }
1121 mCredsDlg = null;
1122
1123 // Clear any pending timeout
1124 mHandler.removeMessages(CANCEL_CREDS_REQUEST);
1125
1126 // Load the page
1127 mWebView.loadUrl(mSettings.getHomePage());
1128
1129 // Update the settings, need to do this last as it can take a moment
1130 // to persist the settings. In the mean time we could be loading
1131 // content.
1132 mSettings.setLoginInitialized(this);
1133 }
1134
1135 // Open the icon database and retain all the icons for visited sites.
1136 private void retainIconsOnStartup() {
1137 final WebIconDatabase db = WebIconDatabase.getInstance();
1138 db.open(getDir("icons", 0).getPath());
1139 try {
1140 Cursor c = Browser.getAllBookmarks(mResolver);
1141 if (!c.moveToFirst()) {
1142 c.deactivate();
1143 return;
1144 }
1145 int urlIndex = c.getColumnIndex(Browser.BookmarkColumns.URL);
1146 do {
1147 String url = c.getString(urlIndex);
1148 db.retainIconForPageUrl(url);
1149 } while (c.moveToNext());
1150 c.deactivate();
1151 } catch (IllegalStateException e) {
1152 Log.e(LOGTAG, "retainIconsOnStartup", e);
1153 }
1154 }
1155
1156 // Helper method for getting the top window.
1157 WebView getTopWindow() {
1158 return mTabControl.getCurrentTopWebView();
1159 }
1160
1161 @Override
1162 public boolean onCreateOptionsMenu(Menu menu) {
1163 super.onCreateOptionsMenu(menu);
1164
1165 MenuInflater inflater = getMenuInflater();
1166 inflater.inflate(R.menu.browser, menu);
1167 mMenu = menu;
1168 updateInLoadMenuItems();
1169 return true;
1170 }
1171
1172 /**
1173 * As the menu can be open when loading state changes
1174 * we must manually update the state of the stop/reload menu
1175 * item
1176 */
1177 private void updateInLoadMenuItems() {
1178 if (mMenu == null) {
1179 return;
1180 }
1181 MenuItem src = mInLoad ?
1182 mMenu.findItem(R.id.stop_menu_id):
1183 mMenu.findItem(R.id.reload_menu_id);
1184 MenuItem dest = mMenu.findItem(R.id.stop_reload_menu_id);
1185 dest.setIcon(src.getIcon());
1186 dest.setTitle(src.getTitle());
1187 }
1188
1189 @Override
1190 public boolean onContextItemSelected(MenuItem item) {
1191 // chording is not an issue with context menus, but we use the same
1192 // options selector, so set mCanChord to true so we can access them.
1193 mCanChord = true;
1194 int id = item.getItemId();
1195 switch (id) {
1196 // -- Browser context menu
1197 case R.id.open_context_menu_id:
1198 case R.id.open_newtab_context_menu_id:
1199 case R.id.bookmark_context_menu_id:
1200 case R.id.save_link_context_menu_id:
1201 case R.id.share_link_context_menu_id:
1202 case R.id.copy_link_context_menu_id:
1203 Message msg = mHandler.obtainMessage(
1204 FOCUS_NODE_HREF, id, 0);
1205 WebView webview = getTopWindow();
1206 msg.obj = webview;
1207 webview.requestFocusNodeHref(msg);
1208 break;
1209
1210 case R.id.download_context_menu_id:
1211 case R.id.view_image_context_menu_id:
1212 Message m = mHandler.obtainMessage(
1213 FOCUS_NODE_HREF, id, 0);
1214 WebView w = getTopWindow();
1215 m.obj = w;
1216 w.requestImageRef(m);
1217 break;
1218 default:
1219 // For other context menus
1220 return onOptionsItemSelected(item);
1221 }
1222 mCanChord = false;
1223 return true;
1224 }
1225
1226 /**
1227 * Overriding this forces the search key to launch global search. The difference
1228 * is the final "true" which requests global search.
1229 */
1230 @Override
1231 public boolean onSearchRequested() {
1232 startSearch(null, false, null, true);
1233 return true;
1234 }
1235
1236 @Override
1237 public boolean onOptionsItemSelected(MenuItem item) {
1238 if (!mCanChord) {
1239 // The user has already fired a shortcut with this hold down of the
1240 // menu key.
1241 return false;
1242 }
1243 switch (item.getItemId()) {
1244 // -- Main menu
1245 case R.id.goto_menu_id: {
1246 String url = getTopWindow().getUrl();
1247 // TODO: Activities are requested to call onSearchRequested, and to override
1248 // that function in order to insert custom fields (e.g. the search query).
1249 startSearch(mSettings.getHomePage().equals(url) ? null : url, true, null, false);
1250 }
1251 break;
1252
1253 case R.id.search_menu_id:
1254 // launch using "global" search, which will bring up the Google search box
1255 onSearchRequested();
1256 break;
1257
1258 case R.id.bookmarks_menu_id:
1259 bookmarksPicker();
1260 break;
1261
1262 case R.id.windows_menu_id:
1263 tabPicker(true, mTabControl.getCurrentIndex(), false);
1264 break;
1265
1266 case R.id.stop_reload_menu_id:
1267 if (mInLoad) {
1268 stopLoading();
1269 } else {
1270 getTopWindow().reload();
1271 }
1272 break;
1273
1274 case R.id.back_menu_id:
1275 getTopWindow().goBack();
1276 break;
1277
1278 case R.id.forward_menu_id:
1279 getTopWindow().goForward();
1280 break;
1281
1282 case R.id.close_menu_id:
1283 // Close the subwindow if it exists.
1284 if (mTabControl.getCurrentSubWindow() != null) {
1285 dismissSubWindow(mTabControl.getCurrentTab());
1286 break;
1287 }
1288 final int currentIndex = mTabControl.getCurrentIndex();
1289 final TabControl.Tab parent =
1290 mTabControl.getCurrentTab().getParentTab();
1291 int indexToShow = -1;
1292 if (parent != null) {
1293 indexToShow = mTabControl.getTabIndex(parent);
1294 } else {
1295 // Get the last tab in the list. If it is the current tab,
1296 // subtract 1 more.
1297 indexToShow = mTabControl.getTabCount() - 1;
1298 if (currentIndex == indexToShow) {
1299 indexToShow--;
1300 }
1301 }
1302 removeTabAndShow(currentIndex, indexToShow);
1303 break;
1304
1305 case R.id.homepage_menu_id:
1306 dismissSubWindow(mTabControl.getCurrentTab());
1307 mWebView.loadUrl(mSettings.getHomePage());
1308 break;
1309
1310 case R.id.preferences_menu_id:
1311 Intent intent = new Intent(this,
1312 BrowserPreferencesPage.class);
1313 startActivityForResult(intent, PREFERENCES_PAGE);
1314 break;
1315
1316/*
1317 Disable Find for version 1.0
1318 case R.id.find_menu_id:
1319 if (null == mFindDialog) {
1320 mFindDialog = new FindDialog(this);
1321 FrameLayout.LayoutParams lp =
1322 new FrameLayout.LayoutParams(
1323 ViewGroup.LayoutParams.FILL_PARENT,
1324 ViewGroup.LayoutParams.WRAP_CONTENT,
1325 Gravity.BOTTOM);
1326 mFindDialog.setLayoutParams(lp);
1327 }
1328 mFindDialog.setWebView(getTopWindow());
1329 mContentView.addView(mFindDialog);
1330 mFindDialog.show();
1331 Animation anim =AnimationUtils.loadAnimation(this,
1332 R.anim.find_dialog_enter);
1333 mFindDialog.startAnimation(anim);
1334 mMenuState = EMPTY_MENU;
1335 break;
1336*/
1337
1338 case R.id.page_info_menu_id:
1339 showPageInfo(mWebView, false);
1340 break;
1341
1342 case R.id.classic_history_menu_id: {
1343 Intent i = new Intent(this, BrowserHistoryPage.class);
1344 i.putExtra("maxTabsOpen",
1345 mTabControl.getTabCount() >=
1346 TabControl.MAX_TABS);
1347 startActivityForResult(i, CLASSIC_HISTORY_PAGE);
1348 }
1349 break;
1350
1351 case R.id.bookmark_page_menu_id:
1352 Browser.saveBookmark(this, getTopWindow().getTitle(),
1353 getTopWindow().getUrl());
1354 break;
1355
1356 case R.id.share_page_menu_id:
1357 Browser.sendString(this, getTopWindow().getUrl());
1358 break;
1359
1360 case R.id.dump_nav_menu_id:
1361 getTopWindow().debugDump();
1362 break;
1363
1364 case R.id.zoom_menu_id:
1365 // FIXME: Can we move this out of WebView? How does this work
1366 // for a subwindow?
1367 getTopWindow().invokeZoomPicker();
1368 break;
1369
1370 case R.id.zoom_in_menu_id:
1371 getTopWindow().zoomIn();
1372 break;
1373
1374 case R.id.zoom_out_menu_id:
1375 getTopWindow().zoomOut();
1376 break;
1377
1378 case R.id.view_downloads_menu_id:
1379 viewDownloads(null);
1380 break;
1381
1382 case R.id.flip_orientation_menu_id:
1383 if (mSettings.getOrientation() !=
1384 ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) {
1385 mSettings.setOrientation(this,
1386 ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
1387 } else {
1388 mSettings.setOrientation(this,
1389 ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
1390 }
1391 setRequestedOrientation(mSettings.getOrientation());
1392 break;
1393
1394 // -- Tab menu
1395 case R.id.view_tab_menu_id:
1396 if (mTabListener != null && mTabOverview != null) {
1397 int pos = mTabOverview.getContextMenuPosition(item);
1398 mTabOverview.setCurrentIndex(pos);
1399 mTabListener.onClick(pos);
1400 }
1401 break;
1402
1403 case R.id.remove_tab_menu_id:
1404 if (mTabListener != null && mTabOverview != null) {
1405 int pos = mTabOverview.getContextMenuPosition(item);
1406 mTabListener.remove(pos);
1407 }
1408 break;
1409
1410 case R.id.new_tab_menu_id:
1411 // No need to check for mTabOverview here since we are not
1412 // dependent on it for a position.
1413 if (mTabListener != null) {
1414 // If the overview happens to be non-null, make the "New
1415 // Tab" cell visible.
1416 if (mTabOverview != null) {
1417 mTabOverview.setCurrentIndex(ImageGrid.NEW_TAB);
1418 }
1419 mTabListener.onClick(ImageGrid.NEW_TAB);
1420 }
1421 break;
1422
1423 case R.id.bookmark_tab_menu_id:
1424 if (mTabListener != null && mTabOverview != null) {
1425 int pos = mTabOverview.getContextMenuPosition(item);
1426 TabControl.Tab t = mTabControl.getTab(pos);
1427 // Since we called populatePickerData for all of the
1428 // tabs, getTitle and getUrl will return appropriate
1429 // values.
1430 Browser.saveBookmark(BrowserActivity.this, t.getTitle(),
1431 t.getUrl());
1432 }
1433 break;
1434
1435 case R.id.history_tab_menu_id: {
1436 Intent i = new Intent(this, BrowserHistoryPage.class);
1437 i.putExtra("maxTabsOpen",
1438 mTabControl.getTabCount() >=
1439 TabControl.MAX_TABS);
1440 startActivityForResult(i, CLASSIC_HISTORY_PAGE);
1441 }
1442 break;
1443
1444 case R.id.bookmarks_tab_menu_id:
1445 bookmarksPicker();
1446 break;
1447
1448 case R.id.properties_tab_menu_id:
1449 if (mTabListener != null && mTabOverview != null) {
1450 int pos = mTabOverview.getContextMenuPosition(item);
1451 TabControl.Tab t = mTabControl.getTab(pos);
1452 // Use the tab's data for the page info dialog.
1453 if (t.getWebView() != null) {
1454 showPageInfo(t.getWebView(), false);
1455 }
1456 // FIXME: what should we display if the WebView is null?
1457 }
1458 break;
1459
1460 default:
1461 if (!super.onOptionsItemSelected(item)) {
1462 return false;
1463 }
1464 // Otherwise fall through.
1465 }
1466 mCanChord = false;
1467 return true;
1468 }
1469
1470 public void closeFind() {
1471 Animation anim = AnimationUtils.loadAnimation(this,
1472 R.anim.find_dialog_exit);
1473 mFindDialog.startAnimation(anim);
1474 mContentView.removeView(mFindDialog);
1475 getTopWindow().requestFocus();
1476 mMenuState = R.id.MAIN_MENU;
1477 }
1478
1479 @Override
1480 public boolean dispatchTouchEvent(MotionEvent event) {
1481 if (super.dispatchTouchEvent(event)) {
1482 return true;
1483 } else {
1484 // We do not use the Dialog class because it places dialogs in the
1485 // middle of the screen. It would take care of dismissing find if
1486 // were using it, but we are doing it manually since we are not.
1487 if (mFindDialog != null && mFindDialog.hasFocus()) {
1488 mFindDialog.dismiss();
1489 }
1490 return false;
1491 }
1492 }
1493
1494 @Override public boolean onPrepareOptionsMenu(Menu menu)
1495 {
1496 // This happens when the user begins to hold down the menu key, so
1497 // allow them to chord to get a shortcut.
1498 mCanChord = true;
1499 // Note: setVisible will decide whether an item is visible; while
1500 // setEnabled() will decide whether an item is enabled, which also means
1501 // whether the matching shortcut key will function.
1502 super.onPrepareOptionsMenu(menu);
1503 switch (mMenuState) {
1504 case R.id.TAB_MENU:
1505 if (mCurrentMenuState != mMenuState) {
1506 menu.setGroupVisible(R.id.MAIN_MENU, false);
1507 menu.setGroupEnabled(R.id.MAIN_MENU, false);
1508 menu.setGroupVisible(R.id.TAB_MENU, true);
1509 menu.setGroupEnabled(R.id.TAB_MENU, true);
1510 }
1511 boolean newT = mTabControl.getTabCount() < TabControl.MAX_TABS;
1512 final MenuItem tab = menu.findItem(R.id.new_tab_menu_id);
1513 tab.setVisible(newT);
1514 tab.setEnabled(newT);
1515 break;
1516 case EMPTY_MENU:
1517 if (mCurrentMenuState != mMenuState) {
1518 menu.setGroupVisible(R.id.MAIN_MENU, false);
1519 menu.setGroupEnabled(R.id.MAIN_MENU, false);
1520 menu.setGroupVisible(R.id.TAB_MENU, false);
1521 menu.setGroupEnabled(R.id.TAB_MENU, false);
1522 }
1523 break;
1524 default:
1525 if (mCurrentMenuState != mMenuState) {
1526 menu.setGroupVisible(R.id.MAIN_MENU, true);
1527 menu.setGroupEnabled(R.id.MAIN_MENU, true);
1528 menu.setGroupVisible(R.id.TAB_MENU, false);
1529 menu.setGroupEnabled(R.id.TAB_MENU, false);
1530 }
1531 final WebView w = getTopWindow();
1532 boolean canGoBack = w.canGoBack();
1533 final MenuItem back = menu.findItem(R.id.back_menu_id);
1534 back.setVisible(canGoBack);
1535 back.setEnabled(canGoBack);
1536 final MenuItem close = menu.findItem(R.id.close_menu_id);
1537 close.setVisible(!canGoBack);
1538 close.setEnabled(!canGoBack);
1539 final MenuItem flip =
1540 menu.findItem(R.id.flip_orientation_menu_id);
1541 boolean keyboardClosed =
1542 getResources().getConfiguration().keyboardHidden ==
1543 Configuration.KEYBOARDHIDDEN_YES;
1544 flip.setEnabled(keyboardClosed);
1545
1546 boolean isHome = mSettings.getHomePage().equals(w.getUrl());
1547 final MenuItem home = menu.findItem(R.id.homepage_menu_id);
1548 home.setVisible(!isHome);
1549 home.setEnabled(!isHome);
1550
1551 menu.findItem(R.id.forward_menu_id)
1552 .setEnabled(w.canGoForward());
1553
1554 menu.findItem(R.id.zoom_in_menu_id).setVisible(false);
1555 menu.findItem(R.id.zoom_out_menu_id).setVisible(false);
1556
1557 // decide whether to show the share link option
1558 PackageManager pm = getPackageManager();
1559 Intent send = new Intent(Intent.ACTION_SEND);
1560 send.setType("text/plain");
1561 List<ResolveInfo> list = pm.queryIntentActivities(send,
1562 PackageManager.MATCH_DEFAULT_ONLY);
1563 menu.findItem(R.id.share_page_menu_id).setVisible(
1564 list.size() > 0);
1565
1566 boolean isNavDump = mSettings.isNavDump();
1567 final MenuItem nav = menu.findItem(R.id.dump_nav_menu_id);
1568 nav.setVisible(isNavDump);
1569 nav.setEnabled(isNavDump);
1570 break;
1571 }
1572 mCurrentMenuState = mMenuState;
1573 return true;
1574 }
1575
1576 @Override
1577 public void onCreateContextMenu(ContextMenu menu, View v,
1578 ContextMenuInfo menuInfo) {
1579 WebView webview = (WebView) v;
1580 WebView.HitTestResult result = webview.getHitTestResult();
1581 if (result == null) {
1582 return;
1583 }
1584
1585 int type = result.getType();
1586 if (type == WebView.HitTestResult.UNKNOWN_TYPE) {
1587 Log.w(LOGTAG,
1588 "We should not show context menu when nothing is touched");
1589 return;
1590 }
1591 if (type == WebView.HitTestResult.EDIT_TEXT_TYPE) {
1592 // let TextView handles context menu
1593 return;
1594 }
1595
1596 // Note, http://b/issue?id=1106666 is requesting that
1597 // an inflated menu can be used again. This is not available
1598 // yet, so inflate each time (yuk!)
1599 MenuInflater inflater = getMenuInflater();
1600 inflater.inflate(R.menu.browsercontext, menu);
1601
1602 // Show the correct menu group
1603 String extra = result.getExtra();
1604 menu.setGroupVisible(R.id.PHONE_MENU,
1605 type == WebView.HitTestResult.PHONE_TYPE);
1606 menu.setGroupVisible(R.id.EMAIL_MENU,
1607 type == WebView.HitTestResult.EMAIL_TYPE);
1608 menu.setGroupVisible(R.id.GEO_MENU,
1609 type == WebView.HitTestResult.GEO_TYPE);
1610 menu.setGroupVisible(R.id.IMAGE_MENU,
1611 type == WebView.HitTestResult.IMAGE_TYPE ||
1612 type == WebView.HitTestResult.IMAGE_ANCHOR_TYPE
1613 || type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE);
1614 menu.setGroupVisible(R.id.ANCHOR_MENU,
1615 type == WebView.HitTestResult.ANCHOR_TYPE ||
1616 type == WebView.HitTestResult.IMAGE_ANCHOR_TYPE
1617 || type == WebView.HitTestResult.SRC_ANCHOR_TYPE
1618 || type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE);
1619
1620 // Setup custom handling depending on the type
1621 switch (type) {
1622 case WebView.HitTestResult.PHONE_TYPE:
1623 menu.setHeaderTitle(extra);
1624 menu.findItem(R.id.dial_context_menu_id).setIntent(
1625 new Intent(Intent.ACTION_VIEW, Uri
1626 .parse(WebView.SCHEME_TEL + extra)));
1627 Intent addIntent = new Intent(Intent.ACTION_INSERT,
1628 Contacts.People.CONTENT_URI);
1629 addIntent.putExtra(Insert.FULL_MODE, true);
1630 addIntent.putExtra(Insert.PHONE, extra);
1631 menu.findItem(R.id.add_contact_context_menu_id).setIntent(
1632 addIntent);
1633 menu.findItem(R.id.copy_phone_context_menu_id).setOnMenuItemClickListener(
1634 new Copy(extra));
1635 break;
1636
1637 case WebView.HitTestResult.EMAIL_TYPE:
1638 menu.setHeaderTitle(extra);
1639 menu.findItem(R.id.email_context_menu_id).setIntent(
1640 new Intent(Intent.ACTION_VIEW, Uri
1641 .parse(WebView.SCHEME_MAILTO + extra)));
1642 menu.findItem(R.id.copy_mail_context_menu_id).setOnMenuItemClickListener(
1643 new Copy(extra));
1644 break;
1645
1646 case WebView.HitTestResult.GEO_TYPE:
1647 menu.setHeaderTitle(extra);
1648 menu.findItem(R.id.map_context_menu_id).setIntent(
1649 new Intent(Intent.ACTION_VIEW, Uri
1650 .parse(WebView.SCHEME_GEO
1651 + URLEncoder.encode(extra))));
1652 menu.findItem(R.id.copy_geo_context_menu_id).setOnMenuItemClickListener(
1653 new Copy(extra));
1654 break;
1655
1656 case WebView.HitTestResult.ANCHOR_TYPE:
1657 case WebView.HitTestResult.IMAGE_ANCHOR_TYPE:
1658 case WebView.HitTestResult.SRC_ANCHOR_TYPE:
1659 case WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE:
1660 mTitleView = (TextView) LayoutInflater.from(this)
1661 .inflate(android.R.layout.browser_link_context_header,
1662 null);
1663 menu.setHeaderView(mTitleView);
1664 // decide whether to show the open link in new tab option
1665 menu.findItem(R.id.open_newtab_context_menu_id).setVisible(
1666 mTabControl.getTabCount() < TabControl.MAX_TABS);
1667 if (type == WebView.HitTestResult.ANCHOR_TYPE
1668 || type == WebView.HitTestResult.IMAGE_ANCHOR_TYPE){
1669 menu.findItem(R.id.bookmark_context_menu_id).setVisible(
1670 false);
1671 menu.findItem(R.id.save_link_context_menu_id).setVisible(
1672 false);
1673 menu.findItem(R.id.copy_link_context_menu_id).setVisible(
1674 false);
1675 menu.findItem(R.id.share_link_context_menu_id).setVisible(
1676 false);
1677 mTitleView.setText(R.string.contextmenu_javascript);
1678 break;
1679 }
1680 Message headerMessage = mHandler.obtainMessage(FOCUS_NODE_HREF,
1681 HEADER_FLAG, 0);
1682 headerMessage.obj = webview;
1683 webview.requestFocusNodeHref(headerMessage);
1684 // decide whether to show the share link option
1685 PackageManager pm = getPackageManager();
1686 Intent send = new Intent(Intent.ACTION_SEND);
1687 send.setType("text/plain");
1688 List<ResolveInfo> list = pm.queryIntentActivities(send,
1689 PackageManager.MATCH_DEFAULT_ONLY);
1690 menu.findItem(R.id.share_link_context_menu_id).setVisible(
1691 list.size() > 0);
1692 if (type == WebView.HitTestResult.ANCHOR_TYPE) {
1693 break;
1694 }
1695 //fall through
1696
1697 case WebView.HitTestResult.IMAGE_TYPE:
1698 break;
1699
1700 default:
1701 Log.w(LOGTAG, "We should not get here.");
1702 break;
1703 }
1704 }
1705
1706 // Used by attachTabToContentView for the WebView's ZoomControl widget.
1707 private static final FrameLayout.LayoutParams ZOOM_PARAMS =
1708 new FrameLayout.LayoutParams(
1709 ViewGroup.LayoutParams.FILL_PARENT,
1710 ViewGroup.LayoutParams.WRAP_CONTENT,
1711 Gravity.BOTTOM);
1712
1713 // Attach the given tab to the content view.
1714 private void attachTabToContentView(TabControl.Tab t) {
1715 final WebView main = t.getWebView();
1716 // Attach the main WebView.
1717 mContentView.addView(main, COVER_SCREEN_PARAMS);
1718 // Attach the Zoom control widget and hide it.
1719 final View zoom = main.getZoomControls();
1720 mContentView.addView(zoom, ZOOM_PARAMS);
1721 zoom.setVisibility(View.GONE);
1722 // Attach the sub window if necessary
1723 attachSubWindow(t);
1724 // Request focus on the top window.
1725 t.getTopWindow().requestFocus();
1726 }
1727
1728 // Attach a sub window to the main WebView of the given tab.
1729 private void attachSubWindow(TabControl.Tab t) {
1730 // If a sub window exists, attach it to the content view.
1731 final WebView subView = t.getSubWebView();
1732 if (subView != null) {
1733 final View container = t.getSubWebViewContainer();
1734 mContentView.addView(container, COVER_SCREEN_PARAMS);
1735 subView.requestFocus();
1736 }
1737 }
1738
1739 // Remove the given tab from the content view.
1740 private void removeTabFromContentView(TabControl.Tab t) {
1741 // Remove the Zoom widget and the main WebView.
1742 mContentView.removeView(t.getWebView().getZoomControls());
1743 mContentView.removeView(t.getWebView());
1744 // Remove the sub window if it exists.
1745 if (t.getSubWebView() != null) {
1746 mContentView.removeView(t.getSubWebViewContainer());
1747 }
1748 }
1749
1750 // Remove the sub window if it exists. Also called by TabControl when the
1751 // user clicks the 'X' to dismiss a sub window.
1752 /* package */ void dismissSubWindow(TabControl.Tab t) {
1753 final WebView mainView = t.getWebView();
1754 if (t.getSubWebView() != null) {
1755 // Remove the container view and request focus on the main WebView.
1756 mContentView.removeView(t.getSubWebViewContainer());
1757 mainView.requestFocus();
1758 // Tell the TabControl to dismiss the subwindow. This will destroy
1759 // the WebView.
1760 mTabControl.dismissSubWindow(t);
1761 }
1762 }
1763
1764 // Send the ANIMTE_FROM_OVERVIEW message after changing the current tab.
1765 private void sendAnimateFromOverview(final TabControl.Tab tab,
1766 final boolean newTab, final String url, final int delay,
1767 final Message msg) {
1768 // Set the current tab.
1769 mTabControl.setCurrentTab(tab);
1770 // Attach the WebView so it will layout.
1771 attachTabToContentView(tab);
1772 // Reset the current WebView.
1773 mWebView = tab.getWebView();
1774 // Set the view to invisibile for now.
1775 mWebView.setVisibility(View.INVISIBLE);
1776 // If there is a sub window, make it invisible too.
1777 if (tab.getSubWebView() != null) {
1778 tab.getSubWebViewContainer().setVisibility(View.INVISIBLE);
1779 }
1780 // Create our fake animating view.
1781 final AnimatingView view = new AnimatingView(this, tab);
1782 // Attach it to the view system and make in invisible so it will
1783 // layout but not flash white on the screen.
1784 mContentView.addView(view, COVER_SCREEN_PARAMS);
1785 view.setVisibility(View.INVISIBLE);
1786 // Send the animate message.
1787 final HashMap map = new HashMap();
1788 map.put("view", view);
1789 map.put("url", url);
1790 map.put("msg", msg);
1791 mHandler.sendMessageDelayed(mHandler.obtainMessage(
1792 ANIMATE_FROM_OVERVIEW, newTab ? 1 : 0, 0, map), delay);
1793 // Increment the count to indicate that we are in an animation.
1794 mAnimationCount++;
1795 // Remove the listener so we don't get any more tab changes.
1796 if (mTabOverview != null) {
1797 mTabOverview.setListener(null);
1798 }
1799 mTabListener = null;
1800
1801 }
1802
1803 // 500ms animation with 800ms delay
1804 private static final int TAB_ANIMATION_DURATION = 500;
1805 private static final int TAB_OVERVIEW_DELAY = 800;
1806
1807 // Called by TabControl when a tab is requesting focus
1808 /* package */ void showTab(TabControl.Tab t) {
1809 // Disallow focus change during a tab animation.
1810 if (mAnimationCount > 0) {
1811 return;
1812 }
1813 int delay = 0;
1814 if (mTabOverview == null) {
1815 // Add a delay so the tab overview can be shown before the second
1816 // animation begins.
1817 delay = TAB_ANIMATION_DURATION + TAB_OVERVIEW_DELAY;
1818 tabPicker(false, mTabControl.getTabIndex(t), false);
1819 }
1820 sendAnimateFromOverview(t, false, null, delay, null);
1821 }
1822
1823 // This method does a ton of stuff. It will attempt to create a new tab
1824 // if we haven't reached MAX_TABS. Otherwise it uses the current tab. If
1825 // url isn't null, it will load the given url. If the tab overview is not
1826 // showing, it will animate to the tab overview, create a new tab and
1827 // animate away from it. After the animation completes, it will dispatch
1828 // the given Message. If the tab overview is already showing (i.e. this
1829 // method is called from TabListener.onClick(), the method will animate
1830 // away from the tab overview.
1831 private void openTabAndShow(String url, final Message msg) {
1832 final boolean newTab = mTabControl.getTabCount() != TabControl.MAX_TABS;
1833 final TabControl.Tab currentTab = mTabControl.getCurrentTab();
1834 if (newTab) {
1835 int delay = 0;
1836 // If the tab overview is up and there are animations, just load
1837 // the url.
1838 if (mTabOverview != null && mAnimationCount > 0) {
1839 if (url != null) {
1840 // We should not have a msg here since onCreateWindow
1841 // checks the animation count and every other caller passes
1842 // null.
1843 assert msg == null;
1844 // just dismiss the subwindow and load the given url.
1845 dismissSubWindow(currentTab);
1846 mWebView.loadUrl(url);
1847 }
1848 } else {
1849 // show mTabOverview if it is not there.
1850 if (mTabOverview == null) {
1851 // We have to delay the animation from the tab picker by the
1852 // length of the tab animation. Add a delay so the tab overview
1853 // can be shown before the second animation begins.
1854 delay = TAB_ANIMATION_DURATION + TAB_OVERVIEW_DELAY;
1855 tabPicker(false, ImageGrid.NEW_TAB, false);
1856 }
1857 // Animate from the Tab overview after any animations have
1858 // finished.
1859 sendAnimateFromOverview(mTabControl.createNewTab(),
1860 true, url, delay, msg);
1861 }
1862 } else if (url != null) {
1863 // We should not have a msg here.
1864 assert msg == null;
1865 if (mTabOverview != null && mAnimationCount == 0) {
1866 sendAnimateFromOverview(currentTab, false, url,
1867 TAB_OVERVIEW_DELAY, null);
1868 } else {
1869 // Get rid of the subwindow if it exists
1870 dismissSubWindow(currentTab);
1871 // Load the given url.
1872 mWebView.loadUrl(url);
1873 }
1874 }
1875 }
1876
1877 private Animation createTabAnimation(final AnimatingView view,
1878 final View cell, boolean scaleDown) {
1879 final AnimationSet set = new AnimationSet(true);
1880 final float scaleX = (float) cell.getWidth() / view.getWidth();
1881 final float scaleY = (float) cell.getHeight() / view.getHeight();
1882 if (scaleDown) {
1883 set.addAnimation(new ScaleAnimation(1.0f, scaleX, 1.0f, scaleY));
1884 set.addAnimation(new TranslateAnimation(0, cell.getLeft(), 0,
1885 cell.getTop()));
1886 } else {
1887 set.addAnimation(new ScaleAnimation(scaleX, 1.0f, scaleY, 1.0f));
1888 set.addAnimation(new TranslateAnimation(cell.getLeft(), 0,
1889 cell.getTop(), 0));
1890 }
1891 set.setDuration(TAB_ANIMATION_DURATION);
1892 set.setInterpolator(new DecelerateInterpolator());
1893 return set;
1894 }
1895
1896 // Animate to the tab overview. currentIndex tells us which position to
1897 // animate to and newIndex is the position that should be selected after
1898 // the animation completes.
1899 // If remove is true, after the animation stops, a confirmation dialog will
1900 // be displayed to the user.
1901 private void animateToTabOverview(final int newIndex, final boolean remove,
1902 final AnimatingView view) {
1903 if (mTabOverview == null) {
1904 return;
1905 }
1906
1907 // Find the view in the ImageGrid allowing for the "New Tab" cell.
1908 int position = mTabControl.getTabIndex(view.mTab);
1909 if (!((ImageAdapter) mTabOverview.getAdapter()).maxedOut()) {
1910 position++;
1911 }
1912
1913 // Offset the tab position with the first visible position to get a
1914 // number between 0 and 3.
1915 position -= mTabOverview.getFirstVisiblePosition();
1916
1917 // Grab the view that we are going to animate to.
1918 final View v = mTabOverview.getChildAt(position);
1919
1920 final Animation.AnimationListener l =
1921 new Animation.AnimationListener() {
1922 public void onAnimationStart(Animation a) {
1923 if (mTabOverview != null) {
1924 mTabOverview.requestFocus();
1925 // Clear the listener so we don't trigger a tab
1926 // selection.
1927 mTabOverview.setListener(null);
1928 }
1929 }
1930 public void onAnimationRepeat(Animation a) {}
1931 public void onAnimationEnd(Animation a) {
1932 // We are no longer animating so decrement the count.
1933 mAnimationCount--;
1934 // Make the view GONE so that it will not draw between
1935 // now and when the Runnable is handled.
1936 view.setVisibility(View.GONE);
1937 // Post a runnable since we can't modify the view
1938 // hierarchy during this callback.
1939 mHandler.post(new Runnable() {
1940 public void run() {
1941 // Remove the AnimatingView.
1942 mContentView.removeView(view);
1943 if (mTabOverview != null) {
1944 // Make newIndex visible.
1945 mTabOverview.setCurrentIndex(newIndex);
1946 // Restore the listener.
1947 mTabOverview.setListener(mTabListener);
1948 // Change the menu to TAB_MENU if the
1949 // ImageGrid is interactive.
1950 if (mTabOverview.isLive()) {
1951 mMenuState = R.id.TAB_MENU;
1952 mTabOverview.requestFocus();
1953 }
1954 }
1955 // If a remove was requested, remove the tab.
1956 if (remove) {
1957 // During a remove, the current tab has
1958 // already changed. Remember the current one
1959 // here.
1960 final TabControl.Tab currentTab =
1961 mTabControl.getCurrentTab();
1962 // Remove the tab at newIndex from
1963 // TabControl and the tab overview.
1964 final TabControl.Tab tab =
1965 mTabControl.getTab(newIndex);
1966 mTabControl.removeTab(tab);
1967 // Restore the current tab.
1968 if (currentTab != tab) {
1969 mTabControl.setCurrentTab(currentTab);
1970 }
1971 if (mTabOverview != null) {
1972 mTabOverview.remove(newIndex);
1973 // Make the current tab visible.
1974 mTabOverview.setCurrentIndex(
1975 mTabControl.getCurrentIndex());
1976 }
1977 }
1978 }
1979 });
1980 }
1981 };
1982
1983 // Do an animation if there is a view to animate to.
1984 if (v != null) {
1985 // Create our animation
1986 final Animation anim = createTabAnimation(view, v, true);
1987 anim.setAnimationListener(l);
1988 // Start animating
1989 view.startAnimation(anim);
1990 } else {
1991 // If something goes wrong and we didn't find a view to animate to,
1992 // just do everything here.
1993 l.onAnimationStart(null);
1994 l.onAnimationEnd(null);
1995 }
1996 }
1997
1998 // Animate from the tab picker. The index supplied is the index to animate
1999 // from.
2000 private void animateFromTabOverview(final AnimatingView view,
2001 final boolean newTab, final String url, final Message msg) {
2002 // mTabOverview may have been dismissed
2003 if (mTabOverview == null) {
2004 return;
2005 }
2006
2007 // firstVisible is the first visible tab on the screen. This helps
2008 // to know which corner of the screen the selected tab is.
2009 int firstVisible = mTabOverview.getFirstVisiblePosition();
2010 // tabPosition is the 0-based index of of the tab being opened
2011 int tabPosition = mTabControl.getTabIndex(view.mTab);
2012 if (!((ImageAdapter) mTabOverview.getAdapter()).maxedOut()) {
2013 // Add one to make room for the "New Tab" cell.
2014 tabPosition++;
2015 }
2016 // If this is a new tab, animate from the "New Tab" cell.
2017 if (newTab) {
2018 tabPosition = 0;
2019 }
2020 // Location corresponds to the four corners of the screen.
2021 // A new tab or 0 is upper left, 0 for an old tab is upper
2022 // right, 1 is lower left, and 2 is lower right
2023 int location = tabPosition - firstVisible;
2024
2025 // Find the view at this location.
2026 final View v = mTabOverview.getChildAt(location);
2027
2028 // Use a delay of 1 second in case we get a bad position
2029 long delay = 1000;
2030 boolean fade = false;
2031
2032 // Wait until the animation completes to load the url.
2033 final Animation.AnimationListener l =
2034 new Animation.AnimationListener() {
2035 public void onAnimationStart(Animation a) {}
2036 public void onAnimationRepeat(Animation a) {}
2037 public void onAnimationEnd(Animation a) {
2038 // The animation is done so allow key events and other
2039 // animations to begin.
2040 mAnimationCount--;
2041 mHandler.post(new Runnable() {
2042 public void run() {
2043 if (v != null) {
2044 mContentView.removeView(view);
2045 mWebView.setVisibility(View.VISIBLE);
2046 // Make the sub window container visible if
2047 // there is one.
2048 if (mTabControl.getCurrentSubWindow() != null) {
2049 mTabControl.getCurrentTab()
2050 .getSubWebViewContainer()
2051 .setVisibility(View.VISIBLE);
2052 }
2053 }
2054 if (url != null) {
2055 // Dismiss the subwindow if one exists.
2056 dismissSubWindow(
2057 mTabControl.getCurrentTab());
2058 mWebView.loadUrl(url);
2059 }
2060 mMenuState = R.id.MAIN_MENU;
2061 // Resume regular updates.
2062 mWebView.resumeTimers();
2063 // Dispatch the message after the animation
2064 // completes.
2065 if (msg != null) {
2066 msg.sendToTarget();
2067 }
2068 }
2069 });
2070 }
2071 };
2072
2073 if (v != null) {
2074 final Animation anim = createTabAnimation(view, v, false);
2075 // Set the listener and start animating
2076 anim.setAnimationListener(l);
2077 view.startAnimation(anim);
2078 // Make the view VISIBLE during the animation.
2079 view.setVisibility(View.VISIBLE);
2080 // Dismiss the tab overview after the animation completes.
2081 delay = anim.getDuration();
2082 } else {
2083 // dismiss mTabOverview and have it fade out just in case we get a
2084 // bad location.
2085 fade = true;
2086 // Go ahead and load the url.
2087 l.onAnimationEnd(null);
2088 }
2089 // Reset all the title bar info.
2090 resetTitle();
2091 // Dismiss the tab overview either after the animation or after a
2092 // second.
2093 mHandler.sendMessageDelayed(mHandler.obtainMessage(
2094 DISMISS_TAB_OVERVIEW, fade ? 1 : 0, 0), delay);
2095 }
2096
2097 private void openTab(String url) {
2098 if (mSettings.openInBackground()) {
2099 TabControl.Tab t = mTabControl.createNewTab();
2100 if (t != null) {
2101 WebView w = t.getWebView();
2102 w.loadUrl(url);
2103 }
2104 } else {
2105 openTabAndShow(url, null);
2106 }
2107 }
2108
2109 private class Copy implements OnMenuItemClickListener {
2110 private CharSequence mText;
2111
2112 public boolean onMenuItemClick(MenuItem item) {
2113 copy(mText);
2114 return true;
2115 }
2116
2117 public Copy(CharSequence toCopy) {
2118 mText = toCopy;
2119 }
2120 }
2121
2122 private void copy(CharSequence text) {
2123 try {
2124 IClipboard clip = IClipboard.Stub.asInterface(ServiceManager.getService("clipboard"));
2125 if (clip != null) {
2126 clip.setClipboardText(text);
2127 }
2128 } catch (android.os.RemoteException e) {
2129 Log.e(LOGTAG, "Copy failed", e);
2130 }
2131 }
2132
2133 /**
2134 * Resets the browser title-view to whatever it must be (for example, if we
2135 * load a page from history).
2136 */
2137 private void resetTitle() {
2138 resetLockIcon();
2139 resetTitleIconAndProgress();
2140 }
2141
2142 /**
2143 * Resets the browser title-view to whatever it must be
2144 * (for example, if we had a loading error)
2145 * When we have a new page, we call resetTitle, when we
2146 * have to reset the titlebar to whatever it used to be
2147 * (for example, if the user chose to stop loading), we
2148 * call resetTitleAndRevertLockIcon.
2149 */
2150 /* package */ void resetTitleAndRevertLockIcon() {
2151 revertLockIcon();
2152 resetTitleIconAndProgress();
2153 }
2154
2155 /**
2156 * Reset the title, favicon, and progress.
2157 */
2158 private void resetTitleIconAndProgress() {
2159 resetTitleAndIcon(mWebView);
2160 int progress = mWebView.getProgress();
2161 mInLoad = (progress != 100);
2162 updateInLoadMenuItems();
2163 mWebChromeClient.onProgressChanged(mWebView, progress);
2164 }
2165
2166 // Reset the title and the icon based on the given item.
2167 private void resetTitleAndIcon(WebView view) {
2168 WebHistoryItem item = view.copyBackForwardList().getCurrentItem();
2169 if (item != null) {
2170 setUrlTitle(item.getUrl(), item.getTitle());
2171 setFavicon(item.getFavicon());
2172 } else {
2173 setUrlTitle(null, null);
2174 setFavicon(null);
2175 }
2176 }
2177
2178 /**
2179 * Sets a title composed of the URL and the title string.
2180 * @param url The URL of the site being loaded.
2181 * @param title The title of the site being loaded.
2182 */
2183 private void setUrlTitle(String url, String title) {
2184 mUrl = url;
2185 mTitle = title;
2186
2187 setTitle(buildUrlTitle(url, title));
2188 }
2189
2190 /**
2191 * Builds and returns the page title, which is some
2192 * combination of the page URL and title.
2193 * @param url The URL of the site being loaded.
2194 * @param title The title of the site being loaded.
2195 * @return The page title.
2196 */
2197 private String buildUrlTitle(String url, String title) {
2198 String urlTitle = "";
2199
2200 if (url != null) {
2201 String titleUrl = buildTitleUrl(url);
2202
2203 if (title != null && 0 < title.length()) {
2204 if (titleUrl != null && 0 < titleUrl.length()) {
2205 urlTitle = titleUrl + ": " + title;
2206 } else {
2207 urlTitle = title;
2208 }
2209 } else {
2210 if (titleUrl != null) {
2211 urlTitle = titleUrl;
2212 }
2213 }
2214 }
2215
2216 return urlTitle;
2217 }
2218
2219 /**
2220 * @param url The URL to build a title version of the URL from.
2221 * @return The title version of the URL or null if fails.
2222 * The title version of the URL can be either the URL hostname,
2223 * or the hostname with an "https://" prefix (for secure URLs),
2224 * or an empty string if, for example, the URL in question is a
2225 * file:// URL with no hostname.
2226 */
2227 private static String buildTitleUrl(String url) {
2228 String titleUrl = null;
2229
2230 if (url != null) {
2231 try {
2232 // parse the url string
2233 URL urlObj = new URL(url);
2234 if (urlObj != null) {
2235 titleUrl = "";
2236
2237 String protocol = urlObj.getProtocol();
2238 String host = urlObj.getHost();
2239
2240 if (host != null && 0 < host.length()) {
2241 titleUrl = host;
2242 if (protocol != null) {
2243 // if a secure site, add an "https://" prefix!
2244 if (protocol.equalsIgnoreCase("https")) {
2245 titleUrl = protocol + "://" + host;
2246 }
2247 }
2248 }
2249 }
2250 } catch (MalformedURLException e) {}
2251 }
2252
2253 return titleUrl;
2254 }
2255
2256 // Set the favicon in the title bar.
2257 private void setFavicon(Bitmap icon) {
2258 Drawable[] array = new Drawable[2];
2259 PaintDrawable p = new PaintDrawable(Color.WHITE);
2260 p.setCornerRadius(3f);
2261 array[0] = p;
2262 if (icon == null) {
2263 array[1] = mGenericFavicon;
2264 } else {
2265 array[1] = new BitmapDrawable(icon);
2266 }
2267 LayerDrawable d = new LayerDrawable(array);
2268 d.setLayerInset(1, 2, 2, 2, 2);
2269 getWindow().setFeatureDrawable(Window.FEATURE_LEFT_ICON, d);
2270 }
2271
2272 /**
2273 * Saves the current lock-icon state before resetting
2274 * the lock icon. If we have an error, we may need to
2275 * roll back to the previous state.
2276 */
2277 private void saveLockIcon() {
2278 mPrevLockType = mLockIconType;
2279 }
2280
2281 /**
2282 * Reverts the lock-icon state to the last saved state,
2283 * for example, if we had an error, and need to cancel
2284 * the load.
2285 */
2286 private void revertLockIcon() {
2287 mLockIconType = mPrevLockType;
2288
2289 if (Config.LOGV) {
2290 Log.v(LOGTAG, "BrowserActivity.revertLockIcon:" +
2291 " revert lock icon to " + mLockIconType);
2292 }
2293
2294 updateLockIconImage(mLockIconType);
2295 }
2296
2297 private void removeTabAndShow(int indexToRemove, int indexToShow) {
2298 int delay = TAB_ANIMATION_DURATION + TAB_OVERVIEW_DELAY;
2299 // Animate to the tab picker, remove the current tab, then
2300 // animate away from the tab picker to the parent WebView.
2301 tabPicker(false, indexToRemove, true);
2302 // Change to the parent tab
2303 final TabControl.Tab tab = mTabControl.getTab(indexToShow);
2304 if (tab != null) {
2305 sendAnimateFromOverview(tab, false, null, delay, null);
2306 } else {
2307 // Send a message to open a new tab.
2308 mHandler.sendMessageDelayed(
2309 mHandler.obtainMessage(OPEN_TAB_AND_SHOW,
2310 mSettings.getHomePage()), delay);
2311 }
2312 }
2313
2314 private void goBackOnePageOrQuit() {
2315 if (mWebView.canGoBack()) {
2316 mWebView.goBack();
2317 } else {
2318 // Check to see if we are closing a window that was created by
2319 // another window. If so, we switch back to that window.
2320 TabControl.Tab parent = mTabControl.getCurrentTab().getParentTab();
2321 if (parent != null) {
2322 removeTabAndShow(mTabControl.getCurrentIndex(),
2323 mTabControl.getTabIndex(parent));
2324 } else {
2325 /*
2326 * Instead of finishing the activity, simply push this to the back
2327 * of the stack and let ActivityManager to choose the foreground
2328 * activity. As BrowserActivity is singleTask, it will be always the
2329 * root of the task. So we can use either true or false for
2330 * moveTaskToBack().
2331 */
2332 moveTaskToBack(true);
2333 }
2334 }
2335 }
2336
2337 public KeyTracker.State onKeyTracker(int keyCode,
2338 KeyEvent event,
2339 KeyTracker.Stage stage,
2340 int duration) {
2341 // if onKeyTracker() is called after activity onStop()
2342 // because of accumulated key events,
2343 // we should ignore it as browser is not active any more.
2344 WebView topWindow = getTopWindow();
2345 if (topWindow == null)
2346 return KeyTracker.State.NOT_TRACKING;
2347
2348 if (keyCode == KeyEvent.KEYCODE_BACK) {
2349 // During animations, block the back key so that other animations
2350 // are not triggered and so that we don't end up destroying all the
2351 // WebViews before finishing the animation.
2352 if (mAnimationCount > 0) {
2353 return KeyTracker.State.DONE_TRACKING;
2354 }
2355 if (stage == KeyTracker.Stage.UP) {
2356 // FIXME: Currently, we do not have a notion of the
2357 // history picker for the subwindow, but maybe we
2358 // should?
2359 WebView subwindow = mTabControl.getCurrentSubWindow();
2360 if (subwindow != null) {
2361 if (subwindow.canGoBack()) {
2362 subwindow.goBack();
2363 } else {
2364 dismissSubWindow(mTabControl.getCurrentTab());
2365 }
2366 } else {
2367 goBackOnePageOrQuit();
2368 }
2369 return KeyTracker.State.DONE_TRACKING;
2370 }
2371 return KeyTracker.State.KEEP_TRACKING;
2372 }
2373 return KeyTracker.State.NOT_TRACKING;
2374 }
2375
2376 @Override public boolean onKeyDown(int keyCode, KeyEvent event) {
2377 if (keyCode == KeyEvent.KEYCODE_MENU) {
2378 mMenuIsDown = true;
2379 }
2380 boolean handled = mKeyTracker.doKeyDown(keyCode, event);
2381 if (!handled) {
2382 switch (keyCode) {
2383 case KeyEvent.KEYCODE_SPACE:
2384 if (mMenuState == R.id.MAIN_MENU){
2385 if (event.isShiftPressed()) {
2386 getTopWindow().pageUp(false);
2387 } else {
2388 getTopWindow().pageDown(false);
2389 }
2390 handled = true;
2391 }
2392 break;
2393
2394 default:
2395 break;
2396 }
2397 }
2398 return handled || super.onKeyDown(keyCode, event);
2399 }
2400
2401 @Override public boolean onKeyUp(int keyCode, KeyEvent event) {
2402 if (keyCode == KeyEvent.KEYCODE_MENU) {
2403 mMenuIsDown = false;
2404 }
2405 return mKeyTracker.doKeyUp(keyCode, event) || super.onKeyUp(keyCode, event);
2406 }
2407
2408 private void stopLoading() {
2409 resetTitleAndRevertLockIcon();
2410 WebView w = getTopWindow();
2411 w.stopLoading();
2412 mWebViewClient.onPageFinished(w, w.getUrl());
2413
2414 cancelStopToast();
2415 mStopToast = Toast
2416 .makeText(this, R.string.stopping, Toast.LENGTH_SHORT);
2417 mStopToast.show();
2418 }
2419
2420 private void cancelStopToast() {
2421 if (mStopToast != null) {
2422 mStopToast.cancel();
2423 mStopToast = null;
2424 }
2425 }
2426
2427 // called by a non-UI thread to post the message
2428 public void postMessage(int what, int arg1, int arg2, Object obj) {
2429 mHandler.sendMessage(mHandler.obtainMessage(what, arg1, arg2, obj));
2430 }
2431
2432 // public message ids
2433 public final static int LOAD_URL = 1001;
2434 public final static int STOP_LOAD = 1002;
2435
2436 // Message Ids
2437 private static final int JS_CONFIRM = 101;
2438 private static final int FOCUS_NODE_HREF = 102;
2439 private static final int DISMISS_TAB_OVERVIEW = 103;
2440 private static final int CANCEL_CREDS_REQUEST = 104;
2441 private static final int ANIMATE_FROM_OVERVIEW = 105;
2442 private static final int ANIMATE_TO_OVERVIEW = 106;
2443 private static final int OPEN_TAB_AND_SHOW = 107;
2444 private static final int CHECK_MEMORY = 108;
2445 private static final int RELEASE_WAKELOCK = 109;
2446
2447 // Private handler for handling javascript and saving passwords
2448 private Handler mHandler = new Handler() {
2449
2450 public void handleMessage(Message msg) {
2451 switch (msg.what) {
2452 case JS_CONFIRM:
2453 JsResult res = (JsResult) msg.obj;
2454 if (msg.arg1 == 0) {
2455 res.cancel();
2456 } else {
2457 res.confirm();
2458 }
2459 break;
2460
2461 case DISMISS_TAB_OVERVIEW:
2462 if (mTabOverview != null) {
2463 if (msg.arg1 == 1) {
2464 AlphaAnimation anim =
2465 new AlphaAnimation(1.0f, 0.0f);
2466 anim.setDuration(500);
2467 anim.startNow();
2468 mTabOverview.startAnimation(anim);
2469 }
2470 // Just in case there was a problem with animating away
2471 // from the tab overview
2472 mWebView.setVisibility(View.VISIBLE);
2473 // Make the sub window container visible.
2474 if (mTabControl.getCurrentSubWindow() != null) {
2475 mTabControl.getCurrentTab().getSubWebViewContainer()
2476 .setVisibility(View.VISIBLE);
2477 }
2478 mContentView.removeView(mTabOverview);
2479 mTabOverview.clear();
2480 // XXX: There are checks for mTabOverview throughout
2481 // this file because this message can be received
2482 // before it is expected. This is because we are not
2483 // enforcing the order of animations properly. In order
2484 // to get this right, we would need to rewrite a lot of
2485 // the code to dispatch this messages after all
2486 // animations have completed.
2487 mTabOverview = null;
2488 mTabListener = null;
2489 }
2490 break;
2491
2492 case ANIMATE_FROM_OVERVIEW:
2493 final HashMap map = (HashMap) msg.obj;
2494 animateFromTabOverview((AnimatingView) map.get("view"),
2495 msg.arg1 == 1, (String) map.get("url"),
2496 (Message) map.get("msg"));
2497 break;
2498
2499 case ANIMATE_TO_OVERVIEW:
2500 animateToTabOverview(msg.arg1, msg.arg2 == 1,
2501 (AnimatingView) msg.obj);
2502 break;
2503
2504 case OPEN_TAB_AND_SHOW:
2505 openTabAndShow((String) msg.obj, null);
2506 break;
2507
2508 case FOCUS_NODE_HREF:
2509 String url = (String) msg.getData().get("url");
2510 if (url == null || url.length() == 0) {
2511 break;
2512 }
2513 WebView view = (WebView) msg.obj;
2514 // Only apply the action if the top window did not change.
2515 if (getTopWindow() != view) {
2516 break;
2517 }
2518 switch (msg.arg1) {
2519 case HEADER_FLAG:
2520 mTitleView.setText(url);
2521 break;
2522 case R.id.open_context_menu_id:
2523 case R.id.view_image_context_menu_id:
2524 loadURL(url);
2525 break;
2526 case R.id.open_newtab_context_menu_id:
2527 openTab(url);
2528 break;
2529 case R.id.bookmark_context_menu_id:
2530 Intent intent = new Intent(BrowserActivity.this,
2531 AddBookmarkPage.class);
2532 intent.putExtra("url", url);
2533 startActivity(intent);
2534 break;
2535 case R.id.share_link_context_menu_id:
2536 Browser.sendString(BrowserActivity.this, url);
2537 break;
2538 case R.id.copy_link_context_menu_id:
2539 copy(url);
2540 break;
2541 case R.id.save_link_context_menu_id:
2542 case R.id.download_context_menu_id:
2543 onDownloadStartNoStream(url, null, null, null, -1);
2544 break;
2545 }
2546 break;
2547
2548 case LOAD_URL:
2549 loadURL((String) msg.obj);
2550 break;
2551
2552 case STOP_LOAD:
2553 stopLoading();
2554 break;
2555
2556 case CANCEL_CREDS_REQUEST:
2557 resumeAfterCredentials();
2558 break;
2559
2560 case CHECK_MEMORY:
2561 // reschedule to check memory condition
2562 mHandler.removeMessages(CHECK_MEMORY);
2563 mHandler.sendMessageDelayed(mHandler.obtainMessage
2564 (CHECK_MEMORY), CHECK_MEMORY_INTERVAL);
2565 checkMemory();
2566 break;
2567
2568 case RELEASE_WAKELOCK:
2569 if (mWakeLock.isHeld()) {
2570 mWakeLock.release();
2571 }
2572 break;
2573 }
2574 }
2575 };
2576
2577 private static final int HEADER_FLAG = Integer.MIN_VALUE;
2578 private TextView mTitleView = null;
2579
2580 // -------------------------------------------------------------------------
2581 // WebViewClient implementation.
2582 //-------------------------------------------------------------------------
2583
2584 // Use in overrideUrlLoading
2585 /* package */ final static String SCHEME_WTAI = "wtai://wp/";
2586 /* package */ final static String SCHEME_WTAI_MC = "wtai://wp/mc;";
2587 /* package */ final static String SCHEME_WTAI_SD = "wtai://wp/sd;";
2588 /* package */ final static String SCHEME_WTAI_AP = "wtai://wp/ap;";
2589
2590 /* package */ WebViewClient getWebViewClient() {
2591 return mWebViewClient;
2592 }
2593
2594 private final WebViewClient mWebViewClient = new WebViewClient() {
2595 @Override
2596 public void onPageStarted(WebView view, String url, Bitmap favicon) {
2597 resetLockIcon(url);
2598 setUrlTitle(url, null);
2599 // Call onReceivedIcon instead of setFavicon so the bookmark
2600 // database can be updated.
2601 mWebChromeClient.onReceivedIcon(view, favicon);
2602
2603 if (mSettings.isTracing() == true) {
2604 // FIXME: we should save the trace file somewhere other than data.
2605 // I can't use "/tmp" as it competes for system memory.
2606 File file = getDir("browserTrace", 0);
2607 String baseDir = file.getPath();
2608 if (!baseDir.endsWith(File.separator)) baseDir += File.separator;
2609 String host;
2610 try {
2611 WebAddress uri = new WebAddress(url);
2612 host = uri.mHost;
2613 } catch (android.net.ParseException ex) {
2614 host = "unknown_host";
2615 }
2616 host = host.replace('.', '_');
2617 baseDir = baseDir + host;
2618 file = new File(baseDir+".data");
2619 if (file.exists() == true) {
2620 file.delete();
2621 }
2622 file = new File(baseDir+".key");
2623 if (file.exists() == true) {
2624 file.delete();
2625 }
2626 mInTrace = true;
2627 Debug.startMethodTracing(baseDir, 8 * 1024 * 1024);
2628 }
2629
2630 // Performance probe
2631 if (false) {
2632 mStart = SystemClock.uptimeMillis();
2633 mProcessStart = Process.getElapsedCpuTime();
2634 long[] sysCpu = new long[7];
2635 if (Process.readProcFile("/proc/stat", SYSTEM_CPU_FORMAT, null,
2636 sysCpu, null)) {
2637 mUserStart = sysCpu[0] + sysCpu[1];
2638 mSystemStart = sysCpu[2];
2639 mIdleStart = sysCpu[3];
2640 mIrqStart = sysCpu[4] + sysCpu[5] + sysCpu[6];
2641 }
2642 mUiStart = SystemClock.currentThreadTimeMillis();
2643 }
2644
2645 if (!mPageStarted) {
2646 mPageStarted = true;
2647 // if onResume() has been called, resumeWebView() does nothing.
2648 resumeWebView();
2649 }
2650
2651 // reset sync timer to avoid sync starts during loading a page
2652 CookieSyncManager.getInstance().resetSync();
2653
2654 mInLoad = true;
2655 updateInLoadMenuItems();
2656
2657 // schedule to check memory condition
2658 mHandler.sendMessageDelayed(mHandler.obtainMessage(CHECK_MEMORY),
2659 CHECK_MEMORY_INTERVAL);
2660 }
2661
2662 @Override
2663 public void onPageFinished(WebView view, String url) {
2664 // Reset the title and icon in case we stopped a provisional
2665 // load.
2666 resetTitleAndIcon(view);
2667 // Make the progress full.
2668 getWindow().setFeatureInt(Window.FEATURE_PROGRESS, 10000);
2669
2670 // Update the lock icon image only once we are done loading
2671 updateLockIconImage(mLockIconType);
2672
2673 // Performance probe
2674 if (false) {
2675 long[] sysCpu = new long[7];
2676 if (Process.readProcFile("/proc/stat", SYSTEM_CPU_FORMAT, null,
2677 sysCpu, null)) {
2678 String uiInfo = "UI thread used "
2679 + (SystemClock.currentThreadTimeMillis() - mUiStart)
2680 + " ms";
2681 if (Config.LOGD) {
2682 Log.d(LOGTAG, uiInfo);
2683 }
2684 //The string that gets written to the log
2685 String performanceString = "It took total "
2686 + (SystemClock.uptimeMillis() - mStart)
2687 + " ms clock time to load the page."
2688 + "\nbrowser process used "
2689 + (Process.getElapsedCpuTime() - mProcessStart)
2690 + " ms, user processes used "
2691 + (sysCpu[0] + sysCpu[1] - mUserStart) * 10
2692 + " ms, kernel used "
2693 + (sysCpu[2] - mSystemStart) * 10
2694 + " ms, idle took " + (sysCpu[3] - mIdleStart) * 10
2695 + " ms and irq took "
2696 + (sysCpu[4] + sysCpu[5] + sysCpu[6] - mIrqStart)
2697 * 10 + " ms, " + uiInfo;
2698 if (Config.LOGD) {
2699 Log.d(LOGTAG, performanceString + "\nWebpage: " + url);
2700 }
2701 if (url != null) {
2702 // strip the url to maintain consistency
2703 String newUrl = new String(url);
2704 if (newUrl.startsWith("http://www.")) {
2705 newUrl = newUrl.substring(11);
2706 } else if (newUrl.startsWith("http://")) {
2707 newUrl = newUrl.substring(7);
2708 } else if (newUrl.startsWith("https://www.")) {
2709 newUrl = newUrl.substring(12);
2710 } else if (newUrl.startsWith("https://")) {
2711 newUrl = newUrl.substring(8);
2712 }
2713 if (Config.LOGD) {
2714 Log.d(LOGTAG, newUrl + " loaded");
2715 }
2716 /*
2717 if (sWhiteList.contains(newUrl)) {
2718 // The string that gets pushed to the statistcs
2719 // service
2720 performanceString = performanceString
2721 + "\nWebpage: "
2722 + newUrl
2723 + "\nCarrier: "
2724 + android.os.SystemProperties
2725 .get("gsm.sim.operator.alpha");
2726 if (mWebView != null
2727 && mWebView.getContext() != null
2728 && mWebView.getContext().getSystemService(
2729 Context.CONNECTIVITY_SERVICE) != null) {
2730 ConnectivityManager cManager =
2731 (ConnectivityManager) mWebView
2732 .getContext().getSystemService(
2733 Context.CONNECTIVITY_SERVICE);
2734 NetworkInfo nInfo = cManager
2735 .getActiveNetworkInfo();
2736 if (nInfo != null) {
2737 performanceString = performanceString
2738 + "\nNetwork Type: "
2739 + nInfo.getType().toString();
2740 }
2741 }
2742 Checkin.logEvent(mResolver,
2743 Checkin.Events.Tag.WEBPAGE_LOAD,
2744 performanceString);
2745 Log.w(LOGTAG, "pushed to the statistics service");
2746 }
2747 */
2748 }
2749 }
2750 }
2751
2752 if (mInTrace) {
2753 mInTrace = false;
2754 Debug.stopMethodTracing();
2755 }
2756
2757 if (mPageStarted) {
2758 mPageStarted = false;
2759 // pauseWebView() will do nothing and return false if onPause()
2760 // is not called yet.
2761 if (pauseWebView()) {
2762 if (mWakeLock.isHeld()) {
2763 mHandler.removeMessages(RELEASE_WAKELOCK);
2764 mWakeLock.release();
2765 }
2766 }
2767 }
2768
2769 if (mInLoad) {
2770 mInLoad = false;
2771 updateInLoadMenuItems();
2772 }
2773
2774 mHandler.removeMessages(CHECK_MEMORY);
2775 checkMemory();
2776 }
2777
2778 // return true if want to hijack the url to let another app to handle it
2779 @Override
2780 public boolean shouldOverrideUrlLoading(WebView view, String url) {
2781 if (url.startsWith(SCHEME_WTAI)) {
2782 // wtai://wp/mc;number
2783 // number=string(phone-number)
2784 if (url.startsWith(SCHEME_WTAI_MC)) {
2785 Intent intent = new Intent(Intent.ACTION_VIEW,
2786 Uri.parse(WebView.SCHEME_TEL +
2787 url.substring(SCHEME_WTAI_MC.length())));
2788 startActivity(intent);
2789 return true;
2790 }
2791 // wtai://wp/sd;dtmf
2792 // dtmf=string(dialstring)
2793 if (url.startsWith(SCHEME_WTAI_SD)) {
2794 // TODO
2795 // only send when there is active voice connection
2796 return false;
2797 }
2798 // wtai://wp/ap;number;name
2799 // number=string(phone-number)
2800 // name=string
2801 if (url.startsWith(SCHEME_WTAI_AP)) {
2802 // TODO
2803 return false;
2804 }
2805 }
2806
2807 Uri uri;
2808 try {
2809 uri = Uri.parse(url);
2810 } catch (IllegalArgumentException ex) {
2811 return false;
2812 }
2813
2814 // check whether other activities want to handle this url
2815 Intent intent = new Intent(Intent.ACTION_VIEW, uri);
2816 intent.addCategory(Intent.CATEGORY_BROWSABLE);
2817 try {
2818 if (startActivityIfNeeded(intent, -1)) {
2819 return true;
2820 }
2821 } catch (ActivityNotFoundException ex) {
2822 // ignore the error. If no application can handle the URL,
2823 // eg about:blank, assume the browser can handle it.
2824 }
2825
2826 if (mMenuIsDown) {
2827 openTab(url);
2828 closeOptionsMenu();
2829 return true;
2830 }
2831
2832 return false;
2833 }
2834
2835 /**
2836 * Updates the lock icon. This method is called when we discover another
2837 * resource to be loaded for this page (for example, javascript). While
2838 * we update the icon type, we do not update the lock icon itself until
2839 * we are done loading, it is slightly more secure this way.
2840 */
2841 @Override
2842 public void onLoadResource(WebView view, String url) {
2843 if (url != null && url.length() > 0) {
2844 // It is only if the page claims to be secure
2845 // that we may have to update the lock:
2846 if (mLockIconType == LOCK_ICON_SECURE) {
2847 // If NOT a 'safe' url, change the lock to mixed content!
2848 if (!(URLUtil.isHttpsUrl(url) || URLUtil.isDataUrl(url) || URLUtil.isAboutUrl(url))) {
2849 mLockIconType = LOCK_ICON_MIXED;
2850 if (Config.LOGV) {
2851 Log.v(LOGTAG, "BrowserActivity.updateLockIcon:" +
2852 " updated lock icon to " + mLockIconType + " due to " + url);
2853 }
2854 }
2855 }
2856 }
2857 }
2858
2859 /**
2860 * Show the dialog, asking the user if they would like to continue after
2861 * an excessive number of HTTP redirects.
2862 */
2863 @Override
2864 public void onTooManyRedirects(WebView view, final Message cancelMsg,
2865 final Message continueMsg) {
2866 new AlertDialog.Builder(BrowserActivity.this)
2867 .setTitle(R.string.browserFrameRedirect)
2868 .setMessage(R.string.browserFrame307Post)
2869 .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
2870 public void onClick(DialogInterface dialog, int which) {
2871 continueMsg.sendToTarget();
2872 }})
2873 .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
2874 public void onClick(DialogInterface dialog, int which) {
2875 cancelMsg.sendToTarget();
2876 }})
2877 .setOnCancelListener(new OnCancelListener() {
2878 public void onCancel(DialogInterface dialog) {
2879 cancelMsg.sendToTarget();
2880 }})
2881 .show();
2882 }
2883
2884 /**
2885 * Show a dialog informing the user of the network error reported by
2886 * WebCore.
2887 */
2888 @Override
2889 public void onReceivedError(WebView view, int errorCode,
2890 String description, String failingUrl) {
2891 if (errorCode != EventHandler.ERROR_LOOKUP &&
2892 errorCode != EventHandler.ERROR_CONNECT &&
2893 errorCode != EventHandler.ERROR_BAD_URL &&
2894 errorCode != EventHandler.FILE_ERROR) {
2895 new AlertDialog.Builder(BrowserActivity.this)
2896 .setTitle((errorCode == EventHandler.FILE_NOT_FOUND_ERROR) ?
2897 R.string.browserFrameFileErrorLabel :
2898 R.string.browserFrameNetworkErrorLabel)
2899 .setMessage(description)
2900 .setPositiveButton(R.string.ok, null)
2901 .show();
2902 }
2903 Log.e(LOGTAG, "onReceivedError code:"+errorCode+" "+description);
2904
2905 // We need to reset the title after an error.
2906 resetTitleAndRevertLockIcon();
2907 }
2908
2909 /**
2910 * Check with the user if it is ok to resend POST data as the page they
2911 * are trying to navigate to is the result of a POST.
2912 */
2913 @Override
2914 public void onFormResubmission(WebView view, final Message dontResend,
2915 final Message resend) {
2916 new AlertDialog.Builder(BrowserActivity.this)
2917 .setTitle(R.string.browserFrameFormResubmitLabel)
2918 .setMessage(R.string.browserFrameFormResubmitMessage)
2919 .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
2920 public void onClick(DialogInterface dialog, int which) {
2921 resend.sendToTarget();
2922 }})
2923 .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
2924 public void onClick(DialogInterface dialog, int which) {
2925 dontResend.sendToTarget();
2926 }})
2927 .setOnCancelListener(new OnCancelListener() {
2928 public void onCancel(DialogInterface dialog) {
2929 dontResend.sendToTarget();
2930 }})
2931 .show();
2932 }
2933
2934 /**
2935 * Insert the url into the visited history database.
2936 * @param url The url to be inserted.
2937 * @param isReload True if this url is being reloaded.
2938 * FIXME: Not sure what to do when reloading the page.
2939 */
2940 @Override
2941 public void doUpdateVisitedHistory(WebView view, String url,
2942 boolean isReload) {
2943 if (url.regionMatches(true, 0, "about:", 0, 6)) {
2944 return;
2945 }
2946 Browser.updateVisitedHistory(mResolver, url, true);
2947 WebIconDatabase.getInstance().retainIconForPageUrl(url);
2948 }
2949
2950 /**
2951 * Displays SSL error(s) dialog to the user.
2952 */
2953 @Override
2954 public void onReceivedSslError(
2955 final WebView view, final SslErrorHandler handler, final SslError error) {
2956
2957 if (mSettings.showSecurityWarnings()) {
2958 final LayoutInflater factory =
2959 LayoutInflater.from(BrowserActivity.this);
2960 final View warningsView =
2961 factory.inflate(R.layout.ssl_warnings, null);
2962 final LinearLayout placeholder =
2963 (LinearLayout)warningsView.findViewById(R.id.placeholder);
2964
2965 if (error.hasError(SslError.SSL_UNTRUSTED)) {
2966 LinearLayout ll = (LinearLayout)factory
2967 .inflate(R.layout.ssl_warning, null);
2968 ((TextView)ll.findViewById(R.id.warning))
2969 .setText(R.string.ssl_untrusted);
2970 placeholder.addView(ll);
2971 }
2972
2973 if (error.hasError(SslError.SSL_IDMISMATCH)) {
2974 LinearLayout ll = (LinearLayout)factory
2975 .inflate(R.layout.ssl_warning, null);
2976 ((TextView)ll.findViewById(R.id.warning))
2977 .setText(R.string.ssl_mismatch);
2978 placeholder.addView(ll);
2979 }
2980
2981 if (error.hasError(SslError.SSL_EXPIRED)) {
2982 LinearLayout ll = (LinearLayout)factory
2983 .inflate(R.layout.ssl_warning, null);
2984 ((TextView)ll.findViewById(R.id.warning))
2985 .setText(R.string.ssl_expired);
2986 placeholder.addView(ll);
2987 }
2988
2989 if (error.hasError(SslError.SSL_NOTYETVALID)) {
2990 LinearLayout ll = (LinearLayout)factory
2991 .inflate(R.layout.ssl_warning, null);
2992 ((TextView)ll.findViewById(R.id.warning))
2993 .setText(R.string.ssl_not_yet_valid);
2994 placeholder.addView(ll);
2995 }
2996
2997 new AlertDialog.Builder(BrowserActivity.this)
2998 .setTitle(R.string.security_warning)
2999 .setIcon(R.drawable.ic_dialog_alert)
3000 .setView(warningsView)
3001 .setPositiveButton(R.string.ssl_continue,
3002 new DialogInterface.OnClickListener() {
3003 public void onClick(DialogInterface dialog, int whichButton) {
3004 handler.proceed();
3005 }
3006 })
3007 .setNeutralButton(R.string.view_certificate,
3008 new DialogInterface.OnClickListener() {
3009 public void onClick(DialogInterface dialog, int whichButton) {
3010 showSSLCertificateOnError(view, handler, error);
3011 }
3012 })
3013 .setNegativeButton(R.string.cancel,
3014 new DialogInterface.OnClickListener() {
3015 public void onClick(DialogInterface dialog, int whichButton) {
3016 handler.cancel();
3017 BrowserActivity.this.resetTitleAndRevertLockIcon();
3018 }
3019 })
3020 .setOnCancelListener(
3021 new DialogInterface.OnCancelListener() {
3022 public void onCancel(DialogInterface dialog) {
3023 handler.cancel();
3024 BrowserActivity.this.resetTitleAndRevertLockIcon();
3025 }
3026 })
3027 .show();
3028 } else {
3029 handler.proceed();
3030 }
3031 }
3032
3033 /**
3034 * Handles an HTTP authentication request.
3035 *
3036 * @param handler The authentication handler
3037 * @param host The host
3038 * @param realm The realm
3039 */
3040 @Override
3041 public void onReceivedHttpAuthRequest(WebView view,
3042 final HttpAuthHandler handler, final String host, final String realm) {
3043 String username = null;
3044 String password = null;
3045
3046 boolean reuseHttpAuthUsernamePassword =
3047 handler.useHttpAuthUsernamePassword();
3048
3049 if (reuseHttpAuthUsernamePassword) {
3050 String[] credentials =
3051 mWebView.getHttpAuthUsernamePassword(host, realm);
3052 if (credentials != null && credentials.length == 2) {
3053 username = credentials[0];
3054 password = credentials[1];
3055 }
3056 }
3057
3058 if (username != null && password != null) {
3059 handler.proceed(username, password);
3060 } else {
3061 showHttpAuthentication(handler, host, realm, null, null, null, 0);
3062 }
3063 }
3064
3065 @Override
3066 public boolean shouldOverrideKeyEvent(WebView view, KeyEvent event) {
3067 if (mMenuIsDown) {
3068 // only check shortcut key when MENU is held
3069 return getWindow().isShortcutKey(event.getKeyCode(), event);
3070 } else {
3071 return false;
3072 }
3073 }
3074
3075 @Override
3076 public void onUnhandledKeyEvent(WebView view, KeyEvent event) {
3077 if (event.isDown()) {
3078 mKeyTracker.doKeyDown(event.getKeyCode(), event);
3079 } else {
3080 mKeyTracker.doKeyUp(event.getKeyCode(), event);
3081 }
3082 }
3083 };
3084
3085 //--------------------------------------------------------------------------
3086 // WebChromeClient implementation
3087 //--------------------------------------------------------------------------
3088
3089 /* package */ WebChromeClient getWebChromeClient() {
3090 return mWebChromeClient;
3091 }
3092
3093 private final WebChromeClient mWebChromeClient = new WebChromeClient() {
3094 // Helper method to create a new tab or sub window.
3095 private void createWindow(final boolean dialog, final Message msg) {
3096 if (dialog) {
3097 mTabControl.createSubWindow();
3098 final TabControl.Tab t = mTabControl.getCurrentTab();
3099 attachSubWindow(t);
3100 WebView.WebViewTransport transport =
3101 (WebView.WebViewTransport) msg.obj;
3102 transport.setWebView(t.getSubWebView());
3103 msg.sendToTarget();
3104 } else {
3105 final TabControl.Tab parent = mTabControl.getCurrentTab();
3106 // openTabAndShow will dispatch the message after creating the
3107 // new WebView. This will prevent another request from coming
3108 // in during the animation.
3109 openTabAndShow(null, msg);
3110 parent.addChildTab(mTabControl.getCurrentTab());
3111 WebView.WebViewTransport transport =
3112 (WebView.WebViewTransport) msg.obj;
3113 transport.setWebView(mWebView);
3114 }
3115 }
3116
3117 @Override
3118 public boolean onCreateWindow(WebView view, final boolean dialog,
3119 final boolean userGesture, final Message resultMsg) {
3120 // Ignore these requests during tab animations or if the tab
3121 // overview is showing.
3122 if (mAnimationCount > 0 || mTabOverview != null) {
3123 return false;
3124 }
3125 // Short-circuit if we can't create any more tabs or sub windows.
3126 if (dialog && mTabControl.getCurrentSubWindow() != null) {
3127 new AlertDialog.Builder(BrowserActivity.this)
3128 .setTitle(R.string.too_many_subwindows_dialog_title)
3129 .setIcon(R.drawable.ic_dialog_alert)
3130 .setMessage(R.string.too_many_subwindows_dialog_message)
3131 .setPositiveButton(R.string.ok, null)
3132 .show();
3133 return false;
3134 } else if (mTabControl.getTabCount() >= TabControl.MAX_TABS) {
3135 new AlertDialog.Builder(BrowserActivity.this)
3136 .setTitle(R.string.too_many_windows_dialog_title)
3137 .setIcon(R.drawable.ic_dialog_alert)
3138 .setMessage(R.string.too_many_windows_dialog_message)
3139 .setPositiveButton(R.string.ok, null)
3140 .show();
3141 return false;
3142 }
3143
3144 // Short-circuit if this was a user gesture.
3145 if (userGesture) {
3146 // createWindow will call openTabAndShow for new Windows and
3147 // that will call tabPicker which will increment
3148 // mAnimationCount.
3149 createWindow(dialog, resultMsg);
3150 return true;
3151 }
3152
3153 // Allow the popup and create the appropriate window.
3154 final AlertDialog.OnClickListener allowListener =
3155 new AlertDialog.OnClickListener() {
3156 public void onClick(DialogInterface d,
3157 int which) {
3158 // Same comment as above for setting
3159 // mAnimationCount.
3160 createWindow(dialog, resultMsg);
3161 // Since we incremented mAnimationCount while the
3162 // dialog was up, we have to decrement it here.
3163 mAnimationCount--;
3164 }
3165 };
3166
3167 // Block the popup by returning a null WebView.
3168 final AlertDialog.OnClickListener blockListener =
3169 new AlertDialog.OnClickListener() {
3170 public void onClick(DialogInterface d, int which) {
3171 resultMsg.sendToTarget();
3172 // We are not going to trigger an animation so
3173 // unblock keys and animation requests.
3174 mAnimationCount--;
3175 }
3176 };
3177
3178 // Build a confirmation dialog to display to the user.
3179 final AlertDialog d =
3180 new AlertDialog.Builder(BrowserActivity.this)
3181 .setTitle(R.string.attention)
3182 .setIcon(R.drawable.ic_dialog_alert)
3183 .setMessage(R.string.popup_window_attempt)
3184 .setPositiveButton(R.string.allow, allowListener)
3185 .setNegativeButton(R.string.block, blockListener)
3186 .setCancelable(false)
3187 .create();
3188
3189 // Show the confirmation dialog.
3190 d.show();
3191 // We want to increment mAnimationCount here to prevent a
3192 // potential race condition. If the user allows a pop-up from a
3193 // site and that pop-up then triggers another pop-up, it is
3194 // possible to get the BACK key between here and when the dialog
3195 // appears.
3196 mAnimationCount++;
3197 return true;
3198 }
3199
3200 @Override
3201 public void onCloseWindow(WebView window) {
3202 final int currentIndex = mTabControl.getCurrentIndex();
3203 final TabControl.Tab parent =
3204 mTabControl.getCurrentTab().getParentTab();
3205 if (parent != null) {
3206 // JavaScript can only close popup window.
3207 removeTabAndShow(currentIndex, mTabControl.getTabIndex(parent));
3208 }
3209 }
3210
3211 @Override
3212 public void onProgressChanged(WebView view, int newProgress) {
3213 getWindow().setFeatureInt(Window.FEATURE_PROGRESS, newProgress*100);
3214
3215 if (newProgress == 100) {
3216 // onProgressChanged() is called for sub-frame too while
3217 // onPageFinished() is only called for the main frame. sync
3218 // cookie and cache promptly here.
3219 CookieSyncManager.getInstance().sync();
3220 }
3221 }
3222
3223 @Override
3224 public void onReceivedTitle(WebView view, String title) {
3225 String url = view.getUrl();
3226
3227 // here, if url is null, we want to reset the title
3228 setUrlTitle(url, title);
3229
3230 if (url == null ||
3231 url.length() >= SQLiteDatabase.SQLITE_MAX_LIKE_PATTERN_LENGTH) {
3232 return;
3233 }
3234 if (url.startsWith("http://www.")) {
3235 url = url.substring(11);
3236 } else if (url.startsWith("http://")) {
3237 url = url.substring(4);
3238 }
3239 try {
3240 url = "%" + url;
3241 String [] selArgs = new String[] { url };
3242
3243 String where = Browser.BookmarkColumns.URL + " LIKE ? AND "
3244 + Browser.BookmarkColumns.BOOKMARK + " = 0";
3245 Cursor c = mResolver.query(Browser.BOOKMARKS_URI,
3246 Browser.HISTORY_PROJECTION, where, selArgs, null);
3247 if (c.moveToFirst()) {
3248 Log.d(LOGTAG, "updating cursor");
3249 // Current implementation of database only has one entry per
3250 // url.
3251 int titleIndex =
3252 c.getColumnIndex(Browser.BookmarkColumns.TITLE);
3253 c.updateString(titleIndex, title);
3254 c.commitUpdates();
3255 }
3256 c.close();
3257 } catch (IllegalStateException e) {
3258 Log.e(LOGTAG, "BrowserActivity onReceived title", e);
3259 } catch (SQLiteException ex) {
3260 Log.e(LOGTAG, "onReceivedTitle() caught SQLiteException: ", ex);
3261 }
3262 }
3263
3264 @Override
3265 public void onReceivedIcon(WebView view, Bitmap icon) {
3266 if (icon != null) {
3267 BrowserBookmarksAdapter.updateBookmarkFavicon(mResolver,
3268 view.getUrl(), icon);
3269 }
3270 setFavicon(icon);
3271 }
3272
3273 //----------------------------------------------------------------------
3274 // JavaScript functions.
3275 //----------------------------------------------------------------------
3276
3277 // Show an alert to the user.
3278 @Override
3279 public boolean onJsAlert(WebView view, String url, String message,
3280 JsResult result) {
3281 String title = url;
3282 if (URLUtil.isDataUrl(url)) {
3283 // For data: urls, we just display 'JavaScript' similar to
3284 // Safari.
3285 title = getString(R.string.js_dialog_title_default);
3286 } else {
3287 try {
3288 URL aUrl = new URL(url);
3289 // Example: "The page at 'http://www.mit.edu' says:"
3290 title = getText(R.string.js_dialog_title_prefix)
3291 + " '"
3292 + (aUrl.getProtocol() + "://" + aUrl.getHost())
3293 + "' "
3294 + getText(R.string.js_dialog_title_suffix);
3295 } catch (MalformedURLException ex) {
3296 // do nothing. just use the url passed as the title
3297 }
3298 }
3299 final JsResult res = result;
3300 new AlertDialog.Builder(BrowserActivity.this)
3301 .setTitle(title)
3302 .setMessage(message)
3303 .setPositiveButton(R.string.ok,
3304 new AlertDialog.OnClickListener() {
3305 public void onClick(DialogInterface dialog,
3306 int which) {
3307 res.confirm();
3308 }
3309 })
3310 .setCancelable(false)
3311 .show();
3312 return true;
3313 }
3314
3315 @Override
3316 public boolean onJsConfirm(WebView view, String url, String message,
3317 final JsResult result) {
3318 String title = url;
3319 try {
3320 URL aUrl = new URL(url);
3321 // Example: "The page at 'http://www.mit.edu' says:"
3322 title = getText(R.string.js_dialog_title_prefix)
3323 + " '"
3324 + (aUrl.getProtocol() + "://" + aUrl.getHost())
3325 + "' "
3326 + getText(R.string.js_dialog_title_suffix);
3327 } catch (MalformedURLException ex) {
3328 // do nothing. just use the url passed as the title
3329 }
3330 new AlertDialog.Builder(BrowserActivity.this)
3331 .setTitle(title)
3332 .setMessage(message)
3333 .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
3334 public void onClick(DialogInterface dialog, int which) {
3335 mHandler.obtainMessage(JS_CONFIRM, 1, 0, result).sendToTarget();
3336 }})
3337 .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
3338 public void onClick(DialogInterface dialog, int which) {
3339 mHandler.obtainMessage(JS_CONFIRM, 0, 0, result).sendToTarget();
3340 }})
3341 .show();
3342 // Return true so WebView knows we will handle the confirm.
3343 return true;
3344 }
3345
3346 @Override
3347 public boolean onJsPrompt(WebView view, String url, String message, String defaultValue,
3348 final JsPromptResult result) {
3349 String title = url;
3350 try {
3351 URL aUrl = new URL(url);
3352 // Example: "The page at 'http://www.mit.edu' says:"
3353 title = getText(R.string.js_dialog_title_prefix)
3354 + " '"
3355 + (aUrl.getProtocol() + "://" + aUrl.getHost())
3356 + "' "
3357 + getText(R.string.js_dialog_title_suffix);
3358 } catch (MalformedURLException ex) {
3359 // do nothing, just use the url passed as the title
3360 }
3361
3362 final LayoutInflater factory = LayoutInflater.from(BrowserActivity.this);
3363 final View v = factory.inflate(R.layout.js_prompt, null);
3364 ((TextView)v.findViewById(R.id.message)).setText(message);
3365 ((EditText)v.findViewById(R.id.value)).setText(defaultValue);
3366
3367 new AlertDialog.Builder(BrowserActivity.this)
3368 .setTitle(title)
3369 .setView(v)
3370 .setPositiveButton(R.string.ok,
3371 new DialogInterface.OnClickListener() {
3372 public void onClick(DialogInterface dialog, int whichButton) {
3373 String value = ((EditText)v.findViewById(R.id.value)).getText()
3374 .toString();
3375 result.confirm(value);
3376 }
3377 })
3378 .setNegativeButton(R.string.cancel,
3379 new DialogInterface.OnClickListener() {
3380 public void onClick(DialogInterface dialog, int whichButton) {
3381 result.cancel();
3382 }
3383 })
3384 .setOnCancelListener(
3385 new DialogInterface.OnCancelListener() {
3386 public void onCancel(DialogInterface dialog) {
3387 result.cancel();
3388 }
3389 })
3390 .show();
3391
3392 // Return true so WebView knows we will handle the prompt.
3393 return true;
3394 }
3395
3396 @Override
3397 public boolean onJsBeforeUnload(WebView view, String url,
3398 String message, final JsResult result) {
3399 final String m =
3400 getString(R.string.js_dialog_before_unload, message);
3401 new AlertDialog.Builder(BrowserActivity.this)
3402 .setMessage(m)
3403 .setPositiveButton(R.string.ok,
3404 new DialogInterface.OnClickListener() {
3405 public void onClick(DialogInterface dialog,
3406 int which) {
3407 // Use JS_CONFIRM since it has the same
3408 // behavior we want here.
3409 mHandler.obtainMessage(JS_CONFIRM, 1, 0,
3410 result).sendToTarget();
3411 }})
3412 .setNegativeButton(R.string.cancel,
3413 new DialogInterface.OnClickListener() {
3414 public void onClick(DialogInterface dialog,
3415 int which) {
3416 // Use JS_CONFIRM since it has the same
3417 // behavior we want here.
3418 mHandler.obtainMessage(JS_CONFIRM, 0, 0,
3419 result).sendToTarget();
3420 }})
3421 .show();
3422 // Return true so WebView knows we will handle the dialog.
3423 return true;
3424 }
3425 };
3426
3427 /**
3428 * Notify the host application a download should be done, or that
3429 * the data should be streamed if a streaming viewer is available.
3430 * @param url The full url to the content that should be downloaded
3431 * @param contentDisposition Content-disposition http header, if
3432 * present.
3433 * @param mimetype The mimetype of the content reported by the server
3434 * @param contentLength The file size reported by the server
3435 */
3436 public void onDownloadStart(String url, String userAgent,
3437 String contentDisposition, String mimetype, long contentLength) {
3438 // if we're dealing wih A/V content that's not explicitly marked
3439 // for download, check if it's streamable.
3440 if (contentDisposition == null
3441 || !contentDisposition.regionMatches(true, 0, "attachment", 0, 10)) {
3442 // query the package manager to see if there's a registered handler
3443 // that matches.
3444 Intent intent = new Intent(Intent.ACTION_VIEW);
3445 intent.setDataAndType(Uri.parse(url), mimetype);
3446 if (getPackageManager().queryIntentActivities(intent,
3447 PackageManager.MATCH_DEFAULT_ONLY).size() != 0) {
3448 // someone knows how to handle this mime type with this scheme, don't download.
3449 try {
3450 startActivity(intent);
3451 return;
3452 } catch (ActivityNotFoundException ex) {
3453 if (Config.LOGD) {
3454 Log.d(LOGTAG, "activity not found for " + mimetype
3455 + " over " + Uri.parse(url).getScheme(), ex);
3456 }
3457 // Best behavior is to fall back to a download in this case
3458 }
3459 }
3460 }
3461 onDownloadStartNoStream(url, userAgent, contentDisposition, mimetype, contentLength);
3462 }
3463
3464 /**
3465 * Notify the host application a download should be done, even if there
3466 * is a streaming viewer available for thise type.
3467 * @param url The full url to the content that should be downloaded
3468 * @param contentDisposition Content-disposition http header, if
3469 * present.
3470 * @param mimetype The mimetype of the content reported by the server
3471 * @param contentLength The file size reported by the server
3472 */
3473 public void onDownloadStartNoStream(String url, String userAgent,
3474 String contentDisposition, String mimetype, long contentLength) {
3475
3476 String filename = URLUtil.guessFileName(url,
3477 contentDisposition, mimetype);
3478
3479 // Check to see if we have an SDCard
3480 if (!Environment.getExternalStorageState().
3481 equals(Environment.MEDIA_MOUNTED)) {
3482 String msg =
3483 getString(R.string.download_no_sdcard_dlg_msg, filename);
3484
3485 new AlertDialog.Builder(this)
3486 .setTitle(R.string.download_no_sdcard_dlg_title)
3487 .setIcon(R.drawable.ic_dialog_alert)
3488 .setMessage(msg)
3489 .setPositiveButton(R.string.ok, null)
3490 .show();
3491 return;
3492 }
3493
3494 String cookies = CookieManager.getInstance().getCookie(url);
3495
3496 ContentValues values = new ContentValues();
3497 values.put(Downloads.URI, url);
3498 values.put(Downloads.COOKIE_DATA, cookies);
3499 values.put(Downloads.USER_AGENT, userAgent);
3500 values.put(Downloads.NOTIFICATION_PACKAGE,
3501 getPackageName());
3502 values.put(Downloads.NOTIFICATION_CLASS,
3503 BrowserDownloadPage.class.getCanonicalName());
3504 values.put(Downloads.VISIBILITY, Downloads.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
3505 values.put(Downloads.MIMETYPE, mimetype);
3506 values.put(Downloads.FILENAME_HINT, filename);
3507 values.put(Downloads.DESCRIPTION, Uri.parse(url).getHost());
3508 if (contentLength > 0) {
3509 values.put(Downloads.TOTAL_BYTES, contentLength);
3510 }
3511 final Uri contentUri =
3512 getContentResolver().insert(Downloads.CONTENT_URI, values);
3513 viewDownloads(contentUri);
3514
3515 }
3516
3517 /**
3518 * Resets the lock icon. This method is called when we start a new load and
3519 * know the url to be loaded.
3520 */
3521 private void resetLockIcon(String url) {
3522 // Save the lock-icon state (we revert to it if the load gets cancelled)
3523 saveLockIcon();
3524
3525 mLockIconType = LOCK_ICON_UNSECURE;
3526 if (URLUtil.isHttpsUrl(url)) {
3527 mLockIconType = LOCK_ICON_SECURE;
3528 if (Config.LOGV) {
3529 Log.v(LOGTAG, "BrowserActivity.resetLockIcon:" +
3530 " reset lock icon to " + mLockIconType);
3531 }
3532 }
3533
3534 updateLockIconImage(LOCK_ICON_UNSECURE);
3535 }
3536
3537 /**
3538 * Resets the lock icon. This method is called when the icon needs to be
3539 * reset but we do not know whether we are loading a secure or not secure
3540 * page.
3541 */
3542 private void resetLockIcon() {
3543 // Save the lock-icon state (we revert to it if the load gets cancelled)
3544 saveLockIcon();
3545
3546 mLockIconType = LOCK_ICON_UNSECURE;
3547
3548 if (Config.LOGV) {
3549 Log.v(LOGTAG, "BrowserActivity.resetLockIcon:" +
3550 " reset lock icon to " + mLockIconType);
3551 }
3552
3553 updateLockIconImage(LOCK_ICON_UNSECURE);
3554 }
3555
3556 /**
3557 * Updates the lock-icon image in the title-bar.
3558 */
3559 private void updateLockIconImage(int lockIconType) {
3560 Drawable d = null;
3561 if (lockIconType == LOCK_ICON_SECURE) {
3562 d = mSecLockIcon;
3563 } else if (lockIconType == LOCK_ICON_MIXED) {
3564 d = mMixLockIcon;
3565 }
3566 getWindow().setFeatureDrawable(Window.FEATURE_RIGHT_ICON, d);
3567 }
3568
3569 /**
3570 * Displays a page-info dialog.
3571 * @param view The target web-view.
3572 * @param fromShowSSLCertificateOnError The flag that indicates whether
3573 * this dialog was opened from the SSL-certificate-on-error dialog or
3574 * not. This is important, since we need to know whether to return to
3575 * the parent dialog or simply dismiss.
3576 */
3577 private void showPageInfo(final WebView view,
3578 final boolean fromShowSSLCertificateOnError) {
3579 final LayoutInflater factory = LayoutInflater
3580 .from(this);
3581
3582 final View pageInfoView = factory.inflate(R.layout.page_info, null);
3583
3584 String url = null;
3585 String title = null;
3586
3587 // Use the cached title and url if this is the current WebView
3588 if (view == mWebView) {
3589 url = mUrl;
3590 title = mTitle;
3591 } else {
3592 url = view.getUrl();
3593 title = view.getTitle();
3594 }
3595
3596 if (url == null) {
3597 url = "";
3598 }
3599 if (title == null) {
3600 title = "";
3601 }
3602
3603 ((TextView) pageInfoView.findViewById(R.id.address)).setText(url);
3604 ((TextView) pageInfoView.findViewById(R.id.title)).setText(title);
3605
3606 mPageInfoView = view;
3607 mPageInfoFromShowSSLCertificateOnError = new Boolean(fromShowSSLCertificateOnError);
3608
3609 AlertDialog.Builder alertDialogBuilder =
3610 new AlertDialog.Builder(this)
3611 .setTitle(R.string.page_info).setIcon(R.drawable.ic_dialog_info)
3612 .setView(pageInfoView)
3613 .setPositiveButton(
3614 R.string.ok,
3615 new DialogInterface.OnClickListener() {
3616 public void onClick(DialogInterface dialog,
3617 int whichButton) {
3618 mPageInfoDialog = null;
3619 mPageInfoView = null;
3620 mPageInfoFromShowSSLCertificateOnError = null;
3621
3622 // if we came here from the SSL error dialog
3623 if (fromShowSSLCertificateOnError) {
3624 // go back to the SSL error dialog
3625 showSSLCertificateOnError(
3626 mSSLCertificateOnErrorView,
3627 mSSLCertificateOnErrorHandler,
3628 mSSLCertificateOnErrorError);
3629 }
3630 }
3631 })
3632 .setOnCancelListener(
3633 new DialogInterface.OnCancelListener() {
3634 public void onCancel(DialogInterface dialog) {
3635 mPageInfoDialog = null;
3636 mPageInfoView = null;
3637 mPageInfoFromShowSSLCertificateOnError = null;
3638
3639 // if we came here from the SSL error dialog
3640 if (fromShowSSLCertificateOnError) {
3641 // go back to the SSL error dialog
3642 showSSLCertificateOnError(
3643 mSSLCertificateOnErrorView,
3644 mSSLCertificateOnErrorHandler,
3645 mSSLCertificateOnErrorError);
3646 }
3647 }
3648 });
3649
3650 // if we have a main top-level page SSL certificate set or a certificate
3651 // error
3652 if (fromShowSSLCertificateOnError || view.getCertificate() != null) {
3653 // add a 'View Certificate' button
3654 alertDialogBuilder.setNeutralButton(
3655 R.string.view_certificate,
3656 new DialogInterface.OnClickListener() {
3657 public void onClick(DialogInterface dialog,
3658 int whichButton) {
3659 mPageInfoDialog = null;
3660 mPageInfoView = null;
3661 mPageInfoFromShowSSLCertificateOnError = null;
3662
3663 // if we came here from the SSL error dialog
3664 if (fromShowSSLCertificateOnError) {
3665 // go back to the SSL error dialog
3666 showSSLCertificateOnError(
3667 mSSLCertificateOnErrorView,
3668 mSSLCertificateOnErrorHandler,
3669 mSSLCertificateOnErrorError);
3670 } else {
3671 // otherwise, display the top-most certificate from
3672 // the chain
3673 if (view.getCertificate() != null) {
3674 showSSLCertificate(view);
3675 }
3676 }
3677 }
3678 });
3679 }
3680
3681 mPageInfoDialog = alertDialogBuilder.show();
3682 }
3683
3684 /**
3685 * Displays the main top-level page SSL certificate dialog
3686 * (accessible from the Page-Info dialog).
3687 * @param view The target web-view.
3688 */
3689 private void showSSLCertificate(final WebView view) {
3690 final View certificateView =
3691 inflateCertificateView(mWebView.getCertificate());
3692 if (certificateView == null) {
3693 return;
3694 }
3695
3696 LayoutInflater factory = LayoutInflater.from(this);
3697
3698 final LinearLayout placeholder =
3699 (LinearLayout)certificateView.findViewById(R.id.placeholder);
3700
3701 LinearLayout ll = (LinearLayout) factory.inflate(
3702 R.layout.ssl_success, placeholder);
3703 ((TextView)ll.findViewById(R.id.success))
3704 .setText(R.string.ssl_certificate_is_valid);
3705
3706 mSSLCertificateView = view;
3707 mSSLCertificateDialog =
3708 new AlertDialog.Builder(this)
3709 .setTitle(R.string.ssl_certificate).setIcon(
3710 R.drawable.ic_dialog_browser_certificate_secure)
3711 .setView(certificateView)
3712 .setPositiveButton(R.string.ok,
3713 new DialogInterface.OnClickListener() {
3714 public void onClick(DialogInterface dialog,
3715 int whichButton) {
3716 mSSLCertificateDialog = null;
3717 mSSLCertificateView = null;
3718
3719 showPageInfo(view, false);
3720 }
3721 })
3722 .setOnCancelListener(
3723 new DialogInterface.OnCancelListener() {
3724 public void onCancel(DialogInterface dialog) {
3725 mSSLCertificateDialog = null;
3726 mSSLCertificateView = null;
3727
3728 showPageInfo(view, false);
3729 }
3730 })
3731 .show();
3732 }
3733
3734 /**
3735 * Displays the SSL error certificate dialog.
3736 * @param view The target web-view.
3737 * @param handler The SSL error handler responsible for cancelling the
3738 * connection that resulted in an SSL error or proceeding per user request.
3739 * @param error The SSL error object.
3740 */
3741 private void showSSLCertificateOnError(
3742 final WebView view, final SslErrorHandler handler, final SslError error) {
3743
3744 final View certificateView =
3745 inflateCertificateView(error.getCertificate());
3746 if (certificateView == null) {
3747 return;
3748 }
3749
3750 LayoutInflater factory = LayoutInflater.from(this);
3751
3752 final LinearLayout placeholder =
3753 (LinearLayout)certificateView.findViewById(R.id.placeholder);
3754
3755 if (error.hasError(SslError.SSL_UNTRUSTED)) {
3756 LinearLayout ll = (LinearLayout)factory
3757 .inflate(R.layout.ssl_warning, placeholder);
3758 ((TextView)ll.findViewById(R.id.warning))
3759 .setText(R.string.ssl_untrusted);
3760 }
3761
3762 if (error.hasError(SslError.SSL_IDMISMATCH)) {
3763 LinearLayout ll = (LinearLayout)factory
3764 .inflate(R.layout.ssl_warning, placeholder);
3765 ((TextView)ll.findViewById(R.id.warning))
3766 .setText(R.string.ssl_mismatch);
3767 }
3768
3769 if (error.hasError(SslError.SSL_EXPIRED)) {
3770 LinearLayout ll = (LinearLayout)factory
3771 .inflate(R.layout.ssl_warning, placeholder);
3772 ((TextView)ll.findViewById(R.id.warning))
3773 .setText(R.string.ssl_expired);
3774 }
3775
3776 if (error.hasError(SslError.SSL_NOTYETVALID)) {
3777 LinearLayout ll = (LinearLayout)factory
3778 .inflate(R.layout.ssl_warning, placeholder);
3779 ((TextView)ll.findViewById(R.id.warning))
3780 .setText(R.string.ssl_not_yet_valid);
3781 }
3782
3783 mSSLCertificateOnErrorHandler = handler;
3784 mSSLCertificateOnErrorView = view;
3785 mSSLCertificateOnErrorError = error;
3786 mSSLCertificateOnErrorDialog =
3787 new AlertDialog.Builder(this)
3788 .setTitle(R.string.ssl_certificate).setIcon(
3789 R.drawable.ic_dialog_browser_certificate_partially_secure)
3790 .setView(certificateView)
3791 .setPositiveButton(R.string.ok,
3792 new DialogInterface.OnClickListener() {
3793 public void onClick(DialogInterface dialog,
3794 int whichButton) {
3795 mSSLCertificateOnErrorDialog = null;
3796 mSSLCertificateOnErrorView = null;
3797 mSSLCertificateOnErrorHandler = null;
3798 mSSLCertificateOnErrorError = null;
3799
3800 mWebViewClient.onReceivedSslError(
3801 view, handler, error);
3802 }
3803 })
3804 .setNeutralButton(R.string.page_info_view,
3805 new DialogInterface.OnClickListener() {
3806 public void onClick(DialogInterface dialog,
3807 int whichButton) {
3808 mSSLCertificateOnErrorDialog = null;
3809
3810 // do not clear the dialog state: we will
3811 // need to show the dialog again once the
3812 // user is done exploring the page-info details
3813
3814 showPageInfo(view, true);
3815 }
3816 })
3817 .setOnCancelListener(
3818 new DialogInterface.OnCancelListener() {
3819 public void onCancel(DialogInterface dialog) {
3820 mSSLCertificateOnErrorDialog = null;
3821 mSSLCertificateOnErrorView = null;
3822 mSSLCertificateOnErrorHandler = null;
3823 mSSLCertificateOnErrorError = null;
3824
3825 mWebViewClient.onReceivedSslError(
3826 view, handler, error);
3827 }
3828 })
3829 .show();
3830 }
3831
3832 /**
3833 * Inflates the SSL certificate view (helper method).
3834 * @param certificate The SSL certificate.
3835 * @return The resultant certificate view with issued-to, issued-by,
3836 * issued-on, expires-on, and possibly other fields set.
3837 * If the input certificate is null, returns null.
3838 */
3839 private View inflateCertificateView(SslCertificate certificate) {
3840 if (certificate == null) {
3841 return null;
3842 }
3843
3844 LayoutInflater factory = LayoutInflater.from(this);
3845
3846 View certificateView = factory.inflate(
3847 R.layout.ssl_certificate, null);
3848
3849 // issued to:
3850 SslCertificate.DName issuedTo = certificate.getIssuedTo();
3851 if (issuedTo != null) {
3852 ((TextView) certificateView.findViewById(R.id.to_common))
3853 .setText(issuedTo.getCName());
3854 ((TextView) certificateView.findViewById(R.id.to_org))
3855 .setText(issuedTo.getOName());
3856 ((TextView) certificateView.findViewById(R.id.to_org_unit))
3857 .setText(issuedTo.getUName());
3858 }
3859
3860 // issued by:
3861 SslCertificate.DName issuedBy = certificate.getIssuedBy();
3862 if (issuedBy != null) {
3863 ((TextView) certificateView.findViewById(R.id.by_common))
3864 .setText(issuedBy.getCName());
3865 ((TextView) certificateView.findViewById(R.id.by_org))
3866 .setText(issuedBy.getOName());
3867 ((TextView) certificateView.findViewById(R.id.by_org_unit))
3868 .setText(issuedBy.getUName());
3869 }
3870
3871 // issued on:
3872 String issuedOn = reformatCertificateDate(
3873 certificate.getValidNotBefore());
3874 ((TextView) certificateView.findViewById(R.id.issued_on))
3875 .setText(issuedOn);
3876
3877 // expires on:
3878 String expiresOn = reformatCertificateDate(
3879 certificate.getValidNotAfter());
3880 ((TextView) certificateView.findViewById(R.id.expires_on))
3881 .setText(expiresOn);
3882
3883 return certificateView;
3884 }
3885
3886 /**
3887 * Re-formats the certificate date (Date.toString()) string to
3888 * a properly localized date string.
3889 * @return Properly localized version of the certificate date string and
3890 * the original certificate date string if fails to localize.
3891 * If the original string is null, returns an empty string "".
3892 */
3893 private String reformatCertificateDate(String certificateDate) {
3894 String reformattedDate = null;
3895
3896 if (certificateDate != null) {
3897 Date date = null;
3898 try {
3899 date = java.text.DateFormat.getInstance().parse(certificateDate);
3900 } catch (ParseException e) {
3901 date = null;
3902 }
3903
3904 if (date != null) {
3905 reformattedDate =
3906 DateFormat.getDateFormat(this).format(date);
3907 }
3908 }
3909
3910 return reformattedDate != null ? reformattedDate :
3911 (certificateDate != null ? certificateDate : "");
3912 }
3913
3914 /**
3915 * Displays an http-authentication dialog.
3916 */
3917 private void showHttpAuthentication(final HttpAuthHandler handler,
3918 final String host, final String realm, final String title,
3919 final String name, final String password, int focusId) {
3920 LayoutInflater factory = LayoutInflater.from(this);
3921 final View v = factory
3922 .inflate(R.layout.http_authentication, null);
3923 if (name != null) {
3924 ((EditText) v.findViewById(R.id.username_edit)).setText(name);
3925 }
3926 if (password != null) {
3927 ((EditText) v.findViewById(R.id.password_edit)).setText(password);
3928 }
3929
3930 String titleText = title;
3931 if (titleText == null) {
3932 titleText = getText(R.string.sign_in_to).toString().replace(
3933 "%s1", host).replace("%s2", realm);
3934 }
3935
3936 mHttpAuthHandler = handler;
3937 mHttpAuthenticationDialog = new AlertDialog.Builder(this)
3938 .setTitle(titleText)
3939 .setIcon(R.drawable.ic_dialog_alert)
3940 .setView(v)
3941 .setPositiveButton(R.string.action,
3942 new DialogInterface.OnClickListener() {
3943 public void onClick(DialogInterface dialog,
3944 int whichButton) {
3945 String nm = ((EditText) v
3946 .findViewById(R.id.username_edit))
3947 .getText().toString();
3948 String pw = ((EditText) v
3949 .findViewById(R.id.password_edit))
3950 .getText().toString();
3951 BrowserActivity.this.setHttpAuthUsernamePassword
3952 (host, realm, nm, pw);
3953 handler.proceed(nm, pw);
3954 mHttpAuthenticationDialog = null;
3955 mHttpAuthHandler = null;
3956 }})
3957 .setNegativeButton(R.string.cancel,
3958 new DialogInterface.OnClickListener() {
3959 public void onClick(DialogInterface dialog,
3960 int whichButton) {
3961 handler.cancel();
3962 BrowserActivity.this.resetTitleAndRevertLockIcon();
3963 mHttpAuthenticationDialog = null;
3964 mHttpAuthHandler = null;
3965 }})
3966 .setOnCancelListener(new DialogInterface.OnCancelListener() {
3967 public void onCancel(DialogInterface dialog) {
3968 handler.cancel();
3969 BrowserActivity.this.resetTitleAndRevertLockIcon();
3970 mHttpAuthenticationDialog = null;
3971 mHttpAuthHandler = null;
3972 }})
3973 .show();
3974 if (focusId != 0) {
3975 mHttpAuthenticationDialog.findViewById(focusId).requestFocus();
3976 } else {
3977 v.findViewById(R.id.username_edit).requestFocus();
3978 }
3979 }
3980
3981 public int getProgress() {
3982 return mWebView.getProgress();
3983 }
3984
3985 /**
3986 * Set HTTP authentication password.
3987 *
3988 * @param host The host for the password
3989 * @param realm The realm for the password
3990 * @param username The username for the password. If it is null, it means
3991 * password can't be saved.
3992 * @param password The password
3993 */
3994 public void setHttpAuthUsernamePassword(String host, String realm,
3995 String username,
3996 String password) {
3997 mWebView.setHttpAuthUsernamePassword(host, realm, username, password);
3998 }
3999
4000 /**
4001 * http stack says net has come or gone... inform the user
4002 * @param up true if net has come up, false if net has gone down
4003 */
4004 public void onNetworkToggle(boolean up) {
4005 if (up) {
4006 if (mAlertDialog != null) {
4007 mAlertDialog.cancel();
4008 mAlertDialog = null;
4009 }
4010 } else {
4011 if (mInLoad && mAlertDialog == null) {
4012 mAlertDialog = new AlertDialog.Builder(this)
4013 .setTitle(R.string.loadSuspendedTitle)
4014 .setMessage(R.string.loadSuspended)
4015 .setPositiveButton(R.string.ok, null)
4016 .show();
4017 }
4018 }
4019 }
4020
4021 @Override
4022 protected void onActivityResult(int requestCode, int resultCode,
4023 Intent intent) {
4024 switch (requestCode) {
4025 case BOOKMARKS_PAGE:
4026 case CLASSIC_HISTORY_PAGE:
4027 if (resultCode == RESULT_OK && intent != null) {
4028 String data = intent.getAction();
4029 Bundle extras = intent.getExtras();
4030 if (extras != null && extras.getBoolean("new_window", false)) {
4031 openTab(data);
4032 } else {
4033 // If the Window overview is up and we are not in the
4034 // middle of an animation, animate away from it to the
4035 // current tab.
4036 if (mTabOverview != null && mAnimationCount == 0) {
4037 sendAnimateFromOverview(mTabControl.getCurrentTab(),
4038 false, data, TAB_OVERVIEW_DELAY, null);
4039 } else {
4040 loadURL(data);
4041 }
4042 }
4043 }
4044 break;
4045 default:
4046 break;
4047 }
4048 getTopWindow().requestFocus();
4049 }
4050
4051 /*
4052 * This method is called as a result of the user selecting the options
4053 * menu to see the download window, or when a download changes state. It
4054 * shows the download window ontop of the current window.
4055 */
4056 private void viewDownloads(Uri downloadRecord) {
4057 Intent intent = new Intent(this,
4058 BrowserDownloadPage.class);
4059 intent.setData(downloadRecord);
4060 startActivityForResult(intent, this.DOWNLOAD_PAGE);
4061
4062 }
4063
4064 /**
4065 * Handle results from Tab Switcher mTabOverview tool
4066 */
4067 private class TabListener implements ImageGrid.Listener {
4068 public void remove(int position) {
4069 // Note: Remove is not enabled if we have only one tab.
4070 if (Config.DEBUG && mTabControl.getTabCount() == 1) {
4071 throw new AssertionError();
4072 }
4073
4074 mTabControl.removeTab(mTabControl.getTab(position));
4075 // The tab overview could have been dismissed before this method is
4076 // called.
4077 if (mTabOverview != null) {
4078 // Remove the tab and change the index.
4079 mTabOverview.remove(position--);
4080 mTabOverview.setCurrentIndex(position);
4081 } else {
4082 position--;
4083 }
4084
4085 // FIXME: This isn't really right. We don't have a current WebView
4086 // since we are switching between tabs and haven't selected a new
4087 // one. This just prevents a NPE in case the user hits home from the
4088 // tab switcher.
4089 int index = position;
4090 if (index == ImageGrid.NEW_TAB) {
4091 index = 0;
4092 }
4093 final TabControl.Tab t = mTabControl.getTab(index);
4094 // Only the current tab ensures its WebView is non-null. This
4095 // implies that we are reloading the freed tab.
4096 mTabControl.setCurrentTab(t);
4097 mWebView = t.getWebView();
4098 }
4099 public void onClick(int index) {
4100 // Change the tab if necessary.
4101 // Index equals ImageGrid.CANCEL when pressing back from the tab
4102 // overview.
4103 if (index == ImageGrid.CANCEL) {
4104 index = mTabControl.getCurrentIndex();
4105 // The current index is -1 if the current tab was removed.
4106 if (index == -1) {
4107 // Take the last tab as a fallback.
4108 index = mTabControl.getTabCount() - 1;
4109 }
4110 }
4111
4112 // Clear all the data for tab picker so next time it will be
4113 // recreated.
4114 mTabControl.wipeAllPickerData();
4115 BrowserActivity.this.getWindow().setFeatureInt(
4116 Window.FEATURE_PROGRESS, Window.PROGRESS_VISIBILITY_ON);
4117 BrowserActivity.this.mMenuState = EMPTY_MENU;
4118
4119 // NEW_TAB means that the "New Tab" cell was clicked on.
4120 if (index == ImageGrid.NEW_TAB) {
4121 openTabAndShow(mSettings.getHomePage(), null);
4122 } else {
4123 sendAnimateFromOverview(mTabControl.getTab(index),
4124 false, null, 0, null);
4125 }
4126 }
4127 }
4128
4129 // A fake View that draws the WebView's picture with a fast zoom filter.
4130 // The View is used in case the tab is freed during the animation because
4131 // of low memory.
4132 private static class AnimatingView extends View {
4133 private static final int ZOOM_BITS = Paint.FILTER_BITMAP_FLAG |
4134 Paint.DITHER_FLAG | Paint.SUBPIXEL_TEXT_FLAG;
4135 private static final DrawFilter sZoomFilter =
4136 new PaintFlagsDrawFilter(ZOOM_BITS, Paint.LINEAR_TEXT_FLAG);
4137 private final Picture mPicture;
4138 private final float mScale;
4139 private final int mScrollX;
4140 private final int mScrollY;
4141 final TabControl.Tab mTab;
4142
4143 AnimatingView(Context ctxt, TabControl.Tab t) {
4144 super(ctxt);
4145 mTab = t;
4146 // Use the top window in the animation since the tab overview will
4147 // display the top window in each cell.
4148 final WebView w = t.getTopWindow();
4149 mPicture = w.capturePicture();
4150 mScale = w.getScale() / w.getWidth();
4151 mScrollX = w.getScrollX();
4152 mScrollY = w.getScrollY();
4153 }
4154
4155 @Override
4156 protected void onDraw(Canvas canvas) {
4157 canvas.save();
4158 canvas.drawColor(Color.WHITE);
4159 canvas.setDrawFilter(sZoomFilter);
4160 float scale = getWidth() * mScale;
4161 canvas.scale(scale, scale);
4162 canvas.translate(-mScrollX, -mScrollY);
4163 canvas.drawPicture(mPicture);
4164 canvas.restore();
4165 }
4166 }
4167
4168 /**
4169 * Open the tab picker. This function will always use the current tab in
4170 * its animation.
4171 * @param stay boolean stating whether the tab picker is to remain open
4172 * (in which case it needs a listener and its menu) or not.
4173 * @param index The index of the tab to show as the selection in the tab
4174 * overview.
4175 * @param remove If true, the tab at index will be removed after the
4176 * animation completes.
4177 */
4178 private void tabPicker(final boolean stay, final int index,
4179 final boolean remove) {
4180 if (mTabOverview != null) {
4181 return;
4182 }
4183
4184 int size = mTabControl.getTabCount();
4185
4186 TabListener l = null;
4187 if (stay) {
4188 l = mTabListener = new TabListener();
4189 }
4190 mTabOverview = new ImageGrid(this, stay, l);
4191
4192 for (int i = 0; i < size; i++) {
4193 final TabControl.Tab t = mTabControl.getTab(i);
4194 mTabControl.populatePickerData(t);
4195 mTabOverview.add(t);
4196 }
4197
4198 // Tell the tab overview to show the current tab, the tab overview will
4199 // handle the "New Tab" case.
4200 int currentIndex = mTabControl.getCurrentIndex();
4201 mTabOverview.setCurrentIndex(currentIndex);
4202
4203 // Attach the tab overview.
4204 mContentView.addView(mTabOverview, COVER_SCREEN_PARAMS);
4205
4206 // Create a fake AnimatingView to animate the WebView's picture.
4207 final TabControl.Tab current = mTabControl.getCurrentTab();
4208 final AnimatingView v = new AnimatingView(this, current);
4209 mContentView.addView(v, COVER_SCREEN_PARAMS);
4210 removeTabFromContentView(current);
4211 // Pause timers to get the animation smoother.
4212 current.getWebView().pauseTimers();
4213
4214 // Send a message so the tab picker has a chance to layout and get
4215 // positions for all the cells.
4216 mHandler.sendMessage(mHandler.obtainMessage(ANIMATE_TO_OVERVIEW,
4217 index, remove ? 1 : 0, v));
4218 // Setting this will indicate that we are animating to the overview. We
4219 // set it here to prevent another request to animate from coming in
4220 // between now and when ANIMATE_TO_OVERVIEW is handled.
4221 mAnimationCount++;
4222 if (stay) {
4223 getWindow().setFeatureDrawable(Window.FEATURE_LEFT_ICON, null);
4224 getWindow().setFeatureInt(Window.FEATURE_PROGRESS,
4225 Window.PROGRESS_VISIBILITY_OFF);
4226 setTitle(R.string.tab_picker_title);
4227 }
4228 // Make the menu empty until the animation completes.
4229 mMenuState = EMPTY_MENU;
4230 }
4231
4232 private void bookmarksPicker() {
4233 Intent intent = new Intent(this,
4234 BrowserBookmarksPage.class);
4235 String title = mWebView.getTitle();
4236 String url = mWebView.getUrl();
4237 // Just in case the user opens bookmarks before a page finishes loading
4238 // so the current history item, and therefore the page, is null.
4239 if (null == url) {
4240 url = mLastEnteredUrl;
4241 // This can happen.
4242 if (null == url) {
4243 url = mSettings.getHomePage();
4244 }
4245 }
4246 // In case the web page has not yet received its associated title.
4247 if (title == null) {
4248 title = url;
4249 }
4250 intent.putExtra("title", title);
4251 intent.putExtra("url", url);
4252 intent.putExtra("maxTabsOpen",
4253 mTabControl.getTabCount() >= TabControl.MAX_TABS);
4254 startActivityForResult(intent, BOOKMARKS_PAGE);
4255 }
4256
4257 // Called when loading from bookmarks or goto.
4258 private void loadURL(String url) {
4259 // In case the user enters nothing.
4260 if (url != null && url.length() != 0) {
4261 url = smartUrlFilter(url);
4262 WebView w = getTopWindow();
4263 if (!mWebViewClient.shouldOverrideUrlLoading(w, url)) {
4264 w.loadUrl(url);
4265 }
4266 }
4267 }
4268
4269 private void checkMemory() {
4270 ActivityManager.MemoryInfo mi = new ActivityManager.MemoryInfo();
4271 ((ActivityManager) getSystemService(ACTIVITY_SERVICE))
4272 .getMemoryInfo(mi);
4273 // FIXME: mi.lowMemory is too aggressive, use (mi.availMem <
4274 // mi.threshold) for now
4275 // if (mi.lowMemory) {
4276 if (mi.availMem < mi.threshold) {
4277 Log.w(LOGTAG, "Browser is freeing memory now because: available="
4278 + (mi.availMem / 1024) + "K threshold="
4279 + (mi.threshold / 1024) + "K");
4280 mTabControl.freeMemory();
4281 }
4282 }
4283
4284 private String smartUrlFilter(Uri inUri) {
4285 if (inUri != null) {
4286 return smartUrlFilter(inUri.toString());
4287 }
4288 return null;
4289 }
4290
4291
4292 // get window count
4293
4294 int getWindowCount(){
4295 if(mTabControl != null){
4296 return mTabControl.getTabCount();
4297 }
4298 return 0;
4299 }
4300
4301 static final Pattern ACCEPTED_URI_SCHEMA = Pattern.compile(
4302 "(?i)" + // switch on case insensitive matching
4303 "(" + // begin group for schema
4304 "(?:http|https|file):\\/\\/" +
4305 "|(?:data|about|content|javascript):" +
4306 ")" +
4307 "(.*)" );
4308
4309 /**
4310 * Attempts to determine whether user input is a URL or search
4311 * terms. Anything with a space is passed to search.
4312 *
4313 * Converts to lowercase any mistakenly uppercased schema (i.e.,
4314 * "Http://" converts to "http://"
4315 *
4316 * @return Original or modified URL
4317 *
4318 */
4319 String smartUrlFilter(String inUrl) {
4320
4321 boolean hasSpace = inUrl.indexOf(' ') != -1;
4322
4323 if (!hasSpace) {
4324 Matcher matcher = ACCEPTED_URI_SCHEMA.matcher(inUrl);
4325 if (matcher.matches()) {
4326 // force scheme to lowercase
4327 String scheme = matcher.group(1);
4328 String lcScheme = scheme.toLowerCase();
4329 if (!lcScheme.equals(scheme)) {
4330 return lcScheme + matcher.group(2);
4331 }
4332 return inUrl;
4333 }
4334 }
4335 if (hasSpace) {
4336 // FIXME: quick search, need to be customized by setting
4337 if (inUrl.length() > 2 && inUrl.charAt(1) == ' ') {
4338 // FIXME: Is this the correct place to add to searches?
4339 // what if someone else calls this function?
4340 char char0 = inUrl.charAt(0);
4341
4342 if (char0 == 'g') {
4343 Browser.addSearchUrl(mResolver, inUrl);
4344 return composeSearchUrl(inUrl.substring(2));
4345
4346 } else if (char0 == 'w') {
4347 Browser.addSearchUrl(mResolver, inUrl);
4348 return URLUtil.composeSearchUrl(inUrl.substring(2),
4349 QuickSearch_W,
4350 QUERY_PLACE_HOLDER);
4351
4352 } else if (char0 == 'd') {
4353 Browser.addSearchUrl(mResolver, inUrl);
4354 return URLUtil.composeSearchUrl(inUrl.substring(2),
4355 QuickSearch_D,
4356 QUERY_PLACE_HOLDER);
4357
4358 } else if (char0 == 'l') {
4359 Browser.addSearchUrl(mResolver, inUrl);
4360 // FIXME: we need location in this case
4361 return URLUtil.composeSearchUrl(inUrl.substring(2),
4362 QuickSearch_L,
4363 QUERY_PLACE_HOLDER);
4364 }
4365 }
4366 } else {
4367 if (Regex.WEB_URL_PATTERN.matcher(inUrl).matches()) {
4368 return URLUtil.guessUrl(inUrl);
4369 }
4370 }
4371
4372 Browser.addSearchUrl(mResolver, inUrl);
4373 return composeSearchUrl(inUrl);
4374 }
4375
4376 /* package */static String composeSearchUrl(String search) {
4377 return URLUtil.composeSearchUrl(search, QuickSearch_G,
4378 QUERY_PLACE_HOLDER);
4379 }
4380
4381 private final static int LOCK_ICON_UNSECURE = 0;
4382 private final static int LOCK_ICON_SECURE = 1;
4383 private final static int LOCK_ICON_MIXED = 2;
4384
4385 private int mLockIconType = LOCK_ICON_UNSECURE;
4386 private int mPrevLockType = LOCK_ICON_UNSECURE;
4387
4388 private WebView mWebView;
4389 private BrowserSettings mSettings;
4390 private TabControl mTabControl;
4391 private ContentResolver mResolver;
4392 private FrameLayout mContentView;
4393 private ImageGrid mTabOverview;
4394
4395 // FIXME, temp address onPrepareMenu performance problem. When we move everything out of
4396 // view, we should rewrite this.
4397 private int mCurrentMenuState = 0;
4398 private int mMenuState = R.id.MAIN_MENU;
4399 private static final int EMPTY_MENU = -1;
4400 private Menu mMenu;
4401
4402 private FindDialog mFindDialog;
4403 // Used to prevent chording to result in firing two shortcuts immediately
4404 // one after another. Fixes bug 1211714.
4405 boolean mCanChord;
4406
4407 private boolean mInLoad;
4408
4409 private boolean mPageStarted;
4410 private boolean mActivityInPause = true;
4411
4412 private boolean mMenuIsDown;
4413
4414 private final KeyTracker mKeyTracker = new KeyTracker(this);
4415
4416 // As trackball doesn't send repeat down, we have to track it ourselves
4417 private boolean mTrackTrackball;
4418
4419 private static boolean mInTrace;
4420
4421 // Performance probe
4422 private static final int[] SYSTEM_CPU_FORMAT = new int[] {
4423 Process.PROC_SPACE_TERM | Process.PROC_COMBINE,
4424 Process.PROC_SPACE_TERM | Process.PROC_OUT_LONG, // 1: user time
4425 Process.PROC_SPACE_TERM | Process.PROC_OUT_LONG, // 2: nice time
4426 Process.PROC_SPACE_TERM | Process.PROC_OUT_LONG, // 3: sys time
4427 Process.PROC_SPACE_TERM | Process.PROC_OUT_LONG, // 4: idle time
4428 Process.PROC_SPACE_TERM | Process.PROC_OUT_LONG, // 5: iowait time
4429 Process.PROC_SPACE_TERM | Process.PROC_OUT_LONG, // 6: irq time
4430 Process.PROC_SPACE_TERM | Process.PROC_OUT_LONG // 7: softirq time
4431 };
4432
4433 private long mStart;
4434 private long mProcessStart;
4435 private long mUserStart;
4436 private long mSystemStart;
4437 private long mIdleStart;
4438 private long mIrqStart;
4439
4440 private long mUiStart;
4441
4442 private Drawable mMixLockIcon;
4443 private Drawable mSecLockIcon;
4444 private Drawable mGenericFavicon;
4445
4446 /* hold a ref so we can auto-cancel if necessary */
4447 private AlertDialog mAlertDialog;
4448
4449 // Wait for credentials before loading google.com
4450 private ProgressDialog mCredsDlg;
4451
4452 // The up-to-date URL and title (these can be different from those stored
4453 // in WebView, since it takes some time for the information in WebView to
4454 // get updated)
4455 private String mUrl;
4456 private String mTitle;
4457
4458 // As PageInfo has different style for landscape / portrait, we have
4459 // to re-open it when configuration changed
4460 private AlertDialog mPageInfoDialog;
4461 private WebView mPageInfoView;
4462 // If the Page-Info dialog is launched from the SSL-certificate-on-error
4463 // dialog, we should not just dismiss it, but should get back to the
4464 // SSL-certificate-on-error dialog. This flag is used to store this state
4465 private Boolean mPageInfoFromShowSSLCertificateOnError;
4466
4467 // as SSLCertificateOnError has different style for landscape / portrait,
4468 // we have to re-open it when configuration changed
4469 private AlertDialog mSSLCertificateOnErrorDialog;
4470 private WebView mSSLCertificateOnErrorView;
4471 private SslErrorHandler mSSLCertificateOnErrorHandler;
4472 private SslError mSSLCertificateOnErrorError;
4473
4474 // as SSLCertificate has different style for landscape / portrait, we
4475 // have to re-open it when configuration changed
4476 private AlertDialog mSSLCertificateDialog;
4477 private WebView mSSLCertificateView;
4478
4479 // as HttpAuthentication has different style for landscape / portrait, we
4480 // have to re-open it when configuration changed
4481 private AlertDialog mHttpAuthenticationDialog;
4482 private HttpAuthHandler mHttpAuthHandler;
4483
4484 /*package*/ static final FrameLayout.LayoutParams COVER_SCREEN_PARAMS =
4485 new FrameLayout.LayoutParams(
4486 ViewGroup.LayoutParams.FILL_PARENT,
4487 ViewGroup.LayoutParams.FILL_PARENT);
4488 // We may provide UI to customize these
4489 // Google search from the browser
4490 final static String QuickSearch_G =
4491 "http://www.google.com/m?client=ms-"
4492 + SystemProperties.get("ro.com.google.clientid", "unknown")
4493 + "&source=android-chrome&q=%s";
4494 // Wikipedia search
4495 final static String QuickSearch_W = "http://en.wikipedia.org/w/index.php?search=%s&go=Go";
4496 // Dictionary search
4497 final static String QuickSearch_D = "http://dictionary.reference.com/search?q=%s";
4498 // Google Mobile Local search
4499 final static String QuickSearch_L = "http://www.google.com/m/search?site=local&q=%s&near=mountain+view";
4500
4501 private final static String QUERY_PLACE_HOLDER = "%s";
4502
4503 private final static String LOGTAG = "browser";
4504
4505 private TabListener mTabListener;
4506
4507 private String mLastEnteredUrl;
4508
4509 private PowerManager.WakeLock mWakeLock;
4510 private final static int WAKELOCK_TIMEOUT = 5 * 60 * 1000; // 5 minutes
4511
4512 private Toast mStopToast;
4513
4514 // Used during animations to prevent other animations from being triggered.
4515 // A count is used since the animation to and from the Window overview can
4516 // overlap. A count of 0 means no animation where a count of > 0 means
4517 // there are animations in progress.
4518 private int mAnimationCount;
4519
4520 // monitor platform changes
4521 private IntentFilter mNetworkStateChangedFilter;
4522 private BroadcastReceiver mNetworkStateIntentReceiver;
4523
4524 // activity requestCode
4525 final static int BOOKMARKS_PAGE = 1;
4526 final static int CLASSIC_HISTORY_PAGE = 2;
4527 final static int DOWNLOAD_PAGE = 3;
4528 final static int PREFERENCES_PAGE = 4;
4529
4530 // the frenquency of checking whether system memory is low
4531 final static int CHECK_MEMORY_INTERVAL = 30000; // 30 seconds
4532}