fune/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
Jan Henning 24822c48c7 Bug 1414084 - Part 9 - Move add-on menu item cache out of BrowserApp. r=Grisha
Bug 832990 solved the issue of us losing the menu item cache if BrowserApp was
destroyed, however the issue remains that we'll miss any Menu:... messages that
are sent while BrowserApp doesn't exist, e.g. if Gecko is initially loaded
through a GeckoView-based activity.

Therefore we now move the menu item cache and the listener for those messages
into a separate class, whose lifetime better matches that of Gecko.

Apart from any necessary changes, we move the existing code as is. The only
additional change is that we make addAddonMenuItemToMenu() static, because we
can.

MozReview-Commit-ID: BJleonLnjmo

--HG--
extra : rebase_source : e36d954488cc44d250948edcbb8a1964e24ddab7
2018-02-25 22:22:37 +01:00

4159 lines
164 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.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.ActivityInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.res.Configuration;
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.StrictMode;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.StringRes;
import android.support.design.widget.Snackbar;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
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.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.DlcStudyService;
import org.mozilla.gecko.dlc.DlcSyncService;
import org.mozilla.gecko.extensions.ExtensionPermissionsHelper;
import org.mozilla.gecko.firstrun.OnboardingHelper;
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.PictureInPictureController;
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.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 org.mozilla.geckoview.GeckoSession;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.URLEncoder;
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,
OnboardingHelper.OnboardingListener {
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 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";
private BrowserSearch mBrowserSearch;
private View mBrowserSearchContainer;
public ViewGroup mBrowserChrome;
public ViewFlipper mActionBarFlipper;
public ActionModeCompatView mActionBar;
private VideoPlayer mVideoPlayer;
private PictureInPictureController mPipController;
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 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;
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 SafeIntent safeStartingIntent;
private Intent startingIntentAfterPip;
private boolean isInAutomation;
// The types of guest mode dialogs we show.
public static enum GuestModeDialog {
ENTERING,
LEAVING
}
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();
private boolean mSuppressNextKeyUp;
// 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 OnboardingHelper mOnboardingHelper; // 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
@SuppressWarnings("fallthrough")
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) {
if (mSuppressNextKeyUp && event.getAction() == KeyEvent.ACTION_UP) {
mSuppressNextKeyUp = false;
return true;
}
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());
return true;
case KeyEvent.KEYCODE_PERIOD:
tab.doStop();
return true;
case KeyEvent.KEYCODE_T:
int flags = Tabs.LOADURL_START_EDITING;
if (tab.isPrivate()) {
flags |= Tabs.LOADURL_PRIVATE;
}
addTab(flags);
return true;
case KeyEvent.KEYCODE_N:
addTab(Tabs.LOADURL_START_EDITING);
return true;
case KeyEvent.KEYCODE_P:
if (event.isShiftPressed()) {
addTab(Tabs.LOADURL_PRIVATE | Tabs.LOADURL_START_EDITING);
return true;
}
break;
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;
safeStartingIntent = new SafeIntent(getIntent());
isInAutomation = IntentUtils.getIsInAutomationFromEnvironment(safeStartingIntent);
GeckoProfile.setIntentArgs(safeStartingIntent.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.
DlcStudyService.enqueueServiceWork(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);
if (mIsAbortingAppLaunch) {
return;
}
mOnboardingHelper = new OnboardingHelper(this, safeStartingIntent);
initSwitchboardAndMma(this, safeStartingIntent, 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;
}
});
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, final boolean isPrivate) {
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, isPrivate);
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 = safeStartingIntent.getAction();
if (Intent.ACTION_VIEW.equals(action)) {
// Show the target URL immediately in the toolbar.
mBrowserToolbar.setTitle(safeStartingIntent.getDataString());
showTabQueuePromptIfApplicable(safeStartingIntent);
} 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(safeStartingIntent);
} 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();
mPipController = new PictureInPictureController(this);
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,
"GeckoView:AccessibilityEnabled",
"Menu:Open",
"LightweightTheme:Update",
"Tab:Added",
"Video:Play",
"CharEncoding:Data",
"CharEncoding:State",
"Settings:Show",
"Updater:Launch",
"Sanitize:Finished",
"Sanitize:OpenTabs",
"NotificationSettings:FeatureTipsStatusUpdated",
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 initSwitchboardAndMma(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;
final SwitchBoard.ConfigStatusListener configStatuslistener = mOnboardingHelper;
final MmaDelegate.MmaVariablesChangedListener variablesChangedListener = mOnboardingHelper;
new AsyncConfigLoader(context, serverUrl, configStatuslistener) {
@Override
protected Void doInBackground(Void... params) {
super.doInBackground(params);
SwitchBoard.loadConfig(context, serverUrl, configStatuslistener);
if (GeckoPreferences.isMmaAvailableAndEnabled(context)) {
// Do LeanPlum start/init here
MmaDelegate.init(BrowserApp.this, variablesChangedListener);
}
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 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).
mOnboardingHelper.checkFirstRun();
}
}
@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
protected void onUserLeaveHint() {
super.onUserLeaveHint();
try {
mPipController.tryEnteringPictureInPictureMode();
} catch (IllegalStateException exception) {
Log.e(LOGTAG, "Cannot enter in Picture In Picture mode:\n" + exception.getMessage());
}
}
@Override
public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode, Configuration newConfig) {
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig);
if (!isInPictureInPictureMode) {
mPipController.cleanResources();
// User clicked a new link to be opened in Firefox.
// We returned from Picture-in-picture mode and now must try to open that link.
if (startingIntentAfterPip != null) {
getApplication().startActivity(startingIntentAfterPip);
startingIntentAfterPip = null;
} else {
// After returning from Picture-in-picture mode the video will still be playing
// in fullscreen. But now we have the status bar showing.
// Call setFullscreen(..) to hide it and offer the same fullscreen video experience
// that the user had before entering in Picture-in-picture mode.
ActivityUtils.setFullScreen(this, true);
}
}
}
@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);
}
MmaDelegate.track(MmaDelegate.RESUMED_FROM_BACKGROUND);
MmaDelegate.notifyDefaultBrowserStatus(this);
}
@Override
public void onStop() {
super.onStop();
if (mIsAbortingAppLaunch) {
return;
}
if (mPipController.isInPipMode()) {
// If screen is locked we should exit PictureInPicture mode
moveTaskToBack(true);
mPipController.cleanResources();
}
// 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 onCommitByKey() {
if (commitEditingMode()) {
// We're committing in response to a key-down event. Since we'll be hiding the
// ToolbarEditLayout, the corresponding key-up event will end up being sent to
// Gecko which we don't want, as this messes up tracking of the last user input.
mSuppressNextKeyUp = true;
}
}
});
mBrowserToolbar.setOnDismissListener(new BrowserToolbar.OnDismissListener() {
@Override
public void onDismiss() {
mBrowserToolbar.cancelEdit();
}
});
// Website suggestions for address bar inputs should not be enabled when running in automation.
// After the upgrade to support library v.26 it could fail otherwise unrelated Robocop tests
// See https://bugzilla.mozilla.org/show_bug.cgi?id=1385464#c3
if (!isInAutomation) {
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);
AddonUICache.getInstance().onDestroyOptionsMenu();
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,
"GeckoView:AccessibilityEnabled",
"Menu:Open",
"LightweightTheme:Update",
"Tab:Added",
"Video:Play",
"CharEncoding:Data",
"CharEncoding:State",
"Settings:Show",
"Updater:Launch",
"Sanitize:Finished",
"Sanitize:OpenTabs",
"NotificationSettings:FeatureTipsStatusUpdated",
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();
MmaDelegate.flushResources(this);
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)
DlcSyncService.enqueueServiceWork(this);
}
break;
case "GeckoView:AccessibilityEnabled":
mDynamicToolbar.setAccessibilityEnabled(message.getBoolean("enabled"));
break;
case "Menu:Open":
if (mBrowserToolbar.isEditing()) {
mBrowserToolbar.cancelEdit();
}
openOptionsMenu();
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;
case "NotificationSettings:FeatureTipsStatusUpdated":
if (message.getBoolean("isMmaEnabled")) {
initSwitchboardAndMma(this, safeStartingIntent, isInAutomation);
} else {
MmaDelegate.stop();
}
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(final int flags) {
if ((flags & Tabs.LOADURL_PRIVATE) == 0) {
MmaDelegate.track(NEW_TAB);
}
Tabs.getInstance().addTab(flags);
}
@Override
public void addTab() {
addTab(Tabs.LOADURL_NONE);
}
@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());
}
/**
* 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 SplashScreen getSplashScreen() {
final ViewGroup main = (ViewGroup) findViewById(R.id.gecko_layout);
final View splashLayout = LayoutInflater.from(this).inflate(R.layout.splash_screen, main);
return (SplashScreen) splashLayout.findViewById(R.id.splash_root);
}
/**
* 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);
}
/**
* @return True if editing mode was successfully committed.
*/
private boolean commitEditingMode() {
if (!mBrowserToolbar.isEditing()) {
return false;
}
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();
return true;
}
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, or if there is no search query
// within the keywordURL, yet one is provided, load the url.
// This may result in a query using the default search engine.
if (TextUtils.isEmpty(keywordUrl) ||
(!TextUtils.isEmpty(keywordSearch) && !StringUtils.queryExists(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.
// If we are not waiting for the onboarding screens we should hide it now.
if (!mOnboardingHelper.isPreparing() &&
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()) {
if (splashScreen == null) {
splashScreen = getSplashScreen();
}
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);
mBrowserToolbar.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 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 (!mOnboardingHelper.hideOnboarding()) {
return false;
}
Telemetry.sendUIEvent(TelemetryContract.Event.CANCEL, method, "firstrun-pane");
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;
}
}
@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);
// Let the AddonUICache handle adding (and removing again) any add-on/browser action
// menu items as required.
AddonUICache.getInstance().onCreateOptionsMenu(mMenu);
// 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.ContentDelegate
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) {
// Currently there is no way to exit PictureInPicture mode programmatically
// https://issuetracker.google.com/issues/37254459
// but because we are "singleTask" we will receive the Intents to open a new link.
// When this happens, the new Intent will trigger `onPictureInPictureModeChanged(..)`
//
// Whenever the user presses a new link to be opened in Firefox we must
// cache the received Intent, wait for PictureInPicture mode to end and then act on that Intent.
if (mPipController.isInPipMode()) {
startingIntentAfterPip = externalIntent;
// Pattern used to exit MultiWindow - https://stackoverflow.com/a/43288507/4249825
// also works for us.
// Without this the old tab would continue playing media.
moveTaskToBack(true);
startingIntentAfterPip.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
// To enter PictureInPicture mode the video must be playing in fullscreen, which also
// means the orientation will be changed to Landscape.
// If when pressing the new link the device is actually in Portrait we will force
// the activity to enter in Portrait before opening the new tab.
setRequestedOrientationForCurrentActivity(ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR);
return;
}
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);
}
@Override
public void onOnboardingProcessStarted() {
if (splashScreen == null) {
splashScreen = getSplashScreen();
}
splashScreen.show(OnboardingHelper.DELAY_SHOW_DEFAULT_ONBOARDING);
}
@Override
public void onOnboardingScreensVisible() {
mHomeScreenContainer.setVisibility(View.VISIBLE);
if (HardwareUtils.isTablet()) {
mTabStrip.setOnTabChangedListener(new BrowserApp.TabStripInterface.OnTabAddedOrRemovedListener() {
@Override
public void onTabChanged() {
hideFirstrunPager(TelemetryContract.Method.BUTTON);
mTabStrip.setOnTabChangedListener(null);
}
});
}
}
@Override
public void onFinishedOnboarding(final boolean showBrowserHint) {
if (showBrowserHint && !Tabs.hasHomepage(this)) {
enterEditingMode();
}
}
}