Wed, 31 Dec 2014 07:22:50 +0100
Correct previous dual key logic pending first delivery installment.
1 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
2 * This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 package org.mozilla.gecko;
8 import java.io.File;
9 import java.io.FileNotFoundException;
10 import java.net.URLEncoder;
11 import java.util.EnumSet;
12 import java.util.Vector;
14 import org.json.JSONArray;
15 import org.json.JSONException;
16 import org.json.JSONObject;
18 import org.mozilla.gecko.DynamicToolbar.PinReason;
19 import org.mozilla.gecko.DynamicToolbar.VisibilityTransition;
20 import org.mozilla.gecko.GeckoProfileDirectories.NoMozillaDirectoryException;
21 import org.mozilla.gecko.Telemetry;
22 import org.mozilla.gecko.TelemetryContract;
23 import org.mozilla.gecko.animation.PropertyAnimator;
24 import org.mozilla.gecko.animation.ViewHelper;
25 import org.mozilla.gecko.db.BrowserContract.Combined;
26 import org.mozilla.gecko.db.BrowserContract.ReadingListItems;
27 import org.mozilla.gecko.db.BrowserDB;
28 import org.mozilla.gecko.favicons.Favicons;
29 import org.mozilla.gecko.favicons.LoadFaviconTask;
30 import org.mozilla.gecko.favicons.OnFaviconLoadedListener;
31 import org.mozilla.gecko.favicons.decoders.IconDirectoryEntry;
32 import org.mozilla.gecko.fxa.FirefoxAccounts;
33 import org.mozilla.gecko.fxa.activities.FxAccountGetStartedActivity;
34 import org.mozilla.gecko.gfx.BitmapUtils;
35 import org.mozilla.gecko.gfx.ImmutableViewportMetrics;
36 import org.mozilla.gecko.gfx.LayerMarginsAnimator;
37 import org.mozilla.gecko.gfx.LayerView;
38 import org.mozilla.gecko.health.BrowserHealthRecorder;
39 import org.mozilla.gecko.health.BrowserHealthReporter;
40 import org.mozilla.gecko.health.HealthRecorder;
41 import org.mozilla.gecko.health.SessionInformation;
42 import org.mozilla.gecko.home.BrowserSearch;
43 import org.mozilla.gecko.home.HomeBanner;
44 import org.mozilla.gecko.home.HomePanelsManager;
45 import org.mozilla.gecko.home.HomePager;
46 import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
47 import org.mozilla.gecko.home.SearchEngine;
48 import org.mozilla.gecko.menu.GeckoMenu;
49 import org.mozilla.gecko.menu.GeckoMenuItem;
50 import org.mozilla.gecko.preferences.GeckoPreferences;
51 import org.mozilla.gecko.prompts.Prompt;
52 import org.mozilla.gecko.prompts.PromptListItem;
53 import org.mozilla.gecko.sync.setup.SyncAccounts;
54 import org.mozilla.gecko.toolbar.AutocompleteHandler;
55 import org.mozilla.gecko.toolbar.BrowserToolbar;
56 import org.mozilla.gecko.toolbar.ToolbarProgressView;
57 import org.mozilla.gecko.util.Clipboard;
58 import org.mozilla.gecko.util.GamepadUtils;
59 import org.mozilla.gecko.util.HardwareUtils;
60 import org.mozilla.gecko.util.MenuUtils;
61 import org.mozilla.gecko.util.StringUtils;
62 import org.mozilla.gecko.util.ThreadUtils;
63 import org.mozilla.gecko.util.UiAsyncTask;
64 import org.mozilla.gecko.widget.ButtonToast;
65 import org.mozilla.gecko.widget.GeckoActionProvider;
67 import android.app.Activity;
68 import android.app.AlertDialog;
69 import android.content.ContentValues;
70 import android.content.Context;
71 import android.content.DialogInterface;
72 import android.content.Intent;
73 import android.content.SharedPreferences;
74 import android.content.res.Configuration;
75 import android.content.res.Resources;
76 import android.database.Cursor;
77 import android.graphics.Bitmap;
78 import android.graphics.Rect;
79 import android.graphics.drawable.BitmapDrawable;
80 import android.graphics.drawable.Drawable;
81 import android.net.Uri;
82 import android.nfc.NdefMessage;
83 import android.nfc.NdefRecord;
84 import android.nfc.NfcAdapter;
85 import android.nfc.NfcEvent;
86 import android.os.Build;
87 import android.os.Bundle;
88 import android.support.v4.app.FragmentManager;
89 import android.text.TextUtils;
90 import android.util.Log;
91 import android.view.InputDevice;
92 import android.view.KeyEvent;
93 import android.view.Menu;
94 import android.view.MenuInflater;
95 import android.view.MenuItem;
96 import android.view.MotionEvent;
97 import android.view.SubMenu;
98 import android.view.View;
99 import android.view.ViewGroup;
100 import android.view.ViewStub;
101 import android.view.ViewTreeObserver;
102 import android.view.Window;
103 import android.view.animation.Interpolator;
104 import android.widget.RelativeLayout;
105 import android.widget.ListView;
106 import android.widget.Toast;
107 import android.widget.ViewFlipper;
109 abstract public class BrowserApp extends GeckoApp
110 implements TabsPanel.TabsLayoutChangeListener,
111 PropertyAnimator.PropertyAnimationListener,
112 View.OnKeyListener,
113 LayerView.OnMetricsChangedListener,
114 BrowserSearch.OnSearchListener,
115 BrowserSearch.OnEditSuggestionListener,
116 HomePager.OnNewTabsListener,
117 OnUrlOpenListener,
118 ActionModeCompat.Presenter {
119 private static final String LOGTAG = "GeckoBrowserApp";
121 private static final int TABS_ANIMATION_DURATION = 450;
123 private static final int READER_ADD_SUCCESS = 0;
124 private static final int READER_ADD_FAILED = 1;
125 private static final int READER_ADD_DUPLICATE = 2;
127 private static final String ADD_SHORTCUT_TOAST = "add_shortcut_toast";
128 public static final String GUEST_BROWSING_ARG = "--guest";
130 private static final String STATE_ABOUT_HOME_TOP_PADDING = "abouthome_top_padding";
132 private static final String BROWSER_SEARCH_TAG = "browser_search";
133 private BrowserSearch mBrowserSearch;
134 private View mBrowserSearchContainer;
136 public ViewFlipper mViewFlipper;
137 public ActionModeCompatView mActionBar;
138 private BrowserToolbar mBrowserToolbar;
139 private ToolbarProgressView mProgressView;
140 private HomePager mHomePager;
141 private TabsPanel mTabsPanel;
142 private ViewGroup mHomePagerContainer;
143 protected Telemetry.Timer mAboutHomeStartupTimer = null;
144 private ActionModeCompat mActionMode;
145 private boolean mShowActionModeEndAnimation = false;
147 private static final int GECKO_TOOLS_MENU = -1;
148 private static final int ADDON_MENU_OFFSET = 1000;
149 private static class MenuItemInfo {
150 public int id;
151 public String label;
152 public String icon;
153 public boolean checkable = false;
154 public boolean checked = false;
155 public boolean enabled = true;
156 public boolean visible = true;
157 public int parent;
158 public boolean added = false; // So we can re-add after a locale change.
159 }
161 // The types of guest mdoe dialogs we show
162 private static enum GuestModeDialog {
163 ENTERING,
164 LEAVING
165 }
167 private Vector<MenuItemInfo> mAddonMenuItemsCache;
168 private PropertyAnimator mMainLayoutAnimator;
170 private static final Interpolator sTabsInterpolator = new Interpolator() {
171 @Override
172 public float getInterpolation(float t) {
173 t -= 1.0f;
174 return t * t * t * t * t + 1.0f;
175 }
176 };
178 private FindInPageBar mFindInPageBar;
179 private MediaCastingBar mMediaCastingBar;
181 // We'll ask for feedback after the user launches the app this many times.
182 private static final int FEEDBACK_LAUNCH_COUNT = 15;
184 // Stored value of the toolbar height, so we know when it's changed.
185 private int mToolbarHeight = 0;
187 // Stored value of whether the last metrics change allowed for toolbar
188 // scrolling.
189 private boolean mDynamicToolbarCanScroll = false;
191 private SharedPreferencesHelper mSharedPreferencesHelper;
193 private OrderedBroadcastHelper mOrderedBroadcastHelper;
195 private BrowserHealthReporter mBrowserHealthReporter;
197 // The tab to be selected on editing mode exit.
198 private Integer mTargetTabForEditingMode = null;
200 // The animator used to toggle HomePager visibility has a race where if the HomePager is shown
201 // (starting the animation), the HomePager is hidden, and the HomePager animation completes,
202 // both the web content and the HomePager will be hidden. This flag is used to prevent the
203 // race by determining if the web content should be hidden at the animation's end.
204 private boolean mHideWebContentOnAnimationEnd = false;
206 private DynamicToolbar mDynamicToolbar = new DynamicToolbar();
208 @Override
209 public void onTabChanged(Tab tab, Tabs.TabEvents msg, Object data) {
210 if (tab == null) {
211 // Only RESTORED is allowed a null tab: it's the only event that
212 // isn't tied to a specific tab.
213 if (msg != Tabs.TabEvents.RESTORED) {
214 throw new IllegalArgumentException("onTabChanged:" + msg + " must specify a tab.");
215 }
216 return;
217 }
219 Log.d(LOGTAG, "BrowserApp.onTabChanged: " + tab.getId() + ": " + msg);
220 switch(msg) {
221 case LOCATION_CHANGE:
222 if (Tabs.getInstance().isSelectedTab(tab)) {
223 maybeCancelFaviconLoad(tab);
224 }
225 // fall through
226 case SELECTED:
227 if (Tabs.getInstance().isSelectedTab(tab)) {
228 updateHomePagerForTab(tab);
230 final TabsPanel.Panel panel = tab.isPrivate()
231 ? TabsPanel.Panel.PRIVATE_TABS
232 : TabsPanel.Panel.NORMAL_TABS;
234 if (areTabsShown() && mTabsPanel.getCurrentPanel() != panel) {
235 showTabs(panel);
236 }
237 }
238 break;
239 case START:
240 if (Tabs.getInstance().isSelectedTab(tab)) {
241 invalidateOptionsMenu();
243 if (mDynamicToolbar.isEnabled()) {
244 mDynamicToolbar.setVisible(true, VisibilityTransition.ANIMATE);
245 }
246 }
247 break;
248 case LOAD_ERROR:
249 case STOP:
250 case MENU_UPDATED:
251 if (Tabs.getInstance().isSelectedTab(tab)) {
252 invalidateOptionsMenu();
253 }
254 break;
255 case PAGE_SHOW:
256 loadFavicon(tab);
257 break;
258 case LINK_FAVICON:
259 // If tab is not loading and the favicon is updated, we
260 // want to load the image straight away. If tab is still
261 // loading, we only load the favicon once the page's content
262 // is fully loaded.
263 if (tab.getState() != Tab.STATE_LOADING) {
264 loadFavicon(tab);
265 }
266 break;
267 }
268 super.onTabChanged(tab, msg, data);
269 }
271 @Override
272 public boolean onKey(View v, int keyCode, KeyEvent event) {
273 // Global onKey handler. This is called if the focused UI doesn't
274 // handle the key event, and before Gecko swallows the events.
275 if (event.getAction() != KeyEvent.ACTION_DOWN) {
276 return false;
277 }
279 // Gamepad support only exists in API-level >= 9
280 if (Build.VERSION.SDK_INT >= 9 &&
281 (event.getSource() & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) {
282 switch (keyCode) {
283 case KeyEvent.KEYCODE_BUTTON_Y:
284 // Toggle/focus the address bar on gamepad-y button.
285 if (mViewFlipper.getVisibility() == View.VISIBLE) {
286 if (mDynamicToolbar.isEnabled() && !isHomePagerVisible()) {
287 mDynamicToolbar.setVisible(false, VisibilityTransition.ANIMATE);
288 if (mLayerView != null) {
289 mLayerView.requestFocus();
290 }
291 } else {
292 // Just focus the address bar when about:home is visible
293 // or when the dynamic toolbar isn't enabled.
294 mBrowserToolbar.requestFocusFromTouch();
295 }
296 } else {
297 mDynamicToolbar.setVisible(true, VisibilityTransition.ANIMATE);
298 mBrowserToolbar.requestFocusFromTouch();
299 }
300 return true;
301 case KeyEvent.KEYCODE_BUTTON_L1:
302 // Go back on L1
303 Tabs.getInstance().getSelectedTab().doBack();
304 return true;
305 case KeyEvent.KEYCODE_BUTTON_R1:
306 // Go forward on R1
307 Tabs.getInstance().getSelectedTab().doForward();
308 return true;
309 }
310 }
312 // Check if this was a shortcut. Meta keys exists only on 11+.
313 final Tab tab = Tabs.getInstance().getSelectedTab();
314 if (Build.VERSION.SDK_INT >= 11 && tab != null && event.isCtrlPressed()) {
315 switch (keyCode) {
316 case KeyEvent.KEYCODE_LEFT_BRACKET:
317 tab.doBack();
318 return true;
320 case KeyEvent.KEYCODE_RIGHT_BRACKET:
321 tab.doForward();
322 return true;
324 case KeyEvent.KEYCODE_R:
325 tab.doReload();
326 return true;
328 case KeyEvent.KEYCODE_PERIOD:
329 tab.doStop();
330 return true;
332 case KeyEvent.KEYCODE_T:
333 addTab();
334 return true;
336 case KeyEvent.KEYCODE_W:
337 Tabs.getInstance().closeTab(tab);
338 return true;
340 case KeyEvent.KEYCODE_F:
341 mFindInPageBar.show();
342 return true;
343 }
344 }
346 return false;
347 }
349 @Override
350 public boolean onKeyDown(int keyCode, KeyEvent event) {
351 if (!mBrowserToolbar.isEditing() && onKey(null, keyCode, event)) {
352 return true;
353 }
354 return super.onKeyDown(keyCode, event);
355 }
357 void handleReaderListStatusRequest(final String url) {
358 ThreadUtils.postToBackgroundThread(new Runnable() {
359 @Override
360 public void run() {
361 final int inReadingList = BrowserDB.isReadingListItem(getContentResolver(), url) ? 1 : 0;
363 final JSONObject json = new JSONObject();
364 try {
365 json.put("url", url);
366 json.put("inReadingList", inReadingList);
367 } catch (JSONException e) {
368 Log.e(LOGTAG, "JSON error - failed to return inReadingList status", e);
369 return;
370 }
372 GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Reader:ListStatusReturn", json.toString()));
373 }
374 });
375 }
377 private void handleReaderAdded(int result, final ContentValues values) {
378 if (result != READER_ADD_SUCCESS) {
379 if (result == READER_ADD_FAILED) {
380 showToast(R.string.reading_list_failed, Toast.LENGTH_SHORT);
381 } else if (result == READER_ADD_DUPLICATE) {
382 showToast(R.string.reading_list_duplicate, Toast.LENGTH_SHORT);
383 }
385 return;
386 }
388 ThreadUtils.postToBackgroundThread(new Runnable() {
389 @Override
390 public void run() {
391 BrowserDB.addReadingListItem(getContentResolver(), values);
392 showToast(R.string.reading_list_added, Toast.LENGTH_SHORT);
393 }
394 });
395 }
397 private ContentValues messageToReadingListContentValues(JSONObject message) {
398 final ContentValues values = new ContentValues();
399 values.put(ReadingListItems.URL, message.optString("url"));
400 values.put(ReadingListItems.TITLE, message.optString("title"));
401 values.put(ReadingListItems.LENGTH, message.optInt("length"));
402 values.put(ReadingListItems.EXCERPT, message.optString("excerpt"));
403 return values;
404 }
406 void handleReaderRemoved(final String url) {
407 ThreadUtils.postToBackgroundThread(new Runnable() {
408 @Override
409 public void run() {
410 BrowserDB.removeReadingListItemWithURL(getContentResolver(), url);
411 showToast(R.string.reading_list_removed, Toast.LENGTH_SHORT);
413 final int count = BrowserDB.getReadingListCount(getContentResolver());
414 GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Reader:ListCountUpdated", Integer.toString(count)));
415 }
416 });
417 }
419 @Override
420 public void onCreate(Bundle savedInstanceState) {
421 mAboutHomeStartupTimer = new Telemetry.UptimeTimer("FENNEC_STARTUP_TIME_ABOUTHOME");
423 final Intent intent = getIntent();
425 String args = intent.getStringExtra("args");
426 if (args != null && args.contains(GUEST_BROWSING_ARG)) {
427 mProfile = GeckoProfile.createGuestProfile(this);
428 } else {
429 GeckoProfile.maybeCleanupGuestProfile(this);
430 }
432 // This has to be prepared prior to calling GeckoApp.onCreate, because
433 // widget code and BrowserToolbar need it, and they're created by the
434 // layout, which GeckoApp takes care of.
435 ((GeckoApplication) getApplication()).prepareLightweightTheme();
436 super.onCreate(savedInstanceState);
438 mViewFlipper = (ViewFlipper) findViewById(R.id.browser_actionbar);
439 mActionBar = (ActionModeCompatView) findViewById(R.id.actionbar);
441 mBrowserToolbar = (BrowserToolbar) findViewById(R.id.browser_toolbar);
442 mProgressView = (ToolbarProgressView) findViewById(R.id.progress);
443 mBrowserToolbar.setProgressBar(mProgressView);
444 if (Intent.ACTION_VIEW.equals(intent.getAction())) {
445 // Show the target URL immediately in the toolbar.
446 mBrowserToolbar.setTitle(intent.getDataString());
448 Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.INTENT);
449 }
451 ((GeckoApp.MainLayout) mMainLayout).setTouchEventInterceptor(new HideTabsTouchListener());
452 ((GeckoApp.MainLayout) mMainLayout).setMotionEventInterceptor(new MotionEventInterceptor() {
453 @Override
454 public boolean onInterceptMotionEvent(View view, MotionEvent event) {
455 // If we get a gamepad panning MotionEvent while the focus is not on the layerview,
456 // put the focus on the layerview and carry on
457 if (mLayerView != null && !mLayerView.hasFocus() && GamepadUtils.isPanningControl(event)) {
458 if (mHomePager == null) {
459 return false;
460 }
462 if (isHomePagerVisible()) {
463 mLayerView.requestFocus();
464 } else {
465 mHomePager.requestFocus();
466 }
467 }
468 return false;
469 }
470 });
472 mHomePagerContainer = (ViewGroup) findViewById(R.id.home_pager_container);
474 mBrowserSearchContainer = findViewById(R.id.search_container);
475 mBrowserSearch = (BrowserSearch) getSupportFragmentManager().findFragmentByTag(BROWSER_SEARCH_TAG);
476 if (mBrowserSearch == null) {
477 mBrowserSearch = BrowserSearch.newInstance();
478 mBrowserSearch.setUserVisibleHint(false);
479 }
481 setBrowserToolbarListeners();
483 mFindInPageBar = (FindInPageBar) findViewById(R.id.find_in_page);
484 mMediaCastingBar = (MediaCastingBar) findViewById(R.id.media_casting);
486 registerEventListener("CharEncoding:Data");
487 registerEventListener("CharEncoding:State");
488 registerEventListener("Feedback:LastUrl");
489 registerEventListener("Feedback:OpenPlayStore");
490 registerEventListener("Feedback:MaybeLater");
491 registerEventListener("Telemetry:Gather");
492 registerEventListener("Settings:Show");
493 registerEventListener("Updater:Launch");
494 registerEventListener("Menu:Add");
495 registerEventListener("Menu:Remove");
496 registerEventListener("Menu:Update");
497 registerEventListener("Accounts:Create");
498 registerEventListener("Accounts:Exist");
499 registerEventListener("Prompt:ShowTop");
501 Distribution.init(this);
502 JavaAddonManager.getInstance().init(getApplicationContext());
503 mSharedPreferencesHelper = new SharedPreferencesHelper(getApplicationContext());
504 mOrderedBroadcastHelper = new OrderedBroadcastHelper(getApplicationContext());
505 mBrowserHealthReporter = new BrowserHealthReporter();
507 if (AppConstants.MOZ_ANDROID_BEAM && Build.VERSION.SDK_INT >= 14) {
508 NfcAdapter nfc = NfcAdapter.getDefaultAdapter(this);
509 if (nfc != null) {
510 nfc.setNdefPushMessageCallback(new NfcAdapter.CreateNdefMessageCallback() {
511 @Override
512 public NdefMessage createNdefMessage(NfcEvent event) {
513 Tab tab = Tabs.getInstance().getSelectedTab();
514 if (tab == null || tab.isPrivate()) {
515 return null;
516 }
517 return new NdefMessage(new NdefRecord[] { NdefRecord.createUri(tab.getURL()) });
518 }
519 }, this);
520 }
521 }
523 if (savedInstanceState != null) {
524 mDynamicToolbar.onRestoreInstanceState(savedInstanceState);
525 mHomePagerContainer.setPadding(0, savedInstanceState.getInt(STATE_ABOUT_HOME_TOP_PADDING), 0, 0);
526 }
528 mDynamicToolbar.setEnabledChangedListener(new DynamicToolbar.OnEnabledChangedListener() {
529 @Override
530 public void onEnabledChanged(boolean enabled) {
531 setDynamicToolbarEnabled(enabled);
532 }
533 });
535 // Set the maximum bits-per-pixel the favicon system cares about.
536 IconDirectoryEntry.setMaxBPP(GeckoAppShell.getScreenDepth());
537 }
539 @Override
540 public void onBackPressed() {
541 if (getSupportFragmentManager().getBackStackEntryCount() > 0) {
542 super.onBackPressed();
543 return;
544 }
546 if (mBrowserToolbar.onBackPressed()) {
547 return;
548 }
550 if (mActionMode != null) {
551 endActionModeCompat();
552 return;
553 }
555 super.onBackPressed();
556 }
558 @Override
559 public void onResume() {
560 super.onResume();
561 unregisterEventListener("Prompt:ShowTop");
562 }
564 @Override
565 public void onPause() {
566 super.onPause();
567 // Register for Prompt:ShowTop so we can foreground this activity even if it's hidden.
568 registerEventListener("Prompt:ShowTop");
569 }
571 private void setBrowserToolbarListeners() {
572 mBrowserToolbar.setOnActivateListener(new BrowserToolbar.OnActivateListener() {
573 public void onActivate() {
574 enterEditingMode();
575 }
576 });
578 mBrowserToolbar.setOnCommitListener(new BrowserToolbar.OnCommitListener() {
579 public void onCommit() {
580 commitEditingMode();
581 }
582 });
584 mBrowserToolbar.setOnDismissListener(new BrowserToolbar.OnDismissListener() {
585 public void onDismiss() {
586 mBrowserToolbar.cancelEdit();
587 }
588 });
590 mBrowserToolbar.setOnFilterListener(new BrowserToolbar.OnFilterListener() {
591 public void onFilter(String searchText, AutocompleteHandler handler) {
592 filterEditingMode(searchText, handler);
593 }
594 });
596 mBrowserToolbar.setOnFocusChangeListener(new View.OnFocusChangeListener() {
597 @Override
598 public void onFocusChange(View v, boolean hasFocus) {
599 if (isHomePagerVisible()) {
600 mHomePager.onToolbarFocusChange(hasFocus);
601 }
602 }
603 });
605 mBrowserToolbar.setOnStartEditingListener(new BrowserToolbar.OnStartEditingListener() {
606 public void onStartEditing() {
607 // Temporarily disable doorhanger notifications.
608 mDoorHangerPopup.disable();
609 }
610 });
612 mBrowserToolbar.setOnStopEditingListener(new BrowserToolbar.OnStopEditingListener() {
613 public void onStopEditing() {
614 selectTargetTabForEditingMode();
616 // Since the underlying LayerView is set visible in hideHomePager, we would
617 // ordinarily want to call it first. However, hideBrowserSearch changes the
618 // visibility of the HomePager and hideHomePager will take no action if the
619 // HomePager is hidden, so we want to call hideBrowserSearch to restore the
620 // HomePager visibility first.
621 hideBrowserSearch();
622 hideHomePager();
624 // Re-enable doorhanger notifications. They may trigger on the selected tab above.
625 mDoorHangerPopup.enable();
626 }
627 });
629 // Intercept key events for gamepad shortcuts
630 mBrowserToolbar.setOnKeyListener(this);
631 }
633 private void showBookmarkDialog() {
634 final Tab tab = Tabs.getInstance().getSelectedTab();
635 final Prompt ps = new Prompt(this, new Prompt.PromptCallback() {
636 @Override
637 public void onPromptFinished(String result) {
638 int itemId = -1;
639 try {
640 itemId = new JSONObject(result).getInt("button");
641 } catch(JSONException ex) {
642 Log.e(LOGTAG, "Exception reading bookmark prompt result", ex);
643 }
645 if (tab == null)
646 return;
648 if (itemId == 0) {
649 new EditBookmarkDialog(BrowserApp.this).show(tab.getURL());
650 } else if (itemId == 1) {
651 String url = tab.getURL();
652 String title = tab.getDisplayTitle();
653 Bitmap favicon = tab.getFavicon();
654 if (url != null && title != null) {
655 GeckoAppShell.createShortcut(title, url, url, favicon, "");
656 }
657 }
658 }
659 });
661 final PromptListItem[] items = new PromptListItem[2];
662 Resources res = getResources();
663 items[0] = new PromptListItem(res.getString(R.string.contextmenu_edit_bookmark));
664 items[1] = new PromptListItem(res.getString(R.string.contextmenu_add_to_launcher));
666 ps.show("", "", items, ListView.CHOICE_MODE_NONE);
667 }
669 private void setDynamicToolbarEnabled(boolean enabled) {
670 ThreadUtils.assertOnUiThread();
672 if (enabled) {
673 if (mLayerView != null) {
674 mLayerView.setOnMetricsChangedListener(this);
675 }
676 setToolbarMargin(0);
677 mHomePagerContainer.setPadding(0, mViewFlipper.getHeight(), 0, 0);
678 } else {
679 // Immediately show the toolbar when disabling the dynamic
680 // toolbar.
681 if (mLayerView != null) {
682 mLayerView.setOnMetricsChangedListener(null);
683 }
684 mHomePagerContainer.setPadding(0, 0, 0, 0);
685 if (mViewFlipper != null) {
686 ViewHelper.setTranslationY(mViewFlipper, 0);
687 }
688 }
690 refreshToolbarHeight();
691 }
693 private static boolean isAboutHome(final Tab tab) {
694 return AboutPages.isAboutHome(tab.getURL());
695 }
697 @Override
698 public boolean onSearchRequested() {
699 enterEditingMode();
700 return true;
701 }
703 @Override
704 public boolean onContextItemSelected(MenuItem item) {
705 final int itemId = item.getItemId();
706 if (itemId == R.id.pasteandgo) {
707 String text = Clipboard.getText();
708 if (!TextUtils.isEmpty(text)) {
709 Tabs.getInstance().loadUrl(text);
710 Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.CONTEXT_MENU);
711 Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.CONTEXT_MENU, "pasteandgo");
712 }
713 return true;
714 }
716 if (itemId == R.id.site_settings) {
717 // This can be selected from either the browser menu or the contextmenu, depending on the size and version (v11+) of the phone.
718 GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Permissions:Get", null));
719 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
720 Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.CONTEXT_MENU, "site_settings");
721 }
722 return true;
723 }
725 if (itemId == R.id.paste) {
726 String text = Clipboard.getText();
727 if (!TextUtils.isEmpty(text)) {
728 enterEditingMode(text);
729 Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.CONTEXT_MENU, "paste");
730 }
731 return true;
732 }
734 if (itemId == R.id.share) {
735 shareCurrentUrl();
736 return true;
737 }
739 if (itemId == R.id.subscribe) {
740 // This can be selected from either the browser menu or the contextmenu, depending on the size and version (v11+) of the phone.
741 Tab tab = Tabs.getInstance().getSelectedTab();
742 if (tab != null && tab.hasFeeds()) {
743 JSONObject args = new JSONObject();
744 try {
745 args.put("tabId", tab.getId());
746 } catch (JSONException e) {
747 Log.e(LOGTAG, "error building json arguments");
748 }
749 GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Feeds:Subscribe", args.toString()));
750 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
751 Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.CONTEXT_MENU, "subscribe");
752 }
753 }
754 return true;
755 }
757 if (itemId == R.id.add_search_engine) {
758 // This can be selected from either the browser menu or the contextmenu, depending on the size and version (v11+) of the phone.
759 Tab tab = Tabs.getInstance().getSelectedTab();
760 if (tab != null && tab.hasOpenSearch()) {
761 JSONObject args = new JSONObject();
762 try {
763 args.put("tabId", tab.getId());
764 } catch (JSONException e) {
765 Log.e(LOGTAG, "error building json arguments");
766 return true;
767 }
768 GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("SearchEngines:Add", args.toString()));
770 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
771 Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.CONTEXT_MENU, "add_search_engine");
772 }
773 }
774 return true;
775 }
777 if (itemId == R.id.copyurl) {
778 Tab tab = Tabs.getInstance().getSelectedTab();
779 if (tab != null) {
780 String url = tab.getURL();
781 if (url != null) {
782 Clipboard.setText(url);
783 Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.CONTEXT_MENU, "copyurl");
784 }
785 }
786 return true;
787 }
789 if (itemId == R.id.add_to_launcher) {
790 Tab tab = Tabs.getInstance().getSelectedTab();
791 if (tab == null) {
792 return true;
793 }
795 final String url = tab.getURL();
796 final String title = tab.getDisplayTitle();
797 if (url == null || title == null) {
798 return true;
799 }
801 final OnFaviconLoadedListener listener = new GeckoAppShell.CreateShortcutFaviconLoadedListener(url, title);
802 Favicons.getSizedFavicon(url,
803 tab.getFaviconURL(),
804 Integer.MAX_VALUE,
805 LoadFaviconTask.FLAG_PERSIST,
806 listener);
807 return true;
808 }
810 return false;
811 }
813 @Override
814 public void setAccessibilityEnabled(boolean enabled) {
815 mDynamicToolbar.setAccessibilityEnabled(enabled);
816 }
818 @Override
819 public void onDestroy() {
820 mDynamicToolbar.destroy();
822 if (mBrowserToolbar != null)
823 mBrowserToolbar.onDestroy();
825 if (mFindInPageBar != null) {
826 mFindInPageBar.onDestroy();
827 mFindInPageBar = null;
828 }
830 if (mMediaCastingBar != null) {
831 mMediaCastingBar.onDestroy();
832 mMediaCastingBar = null;
833 }
835 if (mSharedPreferencesHelper != null) {
836 mSharedPreferencesHelper.uninit();
837 mSharedPreferencesHelper = null;
838 }
840 if (mOrderedBroadcastHelper != null) {
841 mOrderedBroadcastHelper.uninit();
842 mOrderedBroadcastHelper = null;
843 }
845 if (mBrowserHealthReporter != null) {
846 mBrowserHealthReporter.uninit();
847 mBrowserHealthReporter = null;
848 }
850 unregisterEventListener("CharEncoding:Data");
851 unregisterEventListener("CharEncoding:State");
852 unregisterEventListener("Feedback:LastUrl");
853 unregisterEventListener("Feedback:OpenPlayStore");
854 unregisterEventListener("Feedback:MaybeLater");
855 unregisterEventListener("Telemetry:Gather");
856 unregisterEventListener("Settings:Show");
857 unregisterEventListener("Updater:Launch");
858 unregisterEventListener("Menu:Add");
859 unregisterEventListener("Menu:Remove");
860 unregisterEventListener("Menu:Update");
861 unregisterEventListener("Accounts:Create");
862 unregisterEventListener("Accounts:Exist");
864 if (AppConstants.MOZ_ANDROID_BEAM && Build.VERSION.SDK_INT >= 14) {
865 NfcAdapter nfc = NfcAdapter.getDefaultAdapter(this);
866 if (nfc != null) {
867 // null this out even though the docs say it's not needed,
868 // because the source code looks like it will only do this
869 // automatically on API 14+
870 nfc.setNdefPushMessageCallback(null, this);
871 }
872 }
874 super.onDestroy();
875 }
877 @Override
878 protected void initializeChrome() {
879 super.initializeChrome();
881 mDoorHangerPopup.setAnchor(mBrowserToolbar.getDoorHangerAnchor());
883 mDynamicToolbar.setLayerView(mLayerView);
884 setDynamicToolbarEnabled(mDynamicToolbar.isEnabled());
886 // Intercept key events for gamepad shortcuts
887 mLayerView.setOnKeyListener(this);
889 // Initialize the actionbar menu items on startup for both large and small tablets
890 if (HardwareUtils.isTablet()) {
891 onCreatePanelMenu(Window.FEATURE_OPTIONS_PANEL, null);
892 invalidateOptionsMenu();
893 }
894 }
896 private void shareCurrentUrl() {
897 Tab tab = Tabs.getInstance().getSelectedTab();
898 if (tab == null) {
899 return;
900 }
902 String url = tab.getURL();
903 if (url == null) {
904 return;
905 }
907 if (AboutPages.isAboutReader(url)) {
908 url = ReaderModeUtils.getUrlFromAboutReader(url);
909 }
911 GeckoAppShell.openUriExternal(url, "text/plain", "", "",
912 Intent.ACTION_SEND, tab.getDisplayTitle());
914 // Context: Sharing via chrome list (no explicit session is active)
915 Telemetry.sendUIEvent(TelemetryContract.Event.SHARE, TelemetryContract.Method.LIST);
916 }
918 @Override
919 protected void loadStartupTab(String url) {
920 // We aren't showing about:home, so cancel the telemetry timer
921 if (url != null || mShouldRestore) {
922 mAboutHomeStartupTimer.cancel();
923 }
925 super.loadStartupTab(url);
926 }
928 private void setToolbarMargin(int margin) {
929 ((RelativeLayout.LayoutParams) mGeckoLayout.getLayoutParams()).topMargin = margin;
930 mGeckoLayout.requestLayout();
931 }
933 @Override
934 public void onMetricsChanged(ImmutableViewportMetrics aMetrics) {
935 if (isHomePagerVisible() || mViewFlipper == null) {
936 return;
937 }
939 // If the page has shrunk so that the toolbar no longer scrolls, make
940 // sure the toolbar is visible.
941 if (aMetrics.getPageHeight() <= aMetrics.getHeight()) {
942 if (mDynamicToolbarCanScroll) {
943 mDynamicToolbarCanScroll = false;
944 if (mViewFlipper.getVisibility() != View.VISIBLE) {
945 ThreadUtils.postToUiThread(new Runnable() {
946 public void run() {
947 mDynamicToolbar.setVisible(true, VisibilityTransition.ANIMATE);
948 }
949 });
950 }
951 }
952 } else {
953 mDynamicToolbarCanScroll = true;
954 }
956 final View toolbarLayout = mViewFlipper;
957 final int marginTop = Math.round(aMetrics.marginTop);
958 ThreadUtils.postToUiThread(new Runnable() {
959 public void run() {
960 final float translationY = marginTop - toolbarLayout.getHeight();
961 ViewHelper.setTranslationY(toolbarLayout, translationY);
962 ViewHelper.setTranslationY(mProgressView, translationY);
964 if (mDoorHangerPopup.isShowing()) {
965 mDoorHangerPopup.updatePopup();
966 }
967 }
968 });
970 if (mFormAssistPopup != null)
971 mFormAssistPopup.onMetricsChanged(aMetrics);
972 }
974 @Override
975 public void onPanZoomStopped() {
976 if (!mDynamicToolbar.isEnabled() || isHomePagerVisible()) {
977 return;
978 }
980 // Make sure the toolbar is fully hidden or fully shown when the user
981 // lifts their finger. If the page is shorter than the viewport, the
982 // toolbar is always shown.
983 ImmutableViewportMetrics metrics = mLayerView.getViewportMetrics();
984 if (metrics.getPageHeight() < metrics.getHeight()
985 || metrics.marginTop >= mToolbarHeight / 2) {
986 mDynamicToolbar.setVisible(true, VisibilityTransition.ANIMATE);
987 } else {
988 mDynamicToolbar.setVisible(false, VisibilityTransition.ANIMATE);
989 }
990 }
992 public void refreshToolbarHeight() {
993 ThreadUtils.assertOnUiThread();
995 int height = 0;
996 if (mViewFlipper != null) {
997 height = mViewFlipper.getHeight();
998 }
1000 if (!mDynamicToolbar.isEnabled() || isHomePagerVisible()) {
1001 // Use aVisibleHeight here so that when the dynamic toolbar is
1002 // enabled, the padding will animate with the toolbar becoming
1003 // visible.
1004 if (mDynamicToolbar.isEnabled()) {
1005 // When the dynamic toolbar is enabled, set the padding on the
1006 // about:home widget directly - this is to avoid resizing the
1007 // LayerView, which can cause visible artifacts.
1008 mHomePagerContainer.setPadding(0, height, 0, 0);
1009 } else {
1010 setToolbarMargin(height);
1011 height = 0;
1012 }
1013 } else {
1014 setToolbarMargin(0);
1015 }
1017 if (mLayerView != null && height != mToolbarHeight) {
1018 mToolbarHeight = height;
1019 mLayerView.getLayerMarginsAnimator().setMaxMargins(0, height, 0, 0);
1020 mDynamicToolbar.setVisible(true, VisibilityTransition.IMMEDIATE);
1021 }
1022 }
1024 @Override
1025 void toggleChrome(final boolean aShow) {
1026 ThreadUtils.postToUiThread(new Runnable() {
1027 @Override
1028 public void run() {
1029 if (aShow) {
1030 mViewFlipper.setVisibility(View.VISIBLE);
1031 } else {
1032 mViewFlipper.setVisibility(View.GONE);
1033 if (hasTabsSideBar()) {
1034 hideTabs();
1035 }
1036 }
1037 }
1038 });
1040 super.toggleChrome(aShow);
1041 }
1043 @Override
1044 void focusChrome() {
1045 ThreadUtils.postToUiThread(new Runnable() {
1046 @Override
1047 public void run() {
1048 mViewFlipper.setVisibility(View.VISIBLE);
1049 mViewFlipper.requestFocusFromTouch();
1050 }
1051 });
1052 }
1054 @Override
1055 public void refreshChrome() {
1056 invalidateOptionsMenu();
1058 if (mTabsPanel != null) {
1059 updateSideBarState();
1060 mTabsPanel.refresh();
1061 }
1063 mBrowserToolbar.refresh();
1064 }
1066 @Override
1067 public boolean hasTabsSideBar() {
1068 return (mTabsPanel != null && mTabsPanel.isSideBar());
1069 }
1071 private void updateSideBarState() {
1072 if (mMainLayoutAnimator != null)
1073 mMainLayoutAnimator.stop();
1075 boolean isSideBar = (HardwareUtils.isTablet() && getOrientation() == Configuration.ORIENTATION_LANDSCAPE);
1076 final int sidebarWidth = getResources().getDimensionPixelSize(R.dimen.tabs_sidebar_width);
1078 ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) mTabsPanel.getLayoutParams();
1079 lp.width = (isSideBar ? sidebarWidth : ViewGroup.LayoutParams.FILL_PARENT);
1080 mTabsPanel.requestLayout();
1082 final boolean sidebarIsShown = (isSideBar && mTabsPanel.isShown());
1083 final int mainLayoutScrollX = (sidebarIsShown ? -sidebarWidth : 0);
1084 mMainLayout.scrollTo(mainLayoutScrollX, 0);
1086 mTabsPanel.setIsSideBar(isSideBar);
1087 }
1089 @Override
1090 public void handleMessage(String event, JSONObject message) {
1091 try {
1092 if (event.equals("Menu:Add")) {
1093 MenuItemInfo info = new MenuItemInfo();
1094 info.label = message.getString("name");
1095 info.id = message.getInt("id") + ADDON_MENU_OFFSET;
1096 info.icon = message.optString("icon", null);
1097 info.checked = message.optBoolean("checked", false);
1098 info.enabled = message.optBoolean("enabled", true);
1099 info.visible = message.optBoolean("visible", true);
1100 info.checkable = message.optBoolean("checkable", false);
1101 int parent = message.optInt("parent", 0);
1102 info.parent = parent <= 0 ? parent : parent + ADDON_MENU_OFFSET;
1103 final MenuItemInfo menuItemInfo = info;
1104 ThreadUtils.postToUiThread(new Runnable() {
1105 @Override
1106 public void run() {
1107 addAddonMenuItem(menuItemInfo);
1108 }
1109 });
1110 } else if (event.equals("Menu:Remove")) {
1111 final int id = message.getInt("id") + ADDON_MENU_OFFSET;
1112 ThreadUtils.postToUiThread(new Runnable() {
1113 @Override
1114 public void run() {
1115 removeAddonMenuItem(id);
1116 }
1117 });
1118 } else if (event.equals("Menu:Update")) {
1119 final int id = message.getInt("id") + ADDON_MENU_OFFSET;
1120 final JSONObject options = message.getJSONObject("options");
1121 ThreadUtils.postToUiThread(new Runnable() {
1122 @Override
1123 public void run() {
1124 updateAddonMenuItem(id, options);
1125 }
1126 });
1127 } else if (event.equals("CharEncoding:Data")) {
1128 final JSONArray charsets = message.getJSONArray("charsets");
1129 int selected = message.getInt("selected");
1131 final int len = charsets.length();
1132 final String[] titleArray = new String[len];
1133 for (int i = 0; i < len; i++) {
1134 JSONObject charset = charsets.getJSONObject(i);
1135 titleArray[i] = charset.getString("title");
1136 }
1138 final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this);
1139 dialogBuilder.setSingleChoiceItems(titleArray, selected, new AlertDialog.OnClickListener() {
1140 @Override
1141 public void onClick(DialogInterface dialog, int which) {
1142 try {
1143 JSONObject charset = charsets.getJSONObject(which);
1144 GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("CharEncoding:Set", charset.getString("code")));
1145 dialog.dismiss();
1146 } catch (JSONException e) {
1147 Log.e(LOGTAG, "error parsing json", e);
1148 }
1149 }
1150 });
1151 dialogBuilder.setNegativeButton(R.string.button_cancel, new AlertDialog.OnClickListener() {
1152 @Override
1153 public void onClick(DialogInterface dialog, int which) {
1154 dialog.dismiss();
1155 }
1156 });
1157 ThreadUtils.postToUiThread(new Runnable() {
1158 @Override
1159 public void run() {
1160 dialogBuilder.show();
1161 }
1162 });
1163 } else if (event.equals("CharEncoding:State")) {
1164 final boolean visible = message.getString("visible").equals("true");
1165 GeckoPreferences.setCharEncodingState(visible);
1166 final Menu menu = mMenu;
1167 ThreadUtils.postToUiThread(new Runnable() {
1168 @Override
1169 public void run() {
1170 if (menu != null)
1171 menu.findItem(R.id.char_encoding).setVisible(visible);
1172 }
1173 });
1174 } else if (event.equals("Feedback:OpenPlayStore")) {
1175 Intent intent = new Intent(Intent.ACTION_VIEW);
1176 intent.setData(Uri.parse("market://details?id=" + getPackageName()));
1177 startActivity(intent);
1178 } else if (event.equals("Feedback:MaybeLater")) {
1179 resetFeedbackLaunchCount();
1180 } else if (event.equals("Feedback:LastUrl")) {
1181 getLastUrl();
1182 } else if (event.equals("Gecko:DelayedStartup")) {
1183 ThreadUtils.postToUiThread(new Runnable() {
1184 @Override
1185 public void run() {
1186 // Force tabs panel inflation once the initial
1187 // pageload is finished.
1188 ensureTabsPanelExists();
1189 }
1190 });
1192 super.handleMessage(event, message);
1193 } else if (event.equals("Gecko:Ready")) {
1194 // Handle this message in GeckoApp, but also enable the Settings
1195 // menuitem, which is specific to BrowserApp.
1196 super.handleMessage(event, message);
1197 final Menu menu = mMenu;
1198 ThreadUtils.postToUiThread(new Runnable() {
1199 @Override
1200 public void run() {
1201 if (menu != null)
1202 menu.findItem(R.id.settings).setEnabled(true);
1203 }
1204 });
1206 // Display notification for Mozilla data reporting, if data should be collected.
1207 if (AppConstants.MOZ_DATA_REPORTING) {
1208 DataReportingNotification.checkAndNotifyPolicy(GeckoAppShell.getContext());
1209 }
1211 } else if (event.equals("Telemetry:Gather")) {
1212 Telemetry.HistogramAdd("PLACES_PAGES_COUNT", BrowserDB.getCount(getContentResolver(), "history"));
1213 Telemetry.HistogramAdd("PLACES_BOOKMARKS_COUNT", BrowserDB.getCount(getContentResolver(), "bookmarks"));
1214 Telemetry.HistogramAdd("FENNEC_FAVICONS_COUNT", BrowserDB.getCount(getContentResolver(), "favicons"));
1215 Telemetry.HistogramAdd("FENNEC_THUMBNAILS_COUNT", BrowserDB.getCount(getContentResolver(), "thumbnails"));
1216 } else if (event.equals("Reader:ListStatusRequest")) {
1217 handleReaderListStatusRequest(message.getString("url"));
1218 } else if (event.equals("Reader:Added")) {
1219 final int result = message.getInt("result");
1220 handleReaderAdded(result, messageToReadingListContentValues(message));
1221 } else if (event.equals("Reader:Removed")) {
1222 final String url = message.getString("url");
1223 handleReaderRemoved(url);
1224 } else if (event.equals("Reader:Share")) {
1225 final String title = message.getString("title");
1226 final String url = message.getString("url");
1227 GeckoAppShell.openUriExternal(url, "text/plain", "", "",
1228 Intent.ACTION_SEND, title);
1229 } else if (event.equals("Settings:Show")) {
1230 // null strings return "null" (http://code.google.com/p/android/issues/detail?id=13830)
1231 String resource = null;
1232 if (!message.isNull(GeckoPreferences.INTENT_EXTRA_RESOURCES)) {
1233 resource = message.getString(GeckoPreferences.INTENT_EXTRA_RESOURCES);
1234 }
1235 Intent settingsIntent = new Intent(this, GeckoPreferences.class);
1236 GeckoPreferences.setResourceToOpen(settingsIntent, resource);
1237 startActivity(settingsIntent);
1238 } else if (event.equals("Updater:Launch")) {
1239 handleUpdaterLaunch();
1240 } else if (event.equals("Prompt:ShowTop")) {
1241 // Bring this activity to front so the prompt is visible..
1242 Intent bringToFrontIntent = new Intent();
1243 bringToFrontIntent.setClassName(AppConstants.ANDROID_PACKAGE_NAME, AppConstants.BROWSER_INTENT_CLASS_NAME);
1244 bringToFrontIntent.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
1245 startActivity(bringToFrontIntent);
1246 } else if (event.equals("Accounts:Create")) {
1247 // Do exactly the same thing as if you tapped 'Sync'
1248 // in Settings.
1249 final Intent intent = new Intent(getContext(), FxAccountGetStartedActivity.class);
1250 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
1251 getContext().startActivity(intent);
1252 } else if (event.equals("Accounts:Exist")) {
1253 final String kind = message.getString("kind");
1254 final JSONObject response = new JSONObject();
1256 if ("any".equals(kind)) {
1257 response.put("exists", SyncAccounts.syncAccountsExist(getContext()) ||
1258 FirefoxAccounts.firefoxAccountsExist(getContext()));
1259 EventDispatcher.sendResponse(message, response);
1260 } else if ("fxa".equals(kind)) {
1261 response.put("exists", FirefoxAccounts.firefoxAccountsExist(getContext()));
1262 EventDispatcher.sendResponse(message, response);
1263 } else if ("sync11".equals(kind)) {
1264 response.put("exists", SyncAccounts.syncAccountsExist(getContext()));
1265 EventDispatcher.sendResponse(message, response);
1266 } else {
1267 response.put("error", "Unknown kind");
1268 EventDispatcher.sendError(message, response);
1269 }
1270 } else {
1271 super.handleMessage(event, message);
1272 }
1273 } catch (Exception e) {
1274 Log.e(LOGTAG, "Exception handling message \"" + event + "\":", e);
1275 }
1276 }
1278 @Override
1279 public void addTab() {
1280 // Always load about:home when opening a new tab.
1281 Tabs.getInstance().loadUrl(AboutPages.HOME, Tabs.LOADURL_NEW_TAB);
1282 }
1284 @Override
1285 public void addPrivateTab() {
1286 Tabs.getInstance().loadUrl(AboutPages.PRIVATEBROWSING, Tabs.LOADURL_NEW_TAB | Tabs.LOADURL_PRIVATE);
1287 }
1289 @Override
1290 public void showNormalTabs() {
1291 showTabs(TabsPanel.Panel.NORMAL_TABS);
1292 }
1294 @Override
1295 public void showPrivateTabs() {
1296 showTabs(TabsPanel.Panel.PRIVATE_TABS);
1297 }
1298 /**
1299 * Ensure the TabsPanel view is properly inflated and returns
1300 * true when the view has been inflated, false otherwise.
1301 */
1302 private boolean ensureTabsPanelExists() {
1303 if (mTabsPanel != null) {
1304 return false;
1305 }
1307 ViewStub tabsPanelStub = (ViewStub) findViewById(R.id.tabs_panel);
1308 mTabsPanel = (TabsPanel) tabsPanelStub.inflate();
1310 mTabsPanel.setTabsLayoutChangeListener(this);
1311 updateSideBarState();
1313 return true;
1314 }
1316 private void showTabs(final TabsPanel.Panel panel) {
1317 if (Tabs.getInstance().getDisplayCount() == 0)
1318 return;
1320 if (ensureTabsPanelExists()) {
1321 // If we've just inflated the tabs panel, only show it once the current
1322 // layout pass is done to avoid displayed temporary UI states during
1323 // relayout.
1324 ViewTreeObserver vto = mTabsPanel.getViewTreeObserver();
1325 if (vto.isAlive()) {
1326 vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
1327 @Override
1328 public void onGlobalLayout() {
1329 mTabsPanel.getViewTreeObserver().removeGlobalOnLayoutListener(this);
1330 mTabsPanel.show(panel);
1331 }
1332 });
1333 }
1334 } else {
1335 mTabsPanel.show(panel);
1336 }
1337 }
1339 @Override
1340 public void hideTabs() {
1341 mTabsPanel.hide();
1342 }
1344 @Override
1345 public boolean autoHideTabs() {
1346 if (areTabsShown()) {
1347 hideTabs();
1348 return true;
1349 }
1350 return false;
1351 }
1353 @Override
1354 public boolean areTabsShown() {
1355 return (mTabsPanel != null && mTabsPanel.isShown());
1356 }
1358 @Override
1359 public void onTabsLayoutChange(int width, int height) {
1360 int animationLength = TABS_ANIMATION_DURATION;
1362 if (mMainLayoutAnimator != null) {
1363 animationLength = Math.max(1, animationLength - (int)mMainLayoutAnimator.getRemainingTime());
1364 mMainLayoutAnimator.stop(false);
1365 }
1367 if (areTabsShown()) {
1368 mTabsPanel.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
1369 }
1371 mMainLayoutAnimator = new PropertyAnimator(animationLength, sTabsInterpolator);
1372 mMainLayoutAnimator.addPropertyAnimationListener(this);
1374 if (hasTabsSideBar()) {
1375 mMainLayoutAnimator.attach(mMainLayout,
1376 PropertyAnimator.Property.SCROLL_X,
1377 -width);
1378 } else {
1379 mMainLayoutAnimator.attach(mMainLayout,
1380 PropertyAnimator.Property.SCROLL_Y,
1381 -height);
1382 }
1384 mTabsPanel.prepareTabsAnimation(mMainLayoutAnimator);
1385 mBrowserToolbar.prepareTabsAnimation(mMainLayoutAnimator, areTabsShown());
1387 // If the tabs layout is animating onto the screen, pin the dynamic
1388 // toolbar.
1389 if (mDynamicToolbar.isEnabled()) {
1390 if (width > 0 && height > 0) {
1391 mDynamicToolbar.setPinned(true, PinReason.RELAYOUT);
1392 mDynamicToolbar.setVisible(true, VisibilityTransition.ANIMATE);
1393 } else {
1394 mDynamicToolbar.setPinned(false, PinReason.RELAYOUT);
1395 }
1396 }
1398 mMainLayoutAnimator.start();
1399 }
1401 @Override
1402 public void onPropertyAnimationStart() {
1403 }
1405 @Override
1406 public void onPropertyAnimationEnd() {
1407 if (!areTabsShown()) {
1408 mTabsPanel.setVisibility(View.INVISIBLE);
1409 mTabsPanel.setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
1410 }
1412 mTabsPanel.finishTabsAnimation();
1414 mMainLayoutAnimator = null;
1415 }
1417 @Override
1418 public void onSaveInstanceState(Bundle outState) {
1419 super.onSaveInstanceState(outState);
1420 mDynamicToolbar.onSaveInstanceState(outState);
1421 outState.putInt(STATE_ABOUT_HOME_TOP_PADDING, mHomePagerContainer.getPaddingTop());
1422 }
1424 /**
1425 * Attempts to switch to an open tab with the given URL.
1426 *
1427 * @return true if we successfully switched to a tab, false otherwise.
1428 */
1429 private boolean maybeSwitchToTab(String url, EnumSet<OnUrlOpenListener.Flags> flags) {
1430 if (!flags.contains(OnUrlOpenListener.Flags.ALLOW_SWITCH_TO_TAB)) {
1431 return false;
1432 }
1434 final Tabs tabs = Tabs.getInstance();
1435 final Tab tab = tabs.getFirstTabForUrl(url, tabs.getSelectedTab().isPrivate());
1436 if (tab == null) {
1437 return false;
1438 }
1440 // Set the target tab to null so it does not get selected (on editing
1441 // mode exit) in lieu of the tab we are about to select.
1442 mTargetTabForEditingMode = null;
1443 tabs.selectTab(tab.getId());
1445 mBrowserToolbar.cancelEdit();
1447 return true;
1448 }
1450 private void openUrlAndStopEditing(String url) {
1451 openUrlAndStopEditing(url, null, false);
1452 }
1454 private void openUrlAndStopEditing(String url, boolean newTab) {
1455 openUrlAndStopEditing(url, null, newTab);
1456 }
1458 private void openUrlAndStopEditing(String url, String searchEngine) {
1459 openUrlAndStopEditing(url, searchEngine, false);
1460 }
1462 private void openUrlAndStopEditing(String url, String searchEngine, boolean newTab) {
1463 int flags = Tabs.LOADURL_NONE;
1464 if (newTab) {
1465 flags |= Tabs.LOADURL_NEW_TAB;
1466 }
1468 Tabs.getInstance().loadUrl(url, searchEngine, -1, flags);
1470 mBrowserToolbar.cancelEdit();
1471 }
1473 private boolean isHomePagerVisible() {
1474 return (mHomePager != null && mHomePager.isVisible()
1475 && mHomePagerContainer != null && mHomePagerContainer.getVisibility() == View.VISIBLE);
1476 }
1478 /* Favicon stuff. */
1479 private static OnFaviconLoadedListener sFaviconLoadedListener = new OnFaviconLoadedListener() {
1480 @Override
1481 public void onFaviconLoaded(String pageUrl, String faviconURL, Bitmap favicon) {
1482 // If we failed to load a favicon, we use the default favicon instead.
1483 Tabs.getInstance()
1484 .updateFaviconForURL(pageUrl,
1485 (favicon == null) ? Favicons.defaultFavicon : favicon);
1486 }
1487 };
1489 private void loadFavicon(final Tab tab) {
1490 maybeCancelFaviconLoad(tab);
1492 final int tabFaviconSize = getResources().getDimensionPixelSize(R.dimen.browser_toolbar_favicon_size);
1494 int flags = (tab.isPrivate() || tab.getErrorType() != Tab.ErrorType.NONE) ? 0 : LoadFaviconTask.FLAG_PERSIST;
1495 int id = Favicons.getSizedFavicon(tab.getURL(), tab.getFaviconURL(), tabFaviconSize, flags, sFaviconLoadedListener);
1497 tab.setFaviconLoadId(id);
1498 }
1500 private void maybeCancelFaviconLoad(Tab tab) {
1501 int faviconLoadId = tab.getFaviconLoadId();
1503 if (Favicons.NOT_LOADING == faviconLoadId) {
1504 return;
1505 }
1507 // Cancel load task and reset favicon load state if it wasn't already
1508 // in NOT_LOADING state.
1509 Favicons.cancelFaviconLoad(faviconLoadId);
1510 tab.setFaviconLoadId(Favicons.NOT_LOADING);
1511 }
1513 /**
1514 * Enters editing mode with the current tab's URL. There might be no
1515 * tabs loaded by the time the user enters editing mode e.g. just after
1516 * the app starts. In this case, we simply fallback to an empty URL.
1517 */
1518 private void enterEditingMode() {
1519 String url = "";
1521 final Tab tab = Tabs.getInstance().getSelectedTab();
1522 if (tab != null) {
1523 final String userSearch = tab.getUserSearch();
1525 // Check to see if there's a user-entered search term,
1526 // which we save whenever the user performs a search.
1527 url = (TextUtils.isEmpty(userSearch) ? tab.getURL() : userSearch);
1528 }
1530 enterEditingMode(url);
1531 }
1533 /**
1534 * Enters editing mode with the specified URL. This method will
1535 * always open the HISTORY page on about:home.
1536 */
1537 private void enterEditingMode(String url) {
1538 if (url == null) {
1539 throw new IllegalArgumentException("Cannot handle null URLs in enterEditingMode");
1540 }
1542 if (mBrowserToolbar.isEditing() || mBrowserToolbar.isAnimating()) {
1543 return;
1544 }
1546 final Tab selectedTab = Tabs.getInstance().getSelectedTab();
1547 mTargetTabForEditingMode = (selectedTab != null ? selectedTab.getId() : null);
1549 final PropertyAnimator animator = new PropertyAnimator(250);
1550 animator.setUseHardwareLayer(false);
1552 mBrowserToolbar.startEditing(url, animator);
1554 final String panelId = selectedTab.getMostRecentHomePanel();
1555 showHomePagerWithAnimator(panelId, animator);
1557 animator.start();
1558 Telemetry.startUISession(TelemetryContract.Session.AWESOMESCREEN);
1559 }
1561 private void commitEditingMode() {
1562 if (!mBrowserToolbar.isEditing()) {
1563 return;
1564 }
1566 Telemetry.stopUISession(TelemetryContract.Session.AWESOMESCREEN,
1567 TelemetryContract.Reason.COMMIT);
1569 final String url = mBrowserToolbar.commitEdit();
1571 // HACK: We don't know the url that will be loaded when hideHomePager is initially called
1572 // in BrowserToolbar's onStopEditing listener so on the awesomescreen, hideHomePager will
1573 // use the url "about:home" and return without taking any action. hideBrowserSearch is
1574 // then called, but since hideHomePager changes both HomePager and LayerView visibility
1575 // and exited without taking an action, no Views are displayed and graphical corruption is
1576 // visible instead.
1577 //
1578 // Here we call hideHomePager for the second time with the URL to be loaded so that
1579 // hideHomePager is called with the correct state for the upcoming page load.
1580 //
1581 // Expected to be fixed by bug 915825.
1582 hideHomePager(url);
1584 // Don't do anything if the user entered an empty URL.
1585 if (TextUtils.isEmpty(url)) {
1586 return;
1587 }
1589 // If the URL doesn't look like a search query, just load it.
1590 if (!StringUtils.isSearchQuery(url, true)) {
1591 Tabs.getInstance().loadUrl(url, Tabs.LOADURL_USER_ENTERED);
1593 Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL);
1594 return;
1595 }
1597 // Otherwise, check for a bookmark keyword.
1598 ThreadUtils.postToBackgroundThread(new Runnable() {
1599 @Override
1600 public void run() {
1601 final String keyword;
1602 final String keywordSearch;
1604 final int index = url.indexOf(" ");
1605 if (index == -1) {
1606 keyword = url;
1607 keywordSearch = "";
1608 } else {
1609 keyword = url.substring(0, index);
1610 keywordSearch = url.substring(index + 1);
1611 }
1613 final String keywordUrl = BrowserDB.getUrlForKeyword(getContentResolver(), keyword);
1615 // If there isn't a bookmark keyword, load the url. This may result in a query
1616 // using the default search engine.
1617 if (TextUtils.isEmpty(keywordUrl)) {
1618 Tabs.getInstance().loadUrl(url, Tabs.LOADURL_USER_ENTERED);
1619 Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL);
1620 return;
1621 }
1623 recordSearch(null, "barkeyword");
1625 // Otherwise, construct a search query from the bookmark keyword.
1626 final String searchUrl = keywordUrl.replace("%s", URLEncoder.encode(keywordSearch));
1627 Tabs.getInstance().loadUrl(searchUrl, Tabs.LOADURL_USER_ENTERED);
1628 Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, "", "keyword");
1629 }
1630 });
1631 }
1633 /**
1634 * Record in Health Report that a search has occurred.
1635 *
1636 * @param engine
1637 * a search engine instance. Can be null.
1638 * @param where
1639 * where the search was initialized; one of the values in
1640 * {@link BrowserHealthRecorder#SEARCH_LOCATIONS}.
1641 */
1642 private static void recordSearch(SearchEngine engine, String where) {
1643 Log.i(LOGTAG, "Recording search: " +
1644 ((engine == null) ? "null" : engine.name) +
1645 ", " + where);
1646 try {
1647 String identifier = (engine == null) ? "other" : engine.getEngineIdentifier();
1648 JSONObject message = new JSONObject();
1649 message.put("type", BrowserHealthRecorder.EVENT_SEARCH);
1650 message.put("location", where);
1651 message.put("identifier", identifier);
1652 GeckoAppShell.getEventDispatcher().dispatchEvent(message, null);
1653 } catch (Exception e) {
1654 Log.w(LOGTAG, "Error recording search.", e);
1655 }
1656 }
1658 void filterEditingMode(String searchTerm, AutocompleteHandler handler) {
1659 if (TextUtils.isEmpty(searchTerm)) {
1660 hideBrowserSearch();
1661 } else {
1662 showBrowserSearch();
1663 mBrowserSearch.filter(searchTerm, handler);
1664 }
1665 }
1667 /**
1668 * Selects the target tab for editing mode. This is expected to be the tab selected on editing
1669 * mode entry, unless it is subsequently overridden.
1670 *
1671 * A background tab may be selected while editing mode is active (e.g. popups), causing the
1672 * new url to load in the newly selected tab. Call this method on editing mode exit to
1673 * mitigate this.
1674 */
1675 private void selectTargetTabForEditingMode() {
1676 if (mTargetTabForEditingMode != null) {
1677 Tabs.getInstance().selectTab(mTargetTabForEditingMode);
1678 }
1680 mTargetTabForEditingMode = null;
1681 }
1683 /**
1684 * Shows or hides the home pager for the given tab.
1685 */
1686 private void updateHomePagerForTab(Tab tab) {
1687 // Don't change the visibility of the home pager if we're in editing mode.
1688 if (mBrowserToolbar.isEditing()) {
1689 return;
1690 }
1692 if (isAboutHome(tab)) {
1693 String panelId = AboutPages.getPanelIdFromAboutHomeUrl(tab.getURL());
1694 if (panelId == null) {
1695 // No panel was specified in the URL. Try loading the most recent
1696 // home panel for this tab.
1697 panelId = tab.getMostRecentHomePanel();
1698 }
1699 showHomePager(panelId);
1701 if (mDynamicToolbar.isEnabled()) {
1702 mDynamicToolbar.setVisible(true, VisibilityTransition.ANIMATE);
1703 }
1704 } else {
1705 hideHomePager();
1706 }
1707 }
1709 @Override
1710 public void onLocaleReady(final String locale) {
1711 super.onLocaleReady(locale);
1713 HomePanelsManager.getInstance().onLocaleReady(locale);
1715 if (mMenu != null) {
1716 mMenu.clear();
1717 onCreateOptionsMenu(mMenu);
1718 }
1719 }
1721 private void showHomePager(String panelId) {
1722 showHomePagerWithAnimator(panelId, null);
1723 }
1725 private void showHomePagerWithAnimator(String panelId, PropertyAnimator animator) {
1726 if (isHomePagerVisible()) {
1727 // Home pager already visible, make sure it shows the correct panel.
1728 mHomePager.showPanel(panelId);
1729 return;
1730 }
1732 // Refresh toolbar height to possibly restore the toolbar padding
1733 refreshToolbarHeight();
1735 // Show the toolbar before hiding about:home so the
1736 // onMetricsChanged callback still works.
1737 if (mDynamicToolbar.isEnabled()) {
1738 mDynamicToolbar.setVisible(true, VisibilityTransition.IMMEDIATE);
1739 }
1741 if (mHomePager == null) {
1742 final ViewStub homePagerStub = (ViewStub) findViewById(R.id.home_pager_stub);
1743 mHomePager = (HomePager) homePagerStub.inflate();
1745 mHomePager.setOnPanelChangeListener(new HomePager.OnPanelChangeListener() {
1746 @Override
1747 public void onPanelSelected(String panelId) {
1748 final Tab currentTab = Tabs.getInstance().getSelectedTab();
1749 if (currentTab != null) {
1750 currentTab.setMostRecentHomePanel(panelId);
1751 }
1752 }
1753 });
1755 // Don't show the banner in guest mode.
1756 if (!getProfile().inGuestMode()) {
1757 final ViewStub homeBannerStub = (ViewStub) findViewById(R.id.home_banner_stub);
1758 final HomeBanner homeBanner = (HomeBanner) homeBannerStub.inflate();
1759 mHomePager.setBanner(homeBanner);
1761 // Remove the banner from the view hierarchy if it is dismissed.
1762 homeBanner.setOnDismissListener(new HomeBanner.OnDismissListener() {
1763 @Override
1764 public void onDismiss() {
1765 mHomePager.setBanner(null);
1766 mHomePagerContainer.removeView(homeBanner);
1767 }
1768 });
1769 }
1770 }
1772 mHomePagerContainer.setVisibility(View.VISIBLE);
1773 mHomePager.load(getSupportLoaderManager(),
1774 getSupportFragmentManager(),
1775 panelId, animator);
1777 // Hide the web content so it cannot be focused by screen readers.
1778 hideWebContentOnPropertyAnimationEnd(animator);
1779 }
1781 private void hideWebContentOnPropertyAnimationEnd(final PropertyAnimator animator) {
1782 if (animator == null) {
1783 hideWebContent();
1784 return;
1785 }
1787 animator.addPropertyAnimationListener(new PropertyAnimator.PropertyAnimationListener() {
1788 @Override
1789 public void onPropertyAnimationStart() {
1790 mHideWebContentOnAnimationEnd = true;
1791 }
1793 @Override
1794 public void onPropertyAnimationEnd() {
1795 if (mHideWebContentOnAnimationEnd) {
1796 hideWebContent();
1797 }
1798 }
1799 });
1800 }
1802 private void hideWebContent() {
1803 // The view is set to INVISIBLE, rather than GONE, to avoid
1804 // the additional requestLayout() call.
1805 mLayerView.setVisibility(View.INVISIBLE);
1806 }
1808 /**
1809 * Hides the HomePager, using the url of the currently selected tab as the url to be
1810 * loaded.
1811 */
1812 private void hideHomePager() {
1813 final Tab selectedTab = Tabs.getInstance().getSelectedTab();
1814 final String url = (selectedTab != null) ? selectedTab.getURL() : null;
1816 hideHomePager(url);
1817 }
1819 /**
1820 * Hides the HomePager. The given url should be the url of the page to be loaded, or null
1821 * if a new page is not being loaded.
1822 */
1823 private void hideHomePager(final String url) {
1824 if (!isHomePagerVisible() || AboutPages.isAboutHome(url)) {
1825 return;
1826 }
1828 // Prevent race in hiding web content - see declaration for more info.
1829 mHideWebContentOnAnimationEnd = false;
1831 // Display the previously hidden web content (which prevented screen reader access).
1832 mLayerView.setVisibility(View.VISIBLE);
1833 mHomePagerContainer.setVisibility(View.GONE);
1835 if (mHomePager != null) {
1836 mHomePager.unload();
1837 }
1839 mBrowserToolbar.setNextFocusDownId(R.id.layer_view);
1841 // Refresh toolbar height to possibly restore the toolbar padding
1842 refreshToolbarHeight();
1843 }
1845 private void showBrowserSearch() {
1846 if (mBrowserSearch.getUserVisibleHint()) {
1847 return;
1848 }
1850 mBrowserSearchContainer.setVisibility(View.VISIBLE);
1852 // Prevent overdraw by hiding the underlying HomePager View.
1853 mHomePager.setVisibility(View.INVISIBLE);
1855 final FragmentManager fm = getSupportFragmentManager();
1857 // In certain situations, showBrowserSearch() can be called immediately after hideBrowserSearch()
1858 // (see bug 925012). Because of an Android bug (http://code.google.com/p/android/issues/detail?id=61179),
1859 // calling FragmentTransaction#add immediately after FragmentTransaction#remove won't add the fragment's
1860 // view to the layout. Calling FragmentManager#executePendingTransactions before re-adding the fragment
1861 // prevents this issue.
1862 fm.executePendingTransactions();
1864 fm.beginTransaction().add(R.id.search_container, mBrowserSearch, BROWSER_SEARCH_TAG).commitAllowingStateLoss();
1865 mBrowserSearch.setUserVisibleHint(true);
1866 }
1868 private void hideBrowserSearch() {
1869 if (!mBrowserSearch.getUserVisibleHint()) {
1870 return;
1871 }
1873 // To prevent overdraw, the HomePager is hidden when BrowserSearch is displayed:
1874 // reverse that.
1875 mHomePager.setVisibility(View.VISIBLE);
1877 mBrowserSearchContainer.setVisibility(View.INVISIBLE);
1879 getSupportFragmentManager().beginTransaction()
1880 .remove(mBrowserSearch).commitAllowingStateLoss();
1881 mBrowserSearch.setUserVisibleHint(false);
1882 }
1884 private class HideTabsTouchListener implements TouchEventInterceptor {
1885 private boolean mIsHidingTabs = false;
1887 @Override
1888 public boolean onInterceptTouchEvent(View view, MotionEvent event) {
1889 // We need to account for scroll state for the touched view otherwise
1890 // tapping on an "empty" part of the view will still be considered a
1891 // valid touch event.
1892 if (view.getScrollX() != 0 || view.getScrollY() != 0) {
1893 Rect rect = new Rect();
1894 view.getHitRect(rect);
1895 rect.offset(-view.getScrollX(), -view.getScrollY());
1897 int[] viewCoords = new int[2];
1898 view.getLocationOnScreen(viewCoords);
1900 int x = (int) event.getRawX() - viewCoords[0];
1901 int y = (int) event.getRawY() - viewCoords[1];
1903 if (!rect.contains(x, y))
1904 return false;
1905 }
1907 // If the tab tray is showing, hide the tab tray and don't send the event to content.
1908 if (event.getActionMasked() == MotionEvent.ACTION_DOWN && autoHideTabs()) {
1909 mIsHidingTabs = true;
1910 return true;
1911 }
1912 return false;
1913 }
1915 @Override
1916 public boolean onTouch(View view, MotionEvent event) {
1917 if (mIsHidingTabs) {
1918 // Keep consuming events until the gesture finishes.
1919 int action = event.getActionMasked();
1920 if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
1921 mIsHidingTabs = false;
1922 }
1923 return true;
1924 }
1925 return false;
1926 }
1927 }
1929 private static Menu findParentMenu(Menu menu, MenuItem item) {
1930 final int itemId = item.getItemId();
1932 final int count = (menu != null) ? menu.size() : 0;
1933 for (int i = 0; i < count; i++) {
1934 MenuItem menuItem = menu.getItem(i);
1935 if (menuItem.getItemId() == itemId) {
1936 return menu;
1937 }
1938 if (menuItem.hasSubMenu()) {
1939 Menu parent = findParentMenu(menuItem.getSubMenu(), item);
1940 if (parent != null) {
1941 return parent;
1942 }
1943 }
1944 }
1946 return null;
1947 }
1949 /**
1950 * Add the provided item to the provided menu, which should be
1951 * the root (mMenu).
1952 */
1953 private void addAddonMenuItemToMenu(final Menu menu, final MenuItemInfo info) {
1954 info.added = true;
1956 final Menu destination;
1957 if (info.parent == 0) {
1958 destination = menu;
1959 } else if (info.parent == GECKO_TOOLS_MENU) {
1960 MenuItem tools = menu.findItem(R.id.tools);
1961 destination = tools != null ? tools.getSubMenu() : menu;
1962 } else {
1963 MenuItem parent = menu.findItem(info.parent);
1964 if (parent == null) {
1965 return;
1966 }
1968 Menu parentMenu = findParentMenu(menu, parent);
1970 if (!parent.hasSubMenu()) {
1971 parentMenu.removeItem(parent.getItemId());
1972 destination = parentMenu.addSubMenu(Menu.NONE, parent.getItemId(), Menu.NONE, parent.getTitle());
1973 if (parent.getIcon() != null) {
1974 ((SubMenu) destination).getItem().setIcon(parent.getIcon());
1975 }
1976 } else {
1977 destination = parent.getSubMenu();
1978 }
1979 }
1981 MenuItem item = destination.add(Menu.NONE, info.id, Menu.NONE, info.label);
1983 item.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
1984 @Override
1985 public boolean onMenuItemClick(MenuItem item) {
1986 Log.i(LOGTAG, "Menu item clicked");
1987 GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Menu:Clicked", Integer.toString(info.id - ADDON_MENU_OFFSET)));
1988 return true;
1989 }
1990 });
1992 if (info.icon == null) {
1993 item.setIcon(R.drawable.ic_menu_addons_filler);
1994 } else {
1995 final int id = info.id;
1996 BitmapUtils.getDrawable(this, info.icon, new BitmapUtils.BitmapLoader() {
1997 @Override
1998 public void onBitmapFound(Drawable d) {
1999 // TODO: why do we re-find the item?
2000 MenuItem item = destination.findItem(id);
2001 if (item == null) {
2002 return;
2003 }
2004 if (d == null) {
2005 item.setIcon(R.drawable.ic_menu_addons_filler);
2006 return;
2007 }
2008 item.setIcon(d);
2009 }
2010 });
2011 }
2013 item.setCheckable(info.checkable);
2014 item.setChecked(info.checked);
2015 item.setEnabled(info.enabled);
2016 item.setVisible(info.visible);
2017 }
2019 private void addAddonMenuItem(final MenuItemInfo info) {
2020 if (mAddonMenuItemsCache == null) {
2021 mAddonMenuItemsCache = new Vector<MenuItemInfo>();
2022 }
2024 // Mark it as added if the menu was ready.
2025 info.added = (mMenu != null);
2027 // Always cache so we can rebuild after a locale switch.
2028 mAddonMenuItemsCache.add(info);
2030 if (mMenu == null) {
2031 return;
2032 }
2034 addAddonMenuItemToMenu(mMenu, info);
2035 }
2037 private void removeAddonMenuItem(int id) {
2038 // Remove add-on menu item from cache, if available.
2039 if (mAddonMenuItemsCache != null && !mAddonMenuItemsCache.isEmpty()) {
2040 for (MenuItemInfo item : mAddonMenuItemsCache) {
2041 if (item.id == id) {
2042 mAddonMenuItemsCache.remove(item);
2043 break;
2044 }
2045 }
2046 }
2048 if (mMenu == null)
2049 return;
2051 MenuItem menuItem = mMenu.findItem(id);
2052 if (menuItem != null)
2053 mMenu.removeItem(id);
2054 }
2056 private void updateAddonMenuItem(int id, JSONObject options) {
2057 // Set attribute for the menu item in cache, if available
2058 if (mAddonMenuItemsCache != null && !mAddonMenuItemsCache.isEmpty()) {
2059 for (MenuItemInfo item : mAddonMenuItemsCache) {
2060 if (item.id == id) {
2061 item.label = options.optString("name", item.label);
2062 item.checkable = options.optBoolean("checkable", item.checkable);
2063 item.checked = options.optBoolean("checked", item.checked);
2064 item.enabled = options.optBoolean("enabled", item.enabled);
2065 item.visible = options.optBoolean("visible", item.visible);
2066 item.added = (mMenu != null);
2067 break;
2068 }
2069 }
2070 }
2072 if (mMenu == null) {
2073 return;
2074 }
2076 MenuItem menuItem = mMenu.findItem(id);
2077 if (menuItem != null) {
2078 menuItem.setTitle(options.optString("name", menuItem.getTitle().toString()));
2079 menuItem.setCheckable(options.optBoolean("checkable", menuItem.isCheckable()));
2080 menuItem.setChecked(options.optBoolean("checked", menuItem.isChecked()));
2081 menuItem.setEnabled(options.optBoolean("enabled", menuItem.isEnabled()));
2082 menuItem.setVisible(options.optBoolean("visible", menuItem.isVisible()));
2083 }
2084 }
2086 @Override
2087 public boolean onCreateOptionsMenu(Menu menu) {
2088 // Sets mMenu = menu.
2089 super.onCreateOptionsMenu(menu);
2091 // Inform the menu about the action-items bar.
2092 if (menu instanceof GeckoMenu &&
2093 HardwareUtils.isTablet()) {
2094 ((GeckoMenu) menu).setActionItemBarPresenter(mBrowserToolbar);
2095 }
2097 MenuInflater inflater = getMenuInflater();
2098 inflater.inflate(R.menu.browser_app_menu, mMenu);
2100 // Add add-on menu items, if any exist.
2101 if (mAddonMenuItemsCache != null && !mAddonMenuItemsCache.isEmpty()) {
2102 for (MenuItemInfo item : mAddonMenuItemsCache) {
2103 addAddonMenuItemToMenu(mMenu, item);
2104 }
2105 }
2107 // Action providers are available only ICS+.
2108 if (Build.VERSION.SDK_INT >= 14) {
2109 GeckoMenuItem share = (GeckoMenuItem) mMenu.findItem(R.id.share);
2110 GeckoActionProvider provider = GeckoActionProvider.getForType(GeckoActionProvider.DEFAULT_MIME_TYPE, this);
2111 share.setActionProvider(provider);
2112 }
2114 return true;
2115 }
2117 @Override
2118 public void openOptionsMenu() {
2119 if (!hasTabsSideBar() && areTabsShown())
2120 return;
2122 // Scroll custom menu to the top
2123 if (mMenuPanel != null)
2124 mMenuPanel.scrollTo(0, 0);
2126 if (!mBrowserToolbar.openOptionsMenu())
2127 super.openOptionsMenu();
2129 if (mDynamicToolbar.isEnabled()) {
2130 mDynamicToolbar.setVisible(true, VisibilityTransition.ANIMATE);
2131 }
2132 }
2134 @Override
2135 public void closeOptionsMenu() {
2136 if (!mBrowserToolbar.closeOptionsMenu())
2137 super.closeOptionsMenu();
2138 }
2140 @Override
2141 public void setFullScreen(final boolean fullscreen) {
2142 super.setFullScreen(fullscreen);
2143 ThreadUtils.postToUiThread(new Runnable() {
2144 @Override
2145 public void run() {
2146 if (fullscreen) {
2147 mViewFlipper.setVisibility(View.GONE);
2148 if (mDynamicToolbar.isEnabled()) {
2149 mDynamicToolbar.setVisible(false, VisibilityTransition.IMMEDIATE);
2150 mLayerView.getLayerMarginsAnimator().setMaxMargins(0, 0, 0, 0);
2151 } else {
2152 setToolbarMargin(0);
2153 }
2154 } else {
2155 mViewFlipper.setVisibility(View.VISIBLE);
2156 if (mDynamicToolbar.isEnabled()) {
2157 mDynamicToolbar.setVisible(true, VisibilityTransition.IMMEDIATE);
2158 mLayerView.getLayerMarginsAnimator().setMaxMargins(0, mToolbarHeight, 0, 0);
2159 }
2160 }
2161 }
2162 });
2163 }
2165 @Override
2166 public boolean onPrepareOptionsMenu(Menu aMenu) {
2167 if (aMenu == null)
2168 return false;
2170 if (!GeckoThread.checkLaunchState(GeckoThread.LaunchState.GeckoRunning))
2171 aMenu.findItem(R.id.settings).setEnabled(false);
2173 Tab tab = Tabs.getInstance().getSelectedTab();
2174 MenuItem bookmark = aMenu.findItem(R.id.bookmark);
2175 MenuItem back = aMenu.findItem(R.id.back);
2176 MenuItem forward = aMenu.findItem(R.id.forward);
2177 MenuItem share = aMenu.findItem(R.id.share);
2178 MenuItem saveAsPDF = aMenu.findItem(R.id.save_as_pdf);
2179 MenuItem charEncoding = aMenu.findItem(R.id.char_encoding);
2180 MenuItem findInPage = aMenu.findItem(R.id.find_in_page);
2181 MenuItem desktopMode = aMenu.findItem(R.id.desktop_mode);
2182 MenuItem enterGuestMode = aMenu.findItem(R.id.new_guest_session);
2183 MenuItem exitGuestMode = aMenu.findItem(R.id.exit_guest_session);
2185 // Only show the "Quit" menu item on pre-ICS or television devices.
2186 // In ICS+, it's easy to kill an app through the task switcher.
2187 aMenu.findItem(R.id.quit).setVisible(Build.VERSION.SDK_INT < 14 || HardwareUtils.isTelevision());
2189 if (tab == null || tab.getURL() == null) {
2190 bookmark.setEnabled(false);
2191 back.setEnabled(false);
2192 forward.setEnabled(false);
2193 share.setEnabled(false);
2194 saveAsPDF.setEnabled(false);
2195 findInPage.setEnabled(false);
2197 // NOTE: Use MenuUtils.safeSetEnabled because some actions might
2198 // be on the BrowserToolbar context menu
2199 MenuUtils.safeSetEnabled(aMenu, R.id.page, false);
2200 MenuUtils.safeSetEnabled(aMenu, R.id.subscribe, false);
2201 MenuUtils.safeSetEnabled(aMenu, R.id.add_search_engine, false);
2202 MenuUtils.safeSetEnabled(aMenu, R.id.site_settings, false);
2203 MenuUtils.safeSetEnabled(aMenu, R.id.add_to_launcher, false);
2205 return true;
2206 }
2208 bookmark.setEnabled(!AboutPages.isAboutReader(tab.getURL()));
2209 bookmark.setVisible(!GeckoProfile.get(this).inGuestMode());
2210 bookmark.setCheckable(true);
2211 bookmark.setChecked(tab.isBookmark());
2212 bookmark.setIcon(tab.isBookmark() ? R.drawable.ic_menu_bookmark_remove : R.drawable.ic_menu_bookmark_add);
2214 back.setEnabled(tab.canDoBack());
2215 forward.setEnabled(tab.canDoForward());
2216 desktopMode.setChecked(tab.getDesktopMode());
2217 desktopMode.setIcon(tab.getDesktopMode() ? R.drawable.ic_menu_desktop_mode_on : R.drawable.ic_menu_desktop_mode_off);
2219 String url = tab.getURL();
2220 if (AboutPages.isAboutReader(url)) {
2221 String urlFromReader = ReaderModeUtils.getUrlFromAboutReader(url);
2222 if (urlFromReader != null) {
2223 url = urlFromReader;
2224 }
2225 }
2227 // Disable share menuitem for about:, chrome:, file:, and resource: URIs
2228 String scheme = Uri.parse(url).getScheme();
2229 share.setVisible(!GeckoProfile.get(this).inGuestMode());
2230 share.setEnabled(!(scheme.equals("about") || scheme.equals("chrome") ||
2231 scheme.equals("file") || scheme.equals("resource")));
2233 // NOTE: Use MenuUtils.safeSetEnabled because some actions might
2234 // be on the BrowserToolbar context menu
2235 MenuUtils.safeSetEnabled(aMenu, R.id.page, !isAboutHome(tab));
2236 MenuUtils.safeSetEnabled(aMenu, R.id.subscribe, tab.hasFeeds());
2237 MenuUtils.safeSetEnabled(aMenu, R.id.add_search_engine, tab.hasOpenSearch());
2238 MenuUtils.safeSetEnabled(aMenu, R.id.site_settings, !isAboutHome(tab));
2239 MenuUtils.safeSetEnabled(aMenu, R.id.add_to_launcher, !isAboutHome(tab));
2241 // Action providers are available only ICS+.
2242 if (Build.VERSION.SDK_INT >= 14) {
2243 final GeckoActionProvider provider = ((GeckoMenuItem) share).getGeckoActionProvider();
2244 if (provider != null) {
2245 Intent shareIntent = provider.getIntent();
2247 // For efficiency, the provider's intent is only set once
2248 if (shareIntent == null) {
2249 shareIntent = new Intent(Intent.ACTION_SEND);
2250 shareIntent.setType("text/plain");
2251 provider.setIntent(shareIntent);
2252 }
2254 // Replace the existing intent's extras
2255 shareIntent.putExtra(Intent.EXTRA_TEXT, url);
2256 shareIntent.putExtra(Intent.EXTRA_SUBJECT, tab.getDisplayTitle());
2257 shareIntent.putExtra(Intent.EXTRA_TITLE, tab.getDisplayTitle());
2259 // Clear the existing thumbnail extras so we don't share an old thumbnail.
2260 shareIntent.removeExtra("share_screenshot_uri");
2262 // Include the thumbnail of the page being shared.
2263 BitmapDrawable drawable = tab.getThumbnail();
2264 if (drawable != null) {
2265 Bitmap thumbnail = drawable.getBitmap();
2267 // Kobo uses a custom intent extra for sharing thumbnails.
2268 if (Build.MANUFACTURER.equals("Kobo") && thumbnail != null) {
2269 File cacheDir = getExternalCacheDir();
2271 if (cacheDir != null) {
2272 File outFile = new File(cacheDir, "thumbnail.png");
2274 try {
2275 java.io.FileOutputStream out = new java.io.FileOutputStream(outFile);
2276 thumbnail.compress(Bitmap.CompressFormat.PNG, 90, out);
2277 } catch (FileNotFoundException e) {
2278 Log.e(LOGTAG, "File not found", e);
2279 }
2281 shareIntent.putExtra("share_screenshot_uri", Uri.parse(outFile.getPath()));
2282 }
2283 }
2284 }
2285 }
2286 }
2288 // Disable save as PDF for about:home and xul pages.
2289 saveAsPDF.setEnabled(!(isAboutHome(tab) ||
2290 tab.getContentType().equals("application/vnd.mozilla.xul+xml")));
2292 // Disable find in page for about:home, since it won't work on Java content.
2293 findInPage.setEnabled(!isAboutHome(tab));
2295 charEncoding.setVisible(GeckoPreferences.getCharEncodingState());
2297 if (mProfile.inGuestMode())
2298 exitGuestMode.setVisible(true);
2299 else
2300 enterGuestMode.setVisible(true);
2302 return true;
2303 }
2305 @Override
2306 public boolean onOptionsItemSelected(MenuItem item) {
2307 Tab tab = null;
2308 Intent intent = null;
2310 final int itemId = item.getItemId();
2312 // Track the menu action. We don't know much about the context, but we can use this to determine
2313 // the frequency of use for various actions.
2314 Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.MENU, getResources().getResourceEntryName(itemId));
2316 if (itemId == R.id.bookmark) {
2317 tab = Tabs.getInstance().getSelectedTab();
2318 if (tab != null) {
2319 if (item.isChecked()) {
2320 tab.removeBookmark();
2321 Toast.makeText(this, R.string.bookmark_removed, Toast.LENGTH_SHORT).show();
2322 item.setIcon(R.drawable.ic_menu_bookmark_add);
2323 } else {
2324 tab.addBookmark();
2325 getButtonToast().show(false,
2326 getResources().getString(R.string.bookmark_added),
2327 getResources().getString(R.string.bookmark_options),
2328 null,
2329 new ButtonToast.ToastListener() {
2330 @Override
2331 public void onButtonClicked() {
2332 showBookmarkDialog();
2333 }
2335 @Override
2336 public void onToastHidden(ButtonToast.ReasonHidden reason) { }
2337 });
2338 item.setIcon(R.drawable.ic_menu_bookmark_remove);
2339 }
2340 }
2341 return true;
2342 }
2344 if (itemId == R.id.share) {
2345 shareCurrentUrl();
2346 return true;
2347 }
2349 if (itemId == R.id.reload) {
2350 tab = Tabs.getInstance().getSelectedTab();
2351 if (tab != null)
2352 tab.doReload();
2353 return true;
2354 }
2356 if (itemId == R.id.back) {
2357 tab = Tabs.getInstance().getSelectedTab();
2358 if (tab != null)
2359 tab.doBack();
2360 return true;
2361 }
2363 if (itemId == R.id.forward) {
2364 tab = Tabs.getInstance().getSelectedTab();
2365 if (tab != null)
2366 tab.doForward();
2367 return true;
2368 }
2370 if (itemId == R.id.save_as_pdf) {
2371 GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("SaveAs:PDF", null));
2372 return true;
2373 }
2375 if (itemId == R.id.settings) {
2376 intent = new Intent(this, GeckoPreferences.class);
2377 startActivity(intent);
2378 return true;
2379 }
2381 if (itemId == R.id.addons) {
2382 Tabs.getInstance().loadUrlInTab(AboutPages.ADDONS);
2383 return true;
2384 }
2386 if (itemId == R.id.apps) {
2387 Tabs.getInstance().loadUrlInTab(AboutPages.APPS);
2388 return true;
2389 }
2391 if (itemId == R.id.downloads) {
2392 Tabs.getInstance().loadUrlInTab(AboutPages.DOWNLOADS);
2393 return true;
2394 }
2396 if (itemId == R.id.char_encoding) {
2397 GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("CharEncoding:Get", null));
2398 return true;
2399 }
2401 if (itemId == R.id.find_in_page) {
2402 mFindInPageBar.show();
2403 return true;
2404 }
2406 if (itemId == R.id.desktop_mode) {
2407 Tab selectedTab = Tabs.getInstance().getSelectedTab();
2408 if (selectedTab == null)
2409 return true;
2410 JSONObject args = new JSONObject();
2411 try {
2412 args.put("desktopMode", !item.isChecked());
2413 args.put("tabId", selectedTab.getId());
2414 } catch (JSONException e) {
2415 Log.e(LOGTAG, "error building json arguments");
2416 }
2417 GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("DesktopMode:Change", args.toString()));
2418 return true;
2419 }
2421 if (itemId == R.id.new_tab) {
2422 addTab();
2423 return true;
2424 }
2426 if (itemId == R.id.new_private_tab) {
2427 addPrivateTab();
2428 return true;
2429 }
2431 if (itemId == R.id.new_guest_session) {
2432 showGuestModeDialog(GuestModeDialog.ENTERING);
2433 return true;
2434 }
2436 if (itemId == R.id.exit_guest_session) {
2437 showGuestModeDialog(GuestModeDialog.LEAVING);
2438 return true;
2439 }
2441 // We have a few menu items that can also be in the context menu. If
2442 // we have not already handled the item, give the context menu handler
2443 // a chance.
2444 if (onContextItemSelected(item)) {
2445 return true;
2446 }
2448 return super.onOptionsItemSelected(item);
2449 }
2451 private void showGuestModeDialog(final GuestModeDialog type) {
2452 final Prompt ps = new Prompt(this, new Prompt.PromptCallback() {
2453 @Override
2454 public void onPromptFinished(String result) {
2455 try {
2456 int itemId = new JSONObject(result).getInt("button");
2457 if (itemId == 0) {
2458 String args = "";
2459 if (type == GuestModeDialog.ENTERING) {
2460 args = GUEST_BROWSING_ARG;
2461 } else {
2462 GeckoProfile.leaveGuestSession(BrowserApp.this);
2463 }
2464 doRestart(args);
2465 GeckoAppShell.systemExit();
2466 }
2467 } catch(JSONException ex) {
2468 Log.e(LOGTAG, "Exception reading guest mode prompt result", ex);
2469 }
2470 }
2471 });
2473 Resources res = getResources();
2474 ps.setButtons(new String[] {
2475 res.getString(R.string.guest_session_dialog_continue),
2476 res.getString(R.string.guest_session_dialog_cancel)
2477 });
2479 int titleString = 0;
2480 int msgString = 0;
2481 if (type == GuestModeDialog.ENTERING) {
2482 titleString = R.string.new_guest_session_title;
2483 msgString = R.string.new_guest_session_text;
2484 } else {
2485 titleString = R.string.exit_guest_session_title;
2486 msgString = R.string.exit_guest_session_text;
2487 }
2489 ps.show(res.getString(titleString), res.getString(msgString), null, ListView.CHOICE_MODE_NONE);
2490 }
2492 /**
2493 * This will detect if the key pressed is back. If so, will show the history.
2494 */
2495 @Override
2496 public boolean onKeyLongPress(int keyCode, KeyEvent event) {
2497 if (keyCode == KeyEvent.KEYCODE_BACK) {
2498 Tab tab = Tabs.getInstance().getSelectedTab();
2499 if (tab != null) {
2500 return tab.showAllHistory();
2501 }
2502 }
2503 return super.onKeyLongPress(keyCode, event);
2504 }
2506 /*
2507 * If the app has been launched a certain number of times, and we haven't asked for feedback before,
2508 * open a new tab with about:feedback when launching the app from the icon shortcut.
2509 */
2510 @Override
2511 protected void onNewIntent(Intent intent) {
2512 super.onNewIntent(intent);
2514 String action = intent.getAction();
2516 if (AppConstants.MOZ_ANDROID_BEAM && Build.VERSION.SDK_INT >= 10 && NfcAdapter.ACTION_NDEF_DISCOVERED.equals(action)) {
2517 String uri = intent.getDataString();
2518 GeckoAppShell.sendEventToGecko(GeckoEvent.createURILoadEvent(uri));
2519 }
2521 if (!mInitialized) {
2522 return;
2523 }
2525 if (Intent.ACTION_VIEW.equals(action)) {
2526 // Dismiss editing mode if the user is loading a URL from an external app.
2527 mBrowserToolbar.cancelEdit();
2529 Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.INTENT);
2530 return;
2531 }
2533 // Only solicit feedback when the app has been launched from the icon shortcut.
2534 if (!Intent.ACTION_MAIN.equals(action)) {
2535 return;
2536 }
2538 (new UiAsyncTask<Void, Void, Boolean>(ThreadUtils.getBackgroundHandler()) {
2539 @Override
2540 public synchronized Boolean doInBackground(Void... params) {
2541 // Check to see how many times the app has been launched.
2542 SharedPreferences settings = getPreferences(Activity.MODE_PRIVATE);
2543 String keyName = getPackageName() + ".feedback_launch_count";
2544 int launchCount = settings.getInt(keyName, 0);
2545 if (launchCount >= FEEDBACK_LAUNCH_COUNT)
2546 return false;
2548 // Increment the launch count and store the new value.
2549 launchCount++;
2550 settings.edit().putInt(keyName, launchCount).commit();
2552 // If we've reached our magic number, show the feedback page.
2553 return launchCount == FEEDBACK_LAUNCH_COUNT;
2554 }
2556 @Override
2557 public void onPostExecute(Boolean shouldShowFeedbackPage) {
2558 if (shouldShowFeedbackPage)
2559 GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Feedback:Show", null));
2560 }
2561 }).execute();
2562 }
2564 @Override
2565 protected NotificationClient makeNotificationClient() {
2566 // The service is local to Fennec, so we can use it to keep
2567 // Fennec alive during downloads.
2568 return new ServiceNotificationClient(getApplicationContext());
2569 }
2571 private void resetFeedbackLaunchCount() {
2572 ThreadUtils.postToBackgroundThread(new Runnable() {
2573 @Override
2574 public synchronized void run() {
2575 SharedPreferences settings = getPreferences(Activity.MODE_PRIVATE);
2576 settings.edit().putInt(getPackageName() + ".feedback_launch_count", 0).commit();
2577 }
2578 });
2579 }
2581 private void getLastUrl() {
2582 (new UiAsyncTask<Void, Void, String>(ThreadUtils.getBackgroundHandler()) {
2583 @Override
2584 public synchronized String doInBackground(Void... params) {
2585 // Get the most recent URL stored in browser history.
2586 String url = "";
2587 Cursor c = null;
2588 try {
2589 c = BrowserDB.getRecentHistory(getContentResolver(), 1);
2590 if (c.moveToFirst()) {
2591 url = c.getString(c.getColumnIndexOrThrow(Combined.URL));
2592 }
2593 } finally {
2594 if (c != null)
2595 c.close();
2596 }
2597 return url;
2598 }
2600 @Override
2601 public void onPostExecute(String url) {
2602 // Don't bother sending a message if there is no URL.
2603 if (url.length() > 0)
2604 GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Feedback:LastUrl", url));
2605 }
2606 }).execute();
2607 }
2609 // HomePager.OnNewTabsListener
2610 @Override
2611 public void onNewTabs(String[] urls) {
2612 final EnumSet<OnUrlOpenListener.Flags> flags = EnumSet.of(OnUrlOpenListener.Flags.ALLOW_SWITCH_TO_TAB);
2614 for (String url : urls) {
2615 if (!maybeSwitchToTab(url, flags)) {
2616 openUrlAndStopEditing(url, true);
2617 }
2618 }
2619 }
2621 // HomePager.OnUrlOpenListener
2622 @Override
2623 public void onUrlOpen(String url, EnumSet<OnUrlOpenListener.Flags> flags) {
2624 if (flags.contains(OnUrlOpenListener.Flags.OPEN_WITH_INTENT)) {
2625 Intent intent = new Intent(Intent.ACTION_VIEW);
2626 intent.setData(Uri.parse(url));
2627 startActivity(intent);
2628 } else if (!maybeSwitchToTab(url, flags)) {
2629 openUrlAndStopEditing(url);
2630 }
2631 }
2633 // BrowserSearch.OnSearchListener
2634 @Override
2635 public void onSearch(SearchEngine engine, String text) {
2636 recordSearch(engine, "barsuggest");
2637 openUrlAndStopEditing(text, engine.name);
2638 }
2640 // BrowserSearch.OnEditSuggestionListener
2641 @Override
2642 public void onEditSuggestion(String suggestion) {
2643 mBrowserToolbar.onEditSuggestion(suggestion);
2644 }
2646 @Override
2647 public int getLayout() { return R.layout.gecko_app; }
2649 @Override
2650 protected String getDefaultProfileName() throws NoMozillaDirectoryException {
2651 return GeckoProfile.getDefaultProfileName(this);
2652 }
2654 /**
2655 * Launch UI that lets the user update Firefox.
2656 *
2657 * This depends on the current channel: Release and Beta both direct to the
2658 * Google Play Store. If updating is enabled, Aurora, Nightly, and custom
2659 * builds open about:, which provides an update interface.
2660 *
2661 * If updating is not enabled, this simply logs an error.
2662 *
2663 * @return true if update UI was launched.
2664 */
2665 protected boolean handleUpdaterLaunch() {
2666 if (AppConstants.RELEASE_BUILD) {
2667 Intent intent = new Intent(Intent.ACTION_VIEW);
2668 intent.setData(Uri.parse("market://details?id=" + getPackageName()));
2669 startActivity(intent);
2670 return true;
2671 }
2673 if (AppConstants.MOZ_UPDATER) {
2674 Tabs.getInstance().loadUrlInTab(AboutPages.UPDATER);
2675 return true;
2676 }
2678 Log.w(LOGTAG, "No candidate updater found; ignoring launch request.");
2679 return false;
2680 }
2682 /* Implementing ActionModeCompat.Presenter */
2683 @Override
2684 public void startActionModeCompat(final ActionModeCompat.Callback callback) {
2685 // If actionMode is null, we're not currently showing one. Flip to the action mode view
2686 if (mActionMode == null) {
2687 mViewFlipper.showNext();
2688 LayerMarginsAnimator margins = mLayerView.getLayerMarginsAnimator();
2690 // If the toolbar is dynamic and not currently showing, just slide it in
2691 if (mDynamicToolbar.isEnabled() && !margins.areMarginsShown()) {
2692 margins.setMaxMargins(0, mViewFlipper.getHeight(), 0, 0);
2693 mDynamicToolbar.setVisible(true, VisibilityTransition.ANIMATE);
2694 mShowActionModeEndAnimation = true;
2695 } else {
2696 // Otherwise, we animate the actionbar itself
2697 mActionBar.animateIn();
2698 }
2700 mDynamicToolbar.setPinned(true, PinReason.ACTION_MODE);
2701 } else {
2702 // Otherwise, we're already showing an action mode. Just finish it and show the new one
2703 mActionMode.finish();
2704 }
2706 mActionMode = new ActionModeCompat(BrowserApp.this, callback, mActionBar);
2707 if (callback.onCreateActionMode(mActionMode, mActionMode.getMenu())) {
2708 mActionMode.invalidate();
2709 }
2710 }
2712 /* Implementing ActionModeCompat.Presenter */
2713 @Override
2714 public void endActionModeCompat() {
2715 if (mActionMode == null) {
2716 return;
2717 }
2719 mActionMode.finish();
2720 mActionMode = null;
2721 mDynamicToolbar.setPinned(false, PinReason.ACTION_MODE);
2723 mViewFlipper.showPrevious();
2725 // Only slide the urlbar out if it was hidden when the action mode started
2726 // Don't animate hiding it so that there's no flash as we switch back to url mode
2727 if (mShowActionModeEndAnimation) {
2728 mDynamicToolbar.setVisible(false, VisibilityTransition.IMMEDIATE);
2729 mShowActionModeEndAnimation = false;
2730 }
2731 }
2733 @Override
2734 protected HealthRecorder createHealthRecorder(final Context context,
2735 final String profilePath,
2736 final EventDispatcher dispatcher,
2737 final String osLocale,
2738 final String appLocale,
2739 final SessionInformation previousSession) {
2740 return new BrowserHealthRecorder(context,
2741 GeckoSharedPrefs.forApp(context),
2742 profilePath,
2743 dispatcher,
2744 osLocale,
2745 appLocale,
2746 previousSession);
2747 }
2748 }