fune/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
Jan Henning 3a1892a89a Bug 832990 - Part 2 - Save and restore menu item caches via savedInstanceState. r=Grisha
MozReview-Commit-ID: 6x5BDqhwabS

--HG--
extra : rebase_source : ac208a2eee5d26b9da765101ddf439a50087df9c
2018-01-25 20:47:16 +01:00

4576 lines
180 KiB
Java

/* -*- 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 android.Manifest;
import android.annotation.TargetApi;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.DownloadManager;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.nfc.NdefMessage;
import android.nfc.NdefRecord;
import android.nfc.NfcAdapter;
import android.nfc.NfcEvent;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.StrictMode;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.StringRes;
import android.support.annotation.UiThread;
import android.support.design.widget.Snackbar;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.NotificationCompat;
import android.support.v4.content.res.ResourcesCompat;
import android.support.v4.view.MenuItemCompat;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.view.HapticFeedbackConstants;
import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.SubMenu;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.ViewStub;
import android.view.ViewTreeObserver;
import android.view.Window;
import android.view.animation.Interpolator;
import android.widget.Button;
import android.widget.ListView;
import android.widget.ViewFlipper;
import org.mozilla.gecko.AppConstants.Versions;
import org.mozilla.gecko.DynamicToolbar.VisibilityTransition;
import org.mozilla.gecko.Tabs.TabEvents;
import org.mozilla.gecko.activitystream.ActivityStream;
import org.mozilla.gecko.activitystream.ActivityStreamTelemetry;
import org.mozilla.gecko.adjust.AdjustBrowserAppDelegate;
import org.mozilla.gecko.animation.PropertyAnimator;
import org.mozilla.gecko.annotation.RobocopTarget;
import org.mozilla.gecko.bookmarks.BookmarkEditFragment;
import org.mozilla.gecko.bookmarks.BookmarkUtils;
import org.mozilla.gecko.bookmarks.EditBookmarkTask;
import org.mozilla.gecko.cleanup.FileCleanupController;
import org.mozilla.gecko.db.BrowserContract;
import org.mozilla.gecko.db.BrowserDB;
import org.mozilla.gecko.db.SuggestedSites;
import org.mozilla.gecko.delegates.BookmarkStateChangeDelegate;
import org.mozilla.gecko.delegates.BrowserAppDelegate;
import org.mozilla.gecko.delegates.OfflineTabStatusDelegate;
import org.mozilla.gecko.delegates.ScreenshotDelegate;
import org.mozilla.gecko.distribution.Distribution;
import org.mozilla.gecko.distribution.DistributionStoreCallback;
import org.mozilla.gecko.dlc.DownloadContentService;
import org.mozilla.gecko.extensions.ExtensionPermissionsHelper;
import org.mozilla.gecko.firstrun.FirstrunAnimationContainer;
import org.mozilla.gecko.gfx.DynamicToolbarAnimator;
import org.mozilla.gecko.gfx.DynamicToolbarAnimator.PinReason;
import org.mozilla.gecko.home.BrowserSearch;
import org.mozilla.gecko.home.HomeBanner;
import org.mozilla.gecko.home.HomeConfig;
import org.mozilla.gecko.home.HomeConfig.PanelType;
import org.mozilla.gecko.home.HomeConfigPrefsBackend;
import org.mozilla.gecko.home.HomeFragment;
import org.mozilla.gecko.home.HomePager.OnUrlOpenInBackgroundListener;
import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
import org.mozilla.gecko.home.HomePanelsManager;
import org.mozilla.gecko.home.HomeScreen;
import org.mozilla.gecko.home.SearchEngine;
import org.mozilla.gecko.icons.Icons;
import org.mozilla.gecko.icons.IconsHelper;
import org.mozilla.gecko.icons.decoders.FaviconDecoder;
import org.mozilla.gecko.icons.decoders.IconDirectoryEntry;
import org.mozilla.gecko.icons.decoders.LoadFaviconResult;
import org.mozilla.gecko.lwt.LightweightTheme;
import org.mozilla.gecko.media.VideoPlayer;
import org.mozilla.gecko.menu.GeckoMenu;
import org.mozilla.gecko.menu.GeckoMenuItem;
import org.mozilla.gecko.mma.MmaDelegate;
import org.mozilla.gecko.mozglue.GeckoLoader;
import org.mozilla.gecko.mozglue.SafeIntent;
import org.mozilla.gecko.notifications.NotificationHelper;
import org.mozilla.gecko.overlays.ui.ShareDialog;
import org.mozilla.gecko.permissions.Permissions;
import org.mozilla.gecko.preferences.ClearOnShutdownPref;
import org.mozilla.gecko.preferences.GeckoPreferences;
import org.mozilla.gecko.promotion.AddToHomeScreenPromotion;
import org.mozilla.gecko.promotion.ReaderViewBookmarkPromotion;
import org.mozilla.gecko.prompts.Prompt;
import org.mozilla.gecko.reader.ReaderModeUtils;
import org.mozilla.gecko.reader.ReadingListHelper;
import org.mozilla.gecko.reader.SavedReaderViewHelper;
import org.mozilla.gecko.restrictions.Restrictable;
import org.mozilla.gecko.restrictions.Restrictions;
import org.mozilla.gecko.search.SearchEngineManager;
import org.mozilla.gecko.switchboard.AsyncConfigLoader;
import org.mozilla.gecko.switchboard.SwitchBoard;
import org.mozilla.gecko.sync.repositories.android.FennecTabsRepository;
import org.mozilla.gecko.tabqueue.TabQueueHelper;
import org.mozilla.gecko.tabqueue.TabQueuePrompt;
import org.mozilla.gecko.tabs.TabHistoryController;
import org.mozilla.gecko.tabs.TabHistoryController.OnShowTabHistory;
import org.mozilla.gecko.tabs.TabHistoryFragment;
import org.mozilla.gecko.tabs.TabHistoryPage;
import org.mozilla.gecko.tabs.TabsPanel;
import org.mozilla.gecko.telemetry.TelemetryCorePingDelegate;
import org.mozilla.gecko.telemetry.TelemetryUploadService;
import org.mozilla.gecko.telemetry.measurements.SearchCountMeasurements;
import org.mozilla.gecko.toolbar.AutocompleteHandler;
import org.mozilla.gecko.toolbar.BrowserToolbar;
import org.mozilla.gecko.toolbar.BrowserToolbar.TabEditingState;
import org.mozilla.gecko.toolbar.PwaConfirm;
import org.mozilla.gecko.trackingprotection.TrackingProtectionPrompt;
import org.mozilla.gecko.updater.PostUpdateHandler;
import org.mozilla.gecko.updater.UpdateServiceHelper;
import org.mozilla.gecko.util.ActivityUtils;
import org.mozilla.gecko.util.ContextUtils;
import org.mozilla.gecko.util.DrawableUtil;
import org.mozilla.gecko.util.EventCallback;
import org.mozilla.gecko.util.GamepadUtils;
import org.mozilla.gecko.util.GeckoBundle;
import org.mozilla.gecko.util.HardwareUtils;
import org.mozilla.gecko.util.IntentUtils;
import org.mozilla.gecko.util.MenuUtils;
import org.mozilla.gecko.util.PrefUtils;
import org.mozilla.gecko.util.ShortcutUtils;
import org.mozilla.gecko.util.StringUtils;
import org.mozilla.gecko.util.ThreadUtils;
import org.mozilla.gecko.util.WindowUtil;
import org.mozilla.gecko.widget.ActionModePresenter;
import org.mozilla.gecko.widget.AnchoredPopup;
import org.mozilla.gecko.widget.AnimatedProgressBar;
import org.mozilla.gecko.widget.GeckoActionProvider;
import org.mozilla.gecko.widget.SplashScreen;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.regex.Pattern;
import static org.mozilla.gecko.mma.MmaDelegate.NEW_TAB;
public class BrowserApp extends GeckoApp
implements ActionModePresenter,
AnchoredPopup.OnVisibilityChangeListener,
BookmarkEditFragment.Callbacks,
BrowserSearch.OnEditSuggestionListener,
BrowserSearch.OnSearchListener,
DynamicToolbarAnimator.ToolbarChromeProxy,
LayoutInflater.Factory,
LightweightTheme.OnChangeListener,
OnUrlOpenListener,
OnUrlOpenInBackgroundListener,
PropertyAnimator.PropertyAnimationListener,
TabsPanel.TabsLayoutChangeListener,
View.OnKeyListener {
private static final String LOGTAG = "GeckoBrowserApp";
private static final int TABS_ANIMATION_DURATION = 450;
// Intent String extras used to specify custom Switchboard configurations.
private static final String INTENT_KEY_SWITCHBOARD_SERVER = "switchboard-server";
// TODO: Replace with kinto endpoint.
private static final String SWITCHBOARD_SERVER = "https://firefox.settings.services.mozilla.com/v1/buckets/fennec/collections/experiments/records";
private static final String STATE_ABOUT_HOME_TOP_PADDING = "abouthome_top_padding";
private static final String STATE_ADDON_MENU_ITEM_CACHE = "menuitems_cache";
private static final String STATE_BROWSER_ACTION_ITEM_CACHE = "browseractions_cache";
private static final String BROWSER_SEARCH_TAG = "browser_search";
// Request ID for startActivityForResult.
public static final int ACTIVITY_REQUEST_PREFERENCES = 1001;
private static final int ACTIVITY_REQUEST_TAB_QUEUE = 2001;
public static final int ACTIVITY_REQUEST_FIRST_READERVIEW_BOOKMARK = 3001;
public static final int ACTIVITY_RESULT_FIRST_READERVIEW_BOOKMARKS_GOTO_BOOKMARKS = 3002;
public static final int ACTIVITY_RESULT_FIRST_READERVIEW_BOOKMARKS_IGNORE = 3003;
public static final int ACTIVITY_REQUEST_TRIPLE_READERVIEW = 4001;
public static final int ACTIVITY_RESULT_TRIPLE_READERVIEW_ADD_BOOKMARK = 4002;
public static final int ACTIVITY_RESULT_TRIPLE_READERVIEW_IGNORE = 4003;
public static final String ACTION_VIEW_MULTIPLE = AppConstants.ANDROID_PACKAGE_NAME + ".action.VIEW_MULTIPLE";
@RobocopTarget
public static final String EXTRA_SKIP_STARTPANE = "skipstartpane";
private static final String EOL_NOTIFIED = "eol_notified";
private BrowserSearch mBrowserSearch;
private View mBrowserSearchContainer;
public ViewGroup mBrowserChrome;
public ViewFlipper mActionBarFlipper;
public ActionModeCompatView mActionBar;
private VideoPlayer mVideoPlayer;
private BrowserToolbar mBrowserToolbar;
private View doorhangerOverlay;
// We can't name the TabStrip class because it's not included on API 9.
private TabStripInterface mTabStrip;
private AnimatedProgressBar mProgressView;
private FirstrunAnimationContainer mFirstrunAnimationContainer;
private HomeScreen mHomeScreen;
private TabsPanel mTabsPanel;
private boolean showSplashScreen = false;
private SplashScreen splashScreen;
/**
* Container for the home screen implementation. This will be populated with any valid
* home screen implementation (currently that is just the HomePager, but that will be extended
* to permit further experimental replacement panels such as the activity-stream panel).
*/
private ViewGroup mHomeScreenContainer;
private int mCachedRecentTabsCount;
private ActionModeCompat mActionMode;
private TabHistoryController tabHistoryController;
private static final int GECKO_TOOLS_MENU = -1;
private static final int ADDON_MENU_OFFSET = 1000;
private static final int BROWSER_ACTION_MENU_OFFSET = 10000;
public static final String TAB_HISTORY_FRAGMENT_TAG = "tabHistoryFragment";
// When the static action bar is shown, only the real toolbar chrome should be
// shown when the toolbar is visible. Causing the toolbar animator to also
// show the snapshot causes the content to shift under the users finger.
// See: Bug 1358554
private boolean mShowingToolbarChromeForActionBar;
private static class MenuItemInfo implements Parcelable {
public int id;
public String label;
public boolean checkable;
public boolean checked;
public boolean enabled = true;
public boolean visible = true;
public int parent;
public boolean added; // So we can re-add after a locale change.
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(id);
dest.writeString(label);
dest.writeInt(checkable ? 1 : 0);
dest.writeInt(checked ? 1 : 0);
dest.writeInt(enabled ? 1 : 0);
dest.writeInt(visible ? 1 : 0);
dest.writeInt(parent);
dest.writeInt(added ? 1 : 0);
}
public static final Parcelable.Creator<MenuItemInfo> CREATOR
= new Parcelable.Creator<MenuItemInfo>() {
@Override
public MenuItemInfo createFromParcel(Parcel source) {
return new MenuItemInfo(source);
}
@Override
public MenuItemInfo[] newArray(int size) {
return new MenuItemInfo[size];
}
};
private MenuItemInfo(Parcel source) {
id = source.readInt();
label = source.readString();
checkable = source.readInt() != 0;
checked = source.readInt() != 0;
enabled = source.readInt() != 0;
visible = source.readInt() != 0;
parent = source.readInt();
added = source.readInt() != 0;
}
public MenuItemInfo() { }
}
private static class BrowserActionItemInfo extends MenuItemInfo {
public String uuid;
@Override
public void writeToParcel(Parcel dest, int flags) {
super.writeToParcel(dest, flags);
dest.writeString(uuid);
}
public static final Parcelable.Creator<BrowserActionItemInfo> CREATOR
= new Parcelable.Creator<BrowserActionItemInfo>() {
@Override
public BrowserActionItemInfo createFromParcel(Parcel source) {
return new BrowserActionItemInfo(source);
}
@Override
public BrowserActionItemInfo[] newArray(int size) {
return new BrowserActionItemInfo[size];
}
};
private BrowserActionItemInfo(Parcel source) {
super(source);
uuid = source.readString();
}
public BrowserActionItemInfo() { }
}
// The types of guest mode dialogs we show.
public static enum GuestModeDialog {
ENTERING,
LEAVING
}
private ArrayList<MenuItemInfo> mAddonMenuItemsCache;
private ArrayList<BrowserActionItemInfo> mBrowserActionItemsCache;
private PropertyAnimator mMainLayoutAnimator;
private static final Interpolator sTabsInterpolator = new Interpolator() {
@Override
public float getInterpolation(float t) {
t -= 1.0f;
return t * t * t * t * t + 1.0f;
}
};
private FindInPageBar mFindInPageBar;
private MediaCastingBar mMediaCastingBar;
// We'll ask for feedback after the user launches the app this many times.
private static final int FEEDBACK_LAUNCH_COUNT = 15;
// Stored value of the toolbar height, so we know when it's changed.
private int mToolbarHeight;
private SharedPreferencesHelper mSharedPreferencesHelper;
private ReadingListHelper mReadingListHelper;
private AccountsHelper mAccountsHelper;
private ExtensionPermissionsHelper mExtensionPermissionsHelper;
// The tab to be selected on editing mode exit.
private Integer mTargetTabForEditingMode;
private final TabEditingState mLastTabEditingState = new TabEditingState();
// The animator used to toggle HomePager visibility has a race where if the HomePager is shown
// (starting the animation), the HomePager is hidden, and the HomePager animation completes,
// both the web content and the HomePager will be hidden. This flag is used to prevent the
// race by determining if the web content should be hidden at the animation's end.
private boolean mHideWebContentOnAnimationEnd;
private final DynamicToolbar mDynamicToolbar = new DynamicToolbar();
private final TelemetryCorePingDelegate mTelemetryCorePingDelegate = new TelemetryCorePingDelegate();
private final List<BrowserAppDelegate> delegates = Collections.unmodifiableList(Arrays.asList(
new AddToHomeScreenPromotion(),
new ScreenshotDelegate(),
new BookmarkStateChangeDelegate(),
new ReaderViewBookmarkPromotion(),
new PostUpdateHandler(),
mTelemetryCorePingDelegate,
new OfflineTabStatusDelegate(),
new AdjustBrowserAppDelegate(mTelemetryCorePingDelegate)
));
@NonNull
private SearchEngineManager mSearchEngineManager; // Contains reference to Context - DO NOT LEAK!
private boolean mHasResumed;
@Override
public View onCreateView(final View parent, final String name, final Context context, final AttributeSet attrs) {
final View view;
if (BrowserToolbar.class.getName().equals(name)) {
view = BrowserToolbar.create(context, attrs);
} else if (TabsPanel.TabsLayout.class.getName().equals(name)) {
view = TabsPanel.createTabsLayout(context, attrs);
} else {
view = super.onCreateView(name, context, attrs);
}
return view;
}
@Override
public void onTabChanged(Tab tab, TabEvents msg, String data) {
if (!mInitialized) {
super.onTabChanged(tab, msg, data);
return;
}
if (tab == null) {
// Only RESTORED is allowed a null tab: it's the only event that
// isn't tied to a specific tab.
if (msg != Tabs.TabEvents.RESTORED) {
throw new IllegalArgumentException("onTabChanged:" + msg + " must specify a tab.");
}
final Tab selectedTab = Tabs.getInstance().getSelectedTab();
if (selectedTab != null) {
// After restoring the tabs we want to update the home pager immediately. Otherwise we
// might wait for an event coming from Gecko and this can take several seconds. (Bug 1283627)
updateHomePagerForTab(selectedTab);
}
return;
}
Log.d(LOGTAG, "BrowserApp.onTabChanged: " + tab.getId() + ": " + msg);
switch (msg) {
case SELECTED:
if (mVideoPlayer.isPlaying()) {
mVideoPlayer.stop();
}
if (Tabs.getInstance().isSelectedTab(tab) && mDynamicToolbar.isEnabled()) {
final VisibilityTransition transition = (tab.getShouldShowToolbarWithoutAnimationOnFirstSelection()) ?
VisibilityTransition.IMMEDIATE : VisibilityTransition.ANIMATE;
mDynamicToolbar.setVisible(true, transition);
// The first selection has happened - reset the state.
tab.setShouldShowToolbarWithoutAnimationOnFirstSelection(false);
}
// fall through
case LOCATION_CHANGE:
if (Tabs.getInstance().isSelectedTab(tab)) {
updateHomePagerForTab(tab);
}
if (mShowingToolbarChromeForActionBar) {
mDynamicToolbar.setVisible(true, VisibilityTransition.IMMEDIATE);
mShowingToolbarChromeForActionBar = false;
}
break;
case START:
if (Tabs.getInstance().isSelectedTab(tab)) {
invalidateOptionsMenu();
if (mDynamicToolbar.isEnabled()) {
mDynamicToolbar.setVisible(true, VisibilityTransition.ANIMATE);
}
}
break;
case LOAD_ERROR:
case STOP:
case MENU_UPDATED:
if (Tabs.getInstance().isSelectedTab(tab)) {
invalidateOptionsMenu();
}
break;
case PAGE_SHOW:
tab.loadFavicon();
break;
case UNSELECTED:
// We receive UNSELECTED immediately after the SELECTED listeners run
// so we are ensured that the unselectedTabEditingText has not changed.
if (tab.isEditing()) {
// Copy to avoid constructing new objects.
tab.getEditingState().copyFrom(mLastTabEditingState);
}
break;
case START_EDITING:
enterEditingMode();
break;
}
if (HardwareUtils.isTablet() && msg == TabEvents.SELECTED) {
updateEditingModeForTab(tab);
}
super.onTabChanged(tab, msg, data);
}
private void updateEditingModeForTab(final Tab selectedTab) {
// (bug 1086983 comment 11) Because the tab may be selected from the gecko thread and we're
// running this code on the UI thread, the selected tab argument may not still refer to the
// selected tab. However, that means this code should be run again and the initial state
// changes will be overridden. As an optimization, we can skip this update, but it may have
// unknown side-effects so we don't.
if (!Tabs.getInstance().isSelectedTab(selectedTab)) {
Log.w(LOGTAG, "updateEditingModeForTab: Given tab is expected to be selected tab");
}
saveTabEditingState(mLastTabEditingState);
if (selectedTab.isEditing()) {
enterEditingMode();
restoreTabEditingState(selectedTab.getEditingState());
} else {
mBrowserToolbar.cancelEdit();
}
}
private void saveTabEditingState(final TabEditingState editingState) {
mBrowserToolbar.saveTabEditingState(editingState);
editingState.setIsBrowserSearchShown(mBrowserSearch.getUserVisibleHint());
}
private void restoreTabEditingState(final TabEditingState editingState) {
mBrowserToolbar.restoreTabEditingState(editingState);
// Since changing the editing text will show/hide browser search, this
// must be called after we restore the editing state in the edit text View.
if (editingState.isBrowserSearchShown()) {
showBrowserSearch();
} else {
hideBrowserSearch();
}
}
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
if (AndroidGamepadManager.handleKeyEvent(event)) {
return true;
}
// Global onKey handler. This is called if the focused UI doesn't
// handle the key event, and before Gecko swallows the events.
if (event.getAction() != KeyEvent.ACTION_DOWN) {
return false;
}
if ((event.getSource() & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) {
switch (keyCode) {
case KeyEvent.KEYCODE_BUTTON_Y:
// Toggle/focus the address bar on gamepad-y button.
if (mBrowserChrome.getVisibility() == View.VISIBLE) {
if (mDynamicToolbar.isEnabled() && !isHomePagerVisible()) {
mDynamicToolbar.setVisible(false, VisibilityTransition.ANIMATE);
if (mLayerView != null) {
mLayerView.requestFocus();
}
} else {
// Just focus the address bar when about:home is visible
// or when the dynamic toolbar isn't enabled.
mBrowserToolbar.requestFocusFromTouch();
}
} else {
mDynamicToolbar.setVisible(true, VisibilityTransition.ANIMATE);
mBrowserToolbar.requestFocusFromTouch();
}
return true;
case KeyEvent.KEYCODE_BUTTON_L1:
// Go back on L1
Tabs.getInstance().getSelectedTab().doBack();
return true;
case KeyEvent.KEYCODE_BUTTON_R1:
// Go forward on R1
Tabs.getInstance().getSelectedTab().doForward();
return true;
}
}
// Check if this was a shortcut. Meta keys exists only on 11+.
final Tab tab = Tabs.getInstance().getSelectedTab();
if (tab != null && event.isCtrlPressed()) {
switch (keyCode) {
case KeyEvent.KEYCODE_LEFT_BRACKET:
tab.doBack();
return true;
case KeyEvent.KEYCODE_RIGHT_BRACKET:
tab.doForward();
return true;
case KeyEvent.KEYCODE_R:
tab.doReload(event.isShiftPressed() ? true : false);
return true;
case KeyEvent.KEYCODE_PERIOD:
tab.doStop();
return true;
case KeyEvent.KEYCODE_T:
addTab();
return true;
case KeyEvent.KEYCODE_W:
Tabs.getInstance().closeTab(tab);
return true;
case KeyEvent.KEYCODE_F:
mFindInPageBar.show(mBrowserToolbar.isPrivateMode());
return true;
}
}
return false;
}
private Runnable mCheckLongPress;
{
// Only initialise the runnable if we are >= N.
// See onKeyDown() for more details of the back-button long-press workaround
if (!Versions.preN) {
mCheckLongPress = new Runnable() {
public void run() {
handleBackLongPress();
}
};
}
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
// Bug 1304688: Android N has broken passing onKeyLongPress events for the back button, so we
// instead copy the long-press-handler technique from Android's KeyButtonView.
// - For short presses, we cancel the callback in onKeyUp
// - For long presses, the normal keypress is marked as cancelled, hence won't be handled elsewhere
// (but Android still provides the haptic feedback), and the runnable is run.
if (!Versions.preN &&
keyCode == KeyEvent.KEYCODE_BACK) {
ThreadUtils.getUiHandler().removeCallbacks(mCheckLongPress);
ThreadUtils.getUiHandler().postDelayed(mCheckLongPress, ViewConfiguration.getLongPressTimeout());
}
if (!mBrowserToolbar.isEditing() && onKey(null, keyCode, event)) {
return true;
}
return super.onKeyDown(keyCode, event);
}
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
if (!Versions.preN &&
keyCode == KeyEvent.KEYCODE_BACK) {
ThreadUtils.getUiHandler().removeCallbacks(mCheckLongPress);
}
if (AndroidGamepadManager.handleKeyEvent(event)) {
return true;
}
return super.onKeyUp(keyCode, event);
}
@Override
public void onCreate(Bundle savedInstanceState) {
final Context appContext = getApplicationContext();
showSplashScreen = true;
boolean supported = HardwareUtils.isSupportedSystem();
if (supported) {
GeckoLoader.loadMozGlue(appContext);
supported = GeckoLoader.neonCompatible();
}
if (!supported) {
// This build does not support the Android version of the device; Exit early.
super.onCreate(savedInstanceState);
return;
}
final SafeIntent intent = new SafeIntent(getIntent());
final boolean isInAutomation = IntentUtils.getIsInAutomationFromEnvironment(intent);
GeckoProfile.setIntentArgs(intent.getStringExtra("args"));
if (!isInAutomation && AppConstants.MOZ_ANDROID_DOWNLOAD_CONTENT_SERVICE) {
// Kick off download of app content as early as possible so that in the best case it's
// available before the user starts using the browser.
DownloadContentService.startStudy(this);
}
// This has to be prepared prior to calling GeckoApp.onCreate, because
// widget code and BrowserToolbar need it, and they're created by the
// layout, which GeckoApp takes care of.
final GeckoApplication app = (GeckoApplication) getApplication();
app.prepareLightweightTheme();
super.onCreate(savedInstanceState);
initSwitchboard(this, intent, isInAutomation);
initTelemetryUploader(isInAutomation);
mBrowserChrome = (ViewGroup) findViewById(R.id.browser_chrome);
mActionBarFlipper = (ViewFlipper) findViewById(R.id.browser_actionbar);
mActionBar = (ActionModeCompatView) findViewById(R.id.actionbar);
mVideoPlayer = (VideoPlayer) findViewById(R.id.video_player);
mVideoPlayer.setFullScreenListener(new VideoPlayer.FullScreenListener() {
@Override
public void onFullScreenChanged(boolean fullScreen) {
mVideoPlayer.setFullScreen(fullScreen);
setFullScreen(fullScreen);
}
});
mBrowserToolbar = (BrowserToolbar) findViewById(R.id.browser_toolbar);
mBrowserToolbar.setTouchEventInterceptor(new TouchEventInterceptor() {
@Override
public boolean onInterceptTouchEvent(View view, MotionEvent event) {
// Manually dismiss text selection bar if it's not overlaying the toolbar.
mTextSelection.dismiss();
return false;
}
@Override
public boolean onTouch(View v, MotionEvent event) {
return false;
}
});
// If the activity is being restored, the add-ons menu item cache only needs restoring if
// Gecko is already running. Otherwise, we'll simply catch the corresponding events when
// Gecko and the add-ons are starting up.
if (savedInstanceState != null && mIsRestoringActivity) {
mAddonMenuItemsCache = savedInstanceState.getParcelableArrayList(STATE_ADDON_MENU_ITEM_CACHE);
mBrowserActionItemsCache = savedInstanceState.getParcelableArrayList(STATE_BROWSER_ACTION_ITEM_CACHE);
}
app.getLightweightTheme().addListener(this);
mProgressView = (AnimatedProgressBar) findViewById(R.id.page_progress);
mDynamicToolbar.setLayerView(mLayerView);
mProgressView.setDynamicToolbar(mDynamicToolbar);
mBrowserToolbar.setProgressBar(mProgressView);
// Initialize Tab History Controller.
tabHistoryController = new TabHistoryController(new OnShowTabHistory() {
@Override
public void onShowHistory(final List<TabHistoryPage> historyPageList, final int toIndex) {
runOnUiThread(new Runnable() {
@Override
public void run() {
if (BrowserApp.this.isFinishing()) {
// TabHistoryController is rather slow - and involves calling into Gecko
// to retrieve tab history. That means there can be a significant
// delay between the back-button long-press, and onShowHistory()
// being called. Hence we need to guard against the Activity being
// shut down (in which case trying to perform UI changes, such as showing
// fragments below, will crash).
return;
}
final TabHistoryFragment fragment = TabHistoryFragment.newInstance(historyPageList, toIndex);
final FragmentManager fragmentManager = getSupportFragmentManager();
GeckoAppShell.getHapticFeedbackDelegate().performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
fragment.show(R.id.tab_history_panel, fragmentManager.beginTransaction(), TAB_HISTORY_FRAGMENT_TAG);
}
});
}
});
mBrowserToolbar.setTabHistoryController(tabHistoryController);
final String action = intent.getAction();
if (Intent.ACTION_VIEW.equals(action)) {
// Show the target URL immediately in the toolbar.
mBrowserToolbar.setTitle(intent.getDataString());
showTabQueuePromptIfApplicable(intent);
} else if (ACTION_VIEW_MULTIPLE.equals(action) && savedInstanceState == null) {
// We only want to handle this intent if savedInstanceState is null. In the case where
// savedInstanceState is not null this activity is being re-created and we already
// opened tabs for the URLs the last time. Our session store will take care of restoring
// them.
openMultipleTabsFromIntent(intent);
} else if (GuestSession.NOTIFICATION_INTENT.equals(action)) {
GuestSession.onNotificationIntentReceived(this);
} else if (TabQueueHelper.LOAD_URLS_ACTION.equals(action)) {
Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.NOTIFICATION, "tabqueue");
}
if (HardwareUtils.isTablet()) {
mTabStrip = (TabStripInterface) (((ViewStub) findViewById(R.id.tablet_tab_strip)).inflate());
}
((GeckoApp.MainLayout) mMainLayout).setTouchEventInterceptor(new HideOnTouchListener());
((GeckoApp.MainLayout) mMainLayout).setMotionEventInterceptor(new MotionEventInterceptor() {
@Override
public boolean onInterceptMotionEvent(View view, MotionEvent event) {
// If we get a gamepad panning MotionEvent while the focus is not on the layerview,
// put the focus on the layerview and carry on
if (mLayerView != null && !mLayerView.hasFocus() && GamepadUtils.isPanningControl(event)) {
if (mHomeScreen == null) {
return false;
}
if (isHomePagerVisible()) {
mLayerView.requestFocus();
} else {
mHomeScreen.requestFocus();
}
}
return false;
}
});
mHomeScreenContainer = (ViewGroup) findViewById(R.id.home_screen_container);
mBrowserSearchContainer = findViewById(R.id.search_container);
mBrowserSearch = (BrowserSearch) getSupportFragmentManager().findFragmentByTag(BROWSER_SEARCH_TAG);
if (mBrowserSearch == null) {
mBrowserSearch = BrowserSearch.newInstance();
mBrowserSearch.setUserVisibleHint(false);
}
setBrowserToolbarListeners();
mFindInPageBar = (FindInPageBar) findViewById(R.id.find_in_page);
mMediaCastingBar = (MediaCastingBar) findViewById(R.id.media_casting);
doorhangerOverlay = findViewById(R.id.doorhanger_overlay);
EventDispatcher.getInstance().registerGeckoThreadListener(this,
"Search:Keyword",
null);
EventDispatcher.getInstance().registerUiThreadListener(this,
"Accessibility:Enabled",
"Menu:Open",
"Menu:Update",
"Menu:Add",
"Menu:Remove",
"Menu:AddBrowserAction",
"Menu:RemoveBrowserAction",
"Menu:UpdateBrowserAction",
"LightweightTheme:Update",
"Tab:Added",
"Video:Play",
"CharEncoding:Data",
"CharEncoding:State",
"Settings:Show",
"Updater:Launch",
"Sanitize:Finished",
"Sanitize:OpenTabs",
null);
EventDispatcher.getInstance().registerBackgroundThreadListener(this,
"Experiments:GetActive",
"Experiments:SetOverride",
"Experiments:ClearOverride",
"Favicon:Request",
"Feedback:MaybeLater",
"Sanitize:ClearHistory",
"Sanitize:ClearSyncedTabs",
"Telemetry:Gather",
"Download:AndroidDownloadManager",
"Website:AppInstalled",
"Website:AppInstallFailed",
"Website:Metadata",
null);
getAppEventDispatcher().registerUiThreadListener(this, "Prompt:ShowTop");
final GeckoProfile profile = getProfile();
// We want to upload the telemetry core ping as soon after startup as possible. It relies on the
// Distribution being initialized. If you move this initialization, ensure it plays well with telemetry.
final Distribution distribution = Distribution.init(getApplicationContext());
distribution.addOnDistributionReadyCallback(
new DistributionStoreCallback(getApplicationContext(), profile.getName()));
mSearchEngineManager = new SearchEngineManager(this, distribution);
// Init suggested sites engine in BrowserDB.
final SuggestedSites suggestedSites = new SuggestedSites(appContext, distribution);
final BrowserDB db = BrowserDB.from(profile);
db.setSuggestedSites(suggestedSites);
mSharedPreferencesHelper = new SharedPreferencesHelper(appContext);
mReadingListHelper = new ReadingListHelper(appContext, profile);
mAccountsHelper = new AccountsHelper(appContext, profile);
mExtensionPermissionsHelper = new ExtensionPermissionsHelper(this);
if (AppConstants.MOZ_ANDROID_BEAM) {
NfcAdapter nfc = NfcAdapter.getDefaultAdapter(this);
if (nfc != null) {
nfc.setNdefPushMessageCallback(new NfcAdapter.CreateNdefMessageCallback() {
@Override
public NdefMessage createNdefMessage(NfcEvent event) {
Tab tab = Tabs.getInstance().getSelectedTab();
if (tab == null || tab.isPrivate()) {
return null;
}
return new NdefMessage(new NdefRecord[] { NdefRecord.createUri(tab.getURL()) });
}
}, this);
}
}
if (savedInstanceState != null) {
mDynamicToolbar.onRestoreInstanceState(savedInstanceState);
mHomeScreenContainer.setPadding(0, savedInstanceState.getInt(STATE_ABOUT_HOME_TOP_PADDING), 0, 0);
}
mDynamicToolbar.setEnabledChangedListener(new DynamicToolbar.OnEnabledChangedListener() {
@Override
public void onEnabledChanged(boolean enabled) {
setDynamicToolbarEnabled(enabled);
}
});
// Set the maximum bits-per-pixel the favicon system cares about.
IconDirectoryEntry.setMaxBPP(GeckoAppShell.getScreenDepth());
// The update service is enabled for RELEASE_OR_BETA, which includes the release and beta channels.
// However, no updates are served. Therefore, we don't trust the update service directly, and
// try to avoid prompting unnecessarily. See Bug 1232798.
if (!AppConstants.RELEASE_OR_BETA && UpdateServiceHelper.isUpdaterEnabled(this)) {
Permissions.from(this)
.withPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE)
.doNotPrompt()
.andFallback(new Runnable() {
@Override
public void run() {
showUpdaterPermissionSnackbar();
}
})
.run();
}
for (final BrowserAppDelegate delegate : delegates) {
delegate.onCreate(this, savedInstanceState);
}
// We want to get an understanding of how our user base is spread (bug 1221646).
final String installerPackageName = getPackageManager().getInstallerPackageName(getPackageName());
Telemetry.sendUIEvent(TelemetryContract.Event.LAUNCH, TelemetryContract.Method.SYSTEM, "installer_" + installerPackageName);
}
/**
* Initializes the default Switchboard URLs the first time.
* @param intent
*/
private void initSwitchboard(final Context context, final SafeIntent intent, final boolean isInAutomation) {
if (isInAutomation) {
Log.d(LOGTAG, "Switchboard disabled - in automation");
return;
} else if (!AppConstants.MOZ_SWITCHBOARD) {
Log.d(LOGTAG, "Switchboard compile-time disabled");
return;
}
final String serverExtra = intent.getStringExtra(INTENT_KEY_SWITCHBOARD_SERVER);
final String serverUrl = TextUtils.isEmpty(serverExtra) ? SWITCHBOARD_SERVER : serverExtra;
new AsyncConfigLoader(context, serverUrl) {
@Override
protected Void doInBackground(Void... params) {
super.doInBackground(params);
SwitchBoard.loadConfig(context, serverUrl);
if (SwitchBoard.isInExperiment(context, Experiments.LEANPLUM) &&
GeckoPreferences.getBooleanPref(context, GeckoPreferences.PREFS_HEALTHREPORT_UPLOAD_ENABLED, true)) {
// Do LeanPlum start/init here
MmaDelegate.init(BrowserApp.this);
}
return null;
}
}.execute();
}
private static void initTelemetryUploader(final boolean isInAutomation) {
TelemetryUploadService.setDisabled(isInAutomation);
}
private void showUpdaterPermissionSnackbar() {
SnackbarBuilder.SnackbarCallback allowCallback = new SnackbarBuilder.SnackbarCallback() {
@Override
public void onClick(View v) {
Permissions.from(BrowserApp.this)
.withPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE)
.run();
}
};
SnackbarBuilder.builder(this)
.message(R.string.updater_permission_text)
.duration(Snackbar.LENGTH_INDEFINITE)
.action(R.string.updater_permission_allow)
.callback(allowCallback)
.buildAndShow();
}
private void conditionallyNotifyEOL() {
final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads();
try {
final SharedPreferences prefs = GeckoSharedPrefs.forProfile(this);
if (!prefs.contains(EOL_NOTIFIED)) {
// Launch main App to load SUMO url on EOL notification.
final String link = getString(R.string.eol_notification_url,
AppConstants.MOZ_APP_VERSION,
AppConstants.OS_TARGET,
Locales.getLanguageTag(Locale.getDefault()));
final Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setClassName(AppConstants.ANDROID_PACKAGE_NAME, AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS);
intent.setData(Uri.parse(link));
final PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
final Notification notification = new NotificationCompat.Builder(this)
.setContentTitle(getString(R.string.eol_notification_title))
.setContentText(getString(R.string.eol_notification_summary))
.setSmallIcon(R.drawable.ic_status_logo)
.setAutoCancel(true)
.setContentIntent(pendingIntent)
.build();
final NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
final int notificationID = EOL_NOTIFIED.hashCode();
notificationManager.notify(notificationID, notification);
GeckoSharedPrefs.forProfile(this)
.edit()
.putBoolean(EOL_NOTIFIED, true)
.apply();
}
} finally {
StrictMode.setThreadPolicy(savedPolicy);
}
}
/**
* Code to actually show the first run pager, separated
* for distribution purposes.
*/
@UiThread
private void checkFirstrunInternal() {
showFirstrunPager();
if (HardwareUtils.isTablet()) {
mTabStrip.setOnTabChangedListener(new TabStripInterface.OnTabAddedOrRemovedListener() {
@Override
public void onTabChanged() {
hideFirstrunPager(TelemetryContract.Method.BUTTON);
mTabStrip.setOnTabChangedListener(null);
}
});
}
}
/**
* Check and show the firstrun pane if the browser has never been launched and
* is not opening an external link from another application.
*
* @param context Context of application; used to show firstrun pane if appropriate
* @param intent Intent that launched this activity
*/
private void checkFirstrun(Context context, SafeIntent intent) {
if (getProfile().inGuestMode()) {
// We do not want to show any first run tour for guest profiles.
return;
}
if (intent.getBooleanExtra(EXTRA_SKIP_STARTPANE, false)) {
// Note that we don't set the pref, so subsequent launches can result
// in the firstrun pane being shown.
return;
}
final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads();
try {
final SharedPreferences prefs = GeckoSharedPrefs.forProfile(this);
if (prefs.getBoolean(FirstrunAnimationContainer.PREF_FIRSTRUN_ENABLED_OLD, true) &&
prefs.getBoolean(FirstrunAnimationContainer.PREF_FIRSTRUN_ENABLED, true)) {
showSplashScreen = false;
if (!Intent.ACTION_VIEW.equals(intent.getAction())) {
// Check to see if a distribution has turned off the first run pager.
final Distribution distribution = Distribution.getInstance(BrowserApp.this);
if (!distribution.shouldWaitForSystemDistribution()) {
checkFirstrunInternal();
} else {
distribution.addOnDistributionReadyCallback(new Distribution.ReadyCallback() {
@Override
public void distributionNotFound() {
ThreadUtils.postToUiThread(new Runnable() {
@Override
public void run() {
checkFirstrunInternal();
}
});
}
@Override
public void distributionFound(final Distribution distribution) {
// Check preference again in case distribution turned it off.
if (prefs.getBoolean(FirstrunAnimationContainer.PREF_FIRSTRUN_ENABLED, true)) {
ThreadUtils.postToUiThread(new Runnable() {
@Override
public void run() {
checkFirstrunInternal();
}
});
}
}
@Override
public void distributionArrivedLate(final Distribution distribution) {
}
});
}
}
// Don't bother trying again to show the v1 minimal first run.
prefs.edit().putBoolean(FirstrunAnimationContainer.PREF_FIRSTRUN_ENABLED, false).apply();
// We have no intention of stopping this session. The FIRSTRUN session
// ends when the browsing session/activity has ended. All events
// during firstrun will be tagged as FIRSTRUN.
Telemetry.startUISession(TelemetryContract.Session.FIRSTRUN);
}
} finally {
StrictMode.setThreadPolicy(savedPolicy);
}
}
private Class<?> getMediaPlayerManager() {
if (AppConstants.MOZ_MEDIA_PLAYER) {
try {
return Class.forName("org.mozilla.gecko.MediaPlayerManager");
} catch (Exception ex) {
// Ignore failures
Log.e(LOGTAG, "No native casting support", ex);
}
}
return null;
}
@Override
public void onBackPressed() {
if (mTextSelection.dismiss()) {
return;
}
if (getSupportFragmentManager().getBackStackEntryCount() > 0) {
super.onBackPressed();
return;
}
if (mBrowserToolbar.onBackPressed()) {
return;
}
if (mActionMode != null) {
endActionMode();
return;
}
if (hideFirstrunPager(TelemetryContract.Method.BACK)) {
return;
}
if (mVideoPlayer.isFullScreen()) {
mVideoPlayer.setFullScreen(false);
setFullScreen(false);
return;
}
if (mVideoPlayer.isPlaying()) {
mVideoPlayer.stop();
return;
}
super.onBackPressed();
}
@Override
public void onAttachedToWindow() {
final SafeIntent intent = new SafeIntent(getIntent());
if (!IntentUtils.getIsInAutomationFromEnvironment(intent)) {
// We can't show the first run experience until Gecko has finished initialization (bug 1077583).
checkFirstrun(this, intent);
}
}
@Override
protected void processTabQueue() {
if (TabQueueHelper.TAB_QUEUE_ENABLED && mInitialized) {
ThreadUtils.postToBackgroundThread(new Runnable() {
@Override
public void run() {
if (TabQueueHelper.shouldOpenTabQueueUrls(BrowserApp.this)) {
openQueuedTabs();
}
}
});
}
}
@Override
protected void openQueuedTabs() {
ThreadUtils.assertNotOnUiThread();
int queuedTabCount = TabQueueHelper.getTabQueueLength(BrowserApp.this);
Telemetry.addToHistogram("FENNEC_TABQUEUE_QUEUESIZE", queuedTabCount);
Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.INTENT, "tabqueue-delayed");
TabQueueHelper.openQueuedUrls(BrowserApp.this, getProfile(), TabQueueHelper.FILE_NAME, false);
// If there's more than one tab then also show the tabs panel.
if (queuedTabCount > 1) {
ThreadUtils.postToUiThread(new Runnable() {
@Override
public void run() {
showNormalTabs();
}
});
}
}
private void openMultipleTabsFromIntent(final SafeIntent intent) {
final List<String> urls = intent.getStringArrayListExtra("urls");
if (urls != null) {
openUrls(urls);
}
}
@Override
public void onResume() {
super.onResume();
if (mIsAbortingAppLaunch) {
return;
}
if (!mHasResumed) {
getAppEventDispatcher().unregisterUiThreadListener(this, "Prompt:ShowTop");
mHasResumed = true;
}
processTabQueue();
for (BrowserAppDelegate delegate : delegates) {
delegate.onResume(this);
}
}
@Override
public void onPause() {
super.onPause();
if (mIsAbortingAppLaunch) {
return;
}
if (mHasResumed) {
// Register for Prompt:ShowTop so we can foreground this activity even if it's hidden.
getAppEventDispatcher().registerUiThreadListener(this, "Prompt:ShowTop");
mHasResumed = false;
}
for (BrowserAppDelegate delegate : delegates) {
delegate.onPause(this);
}
}
@Override
public void onRestart() {
super.onRestart();
if (mIsAbortingAppLaunch) {
return;
}
for (final BrowserAppDelegate delegate : delegates) {
delegate.onRestart(this);
}
}
@Override
public void onStart() {
super.onStart();
if (mIsAbortingAppLaunch) {
return;
}
// Queue this work so that the first launch of the activity doesn't
// trigger profile init too early.
ThreadUtils.postToBackgroundThread(new Runnable() {
@Override
public void run() {
final GeckoProfile profile = getProfile();
if (profile.inGuestMode()) {
GuestSession.showNotification(BrowserApp.this);
} else {
// If we're restarting, we won't destroy the activity.
// Make sure we remove any guest notifications that might
// have been shown.
GuestSession.hideNotification(BrowserApp.this);
}
// It'd be better to launch this once, in onCreate, but there's ambiguity for when the
// profile is created so we run here instead. Don't worry, call start short-circuits pretty fast.
final SharedPreferences sharedPrefs = GeckoSharedPrefs.forProfileName(BrowserApp.this, profile.getName());
FileCleanupController.startIfReady(BrowserApp.this, sharedPrefs, profile.getDir().getAbsolutePath());
}
});
for (final BrowserAppDelegate delegate : delegates) {
delegate.onStart(this);
}
}
@Override
public void onStop() {
super.onStop();
if (mIsAbortingAppLaunch) {
return;
}
// We only show the guest mode notification when our activity is in the foreground.
GuestSession.hideNotification(this);
for (final BrowserAppDelegate delegate : delegates) {
delegate.onStop(this);
}
onAfterStop();
}
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
// Sending a message to the toolbar when the browser window gains focus
// This is needed for qr code input
if (hasFocus) {
mBrowserToolbar.onParentFocus();
}
}
private void setBrowserToolbarListeners() {
mBrowserToolbar.setOnActivateListener(new BrowserToolbar.OnActivateListener() {
@Override
public void onActivate() {
enterEditingMode();
}
});
mBrowserToolbar.setOnCommitListener(new BrowserToolbar.OnCommitListener() {
@Override
public void onCommit() {
commitEditingMode();
}
});
mBrowserToolbar.setOnDismissListener(new BrowserToolbar.OnDismissListener() {
@Override
public void onDismiss() {
mBrowserToolbar.cancelEdit();
}
});
mBrowserToolbar.setOnFilterListener(new BrowserToolbar.OnFilterListener() {
@Override
public void onFilter(String searchText, AutocompleteHandler handler) {
filterEditingMode(searchText, handler);
}
});
mBrowserToolbar.setOnFocusChangeListener(new View.OnFocusChangeListener() {
@Override
public void onFocusChange(View v, boolean hasFocus) {
if (isHomePagerVisible()) {
mHomeScreen.onToolbarFocusChange(hasFocus);
}
}
});
mBrowserToolbar.setOnStartEditingListener(new BrowserToolbar.OnStartEditingListener() {
@Override
public void onStartEditing() {
final Tab selectedTab = Tabs.getInstance().getSelectedTab();
if (selectedTab != null) {
selectedTab.setIsEditing(true);
}
// Temporarily disable doorhanger notifications.
if (mDoorHangerPopup != null) {
mDoorHangerPopup.disable();
}
}
});
mBrowserToolbar.setOnStopEditingListener(new BrowserToolbar.OnStopEditingListener() {
@Override
public void onStopEditing() {
final Tab selectedTab = Tabs.getInstance().getSelectedTab();
if (selectedTab != null) {
selectedTab.setIsEditing(false);
}
selectTargetTabForEditingMode();
// Since the underlying LayerView is set visible in hideHomePager, we would
// ordinarily want to call it first. However, hideBrowserSearch changes the
// visibility of the HomePager and hideHomePager will take no action if the
// HomePager is hidden, so we want to call hideBrowserSearch to restore the
// HomePager visibility first.
hideBrowserSearch();
hideHomePager();
// Re-enable doorhanger notifications. They may trigger on the selected tab above.
if (mDoorHangerPopup != null) {
mDoorHangerPopup.enable();
}
}
});
// Intercept key events for gamepad shortcuts
mBrowserToolbar.setOnKeyListener(this);
}
private void setDynamicToolbarEnabled(boolean enabled) {
ThreadUtils.assertOnUiThread();
if (mLayerView != null) {
if (enabled) {
mDynamicToolbar.setPinned(false, PinReason.DISABLED);
} else {
// Immediately show the toolbar when disabling the dynamic
// toolbar.
mDynamicToolbar.setPinned(true, PinReason.DISABLED);
mDynamicToolbar.setVisible(true, VisibilityTransition.IMMEDIATE);
}
}
refreshToolbarHeight();
}
private static boolean isAboutHome(final Tab tab) {
return AboutPages.isAboutHome(tab.getURL());
}
@Override
public boolean onSearchRequested() {
enterEditingMode();
return true;
}
@Override
public boolean onContextItemSelected(MenuItem item) {
final int itemId = item.getItemId();
if (itemId == R.id.pasteandgo) {
hideFirstrunPager(TelemetryContract.Method.CONTEXT_MENU);
String text = Clipboard.getText(this);
if (!TextUtils.isEmpty(text)) {
loadUrlOrKeywordSearch(text);
Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.CONTEXT_MENU);
Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.CONTEXT_MENU, "pasteandgo");
}
return true;
}
if (itemId == R.id.paste) {
String text = Clipboard.getText(this);
if (!TextUtils.isEmpty(text)) {
enterEditingMode(text);
showBrowserSearch();
mBrowserSearch.filter(text, null);
Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.CONTEXT_MENU, "paste");
}
return true;
}
if (itemId == R.id.subscribe) {
// This can be selected from either the browser menu or the contextmenu, depending on the size and version (v11+) of the phone.
Tab tab = Tabs.getInstance().getSelectedTab();
if (tab != null && tab.hasFeeds()) {
final GeckoBundle args = new GeckoBundle(1);
args.putInt("tabId", tab.getId());
EventDispatcher.getInstance().dispatch("Feeds:Subscribe", args);
}
return true;
}
if (itemId == R.id.add_search_engine) {
// This can be selected from either the browser menu or the contextmenu, depending on the size and version (v11+) of the phone.
Tab tab = Tabs.getInstance().getSelectedTab();
if (tab != null && tab.hasOpenSearch()) {
final GeckoBundle args = new GeckoBundle(1);
args.putInt("tabId", tab.getId());
EventDispatcher.getInstance().dispatch("SearchEngines:Add", args);
}
return true;
}
if (itemId == R.id.copyurl) {
Tab tab = Tabs.getInstance().getSelectedTab();
if (tab != null) {
String url = ReaderModeUtils.stripAboutReaderUrl(tab.getURL());
if (url != null) {
Clipboard.setText(this, url);
Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.CONTEXT_MENU, "copyurl");
}
}
return true;
}
if (itemId == R.id.pin_to_top_sites) {
final Tab selectedTab = Tabs.getInstance().getSelectedTab();
if (selectedTab != null) {
ThreadUtils.postToBackgroundThread(new Runnable() {
@Override
public void run() {
final ActivityStreamTelemetry.Extras.Builder telemetryExtraBuilder = ActivityStreamTelemetry.Extras.builder();
final BrowserDB db = BrowserDB.from(BrowserApp.this);
final ContentResolver cr = getContentResolver();
final String url = selectedTab.getURL();
final @StringRes int snackbarText;
if (!db.isPinnedForAS(cr, url)) {
db.pinSiteForAS(getContentResolver(), url, selectedTab.getTitle());
snackbarText = R.string.pinned_page_to_top_sites;
telemetryExtraBuilder.set(ActivityStreamTelemetry.Contract.ITEM, ActivityStreamTelemetry.Contract.ITEM_PIN);
} else {
db.unpinSiteForAS(getContentResolver(), url);
snackbarText = R.string.unpinned_page_from_top_sites;
telemetryExtraBuilder.set(ActivityStreamTelemetry.Contract.ITEM, ActivityStreamTelemetry.Contract.ITEM_UNPIN);
}
SnackbarBuilder.builder(BrowserApp.this)
.message(snackbarText)
.buildAndShow();
Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.MENU, // via browser menu.
telemetryExtraBuilder.build());
}
});
}
return true;
}
if (itemId == R.id.add_to_launcher) {
final Tab tab = Tabs.getInstance().getSelectedTab();
if (tab == null) {
return true;
}
final String url = tab.getURL();
final String title = tab.getDisplayTitle();
if (url == null || title == null) {
return true;
}
ThreadUtils.postToBackgroundThread(new Runnable() {
@Override
public void run() {
GeckoApplication.createBrowserShortcut(title, url);
}
});
Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.CONTEXT_MENU,
getResources().getResourceEntryName(itemId));
return true;
}
if (itemId == R.id.set_as_homepage) {
final Tab tab = Tabs.getInstance().getSelectedTab();
if (tab == null) {
return true;
}
final String url = tab.getURL();
if (url == null) {
return true;
}
final SharedPreferences prefs = GeckoSharedPrefs.forProfile(this);
final SharedPreferences.Editor editor = prefs.edit();
editor.putString(GeckoPreferences.PREFS_HOMEPAGE, url);
editor.apply();
Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.CONTEXT_MENU,
getResources().getResourceEntryName(itemId));
return true;
}
return false;
}
@Override
public void onDestroy() {
if (mIsAbortingAppLaunch) {
super.onDestroy();
return;
}
if (mProgressView != null) {
mProgressView.setDynamicToolbar(null);
}
mDynamicToolbar.destroy();
final GeckoApplication app = (GeckoApplication) getApplication();
app.getLightweightTheme().removeListener(this);
if (mBrowserToolbar != null)
mBrowserToolbar.onDestroy();
if (mFindInPageBar != null) {
mFindInPageBar.onDestroy();
mFindInPageBar = null;
}
if (mMediaCastingBar != null) {
mMediaCastingBar.onDestroy();
mMediaCastingBar = null;
}
if (mSharedPreferencesHelper != null) {
mSharedPreferencesHelper.uninit();
mSharedPreferencesHelper = null;
}
if (mReadingListHelper != null) {
mReadingListHelper.uninit();
mReadingListHelper = null;
}
if (mAccountsHelper != null) {
mAccountsHelper.uninit();
mAccountsHelper = null;
}
if (mExtensionPermissionsHelper != null) {
mExtensionPermissionsHelper.uninit();
mExtensionPermissionsHelper = null;
}
mSearchEngineManager.unregisterListeners();
EventDispatcher.getInstance().unregisterGeckoThreadListener(this,
"Search:Keyword",
null);
EventDispatcher.getInstance().unregisterUiThreadListener(this,
"Accessibility:Enabled",
"Menu:Open",
"Menu:Update",
"Menu:Add",
"Menu:Remove",
"Menu:AddBrowserAction",
"Menu:RemoveBrowserAction",
"Menu:UpdateBrowserAction",
"LightweightTheme:Update",
"Tab:Added",
"Video:Play",
"CharEncoding:Data",
"CharEncoding:State",
"Settings:Show",
"Updater:Launch",
"Sanitize:Finished",
"Sanitize:OpenTabs",
null);
EventDispatcher.getInstance().unregisterBackgroundThreadListener(this,
"Experiments:GetActive",
"Experiments:SetOverride",
"Experiments:ClearOverride",
"Favicon:Request",
"Feedback:MaybeLater",
"Sanitize:ClearHistory",
"Sanitize:ClearSyncedTabs",
"Telemetry:Gather",
"Download:AndroidDownloadManager",
"Website:AppInstalled",
"Website:AppInstallFailed",
"Website:Metadata",
null);
getAppEventDispatcher().unregisterUiThreadListener(this, "Prompt:ShowTop");
if (AppConstants.MOZ_ANDROID_BEAM) {
NfcAdapter nfc = NfcAdapter.getDefaultAdapter(this);
if (nfc != null) {
// null this out even though the docs say it's not needed,
// because the source code looks like it will only do this
// automatically on API 14+
nfc.setNdefPushMessageCallback(null, this);
}
}
for (final BrowserAppDelegate delegate : delegates) {
delegate.onDestroy(this);
}
deleteTempFiles(getApplicationContext());
NotificationHelper.destroy();
GeckoNetworkManager.destroy();
super.onDestroy();
}
@Override
protected void initializeChrome() {
super.initializeChrome();
mDoorHangerPopup.setAnchor(mBrowserToolbar.getDoorHangerAnchor());
mDoorHangerPopup.setOnVisibilityChangeListener(this);
if (mLayerView != null) {
mLayerView.getDynamicToolbarAnimator().setToolbarChromeProxy(this);
}
setDynamicToolbarEnabled(mDynamicToolbar.isEnabled());
// Intercept key events for gamepad shortcuts
mLayerView.setOnKeyListener(this);
// Initialize the actionbar menu items on startup for both large and small tablets
if (HardwareUtils.isTablet()) {
onCreatePanelMenu(Window.FEATURE_OPTIONS_PANEL, null);
invalidateOptionsMenu();
}
}
@Override
public void onDoorHangerShow() {
mDynamicToolbar.setVisible(true, VisibilityTransition.ANIMATE);
super.onDoorHangerShow();
}
// ToolbarChromeProxy inteface
@Override
public Bitmap getBitmapOfToolbarChrome() {
if (mBrowserChrome == null) {
return null;
}
Bitmap bm = Bitmap.createBitmap(mBrowserChrome.getWidth(), mBrowserChrome.getHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bm);
Drawable bgDrawable = mBrowserChrome.getBackground();
if (bgDrawable != null) {
bgDrawable.draw(canvas);
} else {
canvas.drawColor(Color.WHITE);
}
mBrowserChrome.draw(canvas);
return bm;
}
@Override
public boolean isToolbarChromeVisible() {
return mBrowserChrome.getVisibility() == View.VISIBLE;
}
@Override
public void toggleToolbarChrome(final boolean aShow) {
if (aShow) {
mBrowserChrome.setVisibility(View.VISIBLE);
} else {
// The chrome needs to be INVISIBLE instead of GONE so that
// it will continue update when the layout changes. This
// ensures the bitmap generated for the static toolbar
// snapshot is the correct size.
mBrowserChrome.setVisibility(View.INVISIBLE);
}
}
public void refreshToolbarHeight() {
ThreadUtils.assertOnUiThread();
int height = 0;
if (mBrowserChrome != null) {
height = mBrowserChrome.getHeight();
}
mHomeScreenContainer.setPadding(0, height, 0, 0);
if (mLayerView != null && height != mToolbarHeight) {
mToolbarHeight = height;
mLayerView.getDynamicToolbarAnimator().setMaxToolbarHeight(height);
mDynamicToolbar.setVisible(true, VisibilityTransition.IMMEDIATE);
}
}
@Override
void toggleChrome(final boolean aShow) {
if (mDynamicToolbar != null) {
mDynamicToolbar.setVisible(aShow, VisibilityTransition.IMMEDIATE);
}
super.toggleChrome(aShow);
}
@Override
void focusChrome() {
if (mDynamicToolbar != null) {
mDynamicToolbar.setVisible(true, VisibilityTransition.IMMEDIATE);
}
mActionBarFlipper.requestFocusFromTouch();
super.focusChrome();
}
@Override
public void refreshChrome() {
invalidateOptionsMenu();
if (mTabsPanel != null) {
mTabsPanel.refresh();
}
if (mTabStrip != null) {
mTabStrip.refresh();
}
mBrowserToolbar.refresh();
}
@Override // BundleEventListener
public void handleMessage(final String event, final GeckoBundle message,
final EventCallback callback) {
switch (event) {
case "Gecko:Ready":
EventDispatcher.getInstance().registerUiThreadListener(this, "Gecko:DelayedStartup");
// Handle this message in GeckoApp, but also enable the Settings
// menuitem, which is specific to BrowserApp.
super.handleMessage(event, message, callback);
final Menu menu = mMenu;
ThreadUtils.postToUiThread(new Runnable() {
@Override
public void run() {
if (menu != null) {
menu.findItem(R.id.settings).setEnabled(true);
menu.findItem(R.id.help).setEnabled(true);
}
}
});
// Display notification for Mozilla data reporting, if data should be collected.
if (AppConstants.MOZ_DATA_REPORTING &&
Restrictions.isAllowed(this, Restrictable.DATA_CHOICES)) {
DataReportingNotification.checkAndNotifyPolicy(this);
}
break;
case "Gecko:DelayedStartup":
EventDispatcher.getInstance().unregisterUiThreadListener(this, "Gecko:DelayedStartup");
// Force tabs panel inflation once the initial pageload is finished.
ensureTabsPanelExists();
if (AppConstants.MOZ_MEDIA_PLAYER) {
// Check if the fragment is already added. This should never be true
// here, but this is a nice safety check. If casting is disabled,
// these classes aren't built. We use reflection to initialize them.
final Class<?> mediaManagerClass = getMediaPlayerManager();
if (mediaManagerClass != null) {
try {
final String tag = "";
mediaManagerClass.getDeclaredField("MEDIA_PLAYER_TAG").get(tag);
Log.i(LOGTAG, "Found tag " + tag);
final Fragment frag = getSupportFragmentManager().findFragmentByTag(tag);
if (frag == null) {
final Method getInstance = mediaManagerClass.getMethod(
"getInstance", (Class[]) null);
final Fragment mpm = (Fragment) getInstance.invoke(null);
getSupportFragmentManager().beginTransaction()
.disallowAddToBackStack().add(mpm, tag).commit();
}
} catch (Exception ex) {
Log.e(LOGTAG, "Error initializing media manager", ex);
}
}
}
if (AppConstants.MOZ_STUMBLER_BUILD_TIME_ENABLED &&
Restrictions.isAllowed(this, Restrictable.DATA_CHOICES)) {
// Start (this acts as ping if started already) the stumbler lib; if
// the stumbler has queued data it will upload it. Stumbler operates
// on its own thread, and startup impact is further minimized by
// delaying work (such as upload) a few seconds. Avoid any potential
// startup CPU/thread contention by delaying the pref broadcast.
GeckoPreferences.broadcastStumblerPref(BrowserApp.this);
}
if (AppConstants.MOZ_ANDROID_DOWNLOAD_CONTENT_SERVICE &&
!IntentUtils.getIsInAutomationFromEnvironment(new SafeIntent(getIntent()))) {
// TODO: Better scheduling of DLC actions (Bug 1257492)
DownloadContentService.startSync(this);
DownloadContentService.startVerification(this);
}
break;
case "Accessibility:Enabled":
mDynamicToolbar.setAccessibilityEnabled(message.getBoolean("enabled"));
break;
case "Menu:Open":
if (mBrowserToolbar.isEditing()) {
mBrowserToolbar.cancelEdit();
}
openOptionsMenu();
break;
case "Menu:Update":
updateAddonMenuItem(message.getInt("id") + ADDON_MENU_OFFSET,
message.getBundle("options"));
break;
case "Menu:Add":
final MenuItemInfo info = new MenuItemInfo();
info.label = message.getString("name");
if (info.label == null) {
Log.e(LOGTAG, "Invalid menu item name");
return;
}
info.id = message.getInt("id") + ADDON_MENU_OFFSET;
info.checked = message.getBoolean("checked", false);
info.enabled = message.getBoolean("enabled", true);
info.visible = message.getBoolean("visible", true);
info.checkable = message.getBoolean("checkable", false);
final int parent = message.getInt("parent", 0);
info.parent = parent <= 0 ? parent : parent + ADDON_MENU_OFFSET;
addAddonMenuItem(info);
break;
case "Menu:Remove":
removeAddonMenuItem(message.getInt("id") + ADDON_MENU_OFFSET);
break;
case "Menu:AddBrowserAction":
final BrowserActionItemInfo browserAction = new BrowserActionItemInfo();
browserAction.label = message.getString("name");
if (TextUtils.isEmpty(browserAction.label)) {
Log.e(LOGTAG, "Invalid browser action name");
return;
}
browserAction.id = message.getInt("id") + BROWSER_ACTION_MENU_OFFSET;
browserAction.uuid = message.getString("uuid");
addBrowserActionMenuItem(browserAction);
break;
case "Menu:RemoveBrowserAction":
removeBrowserActionMenuItem(message.getString("uuid"));
break;
case "Menu:UpdateBrowserAction":
updateBrowserActionMenuItem(message.getString("uuid"),
message.getBundle("options"));
break;
case "LightweightTheme:Update":
mDynamicToolbar.setVisible(true, VisibilityTransition.ANIMATE);
break;
case "Search:Keyword":
storeSearchQuery(message.getString("query"));
recordSearch(GeckoSharedPrefs.forProfile(this), message.getString("identifier"),
TelemetryContract.Method.ACTIONBAR);
break;
case "Prompt:ShowTop":
// Bring this activity to front so the prompt is visible..
Intent bringToFrontIntent = new Intent();
bringToFrontIntent.setClassName(AppConstants.ANDROID_PACKAGE_NAME,
AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS);
bringToFrontIntent.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
startActivity(bringToFrontIntent);
break;
case "Tab:Added":
if (message.getBoolean("cancelEditMode")) {
// Set the target tab to null so it does not get selected (on editing
// mode exit) in lieu of the tab that we're going to open and select.
mTargetTabForEditingMode = null;
mBrowserToolbar.cancelEdit();
}
break;
case "Video:Play":
if (SwitchBoard.isInExperiment(this, Experiments.HLS_VIDEO_PLAYBACK)) {
mVideoPlayer.start(Uri.parse(message.getString("uri")));
Telemetry.sendUIEvent(TelemetryContract.Event.SHOW,
TelemetryContract.Method.CONTENT, "playhls");
}
break;
case "CharEncoding:Data":
final GeckoBundle[] charsets = message.getBundleArray("charsets");
final int selected = message.getInt("selected");
final String[] titleArray = new String[charsets.length];
final String[] codeArray = new String[charsets.length];
for (int i = 0; i < charsets.length; i++) {
final GeckoBundle charset = charsets[i];
titleArray[i] = charset.getString("title");
codeArray[i] = charset.getString("code");
}
final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this);
dialogBuilder.setSingleChoiceItems(titleArray, selected,
new AlertDialog.OnClickListener() {
@Override
public void onClick(final DialogInterface dialog, final int which) {
final GeckoBundle data = new GeckoBundle(1);
data.putString("encoding", codeArray[which]);
EventDispatcher.getInstance().dispatch("CharEncoding:Set", data);
dialog.dismiss();
}
});
dialogBuilder.setNegativeButton(R.string.button_cancel,
new AlertDialog.OnClickListener() {
@Override
public void onClick(final DialogInterface dialog, final int which) {
dialog.dismiss();
}
});
dialogBuilder.show();
break;
case "CharEncoding:State":
final boolean visible = "true".equals(message.getString("visible"));
GeckoPreferences.setCharEncodingState(visible);
if (mMenu != null) {
mMenu.findItem(R.id.char_encoding).setVisible(visible);
}
break;
case "Experiments:GetActive":
final List<String> experiments = SwitchBoard.getActiveExperiments(this);
callback.sendSuccess(experiments.toArray(new String[experiments.size()]));
break;
case "Experiments:SetOverride":
Experiments.setOverride(this, message.getString("name"),
message.getBoolean("isEnabled"));
break;
case "Experiments:ClearOverride":
Experiments.clearOverride(this, message.getString("name"));
break;
case "Favicon:Request":
final String url = message.getString("url");
final boolean shouldSkipNetwork = message.getBoolean("skipNetwork");
if (TextUtils.isEmpty(url)) {
callback.sendError(null);
break;
}
Icons.with(this)
.pageUrl(url)
.privileged(false)
.skipNetworkIf(shouldSkipNetwork)
.executeCallbackOnBackgroundThread()
.build()
.execute(IconsHelper.createBase64EventCallback(callback));
break;
case "Feedback:MaybeLater":
SharedPreferences settings = getPreferences(Activity.MODE_PRIVATE);
settings.edit().putInt(getPackageName() + ".feedback_launch_count", 0).apply();
break;
case "Sanitize:Finished":
if (message.getBoolean("shutdown", false)) {
// Gecko is shutting down and has called our sanitize handlers,
// so we can start exiting, too.
finishAndShutdown(/* restart */ false);
}
break;
case "Sanitize:OpenTabs":
Tabs.getInstance().closeAllTabs();
callback.sendSuccess(null);
break;
case "Sanitize:ClearHistory":
BrowserDB.from(getProfile()).clearHistory(
getContentResolver(), message.getBoolean("clearSearchHistory", false));
callback.sendSuccess(null);
break;
case "Sanitize:ClearSyncedTabs":
FennecTabsRepository.deleteNonLocalClientsAndTabs(this);
callback.sendSuccess(null);
break;
case "Settings:Show":
final Intent settingsIntent = new Intent(this, GeckoPreferences.class);
final String resource = message.getString(GeckoPreferences.INTENT_EXTRA_RESOURCES);
GeckoPreferences.setResourceToOpen(settingsIntent, resource);
startActivityForResult(settingsIntent, ACTIVITY_REQUEST_PREFERENCES);
// Don't use a transition to settings if we're on a device where that
// would look bad.
if (HardwareUtils.IS_KINDLE_DEVICE) {
overridePendingTransition(0, 0);
}
break;
case "Telemetry:Gather":
final BrowserDB db = BrowserDB.from(getProfile());
final ContentResolver cr = getContentResolver();
Telemetry.addToHistogram("PLACES_PAGES_COUNT", db.getCount(cr, "history"));
Telemetry.addToHistogram("FENNEC_BOOKMARKS_COUNT", db.getCount(cr, "bookmarks"));
Telemetry.addToHistogram("BROWSER_IS_USER_DEFAULT",
(isDefaultBrowser(Intent.ACTION_VIEW) ? 1 : 0));
Telemetry.addToHistogram("FENNEC_CUSTOM_HOMEPAGE",
(Tabs.hasHomepage(this) ? 1 : 0));
final SharedPreferences prefs = GeckoSharedPrefs.forProfile(this);
final boolean hasCustomHomepanels =
prefs.contains(HomeConfigPrefsBackend.PREFS_CONFIG_KEY) ||
prefs.contains(HomeConfigPrefsBackend.PREFS_CONFIG_KEY_OLD);
Telemetry.addToHistogram("FENNEC_HOMEPANELS_CUSTOM", hasCustomHomepanels ? 1 : 0);
Telemetry.addToHistogram("FENNEC_READER_VIEW_CACHE_SIZE",
SavedReaderViewHelper.getSavedReaderViewHelper(this)
.getDiskSpacedUsedKB());
if (Versions.feature16Plus) {
Telemetry.addToHistogram("BROWSER_IS_ASSIST_DEFAULT",
(isDefaultBrowser(Intent.ACTION_ASSIST) ? 1 : 0));
}
Telemetry.addToHistogram("FENNEC_ORBOT_INSTALLED",
ContextUtils.isPackageInstalled(this, "org.torproject.android") ? 1 : 0);
break;
case "Website:AppInstalled":
final String name = message.getString("name");
final String startUrl = message.getString("start_url");
final String manifestPath = message.getString("manifest_path");
final String manifestUrl = message.getString("manifest_url");
final LoadFaviconResult loadIconResult = FaviconDecoder
.decodeDataURI(this, message.getString("icon"));
if (loadIconResult != null) {
final Bitmap icon = loadIconResult
.getBestBitmap(GeckoAppShell.getPreferredIconSize());
GeckoApplication.createAppShortcut(name, startUrl, manifestPath, manifestUrl, icon);
Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.PAGEACTION, PwaConfirm.TELEMETRY_EXTRA_ADDED);
} else {
Log.e(LOGTAG, "Failed to load icon!");
}
break;
case "Website:AppInstallFailed":
final String title = message.getString("title");
final String bookmarkUrl = message.getString("url");
GeckoApplication.createBrowserShortcut(title, bookmarkUrl);
break;
case "Updater:Launch":
/**
* Launch UI that lets the user update Firefox.
*
* This depends on the current channel: Release and Beta both direct to
* the Google Play Store. If updating is enabled, Aurora, Nightly, and
* custom builds open about:firefox, which provides an update interface.
*
* If updating is not enabled, this simply logs an error.
*/
if (AppConstants.RELEASE_OR_BETA) {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse("market://details?id=" + getPackageName()));
startActivity(intent);
break;
}
if (AppConstants.MOZ_UPDATER) {
Tabs.getInstance().loadUrlInTab(AboutPages.FIREFOX);
break;
}
Log.w(LOGTAG, "No candidate updater found; ignoring launch request.");
break;
case "Download:AndroidDownloadManager":
// Downloading via Android's download manager
final String uri = message.getString("uri");
final String filename = message.getString("filename");
final String mimeType = message.getString("mimeType");
final DownloadManager.Request request = new DownloadManager.Request(Uri.parse(uri));
request.setMimeType(mimeType);
try {
request.setDestinationInExternalPublicDir(
Environment.DIRECTORY_DOWNLOADS, filename);
} catch (IllegalStateException e) {
Log.e(LOGTAG, "Cannot create download directory");
break;
}
request.allowScanningByMediaScanner();
request.setNotificationVisibility(
DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
request.addRequestHeader("User-Agent", HardwareUtils.isTablet() ?
AppConstants.USER_AGENT_FENNEC_TABLET :
AppConstants.USER_AGENT_FENNEC_MOBILE);
try {
DownloadManager manager = (DownloadManager)
getSystemService(Context.DOWNLOAD_SERVICE);
manager.enqueue(request);
} catch (RuntimeException e) {
Log.e(LOGTAG, "Download failed: " + e);
}
break;
case "Website:Metadata":
final String location = message.getString("location");
final boolean hasImage = message.getBoolean("hasImage");
final String metadata = message.getString("metadata");
final ContentProviderClient contentProviderClient = getContentResolver()
.acquireContentProviderClient(BrowserContract.PageMetadata.CONTENT_URI);
if (contentProviderClient == null) {
Log.w(LOGTAG, "Failed to obtain content provider client for: " +
BrowserContract.PageMetadata.CONTENT_URI);
return;
}
try {
GlobalPageMetadata.getInstance().add(
BrowserDB.from(getProfile()),
contentProviderClient,
location, hasImage, metadata);
} finally {
contentProviderClient.release();
}
break;
default:
super.handleMessage(event, message, callback);
break;
}
}
/**
* Use a dummy Intent to do a default browser check.
*
* @return true if this package is the default browser on this device, false otherwise.
*/
private boolean isDefaultBrowser(String action) {
final Intent viewIntent = new Intent(action, Uri.parse("http://www.mozilla.org"));
final ResolveInfo info = getPackageManager().resolveActivity(viewIntent, PackageManager.MATCH_DEFAULT_ONLY);
if (info == null) {
// No default is set
return false;
}
final String packageName = info.activityInfo.packageName;
return (TextUtils.equals(packageName, getPackageName()));
}
@Override
public void addTab() {
MmaDelegate.track(NEW_TAB);
Tabs.getInstance().addTab();
}
@Override
public void addPrivateTab() {
Tabs.getInstance().addPrivateTab();
}
public void showTrackingProtectionPromptIfApplicable() {
final SharedPreferences prefs = getSharedPreferences();
final boolean hasTrackingProtectionPromptBeShownBefore = prefs.getBoolean(GeckoPreferences.PREFS_TRACKING_PROTECTION_PROMPT_SHOWN, false);
if (hasTrackingProtectionPromptBeShownBefore) {
return;
}
prefs.edit().putBoolean(GeckoPreferences.PREFS_TRACKING_PROTECTION_PROMPT_SHOWN, true).apply();
startActivity(new Intent(BrowserApp.this, TrackingProtectionPrompt.class));
}
@Override
public void showNormalTabs() {
showTabs(TabsPanel.Panel.NORMAL_TABS);
}
@Override
public void showPrivateTabs() {
showTabs(TabsPanel.Panel.PRIVATE_TABS);
}
/**
* Ensure the TabsPanel view is properly inflated and returns
* true when the view has been inflated, false otherwise.
*/
private boolean ensureTabsPanelExists() {
if (mTabsPanel != null) {
return false;
}
ViewStub tabsPanelStub = (ViewStub) findViewById(R.id.tabs_panel);
mTabsPanel = (TabsPanel) tabsPanelStub.inflate();
mTabsPanel.setTabsLayoutChangeListener(this);
return true;
}
private void showTabs(final TabsPanel.Panel panel) {
if (Tabs.getInstance().getDisplayCount() == 0)
return;
hideFirstrunPager(TelemetryContract.Method.BUTTON);
if (ensureTabsPanelExists()) {
// If we've just inflated the tabs panel, only show it once the current
// layout pass is done to avoid displayed temporary UI states during
// relayout.
ViewTreeObserver vto = mTabsPanel.getViewTreeObserver();
if (vto.isAlive()) {
vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
mTabsPanel.getViewTreeObserver().removeGlobalOnLayoutListener(this);
showTabs(panel);
}
});
}
} else {
if (mDoorHangerPopup != null) {
mDoorHangerPopup.disable();
}
if (mTabStrip != null) {
mTabStrip.tabStripIsCovered(true);
}
mTabsPanel.show(panel);
// Hide potentially visible "find in page" bar (Bug 1177338)
mFindInPageBar.hide();
for (final BrowserAppDelegate delegate : delegates) {
delegate.onTabsTrayShown(this, mTabsPanel);
}
}
// Set status bar color with tabs tray background color.
WindowUtil.setTabsTrayStatusBarColor(this);
}
@Override
public void hideTabs() {
mTabsPanel.hide();
if (mTabStrip != null) {
mTabStrip.tabStripIsCovered(false);
}
if (mDoorHangerPopup != null) {
mDoorHangerPopup.enable();
}
for (final BrowserAppDelegate delegate : delegates) {
delegate.onTabsTrayHidden(this, mTabsPanel);
}
refreshStatusBarColor();
}
@Override
public boolean autoHideTabs() {
if (areTabsShown()) {
hideTabs();
return true;
}
return false;
}
public boolean areTabsShown() {
return (mTabsPanel != null && mTabsPanel.isShown());
}
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
@Override
public void onTabsLayoutChange(int width, int height) {
int animationLength = TABS_ANIMATION_DURATION;
if (mMainLayoutAnimator != null) {
animationLength = Math.max(1, animationLength - (int)mMainLayoutAnimator.getRemainingTime());
mMainLayoutAnimator.stop(false);
}
if (areTabsShown()) {
mTabsPanel.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
// Hide the web content from accessibility tools even though it's visible
// so that you can't examine it as long as the tabs are being shown.
if (Versions.feature16Plus) {
mLayerView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
}
} else {
if (Versions.feature16Plus) {
mLayerView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
}
}
mMainLayoutAnimator = new PropertyAnimator(animationLength, sTabsInterpolator);
mMainLayoutAnimator.addPropertyAnimationListener(this);
mMainLayoutAnimator.attach(mMainLayout,
PropertyAnimator.Property.SCROLL_Y,
-height);
mTabsPanel.prepareTabsAnimation(mMainLayoutAnimator);
mBrowserToolbar.triggerTabsPanelTransition(mMainLayoutAnimator, areTabsShown());
// If the tabs panel is animating onto the screen, pin the dynamic
// toolbar.
if (mDynamicToolbar.isEnabled()) {
if (width > 0 && height > 0) {
mDynamicToolbar.setPinned(true, PinReason.RELAYOUT);
mDynamicToolbar.setVisible(true, VisibilityTransition.ANIMATE);
} else {
mDynamicToolbar.setPinned(false, PinReason.RELAYOUT);
}
}
mMainLayoutAnimator.start();
}
@Override
public void onPropertyAnimationStart() {
}
@Override
public void onPropertyAnimationEnd() {
if (!areTabsShown()) {
mTabsPanel.setVisibility(View.INVISIBLE);
mTabsPanel.setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
} else {
// Cancel editing mode to return to page content when the TabsPanel closes. We cancel
// it here because there are graphical glitches if it's canceled while it's visible.
mBrowserToolbar.cancelEdit();
}
mTabsPanel.finishTabsAnimation();
mMainLayoutAnimator = null;
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
mDynamicToolbar.onSaveInstanceState(outState);
outState.putInt(STATE_ABOUT_HOME_TOP_PADDING, mHomeScreenContainer.getPaddingTop());
// The various add-on UI item caches and event listeners should really live somewhere based
// on the Application, so that their lifetime more closely matches that of Gecko itself, as
// GeckoView-based activities can start Gecko (and therefore add-ons) while BrowserApp isn't
// even running.
// For now we'll only guard against the case where BrowserApp is destroyed and later re-
// created while Gecko keeps running throughout, and leave the full solution to bug 1414084.
outState.putParcelableArrayList(STATE_ADDON_MENU_ITEM_CACHE, mAddonMenuItemsCache);
outState.putParcelableArrayList(STATE_BROWSER_ACTION_ITEM_CACHE, mBrowserActionItemsCache);
}
/**
* Attempts to switch to an open tab with the given URL.
* <p>
* If the tab exists, this method cancels any in-progress editing as well as
* calling {@link Tabs#selectTab(int)}.
*
* @param url of tab to switch to.
* @param flags to obey: if {@link OnUrlOpenListener.Flags#ALLOW_SWITCH_TO_TAB}
* is not present, return false.
* @return true if we successfully switched to a tab, false otherwise.
*/
private boolean maybeSwitchToTab(String url, EnumSet<OnUrlOpenListener.Flags> flags) {
if (!flags.contains(OnUrlOpenListener.Flags.ALLOW_SWITCH_TO_TAB)) {
return false;
}
final boolean isPrivate = mBrowserToolbar.isPrivateMode();
final Tabs tabs = Tabs.getInstance();
final Tab tab;
if (AboutPages.isAboutReader(url)) {
tab = tabs.getFirstReaderTabForUrl(url, isPrivate);
} else {
tab = tabs.getFirstTabForUrl(url, isPrivate);
}
if (tab == null) {
return false;
}
return maybeSwitchToTab(tab.getId());
}
/**
* Attempts to switch to an open tab with the given unique tab ID.
* <p>
* If the tab exists, this method cancels any in-progress editing as well as
* calling {@link Tabs#selectTab(int)}.
*
* @param id of tab to switch to.
* @return true if we successfully switched to the tab, false otherwise.
*/
private boolean maybeSwitchToTab(int id) {
final Tabs tabs = Tabs.getInstance();
final Tab tab = tabs.getTab(id);
if (tab == null) {
return false;
}
final Tab oldTab = tabs.getSelectedTab();
if (oldTab != null) {
oldTab.setIsEditing(false);
}
// Set the target tab to null so it does not get selected (on editing
// mode exit) in lieu of the tab we are about to select.
mTargetTabForEditingMode = null;
tabs.selectTab(tab.getId());
mBrowserToolbar.cancelEdit();
return true;
}
public void openUrlAndStopEditing(String url) {
openUrlAndStopEditing(url, null, null, false);
}
private void openUrlAndStopEditingWithReferrer(final String url, final String referrerUri) {
openUrlAndStopEditing(url, null, referrerUri, false);
}
private void openUrlAndStopEditing(String url, String searchEngine) {
openUrlAndStopEditing(url, searchEngine, null, false);
}
private void openUrlAndStopEditing(String url, String searchEngine, @Nullable final String referrerUri,
boolean newTab) {
int flags = Tabs.LOADURL_NONE;
if (newTab) {
flags |= Tabs.LOADURL_NEW_TAB;
if (Tabs.getInstance().getSelectedTab().isPrivate()) {
flags |= Tabs.LOADURL_PRIVATE;
}
}
Tabs.getInstance().loadUrl(url, searchEngine, referrerUri, Tabs.INVALID_TAB_ID, null, flags);
mBrowserToolbar.cancelEdit();
}
private boolean isHomePagerVisible() {
return (mHomeScreen != null && mHomeScreen.isVisible()
&& mHomeScreenContainer != null && mHomeScreenContainer.getVisibility() == View.VISIBLE);
}
private boolean isFirstrunVisible() {
return (mFirstrunAnimationContainer != null && mFirstrunAnimationContainer.isVisible()
&& mHomeScreenContainer != null && mHomeScreenContainer.getVisibility() == View.VISIBLE);
}
/**
* Enters editing mode with the current tab's URL. There might be no
* tabs loaded by the time the user enters editing mode e.g. just after
* the app starts. In this case, we simply fallback to an empty URL.
*/
private void enterEditingMode() {
String url = "";
String telemetryMsg = "urlbar-empty";
final Tab tab = Tabs.getInstance().getSelectedTab();
if (tab != null) {
final String userSearchTerm = tab.getUserRequested();
final String tabURL = tab.getURL();
// Check to see if there's a user-entered search term,
// which we save whenever the user performs a search.
if (!TextUtils.isEmpty(userSearchTerm)) {
url = userSearchTerm;
telemetryMsg = "urlbar-userentered";
} else if (!TextUtils.isEmpty(tabURL)) {
url = tabURL;
telemetryMsg = "urlbar-url";
if (splashScreen != null) {
splashScreen.setVisibility(View.GONE);
}
}
}
enterEditingMode(url);
Telemetry.sendUIEvent(TelemetryContract.Event.SHOW, TelemetryContract.Method.ACTIONBAR, telemetryMsg);
}
/**
* Enters editing mode with the specified URL. If a null
* url is given, the empty String will be used instead.
*/
private void enterEditingMode(@NonNull String url) {
hideFirstrunPager(TelemetryContract.Method.ACTIONBAR);
if (mBrowserToolbar.isEditing() || mBrowserToolbar.isAnimating()) {
return;
}
final Tab selectedTab = Tabs.getInstance().getSelectedTab();
final String panelId;
if (selectedTab != null) {
mTargetTabForEditingMode = selectedTab.getId();
panelId = selectedTab.getMostRecentHomePanel();
} else {
mTargetTabForEditingMode = null;
panelId = null;
}
final PropertyAnimator animator = new PropertyAnimator(250);
animator.setUseHardwareLayer(false);
mBrowserToolbar.startEditing(url, animator);
showHomePagerWithAnimator(panelId, null, animator);
animator.start();
Telemetry.startUISession(TelemetryContract.Session.AWESOMESCREEN);
}
private void commitEditingMode() {
if (!mBrowserToolbar.isEditing()) {
return;
}
Telemetry.stopUISession(TelemetryContract.Session.AWESOMESCREEN,
TelemetryContract.Reason.COMMIT);
final String url = mBrowserToolbar.commitEdit();
// HACK: We don't know the url that will be loaded when hideHomePager is initially called
// in BrowserToolbar's onStopEditing listener so on the awesomescreen, hideHomePager will
// use the url "about:home" and return without taking any action. hideBrowserSearch is
// then called, but since hideHomePager changes both HomePager and LayerView visibility
// and exited without taking an action, no Views are displayed and graphical corruption is
// visible instead.
//
// Here we call hideHomePager for the second time with the URL to be loaded so that
// hideHomePager is called with the correct state for the upcoming page load.
//
// Expected to be fixed by bug 915825.
hideHomePager(url);
loadUrlOrKeywordSearch(url);
clearSelectedTabApplicationId();
}
private void clearSelectedTabApplicationId() {
final Tab selected = Tabs.getInstance().getSelectedTab();
if (selected != null) {
selected.setApplicationId(null);
}
}
private void loadUrlOrKeywordSearch(final String url) {
// Don't do anything if the user entered an empty URL.
if (TextUtils.isEmpty(url)) {
return;
}
// If the URL doesn't look like a search query, just load it.
if (!StringUtils.isSearchQuery(url, true)) {
Tabs.getInstance().loadUrl(url, Tabs.LOADURL_USER_ENTERED);
Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.ACTIONBAR, "user");
return;
}
// Otherwise, check for a bookmark keyword.
final SharedPreferences sharedPrefs = GeckoSharedPrefs.forProfile(this);
final BrowserDB db = BrowserDB.from(getProfile());
ThreadUtils.postToBackgroundThread(new Runnable() {
@Override
public void run() {
final String keyword;
final String keywordSearch;
final int index = url.indexOf(" ");
if (index == -1) {
keyword = url;
keywordSearch = "";
} else {
keyword = url.substring(0, index);
keywordSearch = url.substring(index + 1);
}
final String keywordUrl = db.getUrlForKeyword(getContentResolver(), keyword);
// If there isn't a bookmark keyword, load the url. This may result in a query
// using the default search engine.
if (TextUtils.isEmpty(keywordUrl)) {
Tabs.getInstance().loadUrl(url, Tabs.LOADURL_USER_ENTERED);
Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.ACTIONBAR, "user");
return;
}
// Otherwise, construct a search query from the bookmark keyword.
// Replace lower case bookmark keywords with URLencoded search query or
// replace upper case bookmark keywords with un-encoded search query.
// This makes it match the same behaviour as on Firefox for the desktop.
final String searchUrl = keywordUrl.replace("%s", URLEncoder.encode(keywordSearch)).replace("%S", keywordSearch);
Tabs.getInstance().loadUrl(searchUrl, Tabs.LOADURL_USER_ENTERED);
Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL,
TelemetryContract.Method.ACTIONBAR,
"keyword");
}
});
}
/**
* Records in telemetry that a search has occurred.
*
* @param where where the search was started from
*/
private static void recordSearch(@NonNull final SharedPreferences prefs, @NonNull final String engineIdentifier,
@NonNull final TelemetryContract.Method where) {
// We could include the engine identifier as an extra but we'll
// just capture that with core ping telemetry (bug 1253319).
Telemetry.sendUIEvent(TelemetryContract.Event.SEARCH, where);
SearchCountMeasurements.incrementSearch(prefs, engineIdentifier, where.toString());
}
/**
* Store search query in SearchHistoryProvider.
*
* @param query
* a search query to store. We won't store empty queries.
*/
private void storeSearchQuery(final String query) {
if (TextUtils.isEmpty(query)) {
return;
}
// Filter out URLs and long suggestions
if (query.length() > 50 || Pattern.matches("^(https?|ftp|file)://.*", query)) {
return;
}
final GeckoProfile profile = getProfile();
// Don't bother storing search queries in guest mode
if (profile.inGuestMode()) {
return;
}
final BrowserDB db = BrowserDB.from(profile);
ThreadUtils.postToBackgroundThread(new Runnable() {
@Override
public void run() {
db.getSearches().insert(getContentResolver(), query);
}
});
}
void filterEditingMode(String searchTerm, AutocompleteHandler handler) {
if (TextUtils.isEmpty(searchTerm)) {
hideBrowserSearch();
} else {
showBrowserSearch();
mBrowserSearch.filter(searchTerm, handler);
}
}
/**
* Selects the target tab for editing mode. This is expected to be the tab selected on editing
* mode entry, unless it is subsequently overridden.
*
* A background tab may be selected while editing mode is active (e.g. popups), causing the
* new url to load in the newly selected tab. Call this method on editing mode exit to
* mitigate this.
*
* Note that this method is disabled for new tablets because we can see the selected tab in the
* tab strip and, when the selected tab changes during editing mode as in this hack, the
* temporarily selected tab is visible to users.
*/
private void selectTargetTabForEditingMode() {
if (HardwareUtils.isTablet()) {
return;
}
if (mTargetTabForEditingMode != null) {
Tabs.getInstance().selectTab(mTargetTabForEditingMode);
}
mTargetTabForEditingMode = null;
}
/**
* Shows or hides the home pager for the given tab.
*/
private void updateHomePagerForTab(Tab tab) {
// Don't change the visibility of the home pager if we're in editing mode.
if (mBrowserToolbar.isEditing()) {
return;
}
// History will only store that we were visiting about:home, however the specific panel
// isn't stored. (We are able to navigate directly to homepanels using an about:home?panel=...
// URL, but the reverse doesn't apply: manually switching panels doesn't update the URL.)
// Hence we need to restore the panel, in addition to panel state, here.
if (isAboutHome(tab)) {
// For some reason(e.g. from SearchWidget) we are showing the splash schreen. We should hide it now.
if (splashScreen != null && splashScreen.getVisibility() == View.VISIBLE) {
// Below line will be run when LOCATION_CHANGE. Which means the page load is almost completed.
splashScreen.hide();
}
String panelId = AboutPages.getPanelIdFromAboutHomeUrl(tab.getURL());
Bundle panelRestoreData = null;
if (panelId == null) {
// No panel was specified in the URL. Try loading the most recent
// home panel for this tab.
// Note: this isn't necessarily correct. We don't update the URL when we switch tabs.
// If a user explicitly navigated to about:reader?panel=FOO, and then switches
// to panel BAR, the history URL still contains FOO, and we restore to FOO. In most
// cases however we aren't supplying a panel ID in the URL so this code still works
// for most cases.
// We can't fix this directly since we can't ignore the panelId if we're explicitly
// loading a specific panel, and we currently can't distinguish between loading
// history, and loading new pages, see Bug 1268887
panelId = tab.getMostRecentHomePanel();
panelRestoreData = tab.getMostRecentHomePanelData();
} else if (panelId.equals(HomeConfig.getIdForBuiltinPanelType(PanelType.DEPRECATED_RECENT_TABS))) {
// Redirect to the Combined History panel.
panelId = HomeConfig.getIdForBuiltinPanelType(PanelType.COMBINED_HISTORY);
panelRestoreData = new Bundle();
// Jump directly to the Recent Tabs subview of the Combined History panel.
panelRestoreData.putBoolean("goToRecentTabs", true);
}
showHomePager(panelId, panelRestoreData);
if (mDynamicToolbar.isEnabled()) {
mDynamicToolbar.setVisible(true, VisibilityTransition.ANIMATE);
}
showSplashScreen = false;
} else {
// The tab going to load is not about page. It's a web page.
// If showSplashScreen is true, it means the app is first launched. We want to show the SlashScreen
// But if GeckoThread.isRunning, the will be 0 sec for web rendering.
// In that case, we don't want to show the SlashScreen/
if (showSplashScreen && !GeckoThread.isRunning()) {
final ViewGroup main = (ViewGroup) findViewById(R.id.gecko_layout);
final View splashLayout = LayoutInflater.from(this).inflate(R.layout.splash_screen, main);
splashScreen = (SplashScreen) splashLayout.findViewById(R.id.splash_root);
showSplashScreen = false;
} else if (splashScreen != null) {
// Below line will be run when LOCATION_CHANGE. Which means the page load is almost completed.
splashScreen.hide();
}
hideHomePager();
}
}
@Override
public void onLocaleReady(final String locale) {
Log.d(LOGTAG, "onLocaleReady: " + locale);
super.onLocaleReady(locale);
HomePanelsManager.getInstance().onLocaleReady(locale);
if (mMenu != null) {
mMenu.clear();
onCreateOptionsMenu(mMenu);
}
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
Log.d(LOGTAG, "onActivityResult: " + requestCode + ", " + resultCode + ", " + data);
switch (requestCode) {
case ACTIVITY_REQUEST_PREFERENCES:
// We just returned from preferences. If our locale changed,
// we need to redisplay at this point, and do any other browser-level
// bookkeeping that we associate with a locale change.
if (resultCode != GeckoPreferences.RESULT_CODE_LOCALE_DID_CHANGE) {
Log.d(LOGTAG, "No locale change returning from preferences; nothing to do.");
return;
}
ThreadUtils.postToBackgroundThread(new Runnable() {
@Override
public void run() {
final LocaleManager localeManager = BrowserLocaleManager.getInstance();
final Locale locale = localeManager.getCurrentLocale(getApplicationContext());
Log.d(LOGTAG, "Read persisted locale " + locale);
if (locale == null) {
return;
}
onLocaleChanged(Locales.getLanguageTag(locale));
}
});
break;
case ACTIVITY_REQUEST_TAB_QUEUE:
TabQueueHelper.processTabQueuePromptResponse(resultCode, this);
break;
default:
for (final BrowserAppDelegate delegate : delegates) {
delegate.onActivityResult(this, requestCode, resultCode, data);
}
super.onActivityResult(requestCode, resultCode, data);
}
}
private void showFirstrunPager() {
if (mFirstrunAnimationContainer == null) {
final ViewStub firstrunPagerStub = (ViewStub) findViewById(R.id.firstrun_pager_stub);
mFirstrunAnimationContainer = (FirstrunAnimationContainer) firstrunPagerStub.inflate();
mFirstrunAnimationContainer.load(getApplicationContext(), getSupportFragmentManager());
mFirstrunAnimationContainer.registerOnFinishListener(new FirstrunAnimationContainer.OnFinishListener() {
@Override
public void onFinish() {
if (mFirstrunAnimationContainer.showBrowserHint() &&
!Tabs.hasHomepage(BrowserApp.this)) {
enterEditingMode();
}
}
});
}
mHomeScreenContainer.setVisibility(View.VISIBLE);
}
private void showHomePager(String panelId, Bundle panelRestoreData) {
showHomePagerWithAnimator(panelId, panelRestoreData, null);
}
private void showHomePagerWithAnimator(String panelId, Bundle panelRestoreData, PropertyAnimator animator) {
if (isHomePagerVisible()) {
// Home pager already visible, make sure it shows the correct panel.
mHomeScreen.showPanel(panelId, panelRestoreData);
return;
}
// This must be called before the dynamic toolbar is set visible because it calls
// FormAssistPopup.onMetricsChanged, which queues a runnable that undoes the effect of hide.
// With hide first, onMetricsChanged will return early instead.
mFormAssistPopup.hide();
mFindInPageBar.hide();
// Refresh toolbar height to possibly restore the toolbar padding
refreshToolbarHeight();
// Show the toolbar before hiding about:home so the
// onMetricsChanged callback still works.
if (mDynamicToolbar.isEnabled()) {
mDynamicToolbar.setVisible(true, VisibilityTransition.IMMEDIATE);
}
if (mHomeScreen == null) {
if (ActivityStream.isEnabled(this) &&
!ActivityStream.isHomePanel()) {
final ViewStub asStub = (ViewStub) findViewById(R.id.activity_stream_stub);
mHomeScreen = (HomeScreen) asStub.inflate();
} else {
final ViewStub homePagerStub = (ViewStub) findViewById(R.id.home_pager_stub);
mHomeScreen = (HomeScreen) homePagerStub.inflate();
// For now these listeners are HomePager specific. In future we might want
// to have a more abstracted data storage, with one Bundle containing all
// relevant restore data.
mHomeScreen.setOnPanelChangeListener(new HomeScreen.OnPanelChangeListener() {
@Override
public void onPanelSelected(String panelId) {
final Tab currentTab = Tabs.getInstance().getSelectedTab();
if (currentTab != null) {
currentTab.setMostRecentHomePanel(panelId);
}
}
});
// Set this listener to persist restore data (via the Tab) every time panel state changes.
mHomeScreen.setPanelStateChangeListener(new HomeFragment.PanelStateChangeListener() {
@Override
public void onStateChanged(Bundle bundle) {
final Tab currentTab = Tabs.getInstance().getSelectedTab();
if (currentTab != null) {
currentTab.setMostRecentHomePanelData(bundle);
}
}
@Override
public void setCachedRecentTabsCount(int count) {
mCachedRecentTabsCount = count;
}
@Override
public int getCachedRecentTabsCount() {
return mCachedRecentTabsCount;
}
});
}
// Don't show the banner in guest mode.
if (!Restrictions.isUserRestricted()) {
final ViewStub homeBannerStub = (ViewStub) findViewById(R.id.home_banner_stub);
final HomeBanner homeBanner = (HomeBanner) homeBannerStub.inflate();
mHomeScreen.setBanner(homeBanner);
// Remove the banner from the view hierarchy if it is dismissed.
homeBanner.setOnDismissListener(new HomeBanner.OnDismissListener() {
@Override
public void onDismiss() {
mHomeScreen.setBanner(null);
mHomeScreenContainer.removeView(homeBanner);
}
});
}
}
mHomeScreenContainer.setVisibility(View.VISIBLE);
mHomeScreen.load(getSupportLoaderManager(),
getSupportFragmentManager(),
panelId,
panelRestoreData,
animator);
// Hide the web content so it cannot be focused by screen readers.
hideWebContentOnPropertyAnimationEnd(animator);
}
private void hideWebContentOnPropertyAnimationEnd(final PropertyAnimator animator) {
if (animator == null) {
hideWebContent();
return;
}
animator.addPropertyAnimationListener(new PropertyAnimator.PropertyAnimationListener() {
@Override
public void onPropertyAnimationStart() {
mHideWebContentOnAnimationEnd = true;
}
@Override
public void onPropertyAnimationEnd() {
if (mHideWebContentOnAnimationEnd) {
hideWebContent();
}
}
});
}
private void hideWebContent() {
// The view is set to INVISIBLE, rather than GONE, to avoid
// the additional requestLayout() call.
mLayerView.setVisibility(View.INVISIBLE);
}
/**
* Hide the Onboarding pager on user action, and don't show any onFinish hints.
* @param method TelemetryContract method by which action was taken
* @return boolean of whether pager was visible
*/
private boolean hideFirstrunPager(TelemetryContract.Method method) {
if (!isFirstrunVisible()) {
return false;
}
Telemetry.sendUIEvent(TelemetryContract.Event.CANCEL, method, "firstrun-pane");
// Don't show any onFinish actions when hiding from this Activity.
mFirstrunAnimationContainer.registerOnFinishListener(null);
mFirstrunAnimationContainer.hide();
return true;
}
/**
* Hides the HomePager, using the url of the currently selected tab as the url to be
* loaded.
*/
private void hideHomePager() {
final Tab selectedTab = Tabs.getInstance().getSelectedTab();
final String url = (selectedTab != null) ? selectedTab.getURL() : null;
hideHomePager(url);
}
/**
* Hides the HomePager. The given url should be the url of the page to be loaded, or null
* if a new page is not being loaded.
*/
private void hideHomePager(final String url) {
if (!isHomePagerVisible() || AboutPages.isAboutHome(url)) {
return;
}
// Prevent race in hiding web content - see declaration for more info.
mHideWebContentOnAnimationEnd = false;
// Display the previously hidden web content (which prevented screen reader access).
mLayerView.setVisibility(View.VISIBLE);
mHomeScreenContainer.setVisibility(View.GONE);
if (mHomeScreen != null) {
mHomeScreen.unload();
}
mBrowserToolbar.setNextFocusDownId(R.id.layer_view);
// Refresh toolbar height to possibly restore the toolbar padding
refreshToolbarHeight();
}
private void showBrowserSearchAfterAnimation(PropertyAnimator animator) {
if (animator == null) {
showBrowserSearch();
return;
}
animator.addPropertyAnimationListener(new PropertyAnimator.PropertyAnimationListener() {
@Override
public void onPropertyAnimationStart() {
}
@Override
public void onPropertyAnimationEnd() {
showBrowserSearch();
}
});
}
private void showBrowserSearch() {
if (mBrowserSearch.getUserVisibleHint()) {
return;
}
mBrowserSearchContainer.setVisibility(View.VISIBLE);
// Prevent overdraw by hiding the underlying web content and HomePager View
hideWebContent();
mHomeScreenContainer.setVisibility(View.INVISIBLE);
final FragmentManager fm = getSupportFragmentManager();
// In certain situations, showBrowserSearch() can be called immediately after hideBrowserSearch()
// (see bug 925012). Because of an Android bug (http://code.google.com/p/android/issues/detail?id=61179),
// calling FragmentTransaction#add immediately after FragmentTransaction#remove won't add the fragment's
// view to the layout. Calling FragmentManager#executePendingTransactions before re-adding the fragment
// prevents this issue.
fm.executePendingTransactions();
Fragment f = fm.findFragmentById(R.id.search_container);
// checking if fragment is already present
if (f != null) {
fm.beginTransaction().show(f).commitAllowingStateLoss();
mBrowserSearch.resetScrollState();
} else {
// add fragment if not already present
fm.beginTransaction().add(R.id.search_container, mBrowserSearch, BROWSER_SEARCH_TAG).commitAllowingStateLoss();
}
mBrowserSearch.setUserVisibleHint(true);
// We want to adjust the window size when the keyboard appears to bring the
// SearchEngineBar above the keyboard. However, adjusting the window size
// when hiding the keyboard results in graphical glitches where the keyboard was
// because nothing was being drawn underneath (bug 933422). This can be
// prevented drawing content under the keyboard (i.e. in the Window).
//
// We do this here because there are glitches when unlocking a device with
// BrowserSearch in the foreground if we use BrowserSearch.onStart/Stop.
getWindow().setBackgroundDrawableResource(android.R.color.white);
}
private void hideBrowserSearch() {
if (!mBrowserSearch.getUserVisibleHint()) {
return;
}
final Tab selectedTab = Tabs.getInstance().getSelectedTab();
final String panelId;
final Bundle panelData;
if (selectedTab != null) {
panelId = selectedTab.getMostRecentHomePanel();
panelData = selectedTab.getMostRecentHomePanelData();
} else {
panelId = null;
panelData = null;
}
// To prevent overdraw, the HomePager is hidden when BrowserSearch is displayed:
// reverse that.
showHomePager(panelId, panelData);
mBrowserSearchContainer.setVisibility(View.INVISIBLE);
getSupportFragmentManager().beginTransaction()
.hide(mBrowserSearch).commitAllowingStateLoss();
mBrowserSearch.setUserVisibleHint(false);
getWindow().setBackgroundDrawable(null);
}
/**
* Hides certain UI elements (e.g. button toast) when the user touches the main layout.
*/
private static final class HideOnTouchListener implements TouchEventInterceptor {
@Override
public boolean onInterceptTouchEvent(View view, MotionEvent event) {
if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
SnackbarBuilder.dismissCurrentSnackbar();
}
return false;
}
@Override
public boolean onTouch(View view, MotionEvent event) {
return false;
}
}
private static Menu findParentMenu(Menu menu, MenuItem item) {
final int itemId = item.getItemId();
final int count = (menu != null) ? menu.size() : 0;
for (int i = 0; i < count; i++) {
MenuItem menuItem = menu.getItem(i);
if (menuItem.getItemId() == itemId) {
return menu;
}
if (menuItem.hasSubMenu()) {
Menu parent = findParentMenu(menuItem.getSubMenu(), item);
if (parent != null) {
return parent;
}
}
}
return null;
}
/**
* Add the provided item to the provided menu, which should be
* the root (mMenu).
*/
private void addAddonMenuItemToMenu(final Menu menu, final MenuItemInfo info) {
info.added = true;
final Menu destination;
if (info.parent == 0) {
destination = menu;
} else if (info.parent == GECKO_TOOLS_MENU) {
// The tools menu only exists in our -v11 resources.
final MenuItem tools = menu.findItem(R.id.tools);
destination = tools != null ? tools.getSubMenu() : menu;
} else {
final MenuItem parent = menu.findItem(info.parent);
if (parent == null) {
return;
}
Menu parentMenu = findParentMenu(menu, parent);
if (!parent.hasSubMenu()) {
parentMenu.removeItem(parent.getItemId());
destination = parentMenu.addSubMenu(Menu.NONE, parent.getItemId(), Menu.NONE, parent.getTitle());
if (parent.getIcon() != null) {
((SubMenu) destination).getItem().setIcon(parent.getIcon());
}
} else {
destination = parent.getSubMenu();
}
}
final MenuItem item = destination.add(Menu.NONE, info.id, Menu.NONE, info.label);
item.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem item) {
final GeckoBundle data = new GeckoBundle(1);
data.putInt("item", info.id - ADDON_MENU_OFFSET);
EventDispatcher.getInstance().dispatch("Menu:Clicked", data);
return true;
}
});
item.setCheckable(info.checkable);
item.setChecked(info.checked);
item.setEnabled(info.enabled);
item.setVisible(info.visible);
}
private void addAddonMenuItem(final MenuItemInfo info) {
if (mAddonMenuItemsCache == null) {
mAddonMenuItemsCache = new ArrayList<MenuItemInfo>();
}
// Mark it as added if the menu was ready.
info.added = (mMenu != null);
// Always cache so we can rebuild after a locale switch.
mAddonMenuItemsCache.add(info);
if (mMenu == null) {
return;
}
addAddonMenuItemToMenu(mMenu, info);
}
private void removeAddonMenuItem(int id) {
// Remove add-on menu item from cache, if available.
if (mAddonMenuItemsCache != null && !mAddonMenuItemsCache.isEmpty()) {
for (MenuItemInfo item : mAddonMenuItemsCache) {
if (item.id == id) {
mAddonMenuItemsCache.remove(item);
break;
}
}
}
if (mMenu == null)
return;
final MenuItem menuItem = mMenu.findItem(id);
if (menuItem != null)
mMenu.removeItem(id);
}
private void updateAddonMenuItem(int id, final GeckoBundle options) {
// Set attribute for the menu item in cache, if available
if (mAddonMenuItemsCache != null && !mAddonMenuItemsCache.isEmpty()) {
for (MenuItemInfo item : mAddonMenuItemsCache) {
if (item.id == id) {
item.label = options.getString("name", item.label);
item.checkable = options.getBoolean("checkable", item.checkable);
item.checked = options.getBoolean("checked", item.checked);
item.enabled = options.getBoolean("enabled", item.enabled);
item.visible = options.getBoolean("visible", item.visible);
item.added = (mMenu != null);
break;
}
}
}
if (mMenu == null) {
return;
}
final MenuItem menuItem = mMenu.findItem(id);
if (menuItem != null) {
menuItem.setTitle(options.getString("name", menuItem.getTitle().toString()));
menuItem.setCheckable(options.getBoolean("checkable", menuItem.isCheckable()));
menuItem.setChecked(options.getBoolean("checked", menuItem.isChecked()));
menuItem.setEnabled(options.getBoolean("enabled", menuItem.isEnabled()));
menuItem.setVisible(options.getBoolean("visible", menuItem.isVisible()));
}
}
/**
* Add the provided item to the provided menu, which should be
* the root (mMenu).
*/
private void addBrowserActionMenuItemToMenu(final Menu menu, final BrowserActionItemInfo info) {
info.added = true;
final MenuItem item = menu.add(Menu.NONE, info.id, Menu.NONE, info.label);
item.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem item) {
final GeckoBundle data = new GeckoBundle(1);
data.putString("item", info.uuid);
EventDispatcher.getInstance().dispatch("Menu:BrowserActionClicked", data);
return true;
}
});
item.setCheckable(info.checkable);
item.setChecked(info.checked);
item.setEnabled(info.enabled);
item.setVisible(info.visible);
}
/**
* Adds a WebExtension browser action to the menu.
*/
private void addBrowserActionMenuItem(final BrowserActionItemInfo info) {
if (mBrowserActionItemsCache == null) {
mBrowserActionItemsCache = new ArrayList<BrowserActionItemInfo>();
}
// Mark it as added if the menu was ready.
info.added = (mMenu != null);
// Always cache so we can rebuild after a locale switch.
mBrowserActionItemsCache.add(info);
if (mMenu == null) {
return;
}
addBrowserActionMenuItemToMenu(mMenu, info);
}
/**
* Removes a WebExtension browser action from the menu by its UUID.
*/
private void removeBrowserActionMenuItem(String uuid) {
int id = -1;
// Remove browser action menu item from cache, if available.
if (mBrowserActionItemsCache != null && !mBrowserActionItemsCache.isEmpty()) {
for (BrowserActionItemInfo item : mBrowserActionItemsCache) {
if (item.uuid.equals(uuid)) {
id = item.id;
mBrowserActionItemsCache.remove(item);
break;
}
}
}
if (mMenu == null || id == -1) {
return;
}
final MenuItem menuItem = mMenu.findItem(id);
if (menuItem != null) {
mMenu.removeItem(id);
}
}
/**
* Updates the WebExtension browser action with the specified UUID.
*/
private void updateBrowserActionMenuItem(String uuid, final GeckoBundle options) {
int id = -1;
// Set attribute for the menu item in cache, if available
if (mBrowserActionItemsCache != null && !mBrowserActionItemsCache.isEmpty()) {
for (BrowserActionItemInfo item : mBrowserActionItemsCache) {
if (item.uuid.equals(uuid)) {
id = item.id;
item.label = options.getString("name", item.label);
break;
}
}
}
if (mMenu == null || id == -1) {
return;
}
final MenuItem menuItem = mMenu.findItem(id);
if (menuItem != null) {
menuItem.setTitle(options.getString("name", menuItem.getTitle().toString()));
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Sets mMenu = menu.
super.onCreateOptionsMenu(menu);
// Inform the menu about the action-items bar.
if (menu instanceof GeckoMenu &&
HardwareUtils.isTablet()) {
((GeckoMenu) menu).setActionItemBarPresenter(mBrowserToolbar);
}
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.browser_app_menu, mMenu);
// Add browser action menu items, if any exist.
if (mBrowserActionItemsCache != null && !mBrowserActionItemsCache.isEmpty()) {
for (BrowserActionItemInfo item : mBrowserActionItemsCache) {
addBrowserActionMenuItemToMenu(mMenu, item);
}
}
// Add add-on menu items, if any exist.
if (mAddonMenuItemsCache != null && !mAddonMenuItemsCache.isEmpty()) {
for (MenuItemInfo item : mAddonMenuItemsCache) {
addAddonMenuItemToMenu(mMenu, item);
}
}
// Action providers are available only ICS+.
GeckoMenuItem share = (GeckoMenuItem) mMenu.findItem(R.id.share);
GeckoActionProvider provider = GeckoActionProvider.getForType(GeckoActionProvider.DEFAULT_MIME_TYPE, this);
share.setActionProvider(provider);
return true;
}
@Override
public void openOptionsMenu() {
hideFirstrunPager(TelemetryContract.Method.MENU);
// Disable menu access (for hardware buttons) when the software menu button is inaccessible.
// Note that the software button is always accessible on new tablet.
if (mBrowserToolbar.isEditing() && !HardwareUtils.isTablet()) {
return;
}
if (ActivityUtils.isFullScreen(this)) {
return;
}
if (areTabsShown()) {
mTabsPanel.showMenu();
return;
}
// Scroll custom menu to the top
if (mMenuPanel != null)
mMenuPanel.scrollTo(0, 0);
// Scroll menu ListView (potentially in MenuPanel ViewGroup) to top.
if (mMenu instanceof GeckoMenu) {
((GeckoMenu) mMenu).setSelection(0);
}
if (!mBrowserToolbar.openOptionsMenu())
super.openOptionsMenu();
if (mDynamicToolbar.isEnabled()) {
mDynamicToolbar.setVisible(true, VisibilityTransition.ANIMATE);
}
}
@Override
public void closeOptionsMenu() {
if (!mBrowserToolbar.closeOptionsMenu())
super.closeOptionsMenu();
}
@Override // GeckoView.ContentListener
public void onFullScreen(final GeckoSession session, final boolean fullscreen) {
super.onFullScreen(session, fullscreen);
if (fullscreen) {
mDynamicToolbar.setVisible(false, VisibilityTransition.IMMEDIATE);
mDynamicToolbar.setPinned(true, PinReason.FULL_SCREEN);
} else {
mDynamicToolbar.setPinned(false, PinReason.FULL_SCREEN);
mDynamicToolbar.setVisible(true, VisibilityTransition.IMMEDIATE);
}
}
@Override
public boolean onPrepareOptionsMenu(Menu aMenu) {
if (aMenu == null)
return false;
// Hide the tab history panel when hardware menu button is pressed.
TabHistoryFragment frag = (TabHistoryFragment) getSupportFragmentManager().findFragmentByTag(TAB_HISTORY_FRAGMENT_TAG);
if (frag != null) {
frag.dismiss();
}
if (!GeckoThread.isRunning()) {
aMenu.findItem(R.id.settings).setEnabled(false);
aMenu.findItem(R.id.help).setEnabled(false);
}
Tab tab = Tabs.getInstance().getSelectedTab();
// Unlike other menu items, the bookmark star is not tinted. See {@link ThemedImageButton#setTintedDrawable}.
final MenuItem bookmark = aMenu.findItem(R.id.bookmark);
final MenuItem back = aMenu.findItem(R.id.back);
final MenuItem forward = aMenu.findItem(R.id.forward);
final MenuItem share = aMenu.findItem(R.id.share);
final MenuItem bookmarksList = aMenu.findItem(R.id.bookmarks_list);
final MenuItem historyList = aMenu.findItem(R.id.history_list);
final MenuItem saveAsPDF = aMenu.findItem(R.id.save_as_pdf);
final MenuItem print = aMenu.findItem(R.id.print);
final MenuItem viewPageSource = aMenu.findItem(R.id.view_page_source);
final MenuItem charEncoding = aMenu.findItem(R.id.char_encoding);
final MenuItem findInPage = aMenu.findItem(R.id.find_in_page);
final MenuItem desktopMode = aMenu.findItem(R.id.desktop_mode);
final MenuItem enterGuestMode = aMenu.findItem(R.id.new_guest_session);
final MenuItem exitGuestMode = aMenu.findItem(R.id.exit_guest_session);
// Only show the "Quit" menu item on pre-ICS, television devices,
// or if the user has explicitly enabled the clear on shutdown pref.
// (We check the pref last to save the pref read.)
// In ICS+, it's easy to kill an app through the task switcher.
final SharedPreferences prefs = GeckoSharedPrefs.forProfile(this);
final boolean visible = HardwareUtils.isTelevision() ||
prefs.getBoolean(GeckoPreferences.PREFS_SHOW_QUIT_MENU, false) ||
!PrefUtils.getStringSet(prefs,
ClearOnShutdownPref.PREF,
new HashSet<String>()).isEmpty();
aMenu.findItem(R.id.quit).setVisible(visible);
// If tab data is unavailable we disable most of the context menu and related items and
// return early.
if (tab == null || tab.getURL() == null) {
bookmark.setEnabled(false);
back.setEnabled(false);
forward.setEnabled(false);
share.setEnabled(false);
saveAsPDF.setEnabled(false);
print.setEnabled(false);
findInPage.setEnabled(false);
viewPageSource.setEnabled(false);
// NOTE: Use MenuUtils.safeSetEnabled because some actions might
// be on the BrowserToolbar context menu.
MenuUtils.safeSetEnabled(aMenu, R.id.page, false);
MenuUtils.safeSetEnabled(aMenu, R.id.subscribe, false);
MenuUtils.safeSetEnabled(aMenu, R.id.add_search_engine, false);
MenuUtils.safeSetEnabled(aMenu, R.id.pin_to_top_sites, false);
MenuUtils.safeSetEnabled(aMenu, R.id.add_to_launcher, false);
MenuUtils.safeSetEnabled(aMenu, R.id.set_as_homepage, false);
final MenuItem pinToTopSitesItem = aMenu.findItem(R.id.pin_to_top_sites);
if (pinToTopSitesItem != null) {
// This title is set dynamically so we reset it for this edge case.
pinToTopSitesItem.setTitle(R.string.contextmenu_pin_to_top_sites);
}
return true;
}
// If tab data IS available we need to manually enable items as necessary. They may have
// been disabled if returning early above, hence every item must be toggled, even if it's
// always expected to be enabled (e.g. the bookmark star is always enabled, except when
// we don't have tab data).
final boolean inGuestMode = GeckoProfile.get(this).inGuestMode();
bookmark.setEnabled(true); // Might have been disabled above, ensure it's reenabled
bookmark.setVisible(!inGuestMode);
bookmark.setCheckable(true);
bookmark.setChecked(tab.isBookmark());
bookmark.setTitle(resolveBookmarkTitleID(tab.isBookmark()));
final boolean isPrivate = tab.isPrivate();
// We don't use icons on GB builds so not resolving icons might conserve resources.
bookmark.setIcon(resolveBookmarkIconDrawable(tab.isBookmark(), resolveMenuIconTint(isPrivate)));
back.setEnabled(tab.canDoBack());
forward.setEnabled(tab.canDoForward());
desktopMode.setChecked(tab.getDesktopMode());
View backButtonView = MenuItemCompat.getActionView(back);
if (backButtonView != null) {
backButtonView.setOnLongClickListener(new Button.OnLongClickListener() {
@Override
public boolean onLongClick(View view) {
Tab tab = Tabs.getInstance().getSelectedTab();
if (tab != null) {
closeOptionsMenu();
return tabHistoryController.showTabHistory(tab,
TabHistoryController.HistoryAction.BACK);
}
return false;
}
});
}
View forwardButtonView = MenuItemCompat.getActionView(forward);
if (forwardButtonView != null) {
forwardButtonView.setOnLongClickListener(new Button.OnLongClickListener() {
@Override
public boolean onLongClick(View view) {
Tab tab = Tabs.getInstance().getSelectedTab();
if (tab != null) {
closeOptionsMenu();
return tabHistoryController.showTabHistory(tab,
TabHistoryController.HistoryAction.FORWARD);
}
return false;
}
});
}
String url = tab.getURL();
if (AboutPages.isAboutReader(url)) {
url = ReaderModeUtils.stripAboutReaderUrl(url);
}
// Disable share menuitem for about:, chrome:, file:, and resource: URIs
final boolean shareVisible = Restrictions.isAllowed(this, Restrictable.SHARE);
share.setVisible(shareVisible);
final boolean shareEnabled = StringUtils.isShareableUrl(url) && shareVisible;
share.setEnabled(shareEnabled);
MenuUtils.safeSetEnabled(aMenu, R.id.downloads, Restrictions.isAllowed(this, Restrictable.DOWNLOAD));
final boolean distSetAsHomepage = GeckoSharedPrefs.forProfile(this).getBoolean(GeckoPreferences.PREFS_SET_AS_HOMEPAGE, false);
MenuUtils.safeSetVisible(aMenu, R.id.set_as_homepage, distSetAsHomepage);
// NOTE: Use MenuUtils.safeSetEnabled because some actions might
// be on the BrowserToolbar context menu.
MenuUtils.safeSetEnabled(aMenu, R.id.page, !isAboutHome(tab));
MenuUtils.safeSetEnabled(aMenu, R.id.subscribe, tab.hasFeeds());
MenuUtils.safeSetEnabled(aMenu, R.id.add_search_engine, tab.hasOpenSearch());
MenuUtils.safeSetEnabled(aMenu, R.id.add_to_launcher,
!isAboutHome(tab) && ShortcutUtils.isPinShortcutSupported());
MenuUtils.safeSetEnabled(aMenu, R.id.set_as_homepage, !isAboutHome(tab));
onPrepareOptionsMenuPinToTopSites(aMenu, tab);
// This provider also applies to the quick share menu item.
final GeckoActionProvider provider = ((GeckoMenuItem) share).getGeckoActionProvider();
if (provider != null) {
Intent shareIntent = provider.getIntent();
// For efficiency, the provider's intent is only set once
if (shareIntent == null) {
shareIntent = new Intent(Intent.ACTION_SEND);
shareIntent.setType("text/plain");
provider.setIntent(shareIntent);
}
// Replace the existing intent's extras
shareIntent.putExtra(Intent.EXTRA_TEXT, url);
shareIntent.putExtra(Intent.EXTRA_SUBJECT, tab.getDisplayTitle());
shareIntent.putExtra(Intent.EXTRA_TITLE, tab.getDisplayTitle());
shareIntent.putExtra(ShareDialog.INTENT_EXTRA_DEVICES_ONLY, true);
// Clear the existing thumbnail extras so we don't share an old thumbnail.
shareIntent.removeExtra("share_screenshot_uri");
// Include the thumbnail of the page being shared.
BitmapDrawable drawable = tab.getThumbnail();
if (drawable != null) {
Bitmap thumbnail = drawable.getBitmap();
// Kobo uses a custom intent extra for sharing thumbnails.
if (Build.MANUFACTURER.equals("Kobo") && thumbnail != null) {
File cacheDir = getExternalCacheDir();
if (cacheDir != null) {
File outFile = new File(cacheDir, "thumbnail.png");
try {
final java.io.FileOutputStream out = new java.io.FileOutputStream(outFile);
try {
thumbnail.compress(Bitmap.CompressFormat.PNG, 90, out);
} finally {
try {
out.close();
} catch (final IOException e) { /* Nothing to do here. */ }
}
} catch (FileNotFoundException e) {
Log.e(LOGTAG, "File not found", e);
}
shareIntent.putExtra("share_screenshot_uri", Uri.parse(outFile.getPath()));
}
}
}
}
final boolean privateTabVisible = Restrictions.isAllowed(this, Restrictable.PRIVATE_BROWSING);
MenuUtils.safeSetVisible(aMenu, R.id.new_private_tab, privateTabVisible);
// Disable PDF generation (save and print) for about:home and xul pages.
boolean allowPDF = (!(isAboutHome(tab) ||
tab.getContentType().equals("application/vnd.mozilla.xul+xml") ||
tab.getContentType().startsWith("video/")));
saveAsPDF.setEnabled(allowPDF);
print.setEnabled(allowPDF);
print.setVisible(Versions.feature19Plus);
// Disable find in page and view source for about:home, since it won't work on Java content.
final boolean notInAboutHome = !isAboutHome(tab);
findInPage.setEnabled(notInAboutHome);
viewPageSource.setEnabled(notInAboutHome);
charEncoding.setVisible(GeckoPreferences.getCharEncodingState());
if (getProfile().inGuestMode()) {
exitGuestMode.setVisible(true);
} else {
enterGuestMode.setVisible(true);
}
if (!Restrictions.isAllowed(this, Restrictable.GUEST_BROWSING)) {
MenuUtils.safeSetVisible(aMenu, R.id.new_guest_session, false);
}
if (SwitchBoard.isInExperiment(this, Experiments.TOP_ADDONS_MENU)) {
MenuUtils.safeSetVisible(aMenu, R.id.addons_top_level, true);
GeckoMenuItem item = (GeckoMenuItem) aMenu.findItem(R.id.addons_top_level);
if (item != null) {
if (mExtensionPermissionsHelper.getShowUpdateIcon()) {
item.setIcon(R.drawable.ic_addon_update);
} else {
item.setIcon(null);
}
}
MenuUtils.safeSetVisible(aMenu, R.id.addons, false);
} else {
MenuUtils.safeSetVisible(aMenu, R.id.addons_top_level, false);
MenuUtils.safeSetVisible(aMenu, R.id.addons, true);
}
if (!Restrictions.isAllowed(this, Restrictable.INSTALL_EXTENSION)) {
MenuUtils.safeSetVisible(aMenu, R.id.addons, false);
MenuUtils.safeSetVisible(aMenu, R.id.addons_top_level, false);
}
// Hide panel menu items if the panels themselves are hidden.
// If we don't know whether the panels are hidden, just show the menu items.
bookmarksList.setVisible(prefs.getBoolean(HomeConfig.PREF_KEY_BOOKMARKS_PANEL_ENABLED, true));
historyList.setVisible(prefs.getBoolean(HomeConfig.PREF_KEY_HISTORY_PANEL_ENABLED, true));
return true;
}
private void onPrepareOptionsMenuPinToTopSites(final Menu aMenu, final Tab tab) {
final MenuItem item = aMenu.findItem(R.id.pin_to_top_sites);
if (item == null) {
return;
}
// Set initial state before async query completes.
item.setEnabled(false); // Disable interaction.
item.setTitle(R.string.contextmenu_pin_to_top_sites);
ThreadUtils.postToBackgroundThread(new Runnable() {
@Override
public void run() {
final boolean isPinned = BrowserDB.from(BrowserApp.this).isPinnedForAS(getContentResolver(), tab.getURL());
ThreadUtils.postToUiThread(new Runnable() {
@Override
public void run() {
item.setTitle(isPinned ?
R.string.contextmenu_unpin_from_top_sites : R.string.contextmenu_pin_to_top_sites);
item.setEnabled(true);
}
});
}
});
}
private Drawable resolveBookmarkIconDrawable(final boolean isBookmark, final int tint) {
if (isBookmark) {
return ResourcesCompat.getDrawable(getResources(), R.drawable.star_blue, null);
} else {
return DrawableUtil.tintDrawable(this, R.drawable.ic_menu_bookmark_add, tint);
}
}
private int resolveMenuIconTint(final boolean isPrivate) {
final int tintResId;
if (isPrivate && HardwareUtils.isLargeTablet()) {
tintResId = R.color.menu_item_tint_private;
} else {
tintResId = R.color.menu_item_tint;
}
return ResourcesCompat.getColor(getResources(), tintResId, null);
}
private int resolveBookmarkTitleID(final boolean isBookmark) {
return (isBookmark ? R.string.bookmark_remove : R.string.bookmark);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
Tab tab = null;
Intent intent = null;
final int itemId = item.getItemId();
// Track the menu action. We don't know much about the context, but we can use this to determine
// the frequency of use for various actions.
String extras = getResources().getResourceEntryName(itemId);
if (TextUtils.equals(extras, "new_private_tab")) {
// Mask private browsing
extras = "new_tab";
}
Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.MENU, extras);
mBrowserToolbar.cancelEdit();
if (itemId == R.id.bookmark) {
tab = Tabs.getInstance().getSelectedTab();
if (tab != null) {
final String extra;
if (AboutPages.isAboutReader(tab.getURL())) {
extra = "bookmark_reader";
} else {
extra = "bookmark";
}
final boolean isPrivate = tab.isPrivate();
if (item.isChecked()) {
Telemetry.sendUIEvent(TelemetryContract.Event.UNSAVE, TelemetryContract.Method.MENU, extra);
tab.removeBookmark();
item.setTitle(resolveBookmarkTitleID(false));
item.setIcon(resolveBookmarkIconDrawable(false, resolveMenuIconTint(isPrivate)));
} else {
Telemetry.sendUIEvent(TelemetryContract.Event.SAVE, TelemetryContract.Method.MENU, extra);
tab.addBookmark();
item.setTitle(resolveBookmarkTitleID(true));
item.setIcon(resolveBookmarkIconDrawable(true, resolveMenuIconTint(isPrivate)));
}
}
return true;
}
if (itemId == R.id.share) {
tab = Tabs.getInstance().getSelectedTab();
if (tab != null) {
String url = tab.getURL();
if (url != null) {
url = ReaderModeUtils.stripAboutReaderUrl(url);
// Context: Sharing via chrome list (no explicit session is active)
Telemetry.sendUIEvent(TelemetryContract.Event.SHARE, TelemetryContract.Method.LIST, "menu");
IntentHelper.openUriExternal(url, "text/plain", "", "", Intent.ACTION_SEND, tab.getDisplayTitle(), false);
}
}
return true;
}
if (itemId == R.id.reload) {
tab = Tabs.getInstance().getSelectedTab();
if (tab != null)
tab.doReload(false);
return true;
}
if (itemId == R.id.back) {
tab = Tabs.getInstance().getSelectedTab();
if (tab != null)
tab.doBack();
return true;
}
if (itemId == R.id.forward) {
tab = Tabs.getInstance().getSelectedTab();
if (tab != null)
tab.doForward();
return true;
}
if (itemId == R.id.bookmarks_list) {
final String url = AboutPages.getURLForBuiltinPanelType(PanelType.BOOKMARKS);
Tabs.getInstance().loadUrl(url);
return true;
}
if (itemId == R.id.history_list) {
final String url = AboutPages.getURLForBuiltinPanelType(PanelType.COMBINED_HISTORY);
Tabs.getInstance().loadUrl(url);
return true;
}
if (itemId == R.id.save_as_pdf) {
Telemetry.sendUIEvent(TelemetryContract.Event.SAVE, TelemetryContract.Method.MENU, "pdf");
EventDispatcher.getInstance().dispatch("SaveAs:PDF", null);
return true;
}
if (itemId == R.id.print) {
Telemetry.sendUIEvent(TelemetryContract.Event.SAVE, TelemetryContract.Method.MENU, "print");
PrintHelper.printPDF(this);
return true;
}
if (itemId == R.id.view_page_source) {
tab = Tabs.getInstance().getSelectedTab();
final GeckoBundle args = new GeckoBundle(1);
args.putInt("tabId", tab.getId());
getAppEventDispatcher().dispatch("Tab:ViewSource", args);
}
if (itemId == R.id.settings) {
intent = new Intent(this, GeckoPreferences.class);
// We want to know when the Settings activity returns, because
// we might need to redisplay based on a locale change.
startActivityForResult(intent, ACTIVITY_REQUEST_PREFERENCES);
return true;
}
if (itemId == R.id.help) {
final String VERSION = AppConstants.MOZ_APP_VERSION;
final String OS = AppConstants.OS_TARGET;
final String LOCALE = Locales.getLanguageTag(Locale.getDefault());
final String URL = getResources().getString(R.string.help_link, VERSION, OS, LOCALE);
Tabs.getInstance().loadUrlInTab(URL);
return true;
}
if (itemId == R.id.addons || itemId == R.id.addons_top_level) {
Tabs.getInstance().loadUrlInTab(AboutPages.ADDONS);
return true;
}
if (itemId == R.id.logins) {
Tabs.getInstance().loadUrlInTab(AboutPages.LOGINS);
return true;
}
if (itemId == R.id.downloads) {
Tabs.getInstance().loadUrlInTab(AboutPages.DOWNLOADS);
return true;
}
if (itemId == R.id.char_encoding) {
EventDispatcher.getInstance().dispatch("CharEncoding:Get", null);
return true;
}
if (itemId == R.id.find_in_page) {
mFindInPageBar.show(mBrowserToolbar.isPrivateMode());
return true;
}
if (itemId == R.id.desktop_mode) {
Tab selectedTab = Tabs.getInstance().getSelectedTab();
if (selectedTab == null)
return true;
final GeckoBundle args = new GeckoBundle(2);
args.putBoolean("desktopMode", !item.isChecked());
args.putInt("tabId", selectedTab.getId());
EventDispatcher.getInstance().dispatch("DesktopMode:Change", args);
return true;
}
if (itemId == R.id.new_tab) {
addTab();
return true;
}
if (itemId == R.id.new_private_tab) {
addPrivateTab();
return true;
}
if (itemId == R.id.new_guest_session) {
showGuestModeDialog(GuestModeDialog.ENTERING);
return true;
}
if (itemId == R.id.exit_guest_session) {
showGuestModeDialog(GuestModeDialog.LEAVING);
return true;
}
// We have a few menu items that can also be in the context menu. If
// we have not already handled the item, give the context menu handler
// a chance.
if (onContextItemSelected(item)) {
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
public boolean onMenuItemLongClick(MenuItem item) {
if (item.getItemId() == R.id.reload) {
Tab tab = Tabs.getInstance().getSelectedTab();
if (tab != null) {
tab.doReload(true);
Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.MENU, "reload_force");
}
return true;
}
return super.onMenuItemLongClick(item);
}
public void showGuestModeDialog(final GuestModeDialog type) {
if ((type == GuestModeDialog.ENTERING) == getProfile().inGuestMode()) {
// Don't show enter dialog if we are already in guest mode; same with leaving.
return;
}
final Prompt ps = new Prompt(this, new Prompt.PromptCallback() {
@Override
public void onPromptFinished(final GeckoBundle result) {
final int itemId = result.getInt("button", -1);
if (itemId != 0) {
return;
}
final Context context = GeckoAppShell.getApplicationContext();
if (type == GuestModeDialog.ENTERING) {
GeckoProfile.enterGuestMode(context);
} else {
GeckoProfile.leaveGuestMode(context);
// Now's a good time to make sure we're not displaying the
// Guest Browsing notification.
GuestSession.hideNotification(context);
}
finishAndShutdown(/* restart */ true);
}
});
Resources res = getResources();
ps.setButtons(new String[] {
res.getString(R.string.guest_session_dialog_continue),
res.getString(R.string.guest_session_dialog_cancel)
});
int titleString = 0;
int msgString = 0;
if (type == GuestModeDialog.ENTERING) {
titleString = R.string.new_guest_session_title;
msgString = R.string.new_guest_session_text;
} else {
titleString = R.string.exit_guest_session_title;
msgString = R.string.exit_guest_session_text;
}
ps.show(res.getString(titleString), res.getString(msgString), null, ListView.CHOICE_MODE_NONE);
}
/**
* Handle a long press on the back button
*/
private boolean handleBackLongPress() {
// If the tab search history is already shown, do nothing.
TabHistoryFragment frag = (TabHistoryFragment) getSupportFragmentManager().findFragmentByTag(TAB_HISTORY_FRAGMENT_TAG);
if (frag != null) {
return true;
}
Tab tab = Tabs.getInstance().getSelectedTab();
if (tab != null && !tab.isEditing()) {
return tabHistoryController.showTabHistory(tab, TabHistoryController.HistoryAction.ALL);
}
return false;
}
/**
* This will detect if the key pressed is back. If so, will show the history.
*/
@Override
public boolean onKeyLongPress(int keyCode, KeyEvent event) {
// onKeyLongPress is broken in Android N, see onKeyDown() for more information. We add a version
// check here to match our fallback code in order to avoid handling a long press twice (which
// could happen if newer versions of android and/or other vendors were to fix this problem).
if (Versions.preN &&
keyCode == KeyEvent.KEYCODE_BACK) {
if (handleBackLongPress()) {
return true;
}
}
return super.onKeyLongPress(keyCode, event);
}
/*
* If the app has been launched a certain number of times, and we haven't asked for feedback before,
* open a new tab with about:feedback when launching the app from the icon shortcut.
*/
@Override
protected void onNewIntent(Intent externalIntent) {
final SafeIntent intent = new SafeIntent(externalIntent);
String action = intent.getAction();
final boolean isViewAction = Intent.ACTION_VIEW.equals(action);
final boolean isBookmarkAction = GeckoApp.ACTION_HOMESCREEN_SHORTCUT.equals(action);
final boolean isTabQueueAction = TabQueueHelper.LOAD_URLS_ACTION.equals(action);
final boolean isViewMultipleAction = ACTION_VIEW_MULTIPLE.equals(action);
if (mInitialized && (isViewAction || isBookmarkAction)) {
// Dismiss editing mode if the user is loading a URL from an external app.
mBrowserToolbar.cancelEdit();
// Hide firstrun-pane if the user is loading a URL from an external app.
hideFirstrunPager(TelemetryContract.Method.NONE);
if (isBookmarkAction) {
// GeckoApp.ACTION_HOMESCREEN_SHORTCUT means we're opening a bookmark that
// was added to Android's homescreen.
Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.HOMESCREEN);
}
}
showTabQueuePromptIfApplicable(intent);
// GeckoApp will wrap this unsafe external intent in a SafeIntent.
super.onNewIntent(externalIntent);
if (AppConstants.MOZ_ANDROID_BEAM && NfcAdapter.ACTION_NDEF_DISCOVERED.equals(action)) {
final GeckoBundle data = new GeckoBundle(2);
data.putString("uri", intent.getDataString());
data.putString("flags", "OPEN_NEWTAB");
getAppEventDispatcher().dispatch("Tab:OpenUri", data);
}
// Only solicit feedback when the app has been launched from the icon shortcut.
if (GuestSession.NOTIFICATION_INTENT.equals(action)) {
GuestSession.onNotificationIntentReceived(this);
}
// If the user has clicked the tab queue notification then load the tabs.
if (TabQueueHelper.TAB_QUEUE_ENABLED && mInitialized && isTabQueueAction) {
Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.NOTIFICATION, "tabqueue");
ThreadUtils.postToBackgroundThread(new Runnable() {
@Override
public void run() {
openQueuedTabs();
}
});
}
// Custom intent action for opening multiple URLs at once
if (isViewMultipleAction) {
openMultipleTabsFromIntent(intent);
}
for (final BrowserAppDelegate delegate : delegates) {
delegate.onNewIntent(this, intent);
}
if (!mInitialized || !Intent.ACTION_MAIN.equals(action)) {
return;
}
// Check to see how many times the app has been launched.
final String keyName = getPackageName() + ".feedback_launch_count";
final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads();
// Faster on main thread with an async apply().
try {
SharedPreferences settings = getPreferences(Activity.MODE_PRIVATE);
int launchCount = settings.getInt(keyName, 0);
if (launchCount < FEEDBACK_LAUNCH_COUNT) {
// Increment the launch count and store the new value.
launchCount++;
settings.edit().putInt(keyName, launchCount).apply();
// If we've reached our magic number, show the feedback page.
if (launchCount == FEEDBACK_LAUNCH_COUNT) {
EventDispatcher.getInstance().dispatch("Feedback:Show", null);
}
}
} finally {
StrictMode.setThreadPolicy(savedPolicy);
}
}
public void openUrls(List<String> urls) {
final GeckoBundle data = new GeckoBundle(1);
data.putStringArray("urls", urls.toArray(new String[urls.size()]));
EventDispatcher.getInstance().dispatch("Tabs:OpenMultiple", data);
}
private void showTabQueuePromptIfApplicable(final SafeIntent intent) {
ThreadUtils.postToBackgroundThread(new Runnable() {
@Override
public void run() {
// We only want to show the prompt if the browser has been opened from an external url
if (TabQueueHelper.TAB_QUEUE_ENABLED && mInitialized
&& Intent.ACTION_VIEW.equals(intent.getAction())
&& !intent.getBooleanExtra(BrowserContract.SKIP_TAB_QUEUE_FLAG, false)
&& TabQueueHelper.shouldShowTabQueuePrompt(BrowserApp.this)) {
Intent promptIntent = new Intent(BrowserApp.this, TabQueuePrompt.class);
startActivityForResult(promptIntent, ACTIVITY_REQUEST_TAB_QUEUE);
}
}
});
}
// HomePager.OnUrlOpenListener
@Override
public void onUrlOpen(String url, EnumSet<OnUrlOpenListener.Flags> flags) {
onUrlOpenWithReferrer(url, null, flags);
}
@Override
public void onUrlOpenWithReferrer(final String url, @Nullable final String referrerUri,
final EnumSet<OnUrlOpenListener.Flags> flags) {
if (flags.contains(OnUrlOpenListener.Flags.OPEN_WITH_INTENT)) {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse(url));
startActivity(intent);
} else {
// By default this listener is used for lists where the offline reader-view icon
// is shown - hence we need to redirect to the reader-view page by default.
// However there are some cases where we might not want to use this, e.g.
// for topsites where we do not indicate that a page is an offline reader-view bookmark too.
final String pageURL;
if (!flags.contains(OnUrlOpenListener.Flags.NO_READER_VIEW)) {
pageURL = SavedReaderViewHelper.getReaderURLIfCached(this, url);
} else {
pageURL = url;
}
if (!maybeSwitchToTab(pageURL, flags)) {
openUrlAndStopEditingWithReferrer(pageURL, referrerUri);
clearSelectedTabApplicationId();
}
}
}
// HomePager.OnUrlOpenInBackgroundListener
@Override
public void onUrlOpenInBackground(final String url, EnumSet<OnUrlOpenInBackgroundListener.Flags> flags) {
onUrlOpenInBackgroundWithReferrer(url, null, flags);
}
@Override
public void onUrlOpenInBackgroundWithReferrer(final String url, @Nullable final String referrerUri,
final EnumSet<OnUrlOpenInBackgroundListener.Flags> flags) {
if (url == null) {
throw new IllegalArgumentException("url must not be null");
}
if (flags == null) {
throw new IllegalArgumentException("flags must not be null");
}
// We only use onUrlOpenInBackgroundListener for the homepanel context menus, hence
// we should always be checking whether we want the readermode version
final String pageURL = SavedReaderViewHelper.getReaderURLIfCached(this, url);
final boolean isPrivate = flags.contains(OnUrlOpenInBackgroundListener.Flags.PRIVATE);
int loadFlags = Tabs.LOADURL_NEW_TAB | Tabs.LOADURL_BACKGROUND;
if (isPrivate) {
loadFlags |= Tabs.LOADURL_PRIVATE;
}
final Tab newTab = Tabs.getInstance().loadUrl(pageURL, null, referrerUri, Tabs.INVALID_TAB_ID, null, loadFlags);
// We switch to the desired tab by unique ID, which closes any window
// for a race between opening the tab and closing it, and switching to
// it. We could also switch to the Tab explicitly, but we don't want to
// hold a reference to the Tab itself in the anonymous listener class.
final int newTabId = newTab.getId();
final SnackbarBuilder.SnackbarCallback callback = new SnackbarBuilder.SnackbarCallback() {
@Override
public void onClick(View v) {
Telemetry.sendUIEvent(TelemetryContract.Event.SHOW, TelemetryContract.Method.TOAST, "switchtab");
maybeSwitchToTab(newTabId);
}
};
final String message = isPrivate ?
getResources().getString(R.string.new_private_tab_opened) :
getResources().getString(R.string.new_tab_opened);
final String buttonMessage = getResources().getString(R.string.switch_button_message);
SnackbarBuilder.builder(this)
.message(message)
.duration(Snackbar.LENGTH_LONG)
.action(buttonMessage)
.callback(callback)
.buildAndShow();
}
// BrowserSearch.OnSearchListener
@Override
public void onSearch(SearchEngine engine, final String text, final TelemetryContract.Method method) {
// Don't store searches that happen in private tabs. This assumes the user can only
// perform a search inside the currently selected tab, which is true for searches
// that come from SearchEngineRow.
if (!mBrowserToolbar.isPrivateMode()) {
storeSearchQuery(text);
}
// We don't use SearchEngine.getEngineIdentifier because it can
// return a custom search engine name, which is a privacy concern.
final String identifierToRecord = (engine.identifier != null) ? engine.identifier : "other";
recordSearch(GeckoSharedPrefs.forProfile(this), identifierToRecord, method);
openUrlAndStopEditing(text, engine.name);
}
// BrowserSearch.OnEditSuggestionListener
@Override
public void onEditSuggestion(String suggestion) {
mBrowserToolbar.onEditSuggestion(suggestion);
}
@Override
public int getLayout() { return R.layout.gecko_app; }
@Override
public View getDoorhangerOverlay() {
return doorhangerOverlay;
}
public SearchEngineManager getSearchEngineManager() {
return mSearchEngineManager;
}
// For use from tests only.
@RobocopTarget
public ReadingListHelper getReadingListHelper() {
return mReadingListHelper;
}
@Override
protected ActionModePresenter getTextSelectPresenter() {
return this;
}
/* Implementing ActionModeCompat.Presenter */
@Override
public void startActionMode(final ActionModeCompat.Callback callback) {
// If actionMode is null, we're not currently showing one. Flip to the action mode view
if (mActionMode == null) {
mActionBarFlipper.showNext();
DynamicToolbarAnimator toolbar = mLayerView.getDynamicToolbarAnimator();
// If the toolbar is dynamic and not currently showing, just show the real toolbar
// and keep the animated snapshot hidden
if (mDynamicToolbar.isEnabled() && !isToolbarChromeVisible()) {
toggleToolbarChrome(true);
mShowingToolbarChromeForActionBar = true;
}
mDynamicToolbar.setPinned(true, PinReason.ACTION_MODE);
} else {
// Otherwise, we're already showing an action mode. Just finish it and show the new one
mActionMode.finish();
}
mActionMode = new ActionModeCompat(BrowserApp.this, callback, mActionBar);
if (callback.onCreateActionMode(mActionMode, mActionMode.getMenu())) {
mActionMode.invalidate();
}
mActionMode.animateIn();
}
/* Implementing ActionModeCompat.Presenter */
@Override
public void endActionMode() {
if (mActionMode == null) {
return;
}
mActionMode.finish();
mActionMode = null;
mDynamicToolbar.setPinned(false, PinReason.ACTION_MODE);
mActionBarFlipper.showPrevious();
// Hide the real toolbar chrome if it was hidden before the action bar
// was shown.
if (mShowingToolbarChromeForActionBar) {
toggleToolbarChrome(false);
mShowingToolbarChromeForActionBar = false;
}
}
public static interface TabStripInterface {
public void refresh();
/** Called to let the tab strip know it is now, or is now no longer, being hidden by
* something being drawn over it.
*/
void tabStripIsCovered(boolean covered);
void setOnTabChangedListener(OnTabAddedOrRemovedListener listener);
interface OnTabAddedOrRemovedListener {
void onTabChanged();
}
}
@Override
protected void recordStartupActionTelemetry(final String passedURL, final String action) {
final TelemetryContract.Method method;
if (ACTION_HOMESCREEN_SHORTCUT.equals(action)) {
// This action is also recorded via "loadurl.1" > "homescreen".
method = TelemetryContract.Method.HOMESCREEN;
} else if (passedURL == null) {
Telemetry.sendUIEvent(TelemetryContract.Event.LAUNCH, TelemetryContract.Method.HOMESCREEN, "launcher");
method = TelemetryContract.Method.HOMESCREEN;
} else {
// This is action is also recorded via "loadurl.1" > "intent".
method = TelemetryContract.Method.INTENT;
}
if (GeckoProfile.get(this).inGuestMode()) {
Telemetry.sendUIEvent(TelemetryContract.Event.LAUNCH, method, "guest");
} else if (Restrictions.isRestrictedProfile(this)) {
Telemetry.sendUIEvent(TelemetryContract.Event.LAUNCH, method, "restricted");
}
}
/**
* Launch edit bookmark dialog. The {@link BookmarkEditFragment} needs to be started by an activity
* that implements the interface({@link BookmarkEditFragment.Callbacks}) for handling callback method.
*/
public void showEditBookmarkDialog(String pageUrl) {
if (BookmarkUtils.isEnabled(this)) {
BookmarkEditFragment dialog = BookmarkEditFragment.newInstance(pageUrl);
dialog.show(getSupportFragmentManager(), "edit-bookmark");
} else {
new EditBookmarkDialog(this).show(pageUrl);
}
}
@Override
public void onEditBookmark(@NonNull Bundle bundle) {
new EditBookmarkTask(this, bundle).execute();
}
@Override
public void onLightweightThemeChanged() {
refreshStatusBarColor();
}
@Override
public void onLightweightThemeReset() {
refreshStatusBarColor();
}
private void refreshStatusBarColor() {
final boolean isPrivate = mBrowserToolbar.isPrivateMode();
WindowUtil.setStatusBarColor(BrowserApp.this, isPrivate);
}
}