gecko/mobile/android/base/BrowserApp.java

1577 lines
58 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.animation.PropertyAnimator;
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;
// Tag for the AboutHome fragment. The fragment is automatically attached
// after restoring from a saved state, so we use this tag to identify it.
private static final String ABOUTHOME_TAG = "abouthome";
private SharedPreferencesHelper mSharedPreferencesHelper;
private OrderedBroadcastHelper mOrderedBroadcastHelper;
@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);
RelativeLayout actionBar = (RelativeLayout) 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;
}
});
// Find the Fragment if it was already added from a restored instance state.
mAboutHome = (AboutHome) getSupportFragmentManager().findFragmentByTag(ABOUTHOME_TAG);
if (mAboutHome == null) {
// 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());
mSharedPreferencesHelper = new SharedPreferencesHelper(getApplicationContext());
mOrderedBroadcastHelper = new OrderedBroadcastHelper(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();
if (mSharedPreferencesHelper != null) {
mSharedPreferencesHelper.uninit();
mSharedPreferencesHelper = null;
}
if (mOrderedBroadcastHelper != null) {
mOrderedBroadcastHelper.uninit();
mOrderedBroadcastHelper = null;
}
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 {
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 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() {
invalidateOptionsMenu();
updateSideBarState();
mTabsPanel.refresh();
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent 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);
// Trigger any tab-related events after we start restoring
// the toolbar state above to make ensure animations happen
// on the correct order.
super.onActivityResult(requestCode, resultCode, data);
}
public View getActionBarLayout() {
RelativeLayout actionBar = (RelativeLayout) LayoutInflater.from(this).inflate(R.layout.browser_toolbar, null);
actionBar.setLayoutParams(new RelativeLayout.LayoutParams(RelativeLayout.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);
final int sidebarWidth = getResources().getDimensionPixelSize(R.dimen.tabs_sidebar_width);
ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) mTabsPanel.getLayoutParams();
lp.width = (isSideBar ? sidebarWidth : ViewGroup.LayoutParams.FILL_PARENT);
mTabsPanel.requestLayout();
final boolean sidebarIsShown = (isSideBar && mTabsPanel.isShown());
final int mainLayoutScrollX = (sidebarIsShown ? -sidebarWidth : 0);
mMainLayout.scrollTo(mainLayoutScrollX, 0);
mTabsPanel.setIsSideBar(isSideBar);
}
@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 (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 (areTabsShown()) {
mTabsPanel.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
}
mMainLayoutAnimator = new PropertyAnimator(animationLength, sTabsInterpolator);
mMainLayoutAnimator.setPropertyAnimationListener(this);
if (hasTabsSideBar()) {
mMainLayoutAnimator.attach(mMainLayout,
PropertyAnimator.Property.SCROLL_X,
-width);
} else {
mMainLayoutAnimator.attach(mMainLayout,
PropertyAnimator.Property.SCROLL_Y,
-height);
}
mTabsPanel.prepareTabsAnimation(mMainLayoutAnimator);
mBrowserToolbar.prepareTabsAnimation(areTabsShown());
// 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() {
}
@Override
public void onPropertyAnimationEnd() {
if (!areTabsShown()) {
mTabsPanel.setVisibility(View.INVISIBLE);
mTabsPanel.setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
}
mTabsPanel.finishTabsAnimation();
mBrowserToolbar.finishTabsAnimation(areTabsShown());
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, ABOUTHOME_TAG).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");
}
}