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