/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ package org.mozilla.gecko; import org.mozilla.gecko.AppConstants.Versions; import org.mozilla.gecko.GeckoProfileDirectories.NoMozillaDirectoryException; import org.mozilla.gecko.db.BrowserDB; import org.mozilla.gecko.db.UrlAnnotations; import org.mozilla.gecko.gfx.BitmapUtils; import org.mozilla.gecko.gfx.FullScreenState; import org.mozilla.gecko.gfx.LayerView; import org.mozilla.gecko.health.HealthRecorder; import org.mozilla.gecko.health.SessionInformation; import org.mozilla.gecko.health.StubbedHealthRecorder; import org.mozilla.gecko.home.HomeConfig.PanelType; import org.mozilla.gecko.icons.IconCallback; import org.mozilla.gecko.icons.IconResponse; import org.mozilla.gecko.icons.Icons; import org.mozilla.gecko.menu.GeckoMenu; import org.mozilla.gecko.menu.GeckoMenuInflater; import org.mozilla.gecko.menu.MenuPanel; import org.mozilla.gecko.notifications.NotificationClient; import org.mozilla.gecko.notifications.NotificationHelper; import org.mozilla.gecko.util.IntentUtils; import org.mozilla.gecko.mozglue.SafeIntent; import org.mozilla.gecko.mozglue.GeckoLoader; import org.mozilla.gecko.permissions.Permissions; import org.mozilla.gecko.preferences.ClearOnShutdownPref; import org.mozilla.gecko.preferences.GeckoPreferences; import org.mozilla.gecko.prompts.PromptService; import org.mozilla.gecko.restrictions.Restrictions; import org.mozilla.gecko.tabqueue.TabQueueHelper; import org.mozilla.gecko.text.FloatingToolbarTextSelection; import org.mozilla.gecko.text.TextSelection; import org.mozilla.gecko.updater.UpdateServiceHelper; import org.mozilla.gecko.util.ActivityResultHandler; import org.mozilla.gecko.util.ActivityUtils; import org.mozilla.gecko.util.EventCallback; import org.mozilla.gecko.util.FileUtils; import org.mozilla.gecko.util.GeckoEventListener; import org.mozilla.gecko.util.GeckoRequest; import org.mozilla.gecko.util.HardwareUtils; import org.mozilla.gecko.util.NativeEventListener; import org.mozilla.gecko.util.NativeJSObject; import org.mozilla.gecko.util.PrefUtils; import org.mozilla.gecko.util.ThreadUtils; import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.app.Activity; import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager.NameNotFoundException; import android.content.res.Configuration; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Rect; import android.graphics.RectF; import android.hardware.Sensor; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Environment; import android.os.Handler; import android.os.PowerManager; import android.os.Process; import android.os.StrictMode; import android.provider.ContactsContract; import android.provider.MediaStore.Images.Media; import android.support.annotation.WorkerThread; import android.support.design.widget.Snackbar; import android.text.TextUtils; import android.util.AttributeSet; import android.util.Base64; import android.util.Log; import android.util.SparseBooleanArray; import android.view.Gravity; import android.view.KeyEvent; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.MotionEvent; import android.view.OrientationEventListener; import android.view.SurfaceView; import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.view.Window; import android.widget.AbsoluteLayout; import android.widget.AdapterView; import android.widget.Button; import android.widget.FrameLayout; import android.widget.ListView; import android.widget.RelativeLayout; import android.widget.SimpleAdapter; import android.widget.TextView; import android.widget.Toast; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.lang.ref.WeakReference; import java.net.URL; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; public abstract class GeckoApp extends GeckoActivity implements ContextGetter, GeckoAppShell.GeckoInterface, GeckoEventListener, GeckoMenu.Callback, GeckoMenu.MenuPresenter, NativeEventListener, Tabs.OnTabsChangedListener, ViewTreeObserver.OnGlobalLayoutListener { private static final String LOGTAG = "GeckoApp"; private static final long ONE_DAY_MS = TimeUnit.MILLISECONDS.convert(1, TimeUnit.DAYS); public static final String ACTION_ALERT_CALLBACK = "org.mozilla.gecko.ALERT_CALLBACK"; public static final String ACTION_HOMESCREEN_SHORTCUT = "org.mozilla.gecko.BOOKMARK"; public static final String ACTION_DEBUG = "org.mozilla.gecko.DEBUG"; public static final String ACTION_LAUNCH_SETTINGS = "org.mozilla.gecko.SETTINGS"; public static final String ACTION_LOAD = "org.mozilla.gecko.LOAD"; public static final String ACTION_INIT_PW = "org.mozilla.gecko.INIT_PW"; public static final String ACTION_SWITCH_TAB = "org.mozilla.gecko.SWITCH_TAB"; public static final String INTENT_REGISTER_STUMBLER_LISTENER = "org.mozilla.gecko.STUMBLER_REGISTER_LOCAL_LISTENER"; public static final String EXTRA_STATE_BUNDLE = "stateBundle"; public static final String LAST_SELECTED_TAB = "lastSelectedTab"; public static final String PREFS_ALLOW_STATE_BUNDLE = "allowStateBundle"; public static final String PREFS_VERSION_CODE = "versionCode"; public static final String PREFS_WAS_STOPPED = "wasStopped"; public static final String PREFS_CRASHED_COUNT = "crashedCount"; public static final String PREFS_CLEANUP_TEMP_FILES = "cleanupTempFiles"; public static final String SAVED_STATE_IN_BACKGROUND = "inBackground"; public static final String SAVED_STATE_PRIVATE_SESSION = "privateSession"; // Delay before running one-time "cleanup" tasks that may be needed // after a version upgrade. private static final int CLEANUP_DEFERRAL_SECONDS = 15; private static boolean sAlreadyLoaded; private static WeakReference lastActiveGeckoApp; protected RelativeLayout mRootLayout; protected RelativeLayout mMainLayout; protected RelativeLayout mGeckoLayout; private OrientationEventListener mCameraOrientationEventListener; public List mAppStateListeners = new LinkedList(); protected MenuPanel mMenuPanel; protected Menu mMenu; protected boolean mIsRestoringActivity; /** Tells if we're aborting app launch, e.g. if this is an unsupported device configuration. */ protected boolean mIsAbortingAppLaunch; private PromptService mPromptService; protected TextSelection mTextSelection; protected DoorHangerPopup mDoorHangerPopup; protected FormAssistPopup mFormAssistPopup; protected GeckoView mLayerView; private AbsoluteLayout mPluginContainer; private FullScreenHolder mFullScreenPluginContainer; private View mFullScreenPluginView; private final HashMap mWakeLocks = new HashMap(); protected boolean mLastSessionCrashed; protected boolean mShouldRestore; private boolean mSessionRestoreParsingFinished = false; private EventDispatcher eventDispatcher; private int lastSelectedTabId = -1; private static final class LastSessionParser extends SessionParser { private JSONArray tabs; private JSONObject windowObject; private boolean isExternalURL; private boolean selectNextTab; private boolean tabsWereSkipped; private boolean tabsWereProcessed; public LastSessionParser(JSONArray tabs, JSONObject windowObject, boolean isExternalURL) { this.tabs = tabs; this.windowObject = windowObject; this.isExternalURL = isExternalURL; } public boolean allTabsSkipped() { return tabsWereSkipped && !tabsWereProcessed; } @Override public void onTabRead(final SessionTab sessionTab) { if (sessionTab.isAboutHomeWithoutHistory()) { // This is a tab pointing to about:home with no history. We won't restore // this tab. If we end up restoring no tabs then the browser will decide // whether it needs to open about:home or a different 'homepage'. If we'd // always restore about:home only tabs then we'd never open the homepage. // See bug 1261008. if (sessionTab.isSelected()) { // Unfortunately this tab is the selected tab. Let's just try to select // the first tab. If we haven't restored any tabs so far then remember // to select the next tab that gets restored. if (!Tabs.getInstance().selectLastTab()) { selectNextTab = true; } } // Do not restore this tab. tabsWereSkipped = true; return; } tabsWereProcessed = true; JSONObject tabObject = sessionTab.getTabObject(); int flags = Tabs.LOADURL_NEW_TAB; flags |= ((isExternalURL || !sessionTab.isSelected()) ? Tabs.LOADURL_DELAY_LOAD : 0); flags |= (tabObject.optBoolean("desktopMode") ? Tabs.LOADURL_DESKTOP : 0); flags |= (tabObject.optBoolean("isPrivate") ? Tabs.LOADURL_PRIVATE : 0); final Tab tab = Tabs.getInstance().loadUrl(sessionTab.getUrl(), flags); if (selectNextTab) { // We did not restore the selected tab previously. Now let's select this tab. Tabs.getInstance().selectTab(tab.getId()); selectNextTab = false; } ThreadUtils.postToUiThread(new Runnable() { @Override public void run() { tab.updateTitle(sessionTab.getTitle()); } }); try { tabObject.put("tabId", tab.getId()); } catch (JSONException e) { Log.e(LOGTAG, "JSON error", e); } tabs.put(tabObject); } @Override public void onClosedTabsRead(final JSONArray closedTabData) throws JSONException { windowObject.put("closedTabs", closedTabData); } }; protected boolean mInitialized; protected boolean mWindowFocusInitialized; private Telemetry.Timer mJavaUiStartupTimer; private Telemetry.Timer mGeckoReadyStartupTimer; private String mPrivateBrowsingSession; private volatile HealthRecorder mHealthRecorder; private volatile Locale mLastLocale; protected Intent mRestartIntent; private boolean mWasFirstTabShownAfterActivityUnhidden; abstract public int getLayout(); protected void processTabQueue() {}; protected void openQueuedTabs() {}; @SuppressWarnings("serial") class SessionRestoreException extends Exception { public SessionRestoreException(Exception e) { super(e); } public SessionRestoreException(String message) { super(message); } } void toggleChrome(final boolean aShow) { } void focusChrome() { } @Override public Context getContext() { return this; } @Override public SharedPreferences getSharedPreferences() { return GeckoSharedPrefs.forApp(this); } @Override public Activity getActivity() { return this; } @Override public void addAppStateListener(GeckoAppShell.AppStateListener listener) { mAppStateListeners.add(listener); } @Override public void removeAppStateListener(GeckoAppShell.AppStateListener listener) { mAppStateListeners.remove(listener); } @Override public void onTabChanged(Tab tab, Tabs.TabEvents msg, String data) { // When a tab is closed, it is always unselected first. // When a tab is unselected, another tab is always selected first. switch (msg) { case UNSELECTED: break; case LOCATION_CHANGE: // We only care about location change for the selected tab. if (!Tabs.getInstance().isSelectedTab(tab)) break; // Fall through... case SELECTED: invalidateOptionsMenu(); if (mFormAssistPopup != null) mFormAssistPopup.hide(); break; case DESKTOP_MODE_CHANGE: if (Tabs.getInstance().isSelectedTab(tab)) invalidateOptionsMenu(); break; } } public void refreshChrome() { } @Override public void invalidateOptionsMenu() { if (mMenu == null) { return; } onPrepareOptionsMenu(mMenu); super.invalidateOptionsMenu(); } @Override public boolean onCreateOptionsMenu(Menu menu) { mMenu = menu; MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.gecko_app_menu, mMenu); return true; } @Override public MenuInflater getMenuInflater() { return new GeckoMenuInflater(this); } public MenuPanel getMenuPanel() { if (mMenuPanel == null) { onCreatePanelMenu(Window.FEATURE_OPTIONS_PANEL, null); invalidateOptionsMenu(); } return mMenuPanel; } @Override public boolean onMenuItemClick(MenuItem item) { return onOptionsItemSelected(item); } @Override public boolean onMenuItemLongClick(MenuItem item) { return false; } @Override public void openMenu() { openOptionsMenu(); } @Override public void showMenu(final View menu) { // On devices using the custom menu, focus is cleared from the menu when its tapped. // Close and then reshow it to avoid these issues. See bug 794581 and bug 968182. closeMenu(); // Post the reshow code back to the UI thread to avoid some optimizations Android // has put in place for menus that hide/show themselves quickly. See bug 985400. ThreadUtils.postToUiThread(new Runnable() { @Override public void run() { mMenuPanel.removeAllViews(); mMenuPanel.addView(menu); openOptionsMenu(); } }); } @Override public void closeMenu() { closeOptionsMenu(); } @Override public View onCreatePanelView(int featureId) { if (featureId == Window.FEATURE_OPTIONS_PANEL) { if (mMenuPanel == null) { mMenuPanel = new MenuPanel(this, null); } else { // Prepare the panel every time before showing the menu. onPreparePanel(featureId, mMenuPanel, mMenu); } return mMenuPanel; } return super.onCreatePanelView(featureId); } @Override public boolean onCreatePanelMenu(int featureId, Menu menu) { if (featureId == Window.FEATURE_OPTIONS_PANEL) { if (mMenuPanel == null) { mMenuPanel = (MenuPanel) onCreatePanelView(featureId); } GeckoMenu gMenu = new GeckoMenu(this, null); gMenu.setCallback(this); gMenu.setMenuPresenter(this); menu = gMenu; mMenuPanel.addView(gMenu); return onCreateOptionsMenu(menu); } return super.onCreatePanelMenu(featureId, menu); } @Override public boolean onPreparePanel(int featureId, View view, Menu menu) { if (featureId == Window.FEATURE_OPTIONS_PANEL) { return onPrepareOptionsMenu(menu); } return super.onPreparePanel(featureId, view, menu); } @Override public boolean onMenuOpened(int featureId, Menu menu) { // exit full-screen mode whenever the menu is opened if (mLayerView != null && mLayerView.isFullScreen()) { GeckoAppShell.notifyObservers("FullScreen:Exit", null); } if (featureId == Window.FEATURE_OPTIONS_PANEL) { if (mMenu == null) { // getMenuPanel() will force the creation of the menu as well MenuPanel panel = getMenuPanel(); onPreparePanel(featureId, panel, mMenu); } // Scroll custom menu to the top if (mMenuPanel != null) mMenuPanel.scrollTo(0, 0); return true; } return super.onMenuOpened(featureId, menu); } @Override public boolean onOptionsItemSelected(MenuItem item) { if (item.getItemId() == R.id.quit) { // Make sure the Guest Browsing notification goes away when we quit. GuestSession.hideNotification(this); final SharedPreferences prefs = GeckoSharedPrefs.forProfile(this); final Set clearSet = PrefUtils.getStringSet(prefs, ClearOnShutdownPref.PREF, new HashSet()); final JSONObject clearObj = new JSONObject(); for (String clear : clearSet) { try { clearObj.put(clear, true); } catch (JSONException ex) { Log.e(LOGTAG, "Error adding clear object " + clear, ex); } } final JSONObject res = new JSONObject(); try { res.put("sanitize", clearObj); } catch (JSONException ex) { Log.e(LOGTAG, "Error adding sanitize object", ex); } // If the user has opted out of session restore, and does want to clear history // we also want to prevent the current session info from being saved. if (clearObj.has("private.data.history")) { final String sessionRestore = getSessionRestorePreference(getSharedPreferences()); try { res.put("dontSaveSession", "quit".equals(sessionRestore)); } catch (JSONException ex) { Log.e(LOGTAG, "Error adding session restore data", ex); } } GeckoAppShell.notifyObservers("Browser:Quit", res.toString()); // We don't call doShutdown() here because this creates a race condition which can // cause the clearing of private data to fail. Instead, we shut down the UI only after // we're done sanitizing. return true; } return super.onOptionsItemSelected(item); } @Override public void onOptionsMenuClosed(Menu menu) { mMenuPanel.removeAllViews(); mMenuPanel.addView((GeckoMenu) mMenu); } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { // Handle hardware menu key presses separately so that we can show a custom menu in some cases. if (keyCode == KeyEvent.KEYCODE_MENU) { openOptionsMenu(); return true; } return super.onKeyDown(keyCode, event); } @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putBoolean(SAVED_STATE_IN_BACKGROUND, isApplicationInBackground()); outState.putString(SAVED_STATE_PRIVATE_SESSION, mPrivateBrowsingSession); outState.putInt(LAST_SELECTED_TAB, lastSelectedTabId); } @Override protected void onRestoreInstanceState(final Bundle inState) { lastSelectedTabId = inState.getInt(LAST_SELECTED_TAB); } public void addTab() { } public void addPrivateTab() { } public void showNormalTabs() { } public void showPrivateTabs() { } public void hideTabs() { } /** * Close the tab UI indirectly (not as the result of a direct user * action). This does not force the UI to close; for example in Firefox * tablet mode it will remain open unless the user explicitly closes it. * * @return True if the tab UI was hidden. */ public boolean autoHideTabs() { return false; } @Override public boolean areTabsShown() { return false; } @Override public void handleMessage(final String event, final NativeJSObject message, final EventCallback callback) { if ("Accessibility:Ready".equals(event)) { GeckoAccessibility.updateAccessibilitySettings(this); } else if ("Bookmark:Insert".equals(event)) { final String url = message.getString("url"); final String title = message.getString("title"); final Context context = this; final BrowserDB db = BrowserDB.from(getProfile()); ThreadUtils.postToBackgroundThread(new Runnable() { @Override public void run() { final boolean bookmarkAdded = db.addBookmark(getContentResolver(), title, url); final int resId = bookmarkAdded ? R.string.bookmark_added : R.string.bookmark_already_added; ThreadUtils.postToUiThread(new Runnable() { @Override public void run() { SnackbarBuilder.builder(GeckoApp.this) .message(resId) .duration(Snackbar.LENGTH_LONG) .buildAndShow(); } }); } }); } else if ("Contact:Add".equals(event)) { final String email = message.optString("email", null); final String phone = message.optString("phone", null); if (email != null) { Uri contactUri = Uri.parse(email); Intent i = new Intent(ContactsContract.Intents.SHOW_OR_CREATE_CONTACT, contactUri); startActivity(i); } else if (phone != null) { Uri contactUri = Uri.parse(phone); Intent i = new Intent(ContactsContract.Intents.SHOW_OR_CREATE_CONTACT, contactUri); startActivity(i); } else { // something went wrong. Log.e(LOGTAG, "Received Contact:Add message with no email nor phone number"); } } else if ("DevToolsAuth:Scan".equals(event)) { DevToolsAuthHelper.scan(this, callback); } else if ("DOMFullScreen:Start".equals(event)) { // Local ref to layerView for thread safety LayerView layerView = mLayerView; if (layerView != null) { layerView.setFullScreenState(message.getBoolean("rootElement") ? FullScreenState.ROOT_ELEMENT : FullScreenState.NON_ROOT_ELEMENT); } } else if ("DOMFullScreen:Stop".equals(event)) { // Local ref to layerView for thread safety LayerView layerView = mLayerView; if (layerView != null) { layerView.setFullScreenState(FullScreenState.NONE); } } else if ("Image:SetAs".equals(event)) { String src = message.getString("url"); setImageAs(src); } else if ("Locale:Set".equals(event)) { setLocale(message.getString("locale")); } else if ("Permissions:Data".equals(event)) { final NativeJSObject[] permissions = message.getObjectArray("permissions"); showSiteSettingsDialog(permissions); } else if ("PrivateBrowsing:Data".equals(event)) { mPrivateBrowsingSession = message.optString("session", null); } else if ("Session:StatePurged".equals(event)) { onStatePurged(); } else if ("Sanitize:Finished".equals(event)) { if (message.getBoolean("shutdown")) { // Gecko is shutting down and has called our sanitize handlers, // so we can start exiting, too. doShutdown(); } } else if ("Share:Text".equals(event)) { final String text = message.getString("text"); final Tab tab = Tabs.getInstance().getSelectedTab(); String title = ""; if (tab != null) { title = tab.getDisplayTitle(); } IntentHelper.openUriExternal(text, "text/plain", "", "", Intent.ACTION_SEND, title, false); // Context: Sharing via chrome list (no explicit session is active) Telemetry.sendUIEvent(TelemetryContract.Event.SHARE, TelemetryContract.Method.LIST, "text"); } else if ("Snackbar:Show".equals(event)) { SnackbarBuilder.builder(this) .fromEvent(message) .callback(callback) .buildAndShow(); } else if ("SystemUI:Visibility".equals(event)) { setSystemUiVisible(message.getBoolean("visible")); } else if ("ToggleChrome:Focus".equals(event)) { focusChrome(); } else if ("ToggleChrome:Hide".equals(event)) { toggleChrome(false); } else if ("ToggleChrome:Show".equals(event)) { toggleChrome(true); } else if ("Update:Check".equals(event)) { UpdateServiceHelper.checkForUpdate(this); } else if ("Update:Download".equals(event)) { UpdateServiceHelper.downloadUpdate(this); } else if ("Update:Install".equals(event)) { UpdateServiceHelper.applyUpdate(this); } else if ("RuntimePermissions:Prompt".equals(event)) { String[] permissions = message.getStringArray("permissions"); if (callback == null || permissions == null) { return; } Permissions.from(this) .withPermissions(permissions) .andFallback(new Runnable() { @Override public void run() { callback.sendSuccess(false); } }) .run(new Runnable() { @Override public void run() { callback.sendSuccess(true); } }); } } @Override public void handleMessage(String event, JSONObject message) { try { if (event.equals("Gecko:Ready")) { mGeckoReadyStartupTimer.stop(); geckoConnected(); // This method is already running on the background thread, so we // know that mHealthRecorder will exist. That doesn't stop us being // paranoid. // This method is cheap, so don't spawn a new runnable. final HealthRecorder rec = mHealthRecorder; if (rec != null) { rec.recordGeckoStartupTime(mGeckoReadyStartupTimer.getElapsed()); } GeckoApplication.get().onDelayedStartup(); } else if (event.equals("Gecko:Exited")) { // Gecko thread exited first; let GeckoApp die too. doShutdown(); return; } else if (event.equals("Accessibility:Event")) { GeckoAccessibility.sendAccessibilityEvent(message); } } catch (Exception e) { Log.e(LOGTAG, "Exception handling message \"" + event + "\":", e); } } void onStatePurged() { } /** * @param permissions * Array of JSON objects to represent site permissions. * Example: { type: "offline-app", setting: "Store Offline Data", value: "Allow" } */ private void showSiteSettingsDialog(final NativeJSObject[] permissions) { final AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle(R.string.site_settings_title); final ArrayList> itemList = new ArrayList>(); for (final NativeJSObject permObj : permissions) { final HashMap map = new HashMap(); map.put("setting", permObj.getString("setting")); map.put("value", permObj.getString("value")); itemList.add(map); } // setMultiChoiceItems doesn't support using an adapter, so we're creating a hack with // setSingleChoiceItems and changing the choiceMode below when we create the dialog builder.setSingleChoiceItems(new SimpleAdapter( GeckoApp.this, itemList, R.layout.site_setting_item, new String[] { "setting", "value" }, new int[] { R.id.setting, R.id.value } ), -1, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int id) { } }); builder.setPositiveButton(R.string.site_settings_clear, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int id) { ListView listView = ((AlertDialog) dialog).getListView(); SparseBooleanArray checkedItemPositions = listView.getCheckedItemPositions(); // An array of the indices of the permissions we want to clear JSONArray permissionsToClear = new JSONArray(); for (int i = 0; i < checkedItemPositions.size(); i++) if (checkedItemPositions.get(i)) permissionsToClear.put(i); GeckoAppShell.notifyObservers("Permissions:Clear", permissionsToClear.toString()); } }); builder.setNegativeButton(R.string.site_settings_cancel, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int id) { dialog.cancel(); } }); ThreadUtils.postToUiThread(new Runnable() { @Override public void run() { AlertDialog dialog = builder.create(); dialog.show(); final ListView listView = dialog.getListView(); if (listView != null) { listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); } final Button clearButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE); clearButton.setEnabled(false); dialog.getListView().setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView adapterView, View view, int i, long l) { if (listView.getCheckedItemCount() == 0) { clearButton.setEnabled(false); } else { clearButton.setEnabled(true); } } }); } }); } /* package */ void addFullScreenPluginView(View view) { if (mFullScreenPluginView != null) { Log.w(LOGTAG, "Already have a fullscreen plugin view"); return; } setFullScreen(true); view.setWillNotDraw(false); if (view instanceof SurfaceView) { ((SurfaceView) view).setZOrderOnTop(true); } mFullScreenPluginContainer = new FullScreenHolder(this); FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, Gravity.CENTER); mFullScreenPluginContainer.addView(view, layoutParams); FrameLayout decor = (FrameLayout)getWindow().getDecorView(); decor.addView(mFullScreenPluginContainer, layoutParams); mFullScreenPluginView = view; } @Override public void addPluginView(final View view) { if (ThreadUtils.isOnUiThread()) { addFullScreenPluginView(view); } else { ThreadUtils.postToUiThread(new Runnable() { @Override public void run() { addFullScreenPluginView(view); } }); } } /* package */ void removeFullScreenPluginView(View view) { if (mFullScreenPluginView == null) { Log.w(LOGTAG, "Don't have a fullscreen plugin view"); return; } if (mFullScreenPluginView != view) { Log.w(LOGTAG, "Passed view is not the current full screen view"); return; } mFullScreenPluginContainer.removeView(mFullScreenPluginView); // We need do do this on the next iteration in order to avoid // a deadlock, see comment below in FullScreenHolder ThreadUtils.postToUiThread(new Runnable() { @Override public void run() { mLayerView.showSurface(); } }); FrameLayout decor = (FrameLayout)getWindow().getDecorView(); decor.removeView(mFullScreenPluginContainer); mFullScreenPluginView = null; GeckoScreenOrientation.getInstance().unlock(); setFullScreen(false); } @Override public void removePluginView(final View view) { if (ThreadUtils.isOnUiThread()) { removePluginView(view); } else { ThreadUtils.postToUiThread(new Runnable() { @Override public void run() { removeFullScreenPluginView(view); } }); } } // This method starts downloading an image synchronously and displays the Chooser activity to set the image as wallpaper. private void setImageAs(final String aSrc) { boolean isDataURI = aSrc.startsWith("data:"); Bitmap image = null; InputStream is = null; ByteArrayOutputStream os = null; try { if (isDataURI) { int dataStart = aSrc.indexOf(","); byte[] buf = Base64.decode(aSrc.substring(dataStart + 1), Base64.DEFAULT); image = BitmapUtils.decodeByteArray(buf); } else { int byteRead; byte[] buf = new byte[4192]; os = new ByteArrayOutputStream(); URL url = new URL(aSrc); is = url.openStream(); // Cannot read from same stream twice. Also, InputStream from // URL does not support reset. So converting to byte array. while ((byteRead = is.read(buf)) != -1) { os.write(buf, 0, byteRead); } byte[] imgBuffer = os.toByteArray(); image = BitmapUtils.decodeByteArray(imgBuffer); } if (image != null) { // Some devices don't have a DCIM folder and the Media.insertImage call will fail. File dcimDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES); if (!dcimDir.mkdirs() && !dcimDir.isDirectory()) { SnackbarBuilder.builder(this) .message(R.string.set_image_path_fail) .duration(Snackbar.LENGTH_LONG) .buildAndShow(); return; } String path = Media.insertImage(getContentResolver(), image, null, null); if (path == null) { SnackbarBuilder.builder(this) .message(R.string.set_image_path_fail) .duration(Snackbar.LENGTH_LONG) .buildAndShow(); return; } final Intent intent = new Intent(Intent.ACTION_ATTACH_DATA); intent.addCategory(Intent.CATEGORY_DEFAULT); intent.setData(Uri.parse(path)); // Removes the image from storage once the chooser activity ends. Intent chooser = Intent.createChooser(intent, getString(R.string.set_image_chooser_title)); ActivityResultHandler handler = new ActivityResultHandler() { @Override public void onActivityResult (int resultCode, Intent data) { getContentResolver().delete(intent.getData(), null, null); } }; ActivityHandlerHelper.startIntentForActivity(this, chooser, handler); } else { SnackbarBuilder.builder(this) .message(R.string.set_image_fail) .duration(Snackbar.LENGTH_LONG) .buildAndShow(); } } catch (OutOfMemoryError ome) { Log.e(LOGTAG, "Out of Memory when converting to byte array", ome); } catch (IOException ioe) { Log.e(LOGTAG, "I/O Exception while setting wallpaper", ioe); } finally { if (is != null) { try { is.close(); } catch (IOException ioe) { Log.w(LOGTAG, "I/O Exception while closing stream", ioe); } } if (os != null) { try { os.close(); } catch (IOException ioe) { Log.w(LOGTAG, "I/O Exception while closing stream", ioe); } } } } private int getBitmapSampleSize(BitmapFactory.Options options, int idealWidth, int idealHeight) { int width = options.outWidth; int height = options.outHeight; int inSampleSize = 1; if (height > idealHeight || width > idealWidth) { if (width > height) { inSampleSize = Math.round((float)height / idealHeight); } else { inSampleSize = Math.round((float)width / idealWidth); } } return inSampleSize; } public void requestRender() { mLayerView.requestRender(); } @Override public void setFullScreen(final boolean fullscreen) { ThreadUtils.postToUiThread(new Runnable() { @Override public void run() { ActivityUtils.setFullScreen(GeckoApp.this, fullscreen); } }); } /** * Check and start the Java profiler if MOZ_PROFILER_STARTUP env var is specified. **/ protected static void earlyStartJavaSampler(SafeIntent intent) { String env = intent.getStringExtra("env0"); for (int i = 1; env != null; i++) { if (env.startsWith("MOZ_PROFILER_STARTUP=")) { if (!env.endsWith("=")) { GeckoJavaSampler.start(10, 1000); Log.d(LOGTAG, "Profiling Java on startup"); } break; } env = intent.getStringExtra("env" + i); } } /** * Called when the activity is first created. * * Here we initialize all of our profile settings, Firefox Health Report, * and other one-shot constructions. **/ @Override public void onCreate(Bundle savedInstanceState) { GeckoAppShell.ensureCrashHandling(); eventDispatcher = new EventDispatcher(); // Enable Android Strict Mode for developers' local builds (the "default" channel). if ("default".equals(AppConstants.MOZ_UPDATE_CHANNEL)) { enableStrictMode(); } if (!HardwareUtils.isSupportedSystem()) { // This build does not support the Android version of the device: Show an error and finish the app. mIsAbortingAppLaunch = true; super.onCreate(savedInstanceState); showSDKVersionError(); finish(); return; } // The clock starts...now. Better hurry! mJavaUiStartupTimer = new Telemetry.UptimeTimer("FENNEC_STARTUP_TIME_JAVAUI"); mGeckoReadyStartupTimer = new Telemetry.UptimeTimer("FENNEC_STARTUP_TIME_GECKOREADY"); final SafeIntent intent = new SafeIntent(getIntent()); earlyStartJavaSampler(intent); // GeckoLoader wants to dig some environment variables out of the // incoming intent, so pass it in here. GeckoLoader will do its // business later and dispose of the reference. GeckoLoader.setLastIntent(intent); // Workaround for . try { Class.forName("android.os.AsyncTask"); } catch (ClassNotFoundException e) { } MemoryMonitor.getInstance().init(getApplicationContext()); // GeckoAppShell is tightly coupled to us, rather than // the app context, because various parts of Fennec (e.g., // GeckoScreenOrientation) use GAS to access the Activity in // the guise of fetching a Context. // When that's fixed, `this` can change to // `(GeckoApplication) getApplication()` here. GeckoAppShell.setContextGetter(this); GeckoAppShell.setGeckoInterface(this); // Tell Stumbler to register a local broadcast listener to listen for preference intents. // We do this via intents since we can't easily access Stumbler directly, // as it might be compiled outside of Fennec. getApplicationContext().sendBroadcast( new Intent(INTENT_REGISTER_STUMBLER_LISTENER) ); // Did the OS locale change while we were backgrounded? If so, // we need to die so that Gecko will re-init add-ons that touch // the UI. // This is using a sledgehammer to crack a nut, but it'll do for // now. // Our OS locale pref will be detected as invalid after the // restart, and will be propagated to Gecko accordingly, so there's // no need to touch that here. if (BrowserLocaleManager.getInstance().systemLocaleDidChange()) { Log.i(LOGTAG, "System locale changed. Restarting."); doRestart(); return; } if (sAlreadyLoaded) { // This happens when the GeckoApp activity is destroyed by Android // without killing the entire application (see Bug 769269). mIsRestoringActivity = true; Telemetry.addToHistogram("FENNEC_RESTORING_ACTIVITY", 1); } else { final String action = intent.getAction(); final String args = intent.getStringExtra("args"); sAlreadyLoaded = true; GeckoThread.init(/* profile */ null, args, action, /* debugging */ ACTION_DEBUG.equals(action)); // Speculatively pre-fetch the profile in the background. ThreadUtils.postToBackgroundThread(new Runnable() { @Override public void run() { getProfile(); } }); final String uri = getURIFromIntent(intent); if (!TextUtils.isEmpty(uri)) { // Start a speculative connection as soon as Gecko loads. GeckoThread.speculativeConnect(uri); } } // GeckoThread has to register for "Gecko:Ready" first, so GeckoApp registers // for events after initializing GeckoThread but before launching it. getAppEventDispatcher().registerGeckoThreadListener((GeckoEventListener)this, "Gecko:Ready", "Gecko:Exited", "Accessibility:Event"); getAppEventDispatcher().registerGeckoThreadListener((NativeEventListener)this, "Accessibility:Ready", "Bookmark:Insert", "Contact:Add", "DevToolsAuth:Scan", "DOMFullScreen:Start", "DOMFullScreen:Stop", "Image:SetAs", "Locale:Set", "Permissions:Data", "PrivateBrowsing:Data", "RuntimePermissions:Prompt", "Sanitize:Finished", "Session:StatePurged", "Share:Text", "Snackbar:Show", "SystemUI:Visibility", "ToggleChrome:Focus", "ToggleChrome:Hide", "ToggleChrome:Show", "Update:Check", "Update:Download", "Update:Install"); GeckoThread.launch(); Bundle stateBundle = IntentUtils.getBundleExtraSafe(getIntent(), EXTRA_STATE_BUNDLE); if (stateBundle != null) { // Use the state bundle if it was given as an intent extra. This is // only intended to be used internally via Robocop, so a boolean // is read from a private shared pref to prevent other apps from // injecting states. final SharedPreferences prefs = getSharedPreferences(); if (prefs.getBoolean(PREFS_ALLOW_STATE_BUNDLE, false)) { prefs.edit().remove(PREFS_ALLOW_STATE_BUNDLE).apply(); savedInstanceState = stateBundle; } } else if (savedInstanceState != null) { // Bug 896992 - This intent has already been handled; reset the intent. setIntent(new Intent(Intent.ACTION_MAIN)); } super.onCreate(savedInstanceState); GeckoScreenOrientation.getInstance().update(getResources().getConfiguration().orientation); setContentView(getLayout()); // Set up Gecko layout. mRootLayout = (RelativeLayout) findViewById(R.id.root_layout); mGeckoLayout = (RelativeLayout) findViewById(R.id.gecko_layout); mMainLayout = (RelativeLayout) findViewById(R.id.main_layout); mLayerView = (GeckoView) findViewById(R.id.layer_view); Tabs.getInstance().attachToContext(this, mLayerView); // Use global layout state change to kick off additional initialization mMainLayout.getViewTreeObserver().addOnGlobalLayoutListener(this); if (Versions.preMarshmallow) { mTextSelection = new ActionBarTextSelection(this); } else { mTextSelection = new FloatingToolbarTextSelection(this, mLayerView); } mTextSelection.create(); // Determine whether we should restore tabs. mLastSessionCrashed = updateCrashedState(); mShouldRestore = getSessionRestoreState(savedInstanceState); if (mShouldRestore && savedInstanceState != null) { boolean wasInBackground = savedInstanceState.getBoolean(SAVED_STATE_IN_BACKGROUND, false); // Don't log OOM-kills if only one activity was destroyed. (For example // from "Don't keep activities" on ICS) if (!wasInBackground && !mIsRestoringActivity) { Telemetry.addToHistogram("FENNEC_WAS_KILLED", 1); } mPrivateBrowsingSession = savedInstanceState.getString(SAVED_STATE_PRIVATE_SESSION); } ThreadUtils.postToBackgroundThread(new Runnable() { @Override public void run() { // If we are doing a restore, read the session data so we can send it to Gecko later. String restoreMessage = null; if (!mIsRestoringActivity && mShouldRestore) { final boolean isExternalURL = invokedWithExternalURL(getIntentURI(new SafeIntent(getIntent()))); try { // restoreSessionTabs() will create simple tab stubs with the // URL and title for each page, but we also need to restore // session history. restoreSessionTabs() will inject the IDs // of the tab stubs into the JSON data (which holds the session // history). This JSON data is then sent to Gecko so session // history can be restored for each tab. restoreMessage = restoreSessionTabs(isExternalURL, false); } catch (SessionRestoreException e) { // If mShouldRestore was set to false in restoreSessionTabs(), this means // either that we intentionally skipped all tabs read from the session file, // or else that the file was syntactically valid, but didn't contain any // tabs (e.g. because the user cleared history), therefore we don't need // to switch to the backup copy. if (mShouldRestore) { Log.e(LOGTAG, "An error occurred during restore, switching to backup file", e); // To be on the safe side, we will always attempt to restore from the backup // copy if we end up here. // Since we will also hit this situation regularly during first run though, // we'll only report it in telemetry if we failed to restore despite the // file existing, which means it's very probably damaged. if (getProfile().sessionFileExists()) { Telemetry.addToHistogram("FENNEC_SESSIONSTORE_DAMAGED_SESSION_FILE", 1); } try { restoreMessage = restoreSessionTabs(isExternalURL, true); Telemetry.addToHistogram("FENNEC_SESSIONSTORE_RESTORING_FROM_BACKUP", 1); } catch (SessionRestoreException ex) { if (!mShouldRestore) { // Restoring only "failed" because the backup copy was deliberately empty, too. Telemetry.addToHistogram("FENNEC_SESSIONSTORE_RESTORING_FROM_BACKUP", 1); } else { // Restoring the backup failed, too, so do a normal startup. Log.e(LOGTAG, "An error occurred during restore", ex); mShouldRestore = false; } } } } } synchronized (GeckoApp.this) { mSessionRestoreParsingFinished = true; GeckoApp.this.notifyAll(); } // If we are doing a restore, send the parsed session data to Gecko. if (!mIsRestoringActivity) { GeckoAppShell.notifyObservers("Session:Restore", restoreMessage); } // Make sure sessionstore.old is either updated or deleted as necessary. getProfile().updateSessionFile(mShouldRestore); } }); // Perform background initialization. ThreadUtils.postToBackgroundThread(new Runnable() { @Override public void run() { final SharedPreferences prefs = GeckoApp.this.getSharedPreferences(); // Wait until now to set this, because we'd rather throw an exception than // have a caller of BrowserLocaleManager regress startup. final LocaleManager localeManager = BrowserLocaleManager.getInstance(); localeManager.initialize(getApplicationContext()); SessionInformation previousSession = SessionInformation.fromSharedPrefs(prefs); if (previousSession.wasKilled()) { Telemetry.addToHistogram("FENNEC_WAS_KILLED", 1); } SharedPreferences.Editor editor = prefs.edit(); editor.putBoolean(GeckoAppShell.PREFS_OOM_EXCEPTION, false); // Put a flag to check if we got a normal `onSaveInstanceState` // on exit, or if we were suddenly killed (crash or native OOM). editor.putBoolean(GeckoApp.PREFS_WAS_STOPPED, false); editor.apply(); // The lifecycle of mHealthRecorder is "shortly after onCreate" // through "onDestroy" -- essentially the same as the lifecycle // of the activity itself. final String profilePath = getProfile().getDir().getAbsolutePath(); final EventDispatcher dispatcher = getAppEventDispatcher(); // This is the locale prior to fixing it up. final Locale osLocale = Locale.getDefault(); // Both of these are Java-format locale strings: "en_US", not "en-US". final String osLocaleString = osLocale.toString(); String appLocaleString = localeManager.getAndApplyPersistedLocale(GeckoApp.this); Log.d(LOGTAG, "OS locale is " + osLocaleString + ", app locale is " + appLocaleString); if (appLocaleString == null) { appLocaleString = osLocaleString; } mHealthRecorder = GeckoApp.this.createHealthRecorder(GeckoApp.this, profilePath, dispatcher, osLocaleString, appLocaleString, previousSession); final String uiLocale = appLocaleString; ThreadUtils.postToUiThread(new Runnable() { @Override public void run() { GeckoApp.this.onLocaleReady(uiLocale); } }); // We use per-profile prefs here, because we're tracking against // a Gecko pref. The same applies to the locale switcher! BrowserLocaleManager.storeAndNotifyOSLocale(GeckoSharedPrefs.forProfile(GeckoApp.this), osLocale); } }); IntentHelper.init(this); } @Override public void onStart() { super.onStart(); if (mIsAbortingAppLaunch) { return; } mWasFirstTabShownAfterActivityUnhidden = false; // onStart indicates we were hidden. } @Override protected void onStop() { super.onStop(); // Overriding here is not necessary, but we do this so we don't // forget to add the abort if we override this method later. if (mIsAbortingAppLaunch) { return; } } /** * At this point, the resource system and the rest of the browser are * aware of the locale. * * Now we can display strings! * * You can think of this as being something like a second phase of onCreate, * where you can do string-related operations. Use this in place of embedding * strings in view XML. * * By contrast, onConfigurationChanged does some locale operations, but is in * response to device changes. */ @Override public void onLocaleReady(final String locale) { if (!ThreadUtils.isOnUiThread()) { throw new RuntimeException("onLocaleReady must always be called from the UI thread."); } final Locale loc = Locales.parseLocaleCode(locale); if (loc.equals(mLastLocale)) { Log.d(LOGTAG, "New locale same as old; onLocaleReady has nothing to do."); } // The URL bar hint needs to be populated. TextView urlBar = (TextView) findViewById(R.id.url_bar_title); if (urlBar != null) { final String hint = getResources().getString(R.string.url_bar_default_text); urlBar.setHint(hint); } else { Log.d(LOGTAG, "No URL bar in GeckoApp. Not loading localized hint string."); } mLastLocale = loc; // Allow onConfigurationChanged to take care of the rest. // We don't call this.onConfigurationChanged, because (a) that does // work that's unnecessary after this locale action, and (b) it can // cause a loop! See Bug 1011008, Comment 12. super.onConfigurationChanged(getResources().getConfiguration()); } protected void initializeChrome() { mDoorHangerPopup = new DoorHangerPopup(this); mPluginContainer = (AbsoluteLayout) findViewById(R.id.plugin_container); mFormAssistPopup = (FormAssistPopup) findViewById(R.id.form_assist_popup); } /** * Loads the initial tab at Fennec startup. If we don't restore tabs, this * tab will be about:home, or the homepage if the user has set one. * If we've temporarily disabled restoring to break out of a crash loop, we'll show * the Recent Tabs folder of the Combined History panel, so the user can manually * restore tabs as needed. * If we restore tabs, we don't need to create a new tab. */ protected void loadStartupTab(final int flags) { if (!mShouldRestore) { if (mLastSessionCrashed) { // The Recent Tabs panel no longer exists, but BrowserApp will redirect us // to the Recent Tabs folder of the Combined History panel. Tabs.getInstance().loadUrl(AboutPages.getURLForBuiltinPanelType(PanelType.DEPRECATED_RECENT_TABS), flags); } else { final String homepage = getHomepage(); Tabs.getInstance().loadUrl(!TextUtils.isEmpty(homepage) ? homepage : AboutPages.HOME, flags); } } } /** * Loads the initial tab at Fennec startup. This tab will load with the given * external URL. If that URL is invalid, a startup tab will be loaded. * * @param url External URL to load. * @param intent External intent whose extras modify the request * @param flags Flags used to load the load */ protected void loadStartupTab(final String url, final SafeIntent intent, final int flags) { // Invalid url if (url == null) { loadStartupTab(flags); return; } Tabs.getInstance().loadUrlWithIntentExtras(url, intent, flags); } public String getHomepage() { return null; } private String getIntentURI(SafeIntent intent) { final String passedUri; final String uri = getURIFromIntent(intent); if (!TextUtils.isEmpty(uri)) { passedUri = uri; } else { passedUri = null; } return passedUri; } private boolean invokedWithExternalURL(String uri) { return uri != null && !AboutPages.isAboutHome(uri); } private void initialize() { mInitialized = true; final boolean isFirstTab = !mWasFirstTabShownAfterActivityUnhidden; mWasFirstTabShownAfterActivityUnhidden = true; // Reset since we'll be loading a tab. final SafeIntent intent = new SafeIntent(getIntent()); final String action = intent.getAction(); final String passedUri = getIntentURI(intent); final boolean isExternalURL = invokedWithExternalURL(passedUri); // Start migrating as early as possible, can do this in // parallel with Gecko load. checkMigrateProfile(); Tabs.registerOnTabsChangedListener(this); initializeChrome(); // We need to wait here because mShouldRestore can revert back to // false if a parsing error occurs and the startup tab we load // depends on whether we restore tabs or not. synchronized (this) { while (!mSessionRestoreParsingFinished) { try { wait(); } catch (final InterruptedException e) { // Ignore and wait again. } } } // External URLs should always be loaded regardless of whether Gecko is // already running. if (isExternalURL) { // Restore tabs before opening an external URL so that the new tab // is animated properly. Tabs.getInstance().notifyListeners(null, Tabs.TabEvents.RESTORED); processActionViewIntent(new Runnable() { @Override public void run() { int flags = Tabs.LOADURL_NEW_TAB | Tabs.LOADURL_USER_ENTERED | Tabs.LOADURL_EXTERNAL; if (ACTION_HOMESCREEN_SHORTCUT.equals(action)) { flags |= Tabs.LOADURL_PINNED; } if (isFirstTab) { flags |= Tabs.LOADURL_FIRST_AFTER_ACTIVITY_UNHIDDEN; } loadStartupTab(passedUri, intent, flags); } }); } else { if (!mIsRestoringActivity) { loadStartupTab(Tabs.LOADURL_NEW_TAB); } Tabs.getInstance().notifyListeners(null, Tabs.TabEvents.RESTORED); processTabQueue(); } recordStartupActionTelemetry(passedUri, action); // Check if launched from data reporting notification. if (ACTION_LAUNCH_SETTINGS.equals(action)) { Intent settingsIntent = new Intent(GeckoApp.this, GeckoPreferences.class); // Copy extras. settingsIntent.putExtras(intent.getUnsafe()); startActivity(settingsIntent); } //app state callbacks mAppStateListeners = new LinkedList(); mPromptService = new PromptService(this); // Trigger the completion of the telemetry timer that wraps activity startup, // then grab the duration to give to FHR. mJavaUiStartupTimer.stop(); final long javaDuration = mJavaUiStartupTimer.getElapsed(); ThreadUtils.getBackgroundHandler().postDelayed(new Runnable() { @Override public void run() { final HealthRecorder rec = mHealthRecorder; if (rec != null) { rec.recordJavaStartupTime(javaDuration); } } }, 50); final int updateServiceDelay = 30 * 1000; ThreadUtils.getBackgroundHandler().postDelayed(new Runnable() { @Override public void run() { UpdateServiceHelper.registerForUpdates(GeckoAppShell.getApplicationContext()); } }, updateServiceDelay); if (mIsRestoringActivity) { Tab selectedTab = Tabs.getInstance().getSelectedTab(); if (selectedTab != null) { Tabs.getInstance().notifyListeners(selectedTab, Tabs.TabEvents.SELECTED); } if (GeckoThread.isRunning()) { geckoConnected(); if (mLayerView != null) { mLayerView.setPaintState(LayerView.PAINT_BEFORE_FIRST); } } } } @TargetApi(Build.VERSION_CODES.JELLY_BEAN) @Override public void onGlobalLayout() { if (Versions.preJB) { mMainLayout.getViewTreeObserver().removeGlobalOnLayoutListener(this); } else { mMainLayout.getViewTreeObserver().removeOnGlobalLayoutListener(this); } if (!mInitialized) { initialize(); } } protected void processActionViewIntent(final Runnable openTabsRunnable) { // We need to ensure that if we receive a VIEW action and there are tabs queued then the // site loaded from the intent is on top (last loaded) and selected with all other tabs // being opened behind it. We process the tab queue first and request a callback from the JS - the // listener will open the url from the intent as normal when the tab queue has been processed. ThreadUtils.postToBackgroundThread(new Runnable() { @Override public void run() { if (TabQueueHelper.TAB_QUEUE_ENABLED && TabQueueHelper.shouldOpenTabQueueUrls(GeckoApp.this)) { getAppEventDispatcher().registerGeckoThreadListener(new NativeEventListener() { @Override public void handleMessage(String event, NativeJSObject message, EventCallback callback) { if ("Tabs:TabsOpened".equals(event)) { getAppEventDispatcher().unregisterGeckoThreadListener(this, "Tabs:TabsOpened"); openTabsRunnable.run(); } } }, "Tabs:TabsOpened"); TabQueueHelper.openQueuedUrls(GeckoApp.this, getProfile(), TabQueueHelper.FILE_NAME, true); } else { openTabsRunnable.run(); } } }); } @WorkerThread private String restoreSessionTabs(final boolean isExternalURL, boolean useBackup) throws SessionRestoreException { try { String sessionString = getProfile().readSessionFile(useBackup); if (sessionString == null) { throw new SessionRestoreException("Could not read from session file"); } // If we are doing an OOM restore, parse the session data and // stub the restored tabs immediately. This allows the UI to be // updated before Gecko has restored. final JSONArray tabs = new JSONArray(); final JSONObject windowObject = new JSONObject(); final boolean sessionDataValid; LastSessionParser parser = new LastSessionParser(tabs, windowObject, isExternalURL); if (mPrivateBrowsingSession == null) { sessionDataValid = parser.parse(sessionString); } else { sessionDataValid = parser.parse(sessionString, mPrivateBrowsingSession); } if (tabs.length() > 0) { windowObject.put("tabs", tabs); sessionString = new JSONObject().put("windows", new JSONArray().put(windowObject)).toString(); } else { if (parser.allTabsSkipped() || sessionDataValid) { // If we intentionally skipped all tabs we've read from the session file, we // set mShouldRestore back to false at this point already, so the calling code // can infer that the exception wasn't due to a damaged session store file. // The same applies if the session file was syntactically valid and // simply didn't contain any tabs. mShouldRestore = false; } throw new SessionRestoreException("No tabs could be read from session file"); } JSONObject restoreData = new JSONObject(); restoreData.put("sessionString", sessionString); return restoreData.toString(); } catch (JSONException e) { throw new SessionRestoreException(e); } } public static EventDispatcher getEventDispatcher() { final GeckoApp geckoApp = (GeckoApp) GeckoAppShell.getGeckoInterface(); return geckoApp.getAppEventDispatcher(); } @Override public EventDispatcher getAppEventDispatcher() { return eventDispatcher; } @Override public GeckoProfile getProfile() { return GeckoThread.getActiveProfile(); } /** * Check whether we've crashed during the last browsing session. * * @return True if the crash reporter ran after the last session. */ protected boolean updateCrashedState() { try { File crashFlag = new File(GeckoProfileDirectories.getMozillaDirectory(this), "CRASHED"); if (crashFlag.exists() && crashFlag.delete()) { // Set the flag that indicates we were stopped as expected, as // the crash reporter has run, so it is not a silent OOM crash. getSharedPreferences().edit().putBoolean(PREFS_WAS_STOPPED, true).apply(); return true; } } catch (NoMozillaDirectoryException e) { // If we can't access the Mozilla directory, we're in trouble anyway. Log.e(LOGTAG, "Cannot read crash flag: ", e); } return false; } /** * Determine whether the session should be restored. * * @param savedInstanceState Saved instance state given to the activity * @return Whether to restore */ protected boolean getSessionRestoreState(Bundle savedInstanceState) { final SharedPreferences prefs = getSharedPreferences(); boolean shouldRestore = false; final int versionCode = getVersionCode(); if (mLastSessionCrashed) { if (incrementCrashCount(prefs) <= getSessionStoreMaxCrashResumes(prefs) && getSessionRestoreAfterCrashPreference(prefs)) { shouldRestore = true; } else { shouldRestore = false; } } else if (prefs.getInt(PREFS_VERSION_CODE, 0) != versionCode) { // If the version has changed, the user has done an upgrade, so restore // previous tabs. prefs.edit().putInt(PREFS_VERSION_CODE, versionCode).apply(); shouldRestore = true; } else if (savedInstanceState != null || getSessionRestorePreference(prefs).equals("always") || getRestartFromIntent()) { // We're coming back from a background kill by the OS, the user // has chosen to always restore, or we restarted. shouldRestore = true; } return shouldRestore; } private int incrementCrashCount(SharedPreferences prefs) { final int crashCount = getSuccessiveCrashesCount(prefs) + 1; prefs.edit().putInt(PREFS_CRASHED_COUNT, crashCount).apply(); return crashCount; } private int getSuccessiveCrashesCount(SharedPreferences prefs) { return prefs.getInt(PREFS_CRASHED_COUNT, 0); } private int getSessionStoreMaxCrashResumes(SharedPreferences prefs) { return prefs.getInt(GeckoPreferences.PREFS_RESTORE_SESSION_MAX_CRASH_RESUMES, 1); } private boolean getSessionRestoreAfterCrashPreference(SharedPreferences prefs) { return prefs.getBoolean(GeckoPreferences.PREFS_RESTORE_SESSION_FROM_CRASH, true); } private String getSessionRestorePreference(SharedPreferences prefs) { return prefs.getString(GeckoPreferences.PREFS_RESTORE_SESSION, "always"); } private boolean getRestartFromIntent() { return IntentUtils.getBooleanExtraSafe(getIntent(), "didRestart", false); } /** * Enable Android StrictMode checks (for supported OS versions). * http://developer.android.com/reference/android/os/StrictMode.html */ private void enableStrictMode() { Log.d(LOGTAG, "Enabling Android StrictMode"); StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder() .detectAll() .penaltyLog() .build()); StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder() .detectAll() .penaltyLog() .build()); } @Override public void enableOrientationListener() { // Start listening for orientation events mCameraOrientationEventListener = new OrientationEventListener(this) { @Override public void onOrientationChanged(int orientation) { if (mAppStateListeners != null) { for (GeckoAppShell.AppStateListener listener: mAppStateListeners) { listener.onOrientationChanged(); } } } }; mCameraOrientationEventListener.enable(); } @Override public void disableOrientationListener() { if (mCameraOrientationEventListener != null) { mCameraOrientationEventListener.disable(); mCameraOrientationEventListener = null; } } @Override public String getDefaultUAString() { return HardwareUtils.isTablet() ? AppConstants.USER_AGENT_FENNEC_TABLET : AppConstants.USER_AGENT_FENNEC_MOBILE; } @Override public void createShortcut(final String title, final String url) { Icons.with(this) .pageUrl(url) .skipNetwork() .skipMemory() .forLauncherIcon() .build() .execute(new IconCallback() { @Override public void onIconResponse(IconResponse response) { doCreateShortcut(title, url, response.getBitmap()); } }); } private void doCreateShortcut(final String aTitle, final String aURI, final Bitmap aIcon) { // The intent to be launched by the shortcut. Intent shortcutIntent = new Intent(); shortcutIntent.setAction(GeckoApp.ACTION_HOMESCREEN_SHORTCUT); shortcutIntent.setData(Uri.parse(aURI)); shortcutIntent.setClassName(AppConstants.ANDROID_PACKAGE_NAME, AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS); Intent intent = new Intent(); intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent); intent.putExtra(Intent.EXTRA_SHORTCUT_ICON, getLauncherIcon(aIcon, GeckoAppShell.getPreferredIconSize())); if (aTitle != null) { intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, aTitle); } else { intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, aURI); } // Do not allow duplicate items. intent.putExtra("duplicate", false); intent.setAction("com.android.launcher.action.INSTALL_SHORTCUT"); getApplicationContext().sendBroadcast(intent); // Remember interaction final UrlAnnotations urlAnnotations = BrowserDB.from(getApplicationContext()).getUrlAnnotations(); urlAnnotations.insertHomeScreenShortcut(getContentResolver(), aURI, true); // After shortcut is created, show the mobile desktop. ActivityUtils.goToHomeScreen(this); } private Bitmap getLauncherIcon(Bitmap aSource, int size) { final float[] DEFAULT_LAUNCHER_ICON_HSV = { 32.0f, 1.0f, 1.0f }; final int kOffset = 6; final int kRadius = 5; int insetSize = aSource != null ? size * 2 / 3 : size; Bitmap bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(bitmap); // draw a base color Paint paint = new Paint(); if (aSource == null) { // If we aren't drawing a favicon, just use an orange color. paint.setColor(Color.HSVToColor(DEFAULT_LAUNCHER_ICON_HSV)); canvas.drawRoundRect(new RectF(kOffset, kOffset, size - kOffset, size - kOffset), kRadius, kRadius, paint); } else if (aSource.getWidth() >= insetSize || aSource.getHeight() >= insetSize) { // Otherwise, if the icon is large enough, just draw it. Rect iconBounds = new Rect(0, 0, size, size); canvas.drawBitmap(aSource, null, iconBounds, null); return bitmap; } else { // otherwise use the dominant color from the icon + a layer of transparent white to lighten it somewhat int color = BitmapUtils.getDominantColor(aSource); paint.setColor(color); canvas.drawRoundRect(new RectF(kOffset, kOffset, size - kOffset, size - kOffset), kRadius, kRadius, paint); paint.setColor(Color.argb(100, 255, 255, 255)); canvas.drawRoundRect(new RectF(kOffset, kOffset, size - kOffset, size - kOffset), kRadius, kRadius, paint); } // draw the overlay Bitmap overlay = BitmapUtils.decodeResource(this, R.drawable.home_bg); canvas.drawBitmap(overlay, null, new Rect(0, 0, size, size), null); // draw the favicon if (aSource == null) aSource = BitmapUtils.decodeResource(this, R.drawable.home_star); // by default, we scale the icon to this size int sWidth = insetSize / 2; int sHeight = sWidth; int halfSize = size / 2; canvas.drawBitmap(aSource, null, new Rect(halfSize - sWidth, halfSize - sHeight, halfSize + sWidth, halfSize + sHeight), null); return bitmap; } @Override protected void onNewIntent(Intent externalIntent) { final SafeIntent intent = new SafeIntent(externalIntent); final boolean isFirstTab = !mWasFirstTabShownAfterActivityUnhidden; mWasFirstTabShownAfterActivityUnhidden = true; // Reset since we'll be loading a tab. // if we were previously OOM killed, we can end up here when launching // from external shortcuts, so set this as the intent for initialization if (!mInitialized) { setIntent(externalIntent); return; } final String action = intent.getAction(); final String uri = getURIFromIntent(intent); final String passedUri; if (!TextUtils.isEmpty(uri)) { passedUri = uri; } else { passedUri = null; } if (ACTION_LOAD.equals(action)) { Tabs.getInstance().loadUrl(intent.getDataString()); lastSelectedTabId = -1; } else if (Intent.ACTION_VIEW.equals(action)) { processActionViewIntent(new Runnable() { @Override public void run() { final String url = intent.getDataString(); int flags = Tabs.LOADURL_NEW_TAB | Tabs.LOADURL_USER_ENTERED | Tabs.LOADURL_EXTERNAL; if (isFirstTab) { flags |= Tabs.LOADURL_FIRST_AFTER_ACTIVITY_UNHIDDEN; } Tabs.getInstance().loadUrlWithIntentExtras(url, intent, flags); } }); lastSelectedTabId = -1; } else if (ACTION_HOMESCREEN_SHORTCUT.equals(action)) { mLayerView.loadUri(uri, GeckoView.LOAD_SWITCH_TAB); } else if (Intent.ACTION_SEARCH.equals(action)) { mLayerView.loadUri(uri, GeckoView.LOAD_NEW_TAB); } else if (NotificationHelper.HELPER_BROADCAST_ACTION.equals(action)) { NotificationHelper.getInstance(getApplicationContext()).handleNotificationIntent(intent); } else if (ACTION_LAUNCH_SETTINGS.equals(action)) { // Check if launched from data reporting notification. Intent settingsIntent = new Intent(GeckoApp.this, GeckoPreferences.class); // Copy extras. settingsIntent.putExtras(intent.getUnsafe()); startActivity(settingsIntent); } else if (ACTION_SWITCH_TAB.equals(action)) { final int tabId = intent.getIntExtra("TabId", -1); Tabs.getInstance().selectTab(tabId); lastSelectedTabId = -1; } recordStartupActionTelemetry(passedUri, action); } /** * Handles getting a URI from an intent in a way that is backwards- * compatible with our previous implementations. */ protected String getURIFromIntent(SafeIntent intent) { final String action = intent.getAction(); if (ACTION_ALERT_CALLBACK.equals(action) || NotificationHelper.HELPER_BROADCAST_ACTION.equals(action)) { return null; } return intent.getDataString(); } protected int getOrientation() { return GeckoScreenOrientation.getInstance().getAndroidOrientation(); } @Override public void onResume() { // After an onPause, the activity is back in the foreground. // Undo whatever we did in onPause. super.onResume(); if (mIsAbortingAppLaunch) { return; } GeckoAppShell.setGeckoInterface(this); if (lastSelectedTabId >= 0 && (lastActiveGeckoApp == null || lastActiveGeckoApp.get() != this)) { Tabs.getInstance().selectTab(lastSelectedTabId); } int newOrientation = getResources().getConfiguration().orientation; if (GeckoScreenOrientation.getInstance().update(newOrientation)) { refreshChrome(); } if (mAppStateListeners != null) { for (GeckoAppShell.AppStateListener listener : mAppStateListeners) { listener.onResume(); } } // We use two times: a pseudo-unique wall-clock time to identify the // current session across power cycles, and the elapsed realtime to // track the duration of the session. final long now = System.currentTimeMillis(); final long realTime = android.os.SystemClock.elapsedRealtime(); ThreadUtils.postToBackgroundThread(new Runnable() { @Override public void run() { // Now construct the new session on HealthRecorder's behalf. We do this here // so it can benefit from a single near-startup prefs commit. SessionInformation currentSession = new SessionInformation(now, realTime); SharedPreferences prefs = GeckoApp.this.getSharedPreferences(); SharedPreferences.Editor editor = prefs.edit(); editor.putBoolean(GeckoApp.PREFS_WAS_STOPPED, false); if (!mLastSessionCrashed) { // The last session terminated normally, // so we can reset the count of successive crashes. editor.putInt(GeckoApp.PREFS_CRASHED_COUNT, 0); } currentSession.recordBegin(editor); editor.apply(); final HealthRecorder rec = mHealthRecorder; if (rec != null) { rec.setCurrentSession(currentSession); rec.processDelayed(); } else { Log.w(LOGTAG, "Can't record session: rec is null."); } } }); Restrictions.update(this); } @Override public void onWindowFocusChanged(boolean hasFocus) { super.onWindowFocusChanged(hasFocus); if (!mWindowFocusInitialized && hasFocus) { mWindowFocusInitialized = true; // XXX our editor tests require the GeckoView to have focus to pass, so we have to // manually shift focus to the GeckoView. requestFocus apparently doesn't work at // this stage of starting up, so we have to unset and reset the focusability. mLayerView.setFocusable(false); mLayerView.setFocusable(true); mLayerView.setFocusableInTouchMode(true); getWindow().setBackgroundDrawable(null); } } @Override public void onPause() { if (mIsAbortingAppLaunch) { super.onPause(); return; } final Tab selectedTab = Tabs.getInstance().getSelectedTab(); if (selectedTab != null) { lastSelectedTabId = selectedTab.getId(); } lastActiveGeckoApp = new WeakReference(this); final HealthRecorder rec = mHealthRecorder; final Context context = this; // In some way it's sad that Android will trigger StrictMode warnings // here as the whole point is to save to disk while the activity is not // interacting with the user. ThreadUtils.postToBackgroundThread(new Runnable() { @Override public void run() { SharedPreferences prefs = GeckoApp.this.getSharedPreferences(); SharedPreferences.Editor editor = prefs.edit(); editor.putBoolean(GeckoApp.PREFS_WAS_STOPPED, true); if (rec != null) { rec.recordSessionEnd("P", editor); } // onPause might in fact be called even after a crash, but in that case the // crash reporter will record this fact for us and we'll pick it up in onCreate. mLastSessionCrashed = false; // If we haven't done it before, cleanup any old files in our old temp dir if (prefs.getBoolean(GeckoApp.PREFS_CLEANUP_TEMP_FILES, true)) { File tempDir = GeckoLoader.getGREDir(GeckoApp.this); FileUtils.delTree(tempDir, new FileUtils.NameAndAgeFilter(null, ONE_DAY_MS), false); editor.putBoolean(GeckoApp.PREFS_CLEANUP_TEMP_FILES, false); } editor.apply(); } }); if (mAppStateListeners != null) { for (GeckoAppShell.AppStateListener listener : mAppStateListeners) { listener.onPause(); } } super.onPause(); } @Override public void onRestart() { if (mIsAbortingAppLaunch) { super.onRestart(); return; } // Faster on main thread with an async apply(). final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads(); try { SharedPreferences.Editor editor = GeckoApp.this.getSharedPreferences().edit(); editor.putBoolean(GeckoApp.PREFS_WAS_STOPPED, false); editor.apply(); } finally { StrictMode.setThreadPolicy(savedPolicy); } super.onRestart(); } @Override public void onDestroy() { if (mIsAbortingAppLaunch) { // This build does not support the Android version of the device: // We did not initialize anything, so skip cleaning up. super.onDestroy(); return; } getAppEventDispatcher().unregisterGeckoThreadListener((GeckoEventListener)this, "Gecko:Ready", "Gecko:Exited", "Accessibility:Event"); getAppEventDispatcher().unregisterGeckoThreadListener((NativeEventListener)this, "Accessibility:Ready", "Bookmark:Insert", "Contact:Add", "DevToolsAuth:Scan", "DOMFullScreen:Start", "DOMFullScreen:Stop", "Image:SetAs", "Locale:Set", "Permissions:Data", "PrivateBrowsing:Data", "RuntimePermissions:Prompt", "Sanitize:Finished", "Session:StatePurged", "Share:Text", "Snackbar:Show", "SystemUI:Visibility", "ToggleChrome:Focus", "ToggleChrome:Hide", "ToggleChrome:Show", "Update:Check", "Update:Download", "Update:Install"); if (mPromptService != null) mPromptService.destroy(); final HealthRecorder rec = mHealthRecorder; mHealthRecorder = null; if (rec != null && rec.isEnabled()) { // Closing a HealthRecorder could incur a write. ThreadUtils.postToBackgroundThread(new Runnable() { @Override public void run() { rec.close(GeckoApp.this); } }); } super.onDestroy(); Tabs.unregisterOnTabsChangedListener(this); } public void showSDKVersionError() { final String message = getString(R.string.unsupported_sdk_version, Build.CPU_ABI, Build.VERSION.SDK_INT); Toast.makeText(this, message, Toast.LENGTH_LONG).show(); } // Get a temporary directory, may return null public static File getTempDirectory() { File dir = GeckoApplication.get().getExternalFilesDir("temp"); return dir; } // Delete any files in our temporary directory public static void deleteTempFiles() { File dir = getTempDirectory(); if (dir == null) return; File[] files = dir.listFiles(); if (files == null) return; for (File file : files) { file.delete(); } } @Override public void onConfigurationChanged(Configuration newConfig) { Log.d(LOGTAG, "onConfigurationChanged: " + newConfig.locale); final LocaleManager localeManager = BrowserLocaleManager.getInstance(); final Locale changed = localeManager.onSystemConfigurationChanged(this, getResources(), newConfig, mLastLocale); if (changed != null) { onLocaleChanged(Locales.getLanguageTag(changed)); } // onConfigurationChanged is not called for 180 degree orientation changes, // we will miss such rotations and the screen orientation will not be // updated. if (GeckoScreenOrientation.getInstance().update(newConfig.orientation)) { if (mFormAssistPopup != null) mFormAssistPopup.hide(); refreshChrome(); } super.onConfigurationChanged(newConfig); } public String getContentProcessName() { return AppConstants.MOZ_CHILD_PROCESS_NAME; } public void addEnvToIntent(Intent intent) { Map envMap = System.getenv(); Set> envSet = envMap.entrySet(); Iterator> envIter = envSet.iterator(); int c = 0; while (envIter.hasNext()) { Map.Entry entry = envIter.next(); intent.putExtra("env" + c, entry.getKey() + "=" + entry.getValue()); c++; } } @Override public void doRestart() { doRestart(null, null); } public void doRestart(String args) { doRestart(args, null); } public void doRestart(Intent intent) { doRestart(null, intent); } public void doRestart(String args, Intent restartIntent) { if (restartIntent == null) { restartIntent = new Intent(Intent.ACTION_MAIN); } if (args != null) { restartIntent.putExtra("args", args); } mRestartIntent = restartIntent; Log.d(LOGTAG, "doRestart(\"" + restartIntent + "\")"); doShutdown(); } private void doShutdown() { // Shut down GeckoApp activity. runOnUiThread(new Runnable() { @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) @Override public void run() { if (!isFinishing() && (Versions.preJBMR1 || !isDestroyed())) { finish(); } } }); } private void checkMigrateProfile() { final File profileDir = getProfile().getDir(); if (profileDir != null) { ThreadUtils.postToBackgroundThread(new Runnable() { @Override public void run() { Handler handler = new Handler(); handler.postDelayed(new DeferredCleanupTask(), CLEANUP_DEFERRAL_SECONDS * 1000); } }); } } private static class DeferredCleanupTask implements Runnable { // The cleanup-version setting is recorded to avoid repeating the same // tasks on subsequent startups; CURRENT_CLEANUP_VERSION may be updated // if we need to do additional cleanup for future Gecko versions. private static final String CLEANUP_VERSION = "cleanup-version"; private static final int CURRENT_CLEANUP_VERSION = 1; @Override public void run() { final Context context = GeckoAppShell.getApplicationContext(); long cleanupVersion = GeckoSharedPrefs.forApp(context).getInt(CLEANUP_VERSION, 0); if (cleanupVersion < 1) { // Reduce device storage footprint by removing .ttf files from // the res/fonts directory: we no longer need to copy our // bundled fonts out of the APK in order to use them. // See https://bugzilla.mozilla.org/show_bug.cgi?id=878674. File dir = new File("res/fonts"); if (dir.exists() && dir.isDirectory()) { for (File file : dir.listFiles()) { if (file.isFile() && file.getName().endsWith(".ttf")) { file.delete(); } } if (!dir.delete()) { Log.w(LOGTAG, "unable to delete res/fonts directory (not empty?)"); } } } // Additional cleanup needed for future versions would go here if (cleanupVersion != CURRENT_CLEANUP_VERSION) { SharedPreferences.Editor editor = GeckoSharedPrefs.forApp(context).edit(); editor.putInt(CLEANUP_VERSION, CURRENT_CLEANUP_VERSION); editor.apply(); } } } protected void onDone() { moveTaskToBack(true); } @Override public void onBackPressed() { if (getSupportFragmentManager().getBackStackEntryCount() > 0) { super.onBackPressed(); return; } if (autoHideTabs()) { return; } if (mDoorHangerPopup != null && mDoorHangerPopup.isShowing()) { mDoorHangerPopup.dismiss(); return; } if (mFullScreenPluginView != null) { GeckoAppShell.onFullScreenPluginHidden(mFullScreenPluginView); removeFullScreenPluginView(mFullScreenPluginView); return; } if (mLayerView != null && mLayerView.isFullScreen()) { GeckoAppShell.notifyObservers("FullScreen:Exit", null); return; } final Tabs tabs = Tabs.getInstance(); final Tab tab = tabs.getSelectedTab(); if (tab == null) { onDone(); return; } // Give Gecko a chance to handle the back press first, then fallback to the Java UI. GeckoAppShell.sendRequestToGecko(new GeckoRequest("Browser:OnBackPressed", null) { @Override public void onResponse(NativeJSObject nativeJSObject) { if (!nativeJSObject.getBoolean("handled")) { // Default behavior is Gecko didn't prevent. onDefault(); } } @Override public void onError(NativeJSObject error) { // Default behavior is Gecko didn't prevent, via failure. onDefault(); } // Return from Gecko thread, then back-press through the Java UI. private void onDefault() { ThreadUtils.postToUiThread(new Runnable() { @Override public void run() { if (tab.doBack()) { return; } if (tab.isExternal()) { onDone(); Tab nextSelectedTab = Tabs.getInstance().getNextTab(tab); if (nextSelectedTab != null) { int nextSelectedTabId = nextSelectedTab.getId(); GeckoAppShell.notifyObservers("Tab:KeepZombified", Integer.toString(nextSelectedTabId)); } tabs.closeTab(tab); return; } final int parentId = tab.getParentId(); final Tab parent = tabs.getTab(parentId); if (parent != null) { // The back button should always return to the parent (not a sibling). tabs.closeTab(tab, parent); return; } onDone(); } }); } }); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (!ActivityHandlerHelper.handleActivityResult(requestCode, resultCode, data)) { super.onActivityResult(requestCode, resultCode, data); } } @Override public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { Permissions.onRequestPermissionsResult(this, permissions, grantResults); } @Override public AbsoluteLayout getPluginContainer() { return mPluginContainer; } private static final String CPU = "cpu"; private static final String SCREEN = "screen"; // Called when a Gecko Hal WakeLock is changed @Override // We keep the wake lock independent from the function scope, so we need to // suppress the linter warning. @SuppressLint("Wakelock") public void notifyWakeLockChanged(String topic, String state) { PowerManager.WakeLock wl = mWakeLocks.get(topic); if (state.equals("locked-foreground") && wl == null) { PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); if (CPU.equals(topic)) { wl = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, topic); } else if (SCREEN.equals(topic)) { // ON_AFTER_RELEASE is set, the user activity timer will be reset when the // WakeLock is released, causing the illumination to remain on a bit longer. wl = pm.newWakeLock(PowerManager.SCREEN_BRIGHT_WAKE_LOCK | PowerManager.ON_AFTER_RELEASE, topic); } if (wl != null) { wl.acquire(); mWakeLocks.put(topic, wl); } } else if (!state.equals("locked-foreground") && wl != null) { wl.release(); mWakeLocks.remove(topic); } } @Override public void notifyCheckUpdateResult(String result) { GeckoAppShell.notifyObservers("Update:CheckResult", result); } private void geckoConnected() { mLayerView.setOverScrollMode(View.OVER_SCROLL_NEVER); } @Override public void setAccessibilityEnabled(boolean enabled) { } @Override public boolean openUriExternal(String targetURI, String mimeType, String packageName, String className, String action, String title) { // Default to showing prompt in private browsing to be safe. return IntentHelper.openUriExternal(targetURI, mimeType, packageName, className, action, title, true); } public static class MainLayout extends RelativeLayout { private TouchEventInterceptor mTouchEventInterceptor; private MotionEventInterceptor mMotionEventInterceptor; public MainLayout(Context context, AttributeSet attrs) { super(context, attrs); } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); } public void setTouchEventInterceptor(TouchEventInterceptor interceptor) { mTouchEventInterceptor = interceptor; } public void setMotionEventInterceptor(MotionEventInterceptor interceptor) { mMotionEventInterceptor = interceptor; } @Override public boolean onInterceptTouchEvent(MotionEvent event) { if (mTouchEventInterceptor != null && mTouchEventInterceptor.onInterceptTouchEvent(this, event)) { return true; } return super.onInterceptTouchEvent(event); } @Override public boolean onTouchEvent(MotionEvent event) { if (mTouchEventInterceptor != null && mTouchEventInterceptor.onTouch(this, event)) { return true; } return super.onTouchEvent(event); } @Override public boolean onGenericMotionEvent(MotionEvent event) { if (mMotionEventInterceptor != null && mMotionEventInterceptor.onInterceptMotionEvent(this, event)) { return true; } return super.onGenericMotionEvent(event); } @Override public void setDrawingCacheEnabled(boolean enabled) { // Instead of setting drawing cache in the view itself, we simply // enable drawing caching on its children. This is mainly used in // animations (see PropertyAnimator) super.setChildrenDrawnWithCacheEnabled(enabled); } } private class FullScreenHolder extends FrameLayout { public FullScreenHolder(Context ctx) { super(ctx); setBackgroundColor(0xff000000); } @Override public void addView(View view, int index) { /** * This normally gets called when Flash adds a separate SurfaceView * for the video. It is unhappy if we have the LayerView underneath * it for some reason so we need to hide that. Hiding the LayerView causes * its surface to be destroyed, which causes a pause composition * event to be sent to Gecko. We synchronously wait for that to be * processed. Simultaneously, however, Flash is waiting on a mutex so * the post() below is an attempt to avoid a deadlock. */ super.addView(view, index); ThreadUtils.postToUiThread(new Runnable() { @Override public void run() { mLayerView.hideSurface(); } }); } /** * The methods below are simply copied from what Android WebKit does. * It wasn't ever called in my testing, but might as well * keep it in case it is for some reason. The methods * all return true because we don't want any events * leaking out from the fullscreen view. */ @Override public boolean onKeyDown(int keyCode, KeyEvent event) { if (event.isSystem()) { return super.onKeyDown(keyCode, event); } mFullScreenPluginView.onKeyDown(keyCode, event); return true; } @Override public boolean onKeyUp(int keyCode, KeyEvent event) { if (event.isSystem()) { return super.onKeyUp(keyCode, event); } mFullScreenPluginView.onKeyUp(keyCode, event); return true; } @Override public boolean onTouchEvent(MotionEvent event) { return true; } @Override public boolean onTrackballEvent(MotionEvent event) { mFullScreenPluginView.onTrackballEvent(event); return true; } } private int getVersionCode() { int versionCode = 0; try { versionCode = getPackageManager().getPackageInfo(getPackageName(), 0).versionCode; } catch (NameNotFoundException e) { Log.wtf(LOGTAG, getPackageName() + " not found", e); } return versionCode; } // FHR reason code for a session end prior to a restart for a // locale change. private static final String SESSION_END_LOCALE_CHANGED = "L"; /** * This exists so that a locale can be applied in two places: when saved * in a nested activity, and then again when we get back up to GeckoApp. * * GeckoApp needs to do a bunch more stuff than, say, GeckoPreferences. */ protected void onLocaleChanged(final String locale) { final boolean startNewSession = true; final boolean shouldRestart = false; // If the HealthRecorder is not yet initialized (unlikely), the locale change won't // trigger a session transition and subsequent events will be recorded in an environment // with the wrong locale. final HealthRecorder rec = mHealthRecorder; if (rec != null) { rec.onAppLocaleChanged(locale); rec.onEnvironmentChanged(startNewSession, SESSION_END_LOCALE_CHANGED); } if (!shouldRestart) { ThreadUtils.postToUiThread(new Runnable() { @Override public void run() { GeckoApp.this.onLocaleReady(locale); } }); return; } // Do this in the background so that the health recorder has its // time to finish. ThreadUtils.postToBackgroundThread(new Runnable() { @Override public void run() { GeckoApp.this.doRestart(); } }); } /** * Use BrowserLocaleManager to change our persisted and current locales, * and poke the system to tell it of our changed state. */ protected void setLocale(final String locale) { if (locale == null) { return; } final String resultant = BrowserLocaleManager.getInstance().setSelectedLocale(this, locale); if (resultant == null) { return; } onLocaleChanged(resultant); } private void setSystemUiVisible(final boolean visible) { ThreadUtils.postToUiThread(new Runnable() { @Override public void run() { if (visible) { mMainLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE); } else { mMainLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE); } } }); } protected HealthRecorder createHealthRecorder(final Context context, final String profilePath, final EventDispatcher dispatcher, final String osLocale, final String appLocale, final SessionInformation previousSession) { // GeckoApp does not need to record any health information - return a stub. return new StubbedHealthRecorder(); } protected void recordStartupActionTelemetry(final String passedURL, final String action) { } @Override public void checkUriVisited(String uri) { GlobalHistory.getInstance().checkUriVisited(uri); } @Override public void markUriVisited(final String uri) { final Context context = getApplicationContext(); final BrowserDB db = BrowserDB.from(context); ThreadUtils.postToBackgroundThread(new Runnable() { @Override public void run() { GlobalHistory.getInstance().add(context, db, uri); } }); } @Override public void setUriTitle(final String uri, final String title) { final Context context = getApplicationContext(); final BrowserDB db = BrowserDB.from(context); ThreadUtils.postToBackgroundThread(new Runnable() { @Override public void run() { GlobalHistory.getInstance().update(context.getContentResolver(), db, uri, title); } }); } @Override public String[] getHandlersForMimeType(String mimeType, String action) { Intent intent = IntentHelper.getIntentForActionString(action); if (mimeType != null && mimeType.length() > 0) intent.setType(mimeType); return IntentHelper.getHandlersForIntent(intent); } @Override public String[] getHandlersForURL(String url, String action) { // May contain the whole URL or just the protocol. Uri uri = url.indexOf(':') >= 0 ? Uri.parse(url) : new Uri.Builder().scheme(url).build(); Intent intent = IntentHelper.getOpenURIIntent(getApplicationContext(), uri.toString(), "", TextUtils.isEmpty(action) ? Intent.ACTION_VIEW : action, ""); return IntentHelper.getHandlersForIntent(intent); } @Override public String getDefaultChromeURI() { // Use the chrome URI specified by Gecko's defaultChromeURI pref. return null; } public GeckoView getGeckoView() { return mLayerView; } }