diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt index d7d66dd6c860..e35704dc2e56 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt @@ -611,16 +611,18 @@ class BrowserTabViewModelTest { } @Test - fun whenBrowserNotShownAndOmnibarInputDoesNotHaveFocusThenPrivacyGradeIsNotShown() { + fun whenOmnibarDoesNotHaveFocusThenPrivacyGradeIsShownAndSearchIconIsHidden() { testee.onOmnibarInputStateChanged(query = "", hasFocus = false, hasQueryChanged = false) - assertFalse(browserViewState().showPrivacyGrade) + assertTrue(browserViewState().showPrivacyGrade) + assertFalse(browserViewState().showSearchIcon) } @Test - fun whenBrowserShownAndOmnibarInputDoesNotHaveFocusThenPrivacyGradeIsShown() { + fun whenBrowserShownAndOmnibarInputDoesNotHaveFocusThenPrivacyGradeIsShownAndSearchIconIsHidden() { testee.onUserSubmittedQuery("foo") testee.onOmnibarInputStateChanged(query = "", hasFocus = false, hasQueryChanged = false) assertTrue(browserViewState().showPrivacyGrade) + assertFalse(browserViewState().showSearchIcon) } @Test @@ -630,10 +632,11 @@ class BrowserTabViewModelTest { } @Test - fun whenBrowserShownAndOmnibarInputHasFocusThenPrivacyGradeIsShown() { + fun whenBrowserShownAndOmnibarInputHasFocusThenSearchIconIsShownAndPrivacyGradeIsHidden() { testee.onUserSubmittedQuery("foo") testee.onOmnibarInputStateChanged("", true, hasQueryChanged = false) - assertTrue(browserViewState().showPrivacyGrade) + assertFalse(browserViewState().showPrivacyGrade) + assertTrue(browserViewState().showSearchIcon) } @Test diff --git a/app/src/androidTest/java/com/duckduckgo/app/statistics/VariantManagerTest.kt b/app/src/androidTest/java/com/duckduckgo/app/statistics/VariantManagerTest.kt index f21e13bea35a..2a0c84933679 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/statistics/VariantManagerTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/statistics/VariantManagerTest.kt @@ -43,6 +43,23 @@ class VariantManagerTest { assertEquals(0, variant.features.size) } + // Bottom Bar Navigation Experiment + + @Test + fun bottomBarNavigationControlVariantIsActiveAndHasNoFeatures() { + val variant = variants.first { it.key == "mm" } + assertEqualsDouble(1.0, variant.weight) + assertEquals(0, variant.features.size) + } + + @Test + fun bottomBarNavigationVariantIsActiveAndHasBottomBarNavigationFeature() { + val variant = variants.first { it.key == "mn" } + assertEqualsDouble(1.0, variant.weight) + assertEquals(1, variant.features.size) + assertTrue(variant.hasFeature(BottomBarNavigation)) + } + @Test fun verifyNoDuplicateVariantNames() { val existingNames = mutableSetOf() diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index db27a6f1823f..fa0432c82b70 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -234,6 +234,10 @@ android:name="com.duckduckgo.app.tabs.ui.TabSwitcherActivity" android:label="@string/tabActivityTitle" /> + + { viewState -> viewState?.let { diff --git a/app/src/main/java/com/duckduckgo/app/brokensite/BrokenSiteActivity.kt b/app/src/main/java/com/duckduckgo/app/brokensite/BrokenSiteActivity.kt index 9fc3e2d2a2e7..60dbfa5c4639 100644 --- a/app/src/main/java/com/duckduckgo/app/brokensite/BrokenSiteActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/brokensite/BrokenSiteActivity.kt @@ -31,7 +31,6 @@ import kotlinx.android.synthetic.main.content_broken_sites.* import kotlinx.android.synthetic.main.include_toolbar.* import org.jetbrains.anko.longToast - class BrokenSiteActivity : DuckDuckGoActivity() { private val viewModel: BrokenSiteViewModel by bindViewModel() @@ -40,17 +39,12 @@ class BrokenSiteActivity : DuckDuckGoActivity() { setContentView(R.layout.activity_broken_site) configureListeners() configureObservers() - setupActionBar() + setupToolbar(toolbar) if (savedInstanceState == null) { consumeIntentExtra() } } - private fun setupActionBar() { - setSupportActionBar(toolbar) - supportActionBar?.setDisplayHomeAsUpEnabled(true) - } - private fun consumeIntentExtra() { val url = intent.getStringExtra(URL_EXTRA) val blockedTrackers = intent.getStringExtra(BLOCKED_TRACKERS_EXTRA) diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserPopupMenu.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserPopupMenu.kt index a12f4f0ebf05..5be14ac4ad16 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserPopupMenu.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserPopupMenu.kt @@ -24,15 +24,17 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup.LayoutParams.WRAP_CONTENT import android.widget.PopupWindow +import com.duckduckgo.app.statistics.Variant +import com.duckduckgo.app.statistics.VariantManager -class BrowserPopupMenu : PopupWindow { +class BrowserPopupMenu(layoutInflater: LayoutInflater, variant: Variant, view: View = inflate(layoutInflater, variant)) : + PopupWindow(view, WRAP_CONTENT, WRAP_CONTENT, true) { - constructor(layoutInflater: LayoutInflater, view: View = BrowserPopupMenu.inflate(layoutInflater)) - : super(view, WRAP_CONTENT, WRAP_CONTENT, true) { + // popupwindow gets stuck on the screen on API 22 (tested on 23) without a background + // color. Adding it however garbles the elevation so we cannot have elevation here. + init { if (SDK_INT <= 22) { - // popupwindow gets stuck on the screen on API 22 (tested on 23) without a background - // color. Adding it however garbles the elevation so we cannot have elevation here. setBackgroundDrawable(ColorDrawable(Color.WHITE)) } else { elevation = 6.toFloat() @@ -59,10 +61,21 @@ class BrowserPopupMenu : PopupWindow { private const val margin = 30 - fun inflate(layoutInflater: LayoutInflater): View { + fun inflate(layoutInflater: LayoutInflater, variant: Variant): View { + return if (variant.hasFeature(VariantManager.VariantFeature.BottomBarNavigation)) { + inflateBottomBarWithSearchFeature(layoutInflater) + } else { + inflateToolbarOnly(layoutInflater) + } + } + + private fun inflateToolbarOnly(layoutInflater: LayoutInflater): View { return layoutInflater.inflate(R.layout.popup_window_browser_menu, null) } + private fun inflateBottomBarWithSearchFeature(layoutInflater: LayoutInflater): View { + return layoutInflater.inflate(R.layout.popup_window_browser_bottom_tab_menu, null) + } } } diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt index 515277405d6b..8d8c01ed1f53 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -23,40 +23,75 @@ import android.annotation.SuppressLint import android.app.Activity.RESULT_OK import android.app.ActivityOptions import android.appwidget.AppWidgetManager -import android.content.* +import android.content.ClipData +import android.content.ClipboardManager +import android.content.ComponentName +import android.content.Context +import android.content.Intent import android.content.pm.PackageManager import android.content.res.Configuration import android.media.MediaScannerConnection import android.net.Uri -import android.os.* +import android.os.Build +import android.os.Bundle +import android.os.Environment +import android.os.Handler +import android.os.Message import android.text.Editable -import android.view.* -import android.view.View.* +import android.view.ContextMenu +import android.view.KeyEvent +import android.view.LayoutInflater +import android.view.MenuItem +import android.view.View +import android.view.View.GONE +import android.view.View.OnFocusChangeListener +import android.view.View.VISIBLE +import android.view.View.inflate +import android.view.ViewGroup import android.view.inputmethod.EditorInfo -import android.webkit.* +import android.webkit.ValueCallback +import android.webkit.WebChromeClient +import android.webkit.WebSettings +import android.webkit.WebView import android.webkit.WebView.FindListener import android.webkit.WebView.HitTestResult -import android.webkit.WebView.HitTestResult.* +import android.webkit.WebView.HitTestResult.IMAGE_TYPE +import android.webkit.WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE +import android.webkit.WebView.HitTestResult.UNKNOWN_TYPE +import android.webkit.WebViewDatabase import android.widget.EditText import android.widget.TextView import androidx.annotation.AnyThread import androidx.annotation.StringRes import androidx.appcompat.app.AlertDialog import androidx.constraintlayout.widget.ConstraintSet +import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.content.ContextCompat import androidx.core.content.pm.ShortcutManagerCompat import androidx.core.view.isEmpty +import androidx.core.view.isInvisible import androidx.core.view.isNotEmpty import androidx.core.view.isVisible import androidx.core.view.postDelayed import androidx.fragment.app.Fragment import androidx.fragment.app.transaction -import androidx.lifecycle.* +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleObserver +import androidx.lifecycle.Observer +import androidx.lifecycle.OnLifecycleEvent +import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.LinearLayoutManager import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion import com.duckduckgo.app.bookmarks.ui.EditBookmarkDialogFragment import com.duckduckgo.app.brokensite.BrokenSiteActivity -import com.duckduckgo.app.browser.BrowserTabViewModel.* +import com.duckduckgo.app.browser.BrowserTabViewModel.AutoCompleteViewState +import com.duckduckgo.app.browser.BrowserTabViewModel.BrowserViewState +import com.duckduckgo.app.browser.BrowserTabViewModel.Command +import com.duckduckgo.app.browser.BrowserTabViewModel.CtaViewState +import com.duckduckgo.app.browser.BrowserTabViewModel.FindInPageViewState +import com.duckduckgo.app.browser.BrowserTabViewModel.GlobalLayoutViewState +import com.duckduckgo.app.browser.BrowserTabViewModel.LoadingViewState +import com.duckduckgo.app.browser.BrowserTabViewModel.OmnibarViewState import com.duckduckgo.app.browser.autocomplete.BrowserAutoCompleteSuggestionsAdapter import com.duckduckgo.app.browser.downloader.FileDownloadNotificationManager import com.duckduckgo.app.browser.downloader.FileDownloader @@ -74,7 +109,12 @@ import com.duckduckgo.app.browser.tabpreview.WebViewPreviewGenerator import com.duckduckgo.app.browser.tabpreview.WebViewPreviewPersister import com.duckduckgo.app.browser.ui.HttpAuthenticationDialogFragment import com.duckduckgo.app.browser.useragent.UserAgentProvider -import com.duckduckgo.app.cta.ui.* +import com.duckduckgo.app.cta.ui.Cta +import com.duckduckgo.app.cta.ui.CtaViewModel +import com.duckduckgo.app.cta.ui.DaxBubbleCta +import com.duckduckgo.app.cta.ui.DaxDialogCta +import com.duckduckgo.app.cta.ui.HomePanelCta +import com.duckduckgo.app.cta.ui.HomeTopPanelCta import com.duckduckgo.app.global.ViewModelFactory import com.duckduckgo.app.global.device.DeviceInfo import com.duckduckgo.app.global.model.orderedTrackingEntities @@ -88,24 +128,58 @@ import com.duckduckgo.app.survey.model.Survey import com.duckduckgo.app.survey.ui.SurveyActivity import com.duckduckgo.app.tabs.model.TabEntity import com.duckduckgo.app.tabs.ui.TabSwitcherActivity +import com.duckduckgo.app.tabs.ui.TabSwitcherBottomBarFeatureActivity import com.duckduckgo.app.widget.ui.AddWidgetInstructionsActivity import com.duckduckgo.widget.SearchWidgetLight import com.google.android.material.snackbar.Snackbar import dagger.android.support.AndroidSupportInjection -import kotlinx.android.synthetic.main.fragment_browser_tab.* -import kotlinx.android.synthetic.main.include_cta_buttons.view.* -import kotlinx.android.synthetic.main.include_dax_dialog_cta.* -import kotlinx.android.synthetic.main.include_find_in_page.* -import kotlinx.android.synthetic.main.include_new_browser_tab.* +import kotlinx.android.synthetic.main.fragment_browser_tab.autoCompleteSuggestionsList +import kotlinx.android.synthetic.main.fragment_browser_tab.bottomNavigationBar +import kotlinx.android.synthetic.main.fragment_browser_tab.browserLayout +import kotlinx.android.synthetic.main.fragment_browser_tab.focusDummy +import kotlinx.android.synthetic.main.fragment_browser_tab.rootView +import kotlinx.android.synthetic.main.fragment_browser_tab.webViewContainer +import kotlinx.android.synthetic.main.fragment_browser_tab.webViewFullScreenContainer +import kotlinx.android.synthetic.main.include_add_widget_instruction_buttons.view.closeButton +import kotlinx.android.synthetic.main.include_cta_buttons.view.ctaDismissButton +import kotlinx.android.synthetic.main.include_cta_buttons.view.ctaOkButton +import kotlinx.android.synthetic.main.include_dax_dialog_cta.daxCtaContainer +import kotlinx.android.synthetic.main.include_dax_dialog_cta.dialogTextCta +import kotlinx.android.synthetic.main.include_find_in_page.closeFindInPagePanel +import kotlinx.android.synthetic.main.include_find_in_page.findInPageContainer +import kotlinx.android.synthetic.main.include_find_in_page.findInPageInput +import kotlinx.android.synthetic.main.include_find_in_page.findInPageMatches +import kotlinx.android.synthetic.main.include_find_in_page.nextSearchTermButton +import kotlinx.android.synthetic.main.include_find_in_page.previousSearchTermButton +import kotlinx.android.synthetic.main.include_new_browser_tab.ctaContainer +import kotlinx.android.synthetic.main.include_new_browser_tab.ctaTopContainer +import kotlinx.android.synthetic.main.include_new_browser_tab.ddgLogo +import kotlinx.android.synthetic.main.include_new_browser_tab.newTabLayout import kotlinx.android.synthetic.main.include_omnibar_toolbar.* -import kotlinx.android.synthetic.main.include_omnibar_toolbar.view.* -import kotlinx.android.synthetic.main.include_top_cta.view.* +import kotlinx.android.synthetic.main.include_omnibar_toolbar.view.browserMenu +import kotlinx.android.synthetic.main.include_omnibar_toolbar.view.fireIconMenu +import kotlinx.android.synthetic.main.include_omnibar_toolbar.view.privacyGradeButton +import kotlinx.android.synthetic.main.include_omnibar_toolbar.view.tabsMenu +import kotlinx.android.synthetic.main.layout_browser_bottom_navigation_bar.bottomBarBookmarksItemOne +import kotlinx.android.synthetic.main.layout_browser_bottom_navigation_bar.bottomBarFireItem +import kotlinx.android.synthetic.main.layout_browser_bottom_navigation_bar.bottomBarOverflowItem +import kotlinx.android.synthetic.main.layout_browser_bottom_navigation_bar.bottomBarSearchItem +import kotlinx.android.synthetic.main.layout_browser_bottom_navigation_bar.bottomBarTabsItem +import kotlinx.android.synthetic.main.popup_window_browser_bottom_tab_menu.view.sharePopupMenuItem import kotlinx.android.synthetic.main.popup_window_browser_menu.view.* -import kotlinx.coroutines.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.jetbrains.anko.longToast import org.jetbrains.anko.share import timber.log.Timber import java.io.File +import java.util.Locale import javax.inject.Inject import kotlin.concurrent.thread import kotlin.coroutines.CoroutineContext @@ -189,6 +263,8 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi private lateinit var renderer: BrowserTabFragmentRenderer + private lateinit var decorator: BrowserTabFragmentDecorator + private val viewModel: BrowserTabViewModel by lazy { val viewModel = ViewModelProvider(this, viewModelFactory).get(BrowserTabViewModel::class.java) viewModel.loadData(tabId, initialUrl, skipHome) @@ -205,11 +281,11 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi private val browserActivity get() = activity as? BrowserActivity - private val tabsButton: MenuItem? - get() = toolbar.menu.findItem(R.id.tabs) + private val tabsButton: TabSwitcherButton? + get() = appBarLayout.tabsMenu - private val fireMenuButton: MenuItem? - get() = toolbar.menu.findItem(R.id.fire) + private val fireMenuButton: ViewGroup? + get() = appBarLayout.fireIconMenu private val menuButton: ViewGroup? get() = appBarLayout.browserMenu @@ -246,6 +322,7 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi super.onCreate(savedInstanceState) removeDaxDialogFromActivity() renderer = BrowserTabFragmentRenderer() + decorator = BrowserTabFragmentDecorator() if (savedInstanceState != null) { updateFragmentListener() } @@ -262,17 +339,17 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) - createPopupMenu() + configureObservers() - configureAppBar() + configurePrivacyGrade() configureWebView() viewModel.registerWebViewListener(webViewClient, webChromeClient) configureOmnibarTextInput() configureFindInPage() configureAutoComplete() configureKeyboardAwareLogoAnimation() - configureShowTabSwitcherListener() - configureLongClickOpensNewTabListener() + + decorateWithFeatures() if (savedInstanceState == null) { viewModel.onViewReady() @@ -304,8 +381,8 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi val transport = message.obj as WebView.WebViewTransport transport.webView = webView message.sendToTarget() - val tabsButton = tabsButton?.actionView as TabSwitcherButton - tabsButton.animateCount() + + decorator.animateTabsCount() viewModel.onMessageProcessed() } @@ -319,28 +396,20 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi } } - private fun configureShowTabSwitcherListener() { - tabsButton?.actionView?.setOnClickListener { - launch { viewModel.userLaunchingTabSwitcher() } - } - } - - private fun configureLongClickOpensNewTabListener() { - tabsButton?.actionView?.setOnLongClickListener { - launch { viewModel.userRequestedOpeningNewTab() } - return@setOnLongClickListener true - } - } - private fun launchTabSwitcher() { val activity = activity ?: return - startActivity(TabSwitcherActivity.intent(activity, tabId)) + if (isBottomNavigationFeatureEnabled()) { + startActivity(TabSwitcherBottomBarFeatureActivity.intent(activity, tabId)) + } else { + startActivity(TabSwitcherActivity.intent(activity, tabId)) + } + activity.overridePendingTransition(R.anim.tab_anim_fade_in, R.anim.slide_to_bottom) } override fun onResume() { super.onResume() - addTextChangedListeners() + appBarLayout.setExpanded(true) viewModel.onViewResumed() logoHidingListener.onResume() @@ -349,6 +418,8 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi if (fragmentIsVisible()) { viewModel.onViewVisible() } + + addTextChangedListeners() } override fun onPause() { @@ -362,25 +433,6 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi fragment?.dismiss() } - private fun createPopupMenu() { - popupMenu = BrowserPopupMenu(layoutInflater) - val view = popupMenu.contentView - popupMenu.apply { - onMenuItemClicked(view.forwardPopupMenuItem) { viewModel.onUserPressedForward() } - onMenuItemClicked(view.backPopupMenuItem) { activity?.onBackPressed() } - onMenuItemClicked(view.refreshPopupMenuItem) { viewModel.onRefreshRequested() } - onMenuItemClicked(view.newTabPopupMenuItem) { viewModel.userRequestedOpeningNewTab() } - onMenuItemClicked(view.bookmarksPopupMenuItem) { browserActivity?.launchBookmarks() } - onMenuItemClicked(view.addBookmarksPopupMenuItem) { launch { viewModel.onBookmarkAddRequested() } } - onMenuItemClicked(view.findInPageMenuItem) { viewModel.onFindInPageSelected() } - onMenuItemClicked(view.brokenSitePopupMenuItem) { viewModel.onBrokenSiteSelected() } - onMenuItemClicked(view.settingsPopupMenuItem) { browserActivity?.launchSettings() } - onMenuItemClicked(view.requestDesktopSiteCheckMenuItem) { viewModel.onDesktopSiteModeToggled(view.requestDesktopSiteCheckMenuItem.isChecked) } - onMenuItemClicked(view.sharePageMenuItem) { viewModel.onShareSelected() } - onMenuItemClicked(view.addToHome) { viewModel.onPinPageToHomeSelected() } - } - } - private fun addHomeShortcut(homeShortcut: Command.AddHomeShortcut, context: Context) { val shortcutInfo = shortcutBuilder.buildPinnedPageShortcut(context, homeShortcut) ShortcutManagerCompat.requestPinShortcut(context, shortcutInfo, null) @@ -428,7 +480,9 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi private fun addTabsObserver() { viewModel.tabs.observe(viewLifecycleOwner, Observer> { - it?.let { renderer.renderTabIcon(it) } + it?.let { + decorator.renderTabIcon(it) + } }) } @@ -602,11 +656,9 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi private fun openInNewBackgroundTab() { appBarLayout.setExpanded(true, true) + decorator.updateBottomBarVisibility(true, true) viewModel.tabs.removeObservers(this) - val view = tabsButton?.actionView as TabSwitcherButton - view.increment { - addTabsObserver() - } + decorator.incrementTabs() } private fun openExternalDialog(intent: Intent, fallbackUrl: String? = null, useFirstActivityFound: Boolean = true) { @@ -713,28 +765,22 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi autoCompleteSuggestionsList.adapter = autoCompleteSuggestionsAdapter } - private fun configureAppBar() { - toolbar.inflateMenu(R.menu.menu_browser_activity) + private fun isBottomNavigationFeatureEnabled() = + variantManager.getVariant().hasFeature(VariantManager.VariantFeature.BottomBarNavigation) - toolbar.setOnMenuItemClickListener { menuItem -> - when (menuItem.itemId) { - R.id.fire -> { - browserActivity?.launchFire() - return@setOnMenuItemClickListener true - } - else -> return@setOnMenuItemClickListener false - } + private fun decorateWithFeatures() { + if (isBottomNavigationFeatureEnabled()) { + decorator.decorateWithBottomBarSearch() + } else { + decorator.decorateWithToolbar() } + } + private fun configurePrivacyGrade() { toolbar.privacyGradeButton.setOnClickListener { browserActivity?.launchPrivacyDashboard() } - browserMenu.setOnClickListener { - hideKeyboardImmediately() - launchPopupMenu() - } - viewModel.privacyGrade.observe(viewLifecycleOwner, Observer { Timber.d("Observed grade: $it") it?.let { privacyGrade -> @@ -924,13 +970,8 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi return super.onContextItemSelected(item) } - private fun launchPopupMenu() { - popupMenu.show(rootView, toolbar) - } - private fun bookmarkAdded(bookmarkId: Long, title: String?, url: String?) { - - Snackbar.make(rootView, R.string.bookmarkEdited, Snackbar.LENGTH_LONG) + Snackbar.make(browserLayout, R.string.bookmarkEdited, Snackbar.LENGTH_LONG) .setAction(R.string.edit) { val addBookmarkDialog = EditBookmarkDialogFragment.instance(bookmarkId, title, url) addBookmarkDialog.show(childFragmentManager, ADD_BOOKMARK_FRAGMENT_TAG) @@ -1207,7 +1248,7 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi .show() } - fun omnibarViews(): List = listOf(clearTextButton, omnibarTextInput, privacyGradeButton) + fun omnibarViews(): List = listOf(clearTextButton, omnibarTextInput, privacyGradeButton, searchIcon) companion object { private const val TAB_ID_ARG = "TAB_ID_ARG" @@ -1244,6 +1285,229 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi } } + inner class BrowserTabFragmentDecorator { + fun decorateToolbar(viewState: BrowserViewState) { + if (!variantManager.getVariant().hasFeature(VariantManager.VariantFeature.BottomBarNavigation)) { + decorator.decorateToolbarActions(viewState) + } + } + + private fun decorateToolbarActions(viewState: BrowserViewState) { + tabsButton?.isVisible = viewState.showTabsButton + fireMenuButton?.isVisible = viewState.showFireButton + menuButton?.isVisible = viewState.showMenuButton + } + + fun decorateWithToolbar() { + hideBottomBar() + decorateAppBarWithToolbarOnly() + createPopupMenuWithToolbarOnly() + configureShowTabSwitcherListenerWithToolbarOnly() + configureLongClickOpensNewTabListenerWithToolbarOnly() + removeUnnecessaryLayoutBehaviour() + } + + fun decorateWithBottomBarSearch() { + bindBottomBarButtons() + decorateAppBarWithBottomBar() + createPopupMenuWithBottomBar() + configureShowTabSwitcherListenerWithBottomBarNavigationOnly() + configureLongClickOpensNewTabListenerWithBottomBarNavigationOnly() + } + + private fun decorateAppBarWithToolbarOnly() { + fireMenuButton?.show() + fireMenuButton?.setOnClickListener { + browserActivity?.launchFire() + pixel.fire(String.format(Locale.US, Pixel.PixelName.MENU_ACTION_FIRE_PRESSED.pixelName, variantManager.getVariant().key)) + } + + tabsButton?.show() + } + + private fun decorateAppBarWithBottomBar() { + menuButton?.gone() + tabsButton?.gone() + fireMenuButton?.gone() + } + + private fun createPopupMenuWithToolbarOnly() { + popupMenu = BrowserPopupMenu(layoutInflater, variantManager.getVariant()) + val view = popupMenu.contentView + popupMenu.apply { + onMenuItemClicked(view.forwardPopupMenuItem) { viewModel.onUserPressedForward() } + onMenuItemClicked(view.backPopupMenuItem) { activity?.onBackPressed() } + onMenuItemClicked(view.refreshPopupMenuItem) { + viewModel.onRefreshRequested() + pixel.fire(String.format(Locale.US, Pixel.PixelName.MENU_ACTION_REFRESH_PRESSED.pixelName, variantManager.getVariant().key)) + } + onMenuItemClicked(view.newTabPopupMenuItem) { + viewModel.userRequestedOpeningNewTab() + pixel.fire(String.format(Locale.US, Pixel.PixelName.MENU_ACTION_NEW_TAB_PRESSED.pixelName, variantManager.getVariant().key)) + } + onMenuItemClicked(view.bookmarksPopupMenuItem) { + browserActivity?.launchBookmarks() + pixel.fire(String.format(Locale.US, Pixel.PixelName.MENU_ACTION_BOOKMARKS_PRESSED.pixelName, variantManager.getVariant().key)) + } + onMenuItemClicked(view.addBookmarksPopupMenuItem) { launch { viewModel.onBookmarkAddRequested() } } + onMenuItemClicked(view.findInPageMenuItem) { viewModel.onFindInPageSelected() } + onMenuItemClicked(view.brokenSitePopupMenuItem) { viewModel.onBrokenSiteSelected() } + onMenuItemClicked(view.settingsPopupMenuItem) { browserActivity?.launchSettings() } + onMenuItemClicked(view.requestDesktopSiteCheckMenuItem) { viewModel.onDesktopSiteModeToggled(view.requestDesktopSiteCheckMenuItem.isChecked) } + onMenuItemClicked(view.sharePageMenuItem) { viewModel.onShareSelected() } + onMenuItemClicked(view.addToHome) { viewModel.onPinPageToHomeSelected() } + } + browserMenu.setOnClickListener { + hideKeyboardImmediately() + launchTopAnchoredPopupMenu() + } + } + + private fun launchTopAnchoredPopupMenu() { + popupMenu.show(rootView, toolbar) + pixel.fire(String.format(Locale.US, Pixel.PixelName.MENU_ACTION_POPUP_OPENED.pixelName, variantManager.getVariant().key)) + } + + private fun createPopupMenuWithBottomBar() { + popupMenu = BrowserPopupMenu(layoutInflater, variantManager.getVariant()) + val view = popupMenu.contentView + popupMenu.apply { + onMenuItemClicked(view.forwardPopupMenuItem) { viewModel.onUserPressedForward() } + onMenuItemClicked(view.backPopupMenuItem) { activity?.onBackPressed() } + onMenuItemClicked(view.refreshPopupMenuItem) { + viewModel.onRefreshRequested() + pixel.fire(String.format(Locale.US, Pixel.PixelName.MENU_ACTION_REFRESH_PRESSED.pixelName, variantManager.getVariant().key)) + } + onMenuItemClicked(view.sharePopupMenuItem) { viewModel.onShareSelected() } + onMenuItemClicked(view.newTabPopupMenuItem) { + viewModel.userRequestedOpeningNewTab() + pixel.fire(String.format(Locale.US, Pixel.PixelName.MENU_ACTION_NEW_TAB_PRESSED.pixelName, variantManager.getVariant().key)) + } + onMenuItemClicked(view.addBookmarksPopupMenuItem) { launch { viewModel.onBookmarkAddRequested() } } + onMenuItemClicked(view.findInPageMenuItem) { viewModel.onFindInPageSelected() } + onMenuItemClicked(view.brokenSitePopupMenuItem) { viewModel.onBrokenSiteSelected() } + onMenuItemClicked(view.settingsPopupMenuItem) { browserActivity?.launchSettings() } + onMenuItemClicked(view.requestDesktopSiteCheckMenuItem) { viewModel.onDesktopSiteModeToggled(view.requestDesktopSiteCheckMenuItem.isChecked) } + onMenuItemClicked(view.addToHome) { viewModel.onPinPageToHomeSelected() } + } + } + + private fun launchBottomAnchoredPopupMenu() { + popupMenu.show(rootView, bottomNavigationBar) + pixel.fire(String.format(Locale.US, Pixel.PixelName.MENU_ACTION_POPUP_OPENED.pixelName, variantManager.getVariant().key)) + } + + private fun bindBottomBarButtons() { + bottomNavigationBar.apply { + onItemClicked(bottomBarFireItem) { + browserActivity?.launchFire() + pixel.fire(String.format(Locale.US, Pixel.PixelName.MENU_ACTION_FIRE_PRESSED.pixelName, variantManager.getVariant().key)) + } + onItemClicked(bottomBarBookmarksItemOne) { + browserActivity?.launchBookmarks() + pixel.fire(String.format(Locale.US, Pixel.PixelName.MENU_ACTION_BOOKMARKS_PRESSED.pixelName, variantManager.getVariant().key)) + } + onItemClicked(bottomBarSearchItem) { + omnibarTextInput.requestFocus() + pixel.fire(String.format(Locale.US, Pixel.PixelName.MENU_ACTION_SEARCH_PRESSED.pixelName, variantManager.getVariant().key)) + } + onItemClicked(bottomBarTabsItem) { + viewModel.userLaunchingTabSwitcher() + } + onItemClicked(bottomBarOverflowItem) { + hideKeyboardImmediately() + launchBottomAnchoredPopupMenu() + } + } + } + + fun updateBottomBarVisibility(shouldShow: Boolean, shouldAnimate: Boolean = false) { + if (isBottomNavigationFeatureEnabled()) { + if (shouldShow) { + showBottomBar(shouldAnimate) + } else { + hideBottomBar(shouldAnimate) + } + } + } + + private fun hideBottomBar(shouldAnimate: Boolean = false) { + if (shouldAnimate) { + bottomNavigationBar.animateBarVisibility(isVisible = false) + } + bottomNavigationBar.gone() + } + + private fun showBottomBar(shouldAnimate: Boolean) { + if (shouldAnimate) { + bottomNavigationBar.show() + bottomNavigationBar.animateBarVisibility(isVisible = true) + } else { + bottomNavigationBar.postDelayed(KEYBOARD_DELAY) { + bottomNavigationBar.show() + } + } + } + + private fun configureShowTabSwitcherListenerWithToolbarOnly() { + tabsButton?.setOnClickListener { + launch { viewModel.userLaunchingTabSwitcher() } + } + } + + private fun configureShowTabSwitcherListenerWithBottomBarNavigationOnly() { + bottomBarTabsItem.setOnClickListener { + launch { viewModel.userLaunchingTabSwitcher() } + } + } + + private fun configureLongClickOpensNewTabListenerWithToolbarOnly() { + tabsButton?.setOnLongClickListener { + launch { viewModel.userRequestedOpeningNewTab() } + return@setOnLongClickListener true + } + } + + private fun configureLongClickOpensNewTabListenerWithBottomBarNavigationOnly() { + bottomBarTabsItem.setOnLongClickListener { + launch { viewModel.userRequestedOpeningNewTab() } + return@setOnLongClickListener true + } + } + + private fun removeUnnecessaryLayoutBehaviour() { + val params: CoordinatorLayout.LayoutParams = bottomNavigationBar.getLayoutParams() as CoordinatorLayout.LayoutParams + params.behavior = null + } + + fun animateTabsCount() { + tabsButton?.animateCount() + bottomBarTabsItem.animateCount() + } + + fun renderTabIcon(tabs: List) { + context?.let { + tabsButton?.count = tabs.count() + tabsButton?.hasUnread = tabs.firstOrNull { !it.viewed } != null + + bottomBarTabsItem.count = tabs.count() + bottomBarTabsItem.hasUnread = tabs.firstOrNull { !it.viewed } != null + } + } + + fun incrementTabs() { + if (isBottomNavigationFeatureEnabled()){ + bottomBarTabsItem.increment { + addTabsObserver() + } + } else { + tabsButton?.increment { + addTabsObserver() + } + } + } + } + inner class BrowserTabFragmentRenderer { private var lastSeenOmnibarViewState: OmnibarViewState? = null @@ -1272,18 +1536,34 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi lastSeenOmnibarViewState = viewState if (viewState.isEditing) { - omniBarContainer.background = null cancelAllAnimations() - } else { - omniBarContainer.setBackgroundResource(R.drawable.omnibar_field_background) } if (shouldUpdateOmnibarTextInput(viewState, viewState.omnibarText)) { omnibarTextInput.setText(viewState.omnibarText) appBarLayout.setExpanded(true, true) + decorator.updateBottomBarVisibility(true, true) if (viewState.shouldMoveCaretToEnd) { omnibarTextInput.setSelection(viewState.omnibarText.length) } + } else { + decorator.updateBottomBarVisibility(!viewState.isEditing) + } + + lastSeenBrowserViewState?.let { + renderToolbarMenus(it) + } + + if (ctaContainer.isVisible) { + if (isBottomNavigationFeatureEnabled()) { + lastSeenOmnibarViewState?.let { + if (it.isEditing) { + ctaContainer.setPadding(0, 0, 0, 0) + } else { + ctaContainer.setPadding(0, 0, 0, 46.toPx()) + } + } ?: ctaContainer.setPadding(0, 0, 0, 46.toPx()) + } } } } @@ -1338,7 +1618,6 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi networksContainer.alpha = 0f clearTextButton.alpha = 1f omnibarTextInput.alpha = 1f - privacyGradeButton.alpha = 1f } fun renderGlobalViewState(viewState: GlobalLayoutViewState) { @@ -1414,11 +1693,17 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi } private fun renderToolbarMenus(viewState: BrowserViewState) { - privacyGradeButton?.isVisible = viewState.showPrivacyGrade - clearTextButton?.isVisible = viewState.showClearButton - tabsButton?.isVisible = viewState.showTabsButton - fireMenuButton?.isVisible = viewState.showFireButton - menuButton?.isVisible = viewState.showMenuButton + if (viewState.browserShowing) { + privacyGradeButton?.isInvisible = !viewState.showPrivacyGrade + clearTextButton?.isVisible = viewState.showClearButton + searchIcon?.isVisible = viewState.showSearchIcon + } else { + privacyGradeButton?.isVisible = false + clearTextButton?.isVisible = viewState.showClearButton + searchIcon?.isVisible = true + } + + decorator.decorateToolbar(viewState) } fun renderFindInPageState(viewState: FindInPageViewState) { @@ -1437,14 +1722,6 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi popupMenu.contentView.findInPageMenuItem?.isEnabled = viewState.canFindInPage } - fun renderTabIcon(tabs: List) { - context?.let { - val button = tabsButton?.actionView as TabSwitcherButton - button.count = tabs.count() - button.hasUnread = tabs.firstOrNull { !it.viewed } != null - } - } - fun renderCtaViewState(viewState: CtaViewState) { if (isHidden) { return @@ -1554,10 +1831,20 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi viewModel.onUserDismissedCta() } - ConstraintSet().also { - it.clone(newTabLayout) - it.connect(ddgLogo.id, ConstraintSet.BOTTOM, ctaContainer.id, ConstraintSet.TOP, 0) - it.applyTo(newTabLayout) + if (isBottomNavigationFeatureEnabled()) { + lastSeenOmnibarViewState?.let { + if (it.isEditing) { + ctaContainer.setPadding(0, 0, 0, 0) + } else { + ctaContainer.setPadding(0, 0, 0, 46.toPx()) + } + } ?: ctaContainer.setPadding(0, 0, 0, 46.toPx()) + + ConstraintSet().also { + it.clone(newTabLayout) + it.connect(ddgLogo.id, ConstraintSet.BOTTOM, ctaContainer.id, ConstraintSet.TOP, 0) + it.applyTo(newTabLayout) + } } } diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt index a2d6afe73102..e4e0735dd60c 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -122,6 +122,7 @@ class BrowserTabViewModel( val isDesktopBrowsingMode: Boolean = false, val canChangeBrowsingMode: Boolean = true, val showPrivacyGrade: Boolean = false, + val showSearchIcon: Boolean = false, val showClearButton: Boolean = false, val showTabsButton: Boolean = true, val showFireButton: Boolean = true, @@ -194,7 +195,7 @@ class BrowserTabViewModel( object GenerateWebViewPreviewImage : Command() object LaunchTabSwitcher : Command() class ShowErrorWithAction(val action: () -> Unit) : Command() - sealed class DaxCommand: Command() { + sealed class DaxCommand : Command() { object FinishTrackerAnimation : DaxCommand() class HideDaxDialog(val cta: Cta) : DaxCommand() } @@ -485,22 +486,23 @@ class BrowserTabViewModel( buildSiteFactory(url, title) val currentOmnibarViewState = currentOmnibarViewState() - omnibarViewState.postValue(currentOmnibarViewState.copy(omnibarText = omnibarTextForUrl(url), shouldMoveCaretToEnd = false)) - + omnibarViewState.value = currentOmnibarViewState.copy(omnibarText = omnibarTextForUrl(url), shouldMoveCaretToEnd = false) val currentBrowserViewState = currentBrowserViewState() - findInPageViewState.postValue(FindInPageViewState(visible = false, canFindInPage = true)) - browserViewState.postValue( - currentBrowserViewState.copy( - browserShowing = true, - canAddBookmarks = true, - addToHomeEnabled = true, - addToHomeVisible = addToHomeCapabilityDetector.isAddToHomeSupported(), - canSharePage = true, - showPrivacyGrade = true, - canReportSite = true - ) + findInPageViewState.value = FindInPageViewState(visible = false, canFindInPage = true) + browserViewState.value = currentBrowserViewState.copy( + browserShowing = true, + canAddBookmarks = true, + addToHomeEnabled = true, + addToHomeVisible = addToHomeCapabilityDetector.isAddToHomeSupported(), + canSharePage = true, + showPrivacyGrade = true, + canReportSite = true, + showSearchIcon = false, + showClearButton = false ) + Timber.d("showPrivacyGrade=true, showSearchIcon=false, showClearButton=false") + if (duckDuckGoUrlDetector.isDuckDuckGoQueryUrl(url)) { statisticsUpdater.refreshSearchRetentionAtb() } @@ -536,8 +538,11 @@ class BrowserTabViewModel( addToHomeVisible = addToHomeCapabilityDetector.isAddToHomeSupported(), canSharePage = false, showPrivacyGrade = false, - canReportSite = false + canReportSite = false, + showSearchIcon = true, + showClearButton = true ) + Timber.d("showPrivacyGrade=false, showSearchIcon=true, showClearButton=true") } override fun pageRefreshed(refreshedUrl: String) { @@ -660,18 +665,23 @@ class BrowserTabViewModel( val showAutoCompleteSuggestions = hasFocus && query.isNotBlank() && hasQueryChanged && autoCompleteSuggestionsEnabled val showClearButton = hasFocus && query.isNotBlank() val showControls = !hasFocus || query.isBlank() + val showPrivacyGrade = !hasFocus + val showSearchIcon = hasFocus omnibarViewState.value = currentOmnibarViewState.copy(isEditing = hasFocus) val currentBrowserViewState = currentBrowserViewState() browserViewState.value = currentBrowserViewState.copy( - showPrivacyGrade = currentBrowserViewState.browserShowing, + showPrivacyGrade = showPrivacyGrade, + showSearchIcon = showSearchIcon, showTabsButton = showControls, showFireButton = showControls, showMenuButton = showControls, showClearButton = showClearButton ) + Timber.d("showPrivacyGrade=$showPrivacyGrade, showSearchIcon=$showSearchIcon, showClearButton=$showClearButton") + autoCompleteViewState.value = AutoCompleteViewState(showAutoCompleteSuggestions, autoCompleteSearchResults) if (hasQueryChanged && hasFocus && autoCompleteSuggestionsEnabled) { diff --git a/app/src/main/java/com/duckduckgo/app/browser/DuckDuckGoWebView.kt b/app/src/main/java/com/duckduckgo/app/browser/DuckDuckGoWebView.kt index ad31599f4a2a..54e7c2d1ad06 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/DuckDuckGoWebView.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/DuckDuckGoWebView.kt @@ -140,4 +140,4 @@ class DuckDuckGoWebView : WebView, NestedScrollingChild { */ private const val IME_FLAG_NO_PERSONALIZED_LEARNING = 0x1000000 } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/duckduckgo/app/browser/TabSwitcherButton.kt b/app/src/main/java/com/duckduckgo/app/browser/TabSwitcherButton.kt index e698d3a2a367..9cdf05b2436e 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/TabSwitcherButton.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/TabSwitcherButton.kt @@ -19,11 +19,19 @@ package com.duckduckgo.app.browser import android.animation.Animator import android.animation.AnimatorListenerAdapter import android.content.Context +import android.util.AttributeSet import android.view.LayoutInflater +import android.view.View import android.widget.FrameLayout +import android.widget.LinearLayout +import android.widget.RelativeLayout import kotlinx.android.synthetic.main.view_tab_switcher_button.view.* -class TabSwitcherButton(context: Context) : FrameLayout(context) { +class TabSwitcherButton @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : RelativeLayout(context, attrs, defStyleAttr) { var count = 0 set(value) { @@ -35,46 +43,33 @@ class TabSwitcherButton(context: Context) : FrameLayout(context) { var hasUnread = false set(value) { field = value - anim.progress = if (hasUnread) 1.0f else 0.0f } - init { - LayoutInflater.from(context).inflate(R.layout.view_tab_switcher_button, this, true) + override fun onFinishInflate() { + super.onFinishInflate() + View.inflate(context, R.layout.view_tab_switcher_button, this) - clickZone.setOnClickListener { + setOnClickListener { super.callOnClick() } - clickZone.setOnLongClickListener { + setOnLongClickListener { super.performLongClick() } } fun increment(callback: () -> Unit) { - anim.progress = 0.0f - - anim.addAnimatorListener(object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator?) { - callback() - } - }) - fadeOutCount { count += 1 fadeInCount() + callback() } - - anim.playAnimation() } fun animateCount() { - anim.progress = 0.0f - fadeOutCount { fadeInCount() } - - anim.playAnimation() } private fun fadeOutCount(callback: () -> Unit) { diff --git a/app/src/main/java/com/duckduckgo/app/browser/omnibar/KeyboardAwareEditText.kt b/app/src/main/java/com/duckduckgo/app/browser/omnibar/KeyboardAwareEditText.kt index 61386985ccb4..b9f91e09a3a8 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/omnibar/KeyboardAwareEditText.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/omnibar/KeyboardAwareEditText.kt @@ -35,7 +35,6 @@ class KeyboardAwareEditText : AppCompatEditText { override fun onFocusChanged(focused: Boolean, direction: Int, previouslyFocusedRect: Rect?) { super.onFocusChanged(focused, direction, previouslyFocusedRect) - if (focused) { showKeyboard() } @@ -62,7 +61,5 @@ class KeyboardAwareEditText : AppCompatEditText { interface OnBackKeyListener { fun onBackKey(): Boolean - } - } diff --git a/app/src/main/java/com/duckduckgo/app/browser/omnibar/OmnibarScrolling.kt b/app/src/main/java/com/duckduckgo/app/browser/omnibar/OmnibarScrolling.kt index dc7e17685eb1..398b4c7eae42 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/omnibar/OmnibarScrolling.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/omnibar/OmnibarScrolling.kt @@ -18,15 +18,14 @@ package com.duckduckgo.app.browser.omnibar import android.view.View import com.google.android.material.appbar.AppBarLayout -import com.google.android.material.appbar.AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS -import com.google.android.material.appbar.AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL +import com.google.android.material.appbar.AppBarLayout.LayoutParams.* import javax.inject.Inject class OmnibarScrolling @Inject constructor() { fun enableOmnibarScrolling(toolbarContainer: View) { - updateScrollFlag(SCROLL_FLAG_SCROLL or SCROLL_FLAG_ENTER_ALWAYS, toolbarContainer) + updateScrollFlag(SCROLL_FLAG_SCROLL or SCROLL_FLAG_SNAP or SCROLL_FLAG_ENTER_ALWAYS, toolbarContainer) } fun disableOmnibarScrolling(toolbarContainer: View) { diff --git a/app/src/main/java/com/duckduckgo/app/browser/ui/BottomNavigationBar.kt b/app/src/main/java/com/duckduckgo/app/browser/ui/BottomNavigationBar.kt new file mode 100644 index 000000000000..fe58b16530be --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/browser/ui/BottomNavigationBar.kt @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2020 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.app.browser.ui + +import android.animation.ValueAnimator +import android.content.Context +import android.content.res.TypedArray +import android.util.AttributeSet +import android.view.View +import android.view.animation.DecelerateInterpolator +import android.widget.LinearLayout +import com.duckduckgo.app.browser.R + +class BottomNavigationBar : LinearLayout { + + constructor(context: Context) : super(context) { + init(context, null) + } + + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { + init(context, attrs) + } + + constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super( + context, + attrs, + defStyleAttr + ) { + init(context, attrs) + } + + private fun init(context: Context, attrs: AttributeSet?) { + + val ta: TypedArray = context.obtainStyledAttributes(attrs, R.styleable.BottomNavigationBar) + val resourceId = ta.getResourceId(R.styleable.BottomNavigationBar_layoutResource, R.layout.layout_browser_bottom_navigation_bar) + + ta.recycle() + + View.inflate(context, resourceId, this) + } + + fun onItemClicked(view: View, onClick: () -> Unit) { + view.setOnClickListener { + onClick() + } + } + + fun animateBarVisibility(isVisible: Boolean) { + val offsetAnimator = ValueAnimator().apply { + interpolator = DecelerateInterpolator() + duration = 150L + } + + offsetAnimator.addUpdateListener { + translationY = it.animatedValue as Float + } + + val targetTranslation = if (isVisible) 0f else height.toFloat() + offsetAnimator.setFloatValues(translationY, targetTranslation) + offsetAnimator.start() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/duckduckgo/app/browser/ui/BottomNavigationBehavior.kt b/app/src/main/java/com/duckduckgo/app/browser/ui/BottomNavigationBehavior.kt new file mode 100644 index 000000000000..e08cc216218b --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/browser/ui/BottomNavigationBehavior.kt @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2020 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.app.browser.ui + +import android.animation.ValueAnimator +import android.content.Context +import android.util.AttributeSet +import android.view.Gravity +import android.view.View +import android.view.animation.DecelerateInterpolator +import androidx.coordinatorlayout.widget.CoordinatorLayout +import androidx.core.view.ViewCompat +import com.google.android.material.snackbar.Snackbar + +class BottomNavigationBehavior(context: Context, attrs: AttributeSet) : + CoordinatorLayout.Behavior(context, attrs) { + + @ViewCompat.NestedScrollType + private var lastStartedType: Int = 0 + + private var offsetAnimator: ValueAnimator? = null + + private var isSnappingEnabled = true + + override fun layoutDependsOn(parent: CoordinatorLayout, child: V, dependency: View): Boolean { + if (dependency is Snackbar.SnackbarLayout) { + updateSnackbar(child, dependency) + } + + return super.layoutDependsOn(parent, child, dependency) + } + + override fun onStartNestedScroll( + coordinatorLayout: CoordinatorLayout, + child: V, + directTargetChild: View, + target: View, + axes: Int, + type: Int + ): Boolean { + if (axes != ViewCompat.SCROLL_AXIS_VERTICAL) + return false + + lastStartedType = type + + offsetAnimator?.cancel() + + return true + } + + override fun onNestedPreScroll(coordinatorLayout: CoordinatorLayout, child: V, target: View, dx: Int, dy: Int, consumed: IntArray, type: Int) { + super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type) + child.translationY = kotlin.math.max(0f, kotlin.math.min(child.height.toFloat(), child.translationY + dy)) + } + + override fun onStopNestedScroll(coordinatorLayout: CoordinatorLayout, child: V, target: View, type: Int) { + if (!isSnappingEnabled) + return + + if (lastStartedType == ViewCompat.TYPE_TOUCH || type == ViewCompat.TYPE_NON_TOUCH) { + // find nearest seam + val currTranslation = child.translationY + val childHalfHeight = child.height * 0.5f + + // translate down + if (currTranslation >= childHalfHeight) { + animateBarVisibility(child, isVisible = false) + } + // translate up + else { + animateBarVisibility(child, isVisible = true) + } + } + } + + private fun animateBarVisibility(child: View, isVisible: Boolean) { + if (offsetAnimator == null) { + offsetAnimator = ValueAnimator().apply { + interpolator = DecelerateInterpolator() + duration = 150L + } + + offsetAnimator?.addUpdateListener { + child.translationY = it.animatedValue as Float + } + } else { + offsetAnimator?.cancel() + } + + val targetTranslation = if (isVisible) 0f else child.height.toFloat() + offsetAnimator?.setFloatValues(child.translationY, targetTranslation) + offsetAnimator?.start() + } + + private fun updateSnackbar(child: View, snackbarLayout: Snackbar.SnackbarLayout) { + if (snackbarLayout.layoutParams is CoordinatorLayout.LayoutParams) { + val params = snackbarLayout.layoutParams as CoordinatorLayout.LayoutParams + + params.anchorId = child.id + params.anchorGravity = Gravity.TOP + params.gravity = Gravity.TOP + snackbarLayout.layoutParams = params + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/duckduckgo/app/di/AndroidBindingModule.kt b/app/src/main/java/com/duckduckgo/app/di/AndroidBindingModule.kt index c47d80824cf0..70a801269412 100644 --- a/app/src/main/java/com/duckduckgo/app/di/AndroidBindingModule.kt +++ b/app/src/main/java/com/duckduckgo/app/di/AndroidBindingModule.kt @@ -47,6 +47,7 @@ import com.duckduckgo.app.settings.SettingsActivity import com.duckduckgo.app.survey.ui.SurveyActivity import com.duckduckgo.app.systemsearch.SystemSearchActivity import com.duckduckgo.app.tabs.ui.TabSwitcherActivity +import com.duckduckgo.app.tabs.ui.TabSwitcherBottomBarFeatureActivity import com.duckduckgo.app.widget.ui.AddWidgetInstructionsActivity import dagger.Module import dagger.android.ContributesAndroidInjector @@ -77,6 +78,10 @@ abstract class AndroidBindingModule { @ContributesAndroidInjector abstract fun tabsActivity(): TabSwitcherActivity + @ActivityScoped + @ContributesAndroidInjector + abstract fun tabsExperimentActivity(): TabSwitcherBottomBarFeatureActivity + @ActivityScoped @ContributesAndroidInjector abstract fun privacyDashboardActivity(): PrivacyDashboardActivity diff --git a/app/src/main/java/com/duckduckgo/app/feedback/ui/common/FeedbackActivity.kt b/app/src/main/java/com/duckduckgo/app/feedback/ui/common/FeedbackActivity.kt index e7720c971070..4dd644a7478c 100644 --- a/app/src/main/java/com/duckduckgo/app/feedback/ui/common/FeedbackActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/feedback/ui/common/FeedbackActivity.kt @@ -54,15 +54,10 @@ class FeedbackActivity : DuckDuckGoActivity(), override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_feedback) - setupActionBar() + setupToolbar(toolbar) configureObservers() } - private fun setupActionBar() { - setSupportActionBar(toolbar) - supportActionBar?.setDisplayHomeAsUpEnabled(true) - } - private fun configureObservers() { viewModel.command.observe(this, Observer { it?.let { command -> processCommand(command) } diff --git a/app/src/main/java/com/duckduckgo/app/global/DuckDuckGoActivity.kt b/app/src/main/java/com/duckduckgo/app/global/DuckDuckGoActivity.kt index 7ab6cbdd5add..fd4fb5983540 100644 --- a/app/src/main/java/com/duckduckgo/app/global/DuckDuckGoActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/global/DuckDuckGoActivity.kt @@ -21,9 +21,11 @@ import android.content.BroadcastReceiver import android.os.Bundle import android.view.MenuItem import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.widget.Toolbar import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.localbroadcastmanager.content.LocalBroadcastManager +import com.duckduckgo.app.browser.R import com.duckduckgo.app.settings.db.SettingsDataStore import dagger.android.AndroidInjection import javax.inject.Inject @@ -75,6 +77,12 @@ abstract class DuckDuckGoActivity : AppCompatActivity() { super.onDestroy() } + fun setupToolbar(toolbar: Toolbar) { + setSupportActionBar(toolbar) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + toolbar.setNavigationIcon(R.drawable.ic_back) + } + protected inline fun bindViewModel() = lazy { ViewModelProvider(this, viewModelFactory).get(V::class.java) } } diff --git a/app/src/main/java/com/duckduckgo/app/icon/ui/ChangeIconActivity.kt b/app/src/main/java/com/duckduckgo/app/icon/ui/ChangeIconActivity.kt index 4192ca92e769..85da644eb39c 100644 --- a/app/src/main/java/com/duckduckgo/app/icon/ui/ChangeIconActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/icon/ui/ChangeIconActivity.kt @@ -23,7 +23,6 @@ import androidx.appcompat.app.AlertDialog import androidx.lifecycle.Observer import androidx.recyclerview.widget.GridLayoutManager import com.duckduckgo.app.browser.R -import com.duckduckgo.app.fire.FireActivity import com.duckduckgo.app.global.DuckDuckGoActivity import kotlinx.android.synthetic.main.content_app_icons.appIconsList import kotlinx.android.synthetic.main.include_toolbar.toolbar @@ -44,17 +43,12 @@ class ChangeIconActivity : DuckDuckGoActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_app_icons) - setupActionBar() + setupToolbar(toolbar) configureRecycler() observeViewModel() } - private fun setupActionBar() { - setSupportActionBar(toolbar) - supportActionBar?.setDisplayHomeAsUpEnabled(true) - } - private fun configureRecycler() { appIconsList.layoutManager = GridLayoutManager(this, 4) appIconsList.addItemDecoration(ItemOffsetDecoration(this, R.dimen.changeAppIconListPadding)) diff --git a/app/src/main/java/com/duckduckgo/app/privacy/ui/PrivacyDashboardActivity.kt b/app/src/main/java/com/duckduckgo/app/privacy/ui/PrivacyDashboardActivity.kt index bc0686f7856d..4597f460e60a 100644 --- a/app/src/main/java/com/duckduckgo/app/privacy/ui/PrivacyDashboardActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/privacy/ui/PrivacyDashboardActivity.kt @@ -55,7 +55,7 @@ class PrivacyDashboardActivity : DuckDuckGoActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_privacy_dashboard) - configureToolbar() + setupToolbar(toolbar) viewModel.viewState.observe(this, Observer { it?.let { render(it) } @@ -74,11 +74,6 @@ class PrivacyDashboardActivity : DuckDuckGoActivity() { } } - private fun configureToolbar() { - setSupportActionBar(toolbar) - supportActionBar?.setDisplayHomeAsUpEnabled(true) - } - private fun render(viewState: ViewState) { if (isFinishing) { return diff --git a/app/src/main/java/com/duckduckgo/app/privacy/ui/PrivacyPracticesActivity.kt b/app/src/main/java/com/duckduckgo/app/privacy/ui/PrivacyPracticesActivity.kt index 588a5097a063..f78507b757d6 100644 --- a/app/src/main/java/com/duckduckgo/app/privacy/ui/PrivacyPracticesActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/privacy/ui/PrivacyPracticesActivity.kt @@ -47,7 +47,7 @@ class PrivacyPracticesActivity : DuckDuckGoActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_privacy_practices) - configureToolbar() + setupToolbar(toolbar) configureRecycler() viewModel.viewState.observe(this, Observer { @@ -59,11 +59,6 @@ class PrivacyPracticesActivity : DuckDuckGoActivity() { }) } - private fun configureToolbar() { - setSupportActionBar(toolbar) - supportActionBar?.setDisplayHomeAsUpEnabled(true) - } - private fun configureRecycler() { practicesList.layoutManager = LinearLayoutManager(this) practicesList.adapter = practicesAdapter diff --git a/app/src/main/java/com/duckduckgo/app/privacy/ui/ScorecardActivity.kt b/app/src/main/java/com/duckduckgo/app/privacy/ui/ScorecardActivity.kt index eece8050e2fb..3e5be9a6f0f1 100644 --- a/app/src/main/java/com/duckduckgo/app/privacy/ui/ScorecardActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/privacy/ui/ScorecardActivity.kt @@ -47,7 +47,7 @@ class ScorecardActivity : DuckDuckGoActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_privacy_scorecard) - configureToolbar() + setupToolbar(toolbar) viewModel.viewState.observe(this, Observer { it?.let { render(it) } @@ -58,11 +58,6 @@ class ScorecardActivity : DuckDuckGoActivity() { }) } - private fun configureToolbar() { - setSupportActionBar(toolbar) - supportActionBar?.setDisplayHomeAsUpEnabled(true) - } - private fun render(viewState: ScorecardViewModel.ViewState) { privacyBanner.setImageResource(viewState.afterGrade.banner(viewState.privacyOn)) domain.text = viewState.domain @@ -105,5 +100,4 @@ class ScorecardActivity : DuckDuckGoActivity() { return intent } } - } \ No newline at end of file diff --git a/app/src/main/java/com/duckduckgo/app/privacy/ui/TrackerNetworksActivity.kt b/app/src/main/java/com/duckduckgo/app/privacy/ui/TrackerNetworksActivity.kt index 213c5838f418..7d0daeeed7a3 100644 --- a/app/src/main/java/com/duckduckgo/app/privacy/ui/TrackerNetworksActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/privacy/ui/TrackerNetworksActivity.kt @@ -43,7 +43,7 @@ class TrackerNetworksActivity : DuckDuckGoActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_tracker_networks) - configureToolbar() + setupToolbar(toolbar) configureRecycler() viewModel.viewState.observe(this, Observer { @@ -55,11 +55,6 @@ class TrackerNetworksActivity : DuckDuckGoActivity() { }) } - private fun configureToolbar() { - setSupportActionBar(toolbar) - supportActionBar?.setDisplayHomeAsUpEnabled(true) - } - private fun configureRecycler() { networksList.layoutManager = LinearLayoutManager(this) networksList.adapter = networksAdapter diff --git a/app/src/main/java/com/duckduckgo/app/settings/SettingsActivity.kt b/app/src/main/java/com/duckduckgo/app/settings/SettingsActivity.kt index a778cc0b48e9..4e6026ed7ddb 100644 --- a/app/src/main/java/com/duckduckgo/app/settings/SettingsActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/settings/SettingsActivity.kt @@ -74,7 +74,7 @@ class SettingsActivity : DuckDuckGoActivity(), SettingsAutomaticallyClearWhatFra override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_settings) - setupActionBar() + setupToolbar(toolbar) configureUiEventHandlers() observeViewModel() @@ -162,11 +162,6 @@ class SettingsActivity : DuckDuckGoActivity(), SettingsAutomaticallyClearWhatFra } } - private fun setupActionBar() { - setSupportActionBar(toolbar) - supportActionBar?.setDisplayHomeAsUpEnabled(true) - } - private fun launchFeedback() { val options = ActivityOptions.makeSceneTransitionAnimation(this).toBundle() startActivityForResult(Intent(FeedbackActivity.intent(this)), FEEDBACK_REQUEST_CODE, options) diff --git a/app/src/main/java/com/duckduckgo/app/statistics/VariantManager.kt b/app/src/main/java/com/duckduckgo/app/statistics/VariantManager.kt index f1a6b6e115da..efb92f324a71 100644 --- a/app/src/main/java/com/duckduckgo/app/statistics/VariantManager.kt +++ b/app/src/main/java/com/duckduckgo/app/statistics/VariantManager.kt @@ -19,7 +19,7 @@ package com.duckduckgo.app.statistics import androidx.annotation.WorkerThread import com.duckduckgo.app.statistics.VariantManager.Companion.DEFAULT_VARIANT import com.duckduckgo.app.statistics.VariantManager.Companion.referrerVariant -import com.duckduckgo.app.statistics.VariantManager.VariantFeature.* +import com.duckduckgo.app.statistics.VariantManager.VariantFeature.BottomBarNavigation import com.duckduckgo.app.statistics.store.StatisticsDataStore import timber.log.Timber import java.util.Locale @@ -28,7 +28,9 @@ import java.util.Locale interface VariantManager { // variant-dependant features listed here - sealed class VariantFeature + sealed class VariantFeature { + object BottomBarNavigation : VariantFeature() + } companion object { @@ -41,7 +43,19 @@ interface VariantManager { // SERP variants. "sc" may also be used as a shared control for mobile experiments in // the future if we can filter by app version Variant(key = "sc", weight = 0.0, features = emptyList(), filterBy = { noFilter() }), - Variant(key = "se", weight = 0.0, features = emptyList(), filterBy = { noFilter() }) + Variant(key = "se", weight = 0.0, features = emptyList(), filterBy = { noFilter() }), + + // Bottom Bar Navigation Experiment + Variant( + key = "mm", + weight = 1.0, + features = emptyList(), + filterBy = { noFilter() }), + Variant( + key = "mn", + weight = 1.0, + features = listOf(BottomBarNavigation), + filterBy = { noFilter() }) // All groups in an experiment (control and variants) MUST use the same filters ) diff --git a/app/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt b/app/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt index 9ae7270ad083..00252bcac1eb 100644 --- a/app/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt +++ b/app/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt @@ -163,6 +163,13 @@ interface Pixel { AUTOCOMPLETE_SEARCH_SELECTION("m_aut_s_s"), CHANGE_APP_ICON_OPENED("m_ic"), + + MENU_ACTION_POPUP_OPENED("m_nav_pm_o_%s"), + MENU_ACTION_FIRE_PRESSED("m_nav_f_p_%s"), + MENU_ACTION_REFRESH_PRESSED("m_nav_r_p_%s"), + MENU_ACTION_NEW_TAB_PRESSED("m_nav_nt_p_%s"), + MENU_ACTION_BOOKMARKS_PRESSED("m_nav_b_p_%s"), + MENU_ACTION_SEARCH_PRESSED("m_nav_s_p_%s") } object PixelParameter { diff --git a/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherActivity.kt b/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherActivity.kt index adc737d1fa40..fa1241e2ebba 100644 --- a/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherActivity.kt @@ -85,7 +85,7 @@ class TabSwitcherActivity : DuckDuckGoActivity(), TabSwitcherListener, Coroutine setContentView(R.layout.activity_tab_switcher) extractIntentExtras() configureViewReferences() - configureToolbar() + setupToolbar(toolbar) configureRecycler() configureObservers() } @@ -99,11 +99,6 @@ class TabSwitcherActivity : DuckDuckGoActivity(), TabSwitcherListener, Coroutine toolbar = findViewById(R.id.toolbar) } - private fun configureToolbar() { - setSupportActionBar(toolbar) - supportActionBar?.setDisplayHomeAsUpEnabled(true) - } - private fun configureRecycler() { val numberColumns = gridViewColumnCalculator.calculateNumberOfColumns(TAB_GRID_COLUMN_WIDTH_DP, TAB_GRID_MAX_COLUMN_COUNT) val layoutManager = GridLayoutManager(this, numberColumns) @@ -229,4 +224,4 @@ class TabSwitcherActivity : DuckDuckGoActivity(), TabSwitcherListener, Coroutine private const val TAB_GRID_COLUMN_WIDTH_DP = 180 private const val TAB_GRID_MAX_COLUMN_COUNT = 4 } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherBottomBarFeatureActivity.kt b/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherBottomBarFeatureActivity.kt new file mode 100644 index 000000000000..c1dc8af485df --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherBottomBarFeatureActivity.kt @@ -0,0 +1,247 @@ +/* + * Copyright (c) 2020 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.app.tabs.ui + +import android.content.Context +import android.content.Intent +import android.graphics.Color +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import androidx.lifecycle.Observer +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.ItemTouchHelper +import com.duckduckgo.app.browser.R +import com.duckduckgo.app.browser.tabpreview.WebViewPreviewPersister +import com.duckduckgo.app.global.DuckDuckGoActivity +import com.duckduckgo.app.global.view.ClearPersonalDataAction +import com.duckduckgo.app.global.view.FireDialog +import com.duckduckgo.app.settings.SettingsActivity +import com.duckduckgo.app.statistics.VariantManager +import com.duckduckgo.app.statistics.pixels.Pixel +import com.duckduckgo.app.tabs.model.TabEntity +import com.duckduckgo.app.tabs.ui.TabSwitcherViewModel.Command +import com.duckduckgo.app.tabs.ui.TabSwitcherViewModel.Command.Close +import com.duckduckgo.app.tabs.ui.TabSwitcherViewModel.Command.DisplayMessage +import kotlinx.android.synthetic.main.activity_tab_switcher_bottom_bar_feature.tabsRecycler +import kotlinx.android.synthetic.main.fragment_browser_tab.bottomNavigationBar +import kotlinx.android.synthetic.main.layout_tabs_bottom_navigation_bar.bottomBarFireItem +import kotlinx.android.synthetic.main.layout_tabs_bottom_navigation_bar.bottomBarNewTabItem +import kotlinx.android.synthetic.main.layout_tabs_bottom_navigation_bar.bottomBarOverflowItem +import kotlinx.android.synthetic.main.popup_window_browser_menu.view.settingsPopupMenuItem +import kotlinx.android.synthetic.main.popup_window_tabs_menu.view.closeAllTabs +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch +import org.jetbrains.anko.longToast +import javax.inject.Inject +import kotlin.coroutines.CoroutineContext + +class TabSwitcherBottomBarFeatureActivity : DuckDuckGoActivity(), TabSwitcherListener, CoroutineScope { + + override val coroutineContext: CoroutineContext + get() = SupervisorJob() + Dispatchers.Main + + @Inject + lateinit var clearPersonalDataAction: ClearPersonalDataAction + + @Inject + lateinit var variantManager: VariantManager + + @Inject + lateinit var gridViewColumnCalculator: GridViewColumnCalculator + + @Inject + lateinit var webViewPreviewPersister: WebViewPreviewPersister + + @Inject + lateinit var pixel: Pixel + + private val viewModel: TabSwitcherViewModel by bindViewModel() + + private val tabsAdapter: TabSwitcherAdapter by lazy { TabSwitcherAdapter(this, webViewPreviewPersister) } + + // we need to scroll to show selected tab, but only if it is the first time loading the tabs. + private var firstTimeLoadingTabsList = true + + private var selectedTabId: String? = null + + private lateinit var popupMenu: TabsPopupMenu + + private lateinit var tabGridItemDecorator: TabGridItemDecorator + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + window.statusBarColor = Color.TRANSPARENT + + setContentView(R.layout.activity_tab_switcher_bottom_bar_feature) + extractIntentExtras() + configureRecycler() + configureObservers() + configureBottomBar() + createPopUpMenu() + + } + + private fun extractIntentExtras() { + selectedTabId = intent.getStringExtra(EXTRA_KEY_SELECTED_TAB) + } + + private fun configureRecycler() { + val numberColumns = gridViewColumnCalculator.calculateNumberOfColumns(TAB_GRID_COLUMN_WIDTH_DP, TAB_GRID_MAX_COLUMN_COUNT) + val layoutManager = GridLayoutManager(this, numberColumns) + tabsRecycler.layoutManager = layoutManager + tabsRecycler.adapter = tabsAdapter + + val swipeListener = ItemTouchHelper(SwipeToCloseTabListener(tabsAdapter, numberColumns, object : SwipeToCloseTabListener.OnTabSwipedListener { + override fun onSwiped(tab: TabEntity) { + onTabDeleted(tab) + } + })) + swipeListener.attachToRecyclerView(tabsRecycler) + + tabGridItemDecorator = TabGridItemDecorator(this, selectedTabId) + tabsRecycler.addItemDecoration(tabGridItemDecorator) + } + + private fun configureObservers() { + viewModel.tabs.observe(this, Observer> { + render(it) + }) + viewModel.command.observe(this, Observer { + processCommand(it) + }) + } + + private fun render(tabs: List) { + tabsAdapter.updateData(tabs) + + if (firstTimeLoadingTabsList) { + firstTimeLoadingTabsList = false + + scrollToShowCurrentTab() + } + } + + private fun scrollToShowCurrentTab() { + val index = tabsAdapter.adapterPositionForTab(selectedTabId) + tabsRecycler.scrollToPosition(index) + } + + private fun processCommand(command: Command?) { + when (command) { + is DisplayMessage -> applicationContext?.longToast(command.messageId) + is Close -> finishAfterTransition() + } + } + + private fun configureBottomBar() { + bottomNavigationBar.apply { + onItemClicked(bottomBarNewTabItem) { onNewTabRequested() } + onItemClicked(bottomBarFireItem) { onFire() } + onItemClicked(bottomBarOverflowItem) { popupMenu.show(rootView, bottomNavigationBar) } + } + } + + private fun createPopUpMenu() { + popupMenu = TabsPopupMenu(layoutInflater) + val view = popupMenu.contentView + popupMenu.apply { + onMenuItemClicked(view.closeAllTabs) { closeAllTabs() } + onMenuItemClicked(view.settingsPopupMenuItem) { showSettings() } + } + } + + override fun onCreateOptionsMenu(menu: Menu?): Boolean { + menuInflater.inflate(R.menu.menu_tab_switcher_activity, menu) + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.fire -> onFire() + R.id.newTab, R.id.newTabOverflow -> onNewTabRequested() + R.id.closeAllTabs -> closeAllTabs() + R.id.settings -> showSettings() + } + return super.onOptionsItemSelected(item) + } + + private fun onFire() { + pixel.fire(Pixel.PixelName.FORGET_ALL_PRESSED_TABSWITCHING) + val dialog = FireDialog(context = this, clearPersonalDataAction = clearPersonalDataAction) + dialog.clearComplete = { viewModel.onClearComplete() } + dialog.show() + } + + override fun onNewTabRequested() { + clearObserversEarlyToStopViewUpdates() + launch { viewModel.onNewTabRequested() } + } + + override fun onTabSelected(tab: TabEntity) { + selectedTabId = tab.tabId + updateTabGridItemDecorator(tab) + launch { viewModel.onTabSelected(tab) } + } + + private fun updateTabGridItemDecorator(tab: TabEntity) { + tabGridItemDecorator.selectedTabId = tab.tabId + tabsRecycler.invalidateItemDecorations() + } + + override fun onTabDeleted(tab: TabEntity) { + launch { viewModel.onTabDeleted(tab) } + } + + private fun closeAllTabs() { + launch { + viewModel.tabs.value?.forEach { + viewModel.onTabDeleted(it) + } + } + } + + private fun showSettings() { + startActivity(SettingsActivity.intent(this)) + } + + override fun finish() { + clearObserversEarlyToStopViewUpdates() + super.finish() + overridePendingTransition(R.anim.slide_from_bottom, R.anim.tab_anim_fade_out) + } + + private fun clearObserversEarlyToStopViewUpdates() { + viewModel.tabs.removeObservers(this) + } + + companion object { + fun intent(context: Context, selectedTabId: String? = null): Intent { + val intent = Intent(context, TabSwitcherBottomBarFeatureActivity::class.java) + intent.putExtra(EXTRA_KEY_SELECTED_TAB, selectedTabId) + return intent + } + + const val EXTRA_KEY_SELECTED_TAB = "selected" + + private const val TAB_GRID_COLUMN_WIDTH_DP = 180 + private const val TAB_GRID_MAX_COLUMN_COUNT = 4 + } +} diff --git a/app/src/main/java/com/duckduckgo/app/tabs/ui/TabsPopupMenu.kt b/app/src/main/java/com/duckduckgo/app/tabs/ui/TabsPopupMenu.kt new file mode 100644 index 000000000000..fab4fffac010 --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/tabs/ui/TabsPopupMenu.kt @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2020 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.app.tabs.ui + +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.os.Build.VERSION.SDK_INT +import android.view.Gravity +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup.LayoutParams.WRAP_CONTENT +import android.widget.PopupWindow +import com.duckduckgo.app.browser.R + +class TabsPopupMenu(layoutInflater: LayoutInflater, view: View = inflate(layoutInflater, R.layout.popup_window_tabs_menu)) : + PopupWindow(view, WRAP_CONTENT, WRAP_CONTENT, true) { + + // popupwindow gets stuck on the screen on API 22 (tested on 23) without a background + // color. Adding it however garbles the elevation so we cannot have elevation here. + + init { + if (SDK_INT <= 22) { + // popupwindow gets stuck on the screen on API 22 (tested on 23) without a background + // color. Adding it however garbles the elevation so we cannot have elevation here. + setBackgroundDrawable(ColorDrawable(Color.WHITE)) + } else { + elevation = 6.toFloat() + } + animationStyle = android.R.style.Animation_Dialog + } + + fun onMenuItemClicked(menuView: View, onClick: () -> Unit) { + menuView.setOnClickListener { + onClick() + dismiss() + } + } + + fun show(rootView: View, anchorView: View) { + val anchorLocation = IntArray(2) + anchorView.getLocationOnScreen(anchorLocation) + val x = MARGIN + val y = anchorLocation[1] + MARGIN + showAtLocation(rootView, Gravity.TOP or Gravity.END, x, y) + } + + companion object { + + private const val MARGIN = 30 + + fun inflate(layoutInflater: LayoutInflater, resourceId: Int): View { + return layoutInflater.inflate(resourceId, null) + } + } +} + diff --git a/app/src/main/res/color/browser_menu_icon.xml b/app/src/main/res/color/bottom_bar_icon_color_selector.xml similarity index 81% rename from app/src/main/res/color/browser_menu_icon.xml rename to app/src/main/res/color/bottom_bar_icon_color_selector.xml index da6c0707e404..4fb5b9922830 100644 --- a/app/src/main/res/color/browser_menu_icon.xml +++ b/app/src/main/res/color/bottom_bar_icon_color_selector.xml @@ -1,5 +1,4 @@ - - - + + diff --git a/app/src/main/res/color/browser_menu_text.xml b/app/src/main/res/color/browser_menu_text.xml index 432a80b6c1df..641135f1d39b 100644 --- a/app/src/main/res/color/browser_menu_text.xml +++ b/app/src/main/res/color/browser_menu_text.xml @@ -17,6 +17,6 @@ --> - - + + diff --git a/app/src/main/res/drawable/bottom_navigation_bar_bg.xml b/app/src/main/res/drawable/bottom_navigation_bar_bg.xml new file mode 100644 index 000000000000..1cf17cd39fdb --- /dev/null +++ b/app/src/main/res/drawable/bottom_navigation_bar_bg.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_search_black_24dp.xml b/app/src/main/res/drawable/ic_back.xml similarity index 59% rename from app/src/main/res/drawable/ic_search_black_24dp.xml rename to app/src/main/res/drawable/ic_back.xml index 57b7762c6332..ce34fa83272e 100644 --- a/app/src/main/res/drawable/ic_search_black_24dp.xml +++ b/app/src/main/res/drawable/ic_back.xml @@ -15,11 +15,11 @@ --> - + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + diff --git a/app/src/main/res/drawable/ic_bottom_bar_bookmarks.xml b/app/src/main/res/drawable/ic_bottom_bar_bookmarks.xml new file mode 100644 index 000000000000..bface6f092e5 --- /dev/null +++ b/app/src/main/res/drawable/ic_bottom_bar_bookmarks.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_bottom_bar_refresh.xml b/app/src/main/res/drawable/ic_bottom_bar_refresh.xml new file mode 100644 index 000000000000..d24b16a5d4a5 --- /dev/null +++ b/app/src/main/res/drawable/ic_bottom_bar_refresh.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_bottom_bar_search.xml b/app/src/main/res/drawable/ic_bottom_bar_search.xml new file mode 100644 index 000000000000..838f75c21e93 --- /dev/null +++ b/app/src/main/res/drawable/ic_bottom_bar_search.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_close.xml b/app/src/main/res/drawable/ic_close.xml new file mode 100644 index 000000000000..4e917f150431 --- /dev/null +++ b/app/src/main/res/drawable/ic_close.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_fire_black_24dp.xml b/app/src/main/res/drawable/ic_fire_black_24dp.xml new file mode 100644 index 000000000000..7811efe4484c --- /dev/null +++ b/app/src/main/res/drawable/ic_fire_black_24dp.xml @@ -0,0 +1,25 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_forward.xml b/app/src/main/res/drawable/ic_forward.xml new file mode 100644 index 000000000000..2ccd5ffe53e2 --- /dev/null +++ b/app/src/main/res/drawable/ic_forward.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_newtab.xml b/app/src/main/res/drawable/ic_newtab.xml new file mode 100644 index 000000000000..e5fd10f43cf4 --- /dev/null +++ b/app/src/main/res/drawable/ic_newtab.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_overflow_bookmarks_24dp.xml b/app/src/main/res/drawable/ic_overflow_bookmarks_24dp.xml index 83ac3a8ec093..bf80a21a811e 100644 --- a/app/src/main/res/drawable/ic_overflow_bookmarks_24dp.xml +++ b/app/src/main/res/drawable/ic_overflow_bookmarks_24dp.xml @@ -20,6 +20,6 @@ android:viewportWidth="24.0" android:viewportHeight="24.0"> diff --git a/app/src/main/res/drawable/ic_share.xml b/app/src/main/res/drawable/ic_share.xml new file mode 100644 index 000000000000..a9265a9069bb --- /dev/null +++ b/app/src/main/res/drawable/ic_share.xml @@ -0,0 +1,26 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_tabs.xml b/app/src/main/res/drawable/ic_tabs.xml new file mode 100644 index 000000000000..0ba48fe2cb69 --- /dev/null +++ b/app/src/main/res/drawable/ic_tabs.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/popup_menu_bg.xml b/app/src/main/res/drawable/popup_menu_bg.xml new file mode 100644 index 000000000000..faaf444af467 --- /dev/null +++ b/app/src/main/res/drawable/popup_menu_bg.xml @@ -0,0 +1,20 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_browser.xml b/app/src/main/res/layout/activity_browser.xml index 24bbbdca1c5e..af5c31b1dd5f 100644 --- a/app/src/main/res/layout/activity_browser.xml +++ b/app/src/main/res/layout/activity_browser.xml @@ -21,10 +21,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - app:layout_scrollFlags="scroll|enterAlways" - tools:context="com.duckduckgo.app.browser.BrowserActivity" - tools:menu="@menu/menu_browser_activity"> - + tools:context="com.duckduckgo.app.browser.BrowserActivity"> - + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_tab_switcher_bottom_bar_feature.xml b/app/src/main/res/layout/activity_tab_switcher_bottom_bar_feature.xml new file mode 100644 index 000000000000..175a1bb0d139 --- /dev/null +++ b/app/src/main/res/layout/activity_tab_switcher_bottom_bar_feature.xml @@ -0,0 +1,57 @@ + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_browser_tab.xml b/app/src/main/res/layout/fragment_browser_tab.xml index 24aa0a1e9a27..ef5f9e496c81 100644 --- a/app/src/main/res/layout/fragment_browser_tab.xml +++ b/app/src/main/res/layout/fragment_browser_tab.xml @@ -19,11 +19,32 @@ xmlns:tools="http://schemas.android.com/tools" android:id="@+id/rootView" android:layout_width="match_parent" - android:layout_height="match_parent" - app:layout_scrollFlags="scroll|enterAlways"> + android:layout_height="match_parent"> + + + + + tools:context="com.duckduckgo.app.browser.BrowserActivity"> + android:theme="@style/AppTheme.Dark.AppBarOverlay"> + android:theme="@style/OmnibarToolbarTheme"> - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + tools:visibility="visible"> - diff --git a/app/src/main/res/layout/item_autocomplete_search_suggestion.xml b/app/src/main/res/layout/item_autocomplete_search_suggestion.xml index dbd8be91510b..b929cfa9d7bd 100644 --- a/app/src/main/res/layout/item_autocomplete_search_suggestion.xml +++ b/app/src/main/res/layout/item_autocomplete_search_suggestion.xml @@ -26,7 +26,7 @@ android:id="@+id/phraseOrUrlIndicator" android:layout_width="24dp" android:layout_height="24dp" - android:layout_marginStart="16dp" + android:layout_marginStart="18dp" android:importantForAccessibility="no" android:src="@drawable/ic_loupe_24dp" app:layout_constraintBottom_toBottomOf="parent" @@ -37,7 +37,7 @@ android:id="@+id/phrase" android:layout_width="0dp" android:layout_height="0dp" - android:layout_marginStart="16dp" + android:layout_marginStart="8dp" android:ellipsize="end" android:includeFontPadding="false" android:fontFamily="sans-serif" @@ -58,7 +58,7 @@ android:layout_height="wrap_content" android:padding="6dp" android:paddingStart="16dp" - android:paddingEnd="14dp" + android:paddingEnd="16dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent"> diff --git a/app/src/main/res/layout/layout_browser_bottom_navigation_bar.xml b/app/src/main/res/layout/layout_browser_bottom_navigation_bar.xml new file mode 100644 index 000000000000..69faea48654f --- /dev/null +++ b/app/src/main/res/layout/layout_browser_bottom_navigation_bar.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_tabs_bottom_navigation_bar.xml b/app/src/main/res/layout/layout_tabs_bottom_navigation_bar.xml new file mode 100644 index 000000000000..ac6e55476990 --- /dev/null +++ b/app/src/main/res/layout/layout_tabs_bottom_navigation_bar.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/popup_window_browser_bottom_tab_menu.xml b/app/src/main/res/layout/popup_window_browser_bottom_tab_menu.xml new file mode 100644 index 000000000000..e267ce816c83 --- /dev/null +++ b/app/src/main/res/layout/popup_window_browser_bottom_tab_menu.xml @@ -0,0 +1,145 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/popup_window_browser_menu.xml b/app/src/main/res/layout/popup_window_browser_menu.xml index cf30740ddd16..dc6d31acbf03 100644 --- a/app/src/main/res/layout/popup_window_browser_menu.xml +++ b/app/src/main/res/layout/popup_window_browser_menu.xml @@ -1,6 +1,6 @@ + + + + + + + + diff --git a/app/src/main/res/layout/view_tab_switcher_button.xml b/app/src/main/res/layout/view_tab_switcher_button.xml index eaa521925e51..be244a356832 100644 --- a/app/src/main/res/layout/view_tab_switcher_button.xml +++ b/app/src/main/res/layout/view_tab_switcher_button.xml @@ -14,41 +14,23 @@ ~ limitations under the License. --> - - - + tools:parentTag="android.widget.RelativeLayout"> - - - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/menu/menu_browser_activity.xml b/app/src/main/res/menu/menu_browser_activity.xml deleted file mode 100644 index b1a9ae5d5b9b..000000000000 --- a/app/src/main/res/menu/menu_browser_activity.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/values-v23/styles.xml b/app/src/main/res/values-v23/styles.xml new file mode 100644 index 000000000000..1c6ae16f9a5a --- /dev/null +++ b/app/src/main/res/values-v23/styles.xml @@ -0,0 +1,27 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index ee1fcb945123..084377713189 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -60,5 +60,11 @@ + + + + + + diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index a17b74e8c2ac..80b525be6dca 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -36,4 +36,6 @@ 4dp 75dp + 48dp + \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 886f393147e8..5817403a8c2a 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -149,13 +149,13 @@ + + + +