mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
e0579b7ab8
Refactor the dynamic toolbar code so that the ownership of various properties is clearer, and the page is offset by the toolbar instead of being overlapped. This fixes problems with the scroll origin of the page not corresponding to the visible origin on the screen.
1636 lines
60 KiB
Java
1636 lines
60 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 org.mozilla.gecko.db.BrowserContract.Combined;
|
|
import org.mozilla.gecko.db.BrowserDB;
|
|
import org.mozilla.gecko.gfx.BitmapUtils;
|
|
import org.mozilla.gecko.gfx.GeckoLayerClient;
|
|
import org.mozilla.gecko.gfx.ImmutableViewportMetrics;
|
|
import org.mozilla.gecko.gfx.LayerView;
|
|
import org.mozilla.gecko.gfx.PanZoomController;
|
|
import org.mozilla.gecko.util.FloatUtils;
|
|
import org.mozilla.gecko.util.GamepadUtils;
|
|
import org.mozilla.gecko.util.HardwareUtils;
|
|
import org.mozilla.gecko.util.ThreadUtils;
|
|
import org.mozilla.gecko.util.UiAsyncTask;
|
|
import org.mozilla.gecko.widget.AboutHome;
|
|
|
|
import org.json.JSONArray;
|
|
import org.json.JSONException;
|
|
import org.json.JSONObject;
|
|
|
|
import android.app.Activity;
|
|
import android.app.AlertDialog;
|
|
import android.content.DialogInterface;
|
|
import android.content.Intent;
|
|
import android.content.SharedPreferences;
|
|
import android.content.res.Configuration;
|
|
import android.database.Cursor;
|
|
import android.graphics.Bitmap;
|
|
import android.graphics.PointF;
|
|
import android.graphics.Rect;
|
|
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.util.Log;
|
|
import android.view.InputDevice;
|
|
import android.view.KeyEvent;
|
|
import android.view.LayoutInflater;
|
|
import android.view.Menu;
|
|
import android.view.MenuInflater;
|
|
import android.view.MenuItem;
|
|
import android.view.MotionEvent;
|
|
import android.view.SubMenu;
|
|
import android.view.View;
|
|
import android.view.ViewGroup;
|
|
import android.view.animation.Interpolator;
|
|
import android.widget.LinearLayout;
|
|
import android.widget.RelativeLayout;
|
|
import android.widget.Toast;
|
|
|
|
import java.io.InputStream;
|
|
import java.net.URL;
|
|
import java.util.EnumSet;
|
|
import java.util.Vector;
|
|
|
|
abstract public class BrowserApp extends GeckoApp
|
|
implements TabsPanel.TabsLayoutChangeListener,
|
|
PropertyAnimator.PropertyAnimationListener,
|
|
View.OnKeyListener,
|
|
GeckoLayerClient.OnMetricsChangedListener,
|
|
AboutHome.UriLoadListener,
|
|
AboutHome.LoadCompleteListener {
|
|
private static final String LOGTAG = "GeckoBrowserApp";
|
|
|
|
private static final String PREF_CHROME_DYNAMICTOOLBAR = "browser.chrome.dynamictoolbar";
|
|
|
|
private static final int TABS_ANIMATION_DURATION = 450;
|
|
|
|
private static final int READER_ADD_SUCCESS = 0;
|
|
private static final int READER_ADD_FAILED = 1;
|
|
private static final int READER_ADD_DUPLICATE = 2;
|
|
|
|
public static BrowserToolbar mBrowserToolbar;
|
|
private AboutHome mAboutHome;
|
|
protected Telemetry.Timer mAboutHomeStartupTimer = null;
|
|
|
|
private static final int ADDON_MENU_OFFSET = 1000;
|
|
private class MenuItemInfo {
|
|
public int id;
|
|
public String label;
|
|
public String icon;
|
|
public boolean checkable;
|
|
public boolean checked;
|
|
public boolean enabled;
|
|
public boolean visible;
|
|
public int parent;
|
|
}
|
|
|
|
private Vector<MenuItemInfo> mAddonMenuItemsCache;
|
|
|
|
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 boolean mAccessibilityEnabled = false;
|
|
|
|
// We'll ask for feedback after the user launches the app this many times.
|
|
private static final int FEEDBACK_LAUNCH_COUNT = 15;
|
|
|
|
// Whether the dynamic toolbar pref is enabled.
|
|
private boolean mDynamicToolbarEnabled = false;
|
|
|
|
// Stored value of the toolbar height, so we know when it's changed.
|
|
private int mToolbarHeight = 0;
|
|
|
|
private Integer mPrefObserverId;
|
|
|
|
@Override
|
|
public void onTabChanged(Tab tab, Tabs.TabEvents msg, Object data) {
|
|
switch(msg) {
|
|
case LOCATION_CHANGE:
|
|
if (Tabs.getInstance().isSelectedTab(tab)) {
|
|
maybeCancelFaviconLoad(tab);
|
|
}
|
|
// fall through
|
|
case SELECTED:
|
|
if (Tabs.getInstance().isSelectedTab(tab)) {
|
|
if ("about:home".equals(tab.getURL())) {
|
|
showAboutHome();
|
|
|
|
if (isDynamicToolbarEnabled()) {
|
|
// Show the toolbar.
|
|
mLayerView.getLayerMarginsAnimator().showMargins(false);
|
|
}
|
|
} else {
|
|
hideAboutHome();
|
|
}
|
|
|
|
// Dismiss any SiteIdentity Popup
|
|
SiteIdentityPopup.getInstance().dismiss();
|
|
|
|
final TabsPanel.Panel panel = tab.isPrivate()
|
|
? TabsPanel.Panel.PRIVATE_TABS
|
|
: TabsPanel.Panel.NORMAL_TABS;
|
|
// Delay calling showTabs so that it does not modify the mTabsChangedListeners
|
|
// array while we are still iterating through the array.
|
|
ThreadUtils.postToUiThread(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
if (areTabsShown() && mTabsPanel.getCurrentPanel() != panel)
|
|
showTabs(panel);
|
|
}
|
|
});
|
|
}
|
|
break;
|
|
case START:
|
|
if (Tabs.getInstance().isSelectedTab(tab)) {
|
|
invalidateOptionsMenu();
|
|
|
|
if (isDynamicToolbarEnabled()) {
|
|
// Show the toolbar.
|
|
mLayerView.getLayerMarginsAnimator().showMargins(false);
|
|
}
|
|
}
|
|
break;
|
|
case LOAD_ERROR:
|
|
case STOP:
|
|
case MENU_UPDATED:
|
|
if (Tabs.getInstance().isSelectedTab(tab)) {
|
|
invalidateOptionsMenu();
|
|
}
|
|
break;
|
|
case PAGE_SHOW:
|
|
loadFavicon(tab);
|
|
break;
|
|
case LINK_FAVICON:
|
|
// If tab is not loading and the favicon is updated, we
|
|
// want to load the image straight away. If tab is still
|
|
// loading, we only load the favicon once the page's content
|
|
// is fully loaded.
|
|
if (tab.getState() != Tab.STATE_LOADING) {
|
|
loadFavicon(tab);
|
|
}
|
|
break;
|
|
}
|
|
super.onTabChanged(tab, msg, data);
|
|
}
|
|
|
|
@Override
|
|
void handleClearHistory() {
|
|
super.handleClearHistory();
|
|
updateAboutHomeTopSites();
|
|
}
|
|
|
|
@Override
|
|
public boolean onKey(View v, int keyCode, KeyEvent event) {
|
|
// Global onKey handler. This is called if the focused UI doesn't
|
|
// handle the key event, and before Gecko swallows the events.
|
|
if (event.getAction() != KeyEvent.ACTION_DOWN) {
|
|
return false;
|
|
}
|
|
|
|
// Gamepad support only exists in API-level >= 9
|
|
if (Build.VERSION.SDK_INT >= 9 &&
|
|
(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 (mBrowserToolbar.isVisible()) {
|
|
if (isDynamicToolbarEnabled() && !mAboutHome.getUserVisibleHint()) {
|
|
if (mLayerView != null) {
|
|
mLayerView.getLayerMarginsAnimator().hideMargins(false);
|
|
mLayerView.requestFocus();
|
|
}
|
|
} else {
|
|
// Just focus the address bar when about:home is visible
|
|
// or when the dynamic toolbar isn't enabled.
|
|
mBrowserToolbar.requestFocusFromTouch();
|
|
}
|
|
} else {
|
|
if (mLayerView != null) {
|
|
mLayerView.getLayerMarginsAnimator().showMargins(false);
|
|
}
|
|
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 (Build.VERSION.SDK_INT >= 11 && 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();
|
|
return true;
|
|
|
|
case KeyEvent.KEYCODE_PERIOD:
|
|
tab.doStop();
|
|
return true;
|
|
|
|
case KeyEvent.KEYCODE_T:
|
|
addTab();
|
|
return true;
|
|
|
|
case KeyEvent.KEYCODE_W:
|
|
Tabs.getInstance().closeTab(tab);
|
|
return true;
|
|
|
|
case KeyEvent.KEYCODE_F:
|
|
mFindInPageBar.show();
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
@Override
|
|
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
|
if (onKey(null, keyCode, event)) {
|
|
return true;
|
|
}
|
|
|
|
return super.onKeyDown(keyCode, event);
|
|
}
|
|
|
|
void handleReaderAdded(int result, final String title, final String url) {
|
|
if (result != READER_ADD_SUCCESS) {
|
|
if (result == READER_ADD_FAILED) {
|
|
showToast(R.string.reading_list_failed, Toast.LENGTH_SHORT);
|
|
} else if (result == READER_ADD_DUPLICATE) {
|
|
showToast(R.string.reading_list_duplicate, Toast.LENGTH_SHORT);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
ThreadUtils.postToBackgroundThread(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
BrowserDB.addReadingListItem(getContentResolver(), title, url);
|
|
showToast(R.string.reading_list_added, Toast.LENGTH_SHORT);
|
|
}
|
|
});
|
|
}
|
|
|
|
void handleReaderRemoved(final String url) {
|
|
ThreadUtils.postToBackgroundThread(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
BrowserDB.removeReadingListItemWithURL(getContentResolver(), url);
|
|
showToast(R.string.reading_list_removed, Toast.LENGTH_SHORT);
|
|
}
|
|
});
|
|
}
|
|
|
|
@Override
|
|
void onStatePurged() {
|
|
ThreadUtils.postToUiThread(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
if (mAboutHome != null)
|
|
mAboutHome.setLastTabsVisibility(false);
|
|
}
|
|
});
|
|
|
|
super.onStatePurged();
|
|
}
|
|
|
|
@Override
|
|
public void onCreate(Bundle savedInstanceState) {
|
|
mAboutHomeStartupTimer = new Telemetry.Timer("FENNEC_STARTUP_TIME_ABOUTHOME");
|
|
|
|
super.onCreate(savedInstanceState);
|
|
|
|
LinearLayout actionBar = (LinearLayout) getActionBarLayout();
|
|
mMainLayout.addView(actionBar, 2);
|
|
|
|
((GeckoApp.MainLayout) mMainLayout).setTouchEventInterceptor(new HideTabsTouchListener());
|
|
((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 (mAboutHome.getUserVisibleHint()) {
|
|
mLayerView.requestFocus();
|
|
} else {
|
|
mAboutHome.requestFocus();
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
});
|
|
|
|
// AboutHome will be dynamically attached and detached as
|
|
// about:home is shown. Adding/removing the fragment is not synchronous,
|
|
// so we can't use Fragment#isVisible() to determine whether the
|
|
// about:home is shown. Instead, we use Fragment#getUserVisibleHint()
|
|
// with the hint we set ourselves.
|
|
mAboutHome = AboutHome.newInstance();
|
|
mAboutHome.setUserVisibleHint(false);
|
|
|
|
mBrowserToolbar = new BrowserToolbar(this);
|
|
mBrowserToolbar.from(actionBar);
|
|
|
|
// Intercept key events for gamepad shortcuts
|
|
actionBar.setOnKeyListener(this);
|
|
|
|
if (mTabsPanel != null) {
|
|
mTabsPanel.setTabsLayoutChangeListener(this);
|
|
updateSideBarState();
|
|
}
|
|
|
|
mFindInPageBar = (FindInPageBar) findViewById(R.id.find_in_page);
|
|
|
|
registerEventListener("CharEncoding:Data");
|
|
registerEventListener("CharEncoding:State");
|
|
registerEventListener("Feedback:LastUrl");
|
|
registerEventListener("Feedback:OpenPlayStore");
|
|
registerEventListener("Feedback:MaybeLater");
|
|
registerEventListener("Telemetry:Gather");
|
|
|
|
Distribution.init(this, getPackageResourcePath());
|
|
JavaAddonManager.getInstance().init(getApplicationContext());
|
|
|
|
if (AppConstants.MOZ_ANDROID_BEAM && Build.VERSION.SDK_INT >= 14) {
|
|
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);
|
|
}
|
|
}
|
|
|
|
// Listen to the dynamic toolbar pref
|
|
mPrefObserverId = PrefsHelper.getPref(PREF_CHROME_DYNAMICTOOLBAR, new PrefsHelper.PrefHandlerBase() {
|
|
@Override
|
|
public void prefValue(String pref, boolean value) {
|
|
if (value == mDynamicToolbarEnabled) {
|
|
return;
|
|
}
|
|
mDynamicToolbarEnabled = value;
|
|
|
|
ThreadUtils.postToUiThread(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
// If accessibility is enabled, the dynamic toolbar is
|
|
// forced to be off.
|
|
if (!mAccessibilityEnabled) {
|
|
setDynamicToolbarEnabled(mDynamicToolbarEnabled);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
@Override
|
|
public boolean isObserver() {
|
|
// We want to be notified of changes to be able to switch mode
|
|
// without restarting.
|
|
return true;
|
|
}
|
|
});
|
|
}
|
|
|
|
private void setDynamicToolbarEnabled(boolean enabled) {
|
|
if (enabled) {
|
|
if (mLayerView != null) {
|
|
mLayerView.getLayerClient().setOnMetricsChangedListener(this);
|
|
}
|
|
setToolbarMargin(0);
|
|
} else {
|
|
// Immediately show the toolbar when disabling the dynamic
|
|
// toolbar.
|
|
if (mLayerView != null) {
|
|
mLayerView.getLayerClient().setOnMetricsChangedListener(null);
|
|
}
|
|
mAboutHome.setPadding(0, 0, 0, 0);
|
|
if (mBrowserToolbar != null) {
|
|
mBrowserToolbar.getLayout().scrollTo(0, 0);
|
|
}
|
|
}
|
|
|
|
refreshToolbarHeight();
|
|
}
|
|
|
|
private boolean isDynamicToolbarEnabled() {
|
|
return mDynamicToolbarEnabled && !mAccessibilityEnabled;
|
|
}
|
|
|
|
@Override
|
|
public void setAccessibilityEnabled(boolean enabled) {
|
|
if (mAccessibilityEnabled == enabled) {
|
|
return;
|
|
}
|
|
|
|
// Disable the dynamic toolbar when accessibility features are enabled,
|
|
// and re-read the preference when they're disabled.
|
|
mAccessibilityEnabled = enabled;
|
|
if (mDynamicToolbarEnabled) {
|
|
setDynamicToolbarEnabled(!enabled);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onDestroy() {
|
|
if (mPrefObserverId != null) {
|
|
PrefsHelper.removeObserver(mPrefObserverId);
|
|
mPrefObserverId = null;
|
|
}
|
|
if (mBrowserToolbar != null)
|
|
mBrowserToolbar.onDestroy();
|
|
|
|
unregisterEventListener("CharEncoding:Data");
|
|
unregisterEventListener("CharEncoding:State");
|
|
unregisterEventListener("Feedback:LastUrl");
|
|
unregisterEventListener("Feedback:OpenPlayStore");
|
|
unregisterEventListener("Feedback:MaybeLater");
|
|
unregisterEventListener("Telemetry:Gather");
|
|
|
|
if (AppConstants.MOZ_ANDROID_BEAM && Build.VERSION.SDK_INT >= 14) {
|
|
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);
|
|
}
|
|
}
|
|
|
|
super.onDestroy();
|
|
}
|
|
|
|
@Override
|
|
protected void finishProfileMigration() {
|
|
// Update about:home with the new information.
|
|
updateAboutHomeTopSites();
|
|
|
|
super.finishProfileMigration();
|
|
}
|
|
|
|
@Override
|
|
protected void initializeChrome(String uri, boolean isExternalURL) {
|
|
super.initializeChrome(uri, isExternalURL);
|
|
|
|
mBrowserToolbar.updateBackButton(false);
|
|
mBrowserToolbar.updateForwardButton(false);
|
|
|
|
mDoorHangerPopup.setAnchor(mBrowserToolbar.mFavicon);
|
|
|
|
if (isExternalURL || mRestoreMode != RESTORE_NONE) {
|
|
mAboutHomeStartupTimer.cancel();
|
|
}
|
|
|
|
if (!mIsRestoringActivity) {
|
|
if (!isExternalURL) {
|
|
// show about:home if we aren't restoring previous session
|
|
if (mRestoreMode == RESTORE_NONE) {
|
|
Tab tab = Tabs.getInstance().loadUrl("about:home", Tabs.LOADURL_NEW_TAB);
|
|
} else {
|
|
hideAboutHome();
|
|
}
|
|
} else {
|
|
int flags = Tabs.LOADURL_NEW_TAB | Tabs.LOADURL_USER_ENTERED;
|
|
Tabs.getInstance().loadUrl(uri, flags);
|
|
}
|
|
}
|
|
|
|
// Listen to margin changes to position the toolbar correctly
|
|
if (isDynamicToolbarEnabled()) {
|
|
refreshToolbarHeight();
|
|
mLayerView.getLayerMarginsAnimator().showMargins(true);
|
|
mLayerView.getLayerClient().setOnMetricsChangedListener(this);
|
|
}
|
|
|
|
// Intercept key events for gamepad shortcuts
|
|
mLayerView.setOnKeyListener(this);
|
|
}
|
|
|
|
private void setSidebarMargin(int margin) {
|
|
((RelativeLayout.LayoutParams) mGeckoLayout.getLayoutParams()).leftMargin = margin;
|
|
mGeckoLayout.requestLayout();
|
|
}
|
|
|
|
private void setToolbarMargin(int margin) {
|
|
((RelativeLayout.LayoutParams) mGeckoLayout.getLayoutParams()).topMargin = margin;
|
|
mGeckoLayout.requestLayout();
|
|
}
|
|
|
|
@Override
|
|
public void onMetricsChanged(ImmutableViewportMetrics aMetrics) {
|
|
if (mAboutHome.getUserVisibleHint() || mBrowserToolbar == null) {
|
|
return;
|
|
}
|
|
|
|
final View toolbarLayout = mBrowserToolbar.getLayout();
|
|
final int marginTop = Math.round(aMetrics.marginTop);
|
|
ThreadUtils.postToUiThread(new Runnable() {
|
|
public void run() {
|
|
toolbarLayout.scrollTo(0, toolbarLayout.getHeight() - marginTop);
|
|
}
|
|
});
|
|
}
|
|
|
|
@Override
|
|
public void onPanZoomStopped() {
|
|
if (!isDynamicToolbarEnabled() || mAboutHome.getUserVisibleHint()) {
|
|
return;
|
|
}
|
|
|
|
ImmutableViewportMetrics metrics = mLayerView.getViewportMetrics();
|
|
if (metrics.marginTop >= mToolbarHeight / 2) {
|
|
mLayerView.getLayerMarginsAnimator().showMargins(false);
|
|
} else {
|
|
mLayerView.getLayerMarginsAnimator().hideMargins(false);
|
|
}
|
|
}
|
|
|
|
public void refreshToolbarHeight() {
|
|
int height = 0;
|
|
if (mBrowserToolbar != null) {
|
|
height = mBrowserToolbar.getLayout().getHeight();
|
|
}
|
|
|
|
if (!isDynamicToolbarEnabled() || mAboutHome.getUserVisibleHint()) {
|
|
// Use aVisibleHeight here so that when the dynamic toolbar is
|
|
// enabled, the padding will animate with the toolbar becoming
|
|
// visible.
|
|
if (isDynamicToolbarEnabled()) {
|
|
// When the dynamic toolbar is enabled, set the padding on the
|
|
// about:home widget directly - this is to avoid resizing the
|
|
// LayerView, which can cause visible artifacts.
|
|
mAboutHome.setPadding(0, height, 0, 0);
|
|
} else {
|
|
setToolbarMargin(height);
|
|
height = 0;
|
|
}
|
|
} else {
|
|
setToolbarMargin(0);
|
|
}
|
|
|
|
if (mLayerView != null && height != mToolbarHeight) {
|
|
mToolbarHeight = height;
|
|
mLayerView.getLayerMarginsAnimator().setMaxMargins(0, height, 0, 0);
|
|
mLayerView.getLayerMarginsAnimator().showMargins(true);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
void toggleChrome(final boolean aShow) {
|
|
ThreadUtils.postToUiThread(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
if (aShow) {
|
|
mBrowserToolbar.show();
|
|
} else {
|
|
mBrowserToolbar.hide();
|
|
if (hasTabsSideBar()) {
|
|
hideTabs();
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
super.toggleChrome(aShow);
|
|
}
|
|
|
|
@Override
|
|
void focusChrome() {
|
|
ThreadUtils.postToUiThread(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
mBrowserToolbar.show();
|
|
mBrowserToolbar.requestFocusFromTouch();
|
|
}
|
|
});
|
|
}
|
|
|
|
@Override
|
|
public void refreshChrome() {
|
|
// Only ICS phones use a smaller action-bar in landscape mode.
|
|
if (Build.VERSION.SDK_INT >= 14 && !HardwareUtils.isTablet()) {
|
|
int index = mMainLayout.indexOfChild(mBrowserToolbar.getLayout());
|
|
mMainLayout.removeViewAt(index);
|
|
|
|
LinearLayout actionBar = (LinearLayout) getActionBarLayout();
|
|
mMainLayout.addView(actionBar, index);
|
|
mBrowserToolbar.from(actionBar);
|
|
mBrowserToolbar.refresh();
|
|
|
|
// The favicon view is different now, so we need to update the DoorHangerPopup anchor view.
|
|
if (mDoorHangerPopup != null)
|
|
mDoorHangerPopup.setAnchor(mBrowserToolbar.mFavicon);
|
|
}
|
|
|
|
invalidateOptionsMenu();
|
|
updateSideBarState();
|
|
mTabsPanel.refresh();
|
|
}
|
|
|
|
@Override
|
|
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
|
super.onActivityResult(requestCode, resultCode, data);
|
|
|
|
String url = null;
|
|
|
|
// Don't update the url in the toolbar if the activity was cancelled.
|
|
if (resultCode == Activity.RESULT_OK && data != null) {
|
|
// Don't update the url if the activity was launched to pick a site.
|
|
String targetKey = data.getStringExtra(AwesomeBar.TARGET_KEY);
|
|
if (!AwesomeBar.Target.PICK_SITE.toString().equals(targetKey)) {
|
|
// Update the toolbar with the url that was just entered.
|
|
url = data.getStringExtra(AwesomeBar.URL_KEY);
|
|
}
|
|
}
|
|
|
|
// We always need to call fromAwesomeBarSearch to perform the toolbar animation.
|
|
mBrowserToolbar.fromAwesomeBarSearch(url);
|
|
}
|
|
|
|
public View getActionBarLayout() {
|
|
int actionBarRes;
|
|
|
|
if (!HardwareUtils.hasMenuButton() || HardwareUtils.isTablet())
|
|
actionBarRes = R.layout.browser_toolbar_menu;
|
|
else
|
|
actionBarRes = R.layout.browser_toolbar;
|
|
|
|
LinearLayout actionBar = (LinearLayout) LayoutInflater.from(this).inflate(actionBarRes, null);
|
|
actionBar.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.FILL_PARENT,
|
|
(int) getResources().getDimension(R.dimen.browser_toolbar_height)));
|
|
return actionBar;
|
|
}
|
|
|
|
@Override
|
|
public boolean hasTabsSideBar() {
|
|
return (mTabsPanel != null && mTabsPanel.isSideBar());
|
|
}
|
|
|
|
private void updateSideBarState() {
|
|
if (mMainLayoutAnimator != null)
|
|
mMainLayoutAnimator.stop();
|
|
|
|
boolean isSideBar = (HardwareUtils.isTablet() && mOrientation == Configuration.ORIENTATION_LANDSCAPE);
|
|
|
|
ViewGroup.LayoutParams lp = mTabsPanel.getLayoutParams();
|
|
if (isSideBar) {
|
|
lp.width = getResources().getDimensionPixelSize(R.dimen.tabs_sidebar_width);
|
|
} else {
|
|
lp.width = ViewGroup.LayoutParams.FILL_PARENT;
|
|
}
|
|
mTabsPanel.requestLayout();
|
|
|
|
final boolean changed = (mTabsPanel.isSideBar() != isSideBar);
|
|
final boolean needsRelayout = (changed && mTabsPanel.isShown());
|
|
|
|
if (needsRelayout) {
|
|
final int width;
|
|
final int scrollY;
|
|
|
|
if (isSideBar) {
|
|
width = lp.width;
|
|
mMainLayout.scrollTo(0, 0);
|
|
} else {
|
|
width = 0;
|
|
}
|
|
|
|
mBrowserToolbar.adjustForTabsLayout(width);
|
|
setSidebarMargin(width);
|
|
}
|
|
|
|
if (changed) {
|
|
// Cancel state of previous sidebar state
|
|
mBrowserToolbar.updateTabs(false);
|
|
|
|
mTabsPanel.setIsSideBar(isSideBar);
|
|
mBrowserToolbar.setIsSideBar(isSideBar);
|
|
|
|
// Update with new sidebar state
|
|
mBrowserToolbar.updateTabs(mTabsPanel.isShown());
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void handleMessage(String event, JSONObject message) {
|
|
try {
|
|
if (event.equals("Menu:Add")) {
|
|
MenuItemInfo info = new MenuItemInfo();
|
|
info.label = message.getString("name");
|
|
info.id = message.getInt("id") + ADDON_MENU_OFFSET;
|
|
info.checkable = false;
|
|
info.checked = false;
|
|
info.enabled = true;
|
|
info.visible = true;
|
|
String iconRes = null;
|
|
try { // icon is optional
|
|
iconRes = message.getString("icon");
|
|
} catch (Exception ex) { }
|
|
info.icon = iconRes;
|
|
try {
|
|
info.checkable = message.getBoolean("checkable");
|
|
} catch (Exception ex) { }
|
|
try { // parent is optional
|
|
info.parent = message.getInt("parent") + ADDON_MENU_OFFSET;
|
|
} catch (Exception ex) { }
|
|
final MenuItemInfo menuItemInfo = info;
|
|
ThreadUtils.postToUiThread(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
addAddonMenuItem(menuItemInfo);
|
|
}
|
|
});
|
|
} else if (event.equals("Menu:Remove")) {
|
|
final int id = message.getInt("id") + ADDON_MENU_OFFSET;
|
|
ThreadUtils.postToUiThread(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
removeAddonMenuItem(id);
|
|
}
|
|
});
|
|
} else if (event.equals("Menu:Update")) {
|
|
final int id = message.getInt("id") + ADDON_MENU_OFFSET;
|
|
final JSONObject options = message.getJSONObject("options");
|
|
ThreadUtils.postToUiThread(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
updateAddonMenuItem(id, options);
|
|
}
|
|
});
|
|
} else if (event.equals("CharEncoding:Data")) {
|
|
final JSONArray charsets = message.getJSONArray("charsets");
|
|
int selected = message.getInt("selected");
|
|
|
|
final int len = charsets.length();
|
|
final String[] titleArray = new String[len];
|
|
for (int i = 0; i < len; i++) {
|
|
JSONObject charset = charsets.getJSONObject(i);
|
|
titleArray[i] = charset.getString("title");
|
|
}
|
|
|
|
final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this);
|
|
dialogBuilder.setSingleChoiceItems(titleArray, selected, new AlertDialog.OnClickListener() {
|
|
@Override
|
|
public void onClick(DialogInterface dialog, int which) {
|
|
try {
|
|
JSONObject charset = charsets.getJSONObject(which);
|
|
GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("CharEncoding:Set", charset.getString("code")));
|
|
dialog.dismiss();
|
|
} catch (JSONException e) {
|
|
Log.e(LOGTAG, "error parsing json", e);
|
|
}
|
|
}
|
|
});
|
|
dialogBuilder.setNegativeButton(R.string.button_cancel, new AlertDialog.OnClickListener() {
|
|
@Override
|
|
public void onClick(DialogInterface dialog, int which) {
|
|
dialog.dismiss();
|
|
}
|
|
});
|
|
ThreadUtils.postToUiThread(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
dialogBuilder.show();
|
|
}
|
|
});
|
|
} else if (event.equals("CharEncoding:State")) {
|
|
final boolean visible = message.getString("visible").equals("true");
|
|
GeckoPreferences.setCharEncodingState(visible);
|
|
final Menu menu = mMenu;
|
|
ThreadUtils.postToUiThread(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
if (menu != null)
|
|
menu.findItem(R.id.char_encoding).setVisible(visible);
|
|
}
|
|
});
|
|
} else if (event.equals("Feedback:OpenPlayStore")) {
|
|
Intent intent = new Intent(Intent.ACTION_VIEW);
|
|
intent.setData(Uri.parse("market://details?id=" + getPackageName()));
|
|
startActivity(intent);
|
|
} else if (event.equals("Feedback:MaybeLater")) {
|
|
resetFeedbackLaunchCount();
|
|
} else if (event.equals("Feedback:LastUrl")) {
|
|
getLastUrl();
|
|
} else if (event.equals("Gecko:Ready")) {
|
|
// Handle this message in GeckoApp, but also enable the Settings
|
|
// menuitem, which is specific to BrowserApp.
|
|
super.handleMessage(event, message);
|
|
final Menu menu = mMenu;
|
|
ThreadUtils.postToUiThread(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
if (menu != null)
|
|
menu.findItem(R.id.settings).setEnabled(true);
|
|
}
|
|
});
|
|
} else if (event.equals("Telemetry:Gather")) {
|
|
Telemetry.HistogramAdd("PLACES_PAGES_COUNT", BrowserDB.getCount(getContentResolver(), "history"));
|
|
Telemetry.HistogramAdd("PLACES_BOOKMARKS_COUNT", BrowserDB.getCount(getContentResolver(), "bookmarks"));
|
|
Telemetry.HistogramAdd("FENNEC_FAVICONS_COUNT", BrowserDB.getCount(getContentResolver(), "favicons"));
|
|
Telemetry.HistogramAdd("FENNEC_THUMBNAILS_COUNT", BrowserDB.getCount(getContentResolver(), "thumbnails"));
|
|
} else if (event.equals("Reader:Added")) {
|
|
final int result = message.getInt("result");
|
|
final String title = message.getString("title");
|
|
final String url = message.getString("url");
|
|
handleReaderAdded(result, title, url);
|
|
} else if (event.equals("Reader:Removed")) {
|
|
final String url = message.getString("url");
|
|
handleReaderRemoved(url);
|
|
} else if (event.equals("Reader:Share")) {
|
|
final String title = message.getString("title");
|
|
final String url = message.getString("url");
|
|
GeckoAppShell.openUriExternal(url, "text/plain", "", "",
|
|
Intent.ACTION_SEND, title);
|
|
} else {
|
|
super.handleMessage(event, message);
|
|
}
|
|
} catch (Exception e) {
|
|
Log.e(LOGTAG, "Exception handling message \"" + event + "\":", e);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void addTab() {
|
|
showAwesomebar(AwesomeBar.Target.NEW_TAB);
|
|
}
|
|
|
|
@Override
|
|
public void addPrivateTab() {
|
|
Tabs.getInstance().loadUrl("about:privatebrowsing", Tabs.LOADURL_NEW_TAB | Tabs.LOADURL_PRIVATE);
|
|
}
|
|
|
|
@Override
|
|
public void showNormalTabs() {
|
|
showTabs(TabsPanel.Panel.NORMAL_TABS);
|
|
}
|
|
|
|
@Override
|
|
public void showPrivateTabs() {
|
|
showTabs(TabsPanel.Panel.PRIVATE_TABS);
|
|
}
|
|
|
|
@Override
|
|
public void showRemoteTabs() {
|
|
showTabs(TabsPanel.Panel.REMOTE_TABS);
|
|
}
|
|
|
|
private void showTabs(TabsPanel.Panel panel) {
|
|
if (Tabs.getInstance().getDisplayCount() == 0)
|
|
return;
|
|
|
|
mTabsPanel.show(panel);
|
|
}
|
|
|
|
@Override
|
|
public void hideTabs() {
|
|
mTabsPanel.hide();
|
|
}
|
|
|
|
@Override
|
|
public boolean autoHideTabs() {
|
|
if (!hasTabsSideBar() && areTabsShown()) {
|
|
hideTabs();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
@Override
|
|
public boolean areTabsShown() {
|
|
return mTabsPanel.isShown();
|
|
}
|
|
|
|
@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 (mTabsPanel.isShown())
|
|
mTabsPanel.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
|
|
|
|
mMainLayoutAnimator = new PropertyAnimator(animationLength, sTabsInterpolator);
|
|
mMainLayoutAnimator.setPropertyAnimationListener(this);
|
|
|
|
boolean usingTextureView = mLayerView.shouldUseTextureView();
|
|
mMainLayoutAnimator.setUseHardwareLayer(usingTextureView);
|
|
|
|
if (hasTabsSideBar()) {
|
|
mBrowserToolbar.prepareTabsAnimation(mMainLayoutAnimator, width);
|
|
|
|
// Set the gecko layout for sliding.
|
|
if (!mTabsPanel.isShown()) {
|
|
mGeckoLayout.scrollTo(mTabsPanel.getWidth() * -1, 0);
|
|
setSidebarMargin(0);
|
|
}
|
|
|
|
mMainLayoutAnimator.attach(mGeckoLayout,
|
|
PropertyAnimator.Property.SCROLL_X,
|
|
-width);
|
|
} else {
|
|
mMainLayoutAnimator.attach(mMainLayout,
|
|
PropertyAnimator.Property.SCROLL_Y,
|
|
-height);
|
|
}
|
|
|
|
// If the tabs layout is animating onto the screen, pin the dynamic
|
|
// toolbar.
|
|
if (mLayerView != null && isDynamicToolbarEnabled()) {
|
|
if (width > 0 && height > 0) {
|
|
mLayerView.getLayerMarginsAnimator().showMargins(false);
|
|
mLayerView.getLayerMarginsAnimator().setMarginsPinned(true);
|
|
} else {
|
|
mLayerView.getLayerMarginsAnimator().setMarginsPinned(false);
|
|
}
|
|
}
|
|
|
|
mMainLayoutAnimator.start();
|
|
}
|
|
|
|
@Override
|
|
public void onPropertyAnimationStart() {
|
|
mBrowserToolbar.updateTabs(true);
|
|
|
|
// Although the tabs panel is not animating per se, it will be re-drawn several
|
|
// times while the main/gecko layout slides to left/top. Adding a hardware layer
|
|
// here considerably improves the frame rate of the animation.
|
|
if (Build.VERSION.SDK_INT >= 11)
|
|
mTabsPanel.setLayerType(View.LAYER_TYPE_HARDWARE, null);
|
|
else
|
|
mTabsPanel.setDrawingCacheEnabled(true);
|
|
}
|
|
|
|
@Override
|
|
public void onPropertyAnimationEnd() {
|
|
// Destroy the hardware layer used during the animation
|
|
if (Build.VERSION.SDK_INT >= 11)
|
|
mTabsPanel.setLayerType(View.LAYER_TYPE_NONE, null);
|
|
else
|
|
mTabsPanel.setDrawingCacheEnabled(false);
|
|
|
|
if (mTabsPanel.isShown()) {
|
|
if (hasTabsSideBar()) {
|
|
setSidebarMargin(mTabsPanel.getWidth());
|
|
mGeckoLayout.scrollTo(0, 0);
|
|
}
|
|
|
|
mGeckoLayout.requestLayout();
|
|
} else {
|
|
mTabsPanel.setVisibility(View.INVISIBLE);
|
|
mBrowserToolbar.updateTabs(false);
|
|
mBrowserToolbar.finishTabsAnimation();
|
|
mTabsPanel.setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
|
|
}
|
|
|
|
mBrowserToolbar.refreshBackground();
|
|
|
|
if (hasTabsSideBar())
|
|
mBrowserToolbar.adjustTabsAnimation(true);
|
|
|
|
mMainLayoutAnimator = null;
|
|
}
|
|
|
|
/* Favicon methods */
|
|
private void loadFavicon(final Tab tab) {
|
|
maybeCancelFaviconLoad(tab);
|
|
|
|
long id = Favicons.getInstance().loadFavicon(tab.getURL(), tab.getFaviconURL(), !tab.isPrivate(),
|
|
new Favicons.OnFaviconLoadedListener() {
|
|
|
|
@Override
|
|
public void onFaviconLoaded(String pageUrl, Bitmap favicon) {
|
|
// Leave favicon UI untouched if we failed to load the image
|
|
// for some reason.
|
|
if (favicon == null)
|
|
return;
|
|
|
|
// The tab might be pointing to another URL by the time the
|
|
// favicon is finally loaded, in which case we simply ignore it.
|
|
if (!tab.getURL().equals(pageUrl))
|
|
return;
|
|
|
|
tab.updateFavicon(favicon);
|
|
tab.setFaviconLoadId(Favicons.NOT_LOADING);
|
|
|
|
Tabs.getInstance().notifyListeners(tab, Tabs.TabEvents.FAVICON);
|
|
}
|
|
});
|
|
|
|
tab.setFaviconLoadId(id);
|
|
}
|
|
|
|
private void maybeCancelFaviconLoad(Tab tab) {
|
|
long faviconLoadId = tab.getFaviconLoadId();
|
|
|
|
if (faviconLoadId == Favicons.NOT_LOADING)
|
|
return;
|
|
|
|
// Cancel pending favicon load task
|
|
Favicons.getInstance().cancelFaviconLoad(faviconLoadId);
|
|
|
|
// Reset favicon load state
|
|
tab.setFaviconLoadId(Favicons.NOT_LOADING);
|
|
}
|
|
|
|
|
|
/* About:home UI */
|
|
void updateAboutHomeTopSites() {
|
|
mAboutHome.update(EnumSet.of(AboutHome.UpdateFlags.TOP_SITES));
|
|
}
|
|
|
|
private void showAboutHome() {
|
|
if (mAboutHome.getUserVisibleHint()) {
|
|
return;
|
|
}
|
|
|
|
// Refresh toolbar height to possibly restore the toolbar padding
|
|
refreshToolbarHeight();
|
|
|
|
// Show the toolbar before hiding about:home so the
|
|
// onMetricsChanged callback still works.
|
|
if (isDynamicToolbarEnabled() && mLayerView != null) {
|
|
mLayerView.getLayerMarginsAnimator().showMargins(true);
|
|
}
|
|
|
|
// We use commitAllowingStateLoss() instead of commit() here to avoid an
|
|
// IllegalStateException. showAboutHome() and hideAboutHome() are
|
|
// executed inside of tab's onChange() callback. Since that callback can
|
|
// be triggered asynchronously from Gecko, it's possible that this
|
|
// method can be called while Fennec is in the background. If that
|
|
// happens, using commit() would throw an IllegalStateException since
|
|
// it can't be used between the Activity's onSaveInstanceState() and
|
|
// onResume().
|
|
getSupportFragmentManager().beginTransaction()
|
|
.add(R.id.gecko_layout, mAboutHome).commitAllowingStateLoss();
|
|
mAboutHome.setUserVisibleHint(true);
|
|
|
|
mBrowserToolbar.setNextFocusDownId(R.id.abouthome_content);
|
|
}
|
|
|
|
private void hideAboutHome() {
|
|
if (!mAboutHome.getUserVisibleHint()) {
|
|
return;
|
|
}
|
|
|
|
getSupportFragmentManager().beginTransaction()
|
|
.remove(mAboutHome).commitAllowingStateLoss();
|
|
mAboutHome.setUserVisibleHint(false);
|
|
|
|
mBrowserToolbar.setShadowVisibility(true);
|
|
mBrowserToolbar.setNextFocusDownId(R.id.layer_view);
|
|
|
|
// Refresh toolbar height to possibly restore the toolbar padding
|
|
refreshToolbarHeight();
|
|
}
|
|
|
|
private class HideTabsTouchListener implements TouchEventInterceptor {
|
|
private boolean mIsHidingTabs = false;
|
|
|
|
@Override
|
|
public boolean onInterceptTouchEvent(View view, MotionEvent event) {
|
|
// We need to account for scroll state for the touched view otherwise
|
|
// tapping on an "empty" part of the view will still be considered a
|
|
// valid touch event.
|
|
if (view.getScrollX() != 0 || view.getScrollY() != 0) {
|
|
Rect rect = new Rect();
|
|
view.getHitRect(rect);
|
|
rect.offset(-view.getScrollX(), -view.getScrollY());
|
|
|
|
int[] viewCoords = new int[2];
|
|
view.getLocationOnScreen(viewCoords);
|
|
|
|
int x = (int) event.getRawX() - viewCoords[0];
|
|
int y = (int) event.getRawY() - viewCoords[1];
|
|
|
|
if (!rect.contains(x, y))
|
|
return false;
|
|
}
|
|
|
|
// If the tab tray is showing, hide the tab tray and don't send the event to content.
|
|
if (event.getActionMasked() == MotionEvent.ACTION_DOWN && autoHideTabs()) {
|
|
mIsHidingTabs = true;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
@Override
|
|
public boolean onTouch(View view, MotionEvent event) {
|
|
if (mIsHidingTabs) {
|
|
// Keep consuming events until the gesture finishes.
|
|
int action = event.getActionMasked();
|
|
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
|
|
mIsHidingTabs = false;
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private void addAddonMenuItem(final MenuItemInfo info) {
|
|
if (mMenu == null) {
|
|
if (mAddonMenuItemsCache == null)
|
|
mAddonMenuItemsCache = new Vector<MenuItemInfo>();
|
|
|
|
mAddonMenuItemsCache.add(info);
|
|
return;
|
|
}
|
|
|
|
Menu menu;
|
|
if (info.parent == 0) {
|
|
menu = mMenu;
|
|
} else {
|
|
MenuItem parent = mMenu.findItem(info.parent);
|
|
if (parent == null)
|
|
return;
|
|
|
|
if (!parent.hasSubMenu()) {
|
|
mMenu.removeItem(parent.getItemId());
|
|
menu = mMenu.addSubMenu(Menu.NONE, parent.getItemId(), Menu.NONE, parent.getTitle());
|
|
if (parent.getIcon() != null)
|
|
((SubMenu) menu).getItem().setIcon(parent.getIcon());
|
|
} else {
|
|
menu = parent.getSubMenu();
|
|
}
|
|
}
|
|
|
|
final MenuItem item = menu.add(Menu.NONE, info.id, Menu.NONE, info.label);
|
|
item.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
|
|
@Override
|
|
public boolean onMenuItemClick(MenuItem item) {
|
|
Log.i(LOGTAG, "menu item clicked");
|
|
GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Menu:Clicked", Integer.toString(info.id - ADDON_MENU_OFFSET)));
|
|
return true;
|
|
}
|
|
});
|
|
|
|
if (info.icon != null) {
|
|
if (info.icon.startsWith("data")) {
|
|
BitmapDrawable drawable = new BitmapDrawable(BitmapUtils.getBitmapFromDataURI(info.icon));
|
|
item.setIcon(drawable);
|
|
}
|
|
else if (info.icon.startsWith("jar:") || info.icon.startsWith("file://")) {
|
|
ThreadUtils.postToBackgroundThread(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
try {
|
|
URL url = new URL(info.icon);
|
|
InputStream is = (InputStream) url.getContent();
|
|
try {
|
|
Drawable drawable = Drawable.createFromStream(is, "src");
|
|
item.setIcon(drawable);
|
|
} finally {
|
|
is.close();
|
|
}
|
|
} catch (Exception e) {
|
|
Log.w(LOGTAG, "Unable to set icon", e);
|
|
}
|
|
}
|
|
});
|
|
} else {
|
|
item.setIcon(R.drawable.ic_menu_addons_filler);
|
|
}
|
|
} else {
|
|
item.setIcon(R.drawable.ic_menu_addons_filler);
|
|
}
|
|
|
|
item.setCheckable(info.checkable);
|
|
item.setChecked(info.checked);
|
|
item.setEnabled(info.enabled);
|
|
item.setVisible(info.visible);
|
|
}
|
|
|
|
private void removeAddonMenuItem(int id) {
|
|
// Remove add-on menu item from cache, if available.
|
|
if (mAddonMenuItemsCache != null && !mAddonMenuItemsCache.isEmpty()) {
|
|
for (MenuItemInfo item : mAddonMenuItemsCache) {
|
|
if (item.id == id) {
|
|
mAddonMenuItemsCache.remove(item);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (mMenu == null)
|
|
return;
|
|
|
|
MenuItem menuItem = mMenu.findItem(id);
|
|
if (menuItem != null)
|
|
mMenu.removeItem(id);
|
|
}
|
|
|
|
private void updateAddonMenuItem(int id, JSONObject options) {
|
|
// Set attribute for the menu item in cache, if available
|
|
if (mAddonMenuItemsCache != null && !mAddonMenuItemsCache.isEmpty()) {
|
|
for (MenuItemInfo item : mAddonMenuItemsCache) {
|
|
if (item.id == id) {
|
|
try {
|
|
item.checkable = options.getBoolean("checkable");
|
|
} catch (JSONException e) {}
|
|
|
|
try {
|
|
item.checked = options.getBoolean("checked");
|
|
} catch (JSONException e) {}
|
|
|
|
try {
|
|
item.enabled = options.getBoolean("enabled");
|
|
} catch (JSONException e) {}
|
|
|
|
try {
|
|
item.visible = options.getBoolean("visible");
|
|
} catch (JSONException e) {}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (mMenu == null)
|
|
return;
|
|
|
|
MenuItem menuItem = mMenu.findItem(id);
|
|
if (menuItem != null) {
|
|
try {
|
|
menuItem.setCheckable(options.getBoolean("checkable"));
|
|
} catch (JSONException e) {}
|
|
|
|
try {
|
|
menuItem.setChecked(options.getBoolean("checked"));
|
|
} catch (JSONException e) {}
|
|
|
|
try {
|
|
menuItem.setEnabled(options.getBoolean("enabled"));
|
|
} catch (JSONException e) {}
|
|
|
|
try {
|
|
menuItem.setVisible(options.getBoolean("visible"));
|
|
} catch (JSONException e) {}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public boolean onCreateOptionsMenu(Menu menu) {
|
|
super.onCreateOptionsMenu(menu);
|
|
|
|
// Inform the menu about the action-items bar.
|
|
if (menu instanceof GeckoMenu && HardwareUtils.isTablet())
|
|
((GeckoMenu) menu).setActionItemBarPresenter(mBrowserToolbar);
|
|
|
|
MenuInflater inflater = getMenuInflater();
|
|
inflater.inflate(R.menu.browser_app_menu, mMenu);
|
|
|
|
// Add add-on menu items if any.
|
|
if (mAddonMenuItemsCache != null && !mAddonMenuItemsCache.isEmpty()) {
|
|
for (MenuItemInfo item : mAddonMenuItemsCache) {
|
|
addAddonMenuItem(item);
|
|
}
|
|
|
|
mAddonMenuItemsCache.clear();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public void openOptionsMenu() {
|
|
if (!hasTabsSideBar() && areTabsShown())
|
|
return;
|
|
|
|
// Scroll custom menu to the top
|
|
if (mMenuPanel != null)
|
|
mMenuPanel.scrollTo(0, 0);
|
|
|
|
if (!mBrowserToolbar.openOptionsMenu())
|
|
super.openOptionsMenu();
|
|
|
|
if (isDynamicToolbarEnabled() && mLayerView != null)
|
|
mLayerView.getLayerMarginsAnimator().showMargins(false);
|
|
}
|
|
|
|
@Override
|
|
public void closeOptionsMenu() {
|
|
if (!mBrowserToolbar.closeOptionsMenu())
|
|
super.closeOptionsMenu();
|
|
}
|
|
|
|
@Override
|
|
public void setFullScreen(final boolean fullscreen) {
|
|
super.setFullScreen(fullscreen);
|
|
ThreadUtils.postToUiThread(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
if (fullscreen)
|
|
mBrowserToolbar.hide();
|
|
else
|
|
mBrowserToolbar.show();
|
|
}
|
|
});
|
|
}
|
|
|
|
@Override
|
|
public boolean onPrepareOptionsMenu(Menu aMenu) {
|
|
if (aMenu == null)
|
|
return false;
|
|
|
|
if (!GeckoThread.checkLaunchState(GeckoThread.LaunchState.GeckoRunning))
|
|
aMenu.findItem(R.id.settings).setEnabled(false);
|
|
|
|
Tab tab = Tabs.getInstance().getSelectedTab();
|
|
MenuItem bookmark = aMenu.findItem(R.id.bookmark);
|
|
MenuItem forward = aMenu.findItem(R.id.forward);
|
|
MenuItem share = aMenu.findItem(R.id.share);
|
|
MenuItem saveAsPDF = aMenu.findItem(R.id.save_as_pdf);
|
|
MenuItem charEncoding = aMenu.findItem(R.id.char_encoding);
|
|
MenuItem findInPage = aMenu.findItem(R.id.find_in_page);
|
|
MenuItem desktopMode = aMenu.findItem(R.id.desktop_mode);
|
|
|
|
// Only show the "Quit" menu item on pre-ICS or television devices.
|
|
// In ICS+, it's easy to kill an app through the task switcher.
|
|
aMenu.findItem(R.id.quit).setVisible(Build.VERSION.SDK_INT < 14 || HardwareUtils.isTelevision());
|
|
|
|
if (tab == null || tab.getURL() == null) {
|
|
bookmark.setEnabled(false);
|
|
forward.setEnabled(false);
|
|
share.setEnabled(false);
|
|
saveAsPDF.setEnabled(false);
|
|
findInPage.setEnabled(false);
|
|
return true;
|
|
}
|
|
|
|
bookmark.setEnabled(!tab.getURL().startsWith("about:reader"));
|
|
bookmark.setCheckable(true);
|
|
bookmark.setChecked(tab.isBookmark());
|
|
bookmark.setIcon(tab.isBookmark() ? R.drawable.ic_menu_bookmark_remove : R.drawable.ic_menu_bookmark_add);
|
|
|
|
forward.setEnabled(tab.canDoForward());
|
|
desktopMode.setChecked(tab.getDesktopMode());
|
|
desktopMode.setIcon(tab.getDesktopMode() ? R.drawable.ic_menu_desktop_mode_on : R.drawable.ic_menu_desktop_mode_off);
|
|
|
|
String url = tab.getURL();
|
|
if (ReaderModeUtils.isAboutReader(url)) {
|
|
String urlFromReader = ReaderModeUtils.getUrlFromAboutReader(url);
|
|
if (urlFromReader != null)
|
|
url = urlFromReader;
|
|
}
|
|
|
|
// Disable share menuitem for about:, chrome:, file:, and resource: URIs
|
|
String scheme = Uri.parse(url).getScheme();
|
|
share.setEnabled(!(scheme.equals("about") || scheme.equals("chrome") ||
|
|
scheme.equals("file") || scheme.equals("resource")));
|
|
|
|
// Disable save as PDF for about:home and xul pages
|
|
saveAsPDF.setEnabled(!(tab.getURL().equals("about:home") ||
|
|
tab.getContentType().equals("application/vnd.mozilla.xul+xml")));
|
|
|
|
// Disable find in page for about:home, since it won't work on Java content
|
|
findInPage.setEnabled(!tab.getURL().equals("about:home"));
|
|
|
|
charEncoding.setVisible(GeckoPreferences.getCharEncodingState());
|
|
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public boolean onOptionsItemSelected(MenuItem item) {
|
|
Tab tab = null;
|
|
Intent intent = null;
|
|
switch (item.getItemId()) {
|
|
case R.id.bookmark:
|
|
tab = Tabs.getInstance().getSelectedTab();
|
|
if (tab != null) {
|
|
if (item.isChecked()) {
|
|
tab.removeBookmark();
|
|
Toast.makeText(this, R.string.bookmark_removed, Toast.LENGTH_SHORT).show();
|
|
item.setIcon(R.drawable.ic_menu_bookmark_add);
|
|
} else {
|
|
tab.addBookmark();
|
|
Toast.makeText(this, R.string.bookmark_added, Toast.LENGTH_SHORT).show();
|
|
item.setIcon(R.drawable.ic_menu_bookmark_remove);
|
|
}
|
|
}
|
|
return true;
|
|
case R.id.share:
|
|
shareCurrentUrl();
|
|
return true;
|
|
case R.id.reload:
|
|
tab = Tabs.getInstance().getSelectedTab();
|
|
if (tab != null)
|
|
tab.doReload();
|
|
return true;
|
|
case R.id.forward:
|
|
tab = Tabs.getInstance().getSelectedTab();
|
|
if (tab != null)
|
|
tab.doForward();
|
|
return true;
|
|
case R.id.save_as_pdf:
|
|
GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("SaveAs:PDF", null));
|
|
return true;
|
|
case R.id.settings:
|
|
intent = new Intent(this, GeckoPreferences.class);
|
|
startActivity(intent);
|
|
return true;
|
|
case R.id.addons:
|
|
Tabs.getInstance().loadUrlInTab("about:addons");
|
|
return true;
|
|
case R.id.downloads:
|
|
Tabs.getInstance().loadUrlInTab("about:downloads");
|
|
return true;
|
|
case R.id.apps:
|
|
Tabs.getInstance().loadUrlInTab("about:apps");
|
|
return true;
|
|
case R.id.char_encoding:
|
|
GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("CharEncoding:Get", null));
|
|
return true;
|
|
case R.id.find_in_page:
|
|
mFindInPageBar.show();
|
|
return true;
|
|
case R.id.desktop_mode:
|
|
Tab selectedTab = Tabs.getInstance().getSelectedTab();
|
|
if (selectedTab == null)
|
|
return true;
|
|
JSONObject args = new JSONObject();
|
|
try {
|
|
args.put("desktopMode", !item.isChecked());
|
|
args.put("tabId", selectedTab.getId());
|
|
} catch (JSONException e) {
|
|
Log.e(LOGTAG, "error building json arguments");
|
|
}
|
|
GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("DesktopMode:Change", args.toString()));
|
|
return true;
|
|
case R.id.new_tab:
|
|
addTab();
|
|
return true;
|
|
case R.id.new_private_tab:
|
|
addPrivateTab();
|
|
return true;
|
|
default:
|
|
return super.onOptionsItemSelected(item);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This will detect if the key pressed is back. If so, will show the history.
|
|
*/
|
|
@Override
|
|
public boolean onKeyLongPress(int keyCode, KeyEvent event) {
|
|
if (keyCode == KeyEvent.KEYCODE_BACK) {
|
|
Tab tab = Tabs.getInstance().getSelectedTab();
|
|
if (tab != null) {
|
|
return tab.showAllHistory();
|
|
}
|
|
}
|
|
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 intent) {
|
|
super.onNewIntent(intent);
|
|
|
|
String action = intent.getAction();
|
|
|
|
if (AppConstants.MOZ_ANDROID_BEAM && Build.VERSION.SDK_INT >= 10 && NfcAdapter.ACTION_NDEF_DISCOVERED.equals(action)) {
|
|
String uri = intent.getDataString();
|
|
GeckoAppShell.sendEventToGecko(GeckoEvent.createURILoadEvent(uri));
|
|
}
|
|
|
|
if (!Intent.ACTION_MAIN.equals(action) || !mInitialized) {
|
|
return;
|
|
}
|
|
|
|
(new UiAsyncTask<Void, Void, Boolean>(ThreadUtils.getBackgroundHandler()) {
|
|
@Override
|
|
public synchronized Boolean doInBackground(Void... params) {
|
|
// Check to see how many times the app has been launched.
|
|
SharedPreferences settings = getPreferences(Activity.MODE_PRIVATE);
|
|
String keyName = getPackageName() + ".feedback_launch_count";
|
|
int launchCount = settings.getInt(keyName, 0);
|
|
if (launchCount >= FEEDBACK_LAUNCH_COUNT)
|
|
return false;
|
|
|
|
// Increment the launch count and store the new value.
|
|
launchCount++;
|
|
settings.edit().putInt(keyName, launchCount).commit();
|
|
|
|
// If we've reached our magic number, show the feedback page.
|
|
return launchCount == FEEDBACK_LAUNCH_COUNT;
|
|
}
|
|
|
|
@Override
|
|
public void onPostExecute(Boolean shouldShowFeedbackPage) {
|
|
if (shouldShowFeedbackPage)
|
|
Tabs.getInstance().loadUrlInTab("about:feedback");
|
|
}
|
|
}).execute();
|
|
}
|
|
|
|
@Override
|
|
protected NotificationClient makeNotificationClient() {
|
|
// The service is local to Fennec, so we can use it to keep
|
|
// Fennec alive during downloads.
|
|
return new ServiceNotificationClient(getApplicationContext());
|
|
}
|
|
|
|
private void resetFeedbackLaunchCount() {
|
|
ThreadUtils.postToBackgroundThread(new Runnable() {
|
|
@Override
|
|
public synchronized void run() {
|
|
SharedPreferences settings = getPreferences(Activity.MODE_PRIVATE);
|
|
settings.edit().putInt(getPackageName() + ".feedback_launch_count", 0).commit();
|
|
}
|
|
});
|
|
}
|
|
|
|
private void getLastUrl() {
|
|
(new UiAsyncTask<Void, Void, String>(ThreadUtils.getBackgroundHandler()) {
|
|
@Override
|
|
public synchronized String doInBackground(Void... params) {
|
|
// Get the most recent URL stored in browser history.
|
|
String url = "";
|
|
Cursor c = BrowserDB.getRecentHistory(getContentResolver(), 1);
|
|
if (c.moveToFirst()) {
|
|
url = c.getString(c.getColumnIndexOrThrow(Combined.URL));
|
|
}
|
|
c.close();
|
|
return url;
|
|
}
|
|
|
|
@Override
|
|
public void onPostExecute(String url) {
|
|
// Don't bother sending a message if there is no URL.
|
|
if (url.length() > 0)
|
|
GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Feedback:LastUrl", url));
|
|
}
|
|
}).execute();
|
|
}
|
|
|
|
@Override
|
|
public void onAboutHomeUriLoad(String url) {
|
|
mBrowserToolbar.setProgressVisibility(true);
|
|
Tabs.getInstance().loadUrl(url);
|
|
}
|
|
|
|
@Override
|
|
public void onAboutHomeLoadComplete() {
|
|
mAboutHomeStartupTimer.stop();
|
|
}
|
|
|
|
@Override
|
|
public int getLayout() { return R.layout.gecko_app; }
|
|
|
|
@Override
|
|
protected String getDefaultProfileName() {
|
|
String profile = GeckoProfile.findDefaultProfile(this);
|
|
return (profile != null ? profile : "default");
|
|
}
|
|
}
|