michael@0: /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- michael@0: * This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: package org.mozilla.gecko; michael@0: michael@0: import java.io.File; michael@0: import java.io.FileNotFoundException; michael@0: import java.net.URLEncoder; michael@0: import java.util.EnumSet; michael@0: import java.util.Vector; michael@0: michael@0: import org.json.JSONArray; michael@0: import org.json.JSONException; michael@0: import org.json.JSONObject; michael@0: michael@0: import org.mozilla.gecko.DynamicToolbar.PinReason; michael@0: import org.mozilla.gecko.DynamicToolbar.VisibilityTransition; michael@0: import org.mozilla.gecko.GeckoProfileDirectories.NoMozillaDirectoryException; michael@0: import org.mozilla.gecko.Telemetry; michael@0: import org.mozilla.gecko.TelemetryContract; michael@0: import org.mozilla.gecko.animation.PropertyAnimator; michael@0: import org.mozilla.gecko.animation.ViewHelper; michael@0: import org.mozilla.gecko.db.BrowserContract.Combined; michael@0: import org.mozilla.gecko.db.BrowserContract.ReadingListItems; michael@0: import org.mozilla.gecko.db.BrowserDB; michael@0: import org.mozilla.gecko.favicons.Favicons; michael@0: import org.mozilla.gecko.favicons.LoadFaviconTask; michael@0: import org.mozilla.gecko.favicons.OnFaviconLoadedListener; michael@0: import org.mozilla.gecko.favicons.decoders.IconDirectoryEntry; michael@0: import org.mozilla.gecko.fxa.FirefoxAccounts; michael@0: import org.mozilla.gecko.fxa.activities.FxAccountGetStartedActivity; michael@0: import org.mozilla.gecko.gfx.BitmapUtils; michael@0: import org.mozilla.gecko.gfx.ImmutableViewportMetrics; michael@0: import org.mozilla.gecko.gfx.LayerMarginsAnimator; michael@0: import org.mozilla.gecko.gfx.LayerView; michael@0: import org.mozilla.gecko.health.BrowserHealthRecorder; michael@0: import org.mozilla.gecko.health.BrowserHealthReporter; michael@0: import org.mozilla.gecko.health.HealthRecorder; michael@0: import org.mozilla.gecko.health.SessionInformation; michael@0: import org.mozilla.gecko.home.BrowserSearch; michael@0: import org.mozilla.gecko.home.HomeBanner; michael@0: import org.mozilla.gecko.home.HomePanelsManager; michael@0: import org.mozilla.gecko.home.HomePager; michael@0: import org.mozilla.gecko.home.HomePager.OnUrlOpenListener; michael@0: import org.mozilla.gecko.home.SearchEngine; michael@0: import org.mozilla.gecko.menu.GeckoMenu; michael@0: import org.mozilla.gecko.menu.GeckoMenuItem; michael@0: import org.mozilla.gecko.preferences.GeckoPreferences; michael@0: import org.mozilla.gecko.prompts.Prompt; michael@0: import org.mozilla.gecko.prompts.PromptListItem; michael@0: import org.mozilla.gecko.sync.setup.SyncAccounts; michael@0: import org.mozilla.gecko.toolbar.AutocompleteHandler; michael@0: import org.mozilla.gecko.toolbar.BrowserToolbar; michael@0: import org.mozilla.gecko.toolbar.ToolbarProgressView; michael@0: import org.mozilla.gecko.util.Clipboard; michael@0: import org.mozilla.gecko.util.GamepadUtils; michael@0: import org.mozilla.gecko.util.HardwareUtils; michael@0: import org.mozilla.gecko.util.MenuUtils; michael@0: import org.mozilla.gecko.util.StringUtils; michael@0: import org.mozilla.gecko.util.ThreadUtils; michael@0: import org.mozilla.gecko.util.UiAsyncTask; michael@0: import org.mozilla.gecko.widget.ButtonToast; michael@0: import org.mozilla.gecko.widget.GeckoActionProvider; michael@0: michael@0: import android.app.Activity; michael@0: import android.app.AlertDialog; michael@0: import android.content.ContentValues; michael@0: import android.content.Context; michael@0: import android.content.DialogInterface; michael@0: import android.content.Intent; michael@0: import android.content.SharedPreferences; michael@0: import android.content.res.Configuration; michael@0: import android.content.res.Resources; michael@0: import android.database.Cursor; michael@0: import android.graphics.Bitmap; michael@0: import android.graphics.Rect; michael@0: import android.graphics.drawable.BitmapDrawable; michael@0: import android.graphics.drawable.Drawable; michael@0: import android.net.Uri; michael@0: import android.nfc.NdefMessage; michael@0: import android.nfc.NdefRecord; michael@0: import android.nfc.NfcAdapter; michael@0: import android.nfc.NfcEvent; michael@0: import android.os.Build; michael@0: import android.os.Bundle; michael@0: import android.support.v4.app.FragmentManager; michael@0: import android.text.TextUtils; michael@0: import android.util.Log; michael@0: import android.view.InputDevice; michael@0: import android.view.KeyEvent; michael@0: import android.view.Menu; michael@0: import android.view.MenuInflater; michael@0: import android.view.MenuItem; michael@0: import android.view.MotionEvent; michael@0: import android.view.SubMenu; michael@0: import android.view.View; michael@0: import android.view.ViewGroup; michael@0: import android.view.ViewStub; michael@0: import android.view.ViewTreeObserver; michael@0: import android.view.Window; michael@0: import android.view.animation.Interpolator; michael@0: import android.widget.RelativeLayout; michael@0: import android.widget.ListView; michael@0: import android.widget.Toast; michael@0: import android.widget.ViewFlipper; michael@0: michael@0: abstract public class BrowserApp extends GeckoApp michael@0: implements TabsPanel.TabsLayoutChangeListener, michael@0: PropertyAnimator.PropertyAnimationListener, michael@0: View.OnKeyListener, michael@0: LayerView.OnMetricsChangedListener, michael@0: BrowserSearch.OnSearchListener, michael@0: BrowserSearch.OnEditSuggestionListener, michael@0: HomePager.OnNewTabsListener, michael@0: OnUrlOpenListener, michael@0: ActionModeCompat.Presenter { michael@0: private static final String LOGTAG = "GeckoBrowserApp"; michael@0: michael@0: private static final int TABS_ANIMATION_DURATION = 450; michael@0: michael@0: private static final int READER_ADD_SUCCESS = 0; michael@0: private static final int READER_ADD_FAILED = 1; michael@0: private static final int READER_ADD_DUPLICATE = 2; michael@0: michael@0: private static final String ADD_SHORTCUT_TOAST = "add_shortcut_toast"; michael@0: public static final String GUEST_BROWSING_ARG = "--guest"; michael@0: michael@0: private static final String STATE_ABOUT_HOME_TOP_PADDING = "abouthome_top_padding"; michael@0: michael@0: private static final String BROWSER_SEARCH_TAG = "browser_search"; michael@0: private BrowserSearch mBrowserSearch; michael@0: private View mBrowserSearchContainer; michael@0: michael@0: public ViewFlipper mViewFlipper; michael@0: public ActionModeCompatView mActionBar; michael@0: private BrowserToolbar mBrowserToolbar; michael@0: private ToolbarProgressView mProgressView; michael@0: private HomePager mHomePager; michael@0: private TabsPanel mTabsPanel; michael@0: private ViewGroup mHomePagerContainer; michael@0: protected Telemetry.Timer mAboutHomeStartupTimer = null; michael@0: private ActionModeCompat mActionMode; michael@0: private boolean mShowActionModeEndAnimation = false; michael@0: michael@0: private static final int GECKO_TOOLS_MENU = -1; michael@0: private static final int ADDON_MENU_OFFSET = 1000; michael@0: private static class MenuItemInfo { michael@0: public int id; michael@0: public String label; michael@0: public String icon; michael@0: public boolean checkable = false; michael@0: public boolean checked = false; michael@0: public boolean enabled = true; michael@0: public boolean visible = true; michael@0: public int parent; michael@0: public boolean added = false; // So we can re-add after a locale change. michael@0: } michael@0: michael@0: // The types of guest mdoe dialogs we show michael@0: private static enum GuestModeDialog { michael@0: ENTERING, michael@0: LEAVING michael@0: } michael@0: michael@0: private Vector mAddonMenuItemsCache; michael@0: private PropertyAnimator mMainLayoutAnimator; michael@0: michael@0: private static final Interpolator sTabsInterpolator = new Interpolator() { michael@0: @Override michael@0: public float getInterpolation(float t) { michael@0: t -= 1.0f; michael@0: return t * t * t * t * t + 1.0f; michael@0: } michael@0: }; michael@0: michael@0: private FindInPageBar mFindInPageBar; michael@0: private MediaCastingBar mMediaCastingBar; michael@0: michael@0: // We'll ask for feedback after the user launches the app this many times. michael@0: private static final int FEEDBACK_LAUNCH_COUNT = 15; michael@0: michael@0: // Stored value of the toolbar height, so we know when it's changed. michael@0: private int mToolbarHeight = 0; michael@0: michael@0: // Stored value of whether the last metrics change allowed for toolbar michael@0: // scrolling. michael@0: private boolean mDynamicToolbarCanScroll = false; michael@0: michael@0: private SharedPreferencesHelper mSharedPreferencesHelper; michael@0: michael@0: private OrderedBroadcastHelper mOrderedBroadcastHelper; michael@0: michael@0: private BrowserHealthReporter mBrowserHealthReporter; michael@0: michael@0: // The tab to be selected on editing mode exit. michael@0: private Integer mTargetTabForEditingMode = null; michael@0: michael@0: // The animator used to toggle HomePager visibility has a race where if the HomePager is shown michael@0: // (starting the animation), the HomePager is hidden, and the HomePager animation completes, michael@0: // both the web content and the HomePager will be hidden. This flag is used to prevent the michael@0: // race by determining if the web content should be hidden at the animation's end. michael@0: private boolean mHideWebContentOnAnimationEnd = false; michael@0: michael@0: private DynamicToolbar mDynamicToolbar = new DynamicToolbar(); michael@0: michael@0: @Override michael@0: public void onTabChanged(Tab tab, Tabs.TabEvents msg, Object data) { michael@0: if (tab == null) { michael@0: // Only RESTORED is allowed a null tab: it's the only event that michael@0: // isn't tied to a specific tab. michael@0: if (msg != Tabs.TabEvents.RESTORED) { michael@0: throw new IllegalArgumentException("onTabChanged:" + msg + " must specify a tab."); michael@0: } michael@0: return; michael@0: } michael@0: michael@0: Log.d(LOGTAG, "BrowserApp.onTabChanged: " + tab.getId() + ": " + msg); michael@0: switch(msg) { michael@0: case LOCATION_CHANGE: michael@0: if (Tabs.getInstance().isSelectedTab(tab)) { michael@0: maybeCancelFaviconLoad(tab); michael@0: } michael@0: // fall through michael@0: case SELECTED: michael@0: if (Tabs.getInstance().isSelectedTab(tab)) { michael@0: updateHomePagerForTab(tab); michael@0: michael@0: final TabsPanel.Panel panel = tab.isPrivate() michael@0: ? TabsPanel.Panel.PRIVATE_TABS michael@0: : TabsPanel.Panel.NORMAL_TABS; michael@0: michael@0: if (areTabsShown() && mTabsPanel.getCurrentPanel() != panel) { michael@0: showTabs(panel); michael@0: } michael@0: } michael@0: break; michael@0: case START: michael@0: if (Tabs.getInstance().isSelectedTab(tab)) { michael@0: invalidateOptionsMenu(); michael@0: michael@0: if (mDynamicToolbar.isEnabled()) { michael@0: mDynamicToolbar.setVisible(true, VisibilityTransition.ANIMATE); michael@0: } michael@0: } michael@0: break; michael@0: case LOAD_ERROR: michael@0: case STOP: michael@0: case MENU_UPDATED: michael@0: if (Tabs.getInstance().isSelectedTab(tab)) { michael@0: invalidateOptionsMenu(); michael@0: } michael@0: break; michael@0: case PAGE_SHOW: michael@0: loadFavicon(tab); michael@0: break; michael@0: case LINK_FAVICON: michael@0: // If tab is not loading and the favicon is updated, we michael@0: // want to load the image straight away. If tab is still michael@0: // loading, we only load the favicon once the page's content michael@0: // is fully loaded. michael@0: if (tab.getState() != Tab.STATE_LOADING) { michael@0: loadFavicon(tab); michael@0: } michael@0: break; michael@0: } michael@0: super.onTabChanged(tab, msg, data); michael@0: } michael@0: michael@0: @Override michael@0: public boolean onKey(View v, int keyCode, KeyEvent event) { michael@0: // Global onKey handler. This is called if the focused UI doesn't michael@0: // handle the key event, and before Gecko swallows the events. michael@0: if (event.getAction() != KeyEvent.ACTION_DOWN) { michael@0: return false; michael@0: } michael@0: michael@0: // Gamepad support only exists in API-level >= 9 michael@0: if (Build.VERSION.SDK_INT >= 9 && michael@0: (event.getSource() & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) { michael@0: switch (keyCode) { michael@0: case KeyEvent.KEYCODE_BUTTON_Y: michael@0: // Toggle/focus the address bar on gamepad-y button. michael@0: if (mViewFlipper.getVisibility() == View.VISIBLE) { michael@0: if (mDynamicToolbar.isEnabled() && !isHomePagerVisible()) { michael@0: mDynamicToolbar.setVisible(false, VisibilityTransition.ANIMATE); michael@0: if (mLayerView != null) { michael@0: mLayerView.requestFocus(); michael@0: } michael@0: } else { michael@0: // Just focus the address bar when about:home is visible michael@0: // or when the dynamic toolbar isn't enabled. michael@0: mBrowserToolbar.requestFocusFromTouch(); michael@0: } michael@0: } else { michael@0: mDynamicToolbar.setVisible(true, VisibilityTransition.ANIMATE); michael@0: mBrowserToolbar.requestFocusFromTouch(); michael@0: } michael@0: return true; michael@0: case KeyEvent.KEYCODE_BUTTON_L1: michael@0: // Go back on L1 michael@0: Tabs.getInstance().getSelectedTab().doBack(); michael@0: return true; michael@0: case KeyEvent.KEYCODE_BUTTON_R1: michael@0: // Go forward on R1 michael@0: Tabs.getInstance().getSelectedTab().doForward(); michael@0: return true; michael@0: } michael@0: } michael@0: michael@0: // Check if this was a shortcut. Meta keys exists only on 11+. michael@0: final Tab tab = Tabs.getInstance().getSelectedTab(); michael@0: if (Build.VERSION.SDK_INT >= 11 && tab != null && event.isCtrlPressed()) { michael@0: switch (keyCode) { michael@0: case KeyEvent.KEYCODE_LEFT_BRACKET: michael@0: tab.doBack(); michael@0: return true; michael@0: michael@0: case KeyEvent.KEYCODE_RIGHT_BRACKET: michael@0: tab.doForward(); michael@0: return true; michael@0: michael@0: case KeyEvent.KEYCODE_R: michael@0: tab.doReload(); michael@0: return true; michael@0: michael@0: case KeyEvent.KEYCODE_PERIOD: michael@0: tab.doStop(); michael@0: return true; michael@0: michael@0: case KeyEvent.KEYCODE_T: michael@0: addTab(); michael@0: return true; michael@0: michael@0: case KeyEvent.KEYCODE_W: michael@0: Tabs.getInstance().closeTab(tab); michael@0: return true; michael@0: michael@0: case KeyEvent.KEYCODE_F: michael@0: mFindInPageBar.show(); michael@0: return true; michael@0: } michael@0: } michael@0: michael@0: return false; michael@0: } michael@0: michael@0: @Override michael@0: public boolean onKeyDown(int keyCode, KeyEvent event) { michael@0: if (!mBrowserToolbar.isEditing() && onKey(null, keyCode, event)) { michael@0: return true; michael@0: } michael@0: return super.onKeyDown(keyCode, event); michael@0: } michael@0: michael@0: void handleReaderListStatusRequest(final String url) { michael@0: ThreadUtils.postToBackgroundThread(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: final int inReadingList = BrowserDB.isReadingListItem(getContentResolver(), url) ? 1 : 0; michael@0: michael@0: final JSONObject json = new JSONObject(); michael@0: try { michael@0: json.put("url", url); michael@0: json.put("inReadingList", inReadingList); michael@0: } catch (JSONException e) { michael@0: Log.e(LOGTAG, "JSON error - failed to return inReadingList status", e); michael@0: return; michael@0: } michael@0: michael@0: GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Reader:ListStatusReturn", json.toString())); michael@0: } michael@0: }); michael@0: } michael@0: michael@0: private void handleReaderAdded(int result, final ContentValues values) { michael@0: if (result != READER_ADD_SUCCESS) { michael@0: if (result == READER_ADD_FAILED) { michael@0: showToast(R.string.reading_list_failed, Toast.LENGTH_SHORT); michael@0: } else if (result == READER_ADD_DUPLICATE) { michael@0: showToast(R.string.reading_list_duplicate, Toast.LENGTH_SHORT); michael@0: } michael@0: michael@0: return; michael@0: } michael@0: michael@0: ThreadUtils.postToBackgroundThread(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: BrowserDB.addReadingListItem(getContentResolver(), values); michael@0: showToast(R.string.reading_list_added, Toast.LENGTH_SHORT); michael@0: } michael@0: }); michael@0: } michael@0: michael@0: private ContentValues messageToReadingListContentValues(JSONObject message) { michael@0: final ContentValues values = new ContentValues(); michael@0: values.put(ReadingListItems.URL, message.optString("url")); michael@0: values.put(ReadingListItems.TITLE, message.optString("title")); michael@0: values.put(ReadingListItems.LENGTH, message.optInt("length")); michael@0: values.put(ReadingListItems.EXCERPT, message.optString("excerpt")); michael@0: return values; michael@0: } michael@0: michael@0: void handleReaderRemoved(final String url) { michael@0: ThreadUtils.postToBackgroundThread(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: BrowserDB.removeReadingListItemWithURL(getContentResolver(), url); michael@0: showToast(R.string.reading_list_removed, Toast.LENGTH_SHORT); michael@0: michael@0: final int count = BrowserDB.getReadingListCount(getContentResolver()); michael@0: GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Reader:ListCountUpdated", Integer.toString(count))); michael@0: } michael@0: }); michael@0: } michael@0: michael@0: @Override michael@0: public void onCreate(Bundle savedInstanceState) { michael@0: mAboutHomeStartupTimer = new Telemetry.UptimeTimer("FENNEC_STARTUP_TIME_ABOUTHOME"); michael@0: michael@0: final Intent intent = getIntent(); michael@0: michael@0: String args = intent.getStringExtra("args"); michael@0: if (args != null && args.contains(GUEST_BROWSING_ARG)) { michael@0: mProfile = GeckoProfile.createGuestProfile(this); michael@0: } else { michael@0: GeckoProfile.maybeCleanupGuestProfile(this); michael@0: } michael@0: michael@0: // This has to be prepared prior to calling GeckoApp.onCreate, because michael@0: // widget code and BrowserToolbar need it, and they're created by the michael@0: // layout, which GeckoApp takes care of. michael@0: ((GeckoApplication) getApplication()).prepareLightweightTheme(); michael@0: super.onCreate(savedInstanceState); michael@0: michael@0: mViewFlipper = (ViewFlipper) findViewById(R.id.browser_actionbar); michael@0: mActionBar = (ActionModeCompatView) findViewById(R.id.actionbar); michael@0: michael@0: mBrowserToolbar = (BrowserToolbar) findViewById(R.id.browser_toolbar); michael@0: mProgressView = (ToolbarProgressView) findViewById(R.id.progress); michael@0: mBrowserToolbar.setProgressBar(mProgressView); michael@0: if (Intent.ACTION_VIEW.equals(intent.getAction())) { michael@0: // Show the target URL immediately in the toolbar. michael@0: mBrowserToolbar.setTitle(intent.getDataString()); michael@0: michael@0: Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.INTENT); michael@0: } michael@0: michael@0: ((GeckoApp.MainLayout) mMainLayout).setTouchEventInterceptor(new HideTabsTouchListener()); michael@0: ((GeckoApp.MainLayout) mMainLayout).setMotionEventInterceptor(new MotionEventInterceptor() { michael@0: @Override michael@0: public boolean onInterceptMotionEvent(View view, MotionEvent event) { michael@0: // If we get a gamepad panning MotionEvent while the focus is not on the layerview, michael@0: // put the focus on the layerview and carry on michael@0: if (mLayerView != null && !mLayerView.hasFocus() && GamepadUtils.isPanningControl(event)) { michael@0: if (mHomePager == null) { michael@0: return false; michael@0: } michael@0: michael@0: if (isHomePagerVisible()) { michael@0: mLayerView.requestFocus(); michael@0: } else { michael@0: mHomePager.requestFocus(); michael@0: } michael@0: } michael@0: return false; michael@0: } michael@0: }); michael@0: michael@0: mHomePagerContainer = (ViewGroup) findViewById(R.id.home_pager_container); michael@0: michael@0: mBrowserSearchContainer = findViewById(R.id.search_container); michael@0: mBrowserSearch = (BrowserSearch) getSupportFragmentManager().findFragmentByTag(BROWSER_SEARCH_TAG); michael@0: if (mBrowserSearch == null) { michael@0: mBrowserSearch = BrowserSearch.newInstance(); michael@0: mBrowserSearch.setUserVisibleHint(false); michael@0: } michael@0: michael@0: setBrowserToolbarListeners(); michael@0: michael@0: mFindInPageBar = (FindInPageBar) findViewById(R.id.find_in_page); michael@0: mMediaCastingBar = (MediaCastingBar) findViewById(R.id.media_casting); michael@0: michael@0: registerEventListener("CharEncoding:Data"); michael@0: registerEventListener("CharEncoding:State"); michael@0: registerEventListener("Feedback:LastUrl"); michael@0: registerEventListener("Feedback:OpenPlayStore"); michael@0: registerEventListener("Feedback:MaybeLater"); michael@0: registerEventListener("Telemetry:Gather"); michael@0: registerEventListener("Settings:Show"); michael@0: registerEventListener("Updater:Launch"); michael@0: registerEventListener("Menu:Add"); michael@0: registerEventListener("Menu:Remove"); michael@0: registerEventListener("Menu:Update"); michael@0: registerEventListener("Accounts:Create"); michael@0: registerEventListener("Accounts:Exist"); michael@0: registerEventListener("Prompt:ShowTop"); michael@0: michael@0: Distribution.init(this); michael@0: JavaAddonManager.getInstance().init(getApplicationContext()); michael@0: mSharedPreferencesHelper = new SharedPreferencesHelper(getApplicationContext()); michael@0: mOrderedBroadcastHelper = new OrderedBroadcastHelper(getApplicationContext()); michael@0: mBrowserHealthReporter = new BrowserHealthReporter(); michael@0: michael@0: if (AppConstants.MOZ_ANDROID_BEAM && Build.VERSION.SDK_INT >= 14) { michael@0: NfcAdapter nfc = NfcAdapter.getDefaultAdapter(this); michael@0: if (nfc != null) { michael@0: nfc.setNdefPushMessageCallback(new NfcAdapter.CreateNdefMessageCallback() { michael@0: @Override michael@0: public NdefMessage createNdefMessage(NfcEvent event) { michael@0: Tab tab = Tabs.getInstance().getSelectedTab(); michael@0: if (tab == null || tab.isPrivate()) { michael@0: return null; michael@0: } michael@0: return new NdefMessage(new NdefRecord[] { NdefRecord.createUri(tab.getURL()) }); michael@0: } michael@0: }, this); michael@0: } michael@0: } michael@0: michael@0: if (savedInstanceState != null) { michael@0: mDynamicToolbar.onRestoreInstanceState(savedInstanceState); michael@0: mHomePagerContainer.setPadding(0, savedInstanceState.getInt(STATE_ABOUT_HOME_TOP_PADDING), 0, 0); michael@0: } michael@0: michael@0: mDynamicToolbar.setEnabledChangedListener(new DynamicToolbar.OnEnabledChangedListener() { michael@0: @Override michael@0: public void onEnabledChanged(boolean enabled) { michael@0: setDynamicToolbarEnabled(enabled); michael@0: } michael@0: }); michael@0: michael@0: // Set the maximum bits-per-pixel the favicon system cares about. michael@0: IconDirectoryEntry.setMaxBPP(GeckoAppShell.getScreenDepth()); michael@0: } michael@0: michael@0: @Override michael@0: public void onBackPressed() { michael@0: if (getSupportFragmentManager().getBackStackEntryCount() > 0) { michael@0: super.onBackPressed(); michael@0: return; michael@0: } michael@0: michael@0: if (mBrowserToolbar.onBackPressed()) { michael@0: return; michael@0: } michael@0: michael@0: if (mActionMode != null) { michael@0: endActionModeCompat(); michael@0: return; michael@0: } michael@0: michael@0: super.onBackPressed(); michael@0: } michael@0: michael@0: @Override michael@0: public void onResume() { michael@0: super.onResume(); michael@0: unregisterEventListener("Prompt:ShowTop"); michael@0: } michael@0: michael@0: @Override michael@0: public void onPause() { michael@0: super.onPause(); michael@0: // Register for Prompt:ShowTop so we can foreground this activity even if it's hidden. michael@0: registerEventListener("Prompt:ShowTop"); michael@0: } michael@0: michael@0: private void setBrowserToolbarListeners() { michael@0: mBrowserToolbar.setOnActivateListener(new BrowserToolbar.OnActivateListener() { michael@0: public void onActivate() { michael@0: enterEditingMode(); michael@0: } michael@0: }); michael@0: michael@0: mBrowserToolbar.setOnCommitListener(new BrowserToolbar.OnCommitListener() { michael@0: public void onCommit() { michael@0: commitEditingMode(); michael@0: } michael@0: }); michael@0: michael@0: mBrowserToolbar.setOnDismissListener(new BrowserToolbar.OnDismissListener() { michael@0: public void onDismiss() { michael@0: mBrowserToolbar.cancelEdit(); michael@0: } michael@0: }); michael@0: michael@0: mBrowserToolbar.setOnFilterListener(new BrowserToolbar.OnFilterListener() { michael@0: public void onFilter(String searchText, AutocompleteHandler handler) { michael@0: filterEditingMode(searchText, handler); michael@0: } michael@0: }); michael@0: michael@0: mBrowserToolbar.setOnFocusChangeListener(new View.OnFocusChangeListener() { michael@0: @Override michael@0: public void onFocusChange(View v, boolean hasFocus) { michael@0: if (isHomePagerVisible()) { michael@0: mHomePager.onToolbarFocusChange(hasFocus); michael@0: } michael@0: } michael@0: }); michael@0: michael@0: mBrowserToolbar.setOnStartEditingListener(new BrowserToolbar.OnStartEditingListener() { michael@0: public void onStartEditing() { michael@0: // Temporarily disable doorhanger notifications. michael@0: mDoorHangerPopup.disable(); michael@0: } michael@0: }); michael@0: michael@0: mBrowserToolbar.setOnStopEditingListener(new BrowserToolbar.OnStopEditingListener() { michael@0: public void onStopEditing() { michael@0: selectTargetTabForEditingMode(); michael@0: michael@0: // Since the underlying LayerView is set visible in hideHomePager, we would michael@0: // ordinarily want to call it first. However, hideBrowserSearch changes the michael@0: // visibility of the HomePager and hideHomePager will take no action if the michael@0: // HomePager is hidden, so we want to call hideBrowserSearch to restore the michael@0: // HomePager visibility first. michael@0: hideBrowserSearch(); michael@0: hideHomePager(); michael@0: michael@0: // Re-enable doorhanger notifications. They may trigger on the selected tab above. michael@0: mDoorHangerPopup.enable(); michael@0: } michael@0: }); michael@0: michael@0: // Intercept key events for gamepad shortcuts michael@0: mBrowserToolbar.setOnKeyListener(this); michael@0: } michael@0: michael@0: private void showBookmarkDialog() { michael@0: final Tab tab = Tabs.getInstance().getSelectedTab(); michael@0: final Prompt ps = new Prompt(this, new Prompt.PromptCallback() { michael@0: @Override michael@0: public void onPromptFinished(String result) { michael@0: int itemId = -1; michael@0: try { michael@0: itemId = new JSONObject(result).getInt("button"); michael@0: } catch(JSONException ex) { michael@0: Log.e(LOGTAG, "Exception reading bookmark prompt result", ex); michael@0: } michael@0: michael@0: if (tab == null) michael@0: return; michael@0: michael@0: if (itemId == 0) { michael@0: new EditBookmarkDialog(BrowserApp.this).show(tab.getURL()); michael@0: } else if (itemId == 1) { michael@0: String url = tab.getURL(); michael@0: String title = tab.getDisplayTitle(); michael@0: Bitmap favicon = tab.getFavicon(); michael@0: if (url != null && title != null) { michael@0: GeckoAppShell.createShortcut(title, url, url, favicon, ""); michael@0: } michael@0: } michael@0: } michael@0: }); michael@0: michael@0: final PromptListItem[] items = new PromptListItem[2]; michael@0: Resources res = getResources(); michael@0: items[0] = new PromptListItem(res.getString(R.string.contextmenu_edit_bookmark)); michael@0: items[1] = new PromptListItem(res.getString(R.string.contextmenu_add_to_launcher)); michael@0: michael@0: ps.show("", "", items, ListView.CHOICE_MODE_NONE); michael@0: } michael@0: michael@0: private void setDynamicToolbarEnabled(boolean enabled) { michael@0: ThreadUtils.assertOnUiThread(); michael@0: michael@0: if (enabled) { michael@0: if (mLayerView != null) { michael@0: mLayerView.setOnMetricsChangedListener(this); michael@0: } michael@0: setToolbarMargin(0); michael@0: mHomePagerContainer.setPadding(0, mViewFlipper.getHeight(), 0, 0); michael@0: } else { michael@0: // Immediately show the toolbar when disabling the dynamic michael@0: // toolbar. michael@0: if (mLayerView != null) { michael@0: mLayerView.setOnMetricsChangedListener(null); michael@0: } michael@0: mHomePagerContainer.setPadding(0, 0, 0, 0); michael@0: if (mViewFlipper != null) { michael@0: ViewHelper.setTranslationY(mViewFlipper, 0); michael@0: } michael@0: } michael@0: michael@0: refreshToolbarHeight(); michael@0: } michael@0: michael@0: private static boolean isAboutHome(final Tab tab) { michael@0: return AboutPages.isAboutHome(tab.getURL()); michael@0: } michael@0: michael@0: @Override michael@0: public boolean onSearchRequested() { michael@0: enterEditingMode(); michael@0: return true; michael@0: } michael@0: michael@0: @Override michael@0: public boolean onContextItemSelected(MenuItem item) { michael@0: final int itemId = item.getItemId(); michael@0: if (itemId == R.id.pasteandgo) { michael@0: String text = Clipboard.getText(); michael@0: if (!TextUtils.isEmpty(text)) { michael@0: Tabs.getInstance().loadUrl(text); michael@0: Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.CONTEXT_MENU); michael@0: Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.CONTEXT_MENU, "pasteandgo"); michael@0: } michael@0: return true; michael@0: } michael@0: michael@0: if (itemId == R.id.site_settings) { michael@0: // This can be selected from either the browser menu or the contextmenu, depending on the size and version (v11+) of the phone. michael@0: GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Permissions:Get", null)); michael@0: if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) { michael@0: Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.CONTEXT_MENU, "site_settings"); michael@0: } michael@0: return true; michael@0: } michael@0: michael@0: if (itemId == R.id.paste) { michael@0: String text = Clipboard.getText(); michael@0: if (!TextUtils.isEmpty(text)) { michael@0: enterEditingMode(text); michael@0: Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.CONTEXT_MENU, "paste"); michael@0: } michael@0: return true; michael@0: } michael@0: michael@0: if (itemId == R.id.share) { michael@0: shareCurrentUrl(); michael@0: return true; michael@0: } michael@0: michael@0: if (itemId == R.id.subscribe) { michael@0: // This can be selected from either the browser menu or the contextmenu, depending on the size and version (v11+) of the phone. michael@0: Tab tab = Tabs.getInstance().getSelectedTab(); michael@0: if (tab != null && tab.hasFeeds()) { michael@0: JSONObject args = new JSONObject(); michael@0: try { michael@0: args.put("tabId", tab.getId()); michael@0: } catch (JSONException e) { michael@0: Log.e(LOGTAG, "error building json arguments"); michael@0: } michael@0: GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Feeds:Subscribe", args.toString())); michael@0: if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) { michael@0: Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.CONTEXT_MENU, "subscribe"); michael@0: } michael@0: } michael@0: return true; michael@0: } michael@0: michael@0: if (itemId == R.id.add_search_engine) { michael@0: // This can be selected from either the browser menu or the contextmenu, depending on the size and version (v11+) of the phone. michael@0: Tab tab = Tabs.getInstance().getSelectedTab(); michael@0: if (tab != null && tab.hasOpenSearch()) { michael@0: JSONObject args = new JSONObject(); michael@0: try { michael@0: args.put("tabId", tab.getId()); michael@0: } catch (JSONException e) { michael@0: Log.e(LOGTAG, "error building json arguments"); michael@0: return true; michael@0: } michael@0: GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("SearchEngines:Add", args.toString())); michael@0: michael@0: if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) { michael@0: Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.CONTEXT_MENU, "add_search_engine"); michael@0: } michael@0: } michael@0: return true; michael@0: } michael@0: michael@0: if (itemId == R.id.copyurl) { michael@0: Tab tab = Tabs.getInstance().getSelectedTab(); michael@0: if (tab != null) { michael@0: String url = tab.getURL(); michael@0: if (url != null) { michael@0: Clipboard.setText(url); michael@0: Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.CONTEXT_MENU, "copyurl"); michael@0: } michael@0: } michael@0: return true; michael@0: } michael@0: michael@0: if (itemId == R.id.add_to_launcher) { michael@0: Tab tab = Tabs.getInstance().getSelectedTab(); michael@0: if (tab == null) { michael@0: return true; michael@0: } michael@0: michael@0: final String url = tab.getURL(); michael@0: final String title = tab.getDisplayTitle(); michael@0: if (url == null || title == null) { michael@0: return true; michael@0: } michael@0: michael@0: final OnFaviconLoadedListener listener = new GeckoAppShell.CreateShortcutFaviconLoadedListener(url, title); michael@0: Favicons.getSizedFavicon(url, michael@0: tab.getFaviconURL(), michael@0: Integer.MAX_VALUE, michael@0: LoadFaviconTask.FLAG_PERSIST, michael@0: listener); michael@0: return true; michael@0: } michael@0: michael@0: return false; michael@0: } michael@0: michael@0: @Override michael@0: public void setAccessibilityEnabled(boolean enabled) { michael@0: mDynamicToolbar.setAccessibilityEnabled(enabled); michael@0: } michael@0: michael@0: @Override michael@0: public void onDestroy() { michael@0: mDynamicToolbar.destroy(); michael@0: michael@0: if (mBrowserToolbar != null) michael@0: mBrowserToolbar.onDestroy(); michael@0: michael@0: if (mFindInPageBar != null) { michael@0: mFindInPageBar.onDestroy(); michael@0: mFindInPageBar = null; michael@0: } michael@0: michael@0: if (mMediaCastingBar != null) { michael@0: mMediaCastingBar.onDestroy(); michael@0: mMediaCastingBar = null; michael@0: } michael@0: michael@0: if (mSharedPreferencesHelper != null) { michael@0: mSharedPreferencesHelper.uninit(); michael@0: mSharedPreferencesHelper = null; michael@0: } michael@0: michael@0: if (mOrderedBroadcastHelper != null) { michael@0: mOrderedBroadcastHelper.uninit(); michael@0: mOrderedBroadcastHelper = null; michael@0: } michael@0: michael@0: if (mBrowserHealthReporter != null) { michael@0: mBrowserHealthReporter.uninit(); michael@0: mBrowserHealthReporter = null; michael@0: } michael@0: michael@0: unregisterEventListener("CharEncoding:Data"); michael@0: unregisterEventListener("CharEncoding:State"); michael@0: unregisterEventListener("Feedback:LastUrl"); michael@0: unregisterEventListener("Feedback:OpenPlayStore"); michael@0: unregisterEventListener("Feedback:MaybeLater"); michael@0: unregisterEventListener("Telemetry:Gather"); michael@0: unregisterEventListener("Settings:Show"); michael@0: unregisterEventListener("Updater:Launch"); michael@0: unregisterEventListener("Menu:Add"); michael@0: unregisterEventListener("Menu:Remove"); michael@0: unregisterEventListener("Menu:Update"); michael@0: unregisterEventListener("Accounts:Create"); michael@0: unregisterEventListener("Accounts:Exist"); michael@0: michael@0: if (AppConstants.MOZ_ANDROID_BEAM && Build.VERSION.SDK_INT >= 14) { michael@0: NfcAdapter nfc = NfcAdapter.getDefaultAdapter(this); michael@0: if (nfc != null) { michael@0: // null this out even though the docs say it's not needed, michael@0: // because the source code looks like it will only do this michael@0: // automatically on API 14+ michael@0: nfc.setNdefPushMessageCallback(null, this); michael@0: } michael@0: } michael@0: michael@0: super.onDestroy(); michael@0: } michael@0: michael@0: @Override michael@0: protected void initializeChrome() { michael@0: super.initializeChrome(); michael@0: michael@0: mDoorHangerPopup.setAnchor(mBrowserToolbar.getDoorHangerAnchor()); michael@0: michael@0: mDynamicToolbar.setLayerView(mLayerView); michael@0: setDynamicToolbarEnabled(mDynamicToolbar.isEnabled()); michael@0: michael@0: // Intercept key events for gamepad shortcuts michael@0: mLayerView.setOnKeyListener(this); michael@0: michael@0: // Initialize the actionbar menu items on startup for both large and small tablets michael@0: if (HardwareUtils.isTablet()) { michael@0: onCreatePanelMenu(Window.FEATURE_OPTIONS_PANEL, null); michael@0: invalidateOptionsMenu(); michael@0: } michael@0: } michael@0: michael@0: private void shareCurrentUrl() { michael@0: Tab tab = Tabs.getInstance().getSelectedTab(); michael@0: if (tab == null) { michael@0: return; michael@0: } michael@0: michael@0: String url = tab.getURL(); michael@0: if (url == null) { michael@0: return; michael@0: } michael@0: michael@0: if (AboutPages.isAboutReader(url)) { michael@0: url = ReaderModeUtils.getUrlFromAboutReader(url); michael@0: } michael@0: michael@0: GeckoAppShell.openUriExternal(url, "text/plain", "", "", michael@0: Intent.ACTION_SEND, tab.getDisplayTitle()); michael@0: michael@0: // Context: Sharing via chrome list (no explicit session is active) michael@0: Telemetry.sendUIEvent(TelemetryContract.Event.SHARE, TelemetryContract.Method.LIST); michael@0: } michael@0: michael@0: @Override michael@0: protected void loadStartupTab(String url) { michael@0: // We aren't showing about:home, so cancel the telemetry timer michael@0: if (url != null || mShouldRestore) { michael@0: mAboutHomeStartupTimer.cancel(); michael@0: } michael@0: michael@0: super.loadStartupTab(url); michael@0: } michael@0: michael@0: private void setToolbarMargin(int margin) { michael@0: ((RelativeLayout.LayoutParams) mGeckoLayout.getLayoutParams()).topMargin = margin; michael@0: mGeckoLayout.requestLayout(); michael@0: } michael@0: michael@0: @Override michael@0: public void onMetricsChanged(ImmutableViewportMetrics aMetrics) { michael@0: if (isHomePagerVisible() || mViewFlipper == null) { michael@0: return; michael@0: } michael@0: michael@0: // If the page has shrunk so that the toolbar no longer scrolls, make michael@0: // sure the toolbar is visible. michael@0: if (aMetrics.getPageHeight() <= aMetrics.getHeight()) { michael@0: if (mDynamicToolbarCanScroll) { michael@0: mDynamicToolbarCanScroll = false; michael@0: if (mViewFlipper.getVisibility() != View.VISIBLE) { michael@0: ThreadUtils.postToUiThread(new Runnable() { michael@0: public void run() { michael@0: mDynamicToolbar.setVisible(true, VisibilityTransition.ANIMATE); michael@0: } michael@0: }); michael@0: } michael@0: } michael@0: } else { michael@0: mDynamicToolbarCanScroll = true; michael@0: } michael@0: michael@0: final View toolbarLayout = mViewFlipper; michael@0: final int marginTop = Math.round(aMetrics.marginTop); michael@0: ThreadUtils.postToUiThread(new Runnable() { michael@0: public void run() { michael@0: final float translationY = marginTop - toolbarLayout.getHeight(); michael@0: ViewHelper.setTranslationY(toolbarLayout, translationY); michael@0: ViewHelper.setTranslationY(mProgressView, translationY); michael@0: michael@0: if (mDoorHangerPopup.isShowing()) { michael@0: mDoorHangerPopup.updatePopup(); michael@0: } michael@0: } michael@0: }); michael@0: michael@0: if (mFormAssistPopup != null) michael@0: mFormAssistPopup.onMetricsChanged(aMetrics); michael@0: } michael@0: michael@0: @Override michael@0: public void onPanZoomStopped() { michael@0: if (!mDynamicToolbar.isEnabled() || isHomePagerVisible()) { michael@0: return; michael@0: } michael@0: michael@0: // Make sure the toolbar is fully hidden or fully shown when the user michael@0: // lifts their finger. If the page is shorter than the viewport, the michael@0: // toolbar is always shown. michael@0: ImmutableViewportMetrics metrics = mLayerView.getViewportMetrics(); michael@0: if (metrics.getPageHeight() < metrics.getHeight() michael@0: || metrics.marginTop >= mToolbarHeight / 2) { michael@0: mDynamicToolbar.setVisible(true, VisibilityTransition.ANIMATE); michael@0: } else { michael@0: mDynamicToolbar.setVisible(false, VisibilityTransition.ANIMATE); michael@0: } michael@0: } michael@0: michael@0: public void refreshToolbarHeight() { michael@0: ThreadUtils.assertOnUiThread(); michael@0: michael@0: int height = 0; michael@0: if (mViewFlipper != null) { michael@0: height = mViewFlipper.getHeight(); michael@0: } michael@0: michael@0: if (!mDynamicToolbar.isEnabled() || isHomePagerVisible()) { michael@0: // Use aVisibleHeight here so that when the dynamic toolbar is michael@0: // enabled, the padding will animate with the toolbar becoming michael@0: // visible. michael@0: if (mDynamicToolbar.isEnabled()) { michael@0: // When the dynamic toolbar is enabled, set the padding on the michael@0: // about:home widget directly - this is to avoid resizing the michael@0: // LayerView, which can cause visible artifacts. michael@0: mHomePagerContainer.setPadding(0, height, 0, 0); michael@0: } else { michael@0: setToolbarMargin(height); michael@0: height = 0; michael@0: } michael@0: } else { michael@0: setToolbarMargin(0); michael@0: } michael@0: michael@0: if (mLayerView != null && height != mToolbarHeight) { michael@0: mToolbarHeight = height; michael@0: mLayerView.getLayerMarginsAnimator().setMaxMargins(0, height, 0, 0); michael@0: mDynamicToolbar.setVisible(true, VisibilityTransition.IMMEDIATE); michael@0: } michael@0: } michael@0: michael@0: @Override michael@0: void toggleChrome(final boolean aShow) { michael@0: ThreadUtils.postToUiThread(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: if (aShow) { michael@0: mViewFlipper.setVisibility(View.VISIBLE); michael@0: } else { michael@0: mViewFlipper.setVisibility(View.GONE); michael@0: if (hasTabsSideBar()) { michael@0: hideTabs(); michael@0: } michael@0: } michael@0: } michael@0: }); michael@0: michael@0: super.toggleChrome(aShow); michael@0: } michael@0: michael@0: @Override michael@0: void focusChrome() { michael@0: ThreadUtils.postToUiThread(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: mViewFlipper.setVisibility(View.VISIBLE); michael@0: mViewFlipper.requestFocusFromTouch(); michael@0: } michael@0: }); michael@0: } michael@0: michael@0: @Override michael@0: public void refreshChrome() { michael@0: invalidateOptionsMenu(); michael@0: michael@0: if (mTabsPanel != null) { michael@0: updateSideBarState(); michael@0: mTabsPanel.refresh(); michael@0: } michael@0: michael@0: mBrowserToolbar.refresh(); michael@0: } michael@0: michael@0: @Override michael@0: public boolean hasTabsSideBar() { michael@0: return (mTabsPanel != null && mTabsPanel.isSideBar()); michael@0: } michael@0: michael@0: private void updateSideBarState() { michael@0: if (mMainLayoutAnimator != null) michael@0: mMainLayoutAnimator.stop(); michael@0: michael@0: boolean isSideBar = (HardwareUtils.isTablet() && getOrientation() == Configuration.ORIENTATION_LANDSCAPE); michael@0: final int sidebarWidth = getResources().getDimensionPixelSize(R.dimen.tabs_sidebar_width); michael@0: michael@0: ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) mTabsPanel.getLayoutParams(); michael@0: lp.width = (isSideBar ? sidebarWidth : ViewGroup.LayoutParams.FILL_PARENT); michael@0: mTabsPanel.requestLayout(); michael@0: michael@0: final boolean sidebarIsShown = (isSideBar && mTabsPanel.isShown()); michael@0: final int mainLayoutScrollX = (sidebarIsShown ? -sidebarWidth : 0); michael@0: mMainLayout.scrollTo(mainLayoutScrollX, 0); michael@0: michael@0: mTabsPanel.setIsSideBar(isSideBar); michael@0: } michael@0: michael@0: @Override michael@0: public void handleMessage(String event, JSONObject message) { michael@0: try { michael@0: if (event.equals("Menu:Add")) { michael@0: MenuItemInfo info = new MenuItemInfo(); michael@0: info.label = message.getString("name"); michael@0: info.id = message.getInt("id") + ADDON_MENU_OFFSET; michael@0: info.icon = message.optString("icon", null); michael@0: info.checked = message.optBoolean("checked", false); michael@0: info.enabled = message.optBoolean("enabled", true); michael@0: info.visible = message.optBoolean("visible", true); michael@0: info.checkable = message.optBoolean("checkable", false); michael@0: int parent = message.optInt("parent", 0); michael@0: info.parent = parent <= 0 ? parent : parent + ADDON_MENU_OFFSET; michael@0: final MenuItemInfo menuItemInfo = info; michael@0: ThreadUtils.postToUiThread(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: addAddonMenuItem(menuItemInfo); michael@0: } michael@0: }); michael@0: } else if (event.equals("Menu:Remove")) { michael@0: final int id = message.getInt("id") + ADDON_MENU_OFFSET; michael@0: ThreadUtils.postToUiThread(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: removeAddonMenuItem(id); michael@0: } michael@0: }); michael@0: } else if (event.equals("Menu:Update")) { michael@0: final int id = message.getInt("id") + ADDON_MENU_OFFSET; michael@0: final JSONObject options = message.getJSONObject("options"); michael@0: ThreadUtils.postToUiThread(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: updateAddonMenuItem(id, options); michael@0: } michael@0: }); michael@0: } else if (event.equals("CharEncoding:Data")) { michael@0: final JSONArray charsets = message.getJSONArray("charsets"); michael@0: int selected = message.getInt("selected"); michael@0: michael@0: final int len = charsets.length(); michael@0: final String[] titleArray = new String[len]; michael@0: for (int i = 0; i < len; i++) { michael@0: JSONObject charset = charsets.getJSONObject(i); michael@0: titleArray[i] = charset.getString("title"); michael@0: } michael@0: michael@0: final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this); michael@0: dialogBuilder.setSingleChoiceItems(titleArray, selected, new AlertDialog.OnClickListener() { michael@0: @Override michael@0: public void onClick(DialogInterface dialog, int which) { michael@0: try { michael@0: JSONObject charset = charsets.getJSONObject(which); michael@0: GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("CharEncoding:Set", charset.getString("code"))); michael@0: dialog.dismiss(); michael@0: } catch (JSONException e) { michael@0: Log.e(LOGTAG, "error parsing json", e); michael@0: } michael@0: } michael@0: }); michael@0: dialogBuilder.setNegativeButton(R.string.button_cancel, new AlertDialog.OnClickListener() { michael@0: @Override michael@0: public void onClick(DialogInterface dialog, int which) { michael@0: dialog.dismiss(); michael@0: } michael@0: }); michael@0: ThreadUtils.postToUiThread(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: dialogBuilder.show(); michael@0: } michael@0: }); michael@0: } else if (event.equals("CharEncoding:State")) { michael@0: final boolean visible = message.getString("visible").equals("true"); michael@0: GeckoPreferences.setCharEncodingState(visible); michael@0: final Menu menu = mMenu; michael@0: ThreadUtils.postToUiThread(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: if (menu != null) michael@0: menu.findItem(R.id.char_encoding).setVisible(visible); michael@0: } michael@0: }); michael@0: } else if (event.equals("Feedback:OpenPlayStore")) { michael@0: Intent intent = new Intent(Intent.ACTION_VIEW); michael@0: intent.setData(Uri.parse("market://details?id=" + getPackageName())); michael@0: startActivity(intent); michael@0: } else if (event.equals("Feedback:MaybeLater")) { michael@0: resetFeedbackLaunchCount(); michael@0: } else if (event.equals("Feedback:LastUrl")) { michael@0: getLastUrl(); michael@0: } else if (event.equals("Gecko:DelayedStartup")) { michael@0: ThreadUtils.postToUiThread(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: // Force tabs panel inflation once the initial michael@0: // pageload is finished. michael@0: ensureTabsPanelExists(); michael@0: } michael@0: }); michael@0: michael@0: super.handleMessage(event, message); michael@0: } else if (event.equals("Gecko:Ready")) { michael@0: // Handle this message in GeckoApp, but also enable the Settings michael@0: // menuitem, which is specific to BrowserApp. michael@0: super.handleMessage(event, message); michael@0: final Menu menu = mMenu; michael@0: ThreadUtils.postToUiThread(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: if (menu != null) michael@0: menu.findItem(R.id.settings).setEnabled(true); michael@0: } michael@0: }); michael@0: michael@0: // Display notification for Mozilla data reporting, if data should be collected. michael@0: if (AppConstants.MOZ_DATA_REPORTING) { michael@0: DataReportingNotification.checkAndNotifyPolicy(GeckoAppShell.getContext()); michael@0: } michael@0: michael@0: } else if (event.equals("Telemetry:Gather")) { michael@0: Telemetry.HistogramAdd("PLACES_PAGES_COUNT", BrowserDB.getCount(getContentResolver(), "history")); michael@0: Telemetry.HistogramAdd("PLACES_BOOKMARKS_COUNT", BrowserDB.getCount(getContentResolver(), "bookmarks")); michael@0: Telemetry.HistogramAdd("FENNEC_FAVICONS_COUNT", BrowserDB.getCount(getContentResolver(), "favicons")); michael@0: Telemetry.HistogramAdd("FENNEC_THUMBNAILS_COUNT", BrowserDB.getCount(getContentResolver(), "thumbnails")); michael@0: } else if (event.equals("Reader:ListStatusRequest")) { michael@0: handleReaderListStatusRequest(message.getString("url")); michael@0: } else if (event.equals("Reader:Added")) { michael@0: final int result = message.getInt("result"); michael@0: handleReaderAdded(result, messageToReadingListContentValues(message)); michael@0: } else if (event.equals("Reader:Removed")) { michael@0: final String url = message.getString("url"); michael@0: handleReaderRemoved(url); michael@0: } else if (event.equals("Reader:Share")) { michael@0: final String title = message.getString("title"); michael@0: final String url = message.getString("url"); michael@0: GeckoAppShell.openUriExternal(url, "text/plain", "", "", michael@0: Intent.ACTION_SEND, title); michael@0: } else if (event.equals("Settings:Show")) { michael@0: // null strings return "null" (http://code.google.com/p/android/issues/detail?id=13830) michael@0: String resource = null; michael@0: if (!message.isNull(GeckoPreferences.INTENT_EXTRA_RESOURCES)) { michael@0: resource = message.getString(GeckoPreferences.INTENT_EXTRA_RESOURCES); michael@0: } michael@0: Intent settingsIntent = new Intent(this, GeckoPreferences.class); michael@0: GeckoPreferences.setResourceToOpen(settingsIntent, resource); michael@0: startActivity(settingsIntent); michael@0: } else if (event.equals("Updater:Launch")) { michael@0: handleUpdaterLaunch(); michael@0: } else if (event.equals("Prompt:ShowTop")) { michael@0: // Bring this activity to front so the prompt is visible.. michael@0: Intent bringToFrontIntent = new Intent(); michael@0: bringToFrontIntent.setClassName(AppConstants.ANDROID_PACKAGE_NAME, AppConstants.BROWSER_INTENT_CLASS_NAME); michael@0: bringToFrontIntent.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); michael@0: startActivity(bringToFrontIntent); michael@0: } else if (event.equals("Accounts:Create")) { michael@0: // Do exactly the same thing as if you tapped 'Sync' michael@0: // in Settings. michael@0: final Intent intent = new Intent(getContext(), FxAccountGetStartedActivity.class); michael@0: intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); michael@0: getContext().startActivity(intent); michael@0: } else if (event.equals("Accounts:Exist")) { michael@0: final String kind = message.getString("kind"); michael@0: final JSONObject response = new JSONObject(); michael@0: michael@0: if ("any".equals(kind)) { michael@0: response.put("exists", SyncAccounts.syncAccountsExist(getContext()) || michael@0: FirefoxAccounts.firefoxAccountsExist(getContext())); michael@0: EventDispatcher.sendResponse(message, response); michael@0: } else if ("fxa".equals(kind)) { michael@0: response.put("exists", FirefoxAccounts.firefoxAccountsExist(getContext())); michael@0: EventDispatcher.sendResponse(message, response); michael@0: } else if ("sync11".equals(kind)) { michael@0: response.put("exists", SyncAccounts.syncAccountsExist(getContext())); michael@0: EventDispatcher.sendResponse(message, response); michael@0: } else { michael@0: response.put("error", "Unknown kind"); michael@0: EventDispatcher.sendError(message, response); michael@0: } michael@0: } else { michael@0: super.handleMessage(event, message); michael@0: } michael@0: } catch (Exception e) { michael@0: Log.e(LOGTAG, "Exception handling message \"" + event + "\":", e); michael@0: } michael@0: } michael@0: michael@0: @Override michael@0: public void addTab() { michael@0: // Always load about:home when opening a new tab. michael@0: Tabs.getInstance().loadUrl(AboutPages.HOME, Tabs.LOADURL_NEW_TAB); michael@0: } michael@0: michael@0: @Override michael@0: public void addPrivateTab() { michael@0: Tabs.getInstance().loadUrl(AboutPages.PRIVATEBROWSING, Tabs.LOADURL_NEW_TAB | Tabs.LOADURL_PRIVATE); michael@0: } michael@0: michael@0: @Override michael@0: public void showNormalTabs() { michael@0: showTabs(TabsPanel.Panel.NORMAL_TABS); michael@0: } michael@0: michael@0: @Override michael@0: public void showPrivateTabs() { michael@0: showTabs(TabsPanel.Panel.PRIVATE_TABS); michael@0: } michael@0: /** michael@0: * Ensure the TabsPanel view is properly inflated and returns michael@0: * true when the view has been inflated, false otherwise. michael@0: */ michael@0: private boolean ensureTabsPanelExists() { michael@0: if (mTabsPanel != null) { michael@0: return false; michael@0: } michael@0: michael@0: ViewStub tabsPanelStub = (ViewStub) findViewById(R.id.tabs_panel); michael@0: mTabsPanel = (TabsPanel) tabsPanelStub.inflate(); michael@0: michael@0: mTabsPanel.setTabsLayoutChangeListener(this); michael@0: updateSideBarState(); michael@0: michael@0: return true; michael@0: } michael@0: michael@0: private void showTabs(final TabsPanel.Panel panel) { michael@0: if (Tabs.getInstance().getDisplayCount() == 0) michael@0: return; michael@0: michael@0: if (ensureTabsPanelExists()) { michael@0: // If we've just inflated the tabs panel, only show it once the current michael@0: // layout pass is done to avoid displayed temporary UI states during michael@0: // relayout. michael@0: ViewTreeObserver vto = mTabsPanel.getViewTreeObserver(); michael@0: if (vto.isAlive()) { michael@0: vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { michael@0: @Override michael@0: public void onGlobalLayout() { michael@0: mTabsPanel.getViewTreeObserver().removeGlobalOnLayoutListener(this); michael@0: mTabsPanel.show(panel); michael@0: } michael@0: }); michael@0: } michael@0: } else { michael@0: mTabsPanel.show(panel); michael@0: } michael@0: } michael@0: michael@0: @Override michael@0: public void hideTabs() { michael@0: mTabsPanel.hide(); michael@0: } michael@0: michael@0: @Override michael@0: public boolean autoHideTabs() { michael@0: if (areTabsShown()) { michael@0: hideTabs(); michael@0: return true; michael@0: } michael@0: return false; michael@0: } michael@0: michael@0: @Override michael@0: public boolean areTabsShown() { michael@0: return (mTabsPanel != null && mTabsPanel.isShown()); michael@0: } michael@0: michael@0: @Override michael@0: public void onTabsLayoutChange(int width, int height) { michael@0: int animationLength = TABS_ANIMATION_DURATION; michael@0: michael@0: if (mMainLayoutAnimator != null) { michael@0: animationLength = Math.max(1, animationLength - (int)mMainLayoutAnimator.getRemainingTime()); michael@0: mMainLayoutAnimator.stop(false); michael@0: } michael@0: michael@0: if (areTabsShown()) { michael@0: mTabsPanel.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS); michael@0: } michael@0: michael@0: mMainLayoutAnimator = new PropertyAnimator(animationLength, sTabsInterpolator); michael@0: mMainLayoutAnimator.addPropertyAnimationListener(this); michael@0: michael@0: if (hasTabsSideBar()) { michael@0: mMainLayoutAnimator.attach(mMainLayout, michael@0: PropertyAnimator.Property.SCROLL_X, michael@0: -width); michael@0: } else { michael@0: mMainLayoutAnimator.attach(mMainLayout, michael@0: PropertyAnimator.Property.SCROLL_Y, michael@0: -height); michael@0: } michael@0: michael@0: mTabsPanel.prepareTabsAnimation(mMainLayoutAnimator); michael@0: mBrowserToolbar.prepareTabsAnimation(mMainLayoutAnimator, areTabsShown()); michael@0: michael@0: // If the tabs layout is animating onto the screen, pin the dynamic michael@0: // toolbar. michael@0: if (mDynamicToolbar.isEnabled()) { michael@0: if (width > 0 && height > 0) { michael@0: mDynamicToolbar.setPinned(true, PinReason.RELAYOUT); michael@0: mDynamicToolbar.setVisible(true, VisibilityTransition.ANIMATE); michael@0: } else { michael@0: mDynamicToolbar.setPinned(false, PinReason.RELAYOUT); michael@0: } michael@0: } michael@0: michael@0: mMainLayoutAnimator.start(); michael@0: } michael@0: michael@0: @Override michael@0: public void onPropertyAnimationStart() { michael@0: } michael@0: michael@0: @Override michael@0: public void onPropertyAnimationEnd() { michael@0: if (!areTabsShown()) { michael@0: mTabsPanel.setVisibility(View.INVISIBLE); michael@0: mTabsPanel.setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS); michael@0: } michael@0: michael@0: mTabsPanel.finishTabsAnimation(); michael@0: michael@0: mMainLayoutAnimator = null; michael@0: } michael@0: michael@0: @Override michael@0: public void onSaveInstanceState(Bundle outState) { michael@0: super.onSaveInstanceState(outState); michael@0: mDynamicToolbar.onSaveInstanceState(outState); michael@0: outState.putInt(STATE_ABOUT_HOME_TOP_PADDING, mHomePagerContainer.getPaddingTop()); michael@0: } michael@0: michael@0: /** michael@0: * Attempts to switch to an open tab with the given URL. michael@0: * michael@0: * @return true if we successfully switched to a tab, false otherwise. michael@0: */ michael@0: private boolean maybeSwitchToTab(String url, EnumSet flags) { michael@0: if (!flags.contains(OnUrlOpenListener.Flags.ALLOW_SWITCH_TO_TAB)) { michael@0: return false; michael@0: } michael@0: michael@0: final Tabs tabs = Tabs.getInstance(); michael@0: final Tab tab = tabs.getFirstTabForUrl(url, tabs.getSelectedTab().isPrivate()); michael@0: if (tab == null) { michael@0: return false; michael@0: } michael@0: michael@0: // Set the target tab to null so it does not get selected (on editing michael@0: // mode exit) in lieu of the tab we are about to select. michael@0: mTargetTabForEditingMode = null; michael@0: tabs.selectTab(tab.getId()); michael@0: michael@0: mBrowserToolbar.cancelEdit(); michael@0: michael@0: return true; michael@0: } michael@0: michael@0: private void openUrlAndStopEditing(String url) { michael@0: openUrlAndStopEditing(url, null, false); michael@0: } michael@0: michael@0: private void openUrlAndStopEditing(String url, boolean newTab) { michael@0: openUrlAndStopEditing(url, null, newTab); michael@0: } michael@0: michael@0: private void openUrlAndStopEditing(String url, String searchEngine) { michael@0: openUrlAndStopEditing(url, searchEngine, false); michael@0: } michael@0: michael@0: private void openUrlAndStopEditing(String url, String searchEngine, boolean newTab) { michael@0: int flags = Tabs.LOADURL_NONE; michael@0: if (newTab) { michael@0: flags |= Tabs.LOADURL_NEW_TAB; michael@0: } michael@0: michael@0: Tabs.getInstance().loadUrl(url, searchEngine, -1, flags); michael@0: michael@0: mBrowserToolbar.cancelEdit(); michael@0: } michael@0: michael@0: private boolean isHomePagerVisible() { michael@0: return (mHomePager != null && mHomePager.isVisible() michael@0: && mHomePagerContainer != null && mHomePagerContainer.getVisibility() == View.VISIBLE); michael@0: } michael@0: michael@0: /* Favicon stuff. */ michael@0: private static OnFaviconLoadedListener sFaviconLoadedListener = new OnFaviconLoadedListener() { michael@0: @Override michael@0: public void onFaviconLoaded(String pageUrl, String faviconURL, Bitmap favicon) { michael@0: // If we failed to load a favicon, we use the default favicon instead. michael@0: Tabs.getInstance() michael@0: .updateFaviconForURL(pageUrl, michael@0: (favicon == null) ? Favicons.defaultFavicon : favicon); michael@0: } michael@0: }; michael@0: michael@0: private void loadFavicon(final Tab tab) { michael@0: maybeCancelFaviconLoad(tab); michael@0: michael@0: final int tabFaviconSize = getResources().getDimensionPixelSize(R.dimen.browser_toolbar_favicon_size); michael@0: michael@0: int flags = (tab.isPrivate() || tab.getErrorType() != Tab.ErrorType.NONE) ? 0 : LoadFaviconTask.FLAG_PERSIST; michael@0: int id = Favicons.getSizedFavicon(tab.getURL(), tab.getFaviconURL(), tabFaviconSize, flags, sFaviconLoadedListener); michael@0: michael@0: tab.setFaviconLoadId(id); michael@0: } michael@0: michael@0: private void maybeCancelFaviconLoad(Tab tab) { michael@0: int faviconLoadId = tab.getFaviconLoadId(); michael@0: michael@0: if (Favicons.NOT_LOADING == faviconLoadId) { michael@0: return; michael@0: } michael@0: michael@0: // Cancel load task and reset favicon load state if it wasn't already michael@0: // in NOT_LOADING state. michael@0: Favicons.cancelFaviconLoad(faviconLoadId); michael@0: tab.setFaviconLoadId(Favicons.NOT_LOADING); michael@0: } michael@0: michael@0: /** michael@0: * Enters editing mode with the current tab's URL. There might be no michael@0: * tabs loaded by the time the user enters editing mode e.g. just after michael@0: * the app starts. In this case, we simply fallback to an empty URL. michael@0: */ michael@0: private void enterEditingMode() { michael@0: String url = ""; michael@0: michael@0: final Tab tab = Tabs.getInstance().getSelectedTab(); michael@0: if (tab != null) { michael@0: final String userSearch = tab.getUserSearch(); michael@0: michael@0: // Check to see if there's a user-entered search term, michael@0: // which we save whenever the user performs a search. michael@0: url = (TextUtils.isEmpty(userSearch) ? tab.getURL() : userSearch); michael@0: } michael@0: michael@0: enterEditingMode(url); michael@0: } michael@0: michael@0: /** michael@0: * Enters editing mode with the specified URL. This method will michael@0: * always open the HISTORY page on about:home. michael@0: */ michael@0: private void enterEditingMode(String url) { michael@0: if (url == null) { michael@0: throw new IllegalArgumentException("Cannot handle null URLs in enterEditingMode"); michael@0: } michael@0: michael@0: if (mBrowserToolbar.isEditing() || mBrowserToolbar.isAnimating()) { michael@0: return; michael@0: } michael@0: michael@0: final Tab selectedTab = Tabs.getInstance().getSelectedTab(); michael@0: mTargetTabForEditingMode = (selectedTab != null ? selectedTab.getId() : null); michael@0: michael@0: final PropertyAnimator animator = new PropertyAnimator(250); michael@0: animator.setUseHardwareLayer(false); michael@0: michael@0: mBrowserToolbar.startEditing(url, animator); michael@0: michael@0: final String panelId = selectedTab.getMostRecentHomePanel(); michael@0: showHomePagerWithAnimator(panelId, animator); michael@0: michael@0: animator.start(); michael@0: Telemetry.startUISession(TelemetryContract.Session.AWESOMESCREEN); michael@0: } michael@0: michael@0: private void commitEditingMode() { michael@0: if (!mBrowserToolbar.isEditing()) { michael@0: return; michael@0: } michael@0: michael@0: Telemetry.stopUISession(TelemetryContract.Session.AWESOMESCREEN, michael@0: TelemetryContract.Reason.COMMIT); michael@0: michael@0: final String url = mBrowserToolbar.commitEdit(); michael@0: michael@0: // HACK: We don't know the url that will be loaded when hideHomePager is initially called michael@0: // in BrowserToolbar's onStopEditing listener so on the awesomescreen, hideHomePager will michael@0: // use the url "about:home" and return without taking any action. hideBrowserSearch is michael@0: // then called, but since hideHomePager changes both HomePager and LayerView visibility michael@0: // and exited without taking an action, no Views are displayed and graphical corruption is michael@0: // visible instead. michael@0: // michael@0: // Here we call hideHomePager for the second time with the URL to be loaded so that michael@0: // hideHomePager is called with the correct state for the upcoming page load. michael@0: // michael@0: // Expected to be fixed by bug 915825. michael@0: hideHomePager(url); michael@0: michael@0: // Don't do anything if the user entered an empty URL. michael@0: if (TextUtils.isEmpty(url)) { michael@0: return; michael@0: } michael@0: michael@0: // If the URL doesn't look like a search query, just load it. michael@0: if (!StringUtils.isSearchQuery(url, true)) { michael@0: Tabs.getInstance().loadUrl(url, Tabs.LOADURL_USER_ENTERED); michael@0: michael@0: Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL); michael@0: return; michael@0: } michael@0: michael@0: // Otherwise, check for a bookmark keyword. michael@0: ThreadUtils.postToBackgroundThread(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: final String keyword; michael@0: final String keywordSearch; michael@0: michael@0: final int index = url.indexOf(" "); michael@0: if (index == -1) { michael@0: keyword = url; michael@0: keywordSearch = ""; michael@0: } else { michael@0: keyword = url.substring(0, index); michael@0: keywordSearch = url.substring(index + 1); michael@0: } michael@0: michael@0: final String keywordUrl = BrowserDB.getUrlForKeyword(getContentResolver(), keyword); michael@0: michael@0: // If there isn't a bookmark keyword, load the url. This may result in a query michael@0: // using the default search engine. michael@0: if (TextUtils.isEmpty(keywordUrl)) { michael@0: Tabs.getInstance().loadUrl(url, Tabs.LOADURL_USER_ENTERED); michael@0: Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL); michael@0: return; michael@0: } michael@0: michael@0: recordSearch(null, "barkeyword"); michael@0: michael@0: // Otherwise, construct a search query from the bookmark keyword. michael@0: final String searchUrl = keywordUrl.replace("%s", URLEncoder.encode(keywordSearch)); michael@0: Tabs.getInstance().loadUrl(searchUrl, Tabs.LOADURL_USER_ENTERED); michael@0: Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, "", "keyword"); michael@0: } michael@0: }); michael@0: } michael@0: michael@0: /** michael@0: * Record in Health Report that a search has occurred. michael@0: * michael@0: * @param engine michael@0: * a search engine instance. Can be null. michael@0: * @param where michael@0: * where the search was initialized; one of the values in michael@0: * {@link BrowserHealthRecorder#SEARCH_LOCATIONS}. michael@0: */ michael@0: private static void recordSearch(SearchEngine engine, String where) { michael@0: Log.i(LOGTAG, "Recording search: " + michael@0: ((engine == null) ? "null" : engine.name) + michael@0: ", " + where); michael@0: try { michael@0: String identifier = (engine == null) ? "other" : engine.getEngineIdentifier(); michael@0: JSONObject message = new JSONObject(); michael@0: message.put("type", BrowserHealthRecorder.EVENT_SEARCH); michael@0: message.put("location", where); michael@0: message.put("identifier", identifier); michael@0: GeckoAppShell.getEventDispatcher().dispatchEvent(message, null); michael@0: } catch (Exception e) { michael@0: Log.w(LOGTAG, "Error recording search.", e); michael@0: } michael@0: } michael@0: michael@0: void filterEditingMode(String searchTerm, AutocompleteHandler handler) { michael@0: if (TextUtils.isEmpty(searchTerm)) { michael@0: hideBrowserSearch(); michael@0: } else { michael@0: showBrowserSearch(); michael@0: mBrowserSearch.filter(searchTerm, handler); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Selects the target tab for editing mode. This is expected to be the tab selected on editing michael@0: * mode entry, unless it is subsequently overridden. michael@0: * michael@0: * A background tab may be selected while editing mode is active (e.g. popups), causing the michael@0: * new url to load in the newly selected tab. Call this method on editing mode exit to michael@0: * mitigate this. michael@0: */ michael@0: private void selectTargetTabForEditingMode() { michael@0: if (mTargetTabForEditingMode != null) { michael@0: Tabs.getInstance().selectTab(mTargetTabForEditingMode); michael@0: } michael@0: michael@0: mTargetTabForEditingMode = null; michael@0: } michael@0: michael@0: /** michael@0: * Shows or hides the home pager for the given tab. michael@0: */ michael@0: private void updateHomePagerForTab(Tab tab) { michael@0: // Don't change the visibility of the home pager if we're in editing mode. michael@0: if (mBrowserToolbar.isEditing()) { michael@0: return; michael@0: } michael@0: michael@0: if (isAboutHome(tab)) { michael@0: String panelId = AboutPages.getPanelIdFromAboutHomeUrl(tab.getURL()); michael@0: if (panelId == null) { michael@0: // No panel was specified in the URL. Try loading the most recent michael@0: // home panel for this tab. michael@0: panelId = tab.getMostRecentHomePanel(); michael@0: } michael@0: showHomePager(panelId); michael@0: michael@0: if (mDynamicToolbar.isEnabled()) { michael@0: mDynamicToolbar.setVisible(true, VisibilityTransition.ANIMATE); michael@0: } michael@0: } else { michael@0: hideHomePager(); michael@0: } michael@0: } michael@0: michael@0: @Override michael@0: public void onLocaleReady(final String locale) { michael@0: super.onLocaleReady(locale); michael@0: michael@0: HomePanelsManager.getInstance().onLocaleReady(locale); michael@0: michael@0: if (mMenu != null) { michael@0: mMenu.clear(); michael@0: onCreateOptionsMenu(mMenu); michael@0: } michael@0: } michael@0: michael@0: private void showHomePager(String panelId) { michael@0: showHomePagerWithAnimator(panelId, null); michael@0: } michael@0: michael@0: private void showHomePagerWithAnimator(String panelId, PropertyAnimator animator) { michael@0: if (isHomePagerVisible()) { michael@0: // Home pager already visible, make sure it shows the correct panel. michael@0: mHomePager.showPanel(panelId); michael@0: return; michael@0: } michael@0: michael@0: // Refresh toolbar height to possibly restore the toolbar padding michael@0: refreshToolbarHeight(); michael@0: michael@0: // Show the toolbar before hiding about:home so the michael@0: // onMetricsChanged callback still works. michael@0: if (mDynamicToolbar.isEnabled()) { michael@0: mDynamicToolbar.setVisible(true, VisibilityTransition.IMMEDIATE); michael@0: } michael@0: michael@0: if (mHomePager == null) { michael@0: final ViewStub homePagerStub = (ViewStub) findViewById(R.id.home_pager_stub); michael@0: mHomePager = (HomePager) homePagerStub.inflate(); michael@0: michael@0: mHomePager.setOnPanelChangeListener(new HomePager.OnPanelChangeListener() { michael@0: @Override michael@0: public void onPanelSelected(String panelId) { michael@0: final Tab currentTab = Tabs.getInstance().getSelectedTab(); michael@0: if (currentTab != null) { michael@0: currentTab.setMostRecentHomePanel(panelId); michael@0: } michael@0: } michael@0: }); michael@0: michael@0: // Don't show the banner in guest mode. michael@0: if (!getProfile().inGuestMode()) { michael@0: final ViewStub homeBannerStub = (ViewStub) findViewById(R.id.home_banner_stub); michael@0: final HomeBanner homeBanner = (HomeBanner) homeBannerStub.inflate(); michael@0: mHomePager.setBanner(homeBanner); michael@0: michael@0: // Remove the banner from the view hierarchy if it is dismissed. michael@0: homeBanner.setOnDismissListener(new HomeBanner.OnDismissListener() { michael@0: @Override michael@0: public void onDismiss() { michael@0: mHomePager.setBanner(null); michael@0: mHomePagerContainer.removeView(homeBanner); michael@0: } michael@0: }); michael@0: } michael@0: } michael@0: michael@0: mHomePagerContainer.setVisibility(View.VISIBLE); michael@0: mHomePager.load(getSupportLoaderManager(), michael@0: getSupportFragmentManager(), michael@0: panelId, animator); michael@0: michael@0: // Hide the web content so it cannot be focused by screen readers. michael@0: hideWebContentOnPropertyAnimationEnd(animator); michael@0: } michael@0: michael@0: private void hideWebContentOnPropertyAnimationEnd(final PropertyAnimator animator) { michael@0: if (animator == null) { michael@0: hideWebContent(); michael@0: return; michael@0: } michael@0: michael@0: animator.addPropertyAnimationListener(new PropertyAnimator.PropertyAnimationListener() { michael@0: @Override michael@0: public void onPropertyAnimationStart() { michael@0: mHideWebContentOnAnimationEnd = true; michael@0: } michael@0: michael@0: @Override michael@0: public void onPropertyAnimationEnd() { michael@0: if (mHideWebContentOnAnimationEnd) { michael@0: hideWebContent(); michael@0: } michael@0: } michael@0: }); michael@0: } michael@0: michael@0: private void hideWebContent() { michael@0: // The view is set to INVISIBLE, rather than GONE, to avoid michael@0: // the additional requestLayout() call. michael@0: mLayerView.setVisibility(View.INVISIBLE); michael@0: } michael@0: michael@0: /** michael@0: * Hides the HomePager, using the url of the currently selected tab as the url to be michael@0: * loaded. michael@0: */ michael@0: private void hideHomePager() { michael@0: final Tab selectedTab = Tabs.getInstance().getSelectedTab(); michael@0: final String url = (selectedTab != null) ? selectedTab.getURL() : null; michael@0: michael@0: hideHomePager(url); michael@0: } michael@0: michael@0: /** michael@0: * Hides the HomePager. The given url should be the url of the page to be loaded, or null michael@0: * if a new page is not being loaded. michael@0: */ michael@0: private void hideHomePager(final String url) { michael@0: if (!isHomePagerVisible() || AboutPages.isAboutHome(url)) { michael@0: return; michael@0: } michael@0: michael@0: // Prevent race in hiding web content - see declaration for more info. michael@0: mHideWebContentOnAnimationEnd = false; michael@0: michael@0: // Display the previously hidden web content (which prevented screen reader access). michael@0: mLayerView.setVisibility(View.VISIBLE); michael@0: mHomePagerContainer.setVisibility(View.GONE); michael@0: michael@0: if (mHomePager != null) { michael@0: mHomePager.unload(); michael@0: } michael@0: michael@0: mBrowserToolbar.setNextFocusDownId(R.id.layer_view); michael@0: michael@0: // Refresh toolbar height to possibly restore the toolbar padding michael@0: refreshToolbarHeight(); michael@0: } michael@0: michael@0: private void showBrowserSearch() { michael@0: if (mBrowserSearch.getUserVisibleHint()) { michael@0: return; michael@0: } michael@0: michael@0: mBrowserSearchContainer.setVisibility(View.VISIBLE); michael@0: michael@0: // Prevent overdraw by hiding the underlying HomePager View. michael@0: mHomePager.setVisibility(View.INVISIBLE); michael@0: michael@0: final FragmentManager fm = getSupportFragmentManager(); michael@0: michael@0: // In certain situations, showBrowserSearch() can be called immediately after hideBrowserSearch() michael@0: // (see bug 925012). Because of an Android bug (http://code.google.com/p/android/issues/detail?id=61179), michael@0: // calling FragmentTransaction#add immediately after FragmentTransaction#remove won't add the fragment's michael@0: // view to the layout. Calling FragmentManager#executePendingTransactions before re-adding the fragment michael@0: // prevents this issue. michael@0: fm.executePendingTransactions(); michael@0: michael@0: fm.beginTransaction().add(R.id.search_container, mBrowserSearch, BROWSER_SEARCH_TAG).commitAllowingStateLoss(); michael@0: mBrowserSearch.setUserVisibleHint(true); michael@0: } michael@0: michael@0: private void hideBrowserSearch() { michael@0: if (!mBrowserSearch.getUserVisibleHint()) { michael@0: return; michael@0: } michael@0: michael@0: // To prevent overdraw, the HomePager is hidden when BrowserSearch is displayed: michael@0: // reverse that. michael@0: mHomePager.setVisibility(View.VISIBLE); michael@0: michael@0: mBrowserSearchContainer.setVisibility(View.INVISIBLE); michael@0: michael@0: getSupportFragmentManager().beginTransaction() michael@0: .remove(mBrowserSearch).commitAllowingStateLoss(); michael@0: mBrowserSearch.setUserVisibleHint(false); michael@0: } michael@0: michael@0: private class HideTabsTouchListener implements TouchEventInterceptor { michael@0: private boolean mIsHidingTabs = false; michael@0: michael@0: @Override michael@0: public boolean onInterceptTouchEvent(View view, MotionEvent event) { michael@0: // We need to account for scroll state for the touched view otherwise michael@0: // tapping on an "empty" part of the view will still be considered a michael@0: // valid touch event. michael@0: if (view.getScrollX() != 0 || view.getScrollY() != 0) { michael@0: Rect rect = new Rect(); michael@0: view.getHitRect(rect); michael@0: rect.offset(-view.getScrollX(), -view.getScrollY()); michael@0: michael@0: int[] viewCoords = new int[2]; michael@0: view.getLocationOnScreen(viewCoords); michael@0: michael@0: int x = (int) event.getRawX() - viewCoords[0]; michael@0: int y = (int) event.getRawY() - viewCoords[1]; michael@0: michael@0: if (!rect.contains(x, y)) michael@0: return false; michael@0: } michael@0: michael@0: // If the tab tray is showing, hide the tab tray and don't send the event to content. michael@0: if (event.getActionMasked() == MotionEvent.ACTION_DOWN && autoHideTabs()) { michael@0: mIsHidingTabs = true; michael@0: return true; michael@0: } michael@0: return false; michael@0: } michael@0: michael@0: @Override michael@0: public boolean onTouch(View view, MotionEvent event) { michael@0: if (mIsHidingTabs) { michael@0: // Keep consuming events until the gesture finishes. michael@0: int action = event.getActionMasked(); michael@0: if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { michael@0: mIsHidingTabs = false; michael@0: } michael@0: return true; michael@0: } michael@0: return false; michael@0: } michael@0: } michael@0: michael@0: private static Menu findParentMenu(Menu menu, MenuItem item) { michael@0: final int itemId = item.getItemId(); michael@0: michael@0: final int count = (menu != null) ? menu.size() : 0; michael@0: for (int i = 0; i < count; i++) { michael@0: MenuItem menuItem = menu.getItem(i); michael@0: if (menuItem.getItemId() == itemId) { michael@0: return menu; michael@0: } michael@0: if (menuItem.hasSubMenu()) { michael@0: Menu parent = findParentMenu(menuItem.getSubMenu(), item); michael@0: if (parent != null) { michael@0: return parent; michael@0: } michael@0: } michael@0: } michael@0: michael@0: return null; michael@0: } michael@0: michael@0: /** michael@0: * Add the provided item to the provided menu, which should be michael@0: * the root (mMenu). michael@0: */ michael@0: private void addAddonMenuItemToMenu(final Menu menu, final MenuItemInfo info) { michael@0: info.added = true; michael@0: michael@0: final Menu destination; michael@0: if (info.parent == 0) { michael@0: destination = menu; michael@0: } else if (info.parent == GECKO_TOOLS_MENU) { michael@0: MenuItem tools = menu.findItem(R.id.tools); michael@0: destination = tools != null ? tools.getSubMenu() : menu; michael@0: } else { michael@0: MenuItem parent = menu.findItem(info.parent); michael@0: if (parent == null) { michael@0: return; michael@0: } michael@0: michael@0: Menu parentMenu = findParentMenu(menu, parent); michael@0: michael@0: if (!parent.hasSubMenu()) { michael@0: parentMenu.removeItem(parent.getItemId()); michael@0: destination = parentMenu.addSubMenu(Menu.NONE, parent.getItemId(), Menu.NONE, parent.getTitle()); michael@0: if (parent.getIcon() != null) { michael@0: ((SubMenu) destination).getItem().setIcon(parent.getIcon()); michael@0: } michael@0: } else { michael@0: destination = parent.getSubMenu(); michael@0: } michael@0: } michael@0: michael@0: MenuItem item = destination.add(Menu.NONE, info.id, Menu.NONE, info.label); michael@0: michael@0: item.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { michael@0: @Override michael@0: public boolean onMenuItemClick(MenuItem item) { michael@0: Log.i(LOGTAG, "Menu item clicked"); michael@0: GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Menu:Clicked", Integer.toString(info.id - ADDON_MENU_OFFSET))); michael@0: return true; michael@0: } michael@0: }); michael@0: michael@0: if (info.icon == null) { michael@0: item.setIcon(R.drawable.ic_menu_addons_filler); michael@0: } else { michael@0: final int id = info.id; michael@0: BitmapUtils.getDrawable(this, info.icon, new BitmapUtils.BitmapLoader() { michael@0: @Override michael@0: public void onBitmapFound(Drawable d) { michael@0: // TODO: why do we re-find the item? michael@0: MenuItem item = destination.findItem(id); michael@0: if (item == null) { michael@0: return; michael@0: } michael@0: if (d == null) { michael@0: item.setIcon(R.drawable.ic_menu_addons_filler); michael@0: return; michael@0: } michael@0: item.setIcon(d); michael@0: } michael@0: }); michael@0: } michael@0: michael@0: item.setCheckable(info.checkable); michael@0: item.setChecked(info.checked); michael@0: item.setEnabled(info.enabled); michael@0: item.setVisible(info.visible); michael@0: } michael@0: michael@0: private void addAddonMenuItem(final MenuItemInfo info) { michael@0: if (mAddonMenuItemsCache == null) { michael@0: mAddonMenuItemsCache = new Vector(); michael@0: } michael@0: michael@0: // Mark it as added if the menu was ready. michael@0: info.added = (mMenu != null); michael@0: michael@0: // Always cache so we can rebuild after a locale switch. michael@0: mAddonMenuItemsCache.add(info); michael@0: michael@0: if (mMenu == null) { michael@0: return; michael@0: } michael@0: michael@0: addAddonMenuItemToMenu(mMenu, info); michael@0: } michael@0: michael@0: private void removeAddonMenuItem(int id) { michael@0: // Remove add-on menu item from cache, if available. michael@0: if (mAddonMenuItemsCache != null && !mAddonMenuItemsCache.isEmpty()) { michael@0: for (MenuItemInfo item : mAddonMenuItemsCache) { michael@0: if (item.id == id) { michael@0: mAddonMenuItemsCache.remove(item); michael@0: break; michael@0: } michael@0: } michael@0: } michael@0: michael@0: if (mMenu == null) michael@0: return; michael@0: michael@0: MenuItem menuItem = mMenu.findItem(id); michael@0: if (menuItem != null) michael@0: mMenu.removeItem(id); michael@0: } michael@0: michael@0: private void updateAddonMenuItem(int id, JSONObject options) { michael@0: // Set attribute for the menu item in cache, if available michael@0: if (mAddonMenuItemsCache != null && !mAddonMenuItemsCache.isEmpty()) { michael@0: for (MenuItemInfo item : mAddonMenuItemsCache) { michael@0: if (item.id == id) { michael@0: item.label = options.optString("name", item.label); michael@0: item.checkable = options.optBoolean("checkable", item.checkable); michael@0: item.checked = options.optBoolean("checked", item.checked); michael@0: item.enabled = options.optBoolean("enabled", item.enabled); michael@0: item.visible = options.optBoolean("visible", item.visible); michael@0: item.added = (mMenu != null); michael@0: break; michael@0: } michael@0: } michael@0: } michael@0: michael@0: if (mMenu == null) { michael@0: return; michael@0: } michael@0: michael@0: MenuItem menuItem = mMenu.findItem(id); michael@0: if (menuItem != null) { michael@0: menuItem.setTitle(options.optString("name", menuItem.getTitle().toString())); michael@0: menuItem.setCheckable(options.optBoolean("checkable", menuItem.isCheckable())); michael@0: menuItem.setChecked(options.optBoolean("checked", menuItem.isChecked())); michael@0: menuItem.setEnabled(options.optBoolean("enabled", menuItem.isEnabled())); michael@0: menuItem.setVisible(options.optBoolean("visible", menuItem.isVisible())); michael@0: } michael@0: } michael@0: michael@0: @Override michael@0: public boolean onCreateOptionsMenu(Menu menu) { michael@0: // Sets mMenu = menu. michael@0: super.onCreateOptionsMenu(menu); michael@0: michael@0: // Inform the menu about the action-items bar. michael@0: if (menu instanceof GeckoMenu && michael@0: HardwareUtils.isTablet()) { michael@0: ((GeckoMenu) menu).setActionItemBarPresenter(mBrowserToolbar); michael@0: } michael@0: michael@0: MenuInflater inflater = getMenuInflater(); michael@0: inflater.inflate(R.menu.browser_app_menu, mMenu); michael@0: michael@0: // Add add-on menu items, if any exist. michael@0: if (mAddonMenuItemsCache != null && !mAddonMenuItemsCache.isEmpty()) { michael@0: for (MenuItemInfo item : mAddonMenuItemsCache) { michael@0: addAddonMenuItemToMenu(mMenu, item); michael@0: } michael@0: } michael@0: michael@0: // Action providers are available only ICS+. michael@0: if (Build.VERSION.SDK_INT >= 14) { michael@0: GeckoMenuItem share = (GeckoMenuItem) mMenu.findItem(R.id.share); michael@0: GeckoActionProvider provider = GeckoActionProvider.getForType(GeckoActionProvider.DEFAULT_MIME_TYPE, this); michael@0: share.setActionProvider(provider); michael@0: } michael@0: michael@0: return true; michael@0: } michael@0: michael@0: @Override michael@0: public void openOptionsMenu() { michael@0: if (!hasTabsSideBar() && areTabsShown()) michael@0: return; michael@0: michael@0: // Scroll custom menu to the top michael@0: if (mMenuPanel != null) michael@0: mMenuPanel.scrollTo(0, 0); michael@0: michael@0: if (!mBrowserToolbar.openOptionsMenu()) michael@0: super.openOptionsMenu(); michael@0: michael@0: if (mDynamicToolbar.isEnabled()) { michael@0: mDynamicToolbar.setVisible(true, VisibilityTransition.ANIMATE); michael@0: } michael@0: } michael@0: michael@0: @Override michael@0: public void closeOptionsMenu() { michael@0: if (!mBrowserToolbar.closeOptionsMenu()) michael@0: super.closeOptionsMenu(); michael@0: } michael@0: michael@0: @Override michael@0: public void setFullScreen(final boolean fullscreen) { michael@0: super.setFullScreen(fullscreen); michael@0: ThreadUtils.postToUiThread(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: if (fullscreen) { michael@0: mViewFlipper.setVisibility(View.GONE); michael@0: if (mDynamicToolbar.isEnabled()) { michael@0: mDynamicToolbar.setVisible(false, VisibilityTransition.IMMEDIATE); michael@0: mLayerView.getLayerMarginsAnimator().setMaxMargins(0, 0, 0, 0); michael@0: } else { michael@0: setToolbarMargin(0); michael@0: } michael@0: } else { michael@0: mViewFlipper.setVisibility(View.VISIBLE); michael@0: if (mDynamicToolbar.isEnabled()) { michael@0: mDynamicToolbar.setVisible(true, VisibilityTransition.IMMEDIATE); michael@0: mLayerView.getLayerMarginsAnimator().setMaxMargins(0, mToolbarHeight, 0, 0); michael@0: } michael@0: } michael@0: } michael@0: }); michael@0: } michael@0: michael@0: @Override michael@0: public boolean onPrepareOptionsMenu(Menu aMenu) { michael@0: if (aMenu == null) michael@0: return false; michael@0: michael@0: if (!GeckoThread.checkLaunchState(GeckoThread.LaunchState.GeckoRunning)) michael@0: aMenu.findItem(R.id.settings).setEnabled(false); michael@0: michael@0: Tab tab = Tabs.getInstance().getSelectedTab(); michael@0: MenuItem bookmark = aMenu.findItem(R.id.bookmark); michael@0: MenuItem back = aMenu.findItem(R.id.back); michael@0: MenuItem forward = aMenu.findItem(R.id.forward); michael@0: MenuItem share = aMenu.findItem(R.id.share); michael@0: MenuItem saveAsPDF = aMenu.findItem(R.id.save_as_pdf); michael@0: MenuItem charEncoding = aMenu.findItem(R.id.char_encoding); michael@0: MenuItem findInPage = aMenu.findItem(R.id.find_in_page); michael@0: MenuItem desktopMode = aMenu.findItem(R.id.desktop_mode); michael@0: MenuItem enterGuestMode = aMenu.findItem(R.id.new_guest_session); michael@0: MenuItem exitGuestMode = aMenu.findItem(R.id.exit_guest_session); michael@0: michael@0: // Only show the "Quit" menu item on pre-ICS or television devices. michael@0: // In ICS+, it's easy to kill an app through the task switcher. michael@0: aMenu.findItem(R.id.quit).setVisible(Build.VERSION.SDK_INT < 14 || HardwareUtils.isTelevision()); michael@0: michael@0: if (tab == null || tab.getURL() == null) { michael@0: bookmark.setEnabled(false); michael@0: back.setEnabled(false); michael@0: forward.setEnabled(false); michael@0: share.setEnabled(false); michael@0: saveAsPDF.setEnabled(false); michael@0: findInPage.setEnabled(false); michael@0: michael@0: // NOTE: Use MenuUtils.safeSetEnabled because some actions might michael@0: // be on the BrowserToolbar context menu michael@0: MenuUtils.safeSetEnabled(aMenu, R.id.page, false); michael@0: MenuUtils.safeSetEnabled(aMenu, R.id.subscribe, false); michael@0: MenuUtils.safeSetEnabled(aMenu, R.id.add_search_engine, false); michael@0: MenuUtils.safeSetEnabled(aMenu, R.id.site_settings, false); michael@0: MenuUtils.safeSetEnabled(aMenu, R.id.add_to_launcher, false); michael@0: michael@0: return true; michael@0: } michael@0: michael@0: bookmark.setEnabled(!AboutPages.isAboutReader(tab.getURL())); michael@0: bookmark.setVisible(!GeckoProfile.get(this).inGuestMode()); michael@0: bookmark.setCheckable(true); michael@0: bookmark.setChecked(tab.isBookmark()); michael@0: bookmark.setIcon(tab.isBookmark() ? R.drawable.ic_menu_bookmark_remove : R.drawable.ic_menu_bookmark_add); michael@0: michael@0: back.setEnabled(tab.canDoBack()); michael@0: forward.setEnabled(tab.canDoForward()); michael@0: desktopMode.setChecked(tab.getDesktopMode()); michael@0: desktopMode.setIcon(tab.getDesktopMode() ? R.drawable.ic_menu_desktop_mode_on : R.drawable.ic_menu_desktop_mode_off); michael@0: michael@0: String url = tab.getURL(); michael@0: if (AboutPages.isAboutReader(url)) { michael@0: String urlFromReader = ReaderModeUtils.getUrlFromAboutReader(url); michael@0: if (urlFromReader != null) { michael@0: url = urlFromReader; michael@0: } michael@0: } michael@0: michael@0: // Disable share menuitem for about:, chrome:, file:, and resource: URIs michael@0: String scheme = Uri.parse(url).getScheme(); michael@0: share.setVisible(!GeckoProfile.get(this).inGuestMode()); michael@0: share.setEnabled(!(scheme.equals("about") || scheme.equals("chrome") || michael@0: scheme.equals("file") || scheme.equals("resource"))); michael@0: michael@0: // NOTE: Use MenuUtils.safeSetEnabled because some actions might michael@0: // be on the BrowserToolbar context menu michael@0: MenuUtils.safeSetEnabled(aMenu, R.id.page, !isAboutHome(tab)); michael@0: MenuUtils.safeSetEnabled(aMenu, R.id.subscribe, tab.hasFeeds()); michael@0: MenuUtils.safeSetEnabled(aMenu, R.id.add_search_engine, tab.hasOpenSearch()); michael@0: MenuUtils.safeSetEnabled(aMenu, R.id.site_settings, !isAboutHome(tab)); michael@0: MenuUtils.safeSetEnabled(aMenu, R.id.add_to_launcher, !isAboutHome(tab)); michael@0: michael@0: // Action providers are available only ICS+. michael@0: if (Build.VERSION.SDK_INT >= 14) { michael@0: final GeckoActionProvider provider = ((GeckoMenuItem) share).getGeckoActionProvider(); michael@0: if (provider != null) { michael@0: Intent shareIntent = provider.getIntent(); michael@0: michael@0: // For efficiency, the provider's intent is only set once michael@0: if (shareIntent == null) { michael@0: shareIntent = new Intent(Intent.ACTION_SEND); michael@0: shareIntent.setType("text/plain"); michael@0: provider.setIntent(shareIntent); michael@0: } michael@0: michael@0: // Replace the existing intent's extras michael@0: shareIntent.putExtra(Intent.EXTRA_TEXT, url); michael@0: shareIntent.putExtra(Intent.EXTRA_SUBJECT, tab.getDisplayTitle()); michael@0: shareIntent.putExtra(Intent.EXTRA_TITLE, tab.getDisplayTitle()); michael@0: michael@0: // Clear the existing thumbnail extras so we don't share an old thumbnail. michael@0: shareIntent.removeExtra("share_screenshot_uri"); michael@0: michael@0: // Include the thumbnail of the page being shared. michael@0: BitmapDrawable drawable = tab.getThumbnail(); michael@0: if (drawable != null) { michael@0: Bitmap thumbnail = drawable.getBitmap(); michael@0: michael@0: // Kobo uses a custom intent extra for sharing thumbnails. michael@0: if (Build.MANUFACTURER.equals("Kobo") && thumbnail != null) { michael@0: File cacheDir = getExternalCacheDir(); michael@0: michael@0: if (cacheDir != null) { michael@0: File outFile = new File(cacheDir, "thumbnail.png"); michael@0: michael@0: try { michael@0: java.io.FileOutputStream out = new java.io.FileOutputStream(outFile); michael@0: thumbnail.compress(Bitmap.CompressFormat.PNG, 90, out); michael@0: } catch (FileNotFoundException e) { michael@0: Log.e(LOGTAG, "File not found", e); michael@0: } michael@0: michael@0: shareIntent.putExtra("share_screenshot_uri", Uri.parse(outFile.getPath())); michael@0: } michael@0: } michael@0: } michael@0: } michael@0: } michael@0: michael@0: // Disable save as PDF for about:home and xul pages. michael@0: saveAsPDF.setEnabled(!(isAboutHome(tab) || michael@0: tab.getContentType().equals("application/vnd.mozilla.xul+xml"))); michael@0: michael@0: // Disable find in page for about:home, since it won't work on Java content. michael@0: findInPage.setEnabled(!isAboutHome(tab)); michael@0: michael@0: charEncoding.setVisible(GeckoPreferences.getCharEncodingState()); michael@0: michael@0: if (mProfile.inGuestMode()) michael@0: exitGuestMode.setVisible(true); michael@0: else michael@0: enterGuestMode.setVisible(true); michael@0: michael@0: return true; michael@0: } michael@0: michael@0: @Override michael@0: public boolean onOptionsItemSelected(MenuItem item) { michael@0: Tab tab = null; michael@0: Intent intent = null; michael@0: michael@0: final int itemId = item.getItemId(); michael@0: michael@0: // Track the menu action. We don't know much about the context, but we can use this to determine michael@0: // the frequency of use for various actions. michael@0: Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.MENU, getResources().getResourceEntryName(itemId)); michael@0: michael@0: if (itemId == R.id.bookmark) { michael@0: tab = Tabs.getInstance().getSelectedTab(); michael@0: if (tab != null) { michael@0: if (item.isChecked()) { michael@0: tab.removeBookmark(); michael@0: Toast.makeText(this, R.string.bookmark_removed, Toast.LENGTH_SHORT).show(); michael@0: item.setIcon(R.drawable.ic_menu_bookmark_add); michael@0: } else { michael@0: tab.addBookmark(); michael@0: getButtonToast().show(false, michael@0: getResources().getString(R.string.bookmark_added), michael@0: getResources().getString(R.string.bookmark_options), michael@0: null, michael@0: new ButtonToast.ToastListener() { michael@0: @Override michael@0: public void onButtonClicked() { michael@0: showBookmarkDialog(); michael@0: } michael@0: michael@0: @Override michael@0: public void onToastHidden(ButtonToast.ReasonHidden reason) { } michael@0: }); michael@0: item.setIcon(R.drawable.ic_menu_bookmark_remove); michael@0: } michael@0: } michael@0: return true; michael@0: } michael@0: michael@0: if (itemId == R.id.share) { michael@0: shareCurrentUrl(); michael@0: return true; michael@0: } michael@0: michael@0: if (itemId == R.id.reload) { michael@0: tab = Tabs.getInstance().getSelectedTab(); michael@0: if (tab != null) michael@0: tab.doReload(); michael@0: return true; michael@0: } michael@0: michael@0: if (itemId == R.id.back) { michael@0: tab = Tabs.getInstance().getSelectedTab(); michael@0: if (tab != null) michael@0: tab.doBack(); michael@0: return true; michael@0: } michael@0: michael@0: if (itemId == R.id.forward) { michael@0: tab = Tabs.getInstance().getSelectedTab(); michael@0: if (tab != null) michael@0: tab.doForward(); michael@0: return true; michael@0: } michael@0: michael@0: if (itemId == R.id.save_as_pdf) { michael@0: GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("SaveAs:PDF", null)); michael@0: return true; michael@0: } michael@0: michael@0: if (itemId == R.id.settings) { michael@0: intent = new Intent(this, GeckoPreferences.class); michael@0: startActivity(intent); michael@0: return true; michael@0: } michael@0: michael@0: if (itemId == R.id.addons) { michael@0: Tabs.getInstance().loadUrlInTab(AboutPages.ADDONS); michael@0: return true; michael@0: } michael@0: michael@0: if (itemId == R.id.apps) { michael@0: Tabs.getInstance().loadUrlInTab(AboutPages.APPS); michael@0: return true; michael@0: } michael@0: michael@0: if (itemId == R.id.downloads) { michael@0: Tabs.getInstance().loadUrlInTab(AboutPages.DOWNLOADS); michael@0: return true; michael@0: } michael@0: michael@0: if (itemId == R.id.char_encoding) { michael@0: GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("CharEncoding:Get", null)); michael@0: return true; michael@0: } michael@0: michael@0: if (itemId == R.id.find_in_page) { michael@0: mFindInPageBar.show(); michael@0: return true; michael@0: } michael@0: michael@0: if (itemId == R.id.desktop_mode) { michael@0: Tab selectedTab = Tabs.getInstance().getSelectedTab(); michael@0: if (selectedTab == null) michael@0: return true; michael@0: JSONObject args = new JSONObject(); michael@0: try { michael@0: args.put("desktopMode", !item.isChecked()); michael@0: args.put("tabId", selectedTab.getId()); michael@0: } catch (JSONException e) { michael@0: Log.e(LOGTAG, "error building json arguments"); michael@0: } michael@0: GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("DesktopMode:Change", args.toString())); michael@0: return true; michael@0: } michael@0: michael@0: if (itemId == R.id.new_tab) { michael@0: addTab(); michael@0: return true; michael@0: } michael@0: michael@0: if (itemId == R.id.new_private_tab) { michael@0: addPrivateTab(); michael@0: return true; michael@0: } michael@0: michael@0: if (itemId == R.id.new_guest_session) { michael@0: showGuestModeDialog(GuestModeDialog.ENTERING); michael@0: return true; michael@0: } michael@0: michael@0: if (itemId == R.id.exit_guest_session) { michael@0: showGuestModeDialog(GuestModeDialog.LEAVING); michael@0: return true; michael@0: } michael@0: michael@0: // We have a few menu items that can also be in the context menu. If michael@0: // we have not already handled the item, give the context menu handler michael@0: // a chance. michael@0: if (onContextItemSelected(item)) { michael@0: return true; michael@0: } michael@0: michael@0: return super.onOptionsItemSelected(item); michael@0: } michael@0: michael@0: private void showGuestModeDialog(final GuestModeDialog type) { michael@0: final Prompt ps = new Prompt(this, new Prompt.PromptCallback() { michael@0: @Override michael@0: public void onPromptFinished(String result) { michael@0: try { michael@0: int itemId = new JSONObject(result).getInt("button"); michael@0: if (itemId == 0) { michael@0: String args = ""; michael@0: if (type == GuestModeDialog.ENTERING) { michael@0: args = GUEST_BROWSING_ARG; michael@0: } else { michael@0: GeckoProfile.leaveGuestSession(BrowserApp.this); michael@0: } michael@0: doRestart(args); michael@0: GeckoAppShell.systemExit(); michael@0: } michael@0: } catch(JSONException ex) { michael@0: Log.e(LOGTAG, "Exception reading guest mode prompt result", ex); michael@0: } michael@0: } michael@0: }); michael@0: michael@0: Resources res = getResources(); michael@0: ps.setButtons(new String[] { michael@0: res.getString(R.string.guest_session_dialog_continue), michael@0: res.getString(R.string.guest_session_dialog_cancel) michael@0: }); michael@0: michael@0: int titleString = 0; michael@0: int msgString = 0; michael@0: if (type == GuestModeDialog.ENTERING) { michael@0: titleString = R.string.new_guest_session_title; michael@0: msgString = R.string.new_guest_session_text; michael@0: } else { michael@0: titleString = R.string.exit_guest_session_title; michael@0: msgString = R.string.exit_guest_session_text; michael@0: } michael@0: michael@0: ps.show(res.getString(titleString), res.getString(msgString), null, ListView.CHOICE_MODE_NONE); michael@0: } michael@0: michael@0: /** michael@0: * This will detect if the key pressed is back. If so, will show the history. michael@0: */ michael@0: @Override michael@0: public boolean onKeyLongPress(int keyCode, KeyEvent event) { michael@0: if (keyCode == KeyEvent.KEYCODE_BACK) { michael@0: Tab tab = Tabs.getInstance().getSelectedTab(); michael@0: if (tab != null) { michael@0: return tab.showAllHistory(); michael@0: } michael@0: } michael@0: return super.onKeyLongPress(keyCode, event); michael@0: } michael@0: michael@0: /* michael@0: * If the app has been launched a certain number of times, and we haven't asked for feedback before, michael@0: * open a new tab with about:feedback when launching the app from the icon shortcut. michael@0: */ michael@0: @Override michael@0: protected void onNewIntent(Intent intent) { michael@0: super.onNewIntent(intent); michael@0: michael@0: String action = intent.getAction(); michael@0: michael@0: if (AppConstants.MOZ_ANDROID_BEAM && Build.VERSION.SDK_INT >= 10 && NfcAdapter.ACTION_NDEF_DISCOVERED.equals(action)) { michael@0: String uri = intent.getDataString(); michael@0: GeckoAppShell.sendEventToGecko(GeckoEvent.createURILoadEvent(uri)); michael@0: } michael@0: michael@0: if (!mInitialized) { michael@0: return; michael@0: } michael@0: michael@0: if (Intent.ACTION_VIEW.equals(action)) { michael@0: // Dismiss editing mode if the user is loading a URL from an external app. michael@0: mBrowserToolbar.cancelEdit(); michael@0: michael@0: Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.INTENT); michael@0: return; michael@0: } michael@0: michael@0: // Only solicit feedback when the app has been launched from the icon shortcut. michael@0: if (!Intent.ACTION_MAIN.equals(action)) { michael@0: return; michael@0: } michael@0: michael@0: (new UiAsyncTask(ThreadUtils.getBackgroundHandler()) { michael@0: @Override michael@0: public synchronized Boolean doInBackground(Void... params) { michael@0: // Check to see how many times the app has been launched. michael@0: SharedPreferences settings = getPreferences(Activity.MODE_PRIVATE); michael@0: String keyName = getPackageName() + ".feedback_launch_count"; michael@0: int launchCount = settings.getInt(keyName, 0); michael@0: if (launchCount >= FEEDBACK_LAUNCH_COUNT) michael@0: return false; michael@0: michael@0: // Increment the launch count and store the new value. michael@0: launchCount++; michael@0: settings.edit().putInt(keyName, launchCount).commit(); michael@0: michael@0: // If we've reached our magic number, show the feedback page. michael@0: return launchCount == FEEDBACK_LAUNCH_COUNT; michael@0: } michael@0: michael@0: @Override michael@0: public void onPostExecute(Boolean shouldShowFeedbackPage) { michael@0: if (shouldShowFeedbackPage) michael@0: GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Feedback:Show", null)); michael@0: } michael@0: }).execute(); michael@0: } michael@0: michael@0: @Override michael@0: protected NotificationClient makeNotificationClient() { michael@0: // The service is local to Fennec, so we can use it to keep michael@0: // Fennec alive during downloads. michael@0: return new ServiceNotificationClient(getApplicationContext()); michael@0: } michael@0: michael@0: private void resetFeedbackLaunchCount() { michael@0: ThreadUtils.postToBackgroundThread(new Runnable() { michael@0: @Override michael@0: public synchronized void run() { michael@0: SharedPreferences settings = getPreferences(Activity.MODE_PRIVATE); michael@0: settings.edit().putInt(getPackageName() + ".feedback_launch_count", 0).commit(); michael@0: } michael@0: }); michael@0: } michael@0: michael@0: private void getLastUrl() { michael@0: (new UiAsyncTask(ThreadUtils.getBackgroundHandler()) { michael@0: @Override michael@0: public synchronized String doInBackground(Void... params) { michael@0: // Get the most recent URL stored in browser history. michael@0: String url = ""; michael@0: Cursor c = null; michael@0: try { michael@0: c = BrowserDB.getRecentHistory(getContentResolver(), 1); michael@0: if (c.moveToFirst()) { michael@0: url = c.getString(c.getColumnIndexOrThrow(Combined.URL)); michael@0: } michael@0: } finally { michael@0: if (c != null) michael@0: c.close(); michael@0: } michael@0: return url; michael@0: } michael@0: michael@0: @Override michael@0: public void onPostExecute(String url) { michael@0: // Don't bother sending a message if there is no URL. michael@0: if (url.length() > 0) michael@0: GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Feedback:LastUrl", url)); michael@0: } michael@0: }).execute(); michael@0: } michael@0: michael@0: // HomePager.OnNewTabsListener michael@0: @Override michael@0: public void onNewTabs(String[] urls) { michael@0: final EnumSet flags = EnumSet.of(OnUrlOpenListener.Flags.ALLOW_SWITCH_TO_TAB); michael@0: michael@0: for (String url : urls) { michael@0: if (!maybeSwitchToTab(url, flags)) { michael@0: openUrlAndStopEditing(url, true); michael@0: } michael@0: } michael@0: } michael@0: michael@0: // HomePager.OnUrlOpenListener michael@0: @Override michael@0: public void onUrlOpen(String url, EnumSet flags) { michael@0: if (flags.contains(OnUrlOpenListener.Flags.OPEN_WITH_INTENT)) { michael@0: Intent intent = new Intent(Intent.ACTION_VIEW); michael@0: intent.setData(Uri.parse(url)); michael@0: startActivity(intent); michael@0: } else if (!maybeSwitchToTab(url, flags)) { michael@0: openUrlAndStopEditing(url); michael@0: } michael@0: } michael@0: michael@0: // BrowserSearch.OnSearchListener michael@0: @Override michael@0: public void onSearch(SearchEngine engine, String text) { michael@0: recordSearch(engine, "barsuggest"); michael@0: openUrlAndStopEditing(text, engine.name); michael@0: } michael@0: michael@0: // BrowserSearch.OnEditSuggestionListener michael@0: @Override michael@0: public void onEditSuggestion(String suggestion) { michael@0: mBrowserToolbar.onEditSuggestion(suggestion); michael@0: } michael@0: michael@0: @Override michael@0: public int getLayout() { return R.layout.gecko_app; } michael@0: michael@0: @Override michael@0: protected String getDefaultProfileName() throws NoMozillaDirectoryException { michael@0: return GeckoProfile.getDefaultProfileName(this); michael@0: } michael@0: michael@0: /** michael@0: * Launch UI that lets the user update Firefox. michael@0: * michael@0: * This depends on the current channel: Release and Beta both direct to the michael@0: * Google Play Store. If updating is enabled, Aurora, Nightly, and custom michael@0: * builds open about:, which provides an update interface. michael@0: * michael@0: * If updating is not enabled, this simply logs an error. michael@0: * michael@0: * @return true if update UI was launched. michael@0: */ michael@0: protected boolean handleUpdaterLaunch() { michael@0: if (AppConstants.RELEASE_BUILD) { michael@0: Intent intent = new Intent(Intent.ACTION_VIEW); michael@0: intent.setData(Uri.parse("market://details?id=" + getPackageName())); michael@0: startActivity(intent); michael@0: return true; michael@0: } michael@0: michael@0: if (AppConstants.MOZ_UPDATER) { michael@0: Tabs.getInstance().loadUrlInTab(AboutPages.UPDATER); michael@0: return true; michael@0: } michael@0: michael@0: Log.w(LOGTAG, "No candidate updater found; ignoring launch request."); michael@0: return false; michael@0: } michael@0: michael@0: /* Implementing ActionModeCompat.Presenter */ michael@0: @Override michael@0: public void startActionModeCompat(final ActionModeCompat.Callback callback) { michael@0: // If actionMode is null, we're not currently showing one. Flip to the action mode view michael@0: if (mActionMode == null) { michael@0: mViewFlipper.showNext(); michael@0: LayerMarginsAnimator margins = mLayerView.getLayerMarginsAnimator(); michael@0: michael@0: // If the toolbar is dynamic and not currently showing, just slide it in michael@0: if (mDynamicToolbar.isEnabled() && !margins.areMarginsShown()) { michael@0: margins.setMaxMargins(0, mViewFlipper.getHeight(), 0, 0); michael@0: mDynamicToolbar.setVisible(true, VisibilityTransition.ANIMATE); michael@0: mShowActionModeEndAnimation = true; michael@0: } else { michael@0: // Otherwise, we animate the actionbar itself michael@0: mActionBar.animateIn(); michael@0: } michael@0: michael@0: mDynamicToolbar.setPinned(true, PinReason.ACTION_MODE); michael@0: } else { michael@0: // Otherwise, we're already showing an action mode. Just finish it and show the new one michael@0: mActionMode.finish(); michael@0: } michael@0: michael@0: mActionMode = new ActionModeCompat(BrowserApp.this, callback, mActionBar); michael@0: if (callback.onCreateActionMode(mActionMode, mActionMode.getMenu())) { michael@0: mActionMode.invalidate(); michael@0: } michael@0: } michael@0: michael@0: /* Implementing ActionModeCompat.Presenter */ michael@0: @Override michael@0: public void endActionModeCompat() { michael@0: if (mActionMode == null) { michael@0: return; michael@0: } michael@0: michael@0: mActionMode.finish(); michael@0: mActionMode = null; michael@0: mDynamicToolbar.setPinned(false, PinReason.ACTION_MODE); michael@0: michael@0: mViewFlipper.showPrevious(); michael@0: michael@0: // Only slide the urlbar out if it was hidden when the action mode started michael@0: // Don't animate hiding it so that there's no flash as we switch back to url mode michael@0: if (mShowActionModeEndAnimation) { michael@0: mDynamicToolbar.setVisible(false, VisibilityTransition.IMMEDIATE); michael@0: mShowActionModeEndAnimation = false; michael@0: } michael@0: } michael@0: michael@0: @Override michael@0: protected HealthRecorder createHealthRecorder(final Context context, michael@0: final String profilePath, michael@0: final EventDispatcher dispatcher, michael@0: final String osLocale, michael@0: final String appLocale, michael@0: final SessionInformation previousSession) { michael@0: return new BrowserHealthRecorder(context, michael@0: GeckoSharedPrefs.forApp(context), michael@0: profilePath, michael@0: dispatcher, michael@0: osLocale, michael@0: appLocale, michael@0: previousSession); michael@0: } michael@0: }