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 org.mozilla.gecko.AppConstants.Versions;
9 import org.mozilla.gecko.GeckoProfileDirectories.NoMozillaDirectoryException;
10 import org.mozilla.gecko.annotation.RobocopTarget;
11 import org.mozilla.gecko.annotation.WrapForJNI;
12 import org.mozilla.gecko.health.HealthRecorder;
13 import org.mozilla.gecko.health.SessionInformation;
14 import org.mozilla.gecko.health.StubbedHealthRecorder;
15 import org.mozilla.gecko.home.HomeConfig.PanelType;
16 import org.mozilla.gecko.menu.GeckoMenu;
17 import org.mozilla.gecko.menu.GeckoMenuInflater;
18 import org.mozilla.gecko.menu.MenuPanel;
19 import org.mozilla.gecko.mma.MmaDelegate;
20 import org.mozilla.gecko.notifications.NotificationHelper;
21 import org.mozilla.gecko.util.IntentUtils;
22 import org.mozilla.gecko.mozglue.SafeIntent;
23 import org.mozilla.gecko.mozglue.GeckoLoader;
24 import org.mozilla.gecko.permissions.Permissions;
25 import org.mozilla.gecko.preferences.ClearOnShutdownPref;
26 import org.mozilla.gecko.preferences.GeckoPreferences;
27 import org.mozilla.gecko.prompts.PromptService;
28 import org.mozilla.gecko.restrictions.Restrictions;
29 import org.mozilla.gecko.tabqueue.TabQueueHelper;
30 import org.mozilla.gecko.text.TextSelection;
31 import org.mozilla.gecko.updater.UpdateServiceHelper;
32 import org.mozilla.gecko.util.ActivityUtils;
33 import org.mozilla.gecko.util.BundleEventListener;
34 import org.mozilla.gecko.util.EventCallback;
35 import org.mozilla.gecko.util.FileUtils;
36 import org.mozilla.gecko.util.GeckoBundle;
37 import org.mozilla.gecko.util.HardwareUtils;
38 import org.mozilla.gecko.util.PrefUtils;
39 import org.mozilla.gecko.util.ThreadUtils;
40 import org.mozilla.gecko.util.ViewUtil;
41 import org.mozilla.gecko.widget.ActionModePresenter;
42 import org.mozilla.gecko.widget.AnchoredPopup;
43 import org.mozilla.geckoview.GeckoSession;
44 import org.mozilla.geckoview.GeckoSessionSettings;
45 import org.mozilla.geckoview.GeckoView;
46 
47 import android.animation.Animator;
48 import android.animation.ObjectAnimator;
49 import android.annotation.TargetApi;
50 import android.app.Activity;
51 import android.app.AlertDialog;
52 import android.content.Context;
53 import android.content.DialogInterface;
54 import android.content.Intent;
55 import android.content.SharedPreferences;
56 import android.content.pm.PackageManager.NameNotFoundException;
57 import android.content.res.Configuration;
58 import android.graphics.BitmapFactory;
59 import android.net.Uri;
60 import android.os.Build;
61 import android.os.Bundle;
62 import android.os.Handler;
63 import android.os.StrictMode;
64 import android.provider.ContactsContract;
65 import android.support.annotation.NonNull;
66 import android.support.annotation.WorkerThread;
67 import android.support.design.widget.Snackbar;
68 import android.text.TextUtils;
69 import android.util.AttributeSet;
70 import android.util.Log;
71 import android.util.SparseBooleanArray;
72 import android.util.SparseIntArray;
73 import android.view.KeyEvent;
74 import android.view.Menu;
75 import android.view.MenuInflater;
76 import android.view.MenuItem;
77 import android.view.MotionEvent;
78 import android.view.View;
79 import android.view.ViewTreeObserver;
80 import android.view.Window;
81 import android.widget.AdapterView;
82 import android.widget.Button;
83 import android.widget.ListView;
84 import android.widget.RelativeLayout;
85 import android.widget.SimpleAdapter;
86 import android.widget.TextView;
87 import android.widget.Toast;
88 
89 import org.json.JSONArray;
90 import org.json.JSONException;
91 import org.json.JSONObject;
92 
93 import java.io.File;
94 import java.util.ArrayList;
95 import java.util.HashMap;
96 import java.util.HashSet;
97 import java.util.Iterator;
98 import java.util.Locale;
99 import java.util.Map;
100 import java.util.Set;
101 import java.util.concurrent.TimeUnit;
102 
103 import static org.mozilla.gecko.Tabs.INTENT_EXTRA_SESSION_UUID;
104 import static org.mozilla.gecko.Tabs.INTENT_EXTRA_TAB_ID;
105 import static org.mozilla.gecko.Tabs.INVALID_TAB_ID;
106 import static org.mozilla.gecko.mma.MmaDelegate.DOWNLOAD_MEDIA_SAVED_IMAGE;
107 import static org.mozilla.gecko.mma.MmaDelegate.READER_AVAILABLE;
108 
109 public abstract class GeckoApp extends GeckoActivity
110                                implements AnchoredPopup.OnVisibilityChangeListener,
111                                           BundleEventListener,
112                                           GeckoMenu.Callback,
113                                           GeckoMenu.MenuPresenter,
114                                           GeckoSession.ContentDelegate,
115                                           ScreenOrientationDelegate,
116                                           Tabs.OnTabsChangedListener,
117                                           ViewTreeObserver.OnGlobalLayoutListener {
118 
119     private static final String LOGTAG = "GeckoApp";
120     private static final long ONE_DAY_MS = TimeUnit.MILLISECONDS.convert(1, TimeUnit.DAYS);
121 
122     public static final String ACTION_ALERT_CALLBACK       = "org.mozilla.gecko.ALERT_CALLBACK";
123     public static final String ACTION_HOMESCREEN_SHORTCUT  = "org.mozilla.gecko.BOOKMARK";
124     public static final String ACTION_WEBAPP               = "org.mozilla.gecko.WEBAPP";
125     public static final String ACTION_DEBUG                = "org.mozilla.gecko.DEBUG";
126     public static final String ACTION_LAUNCH_SETTINGS      = "org.mozilla.gecko.SETTINGS";
127     public static final String ACTION_LOAD                 = "org.mozilla.gecko.LOAD";
128     public static final String ACTION_INIT_PW              = "org.mozilla.gecko.INIT_PW";
129     public static final String ACTION_SWITCH_TAB           = "org.mozilla.gecko.SWITCH_TAB";
130     public static final String ACTION_SHUTDOWN             = "org.mozilla.gecko.SHUTDOWN";
131 
132     public static final String INTENT_REGISTER_STUMBLER_LISTENER = "org.mozilla.gecko.STUMBLER_REGISTER_LOCAL_LISTENER";
133 
134     public static final String EXTRA_STATE_BUNDLE          = "stateBundle";
135 
136     public static final String PREFS_ALLOW_STATE_BUNDLE    = "allowStateBundle";
137     public static final String PREFS_FLASH_USAGE           = "playFlashCount";
138     public static final String PREFS_VERSION_CODE          = "versionCode";
139     public static final String PREFS_WAS_STOPPED           = "wasStopped";
140     public static final String PREFS_CRASHED_COUNT         = "crashedCount";
141     public static final String PREFS_CLEANUP_TEMP_FILES    = "cleanupTempFiles";
142 
143     /**
144      * Used with SharedPreferences, per profile, to determine if this is the first run of
145      * the application. When accessing SharedPreferences, the default value of true should be used.
146 
147      * Originally, this was only used for the telemetry core ping logic. To avoid
148      * having to write custom migration logic, we just keep the original pref key.
149      * Be aware of {@link org.mozilla.gecko.fxa.EnvironmentUtils.GECKO_PREFS_IS_FIRST_RUN}.
150      */
151     public static final String PREFS_IS_FIRST_RUN = "telemetry-isFirstRun";
152 
153     public static final String SAVED_STATE_IN_BACKGROUND   = "inBackground";
154     public static final String SAVED_STATE_PRIVATE_SESSION = "privateSession";
155 
156     // Delay before running one-time "cleanup" tasks that may be needed
157     // after a version upgrade.
158     private static final int CLEANUP_DEFERRAL_SECONDS = 15;
159 
160     // Length of time in ms during which crashes are classified as startup crashes
161     // for crash loop detection purposes.
162     private static final int STARTUP_PHASE_DURATION_MS = 30 * 1000;
163 
164     private static boolean sAlreadyLoaded;
165 
166     protected RelativeLayout mRootLayout;
167     protected RelativeLayout mMainLayout;
168 
169     protected RelativeLayout mGeckoLayout;
170     protected MenuPanel mMenuPanel;
171     protected Menu mMenu;
172     protected boolean mIsRestoringActivity;
173 
174     /** Tells if we're aborting app launch, e.g. if this is an unsupported device configuration. */
175     protected boolean mIsAbortingAppLaunch;
176 
177     private PromptService mPromptService;
178     protected TextSelection mTextSelection;
179 
180     protected DoorHangerPopup mDoorHangerPopup;
181     protected FormAssistPopup mFormAssistPopup;
182 
183     protected GeckoView mLayerView;
184 
185     protected boolean mLastSessionCrashed;
186     protected boolean mShouldRestore;
187     private boolean mSessionRestoreParsingFinished = false;
188 
189     private boolean foregrounded = false;
190 
191     private static final class LastSessionParser extends SessionParser {
192         private JSONArray tabs;
193         private JSONObject windowObject;
194         private boolean loadingExternalURL;
195 
196         private boolean selectNextTab;
197         private boolean tabsWereSkipped;
198         private boolean tabsWereProcessed;
199 
200         private SparseIntArray tabIdMap;
201 
202         /**
203          * @param loadingExternalURL Pass true if we're going to open an additional tab to load an
204          *                           URL received through our launch intent.
205          */
LastSessionParser(JSONArray tabs, JSONObject windowObject, boolean loadingExternalURL)206         public LastSessionParser(JSONArray tabs, JSONObject windowObject, boolean loadingExternalURL) {
207             this.tabs = tabs;
208             this.windowObject = windowObject;
209             this.loadingExternalURL = loadingExternalURL;
210 
211             tabIdMap = new SparseIntArray();
212         }
213 
allTabsSkipped()214         public boolean allTabsSkipped() {
215             return tabsWereSkipped && !tabsWereProcessed;
216         }
217 
getNewTabId(int oldTabId)218         public int getNewTabId(int oldTabId) {
219             return tabIdMap.get(oldTabId, INVALID_TAB_ID);
220         }
221 
222         @Override
onTabRead(final SessionTab sessionTab)223         public void onTabRead(final SessionTab sessionTab) {
224             if (sessionTab.isAboutHomeWithoutHistory()) {
225                 // This is a tab pointing to about:home with no history. We won't restore
226                 // this tab. If we end up restoring no tabs then the browser will decide
227                 // whether it needs to open about:home or a different 'homepage'. If we'd
228                 // always restore about:home only tabs then we'd never open the homepage.
229                 // See bug 1261008.
230 
231                 if (!loadingExternalURL && sessionTab.isSelected()) {
232                     // Unfortunately this tab is the selected tab. Let's just try to select
233                     // the first tab. If we haven't restored any tabs so far then remember
234                     // to select the next tab that gets restored.
235 
236                     if (!Tabs.getInstance().selectLastTab()) {
237                         selectNextTab = true;
238                     }
239                 }
240 
241                 // Do not restore this tab.
242                 tabsWereSkipped = true;
243                 return;
244             }
245 
246             tabsWereProcessed = true;
247 
248             JSONObject tabObject = sessionTab.getTabObject();
249 
250             int flags = Tabs.LOADURL_NEW_TAB;
251             flags |= ((loadingExternalURL || !sessionTab.isSelected()) ? Tabs.LOADURL_DELAY_LOAD : 0);
252             flags |= (tabObject.optBoolean("desktopMode") ? Tabs.LOADURL_DESKTOP : 0);
253             flags |= (tabObject.optBoolean("isPrivate") ? Tabs.LOADURL_PRIVATE : 0);
254 
255             final Tab tab = Tabs.getInstance().loadUrl(sessionTab.getUrl(), flags);
256 
257             if (selectNextTab) {
258                 // We did not restore the selected tab previously. Now let's select this tab.
259                 Tabs.getInstance().selectTab(tab.getId());
260                 selectNextTab = false;
261             }
262 
263             ThreadUtils.postToUiThread(new Runnable() {
264                 @Override
265                 public void run() {
266                     tab.updateTitle(sessionTab.getTitle());
267                 }
268             });
269 
270             try {
271                 int oldTabId = tabObject.optInt("tabId", INVALID_TAB_ID);
272                 int newTabId = tab.getId();
273                 tabObject.put("tabId", newTabId);
274                 if  (oldTabId >= 0) {
275                     tabIdMap.put(oldTabId, newTabId);
276                 }
277             } catch (JSONException e) {
278                 Log.e(LOGTAG, "JSON error", e);
279             }
280             tabs.put(tabObject);
281         }
282 
283         @Override
onClosedTabsRead(final JSONArray closedTabData)284         public void onClosedTabsRead(final JSONArray closedTabData) throws JSONException {
285             windowObject.put("closedTabs", closedTabData);
286         }
287 
288         /**
289          * Updates stored parent tab IDs in the session store data to match the new tab IDs
290          * that have been allocated during startup session restore.
291          *
292          * @param tabData A JSONArray containg stored session store tabs.
293          */
updateParentId(final JSONArray tabData)294         public void updateParentId(final JSONArray tabData) {
295             if (tabData == null) {
296                 return;
297             }
298 
299             for (int i = 0; i < tabData.length(); i++) {
300                 try {
301                     JSONObject tabObject = tabData.getJSONObject(i);
302 
303                     int parentId = tabObject.getInt("parentId");
304                     int newParentId = getNewTabId(parentId);
305 
306                     tabObject.put("parentId", newParentId);
307                 } catch (JSONException ex) {
308                     // Tabs are not guaranteed to have a parentId,
309                     // so just skip the tab and try the next one.
310                 }
311             }
312         }
313     };
314 
315     protected boolean mInitialized;
316     protected boolean mWindowFocusInitialized;
317     private Telemetry.Timer mJavaUiStartupTimer;
318     private Telemetry.Timer mGeckoReadyStartupTimer;
319 
320     private String mPrivateBrowsingSession;
321     private boolean mPrivateBrowsingSessionOutdated;
322     private static final int MAX_PRIVATE_TABS_UPDATE_WAIT_MSEC = 500;
323 
324     private volatile HealthRecorder mHealthRecorder;
325     private volatile Locale mLastLocale;
326 
327     private boolean mShutdownOnDestroy;
328     private boolean mRestartOnShutdown;
329 
330     private boolean mWasFirstTabShownAfterActivityUnhidden;
331     private boolean mIsFullscreen;
332 
getLayout()333     abstract public int getLayout();
334 
getDoorhangerOverlay()335     abstract public View getDoorhangerOverlay();
336 
processTabQueue()337     protected void processTabQueue() {};
338 
openQueuedTabs()339     protected void openQueuedTabs() {};
340 
341     @SuppressWarnings("serial")
342     static final class SessionRestoreException extends Exception {
SessionRestoreException(Exception e)343         public SessionRestoreException(Exception e) {
344             super(e);
345         }
346 
SessionRestoreException(String message)347         public SessionRestoreException(String message) {
348             super(message);
349         }
350     }
351 
toggleChrome(final boolean aShow)352     void toggleChrome(final boolean aShow) { }
353 
focusChrome()354     void focusChrome() { }
355 
getSharedPreferences()356     public SharedPreferences getSharedPreferences() {
357         return GeckoSharedPrefs.forApp(this);
358     }
359 
getSharedPreferencesForProfile()360     public SharedPreferences getSharedPreferencesForProfile() {
361         return GeckoSharedPrefs.forProfile(this);
362     }
363 
364     @Override
onTabChanged(Tab tab, Tabs.TabEvents msg, String data)365     public void onTabChanged(Tab tab, Tabs.TabEvents msg, String data) {
366         // When a tab is closed, it is always unselected first.
367         // When a tab is unselected, another tab is always selected first.
368         switch (msg) {
369             case UNSELECTED:
370                 break;
371 
372             case LOCATION_CHANGE:
373                 // We only care about location change for the selected tab.
374                 if (Tabs.getInstance().isSelectedTab(tab)) {
375                     resetOptionsMenu();
376                     resetFormAssistPopup();
377                 }
378                 break;
379 
380             case SELECTED:
381                 resetOptionsMenu();
382                 resetFormAssistPopup();
383                 break;
384 
385             case DESKTOP_MODE_CHANGE:
386                 if (Tabs.getInstance().isSelectedTab(tab))
387                     resetOptionsMenu();
388                 break;
389         }
390     }
391 
resetOptionsMenu()392     private void resetOptionsMenu() {
393         if (mInitialized) {
394             invalidateOptionsMenu();
395         }
396     }
397 
resetFormAssistPopup()398     private void resetFormAssistPopup() {
399         if (mInitialized && mFormAssistPopup != null) {
400             mFormAssistPopup.hide();
401         }
402     }
403 
404     /**
405      * Called on tab selection and tab close - return true to allow updating of this activity's
406      * last selected tab.
407      */
saveAsLastSelectedTab(Tab tab)408     protected boolean saveAsLastSelectedTab(Tab tab) {
409         return false;
410     }
411 
refreshChrome()412     public void refreshChrome() {
413     }
414 
invalidateOptionsMenu()415     public void invalidateOptionsMenu() {
416         if (mMenu == null) {
417             return;
418         }
419 
420         onPrepareOptionsMenu(mMenu);
421 
422         super.invalidateOptionsMenu();
423     }
424 
425     @Override
onCreateOptionsMenu(Menu menu)426     public boolean onCreateOptionsMenu(Menu menu) {
427         mMenu = menu;
428 
429         MenuInflater inflater = getMenuInflater();
430         inflater.inflate(R.menu.gecko_app_menu, mMenu);
431         return true;
432     }
433 
434     @Override
getMenuInflater()435     public MenuInflater getMenuInflater() {
436         return new GeckoMenuInflater(this);
437     }
438 
getMenuPanel()439     public MenuPanel getMenuPanel() {
440         if (mMenuPanel == null || mMenu == null) {
441             onCreatePanelMenu(Window.FEATURE_OPTIONS_PANEL, null);
442             invalidateOptionsMenu();
443         }
444         return mMenuPanel;
445     }
446 
447     @Override
onMenuItemClick(MenuItem item)448     public boolean onMenuItemClick(MenuItem item) {
449         return onOptionsItemSelected(item);
450     }
451 
452     @Override
onMenuItemLongClick(MenuItem item)453     public boolean onMenuItemLongClick(MenuItem item) {
454         return false;
455     }
456 
457     @Override
openMenu()458     public void openMenu() {
459         openOptionsMenu();
460     }
461 
462     @Override
showMenu(final View menu)463     public void showMenu(final View menu) {
464         // On devices using the custom menu, focus is cleared from the menu when its tapped.
465         // Close and then reshow it to avoid these issues. See bug 794581 and bug 968182.
466         closeMenu();
467 
468         // Post the reshow code back to the UI thread to avoid some optimizations Android
469         // has put in place for menus that hide/show themselves quickly. See bug 985400.
470         ThreadUtils.postToUiThread(new Runnable() {
471             @Override
472             public void run() {
473                 mMenuPanel.removeAllViews();
474                 mMenuPanel.addView(menu);
475                 openOptionsMenu();
476             }
477         });
478     }
479 
480     @Override
closeMenu()481     public void closeMenu() {
482         closeOptionsMenu();
483     }
484 
485     @Override
onCreatePanelView(int featureId)486     public View onCreatePanelView(int featureId) {
487         if (featureId == Window.FEATURE_OPTIONS_PANEL) {
488             if (mMenuPanel == null) {
489                 mMenuPanel = new MenuPanel(this, null);
490             } else {
491                 // Prepare the panel every time before showing the menu.
492                 onPreparePanel(featureId, mMenuPanel, mMenu);
493             }
494 
495             return mMenuPanel;
496         }
497 
498         return super.onCreatePanelView(featureId);
499     }
500 
501     @Override
onCreatePanelMenu(int featureId, Menu menu)502     public boolean onCreatePanelMenu(int featureId, Menu menu) {
503         if (featureId == Window.FEATURE_OPTIONS_PANEL) {
504             if (mMenuPanel == null) {
505                 mMenuPanel = (MenuPanel) onCreatePanelView(featureId);
506             }
507 
508             GeckoMenu gMenu = new GeckoMenu(this, null);
509             gMenu.setCallback(this);
510             gMenu.setMenuPresenter(this);
511             menu = gMenu;
512             mMenuPanel.addView(gMenu);
513 
514             return onCreateOptionsMenu(menu);
515         }
516 
517         return super.onCreatePanelMenu(featureId, menu);
518     }
519 
520     @Override
onPreparePanel(int featureId, View view, Menu menu)521     public boolean onPreparePanel(int featureId, View view, Menu menu) {
522         if (featureId == Window.FEATURE_OPTIONS_PANEL) {
523             return onPrepareOptionsMenu(menu);
524         }
525 
526         return super.onPreparePanel(featureId, view, menu);
527     }
528 
529     @Override
onMenuOpened(int featureId, Menu menu)530     public boolean onMenuOpened(int featureId, Menu menu) {
531         // exit full-screen mode whenever the menu is opened
532         if (mIsFullscreen) {
533             EventDispatcher.getInstance().dispatch("FullScreen:Exit", null);
534         }
535 
536         if (featureId == Window.FEATURE_OPTIONS_PANEL) {
537             if (mMenu == null) {
538                 // getMenuPanel() will force the creation of the menu as well
539                 MenuPanel panel = getMenuPanel();
540                 onPreparePanel(featureId, panel, mMenu);
541             }
542 
543             // Scroll custom menu to the top
544             if (mMenuPanel != null)
545                 mMenuPanel.scrollTo(0, 0);
546 
547             return true;
548         }
549 
550         return super.onMenuOpened(featureId, menu);
551     }
552 
553     @Override
onOptionsItemSelected(MenuItem item)554     public boolean onOptionsItemSelected(MenuItem item) {
555         if (item.getItemId() == R.id.quit) {
556             // Make sure the Guest Browsing notification goes away when we quit.
557             GuestSession.hideNotification(this);
558 
559             final SharedPreferences prefs = getSharedPreferencesForProfile();
560             final Set<String> clearSet = PrefUtils.getStringSet(
561                     prefs, ClearOnShutdownPref.PREF, new HashSet<String>());
562 
563             final GeckoBundle clearObj = new GeckoBundle(clearSet.size());
564             for (final String clear : clearSet) {
565                 clearObj.putBoolean(clear, true);
566             }
567 
568             final GeckoBundle res = new GeckoBundle(2);
569             res.putBundle("sanitize", clearObj);
570 
571             // If the user wants to clear open tabs, or else has opted out of session
572             // restore and does want to clear history, we also want to prevent the current
573             // session info from being saved.
574             if (clearObj.containsKey("private.data.openTabs")) {
575                 res.putBoolean("dontSaveSession", true);
576             } else if (clearObj.containsKey("private.data.history")) {
577 
578                 final String sessionRestore =
579                         getSessionRestorePreference(getSharedPreferences());
580                 res.putBoolean("dontSaveSession", "quit".equals(sessionRestore));
581             }
582 
583             EventDispatcher.getInstance().dispatch("Browser:Quit", res);
584 
585             // We don't call shutdown here because this creates a race condition which
586             // can cause the clearing of private data to fail. Instead, we shut down the
587             // UI only after we're done sanitizing.
588             return true;
589         }
590 
591         return super.onOptionsItemSelected(item);
592     }
593 
594     @Override
onOptionsMenuClosed(Menu menu)595     public void onOptionsMenuClosed(Menu menu) {
596         mMenuPanel.removeAllViews();
597         mMenuPanel.addView((GeckoMenu) mMenu);
598     }
599 
600     @Override
onKeyDown(int keyCode, KeyEvent event)601     public boolean onKeyDown(int keyCode, KeyEvent event) {
602         // Handle hardware menu key presses separately so that we can show a custom menu in some cases.
603         if (keyCode == KeyEvent.KEYCODE_MENU) {
604             openOptionsMenu();
605             return true;
606         }
607 
608         return super.onKeyDown(keyCode, event);
609     }
610 
611     @Override
onSaveInstanceState(Bundle outState)612     protected void onSaveInstanceState(Bundle outState) {
613         synchronized (this) {
614             mPrivateBrowsingSessionOutdated = true;
615         }
616         // Through the GeckoActivityMonitor, this will flush tabs if the whole
617         // application is going into the background.
618         super.onSaveInstanceState(outState);
619 
620         // If on the other hand we're merely switching to a different activity
621         // within our app, we need to trigger a tabs flush ourselves.
622         if (!isApplicationInBackground()) {
623             EventDispatcher.getInstance().dispatch("Session:FlushTabs", null);
624         }
625         synchronized (this) {
626             if (GeckoThread.isRunning() && mPrivateBrowsingSessionOutdated) {
627                 try {
628                     wait(MAX_PRIVATE_TABS_UPDATE_WAIT_MSEC);
629                 } catch (final InterruptedException e) { }
630             }
631             outState.putString(SAVED_STATE_PRIVATE_SESSION, mPrivateBrowsingSession);
632         }
633 
634         outState.putBoolean(SAVED_STATE_IN_BACKGROUND, isApplicationInBackground());
635     }
636 
addTab()637     public void addTab() { }
638 
addPrivateTab()639     public void addPrivateTab() { }
640 
showNormalTabs()641     public void showNormalTabs() { }
642 
showPrivateTabs()643     public void showPrivateTabs() { }
644 
hideTabs()645     public void hideTabs() { }
646 
647     /**
648      * Close the tab UI indirectly (not as the result of a direct user
649      * action).  This does not force the UI to close; for example in Firefox
650      * tablet mode it will remain open unless the user explicitly closes it.
651      *
652      * @return True if the tab UI was hidden.
653      */
autoHideTabs()654     public boolean autoHideTabs() { return false; }
655 
656     @Override
handleMessage(final String event, final GeckoBundle message, final EventCallback callback)657     public void handleMessage(final String event, final GeckoBundle message,
658                               final EventCallback callback) {
659         if (event.equals("Gecko:Ready")) {
660             mGeckoReadyStartupTimer.stop();
661 
662             // This method is already running on the background thread, so we
663             // know that mHealthRecorder will exist. That doesn't stop us being
664             // paranoid.
665             // This method is cheap, so don't spawn a new runnable.
666             final HealthRecorder rec = mHealthRecorder;
667             if (rec != null) {
668               rec.recordGeckoStartupTime(mGeckoReadyStartupTimer.getElapsed());
669             }
670 
671             ((GeckoApplication) getApplicationContext()).onDelayedStartup();
672 
673             // Reset the crash loop counter if we remain alive for at least half a minute.
674             ThreadUtils.postDelayedToBackgroundThread(new Runnable() {
675                 @Override
676                 public void run() {
677                     getSharedPreferences().edit().putInt(PREFS_CRASHED_COUNT, 0).apply();
678                 }
679             }, STARTUP_PHASE_DURATION_MS);
680 
681         } else if (event.equals("Gecko:CorruptAPK")) {
682             showCorruptAPKError();
683             if (!isFinishing()) {
684                 finish();
685             }
686 
687         } else if ("Accessibility:Ready".equals(event)) {
688             GeckoAccessibility.updateAccessibilitySettings(this);
689 
690         } else if ("Accessibility:Event".equals(event)) {
691             GeckoAccessibility.sendAccessibilityEvent(mLayerView, message);
692 
693         } else if ("Contact:Add".equals(event)) {
694             final String email = message.getString("email");
695             final String phone = message.getString("phone");
696             if (email != null) {
697                 Uri contactUri = Uri.parse(email);
698                 Intent i = new Intent(ContactsContract.Intents.SHOW_OR_CREATE_CONTACT, contactUri);
699                 startActivity(i);
700             } else if (phone != null) {
701                 Uri contactUri = Uri.parse(phone);
702                 Intent i = new Intent(ContactsContract.Intents.SHOW_OR_CREATE_CONTACT, contactUri);
703                 startActivity(i);
704             } else {
705                 // something went wrong.
706                 Log.e(LOGTAG, "Received Contact:Add message with no email nor phone number");
707             }
708 
709         } else if ("DevToolsAuth:Scan".equals(event)) {
710             DevToolsAuthHelper.scan(this, callback);
711 
712         } else if ("DOMFullScreen:Start".equals(event)) {
713             mIsFullscreen = true;
714 
715         } else if ("DOMFullScreen:Stop".equals(event)) {
716             mIsFullscreen = false;
717 
718         } else if ("Locale:Set".equals(event)) {
719             setLocale(message.getString("locale"));
720 
721         } else if ("Permissions:Data".equals(event)) {
722             final GeckoBundle[] permissions = message.getBundleArray("permissions");
723             showSiteSettingsDialog(permissions);
724 
725         } else if ("PrivateBrowsing:Data".equals(event)) {
726             synchronized (this) {
727                 if (!message.getBoolean("noChange", false)) {
728                     mPrivateBrowsingSession = message.getString("session");
729                 }
730                 mPrivateBrowsingSessionOutdated = false;
731                 notifyAll();
732             }
733 
734         } else if ("SystemUI:Visibility".equals(event)) {
735             if (message.getBoolean("visible", true)) {
736                 mMainLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE);
737             } else {
738                 mMainLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE);
739             }
740 
741         } else if ("ToggleChrome:Focus".equals(event)) {
742             focusChrome();
743 
744         } else if ("ToggleChrome:Hide".equals(event)) {
745             toggleChrome(false);
746 
747         } else if ("ToggleChrome:Show".equals(event)) {
748             toggleChrome(true);
749 
750         } else if ("Update:Check".equals(event)) {
751             UpdateServiceHelper.checkForUpdate(this);
752 
753         } else if ("Update:Download".equals(event)) {
754             UpdateServiceHelper.downloadUpdate(this);
755 
756         } else if ("Update:Install".equals(event)) {
757             UpdateServiceHelper.applyUpdate(this);
758 
759         } else if ("Mma:reader_available".equals(event)) {
760             MmaDelegate.track(READER_AVAILABLE);
761 
762         } else if ("Mma:web_save_media".equals(event) || "Mma:web_save_image".equals(event)) {
763             MmaDelegate.track(DOWNLOAD_MEDIA_SAVED_IMAGE);
764 
765         }
766 
767     }
768 
769     /**
770      * To get a presenter which will response for text-selection. In preMarshmallow Android we want
771      * to provide different UI action when user select a text. Text-selection class will uses this
772      * presenter to trigger UI updating.
773      *
774      * @return a presenter which handle showing/hiding of action mode UI. return *null* if this
775      * activity doesn't handle any text-selection event.
776      */
getTextSelectPresenter()777     protected ActionModePresenter getTextSelectPresenter() {
778         return null;
779     }
780 
781     /**
782      * @param permissions
783      *        Array of JSON objects to represent site permissions.
784      *        Example: { type: "offline-app", setting: "Store Offline Data", value: "Allow" }
785      */
showSiteSettingsDialog(final GeckoBundle[] permissions)786     private void showSiteSettingsDialog(final GeckoBundle[] permissions) {
787         final AlertDialog.Builder builder = new AlertDialog.Builder(this);
788         builder.setTitle(R.string.site_settings_title);
789 
790         final ArrayList<HashMap<String, String>> itemList =
791                 new ArrayList<HashMap<String, String>>();
792         for (final GeckoBundle permObj : permissions) {
793             final HashMap<String, String> map = new HashMap<String, String>();
794             map.put("setting", permObj.getString("setting"));
795             map.put("value", permObj.getString("value"));
796             itemList.add(map);
797         }
798 
799         // setMultiChoiceItems doesn't support using an adapter, so we're creating a hack with
800         // setSingleChoiceItems and changing the choiceMode below when we create the dialog
801         builder.setSingleChoiceItems(new SimpleAdapter(
802             GeckoApp.this,
803             itemList,
804             R.layout.site_setting_item,
805             new String[] { "setting", "value" },
806             new int[] { R.id.setting, R.id.value }
807             ), -1, new DialogInterface.OnClickListener() {
808                 @Override
809                 public void onClick(DialogInterface dialog, int id) { }
810             });
811 
812         builder.setPositiveButton(R.string.site_settings_clear, new DialogInterface.OnClickListener() {
813             @Override
814             public void onClick(DialogInterface dialog, int id) {
815                 ListView listView = ((AlertDialog) dialog).getListView();
816                 SparseBooleanArray checkedItemPositions = listView.getCheckedItemPositions();
817 
818                 // An array of the indices of the permissions we want to clear.
819                 final ArrayList<Integer> permissionsToClear = new ArrayList<>();
820                 for (int i = 0; i < checkedItemPositions.size(); i++) {
821                     if (checkedItemPositions.valueAt(i)) {
822                         permissionsToClear.add(checkedItemPositions.keyAt(i));
823                     }
824                 }
825 
826                 final GeckoBundle data = new GeckoBundle(1);
827                 data.putIntArray("permissions", permissionsToClear);
828                 EventDispatcher.getInstance().dispatch("Permissions:Clear", data);
829             }
830         });
831 
832         builder.setNegativeButton(R.string.site_settings_cancel, new DialogInterface.OnClickListener() {
833             @Override
834             public void onClick(DialogInterface dialog, int id) {
835                 dialog.cancel();
836             }
837         });
838 
839         AlertDialog dialog = builder.create();
840         dialog.show();
841 
842         final ListView listView = dialog.getListView();
843         if (listView != null) {
844             listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
845         }
846 
847         final Button clearButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE);
848         clearButton.setEnabled(false);
849 
850         dialog.getListView().setOnItemClickListener(new AdapterView.OnItemClickListener() {
851             @Override
852             public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {
853                 if (listView.getCheckedItemCount() == 0) {
854                     clearButton.setEnabled(false);
855                 } else {
856                     clearButton.setEnabled(true);
857                 }
858             }
859         });
860     }
861 
getBitmapSampleSize(BitmapFactory.Options options, int idealWidth, int idealHeight)862     private int getBitmapSampleSize(BitmapFactory.Options options, int idealWidth, int idealHeight) {
863         int width = options.outWidth;
864         int height = options.outHeight;
865         int inSampleSize = 1;
866         if (height > idealHeight || width > idealWidth) {
867             if (width > height) {
868                 inSampleSize = Math.round((float)height / idealHeight);
869             } else {
870                 inSampleSize = Math.round((float)width / idealWidth);
871             }
872         }
873         return inSampleSize;
874     }
875 
876     @Override // GeckoSession.ContentDelegate
onTitleChange(final GeckoSession session, final String title)877     public void onTitleChange(final GeckoSession session, final String title) {
878     }
879 
880     @Override // GeckoSession.ContentDelegate
onFocusRequest(final GeckoSession session)881     public void onFocusRequest(final GeckoSession session) {
882     }
883 
884     @Override // GeckoSession.ContentDelegate
onCloseRequest(final GeckoSession session)885     public void onCloseRequest(final GeckoSession session) {
886     }
887 
888     @Override // GeckoSession.ContentDelegate
onFullScreen(final GeckoSession session, final boolean fullScreen)889     public void onFullScreen(final GeckoSession session, final boolean fullScreen) {
890         if (fullScreen) {
891             SnackbarBuilder.builder(this)
892                     .message(R.string.fullscreen_warning)
893                     .duration(Snackbar.LENGTH_LONG).buildAndShow();
894         }
895         ThreadUtils.assertOnUiThread();
896         ActivityUtils.setFullScreen(this, fullScreen);
897     }
898 
899     @Override
onContextMenu(final GeckoSession session, final int screenX, final int screenY, final String uri, final String elementSrc)900     public void onContextMenu(final GeckoSession session, final int screenX,
901                               final int screenY, final String uri,
902                               final String elementSrc) {
903     }
904 
setFullScreen(final boolean fullscreen)905     protected void setFullScreen(final boolean fullscreen) {
906         ThreadUtils.postToUiThread(new Runnable() {
907             @Override
908             public void run() {
909                 onFullScreen(mLayerView.getSession(), fullscreen);
910             }
911         });
912     }
913 
914     /**
915      * Check and start the Java profiler if MOZ_PROFILER_STARTUP env var is specified.
916      **/
earlyStartJavaSampler(SafeIntent intent)917     protected static void earlyStartJavaSampler(SafeIntent intent) {
918         String env = intent.getStringExtra("env0");
919         for (int i = 1; env != null; i++) {
920             if (env.startsWith("MOZ_PROFILER_STARTUP=")) {
921                 if (!env.endsWith("=")) {
922                     GeckoJavaSampler.start(10, 1000);
923                     Log.d(LOGTAG, "Profiling Java on startup");
924                 }
925                 break;
926             }
927             env = intent.getStringExtra("env" + i);
928         }
929     }
930 
931     /**
932      * Called when the activity is first created.
933      *
934      * Here we initialize all of our profile settings, Firefox Health Report,
935      * and other one-shot constructions.
936      **/
937     @Override
onCreate(Bundle savedInstanceState)938     public void onCreate(Bundle savedInstanceState) {
939         // Enable Android Strict Mode for developers' local builds (the "default" channel).
940         if ("default".equals(AppConstants.MOZ_UPDATE_CHANNEL)) {
941             enableStrictMode();
942         }
943 
944         final boolean corruptAPK = GeckoThread.isState(GeckoThread.State.CORRUPT_APK);
945         boolean supported = HardwareUtils.isSupportedSystem();
946         if (supported) {
947             GeckoLoader.loadMozGlue(getApplicationContext());
948             supported = GeckoLoader.neonCompatible();
949         }
950         if (corruptAPK || !supported) {
951             // This build is corrupt or does not support the Android version of the device.
952             // Show an error and finish the app.
953             mIsAbortingAppLaunch = true;
954             super.onCreate(savedInstanceState);
955             if (corruptAPK) {
956                 showCorruptAPKError();
957             } else {
958                 showSDKVersionError();
959             }
960             finish();
961             return;
962         }
963 
964         // The clock starts...now. Better hurry!
965         mJavaUiStartupTimer = new Telemetry.UptimeTimer("FENNEC_STARTUP_TIME_JAVAUI");
966         mGeckoReadyStartupTimer = new Telemetry.UptimeTimer("FENNEC_STARTUP_TIME_GECKOREADY");
967 
968         final SafeIntent intent = new SafeIntent(getIntent());
969 
970         earlyStartJavaSampler(intent);
971 
972         // Workaround for <http://code.google.com/p/android/issues/detail?id=20915>.
973         try {
974             Class.forName("android.os.AsyncTask");
975         } catch (ClassNotFoundException e) { }
976 
977         GeckoAppShell.setScreenOrientationDelegate(this);
978 
979         // Tell Stumbler to register a local broadcast listener to listen for preference intents.
980         // We do this via intents since we can't easily access Stumbler directly,
981         // as it might be compiled outside of Fennec.
982         getApplicationContext().sendBroadcast(
983                 new Intent(INTENT_REGISTER_STUMBLER_LISTENER)
984         );
985 
986         // Did the OS locale change while we were backgrounded? If so,
987         // we need to die so that Gecko will re-init add-ons that touch
988         // the UI.
989         // This is using a sledgehammer to crack a nut, but it'll do for
990         // now.
991         // Our OS locale pref will be detected as invalid after the
992         // restart, and will be propagated to Gecko accordingly, so there's
993         // no need to touch that here.
994         if (BrowserLocaleManager.getInstance().systemLocaleDidChange()) {
995             Log.i(LOGTAG, "System locale changed. Restarting.");
996             finishAndShutdown(/* restart */ true);
997             return;
998         }
999 
1000         if (sAlreadyLoaded) {
1001             // This happens when the GeckoApp activity is destroyed by Android
1002             // without killing the entire application (see Bug 769269).
1003             // In case we have multiple GeckoApp-based activities, this can
1004             // also happen if we're not the first activity to run within a session.
1005             mIsRestoringActivity = true;
1006             Telemetry.addToHistogram("FENNEC_RESTORING_ACTIVITY", 1);
1007 
1008         } else {
1009             final String action = intent.getAction();
1010             final String[] args = GeckoApplication.getDefaultGeckoArgs();
1011             final int flags = ACTION_DEBUG.equals(action) ? GeckoThread.FLAG_DEBUGGING : 0;
1012 
1013             sAlreadyLoaded = true;
1014             GeckoThread.initMainProcess(/* profile */ null, args,
1015                                         intent.getExtras(), flags);
1016 
1017             // Speculatively pre-fetch the profile in the background.
1018             ThreadUtils.postToBackgroundThread(new Runnable() {
1019                 @Override
1020                 public void run() {
1021                     getProfile();
1022                 }
1023             });
1024 
1025             final String uri = getURIFromIntent(intent);
1026             if (!TextUtils.isEmpty(uri)) {
1027                 // Start a speculative connection as soon as Gecko loads.
1028                 GeckoThread.speculativeConnect(uri);
1029             }
1030         }
1031 
1032         // To prevent races, register startup events before launching the Gecko thread.
1033         EventDispatcher.getInstance().registerGeckoThreadListener(this,
1034             "Accessibility:Ready",
1035             "Gecko:Ready",
1036             null);
1037 
1038         EventDispatcher.getInstance().registerUiThreadListener(this,
1039             "Gecko:CorruptAPK",
1040             "Update:Check",
1041             "Update:Download",
1042             "Update:Install",
1043             null);
1044 
1045         GeckoThread.launch();
1046 
1047         Bundle stateBundle = IntentUtils.getBundleExtraSafe(getIntent(), EXTRA_STATE_BUNDLE);
1048         if (stateBundle != null) {
1049             // Use the state bundle if it was given as an intent extra. This is
1050             // only intended to be used internally via Robocop, so a boolean
1051             // is read from a private shared pref to prevent other apps from
1052             // injecting states.
1053             final SharedPreferences prefs = getSharedPreferences();
1054             if (prefs.getBoolean(PREFS_ALLOW_STATE_BUNDLE, false)) {
1055                 prefs.edit().remove(PREFS_ALLOW_STATE_BUNDLE).apply();
1056                 savedInstanceState = stateBundle;
1057             }
1058         } else if (savedInstanceState != null) {
1059             // Bug 896992 - This intent has already been handled; reset the intent.
1060             setIntent(new Intent(Intent.ACTION_MAIN));
1061         }
1062 
1063         super.onCreate(savedInstanceState);
1064 
1065         GeckoScreenOrientation.getInstance().update(getResources().getConfiguration().orientation);
1066 
1067         setContentView(getLayout());
1068 
1069         // Set up Gecko layout.
1070         mRootLayout = (RelativeLayout) findViewById(R.id.root_layout);
1071         mGeckoLayout = (RelativeLayout) findViewById(R.id.gecko_layout);
1072         mMainLayout = (RelativeLayout) findViewById(R.id.main_layout);
1073         mLayerView = (GeckoView) findViewById(R.id.layer_view);
1074 
1075         final GeckoSession session = new GeckoSession();
1076         // If the view already has a session, we need to ensure it is closed.
1077         if (mLayerView.getSession() != null) {
1078             mLayerView.getSession().close();
1079         }
1080         mLayerView.setSession(session);
1081         mLayerView.setOverScrollMode(View.OVER_SCROLL_NEVER);
1082 
1083         session.getSettings().setString(GeckoSessionSettings.CHROME_URI,
1084                                         "chrome://browser/content/browser.xul");
1085         session.setContentDelegate(this);
1086 
1087         GeckoAccessibility.setDelegate(mLayerView);
1088 
1089         getAppEventDispatcher().registerGeckoThreadListener(this,
1090             "Locale:Set",
1091             "PrivateBrowsing:Data",
1092             null);
1093 
1094         getAppEventDispatcher().registerUiThreadListener(this,
1095             "Accessibility:Event",
1096             "Contact:Add",
1097             "DevToolsAuth:Scan",
1098             "DOMFullScreen:Start",
1099             "DOMFullScreen:Stop",
1100             "Mma:reader_available",
1101             "Mma:web_save_image",
1102             "Mma:web_save_media",
1103             "Permissions:Data",
1104             "SystemUI:Visibility",
1105             "ToggleChrome:Focus",
1106             "ToggleChrome:Hide",
1107             "ToggleChrome:Show",
1108             null);
1109 
1110         Tabs.getInstance().attachToContext(this, mLayerView, getAppEventDispatcher());
1111         Tabs.registerOnTabsChangedListener(this);
1112 
1113         // Use global layout state change to kick off additional initialization
1114         mMainLayout.getViewTreeObserver().addOnGlobalLayoutListener(this);
1115 
1116         mTextSelection = TextSelection.Factory.create(mLayerView, getTextSelectPresenter());
1117         mTextSelection.create();
1118 
1119         final Bundle finalSavedInstanceState = savedInstanceState;
1120         ThreadUtils.postToBackgroundThread(new Runnable() {
1121             @Override
1122             public void run() {
1123                 // Determine whether we should restore tabs.
1124                 mLastSessionCrashed = updateCrashedState();
1125                 mShouldRestore = getSessionRestoreState(finalSavedInstanceState);
1126                 if (mShouldRestore && finalSavedInstanceState != null) {
1127                     boolean wasInBackground =
1128                             finalSavedInstanceState.getBoolean(SAVED_STATE_IN_BACKGROUND, false);
1129 
1130                     // Don't log OOM-kills if only one activity was destroyed. (For example
1131                     // from "Don't keep activities" on ICS)
1132                     if (!wasInBackground && !mIsRestoringActivity) {
1133                         Telemetry.addToHistogram("FENNEC_WAS_KILLED", 1);
1134                     }
1135 
1136                     mPrivateBrowsingSession =
1137                             finalSavedInstanceState.getString(SAVED_STATE_PRIVATE_SESSION);
1138                 }
1139 
1140                 // If we are doing a restore, read the session data so we can send it to Gecko later.
1141                 GeckoBundle restoreMessage = null;
1142                 if (!mIsRestoringActivity && mShouldRestore) {
1143                     final boolean isExternalURL = invokedWithExternalURL(getIntentURI(new SafeIntent(getIntent())));
1144                     try {
1145                         // restoreSessionTabs() will create simple tab stubs with the
1146                         // URL and title for each page, but we also need to restore
1147                         // session history. restoreSessionTabs() will inject the IDs
1148                         // of the tab stubs into the JSON data (which holds the session
1149                         // history). This JSON data is then sent to Gecko so session
1150                         // history can be restored for each tab.
1151                         restoreMessage = restoreSessionTabs(isExternalURL, false);
1152                     } catch (SessionRestoreException e) {
1153                         // If mShouldRestore was set to false in restoreSessionTabs(), this means
1154                         // either that we intentionally skipped all tabs read from the session file,
1155                         // or else that the file was syntactically valid, but didn't contain any
1156                         // tabs (e.g. because the user cleared history), therefore we don't need
1157                         // to switch to the backup copy.
1158                         if (mShouldRestore) {
1159                             Log.e(LOGTAG, "An error occurred during restore, switching to backup file", e);
1160                             // To be on the safe side, we will always attempt to restore from the backup
1161                             // copy if we end up here.
1162                             // Since we will also hit this situation regularly during first run though,
1163                             // we'll only report it in telemetry if we failed to restore despite the
1164                             // file existing, which means it's very probably damaged.
1165                             if (getProfile().sessionFileExists()) {
1166                                 Telemetry.addToHistogram("FENNEC_SESSIONSTORE_DAMAGED_SESSION_FILE", 1);
1167                             }
1168                             try {
1169                                 restoreMessage = restoreSessionTabs(isExternalURL, true);
1170                                 Telemetry.addToHistogram("FENNEC_SESSIONSTORE_RESTORING_FROM_BACKUP", 1);
1171                             } catch (SessionRestoreException ex) {
1172                                 if (!mShouldRestore) {
1173                                     // Restoring only "failed" because the backup copy was deliberately empty, too.
1174                                     Telemetry.addToHistogram("FENNEC_SESSIONSTORE_RESTORING_FROM_BACKUP", 1);
1175                                 } else {
1176                                     // Restoring the backup failed, too, so do a normal startup.
1177                                     Log.e(LOGTAG, "An error occurred during restore", ex);
1178                                     mShouldRestore = false;
1179 
1180                                     if (!getSharedPreferencesForProfile().
1181                                             getBoolean(PREFS_IS_FIRST_RUN, true)) {
1182                                         // Except when starting with a fresh profile, we should normally
1183                                         // always have a session file available, even if it might only
1184                                         // contain an empty window.
1185                                         Telemetry.addToHistogram("FENNEC_SESSIONSTORE_ALL_FILES_DAMAGED", 1);
1186                                     }
1187                                 }
1188                             }
1189                         }
1190                     }
1191                 }
1192 
1193                 synchronized (GeckoApp.this) {
1194                     mSessionRestoreParsingFinished = true;
1195                     GeckoApp.this.notifyAll();
1196                 }
1197 
1198                 // If we are doing a restore, send the parsed session data to Gecko.
1199                 if (!mIsRestoringActivity) {
1200                     getAppEventDispatcher().dispatch("Session:Restore", restoreMessage);
1201                 }
1202 
1203                 // Make sure sessionstore.old is either updated or deleted as necessary.
1204                 getProfile().updateSessionFile(mShouldRestore);
1205             }
1206         });
1207 
1208         // Perform background initialization.
1209         ThreadUtils.postToBackgroundThread(new Runnable() {
1210             @Override
1211             public void run() {
1212                 final SharedPreferences prefs = GeckoApp.this.getSharedPreferences();
1213 
1214                 // Wait until now to set this, because we'd rather throw an exception than
1215                 // have a caller of BrowserLocaleManager regress startup.
1216                 final LocaleManager localeManager = BrowserLocaleManager.getInstance();
1217                 localeManager.initialize(getApplicationContext());
1218 
1219                 SessionInformation previousSession = SessionInformation.fromSharedPrefs(prefs);
1220                 if (previousSession.wasKilled()) {
1221                     Telemetry.addToHistogram("FENNEC_WAS_KILLED", 1);
1222                 }
1223 
1224                 SharedPreferences.Editor editor = prefs.edit();
1225                 editor.putBoolean(GeckoAppShell.PREFS_OOM_EXCEPTION, false);
1226 
1227                 // Put a flag to check if we got a normal `onSaveInstanceState`
1228                 // on exit, or if we were suddenly killed (crash or native OOM).
1229                 editor.putBoolean(GeckoApp.PREFS_WAS_STOPPED, false);
1230 
1231                 editor.apply();
1232 
1233                 // The lifecycle of mHealthRecorder is "shortly after onCreate"
1234                 // through "onDestroy" -- essentially the same as the lifecycle
1235                 // of the activity itself.
1236                 final String profilePath = getProfile().getDir().getAbsolutePath();
1237                 final EventDispatcher dispatcher = getAppEventDispatcher();
1238 
1239                 // This is the locale prior to fixing it up.
1240                 final Locale osLocale = Locale.getDefault();
1241 
1242                 // Both of these are Java-format locale strings: "en_US", not "en-US".
1243                 final String osLocaleString = osLocale.getLanguage() + "_" + osLocale.getCountry();
1244                 String appLocaleString = localeManager.getAndApplyPersistedLocale(GeckoApp.this);
1245                 Log.d(LOGTAG, "OS locale is " + osLocaleString + ", app locale is " + appLocaleString);
1246 
1247                 if (appLocaleString == null) {
1248                     appLocaleString = osLocaleString;
1249                 }
1250 
1251                 mHealthRecorder = GeckoApp.this.createHealthRecorder(GeckoApp.this,
1252                                                                      profilePath,
1253                                                                      dispatcher,
1254                                                                      osLocaleString,
1255                                                                      appLocaleString,
1256                                                                      previousSession);
1257 
1258                 final String uiLocale = appLocaleString;
1259                 ThreadUtils.postToUiThread(new Runnable() {
1260                     @Override
1261                     public void run() {
1262                         GeckoApp.this.onLocaleReady(uiLocale);
1263                     }
1264                 });
1265 
1266                 // We use per-profile prefs here, because we're tracking against
1267                 // a Gecko pref. The same applies to the locale switcher!
1268                 BrowserLocaleManager.storeAndNotifyOSLocale(getSharedPreferencesForProfile(), osLocale);
1269             }
1270         });
1271     }
1272 
1273     @Override
onStart()1274     public void onStart() {
1275         super.onStart();
1276         if (mIsAbortingAppLaunch) {
1277             return;
1278         }
1279 
1280         mWasFirstTabShownAfterActivityUnhidden = false; // onStart indicates we were hidden.
1281     }
1282 
1283     @Override
onStop()1284     protected void onStop() {
1285         super.onStop();
1286         // Overriding here is not necessary, but we do this so we don't
1287         // forget to add the abort if we override this method later.
1288         if (mIsAbortingAppLaunch) {
1289             return;
1290         }
1291     }
1292 
1293 
1294     /**
1295      * Derived classes may call this if they require something to be done *after* they've
1296      * done their onStop() handling.
1297      */
onAfterStop()1298     protected void onAfterStop() {
1299         final SharedPreferences sharedPrefs = getSharedPreferencesForProfile();
1300         if (sharedPrefs.getBoolean(PREFS_IS_FIRST_RUN, true)) {
1301             sharedPrefs.edit().putBoolean(PREFS_IS_FIRST_RUN, false).apply();
1302         }
1303     }
1304 
1305     /**
1306      * At this point, the resource system and the rest of the browser are
1307      * aware of the locale.
1308      *
1309      * Now we can display strings!
1310      *
1311      * You can think of this as being something like a second phase of onCreate,
1312      * where you can do string-related operations. Use this in place of embedding
1313      * strings in view XML.
1314      *
1315      * By contrast, onConfigurationChanged does some locale operations, but is in
1316      * response to device changes.
1317      */
1318     @Override
onLocaleReady(final String locale)1319     public void onLocaleReady(final String locale) {
1320         if (!ThreadUtils.isOnUiThread()) {
1321             throw new RuntimeException("onLocaleReady must always be called from the UI thread.");
1322         }
1323 
1324         final Locale loc = Locales.parseLocaleCode(locale);
1325         if (loc.equals(mLastLocale)) {
1326             Log.d(LOGTAG, "New locale same as old; onLocaleReady has nothing to do.");
1327         }
1328         BrowserLocaleManager.getInstance().updateConfiguration(GeckoApp.this, loc);
1329         ViewUtil.setLayoutDirection(getWindow().getDecorView(), loc);
1330         refreshChrome();
1331 
1332         // The URL bar hint needs to be populated.
1333         TextView urlBar = (TextView) findViewById(R.id.url_bar_title);
1334         if (urlBar != null) {
1335             final String hint = getResources().getString(R.string.url_bar_default_text);
1336             urlBar.setHint(hint);
1337         } else {
1338             Log.d(LOGTAG, "No URL bar in GeckoApp. Not loading localized hint string.");
1339         }
1340 
1341         mLastLocale = loc;
1342 
1343         // Allow onConfigurationChanged to take care of the rest.
1344         // We don't call this.onConfigurationChanged, because (a) that does
1345         // work that's unnecessary after this locale action, and (b) it can
1346         // cause a loop! See Bug 1011008, Comment 12.
1347         super.onConfigurationChanged(getResources().getConfiguration());
1348     }
1349 
initializeChrome()1350     protected void initializeChrome() {
1351         mDoorHangerPopup = new DoorHangerPopup(this, getAppEventDispatcher());
1352         mDoorHangerPopup.setOnVisibilityChangeListener(this);
1353         mFormAssistPopup = (FormAssistPopup) findViewById(R.id.form_assist_popup);
1354         mFormAssistPopup.create(mLayerView);
1355     }
1356 
1357     @Override
onDoorHangerShow()1358     public void onDoorHangerShow() {
1359         final View overlay = getDoorhangerOverlay();
1360         if (overlay != null) {
1361             final Animator alphaAnimator = ObjectAnimator.ofFloat(overlay, "alpha", 1);
1362             alphaAnimator.setDuration(250);
1363 
1364             alphaAnimator.start();
1365         }
1366     }
1367 
1368     @Override
onDoorHangerHide()1369     public void onDoorHangerHide() {
1370         final View overlay = getDoorhangerOverlay();
1371         if (overlay != null) {
1372             final Animator alphaAnimator = ObjectAnimator.ofFloat(overlay, "alpha", 0);
1373             alphaAnimator.setDuration(200);
1374 
1375             alphaAnimator.start();
1376         }
1377     }
1378 
1379     /**
1380      * Loads the initial tab at Fennec startup. If we don't restore tabs, this
1381      * tab will be about:home, or the homepage if the user has set one.
1382      * If we've temporarily disabled restoring to break out of a crash loop, we'll show
1383      * the Recent Tabs folder of the Combined History panel, so the user can manually
1384      * restore tabs as needed.
1385      * If we restore tabs, we don't need to create a new tab, unless launch intent specify action
1386      * to be #android.Intent.ACTION_VIEW, which is launched from widget to create a new tab.
1387      */
loadStartupTab(final int flags, String action)1388     protected void loadStartupTab(final int flags, String action) {
1389         if (!mShouldRestore || Intent.ACTION_VIEW.equals(action)) {
1390             if (mLastSessionCrashed) {
1391                 // The Recent Tabs panel no longer exists, but BrowserApp will redirect us
1392                 // to the Recent Tabs folder of the Combined History panel.
1393                 Tabs.getInstance().loadUrl(AboutPages.getURLForBuiltinPanelType(PanelType.DEPRECATED_RECENT_TABS), flags);
1394             } else {
1395                 Tabs.getInstance().loadUrl(Tabs.getHomepageForStartupTab(this), flags);
1396             }
1397         }
1398     }
1399 
1400     /**
1401      * Loads the initial tab at Fennec startup. This tab will load with the given
1402      * external URL. If that URL is invalid, a startup tab will be loaded.
1403      *
1404      * @param url    External URL to load.
1405      * @param intent External intent whose extras modify the request
1406      * @param flags  Flags used to load the load
1407      */
loadStartupTab(final String url, final SafeIntent intent, final int flags)1408     protected void loadStartupTab(final String url, final SafeIntent intent, final int flags) {
1409         // Invalid url
1410         if (url == null) {
1411             loadStartupTab(flags, intent.getAction());
1412             return;
1413         }
1414 
1415         Tabs.getInstance().loadUrlWithIntentExtras(url, intent, flags);
1416     }
1417 
getIntentURI(SafeIntent intent)1418     protected String getIntentURI(SafeIntent intent) {
1419         final String passedUri;
1420         final String uri = getURIFromIntent(intent);
1421 
1422         if (!TextUtils.isEmpty(uri)) {
1423             passedUri = uri;
1424         } else {
1425             passedUri = null;
1426         }
1427         return passedUri;
1428     }
1429 
invokedWithExternalURL(String uri)1430     private boolean invokedWithExternalURL(String uri) {
1431         return uri != null && !AboutPages.isAboutHome(uri);
1432     }
1433 
getNewTabFlags()1434     protected int getNewTabFlags() {
1435         final boolean isFirstTab = !mWasFirstTabShownAfterActivityUnhidden;
1436 
1437         final SafeIntent intent = new SafeIntent(getIntent());
1438         final String action = intent.getAction();
1439 
1440         int flags = Tabs.LOADURL_NEW_TAB | Tabs.LOADURL_USER_ENTERED | Tabs.LOADURL_EXTERNAL;
1441         if (ACTION_HOMESCREEN_SHORTCUT.equals(action)) {
1442             flags |= Tabs.LOADURL_PINNED;
1443         }
1444         if (isFirstTab) {
1445             flags |= Tabs.LOADURL_FIRST_AFTER_ACTIVITY_UNHIDDEN;
1446         }
1447 
1448         return flags;
1449     }
1450 
initialize()1451     private void initialize() {
1452         mInitialized = true;
1453 
1454         mWasFirstTabShownAfterActivityUnhidden = true; // Reset since we'll be loading a tab.
1455 
1456         final SafeIntent intent = new SafeIntent(getIntent());
1457         final String action = intent.getAction();
1458 
1459         final String passedUri = getIntentURI(intent);
1460 
1461         final boolean intentHasURL = passedUri != null;
1462         final boolean isAboutHomeURL = intentHasURL && AboutPages.isDefaultHomePage(passedUri);
1463         final boolean isAssistIntent = Intent.ACTION_ASSIST.equals(action);
1464         final boolean needsNewForegroundTab = intentHasURL || isAssistIntent;
1465 
1466         // Start migrating as early as possible, can do this in
1467         // parallel with Gecko load.
1468         checkMigrateProfile();
1469 
1470         initializeChrome();
1471 
1472         // We need to wait here because mShouldRestore can revert back to
1473         // false if a parsing error occurs and the startup tab we load
1474         // depends on whether we restore tabs or not.
1475         synchronized (this) {
1476             while (!mSessionRestoreParsingFinished) {
1477                 try {
1478                     wait();
1479                 } catch (final InterruptedException e) {
1480                     // Ignore and wait again.
1481                 }
1482             }
1483         }
1484 
1485         if (mIsRestoringActivity && hasGeckoTab(intent)) {
1486             Tabs.getInstance().notifyListeners(null, Tabs.TabEvents.RESTORED);
1487             handleSelectTabIntent(intent);
1488         // External URLs and new tab from widget should always be loaded regardless of whether Gecko is
1489         // already running.
1490         } else if (needsNewForegroundTab) {
1491             // Restore tabs before opening an external URL so that the new tab
1492             // is animated properly.
1493             Tabs.getInstance().notifyListeners(null, Tabs.TabEvents.RESTORED);
1494             processActionViewIntent(new Runnable() {
1495                 @Override
1496                 public void run() {
1497                     if (isAssistIntent) {
1498                         Tabs.getInstance().addTab(Tabs.LOADURL_START_EDITING | Tabs.LOADURL_EXTERNAL);
1499                     } else if (isAboutHomeURL) {
1500                         // respect the user preferences for about:home from external intent calls
1501                         loadStartupTab(Tabs.LOADURL_NEW_TAB, action);
1502                     } else {
1503                         final int flags = getNewTabFlags();
1504                         loadStartupTab(passedUri, intent, flags);
1505                     }
1506                 }
1507             });
1508         } else {
1509             if (!mIsRestoringActivity) {
1510                 loadStartupTab(Tabs.LOADURL_NEW_TAB, action);
1511             }
1512 
1513             Tabs.getInstance().notifyListeners(null, Tabs.TabEvents.RESTORED);
1514 
1515             processTabQueue();
1516         }
1517 
1518         recordStartupActionTelemetry(passedUri, action);
1519 
1520         // Check if launched from data reporting notification.
1521         if (ACTION_LAUNCH_SETTINGS.equals(action)) {
1522             Intent settingsIntent = new Intent(GeckoApp.this, GeckoPreferences.class);
1523             // Copy extras.
1524             settingsIntent.putExtras(intent.getUnsafe());
1525             startActivity(settingsIntent);
1526         }
1527 
1528         mPromptService = new PromptService(this, getAppEventDispatcher());
1529 
1530         // Trigger the completion of the telemetry timer that wraps activity startup,
1531         // then grab the duration to give to FHR.
1532         mJavaUiStartupTimer.stop();
1533         final long javaDuration = mJavaUiStartupTimer.getElapsed();
1534 
1535         ThreadUtils.getBackgroundHandler().postDelayed(new Runnable() {
1536             @Override
1537             public void run() {
1538                 final HealthRecorder rec = mHealthRecorder;
1539                 if (rec != null) {
1540                     rec.recordJavaStartupTime(javaDuration);
1541                 }
1542             }
1543         }, 50);
1544 
1545         final int updateServiceDelay = 30 * 1000;
1546         ThreadUtils.getBackgroundHandler().postDelayed(new Runnable() {
1547             @Override
1548             public void run() {
1549                 UpdateServiceHelper.registerForUpdates(GeckoAppShell.getApplicationContext());
1550             }
1551         }, updateServiceDelay);
1552 
1553         if (mIsRestoringActivity) {
1554             Tab selectedTab = Tabs.getInstance().getSelectedTab();
1555             if (selectedTab != null) {
1556                 Tabs.getInstance().notifyListeners(selectedTab, Tabs.TabEvents.SELECTED);
1557             }
1558         }
1559     }
1560 
1561     @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
1562     @Override
onGlobalLayout()1563     public void onGlobalLayout() {
1564         if (Versions.preJB) {
1565             mMainLayout.getViewTreeObserver().removeGlobalOnLayoutListener(this);
1566         } else {
1567             mMainLayout.getViewTreeObserver().removeOnGlobalLayoutListener(this);
1568         }
1569         if (!mInitialized) {
1570             initialize();
1571         }
1572     }
1573 
processActionViewIntent(final Runnable openTabsRunnable)1574     protected void processActionViewIntent(final Runnable openTabsRunnable) {
1575         // We need to ensure that if we receive a VIEW action and there are tabs queued then the
1576         // site loaded from the intent is on top (last loaded) and selected with all other tabs
1577         // being opened behind it. We process the tab queue first and request a callback from the JS - the
1578         // listener will open the url from the intent as normal when the tab queue has been processed.
1579         ThreadUtils.postToBackgroundThread(new Runnable() {
1580             @Override
1581             public void run() {
1582                 if (TabQueueHelper.TAB_QUEUE_ENABLED && TabQueueHelper.shouldOpenTabQueueUrls(GeckoApp.this)) {
1583 
1584                     getAppEventDispatcher().registerUiThreadListener(new BundleEventListener() {
1585                         @Override
1586                         public void handleMessage(String event, GeckoBundle message, EventCallback callback) {
1587                             if ("Tabs:TabsOpened".equals(event)) {
1588                                 getAppEventDispatcher().unregisterUiThreadListener(this, "Tabs:TabsOpened");
1589                                 openTabsRunnable.run();
1590                             }
1591                         }
1592                     }, "Tabs:TabsOpened");
1593                     TabQueueHelper.openQueuedUrls(GeckoApp.this, getProfile(), TabQueueHelper.FILE_NAME, true);
1594                 } else {
1595                     openTabsRunnable.run();
1596                 }
1597             }
1598         });
1599     }
1600 
1601     @WorkerThread
restoreSessionTabs(final boolean isExternalURL, boolean useBackup)1602     private GeckoBundle restoreSessionTabs(final boolean isExternalURL, boolean useBackup)
1603             throws SessionRestoreException {
1604         String sessionString = getProfile().readSessionFile(useBackup);
1605         if (sessionString == null) {
1606             throw new SessionRestoreException("Could not read from session file");
1607         }
1608 
1609         // If we are doing an OOM restore, parse the session data and
1610         // stub the restored tabs immediately. This allows the UI to be
1611         // updated before Gecko has restored.
1612         final JSONArray tabs = new JSONArray();
1613         final JSONObject windowObject = new JSONObject();
1614         final boolean sessionDataValid;
1615 
1616         LastSessionParser parser = new LastSessionParser(tabs, windowObject, isExternalURL);
1617 
1618         if (mPrivateBrowsingSession == null) {
1619             sessionDataValid = parser.parse(sessionString);
1620         } else {
1621             sessionDataValid = parser.parse(sessionString, mPrivateBrowsingSession);
1622         }
1623 
1624         if (tabs.length() > 0) {
1625             try {
1626                 // Update all parent tab IDs ...
1627                 parser.updateParentId(tabs);
1628                 windowObject.put("tabs", tabs);
1629                 // ... and for recently closed tabs as well (if we've got any).
1630                 final JSONArray closedTabs = windowObject.optJSONArray("closedTabs");
1631                 parser.updateParentId(closedTabs);
1632                 windowObject.putOpt("closedTabs", closedTabs);
1633 
1634                 sessionString = new JSONObject().put(
1635                         "windows", new JSONArray().put(windowObject)).toString();
1636             } catch (final JSONException e) {
1637                 throw new SessionRestoreException(e);
1638             }
1639         } else {
1640             if (parser.allTabsSkipped() || sessionDataValid) {
1641                 // If we intentionally skipped all tabs we've read from the session file, we
1642                 // set mShouldRestore back to false at this point already, so the calling code
1643                 // can infer that the exception wasn't due to a damaged session store file.
1644                 // The same applies if the session file was syntactically valid and
1645                 // simply didn't contain any tabs.
1646                 mShouldRestore = false;
1647             }
1648             throw new SessionRestoreException("No tabs could be read from session file");
1649         }
1650 
1651         final GeckoBundle restoreData = new GeckoBundle(1);
1652         restoreData.putString("sessionString", sessionString);
1653         return restoreData;
1654     }
1655 
1656     @RobocopTarget
getAppEventDispatcher()1657     public @NonNull EventDispatcher getAppEventDispatcher() {
1658         if (mLayerView == null) {
1659             throw new IllegalStateException("Must not call getAppEventDispatcher() until after onCreate()");
1660         }
1661 
1662         return mLayerView.getEventDispatcher();
1663     }
1664 
getProfile()1665     protected static GeckoProfile getProfile() {
1666         return GeckoThread.getActiveProfile();
1667     }
1668 
1669     /**
1670      * Check whether we've crashed during the last browsing session.
1671      *
1672      * @return True if the crash reporter ran after the last session.
1673      */
updateCrashedState()1674     protected boolean updateCrashedState() {
1675         try {
1676             File crashFlag = new File(GeckoProfileDirectories.getMozillaDirectory(this), "CRASHED");
1677             if (crashFlag.exists() && crashFlag.delete()) {
1678                 // Set the flag that indicates we were stopped as expected, as
1679                 // the crash reporter has run, so it is not a silent OOM crash.
1680                 getSharedPreferences().edit().putBoolean(PREFS_WAS_STOPPED, true).apply();
1681                 return true;
1682             }
1683         } catch (NoMozillaDirectoryException e) {
1684             // If we can't access the Mozilla directory, we're in trouble anyway.
1685             Log.e(LOGTAG, "Cannot read crash flag: ", e);
1686         }
1687         return false;
1688     }
1689 
1690     /**
1691      * Determine whether the session should be restored.
1692      *
1693      * @param savedInstanceState Saved instance state given to the activity
1694      * @return                   Whether to restore
1695      */
getSessionRestoreState(Bundle savedInstanceState)1696     protected boolean getSessionRestoreState(Bundle savedInstanceState) {
1697         final SharedPreferences prefs = getSharedPreferences();
1698         boolean shouldRestore = false;
1699 
1700         final int versionCode = getVersionCode();
1701         if (getSessionRestoreResumeOnce(prefs)) {
1702             shouldRestore = true;
1703         } else if (mLastSessionCrashed) {
1704             if (incrementCrashCount(prefs) <= getSessionStoreMaxCrashResumes(prefs) &&
1705                     getSessionRestoreAfterCrashPreference(prefs)) {
1706                 shouldRestore = true;
1707             } else {
1708                 shouldRestore = false;
1709             }
1710         } else if (prefs.getInt(PREFS_VERSION_CODE, 0) != versionCode) {
1711             // If the version has changed, the user has done an upgrade, so restore
1712             // previous tabs.
1713             prefs.edit().putInt(PREFS_VERSION_CODE, versionCode).apply();
1714             shouldRestore = true;
1715         } else if (savedInstanceState != null ||
1716                    getSessionRestorePreference(prefs).equals("always") ||
1717                    getRestartFromIntent()) {
1718             // We're coming back from a background kill by the OS, the user
1719             // has chosen to always restore, or we restarted.
1720             shouldRestore = true;
1721         }
1722 
1723         return shouldRestore;
1724     }
1725 
getSessionRestoreResumeOnce(SharedPreferences prefs)1726     private boolean getSessionRestoreResumeOnce(SharedPreferences prefs) {
1727         boolean resumeOnce = prefs.getBoolean(GeckoPreferences.PREFS_RESTORE_SESSION_ONCE, false);
1728         if (resumeOnce) {
1729             prefs.edit().putBoolean(GeckoPreferences.PREFS_RESTORE_SESSION_ONCE, false).apply();
1730         }
1731         return resumeOnce;
1732     }
1733 
incrementCrashCount(SharedPreferences prefs)1734     private int incrementCrashCount(SharedPreferences prefs) {
1735         final int crashCount = getSuccessiveCrashesCount(prefs) + 1;
1736         prefs.edit().putInt(PREFS_CRASHED_COUNT, crashCount).apply();
1737         return crashCount;
1738     }
1739 
getSuccessiveCrashesCount(SharedPreferences prefs)1740     private int getSuccessiveCrashesCount(SharedPreferences prefs) {
1741         return prefs.getInt(PREFS_CRASHED_COUNT, 0);
1742     }
1743 
getSessionStoreMaxCrashResumes(SharedPreferences prefs)1744     private int getSessionStoreMaxCrashResumes(SharedPreferences prefs) {
1745         return prefs.getInt(GeckoPreferences.PREFS_RESTORE_SESSION_MAX_CRASH_RESUMES, 1);
1746     }
1747 
getSessionRestoreAfterCrashPreference(SharedPreferences prefs)1748     private boolean getSessionRestoreAfterCrashPreference(SharedPreferences prefs) {
1749         return prefs.getBoolean(GeckoPreferences.PREFS_RESTORE_SESSION_FROM_CRASH, true);
1750     }
1751 
getSessionRestorePreference(SharedPreferences prefs)1752     private String getSessionRestorePreference(SharedPreferences prefs) {
1753         return prefs.getString(GeckoPreferences.PREFS_RESTORE_SESSION, "always");
1754     }
1755 
getRestartFromIntent()1756     private boolean getRestartFromIntent() {
1757         return IntentUtils.getBooleanExtraSafe(getIntent(), "didRestart", false);
1758     }
1759 
1760     /**
1761      * Enable Android StrictMode checks.
1762      * http://developer.android.com/reference/android/os/StrictMode.html
1763      */
enableStrictMode()1764     private void enableStrictMode() {
1765         Log.d(LOGTAG, "Enabling Android StrictMode");
1766 
1767         StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
1768                                   .detectAll()
1769                                   .penaltyLog()
1770                                   // Match Android's default configuration - which we use on
1771                                   // automation builds, including release - for network access.
1772                                   .penaltyDeathOnNetwork()
1773                                   .build());
1774 
1775         StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
1776                                .detectAll()
1777                                .penaltyLog()
1778                                .build());
1779     }
1780 
1781     @Override
onNewIntent(Intent externalIntent)1782     protected void onNewIntent(Intent externalIntent) {
1783         final SafeIntent intent = new SafeIntent(externalIntent);
1784         final String action = intent.getAction();
1785 
1786         if (ACTION_SHUTDOWN.equals(action)) {
1787             PrefsHelper.getPref(GeckoPreferences.PREFS_SHUTDOWN_INTENT,
1788                                 new PrefsHelper.PrefHandlerBase() {
1789                 @Override public void prefValue(String pref, boolean value) {
1790                     if (value) {
1791                         mShutdownOnDestroy = true;
1792                         GeckoThread.forceQuit();
1793                     }
1794                 }
1795             });
1796             return;
1797         }
1798 
1799         final boolean isFirstTab = !mWasFirstTabShownAfterActivityUnhidden;
1800         mWasFirstTabShownAfterActivityUnhidden = true; // Reset since we'll be loading a tab.
1801 
1802         // if we were previously OOM killed, we can end up here when launching
1803         // from external shortcuts, so set this as the intent for initialization
1804         if (!mInitialized) {
1805             setIntent(externalIntent);
1806             return;
1807         }
1808 
1809         final String uri = getURIFromIntent(intent);
1810         final String passedUri;
1811         if (!TextUtils.isEmpty(uri)) {
1812             passedUri = uri;
1813         } else {
1814             passedUri = null;
1815         }
1816 
1817         if (hasGeckoTab(intent)) {
1818             // This also covers ACTION_SWITCH_TAB.
1819             handleSelectTabIntent(intent);
1820         } else if (ACTION_LOAD.equals(action)) {
1821             Tabs.getInstance().loadUrl(intent.getDataString());
1822         } else if (Intent.ACTION_VIEW.equals(action)) {
1823             processActionViewIntent(new Runnable() {
1824                 @Override
1825                 public void run() {
1826                     final String url = intent.getDataString();
1827                     int flags = Tabs.LOADURL_NEW_TAB | Tabs.LOADURL_USER_ENTERED | Tabs.LOADURL_EXTERNAL;
1828                     if (isFirstTab) {
1829                         flags |= Tabs.LOADURL_FIRST_AFTER_ACTIVITY_UNHIDDEN;
1830                     }
1831                     Tabs.getInstance().loadUrlWithIntentExtras(url, intent, flags);
1832                 }
1833             });
1834         } else if (Intent.ACTION_ASSIST.equals(action)) {
1835             Tabs.getInstance().addTab(Tabs.LOADURL_START_EDITING | Tabs.LOADURL_EXTERNAL);
1836             autoHideTabs();
1837         } else if (ACTION_HOMESCREEN_SHORTCUT.equals(action)) {
1838             final GeckoBundle data = new GeckoBundle(2);
1839             data.putString("uri", uri);
1840             data.putString("flags", "OPEN_SWITCHTAB");
1841             getAppEventDispatcher().dispatch("Tab:OpenUri", data);
1842         } else if (Intent.ACTION_SEARCH.equals(action)) {
1843             final GeckoBundle data = new GeckoBundle(2);
1844             data.putString("uri", uri);
1845             data.putString("flags", "OPEN_NEWTAB");
1846             getAppEventDispatcher().dispatch("Tab:OpenUri", data);
1847         } else if (NotificationHelper.HELPER_BROADCAST_ACTION.equals(action)) {
1848             NotificationHelper.getInstance(getApplicationContext()).handleNotificationIntent(intent);
1849         } else if (ACTION_LAUNCH_SETTINGS.equals(action)) {
1850             // Check if launched from data reporting notification.
1851             Intent settingsIntent = new Intent(GeckoApp.this, GeckoPreferences.class);
1852             // Copy extras.
1853             settingsIntent.putExtras(intent.getUnsafe());
1854             startActivity(settingsIntent);
1855         }
1856 
1857         recordStartupActionTelemetry(passedUri, action);
1858     }
1859 
1860     /**
1861      * Check whether an intent with tab switch extras refers to a tab that
1862      * is actually existing at the moment.
1863      *
1864      * @param intent The intent to be checked.
1865      * @return True if the tab specified in the intent is existing in our Tabs list.
1866      */
hasGeckoTab(SafeIntent intent)1867     protected boolean hasGeckoTab(SafeIntent intent) {
1868         final int tabId = intent.getIntExtra(INTENT_EXTRA_TAB_ID, INVALID_TAB_ID);
1869         final String intentSessionUUID = intent.getStringExtra(INTENT_EXTRA_SESSION_UUID);
1870         final Tab tabToCheck = Tabs.getInstance().getTab(tabId);
1871 
1872         // We only care about comparing session UUIDs if one was specified in the intent.
1873         // Otherwise, we just try matching the tab ID with one of our open tabs.
1874         return tabToCheck != null && (!intent.hasExtra(INTENT_EXTRA_SESSION_UUID) ||
1875                 GeckoApplication.getSessionUUID().equals(intentSessionUUID));
1876     }
1877 
handleSelectTabIntent(SafeIntent intent)1878     protected void handleSelectTabIntent(SafeIntent intent) {
1879         final int tabId = intent.getIntExtra(INTENT_EXTRA_TAB_ID, INVALID_TAB_ID);
1880         Tabs.getInstance().selectTab(tabId);
1881     }
1882 
1883     /**
1884      * Handles getting a URI from an intent in a way that is backwards-
1885      * compatible with our previous implementations.
1886      */
getURIFromIntent(SafeIntent intent)1887     protected String getURIFromIntent(SafeIntent intent) {
1888         final String action = intent.getAction();
1889         if (ACTION_ALERT_CALLBACK.equals(action) ||
1890                 NotificationHelper.HELPER_BROADCAST_ACTION.equals(action)) {
1891             return null;
1892         }
1893 
1894         return intent.getDataString();
1895     }
1896 
getOrientation()1897     protected int getOrientation() {
1898         return GeckoScreenOrientation.getInstance().getAndroidOrientation();
1899     }
1900 
1901     @WrapForJNI(calledFrom = "gecko")
launchOrBringToFront()1902     public static void launchOrBringToFront() {
1903         final Activity activity = GeckoActivityMonitor.getInstance().getCurrentActivity();
1904 
1905         // Check that BrowserApp is not the current foreground activity.
1906         if (activity instanceof BrowserApp && ((GeckoApp) activity).foregrounded) {
1907             return;
1908         }
1909 
1910         Intent intent = new Intent(Intent.ACTION_MAIN);
1911         intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
1912                         Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
1913         intent.setClassName(AppConstants.ANDROID_PACKAGE_NAME,
1914                             AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS);
1915         GeckoAppShell.getApplicationContext().startActivity(intent);
1916     }
1917 
1918     @Override
onResume()1919     public void onResume()
1920     {
1921         // After an onPause, the activity is back in the foreground.
1922         // Undo whatever we did in onPause.
1923         super.onResume();
1924 
1925         if (mIsAbortingAppLaunch) {
1926             return;
1927         }
1928 
1929         foregrounded = true;
1930 
1931         GeckoAppShell.setScreenOrientationDelegate(this);
1932 
1933         int newOrientation = getResources().getConfiguration().orientation;
1934         if (GeckoScreenOrientation.getInstance().update(newOrientation)) {
1935             refreshChrome();
1936         }
1937 
1938         // We use two times: a pseudo-unique wall-clock time to identify the
1939         // current session across power cycles, and the elapsed realtime to
1940         // track the duration of the session.
1941         final long now = System.currentTimeMillis();
1942         final long realTime = android.os.SystemClock.elapsedRealtime();
1943 
1944         ThreadUtils.postToBackgroundThread(new Runnable() {
1945             @Override
1946             public void run() {
1947                 // Now construct the new session on HealthRecorder's behalf. We do this here
1948                 // so it can benefit from a single near-startup prefs commit.
1949                 SessionInformation currentSession = new SessionInformation(now, realTime);
1950 
1951                 SharedPreferences prefs = GeckoApp.this.getSharedPreferences();
1952                 SharedPreferences.Editor editor = prefs.edit();
1953                 editor.putBoolean(GeckoApp.PREFS_WAS_STOPPED, false);
1954 
1955                 if (!mLastSessionCrashed) {
1956                     // The last session terminated normally,
1957                     // so we can reset the count of successive crashes.
1958                     editor.putInt(GeckoApp.PREFS_CRASHED_COUNT, 0);
1959                 }
1960 
1961                 currentSession.recordBegin(editor);
1962                 editor.apply();
1963 
1964                 final HealthRecorder rec = mHealthRecorder;
1965                 if (rec != null) {
1966                     rec.setCurrentSession(currentSession);
1967                     rec.processDelayed();
1968                 } else {
1969                     Log.w(LOGTAG, "Can't record session: rec is null.");
1970                 }
1971             }
1972         });
1973 
1974         Restrictions.update(this);
1975     }
1976 
1977     @Override
onWindowFocusChanged(boolean hasFocus)1978     public void onWindowFocusChanged(boolean hasFocus) {
1979         super.onWindowFocusChanged(hasFocus);
1980 
1981         if (!mWindowFocusInitialized && hasFocus) {
1982             mWindowFocusInitialized = true;
1983             // XXX our editor tests require the GeckoView to have focus to pass, so we have to
1984             // manually shift focus to the GeckoView. requestFocus apparently doesn't work at
1985             // this stage of starting up, so we have to unset and reset the focusability.
1986             mLayerView.setFocusable(false);
1987             mLayerView.setFocusable(true);
1988             mLayerView.setFocusableInTouchMode(true);
1989             getWindow().setBackgroundDrawable(null);
1990         }
1991     }
1992 
1993     @Override
onPause()1994     public void onPause()
1995     {
1996         if (mIsAbortingAppLaunch) {
1997             super.onPause();
1998             return;
1999         }
2000 
2001         foregrounded = false;
2002 
2003         final HealthRecorder rec = mHealthRecorder;
2004         final Context context = this;
2005 
2006         // In some way it's sad that Android will trigger StrictMode warnings
2007         // here as the whole point is to save to disk while the activity is not
2008         // interacting with the user.
2009         ThreadUtils.postToBackgroundThread(new Runnable() {
2010             @Override
2011             public void run() {
2012                 SharedPreferences prefs = GeckoApp.this.getSharedPreferences();
2013                 SharedPreferences.Editor editor = prefs.edit();
2014                 editor.putBoolean(GeckoApp.PREFS_WAS_STOPPED, true);
2015                 if (rec != null) {
2016                     rec.recordSessionEnd("P", editor);
2017                 }
2018 
2019                 // onPause might in fact be called even after a crash, but in that case the
2020                 // crash reporter will record this fact for us and we'll pick it up in onCreate.
2021                 mLastSessionCrashed = false;
2022 
2023                 // If we haven't done it before, cleanup any old files in our old temp dir
2024                 if (prefs.getBoolean(GeckoApp.PREFS_CLEANUP_TEMP_FILES, true)) {
2025                     File tempDir = GeckoLoader.getGREDir(GeckoApp.this);
2026                     FileUtils.delTree(tempDir, new FileUtils.NameAndAgeFilter(null, ONE_DAY_MS), false);
2027 
2028                     editor.putBoolean(GeckoApp.PREFS_CLEANUP_TEMP_FILES, false);
2029                 }
2030 
2031                 editor.apply();
2032             }
2033         });
2034 
2035         GeckoAppShell.setScreenOrientationDelegate(null);
2036 
2037         super.onPause();
2038     }
2039 
2040     @Override
onRestart()2041     public void onRestart() {
2042         if (mIsAbortingAppLaunch) {
2043             super.onRestart();
2044             return;
2045         }
2046 
2047         // Faster on main thread with an async apply().
2048         final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads();
2049         try {
2050             SharedPreferences.Editor editor = GeckoApp.this.getSharedPreferences().edit();
2051             editor.putBoolean(GeckoApp.PREFS_WAS_STOPPED, false);
2052             editor.apply();
2053         } finally {
2054             StrictMode.setThreadPolicy(savedPolicy);
2055         }
2056 
2057         super.onRestart();
2058     }
2059 
2060     @Override
onDestroy()2061     public void onDestroy() {
2062         if (mIsAbortingAppLaunch) {
2063             // This build does not support the Android version of the device:
2064             // We did not initialize anything, so skip cleaning up.
2065             super.onDestroy();
2066             return;
2067         }
2068 
2069         if (mFormAssistPopup != null) {
2070             mFormAssistPopup.destroy();
2071             mFormAssistPopup = null;
2072         }
2073 
2074         if (mDoorHangerPopup != null) {
2075             mDoorHangerPopup.destroy();
2076             mDoorHangerPopup = null;
2077         }
2078 
2079         if (mTextSelection != null) {
2080             mTextSelection.destroy();
2081             mTextSelection = null;
2082         }
2083 
2084         EventDispatcher.getInstance().unregisterGeckoThreadListener(this,
2085             "Accessibility:Ready",
2086             "Gecko:Ready",
2087             null);
2088 
2089         EventDispatcher.getInstance().unregisterUiThreadListener(this,
2090             "Gecko:CorruptAPK",
2091             "Update:Check",
2092             "Update:Download",
2093             "Update:Install",
2094             null);
2095 
2096         getAppEventDispatcher().unregisterGeckoThreadListener(this,
2097             "Locale:Set",
2098             "PrivateBrowsing:Data",
2099             null);
2100 
2101         getAppEventDispatcher().unregisterUiThreadListener(this,
2102             "Accessibility:Event",
2103             "Contact:Add",
2104             "DevToolsAuth:Scan",
2105             "DOMFullScreen:Start",
2106             "DOMFullScreen:Stop",
2107             "Mma:reader_available",
2108             "Mma:web_save_image",
2109             "Mma:web_save_media",
2110             "Permissions:Data",
2111             "SystemUI:Visibility",
2112             "ToggleChrome:Focus",
2113             "ToggleChrome:Hide",
2114             "ToggleChrome:Show",
2115             null);
2116 
2117         if (mPromptService != null) {
2118             mPromptService.destroy();
2119             mPromptService = null;
2120         }
2121 
2122         final HealthRecorder rec = mHealthRecorder;
2123         mHealthRecorder = null;
2124         if (rec != null && rec.isEnabled()) {
2125             // Closing a HealthRecorder could incur a write.
2126             ThreadUtils.postToBackgroundThread(new Runnable() {
2127                 @Override
2128                 public void run() {
2129                     rec.close(GeckoApp.this);
2130                 }
2131             });
2132         }
2133 
2134         super.onDestroy();
2135 
2136         Tabs.unregisterOnTabsChangedListener(this);
2137         Tabs.getInstance().detachFromContext();
2138 
2139         if (mShutdownOnDestroy) {
2140             GeckoApplication.shutdown(!mRestartOnShutdown ? null : new Intent(
2141                     Intent.ACTION_MAIN, /* uri */ null, getApplicationContext(), getClass()));
2142         }
2143     }
2144 
showSDKVersionError()2145     public void showSDKVersionError() {
2146         final String message = getString(R.string.unsupported_sdk_version,
2147                 HardwareUtils.getRealAbi(), Integer.toString(Build.VERSION.SDK_INT));
2148         Toast.makeText(this, message, Toast.LENGTH_LONG).show();
2149     }
2150 
showCorruptAPKError()2151     private void showCorruptAPKError() {
2152         Toast.makeText(this, getString(R.string.corrupt_apk), Toast.LENGTH_LONG).show();
2153     }
2154 
2155     // Get a temporary directory, may return null
getTempDirectory(@onNull Context context)2156     public static File getTempDirectory(@NonNull Context context) {
2157         return context.getApplicationContext().getExternalFilesDir("temp");
2158     }
2159 
2160     // Delete any files in our temporary directory
deleteTempFiles(Context context)2161     public static void deleteTempFiles(Context context) {
2162         File dir = getTempDirectory(context);
2163         if (dir == null)
2164             return;
2165         File[] files = dir.listFiles();
2166         if (files == null)
2167             return;
2168         for (File file : files) {
2169             file.delete();
2170         }
2171     }
2172 
2173     @Override
onConfigurationChanged(Configuration newConfig)2174     public void onConfigurationChanged(Configuration newConfig) {
2175         Log.d(LOGTAG, "onConfigurationChanged: " + newConfig.locale);
2176 
2177         final LocaleManager localeManager = BrowserLocaleManager.getInstance();
2178         final Locale changed = localeManager.onSystemConfigurationChanged(this, getResources(), newConfig, mLastLocale);
2179         if (changed != null) {
2180             onLocaleChanged(Locales.getLanguageTag(changed));
2181         }
2182 
2183         // onConfigurationChanged is not called for 180 degree orientation changes,
2184         // we will miss such rotations and the screen orientation will not be
2185         // updated.
2186         if (GeckoScreenOrientation.getInstance().update(newConfig.orientation)) {
2187             if (mFormAssistPopup != null)
2188                 mFormAssistPopup.hide();
2189             refreshChrome();
2190         }
2191         super.onConfigurationChanged(newConfig);
2192     }
2193 
getContentProcessName()2194     public String getContentProcessName() {
2195         return AppConstants.MOZ_CHILD_PROCESS_NAME;
2196     }
2197 
addEnvToIntent(Intent intent)2198     public void addEnvToIntent(Intent intent) {
2199         Map<String, String> envMap = System.getenv();
2200         Set<Map.Entry<String, String>> envSet = envMap.entrySet();
2201         Iterator<Map.Entry<String, String>> envIter = envSet.iterator();
2202         int c = 0;
2203         while (envIter.hasNext()) {
2204             Map.Entry<String, String> entry = envIter.next();
2205             intent.putExtra("env" + c, entry.getKey() + "="
2206                             + entry.getValue());
2207             c++;
2208         }
2209     }
2210 
2211     @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
finishAndShutdown(final boolean restart)2212     protected void finishAndShutdown(final boolean restart) {
2213         ThreadUtils.assertOnUiThread();
2214 
2215         mShutdownOnDestroy = true;
2216         mRestartOnShutdown = restart;
2217 
2218         // Shut down the activity and then Gecko.
2219         if (!isFinishing() && (Versions.preJBMR1 || !isDestroyed())) {
2220             finish();
2221         }
2222     }
2223 
checkMigrateProfile()2224     private void checkMigrateProfile() {
2225         final File profileDir = getProfile().getDir();
2226 
2227         if (profileDir != null) {
2228             ThreadUtils.postToBackgroundThread(new Runnable() {
2229                 @Override
2230                 public void run() {
2231                     Handler handler = new Handler();
2232                     handler.postDelayed(new DeferredCleanupTask(), CLEANUP_DEFERRAL_SECONDS * 1000);
2233                 }
2234             });
2235         }
2236     }
2237 
2238     private static class DeferredCleanupTask implements Runnable {
2239         // The cleanup-version setting is recorded to avoid repeating the same
2240         // tasks on subsequent startups; CURRENT_CLEANUP_VERSION may be updated
2241         // if we need to do additional cleanup for future Gecko versions.
2242 
2243         private static final String CLEANUP_VERSION = "cleanup-version";
2244         private static final int CURRENT_CLEANUP_VERSION = 1;
2245 
2246         @Override
run()2247         public void run() {
2248             final Context context = GeckoAppShell.getApplicationContext();
2249             long cleanupVersion = GeckoSharedPrefs.forApp(context).getInt(CLEANUP_VERSION, 0);
2250 
2251             if (cleanupVersion < 1) {
2252                 // Reduce device storage footprint by removing .ttf files from
2253                 // the res/fonts directory: we no longer need to copy our
2254                 // bundled fonts out of the APK in order to use them.
2255                 // See https://bugzilla.mozilla.org/show_bug.cgi?id=878674.
2256                 File dir = new File("res/fonts");
2257                 if (dir.exists() && dir.isDirectory()) {
2258                     for (File file : dir.listFiles()) {
2259                         if (file.isFile() && file.getName().endsWith(".ttf")) {
2260                             file.delete();
2261                         }
2262                     }
2263                     if (!dir.delete()) {
2264                         Log.w(LOGTAG, "unable to delete res/fonts directory (not empty?)");
2265                     }
2266                 }
2267             }
2268 
2269             // Additional cleanup needed for future versions would go here
2270 
2271             if (cleanupVersion != CURRENT_CLEANUP_VERSION) {
2272                 SharedPreferences.Editor editor = GeckoSharedPrefs.forApp(context).edit();
2273                 editor.putInt(CLEANUP_VERSION, CURRENT_CLEANUP_VERSION);
2274                 editor.apply();
2275             }
2276         }
2277     }
2278 
onDone()2279     protected void onDone() {
2280         moveTaskToBack(true);
2281     }
2282 
2283     @Override
onBackPressed()2284     public void onBackPressed() {
2285         if (getSupportFragmentManager().getBackStackEntryCount() > 0) {
2286             super.onBackPressed();
2287             return;
2288         }
2289 
2290         if (autoHideTabs()) {
2291             return;
2292         }
2293 
2294         if (mDoorHangerPopup != null && mDoorHangerPopup.isShowing()) {
2295             mDoorHangerPopup.dismiss();
2296             return;
2297         }
2298 
2299         if (mIsFullscreen) {
2300             EventDispatcher.getInstance().dispatch("FullScreen:Exit", null);
2301             return;
2302         }
2303 
2304         final Tabs tabs = Tabs.getInstance();
2305         final Tab tab = tabs.getSelectedTab();
2306         if (tab == null) {
2307             onDone();
2308             return;
2309         }
2310 
2311         // Give Gecko a chance to handle the back press first, then fallback to the Java UI.
2312         getAppEventDispatcher().dispatch("Browser:OnBackPressed", null, new EventCallback() {
2313             @Override
2314             public void sendSuccess(final Object response) {
2315                 if (!((GeckoBundle) response).getBoolean("handled")) {
2316                     // Default behavior is Gecko didn't prevent.
2317                     onDefault();
2318                 }
2319             }
2320 
2321             @Override
2322             public void sendError(final Object error) {
2323                 // Default behavior is Gecko didn't prevent, via failure.
2324                 onDefault();
2325             }
2326 
2327             private void onDefault() {
2328                 ThreadUtils.assertOnUiThread();
2329 
2330                 if (tab.doBack()) {
2331                     return;
2332                 }
2333 
2334                 if (tab.isExternal()) {
2335                     onDone();
2336                     Tab nextSelectedTab = Tabs.getInstance().getNextTab(tab);
2337                     // Closing the tab will select the next tab. There's no need to unzombify it
2338                     // if we're exiting.
2339                     if (nextSelectedTab != null) {
2340                         final GeckoBundle data = new GeckoBundle(1);
2341                         data.putInt("nextSelectedTabId", nextSelectedTab.getId());
2342                         EventDispatcher.getInstance().dispatch("Tab:KeepZombified", data);
2343                     }
2344                     tabs.closeTab(tab);
2345                     return;
2346                 }
2347 
2348                 final int parentId = tab.getParentId();
2349                 final Tab parent = tabs.getTab(parentId);
2350                 if (parent != null) {
2351                     // The back button should always return to the parent (not a sibling).
2352                     tabs.closeTab(tab, parent);
2353                     return;
2354                 }
2355 
2356                 onDone();
2357             }
2358         });
2359     }
2360 
2361     @Override
onActivityResult(int requestCode, int resultCode, Intent data)2362     protected void onActivityResult(int requestCode, int resultCode, Intent data) {
2363         if (!ActivityHandlerHelper.handleActivityResult(requestCode, resultCode, data)) {
2364             super.onActivityResult(requestCode, resultCode, data);
2365         }
2366     }
2367 
2368     @Override
onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults)2369     public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
2370         Permissions.onRequestPermissionsResult(this, permissions, grantResults);
2371     }
2372 
2373     public static class MainLayout extends RelativeLayout {
2374         private TouchEventInterceptor mTouchEventInterceptor;
2375         private MotionEventInterceptor mMotionEventInterceptor;
2376 
MainLayout(Context context, AttributeSet attrs)2377         public MainLayout(Context context, AttributeSet attrs) {
2378             super(context, attrs);
2379         }
2380 
2381         @Override
onLayout(boolean changed, int left, int top, int right, int bottom)2382         protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
2383             super.onLayout(changed, left, top, right, bottom);
2384         }
2385 
setTouchEventInterceptor(TouchEventInterceptor interceptor)2386         public void setTouchEventInterceptor(TouchEventInterceptor interceptor) {
2387             mTouchEventInterceptor = interceptor;
2388         }
2389 
setMotionEventInterceptor(MotionEventInterceptor interceptor)2390         public void setMotionEventInterceptor(MotionEventInterceptor interceptor) {
2391             mMotionEventInterceptor = interceptor;
2392         }
2393 
2394         @Override
onInterceptTouchEvent(MotionEvent event)2395         public boolean onInterceptTouchEvent(MotionEvent event) {
2396             if (mTouchEventInterceptor != null && mTouchEventInterceptor.onInterceptTouchEvent(this, event)) {
2397                 return true;
2398             }
2399             return super.onInterceptTouchEvent(event);
2400         }
2401 
2402         @Override
onTouchEvent(MotionEvent event)2403         public boolean onTouchEvent(MotionEvent event) {
2404             if (mTouchEventInterceptor != null && mTouchEventInterceptor.onTouch(this, event)) {
2405                 return true;
2406             }
2407             return super.onTouchEvent(event);
2408         }
2409 
2410         @Override
onGenericMotionEvent(MotionEvent event)2411         public boolean onGenericMotionEvent(MotionEvent event) {
2412             if (mMotionEventInterceptor != null && mMotionEventInterceptor.onInterceptMotionEvent(this, event)) {
2413                 return true;
2414             }
2415             return super.onGenericMotionEvent(event);
2416         }
2417 
2418         @Override
setDrawingCacheEnabled(boolean enabled)2419         public void setDrawingCacheEnabled(boolean enabled) {
2420             // Instead of setting drawing cache in the view itself, we simply
2421             // enable drawing caching on its children. This is mainly used in
2422             // animations (see PropertyAnimator)
2423             super.setChildrenDrawnWithCacheEnabled(enabled);
2424         }
2425     }
2426 
getVersionCode()2427     private int getVersionCode() {
2428         int versionCode = 0;
2429         try {
2430             versionCode = getPackageManager().getPackageInfo(getPackageName(), 0).versionCode;
2431         } catch (NameNotFoundException e) {
2432             Log.wtf(LOGTAG, getPackageName() + " not found", e);
2433         }
2434         return versionCode;
2435     }
2436 
2437     // FHR reason code for a session end prior to a restart for a
2438     // locale change.
2439     private static final String SESSION_END_LOCALE_CHANGED = "L";
2440 
2441     /**
2442      * This exists so that a locale can be applied in two places: when saved
2443      * in a nested activity, and then again when we get back up to GeckoApp.
2444      *
2445      * GeckoApp needs to do a bunch more stuff than, say, GeckoPreferences.
2446      */
onLocaleChanged(final String locale)2447     protected void onLocaleChanged(final String locale) {
2448         final boolean startNewSession = true;
2449         final boolean shouldRestart = false;
2450 
2451         // If the HealthRecorder is not yet initialized (unlikely), the locale change won't
2452         // trigger a session transition and subsequent events will be recorded in an environment
2453         // with the wrong locale.
2454         final HealthRecorder rec = mHealthRecorder;
2455         if (rec != null) {
2456             rec.onAppLocaleChanged(locale);
2457             rec.onEnvironmentChanged(startNewSession, SESSION_END_LOCALE_CHANGED);
2458         }
2459 
2460         final Runnable runnable = new Runnable() {
2461             @Override
2462             public void run() {
2463                 if (!ThreadUtils.isOnUiThread()) {
2464                     ThreadUtils.postToUiThread(this);
2465                     return;
2466                 }
2467                 if (!shouldRestart) {
2468                     GeckoApp.this.onLocaleReady(locale);
2469                 } else {
2470                     finishAndShutdown(/* restart */ true);
2471                 }
2472             }
2473         };
2474 
2475         if (!shouldRestart) {
2476             ThreadUtils.postToUiThread(runnable);
2477         } else {
2478             // Do this in the background so that the health recorder has its
2479             // time to finish.
2480             ThreadUtils.postToBackgroundThread(runnable);
2481         }
2482     }
2483 
2484     /**
2485      * Use BrowserLocaleManager to change our persisted and current locales,
2486      * and poke the system to tell it of our changed state.
2487      */
setLocale(final String locale)2488     protected void setLocale(final String locale) {
2489         if (locale == null) {
2490             return;
2491         }
2492 
2493         final String resultant = BrowserLocaleManager.getInstance().setSelectedLocale(this, locale);
2494         if (resultant == null) {
2495             return;
2496         }
2497 
2498         onLocaleChanged(resultant);
2499     }
2500 
createHealthRecorder(final Context context, final String profilePath, final EventDispatcher dispatcher, final String osLocale, final String appLocale, final SessionInformation previousSession)2501     protected HealthRecorder createHealthRecorder(final Context context,
2502                                                   final String profilePath,
2503                                                   final EventDispatcher dispatcher,
2504                                                   final String osLocale,
2505                                                   final String appLocale,
2506                                                   final SessionInformation previousSession) {
2507         // GeckoApp does not need to record any health information - return a stub.
2508         return new StubbedHealthRecorder();
2509     }
2510 
recordStartupActionTelemetry(final String passedURL, final String action)2511     protected void recordStartupActionTelemetry(final String passedURL, final String action) {
2512     }
2513 
getGeckoView()2514     public GeckoView getGeckoView() {
2515         return mLayerView;
2516     }
2517 
2518     @Override
setRequestedOrientationForCurrentActivity(int requestedActivityInfoOrientation)2519     public boolean setRequestedOrientationForCurrentActivity(int requestedActivityInfoOrientation) {
2520         // We want to support the Screen Orientation API, and it always makes sense to lock the
2521         // orientation of a browser Activity, so we support locking.
2522         if (getRequestedOrientation() == requestedActivityInfoOrientation) {
2523             return false;
2524         }
2525         setRequestedOrientation(requestedActivityInfoOrientation);
2526         return true;
2527     }
2528 }
2529