mobile/android/base/GeckoApp.java

changeset 0
6474c204b198
equal deleted inserted replaced
-1:000000000000 0:fddeaf4649c8
1 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
2 * This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5
6 package org.mozilla.gecko;
7
8 import java.io.BufferedOutputStream;
9 import java.io.ByteArrayOutputStream;
10 import java.io.File;
11 import java.io.IOException;
12 import java.io.InputStream;
13 import java.io.OutputStream;
14 import java.net.HttpURLConnection;
15 import java.net.URL;
16 import java.text.DateFormat;
17 import java.text.SimpleDateFormat;
18 import java.util.ArrayList;
19 import java.util.Arrays;
20 import java.util.Date;
21 import java.util.HashMap;
22 import java.util.Iterator;
23 import java.util.LinkedList;
24 import java.util.List;
25 import java.util.Locale;
26 import java.util.Map;
27 import java.util.Set;
28 import java.util.regex.Matcher;
29 import java.util.regex.Pattern;
30
31 import org.json.JSONArray;
32 import org.json.JSONException;
33 import org.json.JSONObject;
34 import org.mozilla.gecko.GeckoProfileDirectories.NoMozillaDirectoryException;
35 import org.mozilla.gecko.background.announcements.AnnouncementsBroadcastService;
36 import org.mozilla.gecko.db.BrowserDB;
37 import org.mozilla.gecko.favicons.Favicons;
38 import org.mozilla.gecko.gfx.BitmapUtils;
39 import org.mozilla.gecko.gfx.Layer;
40 import org.mozilla.gecko.gfx.LayerView;
41 import org.mozilla.gecko.gfx.PluginLayer;
42 import org.mozilla.gecko.health.HealthRecorder;
43 import org.mozilla.gecko.health.SessionInformation;
44 import org.mozilla.gecko.health.StubbedHealthRecorder;
45 import org.mozilla.gecko.menu.GeckoMenu;
46 import org.mozilla.gecko.menu.GeckoMenuInflater;
47 import org.mozilla.gecko.menu.MenuPanel;
48 import org.mozilla.gecko.mozglue.GeckoLoader;
49 import org.mozilla.gecko.preferences.GeckoPreferences;
50 import org.mozilla.gecko.prompts.PromptService;
51 import org.mozilla.gecko.updater.UpdateService;
52 import org.mozilla.gecko.updater.UpdateServiceHelper;
53 import org.mozilla.gecko.util.ActivityResultHandler;
54 import org.mozilla.gecko.util.FileUtils;
55 import org.mozilla.gecko.util.GeckoEventListener;
56 import org.mozilla.gecko.util.HardwareUtils;
57 import org.mozilla.gecko.util.ThreadUtils;
58 import org.mozilla.gecko.util.UiAsyncTask;
59 import org.mozilla.gecko.webapp.EventListener;
60 import org.mozilla.gecko.webapp.UninstallListener;
61 import org.mozilla.gecko.widget.ButtonToast;
62
63 import android.app.Activity;
64 import android.app.AlertDialog;
65 import android.app.Dialog;
66 import android.content.Context;
67 import android.content.DialogInterface;
68 import android.content.Intent;
69 import android.content.SharedPreferences;
70 import android.content.pm.PackageManager.NameNotFoundException;
71 import android.content.res.Configuration;
72 import android.graphics.Bitmap;
73 import android.graphics.BitmapFactory;
74 import android.graphics.RectF;
75 import android.graphics.drawable.Drawable;
76 import android.hardware.Sensor;
77 import android.hardware.SensorEvent;
78 import android.hardware.SensorEventListener;
79 import android.location.Location;
80 import android.location.LocationListener;
81 import android.net.Uri;
82 import android.net.wifi.ScanResult;
83 import android.net.wifi.WifiManager;
84 import android.os.Build;
85 import android.os.Bundle;
86 import android.os.Handler;
87 import android.os.PowerManager;
88 import android.os.StrictMode;
89 import android.provider.ContactsContract;
90 import android.provider.MediaStore.Images.Media;
91 import android.telephony.CellLocation;
92 import android.telephony.NeighboringCellInfo;
93 import android.telephony.PhoneStateListener;
94 import android.telephony.SignalStrength;
95 import android.telephony.TelephonyManager;
96 import android.telephony.gsm.GsmCellLocation;
97 import android.text.TextUtils;
98 import android.util.AttributeSet;
99 import android.util.Base64;
100 import android.util.Log;
101 import android.util.SparseBooleanArray;
102 import android.view.Gravity;
103 import android.view.KeyEvent;
104 import android.view.Menu;
105 import android.view.MenuInflater;
106 import android.view.MenuItem;
107 import android.view.MotionEvent;
108 import android.view.OrientationEventListener;
109 import android.view.SurfaceHolder;
110 import android.view.SurfaceView;
111 import android.view.TextureView;
112 import android.view.View;
113 import android.view.ViewGroup;
114 import android.view.ViewStub;
115 import android.view.Window;
116 import android.view.WindowManager;
117 import android.widget.AbsoluteLayout;
118 import android.widget.FrameLayout;
119 import android.widget.ListView;
120 import android.widget.RelativeLayout;
121 import android.widget.SimpleAdapter;
122 import android.widget.TextView;
123 import android.widget.Toast;
124
125 public abstract class GeckoApp
126 extends GeckoActivity
127 implements
128 ContextGetter,
129 GeckoAppShell.GeckoInterface,
130 GeckoEventListener,
131 GeckoMenu.Callback,
132 GeckoMenu.MenuPresenter,
133 LocationListener,
134 SensorEventListener,
135 Tabs.OnTabsChangedListener
136 {
137 private static final String LOGTAG = "GeckoApp";
138 private static final int ONE_DAY_MS = 1000*60*60*24;
139
140 private static enum StartupAction {
141 NORMAL, /* normal application start */
142 URL, /* launched with a passed URL */
143 PREFETCH /* launched with a passed URL that we prefetch */
144 }
145
146 public static final String ACTION_ALERT_CALLBACK = "org.mozilla.gecko.ACTION_ALERT_CALLBACK";
147 public static final String ACTION_BOOKMARK = "org.mozilla.gecko.BOOKMARK";
148 public static final String ACTION_DEBUG = "org.mozilla.gecko.DEBUG";
149 public static final String ACTION_LAUNCH_SETTINGS = "org.mozilla.gecko.SETTINGS";
150 public static final String ACTION_LOAD = "org.mozilla.gecko.LOAD";
151 public static final String ACTION_INIT_PW = "org.mozilla.gecko.INIT_PW";
152 public static final String ACTION_WEBAPP_PREFIX = "org.mozilla.gecko.WEBAPP";
153
154 public static final String EXTRA_STATE_BUNDLE = "stateBundle";
155
156 public static final String PREFS_ALLOW_STATE_BUNDLE = "allowStateBundle";
157 public static final String PREFS_OOM_EXCEPTION = "OOMException";
158 public static final String PREFS_VERSION_CODE = "versionCode";
159 public static final String PREFS_WAS_STOPPED = "wasStopped";
160 public static final String PREFS_CRASHED = "crashed";
161 public static final String PREFS_CLEANUP_TEMP_FILES = "cleanupTempFiles";
162
163 public static final String SAVED_STATE_IN_BACKGROUND = "inBackground";
164 public static final String SAVED_STATE_PRIVATE_SESSION = "privateSession";
165
166 static private final String LOCATION_URL = "https://location.services.mozilla.com/v1/submit";
167
168 // Delay before running one-time "cleanup" tasks that may be needed
169 // after a version upgrade.
170 private static final int CLEANUP_DEFERRAL_SECONDS = 15;
171
172 protected RelativeLayout mMainLayout;
173 protected RelativeLayout mGeckoLayout;
174 public View getView() { return mGeckoLayout; }
175 private View mCameraView;
176 private OrientationEventListener mCameraOrientationEventListener;
177 public List<GeckoAppShell.AppStateListener> mAppStateListeners;
178 protected MenuPanel mMenuPanel;
179 protected Menu mMenu;
180 protected GeckoProfile mProfile;
181 protected boolean mIsRestoringActivity;
182
183 private ContactService mContactService;
184 private PromptService mPromptService;
185 private TextSelection mTextSelection;
186
187 protected DoorHangerPopup mDoorHangerPopup;
188 protected FormAssistPopup mFormAssistPopup;
189 protected ButtonToast mToast;
190
191 protected LayerView mLayerView;
192 private AbsoluteLayout mPluginContainer;
193
194 private FullScreenHolder mFullScreenPluginContainer;
195 private View mFullScreenPluginView;
196
197 private HashMap<String, PowerManager.WakeLock> mWakeLocks = new HashMap<String, PowerManager.WakeLock>();
198
199 protected boolean mShouldRestore;
200 protected boolean mInitialized = false;
201 private Telemetry.Timer mJavaUiStartupTimer;
202 private Telemetry.Timer mGeckoReadyStartupTimer;
203
204 private String mPrivateBrowsingSession;
205
206 private volatile HealthRecorder mHealthRecorder = null;
207
208 private int mSignalStrenth;
209 private PhoneStateListener mPhoneStateListener = null;
210 private boolean mShouldReportGeoData;
211 private EventListener mWebappEventListener;
212
213 abstract public int getLayout();
214 abstract public boolean hasTabsSideBar();
215 abstract protected String getDefaultProfileName() throws NoMozillaDirectoryException;
216
217 private static final String RESTARTER_ACTION = "org.mozilla.gecko.restart";
218 private static final String RESTARTER_CLASS = "org.mozilla.gecko.Restarter";
219
220 @SuppressWarnings("serial")
221 class SessionRestoreException extends Exception {
222 public SessionRestoreException(Exception e) {
223 super(e);
224 }
225
226 public SessionRestoreException(String message) {
227 super(message);
228 }
229 }
230
231 void toggleChrome(final boolean aShow) { }
232
233 void focusChrome() { }
234
235 @Override
236 public Context getContext() {
237 return this;
238 }
239
240 @Override
241 public SharedPreferences getSharedPreferences() {
242 return GeckoSharedPrefs.forApp(this);
243 }
244
245 public Activity getActivity() {
246 return this;
247 }
248
249 public LocationListener getLocationListener() {
250 if (mShouldReportGeoData && mPhoneStateListener == null) {
251 mPhoneStateListener = new PhoneStateListener() {
252 public void onSignalStrengthsChanged(SignalStrength signalStrength) {
253 setCurrentSignalStrenth(signalStrength);
254 }
255 };
256 TelephonyManager tm = (TelephonyManager)getSystemService(Context.TELEPHONY_SERVICE);
257 tm.listen(mPhoneStateListener, PhoneStateListener.LISTEN_SIGNAL_STRENGTHS);
258 }
259 return this;
260 }
261
262 public SensorEventListener getSensorEventListener() {
263 return this;
264 }
265
266 public View getCameraView() {
267 return mCameraView;
268 }
269
270 public void addAppStateListener(GeckoAppShell.AppStateListener listener) {
271 mAppStateListeners.add(listener);
272 }
273
274 public void removeAppStateListener(GeckoAppShell.AppStateListener listener) {
275 mAppStateListeners.remove(listener);
276 }
277
278 public FormAssistPopup getFormAssistPopup() {
279 return mFormAssistPopup;
280 }
281
282 @Override
283 public void onTabChanged(Tab tab, Tabs.TabEvents msg, Object data) {
284 // When a tab is closed, it is always unselected first.
285 // When a tab is unselected, another tab is always selected first.
286 switch(msg) {
287 case UNSELECTED:
288 hidePlugins(tab);
289 break;
290
291 case LOCATION_CHANGE:
292 // We only care about location change for the selected tab.
293 if (!Tabs.getInstance().isSelectedTab(tab))
294 break;
295 // Fall through...
296 case SELECTED:
297 invalidateOptionsMenu();
298 if (mFormAssistPopup != null)
299 mFormAssistPopup.hide();
300 break;
301
302 case LOADED:
303 // Sync up the layer view and the tab if the tab is
304 // currently displayed.
305 LayerView layerView = mLayerView;
306 if (layerView != null && Tabs.getInstance().isSelectedTab(tab))
307 layerView.setBackgroundColor(tab.getBackgroundColor());
308 break;
309
310 case DESKTOP_MODE_CHANGE:
311 if (Tabs.getInstance().isSelectedTab(tab))
312 invalidateOptionsMenu();
313 break;
314 }
315 }
316
317 public void refreshChrome() { }
318
319 @Override
320 public void invalidateOptionsMenu() {
321 if (mMenu == null)
322 return;
323
324 onPrepareOptionsMenu(mMenu);
325
326 if (Build.VERSION.SDK_INT >= 11)
327 super.invalidateOptionsMenu();
328 }
329
330 @Override
331 public boolean onCreateOptionsMenu(Menu menu) {
332 mMenu = menu;
333
334 MenuInflater inflater = getMenuInflater();
335 inflater.inflate(R.menu.gecko_app_menu, mMenu);
336 return true;
337 }
338
339 @Override
340 public MenuInflater getMenuInflater() {
341 if (Build.VERSION.SDK_INT >= 11)
342 return new GeckoMenuInflater(this);
343 else
344 return super.getMenuInflater();
345 }
346
347 public MenuPanel getMenuPanel() {
348 if (mMenuPanel == null) {
349 onCreatePanelMenu(Window.FEATURE_OPTIONS_PANEL, null);
350 invalidateOptionsMenu();
351 }
352 return mMenuPanel;
353 }
354
355 @Override
356 public boolean onMenuItemSelected(MenuItem item) {
357 return onOptionsItemSelected(item);
358 }
359
360 @Override
361 public void openMenu() {
362 openOptionsMenu();
363 }
364
365 @Override
366 public void showMenu(final View menu) {
367 // On devices using the custom menu, focus is cleared from the menu when its tapped.
368 // Close and then reshow it to avoid these issues. See bug 794581 and bug 968182.
369 closeMenu();
370
371 // Post the reshow code back to the UI thread to avoid some optimizations Android
372 // has put in place for menus that hide/show themselves quickly. See bug 985400.
373 ThreadUtils.postToUiThread(new Runnable() {
374 @Override
375 public void run() {
376 mMenuPanel.removeAllViews();
377 mMenuPanel.addView(menu);
378 openOptionsMenu();
379 }
380 });
381 }
382
383 @Override
384 public void closeMenu() {
385 closeOptionsMenu();
386 }
387
388 @Override
389 public View onCreatePanelView(int featureId) {
390 if (Build.VERSION.SDK_INT >= 11 && featureId == Window.FEATURE_OPTIONS_PANEL) {
391 if (mMenuPanel == null) {
392 mMenuPanel = new MenuPanel(this, null);
393 } else {
394 // Prepare the panel everytime before showing the menu.
395 onPreparePanel(featureId, mMenuPanel, mMenu);
396 }
397
398 return mMenuPanel;
399 }
400
401 return super.onCreatePanelView(featureId);
402 }
403
404 @Override
405 public boolean onCreatePanelMenu(int featureId, Menu menu) {
406 if (Build.VERSION.SDK_INT >= 11 && featureId == Window.FEATURE_OPTIONS_PANEL) {
407 if (mMenuPanel == null) {
408 mMenuPanel = (MenuPanel) onCreatePanelView(featureId);
409 }
410
411 GeckoMenu gMenu = new GeckoMenu(this, null);
412 gMenu.setCallback(this);
413 gMenu.setMenuPresenter(this);
414 menu = gMenu;
415 mMenuPanel.addView(gMenu);
416
417 return onCreateOptionsMenu(menu);
418 }
419
420 return super.onCreatePanelMenu(featureId, menu);
421 }
422
423 @Override
424 public boolean onPreparePanel(int featureId, View view, Menu menu) {
425 if (Build.VERSION.SDK_INT >= 11 && featureId == Window.FEATURE_OPTIONS_PANEL)
426 return onPrepareOptionsMenu(menu);
427
428 return super.onPreparePanel(featureId, view, menu);
429 }
430
431 @Override
432 public boolean onMenuOpened(int featureId, Menu menu) {
433 // exit full-screen mode whenever the menu is opened
434 if (mLayerView != null && mLayerView.isFullScreen()) {
435 GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("FullScreen:Exit", null));
436 }
437
438 if (Build.VERSION.SDK_INT >= 11 && featureId == Window.FEATURE_OPTIONS_PANEL) {
439 if (mMenu == null) {
440 // getMenuPanel() will force the creation of the menu as well
441 MenuPanel panel = getMenuPanel();
442 onPreparePanel(featureId, panel, mMenu);
443 }
444
445 // Scroll custom menu to the top
446 if (mMenuPanel != null)
447 mMenuPanel.scrollTo(0, 0);
448
449 return true;
450 }
451
452 return super.onMenuOpened(featureId, menu);
453 }
454
455 @Override
456 public boolean onOptionsItemSelected(MenuItem item) {
457 if (item.getItemId() == R.id.quit) {
458 if (GeckoThread.checkAndSetLaunchState(GeckoThread.LaunchState.GeckoRunning, GeckoThread.LaunchState.GeckoExiting)) {
459 GeckoAppShell.notifyGeckoOfEvent(GeckoEvent.createBroadcastEvent("Browser:Quit", null));
460 } else {
461 GeckoAppShell.systemExit();
462 }
463 return true;
464 }
465
466 return super.onOptionsItemSelected(item);
467 }
468
469 @Override
470 public void onOptionsMenuClosed(Menu menu) {
471 if (Build.VERSION.SDK_INT >= 11) {
472 mMenuPanel.removeAllViews();
473 mMenuPanel.addView((GeckoMenu) mMenu);
474 }
475 }
476
477 @Override
478 public boolean onKeyDown(int keyCode, KeyEvent event) {
479 // Handle hardware menu key presses separately so that we can show a custom menu in some cases.
480 if (keyCode == KeyEvent.KEYCODE_MENU) {
481 openOptionsMenu();
482 return true;
483 }
484
485 return super.onKeyDown(keyCode, event);
486 }
487
488 @Override
489 protected void onSaveInstanceState(Bundle outState) {
490 super.onSaveInstanceState(outState);
491
492 if (mToast != null) {
493 mToast.onSaveInstanceState(outState);
494 }
495
496 outState.putBoolean(SAVED_STATE_IN_BACKGROUND, isApplicationInBackground());
497 outState.putString(SAVED_STATE_PRIVATE_SESSION, mPrivateBrowsingSession);
498 }
499
500 void handleFaviconRequest(final String url) {
501 (new UiAsyncTask<Void, Void, String>(ThreadUtils.getBackgroundHandler()) {
502 @Override
503 public String doInBackground(Void... params) {
504 return Favicons.getFaviconURLForPageURL(url);
505 }
506
507 @Override
508 public void onPostExecute(String faviconUrl) {
509 JSONObject args = new JSONObject();
510
511 if (faviconUrl != null) {
512 try {
513 args.put("url", url);
514 args.put("faviconUrl", faviconUrl);
515 } catch (JSONException e) {
516 Log.w(LOGTAG, "Error building JSON favicon arguments.", e);
517 }
518 }
519
520 GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Reader:FaviconReturn", args.toString()));
521 }
522 }).execute();
523 }
524
525 void handleClearHistory() {
526 BrowserDB.clearHistory(getContentResolver());
527 }
528
529 public void addTab() { }
530
531 public void addPrivateTab() { }
532
533 public void showNormalTabs() { }
534
535 public void showPrivateTabs() { }
536
537 public void hideTabs() { }
538
539 /**
540 * Close the tab UI indirectly (not as the result of a direct user
541 * action). This does not force the UI to close; for example in Firefox
542 * tablet mode it will remain open unless the user explicitly closes it.
543 *
544 * @return True if the tab UI was hidden.
545 */
546 public boolean autoHideTabs() { return false; }
547
548 public boolean areTabsShown() { return false; }
549
550 @Override
551 public void handleMessage(String event, JSONObject message) {
552 try {
553 if (event.equals("Toast:Show")) {
554 final String msg = message.getString("message");
555 final JSONObject button = message.optJSONObject("button");
556 if (button != null) {
557 final String label = button.optString("label");
558 final String icon = button.optString("icon");
559 final String id = button.optString("id");
560 showButtonToast(msg, label, icon, id);
561 } else {
562 final String duration = message.getString("duration");
563 showNormalToast(msg, duration);
564 }
565 } else if (event.equals("log")) {
566 // generic log listener
567 final String msg = message.getString("msg");
568 Log.d(LOGTAG, "Log: " + msg);
569 } else if (event.equals("Reader:FaviconRequest")) {
570 final String url = message.getString("url");
571 handleFaviconRequest(url);
572 } else if (event.equals("Gecko:DelayedStartup")) {
573 ThreadUtils.postToBackgroundThread(new UninstallListener.DelayedStartupTask(this));
574 } else if (event.equals("Gecko:Ready")) {
575 mGeckoReadyStartupTimer.stop();
576 geckoConnected();
577
578 // This method is already running on the background thread, so we
579 // know that mHealthRecorder will exist. That doesn't stop us being
580 // paranoid.
581 // This method is cheap, so don't spawn a new runnable.
582 final HealthRecorder rec = mHealthRecorder;
583 if (rec != null) {
584 rec.recordGeckoStartupTime(mGeckoReadyStartupTimer.getElapsed());
585 }
586 } else if (event.equals("ToggleChrome:Hide")) {
587 toggleChrome(false);
588 } else if (event.equals("ToggleChrome:Show")) {
589 toggleChrome(true);
590 } else if (event.equals("ToggleChrome:Focus")) {
591 focusChrome();
592 } else if (event.equals("DOMFullScreen:Start")) {
593 // Local ref to layerView for thread safety
594 LayerView layerView = mLayerView;
595 if (layerView != null) {
596 layerView.setFullScreen(true);
597 }
598 } else if (event.equals("DOMFullScreen:Stop")) {
599 // Local ref to layerView for thread safety
600 LayerView layerView = mLayerView;
601 if (layerView != null) {
602 layerView.setFullScreen(false);
603 }
604 } else if (event.equals("Permissions:Data")) {
605 String host = message.getString("host");
606 JSONArray permissions = message.getJSONArray("permissions");
607 showSiteSettingsDialog(host, permissions);
608 } else if (event.equals("Session:StatePurged")) {
609 onStatePurged();
610 } else if (event.equals("Bookmark:Insert")) {
611 final String url = message.getString("url");
612 final String title = message.getString("title");
613 final Context context = this;
614 ThreadUtils.postToUiThread(new Runnable() {
615 @Override
616 public void run() {
617 Toast.makeText(context, R.string.bookmark_added, Toast.LENGTH_SHORT).show();
618 ThreadUtils.postToBackgroundThread(new Runnable() {
619 @Override
620 public void run() {
621 BrowserDB.addBookmark(getContentResolver(), title, url);
622 }
623 });
624 }
625 });
626 } else if (event.equals("Accessibility:Event")) {
627 GeckoAccessibility.sendAccessibilityEvent(message);
628 } else if (event.equals("Accessibility:Ready")) {
629 GeckoAccessibility.updateAccessibilitySettings(this);
630 } else if (event.equals("Shortcut:Remove")) {
631 final String url = message.getString("url");
632 final String origin = message.getString("origin");
633 final String title = message.getString("title");
634 final String type = message.getString("shortcutType");
635 GeckoAppShell.removeShortcut(title, url, origin, type);
636 } else if (event.equals("Share:Text")) {
637 String text = message.getString("text");
638 GeckoAppShell.openUriExternal(text, "text/plain", "", "", Intent.ACTION_SEND, "");
639
640 // Context: Sharing via chrome list (no explicit session is active)
641 Telemetry.sendUIEvent(TelemetryContract.Event.SHARE, TelemetryContract.Method.LIST);
642 } else if (event.equals("Image:SetAs")) {
643 String src = message.getString("url");
644 setImageAs(src);
645 } else if (event.equals("Sanitize:ClearHistory")) {
646 handleClearHistory();
647 } else if (event.equals("Update:Check")) {
648 startService(new Intent(UpdateServiceHelper.ACTION_CHECK_FOR_UPDATE, null, this, UpdateService.class));
649 } else if (event.equals("Update:Download")) {
650 startService(new Intent(UpdateServiceHelper.ACTION_DOWNLOAD_UPDATE, null, this, UpdateService.class));
651 } else if (event.equals("Update:Install")) {
652 startService(new Intent(UpdateServiceHelper.ACTION_APPLY_UPDATE, null, this, UpdateService.class));
653 } else if (event.equals("PrivateBrowsing:Data")) {
654 // null strings return "null" (http://code.google.com/p/android/issues/detail?id=13830)
655 if (message.isNull("session")) {
656 mPrivateBrowsingSession = null;
657 } else {
658 mPrivateBrowsingSession = message.getString("session");
659 }
660 } else if (event.equals("Contact:Add")) {
661 if (!message.isNull("email")) {
662 Uri contactUri = Uri.parse(message.getString("email"));
663 Intent i = new Intent(ContactsContract.Intents.SHOW_OR_CREATE_CONTACT, contactUri);
664 startActivity(i);
665 } else if (!message.isNull("phone")) {
666 Uri contactUri = Uri.parse(message.getString("phone"));
667 Intent i = new Intent(ContactsContract.Intents.SHOW_OR_CREATE_CONTACT, contactUri);
668 startActivity(i);
669 } else {
670 // something went wrong.
671 Log.e(LOGTAG, "Received Contact:Add message with no email nor phone number");
672 }
673 } else if (event.equals("Intent:GetHandlers")) {
674 Intent intent = GeckoAppShell.getOpenURIIntent((Context) this, message.optString("url"),
675 message.optString("mime"), message.optString("action"), message.optString("title"));
676 String[] handlers = GeckoAppShell.getHandlersForIntent(intent);
677 List<String> appList = Arrays.asList(handlers);
678 JSONObject handlersJSON = new JSONObject();
679 handlersJSON.put("apps", new JSONArray(appList));
680 EventDispatcher.sendResponse(message, handlersJSON);
681 } else if (event.equals("Intent:Open")) {
682 GeckoAppShell.openUriExternal(message.optString("url"),
683 message.optString("mime"), message.optString("packageName"),
684 message.optString("className"), message.optString("action"), message.optString("title"));
685 } else if (event.equals("Intent:OpenForResult")) {
686 Intent intent = GeckoAppShell.getOpenURIIntent(this,
687 message.optString("url"),
688 message.optString("mime"),
689 message.optString("action"),
690 message.optString("title"));
691 intent.setClassName(message.optString("packageName"), message.optString("className"));
692
693 intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
694
695 final JSONObject originalMessage = message;
696 ActivityHandlerHelper.startIntentForActivity(this,
697 intent,
698 new ActivityResultHandler() {
699 @Override
700 public void onActivityResult (int resultCode, Intent data) {
701 JSONObject response = new JSONObject();
702
703 try {
704 if (data != null) {
705 response.put("extras", bundleToJSON(data.getExtras()));
706 }
707 response.put("resultCode", resultCode);
708 } catch (JSONException e) {
709 Log.w(LOGTAG, "Error building JSON response.", e);
710 }
711
712 EventDispatcher.sendResponse(originalMessage, response);
713 }
714 });
715 } else if (event.equals("Locale:Set")) {
716 setLocale(message.getString("locale"));
717 } else if (event.equals("NativeApp:IsDebuggable")) {
718 JSONObject ret = new JSONObject();
719 ret.put("isDebuggable", getIsDebuggable());
720 EventDispatcher.sendResponse(message, ret);
721 } else if (event.equals("SystemUI:Visibility")) {
722 setSystemUiVisible(message.getBoolean("visible"));
723 }
724 } catch (Exception e) {
725 Log.e(LOGTAG, "Exception handling message \"" + event + "\":", e);
726 }
727 }
728
729 void onStatePurged() { }
730
731 /**
732 * @param aPermissions
733 * Array of JSON objects to represent site permissions.
734 * Example: { type: "offline-app", setting: "Store Offline Data", value: "Allow" }
735 */
736 private void showSiteSettingsDialog(String aHost, JSONArray aPermissions) {
737 final AlertDialog.Builder builder = new AlertDialog.Builder(this);
738
739 View customTitleView = getLayoutInflater().inflate(R.layout.site_setting_title, null);
740 ((TextView) customTitleView.findViewById(R.id.title)).setText(R.string.site_settings_title);
741 ((TextView) customTitleView.findViewById(R.id.host)).setText(aHost);
742 builder.setCustomTitle(customTitleView);
743
744 // If there are no permissions to clear, show the user a message about that.
745 // In the future, we want to disable the menu item if there are no permissions to clear.
746 if (aPermissions.length() == 0) {
747 builder.setMessage(R.string.site_settings_no_settings);
748 } else {
749
750 ArrayList <HashMap<String, String>> itemList = new ArrayList <HashMap<String, String>>();
751 for (int i = 0; i < aPermissions.length(); i++) {
752 try {
753 JSONObject permObj = aPermissions.getJSONObject(i);
754 HashMap<String, String> map = new HashMap<String, String>();
755 map.put("setting", permObj.getString("setting"));
756 map.put("value", permObj.getString("value"));
757 itemList.add(map);
758 } catch (JSONException e) {
759 Log.w(LOGTAG, "Exception populating settings items.", e);
760 }
761 }
762
763 // setMultiChoiceItems doesn't support using an adapter, so we're creating a hack with
764 // setSingleChoiceItems and changing the choiceMode below when we create the dialog
765 builder.setSingleChoiceItems(new SimpleAdapter(
766 GeckoApp.this,
767 itemList,
768 R.layout.site_setting_item,
769 new String[] { "setting", "value" },
770 new int[] { R.id.setting, R.id.value }
771 ), -1, new DialogInterface.OnClickListener() {
772 @Override
773 public void onClick(DialogInterface dialog, int id) { }
774 });
775
776 builder.setPositiveButton(R.string.site_settings_clear, new DialogInterface.OnClickListener() {
777 @Override
778 public void onClick(DialogInterface dialog, int id) {
779 ListView listView = ((AlertDialog) dialog).getListView();
780 SparseBooleanArray checkedItemPositions = listView.getCheckedItemPositions();
781
782 // An array of the indices of the permissions we want to clear
783 JSONArray permissionsToClear = new JSONArray();
784 for (int i = 0; i < checkedItemPositions.size(); i++)
785 if (checkedItemPositions.get(i))
786 permissionsToClear.put(i);
787
788 GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent(
789 "Permissions:Clear", permissionsToClear.toString()));
790 }
791 });
792 }
793
794 builder.setNegativeButton(R.string.site_settings_cancel, new DialogInterface.OnClickListener(){
795 @Override
796 public void onClick(DialogInterface dialog, int id) {
797 dialog.cancel();
798 }
799 });
800
801 ThreadUtils.postToUiThread(new Runnable() {
802 @Override
803 public void run() {
804 Dialog dialog = builder.create();
805 dialog.show();
806
807 ListView listView = ((AlertDialog) dialog).getListView();
808 if (listView != null) {
809 listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
810 int listSize = listView.getAdapter().getCount();
811 for (int i = 0; i < listSize; i++)
812 listView.setItemChecked(i, true);
813 }
814 }
815 });
816 }
817
818 public void showToast(final int resId, final int duration) {
819 ThreadUtils.postToUiThread(new Runnable() {
820 @Override
821 public void run() {
822 Toast.makeText(GeckoApp.this, resId, duration).show();
823 }
824 });
825 }
826
827 public void showNormalToast(final String message, final String duration) {
828 ThreadUtils.postToUiThread(new Runnable() {
829 @Override
830 public void run() {
831 Toast toast;
832 if (duration.equals("long")) {
833 toast = Toast.makeText(GeckoApp.this, message, Toast.LENGTH_LONG);
834 } else {
835 toast = Toast.makeText(GeckoApp.this, message, Toast.LENGTH_SHORT);
836 }
837 toast.show();
838 }
839 });
840 }
841
842 protected ButtonToast getButtonToast() {
843 if (mToast != null) {
844 return mToast;
845 }
846
847 ViewStub toastStub = (ViewStub) findViewById(R.id.toast_stub);
848 mToast = new ButtonToast(toastStub.inflate());
849
850 return mToast;
851 }
852
853 void showButtonToast(final String message, final String buttonText,
854 final String buttonIcon, final String buttonId) {
855 BitmapUtils.getDrawable(GeckoApp.this, buttonIcon, new BitmapUtils.BitmapLoader() {
856 @Override
857 public void onBitmapFound(final Drawable d) {
858 getButtonToast().show(false, message, buttonText, d, new ButtonToast.ToastListener() {
859 @Override
860 public void onButtonClicked() {
861 GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Toast:Click", buttonId));
862 }
863
864 @Override
865 public void onToastHidden(ButtonToast.ReasonHidden reason) {
866 if (reason == ButtonToast.ReasonHidden.TIMEOUT) {
867 GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Toast:Hidden", buttonId));
868 }
869 }
870 });
871 }
872 });
873 }
874
875 private JSONObject bundleToJSON(Bundle bundle) {
876 JSONObject json = new JSONObject();
877 if (bundle == null) {
878 return json;
879 }
880
881 for (String key : bundle.keySet()) {
882 try {
883 json.put(key, bundle.get(key));
884 } catch (JSONException e) {
885 Log.w(LOGTAG, "Error building JSON response.", e);
886 }
887 }
888
889 return json;
890 }
891
892 private void addFullScreenPluginView(View view) {
893 if (mFullScreenPluginView != null) {
894 Log.w(LOGTAG, "Already have a fullscreen plugin view");
895 return;
896 }
897
898 setFullScreen(true);
899
900 view.setWillNotDraw(false);
901 if (view instanceof SurfaceView) {
902 ((SurfaceView) view).setZOrderOnTop(true);
903 }
904
905 mFullScreenPluginContainer = new FullScreenHolder(this);
906
907 FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(
908 ViewGroup.LayoutParams.FILL_PARENT,
909 ViewGroup.LayoutParams.FILL_PARENT,
910 Gravity.CENTER);
911 mFullScreenPluginContainer.addView(view, layoutParams);
912
913
914 FrameLayout decor = (FrameLayout)getWindow().getDecorView();
915 decor.addView(mFullScreenPluginContainer, layoutParams);
916
917 mFullScreenPluginView = view;
918 }
919
920 public void addPluginView(final View view, final RectF rect, final boolean isFullScreen) {
921 ThreadUtils.postToUiThread(new Runnable() {
922 @Override
923 public void run() {
924 Tabs tabs = Tabs.getInstance();
925 Tab tab = tabs.getSelectedTab();
926
927 if (isFullScreen) {
928 addFullScreenPluginView(view);
929 return;
930 }
931
932 PluginLayer layer = (PluginLayer) tab.getPluginLayer(view);
933 if (layer == null) {
934 layer = new PluginLayer(view, rect, mLayerView.getRenderer().getMaxTextureSize());
935 tab.addPluginLayer(view, layer);
936 } else {
937 layer.reset(rect);
938 layer.setVisible(true);
939 }
940
941 mLayerView.addLayer(layer);
942 }
943 });
944 }
945
946 private void removeFullScreenPluginView(View view) {
947 if (mFullScreenPluginView == null) {
948 Log.w(LOGTAG, "Don't have a fullscreen plugin view");
949 return;
950 }
951
952 if (mFullScreenPluginView != view) {
953 Log.w(LOGTAG, "Passed view is not the current full screen view");
954 return;
955 }
956
957 mFullScreenPluginContainer.removeView(mFullScreenPluginView);
958
959 // We need do do this on the next iteration in order to avoid
960 // a deadlock, see comment below in FullScreenHolder
961 ThreadUtils.postToUiThread(new Runnable() {
962 @Override
963 public void run() {
964 mLayerView.showSurface();
965 }
966 });
967
968 FrameLayout decor = (FrameLayout)getWindow().getDecorView();
969 decor.removeView(mFullScreenPluginContainer);
970
971 mFullScreenPluginView = null;
972
973 GeckoScreenOrientation.getInstance().unlock();
974 setFullScreen(false);
975 }
976
977 public void removePluginView(final View view, final boolean isFullScreen) {
978 ThreadUtils.postToUiThread(new Runnable() {
979 @Override
980 public void run() {
981 Tabs tabs = Tabs.getInstance();
982 Tab tab = tabs.getSelectedTab();
983
984 if (isFullScreen) {
985 removeFullScreenPluginView(view);
986 return;
987 }
988
989 PluginLayer layer = (PluginLayer) tab.removePluginLayer(view);
990 if (layer != null) {
991 layer.destroy();
992 }
993 }
994 });
995 }
996
997 // This method starts downloading an image synchronously and displays the Chooser activity to set the image as wallpaper.
998 private void setImageAs(final String aSrc) {
999 boolean isDataURI = aSrc.startsWith("data:");
1000 Bitmap image = null;
1001 InputStream is = null;
1002 ByteArrayOutputStream os = null;
1003 try {
1004 if (isDataURI) {
1005 int dataStart = aSrc.indexOf(",");
1006 byte[] buf = Base64.decode(aSrc.substring(dataStart+1), Base64.DEFAULT);
1007 image = BitmapUtils.decodeByteArray(buf);
1008 } else {
1009 int byteRead;
1010 byte[] buf = new byte[4192];
1011 os = new ByteArrayOutputStream();
1012 URL url = new URL(aSrc);
1013 is = url.openStream();
1014
1015 // Cannot read from same stream twice. Also, InputStream from
1016 // URL does not support reset. So converting to byte array.
1017
1018 while((byteRead = is.read(buf)) != -1) {
1019 os.write(buf, 0, byteRead);
1020 }
1021 byte[] imgBuffer = os.toByteArray();
1022 image = BitmapUtils.decodeByteArray(imgBuffer);
1023 }
1024 if (image != null) {
1025 String path = Media.insertImage(getContentResolver(),image, null, null);
1026 final Intent intent = new Intent(Intent.ACTION_ATTACH_DATA);
1027 intent.addCategory(Intent.CATEGORY_DEFAULT);
1028 intent.setData(Uri.parse(path));
1029
1030 // Removes the image from storage once the chooser activity ends.
1031 Intent chooser = Intent.createChooser(intent, getString(R.string.set_image_chooser_title));
1032 ActivityResultHandler handler = new ActivityResultHandler() {
1033 @Override
1034 public void onActivityResult (int resultCode, Intent data) {
1035 getContentResolver().delete(intent.getData(), null, null);
1036 }
1037 };
1038 ActivityHandlerHelper.startIntentForActivity(this, chooser, handler);
1039 } else {
1040 Toast.makeText((Context) this, R.string.set_image_fail, Toast.LENGTH_SHORT).show();
1041 }
1042 } catch(OutOfMemoryError ome) {
1043 Log.e(LOGTAG, "Out of Memory when converting to byte array", ome);
1044 } catch(IOException ioe) {
1045 Log.e(LOGTAG, "I/O Exception while setting wallpaper", ioe);
1046 } finally {
1047 if (is != null) {
1048 try {
1049 is.close();
1050 } catch(IOException ioe) {
1051 Log.w(LOGTAG, "I/O Exception while closing stream", ioe);
1052 }
1053 }
1054 if (os != null) {
1055 try {
1056 os.close();
1057 } catch(IOException ioe) {
1058 Log.w(LOGTAG, "I/O Exception while closing stream", ioe);
1059 }
1060 }
1061 }
1062 }
1063
1064 private int getBitmapSampleSize(BitmapFactory.Options options, int idealWidth, int idealHeight) {
1065 int width = options.outWidth;
1066 int height = options.outHeight;
1067 int inSampleSize = 1;
1068 if (height > idealHeight || width > idealWidth) {
1069 if (width > height) {
1070 inSampleSize = Math.round((float)height / (float)idealHeight);
1071 } else {
1072 inSampleSize = Math.round((float)width / (float)idealWidth);
1073 }
1074 }
1075 return inSampleSize;
1076 }
1077
1078 private void hidePluginLayer(Layer layer) {
1079 LayerView layerView = mLayerView;
1080 layerView.removeLayer(layer);
1081 layerView.requestRender();
1082 }
1083
1084 private void showPluginLayer(Layer layer) {
1085 LayerView layerView = mLayerView;
1086 layerView.addLayer(layer);
1087 layerView.requestRender();
1088 }
1089
1090 public void requestRender() {
1091 mLayerView.requestRender();
1092 }
1093
1094 public void hidePlugins(Tab tab) {
1095 for (Layer layer : tab.getPluginLayers()) {
1096 if (layer instanceof PluginLayer) {
1097 ((PluginLayer) layer).setVisible(false);
1098 }
1099
1100 hidePluginLayer(layer);
1101 }
1102
1103 requestRender();
1104 }
1105
1106 public void showPlugins() {
1107 Tabs tabs = Tabs.getInstance();
1108 Tab tab = tabs.getSelectedTab();
1109
1110 showPlugins(tab);
1111 }
1112
1113 public void showPlugins(Tab tab) {
1114 for (Layer layer : tab.getPluginLayers()) {
1115 showPluginLayer(layer);
1116
1117 if (layer instanceof PluginLayer) {
1118 ((PluginLayer) layer).setVisible(true);
1119 }
1120 }
1121
1122 requestRender();
1123 }
1124
1125 public void setFullScreen(final boolean fullscreen) {
1126 ThreadUtils.postToUiThread(new Runnable() {
1127 @Override
1128 public void run() {
1129 // Hide/show the system notification bar
1130 Window window = getWindow();
1131 window.setFlags(fullscreen ?
1132 WindowManager.LayoutParams.FLAG_FULLSCREEN : 0,
1133 WindowManager.LayoutParams.FLAG_FULLSCREEN);
1134
1135 if (Build.VERSION.SDK_INT >= 11)
1136 window.getDecorView().setSystemUiVisibility(fullscreen ? 1 : 0);
1137 }
1138 });
1139 }
1140
1141 /**
1142 * Check and start the Java profiler if MOZ_PROFILER_STARTUP env var is specified
1143 **/
1144 protected void earlyStartJavaSampler(Intent intent)
1145 {
1146 String env = intent.getStringExtra("env0");
1147 for (int i = 1; env != null; i++) {
1148 if (env.startsWith("MOZ_PROFILER_STARTUP=")) {
1149 if (!env.endsWith("=")) {
1150 GeckoJavaSampler.start(10, 1000);
1151 Log.d(LOGTAG, "Profiling Java on startup");
1152 }
1153 break;
1154 }
1155 env = intent.getStringExtra("env" + i);
1156 }
1157 }
1158
1159 /**
1160 * Called when the activity is first created.
1161 *
1162 * Here we initialize all of our profile settings, Firefox Health Report,
1163 * and other one-shot constructions.
1164 **/
1165 @Override
1166 public void onCreate(Bundle savedInstanceState)
1167 {
1168 GeckoAppShell.registerGlobalExceptionHandler();
1169
1170 // Enable Android Strict Mode for developers' local builds (the "default" channel).
1171 if ("default".equals(AppConstants.MOZ_UPDATE_CHANNEL)) {
1172 enableStrictMode();
1173 }
1174
1175 // The clock starts...now. Better hurry!
1176 mJavaUiStartupTimer = new Telemetry.UptimeTimer("FENNEC_STARTUP_TIME_JAVAUI");
1177 mGeckoReadyStartupTimer = new Telemetry.UptimeTimer("FENNEC_STARTUP_TIME_GECKOREADY");
1178
1179 final Intent intent = getIntent();
1180 final String args = intent.getStringExtra("args");
1181
1182 earlyStartJavaSampler(intent);
1183
1184 // GeckoLoader wants to dig some environment variables out of the
1185 // incoming intent, so pass it in here. GeckoLoader will do its
1186 // business later and dispose of the reference.
1187 GeckoLoader.setLastIntent(intent);
1188
1189 if (mProfile == null) {
1190 String profileName = null;
1191 String profilePath = null;
1192 if (args != null) {
1193 if (args.contains("-P")) {
1194 Pattern p = Pattern.compile("(?:-P\\s*)(\\w*)(\\s*)");
1195 Matcher m = p.matcher(args);
1196 if (m.find()) {
1197 profileName = m.group(1);
1198 }
1199 }
1200
1201 if (args.contains("-profile")) {
1202 Pattern p = Pattern.compile("(?:-profile\\s*)(\\S*)(\\s*)");
1203 Matcher m = p.matcher(args);
1204 if (m.find()) {
1205 profilePath = m.group(1);
1206 }
1207 if (profileName == null) {
1208 try {
1209 profileName = getDefaultProfileName();
1210 } catch (NoMozillaDirectoryException e) {
1211 Log.wtf(LOGTAG, "Unable to fetch default profile name!", e);
1212 // There's nothing at all we can do now. If the Mozilla directory
1213 // didn't exist, then we're screwed.
1214 // Crash here so we can fix the bug.
1215 throw new RuntimeException(e);
1216 }
1217 if (profileName == null)
1218 profileName = GeckoProfile.DEFAULT_PROFILE;
1219 }
1220 GeckoProfile.sIsUsingCustomProfile = true;
1221 }
1222
1223 if (profileName != null || profilePath != null) {
1224 mProfile = GeckoProfile.get(this, profileName, profilePath);
1225 }
1226 }
1227 }
1228
1229 BrowserDB.initialize(getProfile().getName());
1230
1231 // Workaround for <http://code.google.com/p/android/issues/detail?id=20915>.
1232 try {
1233 Class.forName("android.os.AsyncTask");
1234 } catch (ClassNotFoundException e) {}
1235
1236 MemoryMonitor.getInstance().init(getApplicationContext());
1237
1238 // GeckoAppShell is tightly coupled to us, rather than
1239 // the app context, because various parts of Fennec (e.g.,
1240 // GeckoScreenOrientation) use GAS to access the Activity in
1241 // the guise of fetching a Context.
1242 // When that's fixed, `this` can change to
1243 // `(GeckoApplication) getApplication()` here.
1244 GeckoAppShell.setContextGetter(this);
1245 GeckoAppShell.setGeckoInterface(this);
1246
1247 ThreadUtils.setUiThread(Thread.currentThread(), new Handler());
1248
1249 Tabs.getInstance().attachToContext(this);
1250 try {
1251 Favicons.attachToContext(this);
1252 } catch (Exception e) {
1253 Log.e(LOGTAG, "Exception starting favicon cache. Corrupt resources?", e);
1254 }
1255
1256 // Did the OS locale change while we were backgrounded? If so,
1257 // we need to die so that Gecko will re-init add-ons that touch
1258 // the UI.
1259 // This is using a sledgehammer to crack a nut, but it'll do for
1260 // now.
1261 if (BrowserLocaleManager.getInstance().systemLocaleDidChange()) {
1262 Log.i(LOGTAG, "System locale changed. Restarting.");
1263 doRestart();
1264 GeckoAppShell.systemExit();
1265 return;
1266 }
1267
1268 if (GeckoThread.isCreated()) {
1269 // This happens when the GeckoApp activity is destroyed by Android
1270 // without killing the entire application (see Bug 769269).
1271 mIsRestoringActivity = true;
1272 Telemetry.HistogramAdd("FENNEC_RESTORING_ACTIVITY", 1);
1273 }
1274
1275 // Fix for Bug 830557 on Tegra boards running Froyo.
1276 // This fix must be done before doing layout.
1277 // Assume the bug is fixed in Gingerbread and up.
1278 if (Build.VERSION.SDK_INT < 9) {
1279 try {
1280 Class<?> inputBindResultClass =
1281 Class.forName("com.android.internal.view.InputBindResult");
1282 java.lang.reflect.Field creatorField =
1283 inputBindResultClass.getField("CREATOR");
1284 Log.i(LOGTAG, "froyo startup fix: " + String.valueOf(creatorField.get(null)));
1285 } catch (Exception e) {
1286 Log.w(LOGTAG, "froyo startup fix failed", e);
1287 }
1288 }
1289
1290 Bundle stateBundle = getIntent().getBundleExtra(EXTRA_STATE_BUNDLE);
1291 if (stateBundle != null) {
1292 // Use the state bundle if it was given as an intent extra. This is
1293 // only intended to be used internally via Robocop, so a boolean
1294 // is read from a private shared pref to prevent other apps from
1295 // injecting states.
1296 final SharedPreferences prefs = getSharedPreferences();
1297 if (prefs.getBoolean(PREFS_ALLOW_STATE_BUNDLE, false)) {
1298 Log.i(LOGTAG, "Restoring state from intent bundle");
1299 prefs.edit().remove(PREFS_ALLOW_STATE_BUNDLE).commit();
1300 savedInstanceState = stateBundle;
1301 }
1302 } else if (savedInstanceState != null) {
1303 // Bug 896992 - This intent has already been handled; reset the intent.
1304 setIntent(new Intent(Intent.ACTION_MAIN));
1305 }
1306
1307 super.onCreate(savedInstanceState);
1308
1309 GeckoScreenOrientation.getInstance().update(getResources().getConfiguration().orientation);
1310
1311 setContentView(getLayout());
1312
1313 // Set up Gecko layout.
1314 mGeckoLayout = (RelativeLayout) findViewById(R.id.gecko_layout);
1315 mMainLayout = (RelativeLayout) findViewById(R.id.main_layout);
1316
1317 // Determine whether we should restore tabs.
1318 mShouldRestore = getSessionRestoreState(savedInstanceState);
1319 if (mShouldRestore && savedInstanceState != null) {
1320 boolean wasInBackground =
1321 savedInstanceState.getBoolean(SAVED_STATE_IN_BACKGROUND, false);
1322
1323 // Don't log OOM-kills if only one activity was destroyed. (For example
1324 // from "Don't keep activities" on ICS)
1325 if (!wasInBackground && !mIsRestoringActivity) {
1326 Telemetry.HistogramAdd("FENNEC_WAS_KILLED", 1);
1327 }
1328
1329 mPrivateBrowsingSession = savedInstanceState.getString(SAVED_STATE_PRIVATE_SESSION);
1330 }
1331
1332 // Perform background initialization.
1333 ThreadUtils.postToBackgroundThread(new Runnable() {
1334 @Override
1335 public void run() {
1336 final SharedPreferences prefs = GeckoApp.this.getSharedPreferences();
1337
1338 // Wait until now to set this, because we'd rather throw an exception than
1339 // have a caller of BrowserLocaleManager regress startup.
1340 BrowserLocaleManager.getInstance().initialize(getApplicationContext());
1341
1342 SessionInformation previousSession = SessionInformation.fromSharedPrefs(prefs);
1343 if (previousSession.wasKilled()) {
1344 Telemetry.HistogramAdd("FENNEC_WAS_KILLED", 1);
1345 }
1346
1347 SharedPreferences.Editor editor = prefs.edit();
1348 editor.putBoolean(GeckoApp.PREFS_OOM_EXCEPTION, false);
1349
1350 // Put a flag to check if we got a normal `onSaveInstanceState`
1351 // on exit, or if we were suddenly killed (crash or native OOM).
1352 editor.putBoolean(GeckoApp.PREFS_WAS_STOPPED, false);
1353
1354 editor.commit();
1355
1356 // The lifecycle of mHealthRecorder is "shortly after onCreate"
1357 // through "onDestroy" -- essentially the same as the lifecycle
1358 // of the activity itself.
1359 final String profilePath = getProfile().getDir().getAbsolutePath();
1360 final EventDispatcher dispatcher = GeckoAppShell.getEventDispatcher();
1361 Log.i(LOGTAG, "Creating HealthRecorder.");
1362
1363 final String osLocale = Locale.getDefault().toString();
1364 String appLocale = BrowserLocaleManager.getInstance().getAndApplyPersistedLocale(GeckoApp.this);
1365 Log.d(LOGTAG, "OS locale is " + osLocale + ", app locale is " + appLocale);
1366
1367 if (appLocale == null) {
1368 appLocale = osLocale;
1369 }
1370
1371 mHealthRecorder = GeckoApp.this.createHealthRecorder(GeckoApp.this,
1372 profilePath,
1373 dispatcher,
1374 osLocale,
1375 appLocale,
1376 previousSession);
1377
1378 final String uiLocale = appLocale;
1379 ThreadUtils.postToUiThread(new Runnable() {
1380 @Override
1381 public void run() {
1382 GeckoApp.this.onLocaleReady(uiLocale);
1383 }
1384 });
1385 }
1386 });
1387
1388 GeckoAppShell.setNotificationClient(makeNotificationClient());
1389 NotificationHelper.init(getApplicationContext());
1390 }
1391
1392 /**
1393 * At this point, the resource system and the rest of the browser are
1394 * aware of the locale.
1395 *
1396 * Now we can display strings!
1397 *
1398 * You can think of this as being something like a second phase of onCreate,
1399 * where you can do string-related operations. Use this in place of embedding
1400 * strings in view XML.
1401 *
1402 * By contrast, onConfigurationChanged does some locale operations, but is in
1403 * response to device changes.
1404 */
1405 @Override
1406 public void onLocaleReady(final String locale) {
1407 if (!ThreadUtils.isOnUiThread()) {
1408 throw new RuntimeException("onLocaleReady must always be called from the UI thread.");
1409 }
1410
1411 // The URL bar hint needs to be populated.
1412 TextView urlBar = (TextView) findViewById(R.id.url_bar_title);
1413 if (urlBar != null) {
1414 final String hint = getResources().getString(R.string.url_bar_default_text);
1415 urlBar.setHint(hint);
1416 } else {
1417 Log.d(LOGTAG, "No URL bar in GeckoApp. Not loading localized hint string.");
1418 }
1419
1420 // Allow onConfigurationChanged to take care of the rest.
1421 onConfigurationChanged(getResources().getConfiguration());
1422 }
1423
1424 protected void initializeChrome() {
1425 mDoorHangerPopup = new DoorHangerPopup(this);
1426 mPluginContainer = (AbsoluteLayout) findViewById(R.id.plugin_container);
1427 mFormAssistPopup = (FormAssistPopup) findViewById(R.id.form_assist_popup);
1428
1429 if (mCameraView == null) {
1430 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
1431 mCameraView = new SurfaceView(this);
1432 ((SurfaceView)mCameraView).getHolder().setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
1433 } else {
1434 mCameraView = new TextureView(this);
1435 }
1436 }
1437
1438 if (mLayerView == null) {
1439 LayerView layerView = (LayerView) findViewById(R.id.layer_view);
1440 layerView.initializeView(GeckoAppShell.getEventDispatcher());
1441 mLayerView = layerView;
1442 GeckoAppShell.setLayerView(layerView);
1443 // bind the GeckoEditable instance to the new LayerView
1444 GeckoAppShell.notifyIMEContext(GeckoEditableListener.IME_STATE_DISABLED, "", "", "");
1445 }
1446 }
1447
1448 /**
1449 * Loads the initial tab at Fennec startup.
1450 *
1451 * If Fennec was opened with an external URL, that URL will be loaded.
1452 * Otherwise, unless there was a session restore, the default URL
1453 * (about:home) be loaded.
1454 *
1455 * @param url External URL to load, or null to load the default URL
1456 */
1457 protected void loadStartupTab(String url) {
1458 if (url == null) {
1459 if (!mShouldRestore) {
1460 // Show about:home if we aren't restoring previous session and
1461 // there's no external URL.
1462 Tabs.getInstance().loadUrl(AboutPages.HOME, Tabs.LOADURL_NEW_TAB);
1463 }
1464 } else {
1465 // If given an external URL, load it
1466 int flags = Tabs.LOADURL_NEW_TAB | Tabs.LOADURL_USER_ENTERED | Tabs.LOADURL_EXTERNAL;
1467 Tabs.getInstance().loadUrl(url, flags);
1468 }
1469 }
1470
1471 private void initialize() {
1472 mInitialized = true;
1473
1474 Intent intent = getIntent();
1475 String action = intent.getAction();
1476
1477 String passedUri = null;
1478 final String uri = getURIFromIntent(intent);
1479 if (!TextUtils.isEmpty(uri)) {
1480 passedUri = uri;
1481 }
1482
1483 final boolean isExternalURL = passedUri != null &&
1484 !AboutPages.isAboutHome(passedUri);
1485 StartupAction startupAction;
1486 if (isExternalURL) {
1487 startupAction = StartupAction.URL;
1488 } else {
1489 startupAction = StartupAction.NORMAL;
1490 }
1491
1492 // Start migrating as early as possible, can do this in
1493 // parallel with Gecko load.
1494 checkMigrateProfile();
1495
1496 Uri data = intent.getData();
1497 if (data != null && "http".equals(data.getScheme())) {
1498 startupAction = StartupAction.PREFETCH;
1499 ThreadUtils.postToBackgroundThread(new PrefetchRunnable(data.toString()));
1500 }
1501
1502 Tabs.registerOnTabsChangedListener(this);
1503
1504 initializeChrome();
1505
1506 // If we are doing a restore, read the session data and send it to Gecko
1507 if (!mIsRestoringActivity) {
1508 String restoreMessage = null;
1509 if (mShouldRestore) {
1510 try {
1511 // restoreSessionTabs() will create simple tab stubs with the
1512 // URL and title for each page, but we also need to restore
1513 // session history. restoreSessionTabs() will inject the IDs
1514 // of the tab stubs into the JSON data (which holds the session
1515 // history). This JSON data is then sent to Gecko so session
1516 // history can be restored for each tab.
1517 restoreMessage = restoreSessionTabs(isExternalURL);
1518 } catch (SessionRestoreException e) {
1519 // If restore failed, do a normal startup
1520 Log.e(LOGTAG, "An error occurred during restore", e);
1521 mShouldRestore = false;
1522 }
1523 }
1524
1525 GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Session:Restore", restoreMessage));
1526 }
1527
1528 // External URLs should always be loaded regardless of whether Gecko is
1529 // already running.
1530 if (isExternalURL) {
1531 loadStartupTab(passedUri);
1532 } else if (!mIsRestoringActivity) {
1533 loadStartupTab(null);
1534 }
1535
1536 // We now have tab stubs from the last session. Any future tabs should
1537 // be animated.
1538 Tabs.getInstance().notifyListeners(null, Tabs.TabEvents.RESTORED);
1539
1540 // If we're not restoring, move the session file so it can be read for
1541 // the last tabs section.
1542 if (!mShouldRestore) {
1543 getProfile().moveSessionFile();
1544 }
1545
1546 Telemetry.HistogramAdd("FENNEC_STARTUP_GECKOAPP_ACTION", startupAction.ordinal());
1547
1548 if (!mIsRestoringActivity) {
1549 GeckoThread.setArgs(intent.getStringExtra("args"));
1550 GeckoThread.setAction(intent.getAction());
1551 GeckoThread.setUri(passedUri);
1552 }
1553 if (!ACTION_DEBUG.equals(action) &&
1554 GeckoThread.checkAndSetLaunchState(GeckoThread.LaunchState.Launching, GeckoThread.LaunchState.Launched)) {
1555 GeckoThread.createAndStart();
1556 } else if (ACTION_DEBUG.equals(action) &&
1557 GeckoThread.checkAndSetLaunchState(GeckoThread.LaunchState.Launching, GeckoThread.LaunchState.WaitForDebugger)) {
1558 ThreadUtils.getUiHandler().postDelayed(new Runnable() {
1559 @Override
1560 public void run() {
1561 GeckoThread.setLaunchState(GeckoThread.LaunchState.Launching);
1562 GeckoThread.createAndStart();
1563 }
1564 }, 1000 * 5 /* 5 seconds */);
1565 }
1566
1567 // Check if launched from data reporting notification.
1568 if (ACTION_LAUNCH_SETTINGS.equals(action)) {
1569 Intent settingsIntent = new Intent(GeckoApp.this, GeckoPreferences.class);
1570 // Copy extras.
1571 settingsIntent.putExtras(intent);
1572 startActivity(settingsIntent);
1573 }
1574
1575 //app state callbacks
1576 mAppStateListeners = new LinkedList<GeckoAppShell.AppStateListener>();
1577
1578 //register for events
1579 registerEventListener("log");
1580 registerEventListener("Reader:ListStatusRequest");
1581 registerEventListener("Reader:Added");
1582 registerEventListener("Reader:Removed");
1583 registerEventListener("Reader:Share");
1584 registerEventListener("Reader:FaviconRequest");
1585 registerEventListener("onCameraCapture");
1586 registerEventListener("Gecko:Ready");
1587 registerEventListener("Gecko:DelayedStartup");
1588 registerEventListener("Toast:Show");
1589 registerEventListener("DOMFullScreen:Start");
1590 registerEventListener("DOMFullScreen:Stop");
1591 registerEventListener("ToggleChrome:Hide");
1592 registerEventListener("ToggleChrome:Show");
1593 registerEventListener("ToggleChrome:Focus");
1594 registerEventListener("Permissions:Data");
1595 registerEventListener("Session:StatePurged");
1596 registerEventListener("Bookmark:Insert");
1597 registerEventListener("Accessibility:Event");
1598 registerEventListener("Accessibility:Ready");
1599 registerEventListener("Shortcut:Remove");
1600 registerEventListener("Share:Text");
1601 registerEventListener("Image:SetAs");
1602 registerEventListener("Sanitize:ClearHistory");
1603 registerEventListener("Update:Check");
1604 registerEventListener("Update:Download");
1605 registerEventListener("Update:Install");
1606 registerEventListener("PrivateBrowsing:Data");
1607 registerEventListener("Contact:Add");
1608 registerEventListener("Intent:Open");
1609 registerEventListener("Intent:OpenForResult");
1610 registerEventListener("Intent:GetHandlers");
1611 registerEventListener("Locale:Set");
1612 registerEventListener("NativeApp:IsDebuggable");
1613 registerEventListener("SystemUI:Visibility");
1614
1615 if (mWebappEventListener == null) {
1616 mWebappEventListener = new EventListener();
1617 mWebappEventListener.registerEvents();
1618 }
1619
1620 if (SmsManager.getInstance() != null) {
1621 SmsManager.getInstance().start();
1622 }
1623
1624 mContactService = new ContactService(GeckoAppShell.getEventDispatcher(), this);
1625
1626 mPromptService = new PromptService(this);
1627
1628 mTextSelection = new TextSelection((TextSelectionHandle) findViewById(R.id.start_handle),
1629 (TextSelectionHandle) findViewById(R.id.middle_handle),
1630 (TextSelectionHandle) findViewById(R.id.end_handle),
1631 GeckoAppShell.getEventDispatcher(),
1632 this);
1633
1634 PrefsHelper.getPref("app.update.autodownload", new PrefsHelper.PrefHandlerBase() {
1635 @Override public void prefValue(String pref, String value) {
1636 UpdateServiceHelper.registerForUpdates(GeckoApp.this, value);
1637 }
1638 });
1639
1640 PrefsHelper.getPref("app.geo.reportdata", new PrefsHelper.PrefHandlerBase() {
1641 @Override public void prefValue(String pref, int value) {
1642 if (value == 1)
1643 mShouldReportGeoData = true;
1644 else
1645 mShouldReportGeoData = false;
1646 }
1647 });
1648
1649 // Trigger the completion of the telemetry timer that wraps activity startup,
1650 // then grab the duration to give to FHR.
1651 mJavaUiStartupTimer.stop();
1652 final long javaDuration = mJavaUiStartupTimer.getElapsed();
1653
1654 ThreadUtils.getBackgroundHandler().postDelayed(new Runnable() {
1655 @Override
1656 public void run() {
1657 final HealthRecorder rec = mHealthRecorder;
1658 if (rec != null) {
1659 rec.recordJavaStartupTime(javaDuration);
1660 }
1661
1662 // Record our launch time for the announcements service
1663 // to use in assessing inactivity.
1664 final Context context = GeckoApp.this;
1665 AnnouncementsBroadcastService.recordLastLaunch(context);
1666
1667 // Kick off our background services. We do this by invoking the broadcast
1668 // receiver, which uses the system alarm infrastructure to perform tasks at
1669 // intervals.
1670 GeckoPreferences.broadcastAnnouncementsPref(context);
1671 GeckoPreferences.broadcastHealthReportUploadPref(context);
1672 if (!GeckoThread.checkLaunchState(GeckoThread.LaunchState.Launched)) {
1673 return;
1674 }
1675 }
1676 }, 50);
1677
1678 if (mIsRestoringActivity) {
1679 GeckoThread.setLaunchState(GeckoThread.LaunchState.GeckoRunning);
1680 Tab selectedTab = Tabs.getInstance().getSelectedTab();
1681 if (selectedTab != null)
1682 Tabs.getInstance().notifyListeners(selectedTab, Tabs.TabEvents.SELECTED);
1683 geckoConnected();
1684 GeckoAppShell.setLayerClient(mLayerView.getLayerClientObject());
1685 GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Viewport:Flush", null));
1686 }
1687
1688 if (ACTION_ALERT_CALLBACK.equals(action)) {
1689 processAlertCallback(intent);
1690 }
1691 }
1692
1693 private String restoreSessionTabs(final boolean isExternalURL) throws SessionRestoreException {
1694 try {
1695 String sessionString = getProfile().readSessionFile(false);
1696 if (sessionString == null) {
1697 throw new SessionRestoreException("Could not read from session file");
1698 }
1699
1700 // If we are doing an OOM restore, parse the session data and
1701 // stub the restored tabs immediately. This allows the UI to be
1702 // updated before Gecko has restored.
1703 if (mShouldRestore) {
1704 final JSONArray tabs = new JSONArray();
1705 SessionParser parser = new SessionParser() {
1706 @Override
1707 public void onTabRead(SessionTab sessionTab) {
1708 JSONObject tabObject = sessionTab.getTabObject();
1709
1710 int flags = Tabs.LOADURL_NEW_TAB;
1711 flags |= ((isExternalURL || !sessionTab.isSelected()) ? Tabs.LOADURL_DELAY_LOAD : 0);
1712 flags |= (tabObject.optBoolean("desktopMode") ? Tabs.LOADURL_DESKTOP : 0);
1713 flags |= (tabObject.optBoolean("isPrivate") ? Tabs.LOADURL_PRIVATE : 0);
1714
1715 Tab tab = Tabs.getInstance().loadUrl(sessionTab.getUrl(), flags);
1716 tab.updateTitle(sessionTab.getTitle());
1717
1718 try {
1719 tabObject.put("tabId", tab.getId());
1720 } catch (JSONException e) {
1721 Log.e(LOGTAG, "JSON error", e);
1722 }
1723 tabs.put(tabObject);
1724 }
1725 };
1726
1727 if (mPrivateBrowsingSession == null) {
1728 parser.parse(sessionString);
1729 } else {
1730 parser.parse(sessionString, mPrivateBrowsingSession);
1731 }
1732
1733 if (tabs.length() > 0) {
1734 sessionString = new JSONObject().put("windows", new JSONArray().put(new JSONObject().put("tabs", tabs))).toString();
1735 } else {
1736 throw new SessionRestoreException("No tabs could be read from session file");
1737 }
1738 }
1739
1740 JSONObject restoreData = new JSONObject();
1741 restoreData.put("sessionString", sessionString);
1742 return restoreData.toString();
1743
1744 } catch (JSONException e) {
1745 throw new SessionRestoreException(e);
1746 }
1747 }
1748
1749 public GeckoProfile getProfile() {
1750 // fall back to default profile if we didn't load a specific one
1751 if (mProfile == null) {
1752 mProfile = GeckoProfile.get(this);
1753 }
1754 return mProfile;
1755 }
1756
1757 /**
1758 * Determine whether the session should be restored.
1759 *
1760 * @param savedInstanceState Saved instance state given to the activity
1761 * @return Whether to restore
1762 */
1763 protected boolean getSessionRestoreState(Bundle savedInstanceState) {
1764 final SharedPreferences prefs = getSharedPreferences();
1765 boolean shouldRestore = false;
1766
1767 final int versionCode = getVersionCode();
1768 if (prefs.getInt(PREFS_VERSION_CODE, 0) != versionCode) {
1769 // If the version has changed, the user has done an upgrade, so restore
1770 // previous tabs.
1771 ThreadUtils.postToBackgroundThread(new Runnable() {
1772 @Override
1773 public void run() {
1774 prefs.edit()
1775 .putInt(PREFS_VERSION_CODE, versionCode)
1776 .commit();
1777 }
1778 });
1779
1780 shouldRestore = true;
1781 } else if (savedInstanceState != null ||
1782 getSessionRestorePreference().equals("always") ||
1783 getRestartFromIntent()) {
1784 // We're coming back from a background kill by the OS, the user
1785 // has chosen to always restore, or we restarted.
1786 shouldRestore = true;
1787 } else if (prefs.getBoolean(GeckoApp.PREFS_CRASHED, false)) {
1788 ThreadUtils.postToBackgroundThread(new Runnable() {
1789 @Override
1790 public void run() {
1791 prefs.edit().putBoolean(PREFS_CRASHED, false).commit();
1792 }
1793 });
1794 shouldRestore = true;
1795 }
1796
1797 return shouldRestore;
1798 }
1799
1800 private String getSessionRestorePreference() {
1801 return getSharedPreferences().getString(GeckoPreferences.PREFS_RESTORE_SESSION, "quit");
1802 }
1803
1804 private boolean getRestartFromIntent() {
1805 return getIntent().getBooleanExtra("didRestart", false);
1806 }
1807
1808 /**
1809 * Enable Android StrictMode checks (for supported OS versions).
1810 * http://developer.android.com/reference/android/os/StrictMode.html
1811 */
1812 private void enableStrictMode() {
1813 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.GINGERBREAD) {
1814 return;
1815 }
1816
1817 Log.d(LOGTAG, "Enabling Android StrictMode");
1818
1819 StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
1820 .detectAll()
1821 .penaltyLog()
1822 .build());
1823
1824 StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
1825 .detectAll()
1826 .penaltyLog()
1827 .build());
1828 }
1829
1830 public void enableCameraView() {
1831 // Start listening for orientation events
1832 mCameraOrientationEventListener = new OrientationEventListener(this) {
1833 @Override
1834 public void onOrientationChanged(int orientation) {
1835 if (mAppStateListeners != null) {
1836 for (GeckoAppShell.AppStateListener listener: mAppStateListeners) {
1837 listener.onOrientationChanged();
1838 }
1839 }
1840 }
1841 };
1842 mCameraOrientationEventListener.enable();
1843
1844 // Try to make it fully transparent.
1845 if (mCameraView instanceof SurfaceView) {
1846 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
1847 mCameraView.setAlpha(0.0f);
1848 }
1849 } else if (mCameraView instanceof TextureView) {
1850 mCameraView.setAlpha(0.0f);
1851 }
1852 ViewGroup mCameraLayout = (ViewGroup) findViewById(R.id.camera_layout);
1853 // Some phones (eg. nexus S) need at least a 8x16 preview size
1854 mCameraLayout.addView(mCameraView,
1855 new AbsoluteLayout.LayoutParams(8, 16, 0, 0));
1856 }
1857
1858 public void disableCameraView() {
1859 if (mCameraOrientationEventListener != null) {
1860 mCameraOrientationEventListener.disable();
1861 mCameraOrientationEventListener = null;
1862 }
1863 ViewGroup mCameraLayout = (ViewGroup) findViewById(R.id.camera_layout);
1864 mCameraLayout.removeView(mCameraView);
1865 }
1866
1867 public String getDefaultUAString() {
1868 return HardwareUtils.isTablet() ? AppConstants.USER_AGENT_FENNEC_TABLET :
1869 AppConstants.USER_AGENT_FENNEC_MOBILE;
1870 }
1871
1872 public String getUAStringForHost(String host) {
1873 // With our standard UA String, we get a 200 response code and
1874 // client-side redirect from t.co. This bot-like UA gives us a
1875 // 301 response code
1876 if ("t.co".equals(host)) {
1877 return AppConstants.USER_AGENT_BOT_LIKE;
1878 }
1879 return getDefaultUAString();
1880 }
1881
1882 class PrefetchRunnable implements Runnable {
1883 private String mPrefetchUrl;
1884
1885 PrefetchRunnable(String prefetchUrl) {
1886 mPrefetchUrl = prefetchUrl;
1887 }
1888
1889 @Override
1890 public void run() {
1891 HttpURLConnection connection = null;
1892 try {
1893 URL url = new URL(mPrefetchUrl);
1894 // data url should have an http scheme
1895 connection = (HttpURLConnection) url.openConnection();
1896 connection.setRequestProperty("User-Agent", getUAStringForHost(url.getHost()));
1897 connection.setInstanceFollowRedirects(false);
1898 connection.setRequestMethod("GET");
1899 connection.connect();
1900 } catch (Exception e) {
1901 Log.e(LOGTAG, "Exception prefetching URL", e);
1902 } finally {
1903 if (connection != null)
1904 connection.disconnect();
1905 }
1906 }
1907 }
1908
1909 private void processAlertCallback(Intent intent) {
1910 String alertName = "";
1911 String alertCookie = "";
1912 Uri data = intent.getData();
1913 if (data != null) {
1914 alertName = data.getQueryParameter("name");
1915 if (alertName == null)
1916 alertName = "";
1917 alertCookie = data.getQueryParameter("cookie");
1918 if (alertCookie == null)
1919 alertCookie = "";
1920 }
1921 handleNotification(ACTION_ALERT_CALLBACK, alertName, alertCookie);
1922 }
1923
1924 @Override
1925 protected void onNewIntent(Intent intent) {
1926 if (GeckoThread.checkLaunchState(GeckoThread.LaunchState.GeckoExiting)) {
1927 // We're exiting and shouldn't try to do anything else. In the case
1928 // where we are hung while exiting, we should force the process to exit.
1929 GeckoAppShell.systemExit();
1930 return;
1931 }
1932
1933 // if we were previously OOM killed, we can end up here when launching
1934 // from external shortcuts, so set this as the intent for initialization
1935 if (!mInitialized) {
1936 setIntent(intent);
1937 return;
1938 }
1939
1940 final String action = intent.getAction();
1941
1942 if (ACTION_LOAD.equals(action)) {
1943 String uri = intent.getDataString();
1944 Tabs.getInstance().loadUrl(uri);
1945 } else if (Intent.ACTION_VIEW.equals(action)) {
1946 String uri = intent.getDataString();
1947 Tabs.getInstance().loadUrl(uri, Tabs.LOADURL_NEW_TAB |
1948 Tabs.LOADURL_USER_ENTERED |
1949 Tabs.LOADURL_EXTERNAL);
1950 } else if (action != null && action.startsWith(ACTION_WEBAPP_PREFIX)) {
1951 // A lightweight mechanism for loading a web page as a webapp
1952 // without installing the app natively nor registering it in the DOM
1953 // application registry.
1954 String uri = getURIFromIntent(intent);
1955 GeckoAppShell.sendEventToGecko(GeckoEvent.createWebappLoadEvent(uri));
1956 } else if (ACTION_BOOKMARK.equals(action)) {
1957 String uri = getURIFromIntent(intent);
1958 GeckoAppShell.sendEventToGecko(GeckoEvent.createBookmarkLoadEvent(uri));
1959 } else if (Intent.ACTION_SEARCH.equals(action)) {
1960 String uri = getURIFromIntent(intent);
1961 GeckoAppShell.sendEventToGecko(GeckoEvent.createURILoadEvent(uri));
1962 } else if (ACTION_ALERT_CALLBACK.equals(action)) {
1963 processAlertCallback(intent);
1964 } else if (ACTION_LAUNCH_SETTINGS.equals(action)) {
1965 // Check if launched from data reporting notification.
1966 Intent settingsIntent = new Intent(GeckoApp.this, GeckoPreferences.class);
1967 // Copy extras.
1968 settingsIntent.putExtras(intent);
1969 startActivity(settingsIntent);
1970 }
1971 }
1972
1973 /*
1974 * Handles getting a uri from and intent in a way that is backwards
1975 * compatable with our previous implementations
1976 */
1977 protected String getURIFromIntent(Intent intent) {
1978 final String action = intent.getAction();
1979 if (ACTION_ALERT_CALLBACK.equals(action))
1980 return null;
1981
1982 String uri = intent.getDataString();
1983 if (uri != null)
1984 return uri;
1985
1986 if ((action != null && action.startsWith(ACTION_WEBAPP_PREFIX)) || ACTION_BOOKMARK.equals(action)) {
1987 uri = intent.getStringExtra("args");
1988 if (uri != null && uri.startsWith("--url=")) {
1989 uri.replace("--url=", "");
1990 }
1991 }
1992 return uri;
1993 }
1994
1995 protected int getOrientation() {
1996 return GeckoScreenOrientation.getInstance().getAndroidOrientation();
1997 }
1998
1999 @Override
2000 public void onResume()
2001 {
2002 // After an onPause, the activity is back in the foreground.
2003 // Undo whatever we did in onPause.
2004 super.onResume();
2005
2006 int newOrientation = getResources().getConfiguration().orientation;
2007 if (GeckoScreenOrientation.getInstance().update(newOrientation)) {
2008 refreshChrome();
2009 }
2010
2011 // User may have enabled/disabled accessibility.
2012 GeckoAccessibility.updateAccessibilitySettings(this);
2013
2014 if (mAppStateListeners != null) {
2015 for (GeckoAppShell.AppStateListener listener: mAppStateListeners) {
2016 listener.onResume();
2017 }
2018 }
2019
2020 // We use two times: a pseudo-unique wall-clock time to identify the
2021 // current session across power cycles, and the elapsed realtime to
2022 // track the duration of the session.
2023 final long now = System.currentTimeMillis();
2024 final long realTime = android.os.SystemClock.elapsedRealtime();
2025
2026 ThreadUtils.postToBackgroundThread(new Runnable() {
2027 @Override
2028 public void run() {
2029 // Now construct the new session on HealthRecorder's behalf. We do this here
2030 // so it can benefit from a single near-startup prefs commit.
2031 SessionInformation currentSession = new SessionInformation(now, realTime);
2032
2033 SharedPreferences prefs = GeckoApp.this.getSharedPreferences();
2034 SharedPreferences.Editor editor = prefs.edit();
2035 editor.putBoolean(GeckoApp.PREFS_WAS_STOPPED, false);
2036 currentSession.recordBegin(editor);
2037 editor.commit();
2038
2039 final HealthRecorder rec = mHealthRecorder;
2040 if (rec != null) {
2041 rec.setCurrentSession(currentSession);
2042 } else {
2043 Log.w(LOGTAG, "Can't record session: rec is null.");
2044 }
2045 }
2046 });
2047 }
2048
2049 @Override
2050 public void onWindowFocusChanged(boolean hasFocus) {
2051 super.onWindowFocusChanged(hasFocus);
2052
2053 if (!mInitialized && hasFocus) {
2054 initialize();
2055 getWindow().setBackgroundDrawable(null);
2056 }
2057 }
2058
2059 @Override
2060 public void onPause()
2061 {
2062 final HealthRecorder rec = mHealthRecorder;
2063 final Context context = this;
2064
2065 // In some way it's sad that Android will trigger StrictMode warnings
2066 // here as the whole point is to save to disk while the activity is not
2067 // interacting with the user.
2068 ThreadUtils.postToBackgroundThread(new Runnable() {
2069 @Override
2070 public void run() {
2071 SharedPreferences prefs = GeckoApp.this.getSharedPreferences();
2072 SharedPreferences.Editor editor = prefs.edit();
2073 editor.putBoolean(GeckoApp.PREFS_WAS_STOPPED, true);
2074 if (rec != null) {
2075 rec.recordSessionEnd("P", editor);
2076 }
2077
2078 // If we haven't done it before, cleanup any old files in our old temp dir
2079 if (prefs.getBoolean(GeckoApp.PREFS_CLEANUP_TEMP_FILES, true)) {
2080 File tempDir = GeckoLoader.getGREDir(GeckoApp.this);
2081 FileUtils.delTree(tempDir, new FileUtils.NameAndAgeFilter(null, ONE_DAY_MS), false);
2082
2083 editor.putBoolean(GeckoApp.PREFS_CLEANUP_TEMP_FILES, false);
2084 }
2085
2086 editor.commit();
2087
2088 // In theory, the first browser session will not run long enough that we need to
2089 // prune during it and we'd rather run it when the browser is inactive so we wait
2090 // until here to register the prune service.
2091 GeckoPreferences.broadcastHealthReportPrune(context);
2092 }
2093 });
2094
2095 if (mAppStateListeners != null) {
2096 for(GeckoAppShell.AppStateListener listener: mAppStateListeners) {
2097 listener.onPause();
2098 }
2099 }
2100
2101 super.onPause();
2102 }
2103
2104 @Override
2105 public void onRestart()
2106 {
2107 ThreadUtils.postToBackgroundThread(new Runnable() {
2108 @Override
2109 public void run() {
2110 SharedPreferences prefs = GeckoApp.this.getSharedPreferences();
2111 SharedPreferences.Editor editor = prefs.edit();
2112 editor.putBoolean(GeckoApp.PREFS_WAS_STOPPED, false);
2113 editor.commit();
2114 }
2115 });
2116
2117 super.onRestart();
2118 }
2119
2120 @Override
2121 public void onDestroy()
2122 {
2123 unregisterEventListener("log");
2124 unregisterEventListener("Reader:ListStatusRequest");
2125 unregisterEventListener("Reader:Added");
2126 unregisterEventListener("Reader:Removed");
2127 unregisterEventListener("Reader:Share");
2128 unregisterEventListener("Reader:FaviconRequest");
2129 unregisterEventListener("onCameraCapture");
2130 unregisterEventListener("Gecko:Ready");
2131 unregisterEventListener("Gecko:DelayedStartup");
2132 unregisterEventListener("Toast:Show");
2133 unregisterEventListener("DOMFullScreen:Start");
2134 unregisterEventListener("DOMFullScreen:Stop");
2135 unregisterEventListener("ToggleChrome:Hide");
2136 unregisterEventListener("ToggleChrome:Show");
2137 unregisterEventListener("ToggleChrome:Focus");
2138 unregisterEventListener("Permissions:Data");
2139 unregisterEventListener("Session:StatePurged");
2140 unregisterEventListener("Bookmark:Insert");
2141 unregisterEventListener("Accessibility:Event");
2142 unregisterEventListener("Accessibility:Ready");
2143 unregisterEventListener("Shortcut:Remove");
2144 unregisterEventListener("Share:Text");
2145 unregisterEventListener("Image:SetAs");
2146 unregisterEventListener("Sanitize:ClearHistory");
2147 unregisterEventListener("Update:Check");
2148 unregisterEventListener("Update:Download");
2149 unregisterEventListener("Update:Install");
2150 unregisterEventListener("PrivateBrowsing:Data");
2151 unregisterEventListener("Contact:Add");
2152 unregisterEventListener("Intent:Open");
2153 unregisterEventListener("Intent:GetHandlers");
2154 unregisterEventListener("Locale:Set");
2155 unregisterEventListener("NativeApp:IsDebuggable");
2156 unregisterEventListener("SystemUI:Visibility");
2157
2158 if (mWebappEventListener != null) {
2159 mWebappEventListener.unregisterEvents();
2160 mWebappEventListener = null;
2161 }
2162
2163 deleteTempFiles();
2164
2165 if (mLayerView != null)
2166 mLayerView.destroy();
2167 if (mDoorHangerPopup != null)
2168 mDoorHangerPopup.destroy();
2169 if (mFormAssistPopup != null)
2170 mFormAssistPopup.destroy();
2171 if (mContactService != null)
2172 mContactService.destroy();
2173 if (mPromptService != null)
2174 mPromptService.destroy();
2175 if (mTextSelection != null)
2176 mTextSelection.destroy();
2177 NotificationHelper.destroy();
2178
2179 if (SmsManager.getInstance() != null) {
2180 SmsManager.getInstance().stop();
2181 if (isFinishing())
2182 SmsManager.getInstance().shutdown();
2183 }
2184
2185 final HealthRecorder rec = mHealthRecorder;
2186 mHealthRecorder = null;
2187 if (rec != null && rec.isEnabled()) {
2188 // Closing a BrowserHealthRecorder could incur a write.
2189 ThreadUtils.postToBackgroundThread(new Runnable() {
2190 @Override
2191 public void run() {
2192 rec.close();
2193 }
2194 });
2195 }
2196
2197 Favicons.close();
2198
2199 super.onDestroy();
2200
2201 Tabs.unregisterOnTabsChangedListener(this);
2202 }
2203
2204 protected void registerEventListener(String event) {
2205 GeckoAppShell.getEventDispatcher().registerEventListener(event, this);
2206 }
2207
2208 protected void unregisterEventListener(String event) {
2209 GeckoAppShell.getEventDispatcher().unregisterEventListener(event, this);
2210 }
2211
2212 // Get a temporary directory, may return null
2213 public static File getTempDirectory() {
2214 File dir = GeckoApplication.get().getExternalFilesDir("temp");
2215 return dir;
2216 }
2217
2218 // Delete any files in our temporary directory
2219 public static void deleteTempFiles() {
2220 File dir = getTempDirectory();
2221 if (dir == null)
2222 return;
2223 File[] files = dir.listFiles();
2224 if (files == null)
2225 return;
2226 for (File file : files) {
2227 file.delete();
2228 }
2229 }
2230
2231 @Override
2232 public void onConfigurationChanged(Configuration newConfig) {
2233 Log.d(LOGTAG, "onConfigurationChanged: " + newConfig.locale);
2234 BrowserLocaleManager.getInstance().correctLocale(this, getResources(), newConfig);
2235
2236 // onConfigurationChanged is not called for 180 degree orientation changes,
2237 // we will miss such rotations and the screen orientation will not be
2238 // updated.
2239 if (GeckoScreenOrientation.getInstance().update(newConfig.orientation)) {
2240 if (mFormAssistPopup != null)
2241 mFormAssistPopup.hide();
2242 refreshChrome();
2243 }
2244 super.onConfigurationChanged(newConfig);
2245 }
2246
2247 public String getContentProcessName() {
2248 return AppConstants.MOZ_CHILD_PROCESS_NAME;
2249 }
2250
2251 public void addEnvToIntent(Intent intent) {
2252 Map<String,String> envMap = System.getenv();
2253 Set<Map.Entry<String,String>> envSet = envMap.entrySet();
2254 Iterator<Map.Entry<String,String>> envIter = envSet.iterator();
2255 int c = 0;
2256 while (envIter.hasNext()) {
2257 Map.Entry<String,String> entry = envIter.next();
2258 intent.putExtra("env" + c, entry.getKey() + "="
2259 + entry.getValue());
2260 c++;
2261 }
2262 }
2263
2264 public void doRestart() {
2265 doRestart(RESTARTER_ACTION, null);
2266 }
2267
2268 public void doRestart(String args) {
2269 doRestart(RESTARTER_ACTION, args);
2270 }
2271
2272 public void doRestart(String action, String args) {
2273 Log.d(LOGTAG, "doRestart(\"" + action + "\")");
2274 try {
2275 Intent intent = new Intent(action);
2276 intent.setClassName(AppConstants.ANDROID_PACKAGE_NAME, RESTARTER_CLASS);
2277 /* TODO: addEnvToIntent(intent); */
2278 if (args != null)
2279 intent.putExtra("args", args);
2280 intent.putExtra("didRestart", true);
2281 Log.d(LOGTAG, "Restart intent: " + intent.toString());
2282 GeckoAppShell.killAnyZombies();
2283 startActivity(intent);
2284 } catch (Exception e) {
2285 Log.e(LOGTAG, "Error effecting restart.", e);
2286 }
2287
2288 finish();
2289 // Give the restart process time to start before we die
2290 GeckoAppShell.waitForAnotherGeckoProc();
2291 }
2292
2293 public void handleNotification(String action, String alertName, String alertCookie) {
2294 // If Gecko isn't running yet, we ignore the notification. Note that
2295 // even if Gecko is running but it was restarted since the notification
2296 // was created, the notification won't be handled (bug 849653).
2297 if (GeckoThread.checkLaunchState(GeckoThread.LaunchState.GeckoRunning)) {
2298 GeckoAppShell.handleNotification(action, alertName, alertCookie);
2299 }
2300 }
2301
2302 private void checkMigrateProfile() {
2303 final File profileDir = getProfile().getDir();
2304
2305 if (profileDir != null) {
2306 ThreadUtils.postToBackgroundThread(new Runnable() {
2307 @Override
2308 public void run() {
2309 Handler handler = new Handler();
2310 handler.postDelayed(new DeferredCleanupTask(), CLEANUP_DEFERRAL_SECONDS * 1000);
2311 }
2312 });
2313 }
2314 }
2315
2316 private class DeferredCleanupTask implements Runnable {
2317 // The cleanup-version setting is recorded to avoid repeating the same
2318 // tasks on subsequent startups; CURRENT_CLEANUP_VERSION may be updated
2319 // if we need to do additional cleanup for future Gecko versions.
2320
2321 private static final String CLEANUP_VERSION = "cleanup-version";
2322 private static final int CURRENT_CLEANUP_VERSION = 1;
2323
2324 @Override
2325 public void run() {
2326 long cleanupVersion = getSharedPreferences().getInt(CLEANUP_VERSION, 0);
2327
2328 if (cleanupVersion < 1) {
2329 // Reduce device storage footprint by removing .ttf files from
2330 // the res/fonts directory: we no longer need to copy our
2331 // bundled fonts out of the APK in order to use them.
2332 // See https://bugzilla.mozilla.org/show_bug.cgi?id=878674.
2333 File dir = new File("res/fonts");
2334 if (dir.exists() && dir.isDirectory()) {
2335 for (File file : dir.listFiles()) {
2336 if (file.isFile() && file.getName().endsWith(".ttf")) {
2337 Log.i(LOGTAG, "deleting " + file.toString());
2338 file.delete();
2339 }
2340 }
2341 if (!dir.delete()) {
2342 Log.w(LOGTAG, "unable to delete res/fonts directory (not empty?)");
2343 } else {
2344 Log.i(LOGTAG, "res/fonts directory deleted");
2345 }
2346 }
2347 }
2348
2349 // Additional cleanup needed for future versions would go here
2350
2351 if (cleanupVersion != CURRENT_CLEANUP_VERSION) {
2352 SharedPreferences.Editor editor = GeckoApp.this.getSharedPreferences().edit();
2353 editor.putInt(CLEANUP_VERSION, CURRENT_CLEANUP_VERSION);
2354 editor.commit();
2355 }
2356 }
2357 }
2358
2359 public PromptService getPromptService() {
2360 return mPromptService;
2361 }
2362
2363 @Override
2364 public void onBackPressed() {
2365 if (getSupportFragmentManager().getBackStackEntryCount() > 0) {
2366 super.onBackPressed();
2367 return;
2368 }
2369
2370 if (autoHideTabs()) {
2371 return;
2372 }
2373
2374 if (mDoorHangerPopup != null && mDoorHangerPopup.isShowing()) {
2375 mDoorHangerPopup.dismiss();
2376 return;
2377 }
2378
2379 if (mFullScreenPluginView != null) {
2380 GeckoAppShell.onFullScreenPluginHidden(mFullScreenPluginView);
2381 removeFullScreenPluginView(mFullScreenPluginView);
2382 return;
2383 }
2384
2385 if (mLayerView != null && mLayerView.isFullScreen()) {
2386 GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("FullScreen:Exit", null));
2387 return;
2388 }
2389
2390 Tabs tabs = Tabs.getInstance();
2391 Tab tab = tabs.getSelectedTab();
2392 if (tab == null) {
2393 moveTaskToBack(true);
2394 return;
2395 }
2396
2397 if (tab.doBack())
2398 return;
2399
2400 if (tab.isExternal()) {
2401 moveTaskToBack(true);
2402 tabs.closeTab(tab);
2403 return;
2404 }
2405
2406 int parentId = tab.getParentId();
2407 Tab parent = tabs.getTab(parentId);
2408 if (parent != null) {
2409 // The back button should always return to the parent (not a sibling).
2410 tabs.closeTab(tab, parent);
2411 return;
2412 }
2413
2414 moveTaskToBack(true);
2415 }
2416
2417 @Override
2418 protected void onActivityResult(int requestCode, int resultCode, Intent data) {
2419 if (!ActivityHandlerHelper.handleActivityResult(requestCode, resultCode, data)) {
2420 super.onActivityResult(requestCode, resultCode, data);
2421 }
2422 }
2423
2424 public AbsoluteLayout getPluginContainer() { return mPluginContainer; }
2425
2426 // Accelerometer.
2427 @Override
2428 public void onAccuracyChanged(Sensor sensor, int accuracy) {
2429 }
2430
2431 @Override
2432 public void onSensorChanged(SensorEvent event) {
2433 GeckoAppShell.sendEventToGecko(GeckoEvent.createSensorEvent(event));
2434 }
2435
2436 // Geolocation.
2437 @Override
2438 public void onLocationChanged(Location location) {
2439 // No logging here: user-identifying information.
2440 GeckoAppShell.sendEventToGecko(GeckoEvent.createLocationEvent(location));
2441 if (mShouldReportGeoData)
2442 collectAndReportLocInfo(location);
2443 }
2444
2445 public void setCurrentSignalStrenth(SignalStrength ss) {
2446 if (ss.isGsm())
2447 mSignalStrenth = ss.getGsmSignalStrength();
2448 }
2449
2450 private int getCellInfo(JSONArray cellInfo) {
2451 TelephonyManager tm = (TelephonyManager)getSystemService(Context.TELEPHONY_SERVICE);
2452 if (tm == null)
2453 return TelephonyManager.PHONE_TYPE_NONE;
2454 List<NeighboringCellInfo> cells = tm.getNeighboringCellInfo();
2455 CellLocation cl = tm.getCellLocation();
2456 String mcc = "", mnc = "";
2457 if (cl instanceof GsmCellLocation) {
2458 JSONObject obj = new JSONObject();
2459 GsmCellLocation gcl = (GsmCellLocation)cl;
2460 try {
2461 obj.put("lac", gcl.getLac());
2462 obj.put("cid", gcl.getCid());
2463
2464 int psc = (Build.VERSION.SDK_INT >= 9) ? gcl.getPsc() : -1;
2465 obj.put("psc", psc);
2466
2467 switch(tm.getNetworkType()) {
2468 case TelephonyManager.NETWORK_TYPE_GPRS:
2469 case TelephonyManager.NETWORK_TYPE_EDGE:
2470 obj.put("radio", "gsm");
2471 break;
2472 case TelephonyManager.NETWORK_TYPE_UMTS:
2473 case TelephonyManager.NETWORK_TYPE_HSDPA:
2474 case TelephonyManager.NETWORK_TYPE_HSUPA:
2475 case TelephonyManager.NETWORK_TYPE_HSPA:
2476 case TelephonyManager.NETWORK_TYPE_HSPAP:
2477 obj.put("radio", "umts");
2478 break;
2479 }
2480 String mcc_mnc = tm.getNetworkOperator();
2481 if (mcc_mnc.length() > 3) {
2482 mcc = mcc_mnc.substring(0, 3);
2483 mnc = mcc_mnc.substring(3);
2484 obj.put("mcc", mcc);
2485 obj.put("mnc", mnc);
2486 }
2487 obj.put("asu", mSignalStrenth);
2488 } catch(JSONException jsonex) {}
2489 cellInfo.put(obj);
2490 }
2491 if (cells != null) {
2492 for (NeighboringCellInfo nci : cells) {
2493 try {
2494 JSONObject obj = new JSONObject();
2495 obj.put("lac", nci.getLac());
2496 obj.put("cid", nci.getCid());
2497 obj.put("psc", nci.getPsc());
2498 obj.put("mcc", mcc);
2499 obj.put("mnc", mnc);
2500
2501 int dbm;
2502 switch(nci.getNetworkType()) {
2503 case TelephonyManager.NETWORK_TYPE_GPRS:
2504 case TelephonyManager.NETWORK_TYPE_EDGE:
2505 obj.put("radio", "gsm");
2506 break;
2507 case TelephonyManager.NETWORK_TYPE_UMTS:
2508 case TelephonyManager.NETWORK_TYPE_HSDPA:
2509 case TelephonyManager.NETWORK_TYPE_HSUPA:
2510 case TelephonyManager.NETWORK_TYPE_HSPA:
2511 case TelephonyManager.NETWORK_TYPE_HSPAP:
2512 obj.put("radio", "umts");
2513 break;
2514 }
2515
2516 obj.put("asu", nci.getRssi());
2517 cellInfo.put(obj);
2518 } catch(JSONException jsonex) {}
2519 }
2520 }
2521 return tm.getPhoneType();
2522 }
2523
2524 private static boolean shouldLog(final ScanResult sr) {
2525 return sr.SSID == null || !sr.SSID.endsWith("_nomap");
2526 }
2527
2528 private void collectAndReportLocInfo(Location location) {
2529 final JSONObject locInfo = new JSONObject();
2530 WifiManager wm = (WifiManager)getSystemService(Context.WIFI_SERVICE);
2531 wm.startScan();
2532 try {
2533 JSONArray cellInfo = new JSONArray();
2534
2535 String radioType = getRadioTypeName(getCellInfo(cellInfo));
2536 if (radioType != null) {
2537 locInfo.put("radio", radioType);
2538 }
2539
2540 locInfo.put("lon", location.getLongitude());
2541 locInfo.put("lat", location.getLatitude());
2542
2543 // If we have an accuracy, round it up to the next meter.
2544 if (location.hasAccuracy()) {
2545 locInfo.put("accuracy", (int) Math.ceil(location.getAccuracy()));
2546 }
2547
2548 // If we have an altitude, round it to the nearest meter.
2549 if (location.hasAltitude()) {
2550 locInfo.put("altitude", Math.round(location.getAltitude()));
2551 }
2552
2553 // Reduce timestamp precision so as to expose less PII.
2554 DateFormat df = new SimpleDateFormat("yyyy-MM-dd", Locale.US);
2555 locInfo.put("time", df.format(new Date(location.getTime())));
2556 locInfo.put("cell", cellInfo);
2557
2558 JSONArray wifiInfo = new JSONArray();
2559 List<ScanResult> aps = wm.getScanResults();
2560 if (aps != null) {
2561 for (ScanResult ap : aps) {
2562 if (!shouldLog(ap))
2563 continue;
2564
2565 JSONObject obj = new JSONObject();
2566 obj.put("key", ap.BSSID);
2567 obj.put("frequency", ap.frequency);
2568 obj.put("signal", ap.level);
2569 wifiInfo.put(obj);
2570 }
2571 }
2572 locInfo.put("wifi", wifiInfo);
2573 } catch (JSONException jsonex) {
2574 Log.w(LOGTAG, "json exception", jsonex);
2575 return;
2576 }
2577
2578 ThreadUtils.postToBackgroundThread(new Runnable() {
2579 public void run() {
2580 try {
2581 URL url = new URL(LOCATION_URL);
2582 HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
2583 try {
2584 urlConnection.setDoOutput(true);
2585
2586 // Workaround for a bug in Android HttpURLConnection. When the library
2587 // reuses a stale connection, the connection may fail with an EOFException.
2588 if (Build.VERSION.SDK_INT >= 14 && Build.VERSION.SDK_INT <= 18) {
2589 urlConnection.setRequestProperty("Connection", "Close");
2590 }
2591
2592 JSONArray batch = new JSONArray();
2593 batch.put(locInfo);
2594 JSONObject wrapper = new JSONObject();
2595 wrapper.put("items", batch);
2596 byte[] bytes = wrapper.toString().getBytes();
2597 urlConnection.setFixedLengthStreamingMode(bytes.length);
2598 OutputStream out = new BufferedOutputStream(urlConnection.getOutputStream());
2599 out.write(bytes);
2600 out.flush();
2601 } catch (JSONException jsonex) {
2602 Log.e(LOGTAG, "error wrapping data as a batch", jsonex);
2603 } catch (IOException ioex) {
2604 Log.e(LOGTAG, "error submitting data", ioex);
2605 } finally {
2606 urlConnection.disconnect();
2607 }
2608 } catch (IOException ioex) {
2609 Log.e(LOGTAG, "error submitting data", ioex);
2610 }
2611 }
2612 });
2613 }
2614
2615 private static String getRadioTypeName(int phoneType) {
2616 switch (phoneType) {
2617 case TelephonyManager.PHONE_TYPE_CDMA:
2618 return "cdma";
2619
2620 case TelephonyManager.PHONE_TYPE_GSM:
2621 return "gsm";
2622
2623 case TelephonyManager.PHONE_TYPE_NONE:
2624 case TelephonyManager.PHONE_TYPE_SIP:
2625 // These devices have no radio.
2626 return null;
2627
2628 default:
2629 Log.e(LOGTAG, "", new IllegalArgumentException("Unexpected PHONE_TYPE: " + phoneType));
2630 return null;
2631 }
2632 }
2633
2634 @Override
2635 public void onProviderDisabled(String provider)
2636 {
2637 }
2638
2639 @Override
2640 public void onProviderEnabled(String provider)
2641 {
2642 }
2643
2644 @Override
2645 public void onStatusChanged(String provider, int status, Bundle extras)
2646 {
2647 }
2648
2649 // Called when a Gecko Hal WakeLock is changed
2650 public void notifyWakeLockChanged(String topic, String state) {
2651 PowerManager.WakeLock wl = mWakeLocks.get(topic);
2652 if (state.equals("locked-foreground") && wl == null) {
2653 PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
2654 wl = pm.newWakeLock(PowerManager.SCREEN_BRIGHT_WAKE_LOCK, topic);
2655 wl.acquire();
2656 mWakeLocks.put(topic, wl);
2657 } else if (!state.equals("locked-foreground") && wl != null) {
2658 wl.release();
2659 mWakeLocks.remove(topic);
2660 }
2661 }
2662
2663 public void notifyCheckUpdateResult(String result) {
2664 GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Update:CheckResult", result));
2665 }
2666
2667 protected void geckoConnected() {
2668 mLayerView.geckoConnected();
2669 mLayerView.setOverScrollMode(View.OVER_SCROLL_NEVER);
2670 }
2671
2672 public void setAccessibilityEnabled(boolean enabled) {
2673 }
2674
2675 public static class MainLayout extends RelativeLayout {
2676 private TouchEventInterceptor mTouchEventInterceptor;
2677 private MotionEventInterceptor mMotionEventInterceptor;
2678
2679 public MainLayout(Context context, AttributeSet attrs) {
2680 super(context, attrs);
2681 }
2682
2683 public void setTouchEventInterceptor(TouchEventInterceptor interceptor) {
2684 mTouchEventInterceptor = interceptor;
2685 }
2686
2687 public void setMotionEventInterceptor(MotionEventInterceptor interceptor) {
2688 mMotionEventInterceptor = interceptor;
2689 }
2690
2691 @Override
2692 public boolean onInterceptTouchEvent(MotionEvent event) {
2693 if (mTouchEventInterceptor != null && mTouchEventInterceptor.onInterceptTouchEvent(this, event)) {
2694 return true;
2695 }
2696 return super.onInterceptTouchEvent(event);
2697 }
2698
2699 @Override
2700 public boolean onTouchEvent(MotionEvent event) {
2701 if (mTouchEventInterceptor != null && mTouchEventInterceptor.onTouch(this, event)) {
2702 return true;
2703 }
2704 return super.onTouchEvent(event);
2705 }
2706
2707 @Override
2708 public boolean onGenericMotionEvent(MotionEvent event) {
2709 if (mMotionEventInterceptor != null && mMotionEventInterceptor.onInterceptMotionEvent(this, event)) {
2710 return true;
2711 }
2712 return super.onGenericMotionEvent(event);
2713 }
2714
2715 @Override
2716 public void setDrawingCacheEnabled(boolean enabled) {
2717 // Instead of setting drawing cache in the view itself, we simply
2718 // enable drawing caching on its children. This is mainly used in
2719 // animations (see PropertyAnimator)
2720 super.setChildrenDrawnWithCacheEnabled(enabled);
2721 }
2722 }
2723
2724 private class FullScreenHolder extends FrameLayout {
2725
2726 public FullScreenHolder(Context ctx) {
2727 super(ctx);
2728 }
2729
2730 @Override
2731 public void addView(View view, int index) {
2732 /**
2733 * This normally gets called when Flash adds a separate SurfaceView
2734 * for the video. It is unhappy if we have the LayerView underneath
2735 * it for some reason so we need to hide that. Hiding the LayerView causes
2736 * its surface to be destroyed, which causes a pause composition
2737 * event to be sent to Gecko. We synchronously wait for that to be
2738 * processed. Simultaneously, however, Flash is waiting on a mutex so
2739 * the post() below is an attempt to avoid a deadlock.
2740 */
2741 super.addView(view, index);
2742
2743 ThreadUtils.postToUiThread(new Runnable() {
2744 @Override
2745 public void run() {
2746 mLayerView.hideSurface();
2747 }
2748 });
2749 }
2750
2751 /**
2752 * The methods below are simply copied from what Android WebKit does.
2753 * It wasn't ever called in my testing, but might as well
2754 * keep it in case it is for some reason. The methods
2755 * all return true because we don't want any events
2756 * leaking out from the fullscreen view.
2757 */
2758 @Override
2759 public boolean onKeyDown(int keyCode, KeyEvent event) {
2760 if (event.isSystem()) {
2761 return super.onKeyDown(keyCode, event);
2762 }
2763 mFullScreenPluginView.onKeyDown(keyCode, event);
2764 return true;
2765 }
2766
2767 @Override
2768 public boolean onKeyUp(int keyCode, KeyEvent event) {
2769 if (event.isSystem()) {
2770 return super.onKeyUp(keyCode, event);
2771 }
2772 mFullScreenPluginView.onKeyUp(keyCode, event);
2773 return true;
2774 }
2775
2776 @Override
2777 public boolean onTouchEvent(MotionEvent event) {
2778 return true;
2779 }
2780
2781 @Override
2782 public boolean onTrackballEvent(MotionEvent event) {
2783 mFullScreenPluginView.onTrackballEvent(event);
2784 return true;
2785 }
2786 }
2787
2788 protected NotificationClient makeNotificationClient() {
2789 // Don't use a notification service; we may be killed in the background
2790 // during downloads.
2791 return new AppNotificationClient(getApplicationContext());
2792 }
2793
2794 private int getVersionCode() {
2795 int versionCode = 0;
2796 try {
2797 versionCode = getPackageManager().getPackageInfo(getPackageName(), 0).versionCode;
2798 } catch (NameNotFoundException e) {
2799 Log.wtf(LOGTAG, getPackageName() + " not found", e);
2800 }
2801 return versionCode;
2802 }
2803
2804 protected boolean getIsDebuggable() {
2805 // Return false so Fennec doesn't appear to be debuggable. WebappImpl
2806 // then overrides this and returns the value of android:debuggable for
2807 // the webapp APK, so webapps get the behavior supported by this method
2808 // (i.e. automatic configuration and enabling of the remote debugger).
2809 return false;
2810
2811 // If we ever want to expose this for Fennec, here's how we would do it:
2812 // int flags = 0;
2813 // try {
2814 // flags = getPackageManager().getPackageInfo(getPackageName(), 0).applicationInfo.flags;
2815 // } catch (NameNotFoundException e) {
2816 // Log.wtf(LOGTAG, getPackageName() + " not found", e);
2817 // }
2818 // return (flags & android.content.pm.ApplicationInfo.FLAG_DEBUGGABLE) != 0;
2819 }
2820
2821 // FHR reason code for a session end prior to a restart for a
2822 // locale change.
2823 private static final String SESSION_END_LOCALE_CHANGED = "L";
2824
2825 /**
2826 * Use BrowserLocaleManager to change our persisted and current locales,
2827 * and poke HealthRecorder to tell it of our changed state.
2828 */
2829 private void setLocale(final String locale) {
2830 if (locale == null) {
2831 return;
2832 }
2833 final String resultant = BrowserLocaleManager.getInstance().setSelectedLocale(this, locale);
2834 if (resultant == null) {
2835 return;
2836 }
2837
2838 final boolean startNewSession = true;
2839 final boolean shouldRestart = false;
2840
2841 // If the HealthRecorder is not yet initialized (unlikely), the locale change won't
2842 // trigger a session transition and subsequent events will be recorded in an environment
2843 // with the wrong locale.
2844 final HealthRecorder rec = mHealthRecorder;
2845 if (rec != null) {
2846 rec.onAppLocaleChanged(resultant);
2847 rec.onEnvironmentChanged(startNewSession, SESSION_END_LOCALE_CHANGED);
2848 }
2849
2850 if (!shouldRestart) {
2851 ThreadUtils.postToUiThread(new Runnable() {
2852 @Override
2853 public void run() {
2854 GeckoApp.this.onLocaleReady(resultant);
2855 }
2856 });
2857 return;
2858 }
2859
2860 // Do this in the background so that the health recorder has its
2861 // time to finish.
2862 ThreadUtils.postToBackgroundThread(new Runnable() {
2863 @Override
2864 public void run() {
2865 GeckoApp.this.doRestart();
2866 GeckoApp.this.finish();
2867 }
2868 });
2869 }
2870
2871 private void setSystemUiVisible(final boolean visible) {
2872 if (Build.VERSION.SDK_INT < 14) {
2873 return;
2874 }
2875
2876 ThreadUtils.postToUiThread(new Runnable() {
2877 @Override
2878 public void run() {
2879 if (visible) {
2880 mMainLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE);
2881 } else {
2882 mMainLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE);
2883 }
2884 }
2885 });
2886 }
2887
2888 protected HealthRecorder createHealthRecorder(final Context context,
2889 final String profilePath,
2890 final EventDispatcher dispatcher,
2891 final String osLocale,
2892 final String appLocale,
2893 final SessionInformation previousSession) {
2894 // GeckoApp does not need to record any health information - return a stub.
2895 return new StubbedHealthRecorder();
2896 }
2897 }

mercurial