diff --git a/app/src/main/java/org/mozilla/focus/activity/MainActivity.kt b/app/src/main/java/org/mozilla/focus/activity/MainActivity.kt index 4adbe716f7..bf9b2a0bb9 100644 --- a/app/src/main/java/org/mozilla/focus/activity/MainActivity.kt +++ b/app/src/main/java/org/mozilla/focus/activity/MainActivity.kt @@ -30,6 +30,8 @@ import androidx.appcompat.app.AlertDialog import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.fragment.app.DialogFragment +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager import androidx.lifecycle.LiveData import androidx.lifecycle.Observer import androidx.localbroadcastmanager.content.LocalBroadcastManager @@ -83,8 +85,8 @@ import org.mozilla.rocket.landing.NavigationModel import org.mozilla.rocket.landing.OrientationState import org.mozilla.rocket.landing.PortraitComponent import org.mozilla.rocket.landing.PortraitStateModel -import org.mozilla.rocket.menu.BrowserMenuDialog -import org.mozilla.rocket.menu.HomeMenuDialog +import org.mozilla.rocket.menu.BottomSheetBrowserMenuFragment +import org.mozilla.rocket.menu.BottomSheetHomeMenuFragment import org.mozilla.rocket.periodic.FirstLaunchWorker import org.mozilla.rocket.periodic.PeriodicReceiver import org.mozilla.rocket.privately.PrivateMode @@ -131,8 +133,6 @@ class MainActivity : private lateinit var downloadIndicatorViewModel: DownloadIndicatorViewModel private var promotionModel: PromotionModel? = null - private lateinit var homeMenu: HomeMenuDialog - private lateinit var browserMenu: BrowserMenuDialog private var myshotOnBoardingDialog: Dialog? = null private lateinit var screenNavigator: ScreenNavigator @@ -231,22 +231,10 @@ class MainActivity : } private fun setUpMenu() { - if (::homeMenu.isInitialized) { - homeMenu.release() - } - homeMenu = HomeMenuDialog(this, R.style.BottomSheetTheme).apply { - setCanceledOnTouchOutside(true) - setOnShowListener { portraitStateModel.request(PortraitComponent.BottomMenu) } - setOnDismissListener { portraitStateModel.cancelRequest(PortraitComponent.BottomMenu) } - } - if (::browserMenu.isInitialized) { - browserMenu.release() - } - browserMenu = BrowserMenuDialog(this, R.style.BottomSheetTheme).apply { - setCanceledOnTouchOutside(true) - setOnShowListener { portraitStateModel.request(PortraitComponent.BottomMenu) } - setOnDismissListener { portraitStateModel.cancelRequest(PortraitComponent.BottomMenu) } - } + supportFragmentManager.registerFragmentLifecycleCallbacks( + FragmentStateListener(portraitStateModel), + false + ) } private fun initBroadcastReceivers() { @@ -355,8 +343,12 @@ class MainActivity : TabTray.show(supportFragmentManager) } ) - showHomeMenu.observe(this@MainActivity, Observer { homeMenu.show() }) - showBrowserMenu.observe(this@MainActivity, Observer { browserMenu.show() }) + showHomeMenu.observe(this@MainActivity) { + BottomSheetHomeMenuFragment.show(supportFragmentManager) + } + showBrowserMenu.observe(this@MainActivity) { + BottomSheetBrowserMenuFragment.show(supportFragmentManager) + } showNewTab.observe( this@MainActivity, Observer { @@ -698,8 +690,8 @@ class MainActivity : } private fun dismissAllMenus() { - homeMenu.dismiss() - browserMenu.dismiss() + BottomSheetHomeMenuFragment.dismiss(supportFragmentManager) + BottomSheetBrowserMenuFragment.dismiss(supportFragmentManager) visibleBrowserFragment?.run { dismissAllMenus() } getListPanelFragment()?.dismissAllowingStateLoss() myshotOnBoardingDialog?.run { @@ -862,7 +854,7 @@ class MainActivity : @VisibleForTesting @UiThread fun showMyShotOnBoarding() { - val view = browserMenu.findViewById(R.id.menu_screenshots) + val view = BottomSheetBrowserMenuFragment.getScreenshotMenuButton(supportFragmentManager) view?.post { myshotOnBoardingDialog = DialogUtils.showMyShotOnBoarding( this@MainActivity, @@ -878,7 +870,6 @@ class MainActivity : ) chromeViewModel.onMyShotOnBoardingDisplayed() } - browserMenu.show() } private fun checkInAppUpdate() { @@ -1011,6 +1002,30 @@ class MainActivity : } } + private class FragmentStateListener( + private val portraitStateModel: PortraitStateModel + ) : FragmentManager.FragmentLifecycleCallbacks() { + override fun onFragmentAttached( + fm: FragmentManager, + f: Fragment, + context: Context + ) { + when (f.tag) { + BottomSheetBrowserMenuFragment.TAG, + BottomSheetHomeMenuFragment.TAG -> + portraitStateModel.request(PortraitComponent.BottomMenu) + } + } + + override fun onFragmentDetached(fm: FragmentManager, f: Fragment) { + when (f.tag) { + BottomSheetBrowserMenuFragment.TAG, + BottomSheetHomeMenuFragment.TAG -> + portraitStateModel.cancelRequest(PortraitComponent.BottomMenu) + } + } + } + companion object { const val REQUEST_CODE_IN_APP_UPDATE = 1024 const val ACTION_INSTALL_IN_APP_UPDATE = "action_install_in_app_update" diff --git a/app/src/main/java/org/mozilla/focus/fragment/ListPanelDialog.kt b/app/src/main/java/org/mozilla/focus/fragment/ListPanelDialog.kt index 1f810900c8..b1c8116a86 100644 --- a/app/src/main/java/org/mozilla/focus/fragment/ListPanelDialog.kt +++ b/app/src/main/java/org/mozilla/focus/fragment/ListPanelDialog.kt @@ -56,6 +56,8 @@ class ListPanelDialog : DialogFragment() { this.dismissAllowingStateLoss() } + ScrollableBottomSheetHelper.setRoundedCorner(it.container) + setTopButtonsClickListener(it) enableLoadMore(it.mainContent) }.root diff --git a/app/src/main/java/org/mozilla/focus/utils/ScrollableBottomSheetHelper.kt b/app/src/main/java/org/mozilla/focus/utils/ScrollableBottomSheetHelper.kt index 3fcc557298..17d97feead 100644 --- a/app/src/main/java/org/mozilla/focus/utils/ScrollableBottomSheetHelper.kt +++ b/app/src/main/java/org/mozilla/focus/utils/ScrollableBottomSheetHelper.kt @@ -22,7 +22,6 @@ object ScrollableBottomSheetHelper { menuBottomMargin: Float = context.resources.getDimension(R.dimen.menu_bottom_margin), dismissListener: () -> Unit ) { - setRoundedCorner(binding.container, cornerRadius) binding.container.setOnClickListener { dismissListener.invoke() } @@ -35,7 +34,10 @@ object ScrollableBottomSheetHelper { ) } - private fun setRoundedCorner(container: CoordinatorLayout, cornerRadius: Float) { + fun setRoundedCorner( + container: ViewGroup, + cornerRadius: Float = container.resources.getDimension(R.dimen.menu_corner_radius) + ) { container.outlineProvider = RoundedCornerOutlineProvider(cornerRadius) container.clipToOutline = true } @@ -55,7 +57,7 @@ object ScrollableBottomSheetHelper { val callback = BottomSheetCallback( bottomSheetBehavior, - rootView, + bottomSheet, menuBottomMargin, cornerRadius, dismissListener @@ -74,7 +76,7 @@ object ScrollableBottomSheetHelper { private class BottomSheetCallback( val bottomSheetBehavior: BottomSheetBehavior, - val rootView: ViewGroup, + val movableView: ViewGroup, menuBottomMargin: Float, cornerRadius: Float, val dismissListener: () -> Unit @@ -106,12 +108,12 @@ object ScrollableBottomSheetHelper { if (translationYChanged) { this.translationY = currentTranslationY if (abs(currentTranslationY) <= maxTranslationY) { - rootView.translationY = currentTranslationY + movableView.translationY = currentTranslationY } else if (currentTranslationY > maxTranslationY && - rootView.translationY < maxTranslationY + movableView.translationY < maxTranslationY ) { // In case of fast changing - rootView.translationY = maxTranslationY + movableView.translationY = maxTranslationY } } } diff --git a/app/src/main/java/org/mozilla/rocket/di/AppComponent.kt b/app/src/main/java/org/mozilla/rocket/di/AppComponent.kt index 16ecb9d07a..6b8a5d2203 100644 --- a/app/src/main/java/org/mozilla/rocket/di/AppComponent.kt +++ b/app/src/main/java/org/mozilla/rocket/di/AppComponent.kt @@ -43,8 +43,8 @@ import org.mozilla.rocket.home.HomeFragment import org.mozilla.rocket.home.di.HomeModule import org.mozilla.rocket.home.topsites.domain.GetTopSitesUseCase import org.mozilla.rocket.home.topsites.ui.AddNewTopSitesFragment -import org.mozilla.rocket.menu.BrowserMenuDialog -import org.mozilla.rocket.menu.HomeMenuDialog +import org.mozilla.rocket.menu.BottomSheetBrowserMenuFragment +import org.mozilla.rocket.menu.BottomSheetHomeMenuFragment import org.mozilla.rocket.menu.PrivateBrowserMenuDialog import org.mozilla.rocket.privately.PrivateModeActivity import org.mozilla.rocket.privately.home.PrivateHomeFragment @@ -91,8 +91,8 @@ interface AppComponent { fun inject(tabTrayFragment: TabTrayFragment) fun inject(privateHomeFragment: PrivateHomeFragment) fun inject(urlInputFragment: UrlInputFragment) - fun inject(homeMenuDialog: HomeMenuDialog) - fun inject(browserMenuDialog: BrowserMenuDialog) + fun inject(homeMenuFragment: BottomSheetHomeMenuFragment) + fun inject(browserMenuFragment: BottomSheetBrowserMenuFragment) fun inject(privateBrowserMenuDialog: PrivateBrowserMenuDialog) fun inject(browsingHistoryFragment: BrowsingHistoryFragment) fun inject(privateModeActivity: PrivateModeActivity) diff --git a/app/src/main/java/org/mozilla/rocket/menu/BrowserMenuDialog.kt b/app/src/main/java/org/mozilla/rocket/menu/BottomSheetBrowserMenuFragment.kt similarity index 70% rename from app/src/main/java/org/mozilla/rocket/menu/BrowserMenuDialog.kt rename to app/src/main/java/org/mozilla/rocket/menu/BottomSheetBrowserMenuFragment.kt index 3a63054dc4..7bb2c47712 100644 --- a/app/src/main/java/org/mozilla/rocket/menu/BrowserMenuDialog.kt +++ b/app/src/main/java/org/mozilla/rocket/menu/BottomSheetBrowserMenuFragment.kt @@ -1,17 +1,16 @@ package org.mozilla.rocket.menu -import android.content.Context -import android.graphics.Outline import android.os.Bundle import android.os.Handler import android.os.Looper +import android.view.LayoutInflater import android.view.View -import android.view.ViewOutlineProvider -import android.widget.ScrollView +import android.view.ViewGroup import android.widget.Toast -import androidx.annotation.StyleRes import androidx.core.content.ContextCompat import androidx.core.content.pm.ShortcutManagerCompat +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.FragmentManager import androidx.lifecycle.Observer import dagger.Lazy import org.mozilla.fileutils.FileUtils @@ -19,19 +18,19 @@ import org.mozilla.focus.R import org.mozilla.focus.databinding.BottomSheetBrowserMenuBinding import org.mozilla.focus.telemetry.TelemetryWrapper import org.mozilla.focus.utils.FormatUtils +import org.mozilla.focus.utils.ScrollableBottomSheetHelper import org.mozilla.rocket.chrome.BottomBarItemAdapter import org.mozilla.rocket.chrome.ChromeViewModel import org.mozilla.rocket.chrome.MenuViewModel -import org.mozilla.rocket.chrome.bottombar.BottomBarItem.ItemType +import org.mozilla.rocket.chrome.bottombar.BottomBarItem import org.mozilla.rocket.content.appComponent import org.mozilla.rocket.content.getActivityViewModel import org.mozilla.rocket.extension.nonNullObserve import org.mozilla.rocket.extension.switchFrom import org.mozilla.rocket.nightmode.AdjustBrightnessDialog -import org.mozilla.rocket.widget.LifecycleBottomSheetDialog import javax.inject.Inject -class BrowserMenuDialog : LifecycleBottomSheetDialog { +class BottomSheetBrowserMenuFragment : DialogFragment() { @Inject lateinit var chromeViewModelCreator: Lazy @@ -39,91 +38,85 @@ class BrowserMenuDialog : LifecycleBottomSheetDialog { @Inject lateinit var menuViewModelCreator: Lazy - private lateinit var menuViewModel: MenuViewModel + private val uiHandler = Handler(Looper.getMainLooper()) + private lateinit var chromeViewModel: ChromeViewModel + private lateinit var menuViewModel: MenuViewModel private lateinit var bottomBarItemAdapter: BottomBarItemAdapter - private lateinit var binding: BottomSheetBrowserMenuBinding - private val uiHandler = Handler(Looper.getMainLooper()) - - constructor(context: Context) : super(context) - constructor(context: Context, @StyleRes theme: Int) : super(context, theme) + private var binding: BottomSheetBrowserMenuBinding? = null override fun onCreate(savedInstanceState: Bundle?) { appComponent().inject(this) super.onCreate(savedInstanceState) + // overwrite android.R.style.Theme_Panel, so it looks like normal Fragment + setStyle(STYLE_NO_TITLE, R.style.BottomSheetTheme) + chromeViewModel = getActivityViewModel(chromeViewModelCreator) menuViewModel = getActivityViewModel(menuViewModelCreator) - - initLayout() - observeChromeAction() - setCancelable(false) - setCanceledOnTouchOutside(true) } - override fun dismiss() { - if (::binding.isInitialized) { - binding.scrollView.fullScroll(ScrollView.FOCUS_UP) - } - super.dismiss() - } + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View = BottomSheetBrowserMenuBinding.inflate(inflater, container, false).also { + binding = it + initLayout(it) + observeChromeAction() + }.root - override fun onDetachedFromWindow() { + override fun onDestroyView() { + super.onDestroyView() uiHandler.removeCallbacksAndMessages(null) - super.onDetachedFromWindow() + binding = null } - private fun initLayout() { - binding = BottomSheetBrowserMenuBinding.inflate(layoutInflater, null, false) - binding.contentLayout.apply { - outlineProvider = object : ViewOutlineProvider() { - override fun getOutline(view: View, outline: Outline) { - outline.setRoundRect( - 0, - 0, - view.width, - view.height, - resources.getDimension(R.dimen.menu_corner_radius) - ) - } - } - clipToOutline = true + private fun initLayout(binding: BottomSheetBrowserMenuBinding) { + val helperBinding = ScrollableBottomSheetHelper.Binding( + binding.root, + binding.container, + binding.bottomSheet + ) + + ScrollableBottomSheetHelper.makeViewScrollable(this.requireContext(), helperBinding) { + this.dismissAllowingStateLoss() } - initMenuTabs() - initMenuItems() - initBottomBar() - setContentView(binding.root) + + initMenuTabs(binding) + initMenuItems(binding) + initBottomBar(binding) } - private fun initMenuTabs() { + private fun initMenuTabs(binding: BottomSheetBrowserMenuBinding) { binding.contentLayout.apply { - chromeViewModel.hasUnreadScreenshot.observe(this@BrowserMenuDialog) { + chromeViewModel.hasUnreadScreenshot.observe(this@BottomSheetBrowserMenuFragment) { binding.imgScreenshots.isActivated = it } binding.menuScreenshots.setOnClickListener { postDelayClickEvent { - cancel() + dismissAllowingStateLoss() chromeViewModel.showScreenshots() } } binding.menuBookmark.setOnClickListener { postDelayClickEvent { - cancel() + dismissAllowingStateLoss() chromeViewModel.showBookmarks.call() TelemetryWrapper.clickMenuBookmark() } } binding.menuHistory.setOnClickListener { postDelayClickEvent { - cancel() + dismissAllowingStateLoss() chromeViewModel.showHistory.call() TelemetryWrapper.clickMenuHistory() } } binding.menuDownload.setOnClickListener { postDelayClickEvent { - cancel() + dismissAllowingStateLoss() chromeViewModel.showDownloadPanel.call() TelemetryWrapper.clickMenuDownload() } @@ -131,36 +124,36 @@ class BrowserMenuDialog : LifecycleBottomSheetDialog { } } - private fun initMenuItems() { + private fun initMenuItems(binding: BottomSheetBrowserMenuBinding) { binding.contentLayout.apply { - chromeViewModel.isTurboModeEnabled.observe(this@BrowserMenuDialog) { + chromeViewModel.isTurboModeEnabled.observe(this@BottomSheetBrowserMenuFragment) { binding.turboModeSwitch.isChecked = it } - chromeViewModel.isBlockImageEnabled.observe(this@BrowserMenuDialog) { + chromeViewModel.isBlockImageEnabled.observe(this@BottomSheetBrowserMenuFragment) { binding.blockImagesSwitch.isChecked = it } - chromeViewModel.isNightMode.observe(this@BrowserMenuDialog) { nightModeSettings -> + chromeViewModel.isNightMode.observe(this@BottomSheetBrowserMenuFragment) { nightModeSettings -> binding.nightModeSwitch.isChecked = nightModeSettings.isEnabled } binding.menuFindInPage.setOnClickListener { postDelayClickEvent { - cancel() + dismissAllowingStateLoss() chromeViewModel.showFindInPage.call() } } binding.menuPinShortcut.setOnClickListener { postDelayClickEvent { - cancel() + dismissAllowingStateLoss() chromeViewModel.pinShortcut.call() TelemetryWrapper.clickMenuPinShortcut() } } binding.menuPinSite.setOnClickListener { postDelayClickEvent { - cancel() + dismissAllowingStateLoss() chromeViewModel.pinSite.call() } } @@ -190,7 +183,7 @@ class BrowserMenuDialog : LifecycleBottomSheetDialog { } binding.menuPreferences.setOnClickListener { postDelayClickEvent { - cancel() + dismissAllowingStateLoss() chromeViewModel.checkToDriveDefaultBrowser() chromeViewModel.openPreference.call() TelemetryWrapper.clickMenuSettings() @@ -198,14 +191,14 @@ class BrowserMenuDialog : LifecycleBottomSheetDialog { } binding.menuDelete.setOnClickListener { postDelayClickEvent { - cancel() + dismissAllowingStateLoss() onDeleteClicked() TelemetryWrapper.clickMenuClearCache() } } binding.menuExit.setOnClickListener { postDelayClickEvent { - cancel() + dismissAllowingStateLoss() chromeViewModel.exitApp.call() TelemetryWrapper.clickMenuExit() } @@ -214,6 +207,7 @@ class BrowserMenuDialog : LifecycleBottomSheetDialog { } private fun onDeleteClicked() { + val context = context ?: return val diff = FileUtils.clearCache(context) val stringId = if (diff < 0) R.string.message_clear_cache_fail else R.string.message_cleared_cached @@ -226,6 +220,7 @@ class BrowserMenuDialog : LifecycleBottomSheetDialog { } private fun showAdjustBrightness() { + val context = context ?: return ContextCompat.startActivity( context, AdjustBrightnessDialog.Intents.getStartIntentFromMenu(context), @@ -233,51 +228,51 @@ class BrowserMenuDialog : LifecycleBottomSheetDialog { ) } - private fun initBottomBar() { + private fun initBottomBar(binding: BottomSheetBrowserMenuBinding) { val bottomBar = binding.menuBottomBar bottomBar.setOnItemClickListener { type, position -> - cancel() + dismissAllowingStateLoss() when (type) { - ItemType.TAB_COUNTER -> { + BottomBarItem.ItemType.TAB_COUNTER -> { chromeViewModel.showTabTray.call() TelemetryWrapper.showTabTrayToolbar( TelemetryWrapper.Extra_Value.MENU, position ) } - ItemType.MENU -> { + BottomBarItem.ItemType.MENU -> { chromeViewModel.showBrowserMenu.call() TelemetryWrapper.showMenuToolbar( TelemetryWrapper.Extra_Value.MENU, position ) } - ItemType.HOME -> { + BottomBarItem.ItemType.HOME -> { chromeViewModel.showNewTab.call() TelemetryWrapper.clickAddTabToolbar( TelemetryWrapper.Extra_Value.MENU, position ) } - ItemType.SEARCH -> { + BottomBarItem.ItemType.SEARCH -> { chromeViewModel.showUrlInput.call() TelemetryWrapper.clickToolbarSearch( TelemetryWrapper.Extra_Value.MENU, position ) } - ItemType.CAPTURE -> chromeViewModel.onDoScreenshot( + BottomBarItem.ItemType.CAPTURE -> chromeViewModel.onDoScreenshot( ChromeViewModel.ScreenCaptureTelemetryData( TelemetryWrapper.Extra_Value.MENU, position ) ) - ItemType.PIN_SHORTCUT -> { + BottomBarItem.ItemType.PIN_SHORTCUT -> { chromeViewModel.pinShortcut.call() TelemetryWrapper.clickAddToHome(TelemetryWrapper.Extra_Value.MENU, position) } - ItemType.BOOKMARK -> { - val nullableItem = bottomBarItemAdapter.getItem(ItemType.BOOKMARK) + BottomBarItem.ItemType.BOOKMARK -> { + val nullableItem = bottomBarItemAdapter.getItem(BottomBarItem.ItemType.BOOKMARK) val isActivated = nullableItem?.view?.isActivated == true TelemetryWrapper.clickToolbarBookmark( !isActivated, @@ -286,35 +281,35 @@ class BrowserMenuDialog : LifecycleBottomSheetDialog { ) chromeViewModel.toggleBookmark() } - ItemType.REFRESH -> { + BottomBarItem.ItemType.REFRESH -> { chromeViewModel.refreshOrStop.call() TelemetryWrapper.clickToolbarReload( TelemetryWrapper.Extra_Value.MENU, position ) } - ItemType.SHARE -> { + BottomBarItem.ItemType.SHARE -> { chromeViewModel.share.call() TelemetryWrapper.clickToolbarShare( TelemetryWrapper.Extra_Value.MENU, position ) } - ItemType.NEXT -> { + BottomBarItem.ItemType.NEXT -> { chromeViewModel.goNext.call() TelemetryWrapper.clickToolbarForward( TelemetryWrapper.Extra_Value.MENU, position ) } - ItemType.BACK -> { + BottomBarItem.ItemType.BACK -> { chromeViewModel.goBack.call() TelemetryWrapper.clickToolbarBack(position) } - ItemType.PRIVATE_HOME, - ItemType.DELETE, - ItemType.TRACKER, - ItemType.SHOPPING_SEARCH -> + BottomBarItem.ItemType.PRIVATE_HOME, + BottomBarItem.ItemType.DELETE, + BottomBarItem.ItemType.TRACKER, + BottomBarItem.ItemType.SHOPPING_SEARCH -> throw IllegalArgumentException("Unhandled bottom bar item, type: $type") } // move Telemetry to ScreenCaptureTask doInBackground() cause we need to init category first. } @@ -337,11 +332,12 @@ class BrowserMenuDialog : LifecycleBottomSheetDialog { } private fun hidePinShortcutButtonIfNotSupported() { + val context = context ?: return val requestPinShortcutSupported = ShortcutManagerCompat.isRequestPinShortcutSupported(context) if (!requestPinShortcutSupported) { val pinShortcutItem = - bottomBarItemAdapter.getItem(ItemType.PIN_SHORTCUT) + bottomBarItemAdapter.getItem(BottomBarItem.ItemType.PIN_SHORTCUT) pinShortcutItem?.view?.apply { visibility = View.GONE } @@ -352,6 +348,40 @@ class BrowserMenuDialog : LifecycleBottomSheetDialog { * Post delay click event to wait the clicking feedback shows */ private fun postDelayClickEvent(action: () -> Unit) { - uiHandler.postDelayed({ action() }, 150) + uiHandler.postDelayed( + { + action() + }, + 150 + ) + } + + companion object { + const val TAG = "BottomSheetBrowserMenuFragment" + + fun createInstance(): BottomSheetBrowserMenuFragment { + return BottomSheetBrowserMenuFragment() + } + + fun show(supportFragmentManager: FragmentManager) { + createInstance().show(supportFragmentManager, TAG) + } + + fun dismiss(supportFragmentManager: FragmentManager) { + val fragment = getDisplayedFragment(supportFragmentManager) + fragment?.dismissAllowingStateLoss() + } + + fun getScreenshotMenuButton(supportFragmentManager: FragmentManager): View? { + val fragment = getDisplayedFragment(supportFragmentManager) + return fragment?.binding?.menuScreenshots + } + + private fun getDisplayedFragment( + supportFragmentManager: FragmentManager + ): BottomSheetBrowserMenuFragment? { + val taggedFragment = supportFragmentManager.findFragmentByTag(TAG) ?: return null + return taggedFragment as? BottomSheetBrowserMenuFragment ?: return null + } } } diff --git a/app/src/main/java/org/mozilla/rocket/menu/HomeMenuDialog.kt b/app/src/main/java/org/mozilla/rocket/menu/BottomSheetHomeMenuFragment.kt similarity index 67% rename from app/src/main/java/org/mozilla/rocket/menu/HomeMenuDialog.kt rename to app/src/main/java/org/mozilla/rocket/menu/BottomSheetHomeMenuFragment.kt index 2d328477f1..3b797ef0c8 100644 --- a/app/src/main/java/org/mozilla/rocket/menu/HomeMenuDialog.kt +++ b/app/src/main/java/org/mozilla/rocket/menu/BottomSheetHomeMenuFragment.kt @@ -1,17 +1,16 @@ package org.mozilla.rocket.menu -import android.content.Context -import android.graphics.Outline import android.os.Bundle import android.os.Handler import android.os.Looper +import android.view.LayoutInflater import android.view.View -import android.view.ViewOutlineProvider -import android.widget.ScrollView +import android.view.ViewGroup import android.widget.Toast -import androidx.annotation.StyleRes import androidx.core.content.ContextCompat import androidx.core.view.isVisible +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.FragmentManager import androidx.lifecycle.Observer import dagger.Lazy import org.mozilla.fileutils.FileUtils @@ -19,17 +18,16 @@ import org.mozilla.focus.R import org.mozilla.focus.databinding.BottomSheetHomeMenuBinding import org.mozilla.focus.telemetry.TelemetryWrapper import org.mozilla.focus.utils.FormatUtils +import org.mozilla.focus.utils.ScrollableBottomSheetHelper import org.mozilla.rocket.chrome.ChromeViewModel import org.mozilla.rocket.chrome.MenuViewModel import org.mozilla.rocket.content.appComponent import org.mozilla.rocket.content.getActivityViewModel -import org.mozilla.rocket.extension.toFragmentActivity import org.mozilla.rocket.nightmode.AdjustBrightnessDialog import org.mozilla.rocket.shopping.search.ui.ShoppingSearchActivity -import org.mozilla.rocket.widget.LifecycleBottomSheetDialog import javax.inject.Inject -class HomeMenuDialog : LifecycleBottomSheetDialog { +class BottomSheetHomeMenuFragment : DialogFragment() { @Inject lateinit var chromeViewModelCreator: Lazy @@ -40,88 +38,80 @@ class HomeMenuDialog : LifecycleBottomSheetDialog { private lateinit var chromeViewModel: ChromeViewModel private lateinit var menuViewModel: MenuViewModel - private lateinit var binding: BottomSheetHomeMenuBinding + private var binding: BottomSheetHomeMenuBinding? = null private val uiHandler = Handler(Looper.getMainLooper()) - constructor(context: Context) : super(context) - constructor(context: Context, @StyleRes theme: Int) : super(context, theme) - override fun onCreate(savedInstanceState: Bundle?) { appComponent().inject(this) super.onCreate(savedInstanceState) - context.toFragmentActivity().lifecycle.addObserver(this) + // overwrite android.R.style.Theme_Panel, so it looks like normal Fragment + setStyle(STYLE_NO_TITLE, R.style.BottomSheetTheme) + chromeViewModel = getActivityViewModel(chromeViewModelCreator) menuViewModel = getActivityViewModel(menuViewModelCreator) - - initLayout() - observeChromeAction() - setCancelable(false) - setCanceledOnTouchOutside(true) } - override fun dismiss() { - if (::binding.isInitialized) { - resetStates() - } - super.dismiss() - } + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View = BottomSheetHomeMenuBinding.inflate(inflater, container, false).also { + binding = it + initLayout(it) + observeChromeAction() + }.root - override fun onDetachedFromWindow() { + override fun onDestroyView() { + super.onDestroyView() uiHandler.removeCallbacksAndMessages(null) - super.onDetachedFromWindow() + binding = null } - private fun resetStates() { - binding.scrollView.fullScroll(ScrollView.FOCUS_UP) - hideNewItemHint() - } + private fun initLayout(binding: BottomSheetHomeMenuBinding) { + val helperBinding = ScrollableBottomSheetHelper.Binding( + binding.root, + binding.container, + binding.bottomSheet + ) - private fun initLayout() { - binding = BottomSheetHomeMenuBinding.inflate(layoutInflater, null, false) - binding.scrollView.apply { - outlineProvider = object : ViewOutlineProvider() { - override fun getOutline(view: View, outline: Outline) { - val dimen = resources.getDimension(R.dimen.menu_corner_radius) - outline.setRoundRect(0, 0, view.width, view.height, dimen) - } - } - clipToOutline = true + ScrollableBottomSheetHelper.makeViewScrollable(this.requireContext(), helperBinding) { + this.dismissAllowingStateLoss() } - initMenuTabs() - initMenuItems() - setContentView(binding.root) + + initMenuTabs(binding) + initMenuItems(binding) } - private fun initMenuTabs() { + private fun initMenuTabs(binding: BottomSheetHomeMenuBinding) { binding.contentLayout.apply { - chromeViewModel.hasUnreadScreenshot.observe(this@HomeMenuDialog) { + chromeViewModel.hasUnreadScreenshot.observe(this@BottomSheetHomeMenuFragment) { binding.imgScreenshots.isActivated = it } binding.menuScreenshots.setOnClickListener { postDelayClickEvent { - cancel() + dismissAllowingStateLoss() chromeViewModel.showScreenshots() } } binding.menuBookmark.setOnClickListener { postDelayClickEvent { - cancel() + dismissAllowingStateLoss() chromeViewModel.showBookmarks.call() TelemetryWrapper.clickMenuBookmark() } } binding.menuHistory.setOnClickListener { postDelayClickEvent { - cancel() + dismissAllowingStateLoss() chromeViewModel.showHistory.call() TelemetryWrapper.clickMenuHistory() } } binding.menuDownload.setOnClickListener { postDelayClickEvent { - cancel() + dismissAllowingStateLoss() chromeViewModel.showDownloadPanel.call() TelemetryWrapper.clickMenuDownload() } @@ -129,19 +119,21 @@ class HomeMenuDialog : LifecycleBottomSheetDialog { } } - private fun initMenuItems() { + private fun initMenuItems(binding: BottomSheetHomeMenuBinding) { binding.contentLayout.apply { - chromeViewModel.isNightMode.observe(this@HomeMenuDialog) { nightModeSettings -> + chromeViewModel.isNightMode.observe(this@BottomSheetHomeMenuFragment) { nightModeSettings -> binding.nightModeSwitch.isChecked = nightModeSettings.isEnabled } - menuViewModel.isHomeScreenShoppingSearchEnabled.observe(this@HomeMenuDialog) { + menuViewModel.isHomeScreenShoppingSearchEnabled.observe(this@BottomSheetHomeMenuFragment) { binding.btnPrivateBrowsing.isVisible = !it binding.menuSmartShoppingSearch.isVisible = it } - chromeViewModel.isPrivateBrowsingActive.observe(this@HomeMenuDialog) { - binding.imgPrivateMode.isActivated = it + chromeViewModel.isPrivateBrowsingActive.observe(this@BottomSheetHomeMenuFragment) { + // TODO: how to re-enable this? + // we removed this image, and use `drawableStart` instead + // binding.imgPrivateMode.isActivated = it } - menuViewModel.shouldShowNewMenuItemHint.observe(this@HomeMenuDialog) { + menuViewModel.shouldShowNewMenuItemHint.observe(this@BottomSheetHomeMenuFragment) { if (it) { showNewItemHint() menuViewModel.onNewMenuItemDisplayed() @@ -150,14 +142,14 @@ class HomeMenuDialog : LifecycleBottomSheetDialog { binding.btnPrivateBrowsing.setOnClickListener { postDelayClickEvent { - cancel() + dismissAllowingStateLoss() chromeViewModel.togglePrivateMode.call() TelemetryWrapper.togglePrivateMode(true) } } binding.menuSmartShoppingSearch.setOnClickListener { postDelayClickEvent { - cancel() + dismissAllowingStateLoss() showShoppingSearch() } } @@ -173,21 +165,21 @@ class HomeMenuDialog : LifecycleBottomSheetDialog { } binding.menuAddTopSites.setOnClickListener { postDelayClickEvent { - cancel() + dismissAllowingStateLoss() chromeViewModel.onAddNewTopSiteMenuClicked() TelemetryWrapper.clickMenuAddTopsite() } } binding.menuThemes.setOnClickListener { postDelayClickEvent { - cancel() + dismissAllowingStateLoss() chromeViewModel.onThemeSettingMenuClicked() TelemetryWrapper.clickMenuTheme() } } binding.menuPreferences.setOnClickListener { postDelayClickEvent { - cancel() + dismissAllowingStateLoss() chromeViewModel.checkToDriveDefaultBrowser() chromeViewModel.openPreference.call() TelemetryWrapper.clickMenuSettings() @@ -195,14 +187,14 @@ class HomeMenuDialog : LifecycleBottomSheetDialog { } binding.menuDelete.setOnClickListener { postDelayClickEvent { - cancel() + dismissAllowingStateLoss() onDeleteClicked() TelemetryWrapper.clickMenuClearCache() } } binding.menuExit.setOnClickListener { postDelayClickEvent { - cancel() + dismissAllowingStateLoss() chromeViewModel.exitApp.call() TelemetryWrapper.clickMenuExit() } @@ -211,16 +203,13 @@ class HomeMenuDialog : LifecycleBottomSheetDialog { } private fun showNewItemHint() { + val binding = binding ?: return binding.addTopSitesRedDot.visibility = View.VISIBLE binding.themesRedDot.visibility = View.VISIBLE } - private fun hideNewItemHint() { - binding.addTopSitesRedDot.visibility = View.INVISIBLE - binding.themesRedDot.visibility = View.INVISIBLE - } - private fun onDeleteClicked() { + val context = context ?: return val diff = FileUtils.clearCache(context) val stringId = if (diff < 0) R.string.message_clear_cache_fail else R.string.message_cleared_cached @@ -233,6 +222,7 @@ class HomeMenuDialog : LifecycleBottomSheetDialog { } private fun showAdjustBrightness() { + val context = context ?: return ContextCompat.startActivity( context, AdjustBrightnessDialog.Intents.getStartIntentFromMenu(context), @@ -241,6 +231,7 @@ class HomeMenuDialog : LifecycleBottomSheetDialog { } private fun showShoppingSearch() { + val context = context ?: return context.startActivity(ShoppingSearchActivity.getStartIntent(context)) } @@ -255,4 +246,22 @@ class HomeMenuDialog : LifecycleBottomSheetDialog { 150 ) } + + companion object { + const val TAG = "BottomSheetHomeMenuFragment" + + fun createInstance(): BottomSheetHomeMenuFragment { + return BottomSheetHomeMenuFragment() + } + + fun show(supportFragmentManager: FragmentManager) { + createInstance().show(supportFragmentManager, TAG) + } + + fun dismiss(supportFragmentManager: FragmentManager) { + val taggedFragment = supportFragmentManager.findFragmentByTag(TAG) ?: return + val dialogFragment = taggedFragment as? BottomSheetHomeMenuFragment ?: return + dialogFragment.dismissAllowingStateLoss() + } + } } diff --git a/app/src/main/res/drawable/background_bottom_sheet.xml b/app/src/main/res/drawable/background_bottom_sheet.xml new file mode 100644 index 0000000000..9a49305530 --- /dev/null +++ b/app/src/main/res/drawable/background_bottom_sheet.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/app/src/main/res/layout/bottom_sheet_browser_menu.xml b/app/src/main/res/layout/bottom_sheet_browser_menu.xml index e6281e6163..b3a1ee9670 100644 --- a/app/src/main/res/layout/bottom_sheet_browser_menu.xml +++ b/app/src/main/res/layout/bottom_sheet_browser_menu.xml @@ -7,284 +7,292 @@ + android:orientation="vertical"> - - - - + + - - + android:paddingBottom="8dp"> - + + + + + + + + + + + android:paddingTop="10dp" + android:text="@string/label_menu_history" + app:drawableTopCompat="@drawable/menu_history" + app:layout_constraintEnd_toStartOf="@id/menu_download" + app:layout_constraintStart_toEndOf="@id/menu_bookmark" + app:layout_constraintTop_toTopOf="parent" /> - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + style="@style/MenuButtonText" + android:layout_width="74dp" + android:layout_height="wrap_content" + android:layout_marginEnd="16dp" + android:background="?android:attr/selectableItemBackgroundBorderless" + android:drawablePadding="10dp" + android:gravity="top|center_horizontal" + android:paddingTop="10dp" + android:text="@string/label_menu_download" + app:drawableTopCompat="@drawable/menu_download" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@id/menu_history" + app:layout_constraintTop_toTopOf="parent" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + app:dividerColor="@color/paletteLightGreyA100" + tools:background="#F0F" /> - diff --git a/app/src/main/res/layout/bottom_sheet_home_menu.xml b/app/src/main/res/layout/bottom_sheet_home_menu.xml index 4e242713d5..1404933e84 100644 --- a/app/src/main/res/layout/bottom_sheet_home_menu.xml +++ b/app/src/main/res/layout/bottom_sheet_home_menu.xml @@ -1,447 +1,316 @@ + + - - + + + android:layout_height="match_parent"> - - - + + + android:paddingBottom="8dp"> + app:layout_constraintEnd_toStartOf="@id/menu_bookmark" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent"> + android:src="@drawable/menu_my_shots_states" /> + android:gravity="top|center_horizontal" + android:text="@string/label_menu_my_shots" /> - - - - - - - - - + + - - - - - - - - + + - - - - - - - - - - - - - - - - - - - - - - - + android:layout_marginEnd="16dp" + android:background="?android:attr/selectableItemBackgroundBorderless" + android:drawablePadding="10dp" + android:gravity="top|center_horizontal" + android:paddingTop="10dp" + android:text="@string/label_menu_download" + app:drawableTopCompat="@drawable/menu_download" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@id/menu_history" + app:layout_constraintTop_toTopOf="parent" /> + + - + android:layout_height="1dp" + android:layout_marginTop="4dp" + android:layout_marginBottom="7dp" + android:background="@color/paletteLightGreyA100" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/first_barrier" /> + + + - - + android:layout_marginEnd="22dp" + android:src="@drawable/ic_arrow_right" + app:layout_constraintBottom_toBottomOf="@id/menu_exit" + app:layout_constraintEnd_toEndOf="@id/menu_exit" + app:layout_constraintTop_toTopOf="@id/menu_exit" /> - - + - - - - - - + style="@style/BottomSheetRowMenuItemTextStyle" + android:drawableStart="@drawable/icon_night_mode" + android:text="@string/label_menu_night_mode" + app:layout_constraintEnd_toStartOf="@id/menu_night_mode_gutter" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/btn_private_browsing" /> + android:background="@color/paletteLightGreyA100" + app:layout_constraintBottom_toBottomOf="@id/menu_night_mode" + app:layout_constraintStart_toEndOf="@id/menu_night_mode" + app:layout_constraintTop_toTopOf="@id/menu_night_mode" /> - + android:layout_marginStart="12dp" + android:layout_marginEnd="24dp" + app:layout_constraintBottom_toBottomOf="@id/menu_night_mode" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@id/menu_night_mode_gutter" + app:layout_constraintTop_toTopOf="@id/menu_night_mode" /> + + + + - - - + - - - - + android:visibility="invisible" + app:layout_constraintBottom_toBottomOf="@id/menu_themes" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="@id/menu_themes" + tools:visibility="visible" /> + + + - - - - + android:layout_marginEnd="22dp" + android:src="@drawable/ic_arrow_right" + app:layout_constraintBottom_toBottomOf="@id/menu_add_top_sites" + app:layout_constraintEnd_toEndOf="@id/menu_add_top_sites" + app:layout_constraintTop_toTopOf="@id/menu_add_top_sites" /> - - - - + android:visibility="invisible" + app:layout_constraintBottom_toBottomOf="@id/menu_add_top_sites" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="@id/menu_add_top_sites" + tools:visibility="visible" /> + + + - - - - - - - - - - + android:layout_marginEnd="22dp" + android:src="@drawable/ic_arrow_right" + app:layout_constraintBottom_toBottomOf="@id/menu_preferences" + app:layout_constraintEnd_toEndOf="@id/menu_preferences" + app:layout_constraintTop_toTopOf="@id/menu_preferences" /> + + + - - - - - - - - - - - - + android:layout_marginEnd="22dp" + android:src="@drawable/ic_arrow_right" + app:layout_constraintBottom_toBottomOf="@id/menu_delete" + app:layout_constraintEnd_toEndOf="@id/menu_delete" + app:layout_constraintTop_toTopOf="@id/menu_delete" /> + + + - - - - - - - - - - \ No newline at end of file + android:layout_width="24dp" + android:layout_height="24dp" + android:layout_marginEnd="22dp" + android:src="@drawable/ic_arrow_right" + app:layout_constraintBottom_toBottomOf="@id/menu_exit" + app:layout_constraintEnd_toEndOf="@id/menu_exit" + app:layout_constraintTop_toTopOf="@id/menu_exit" /> + + + + + + + + + + + diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 574a3d5de3..b8c883e5b5 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -24,6 +24,8 @@ 12dp 48dp 363dp + 320dp + 280dp 4dp 16dp diff --git a/buildSrc/src/main/java/Dependencies.kt b/buildSrc/src/main/java/Dependencies.kt index c90def4321..d1eb85166a 100644 --- a/buildSrc/src/main/java/Dependencies.kt +++ b/buildSrc/src/main/java/Dependencies.kt @@ -8,7 +8,7 @@ object Versions { const val gms_oss_licenses_plugin = "0.10.4" const val support = "1.0.0" const val appcompat = "1.3.1" - const val material = "1.4.0" + const val material = "1.5.0" const val cardview = "1.0.0" const val recyclerview = "1.2.1" const val constraint = "2.1.1"