app: InAppUpdatePresenter use compose for banners
authorDa Risk <da_risk@geekorum.com>
Thu, 14 Jul 2022 18:19:40 -0400
changeset 898 047d64521f8e
parent 897 3f9b625821ec
child 899 0b39012f1650
app: InAppUpdatePresenter use compose for banners
app/build.gradle.kts
app/src/main/java/com/geekorum/ttrss/articles_list/ArticleListActivity.kt
app/src/main/java/com/geekorum/ttrss/articles_list/InAppUpdatePresenter.kt
app/src/main/java/com/geekorum/ttrss/in_app_update/InAppUpdateViewModel.kt
app/src/main/res/layout-w800dp/activity_article_list.xml
app/src/main/res/layout/activity_article_list.xml
--- a/app/build.gradle.kts	Fri Jul 08 21:01:52 2022 -0400
+++ b/app/build.gradle.kts	Thu Jul 14 18:19:40 2022 -0400
@@ -162,6 +162,7 @@
     api("com.google.accompanist:accompanist-insets:$accompanistVersion")
     api("com.google.accompanist:accompanist-insets-ui:$accompanistVersion")
     api("com.google.accompanist:accompanist-swiperefresh:$accompanistVersion")
+    api("com.google.accompanist:accompanist-drawablepainter:$accompanistVersion")
     api("androidx.compose.ui:ui-tooling:$composeVersion")
 
 
--- a/app/src/main/java/com/geekorum/ttrss/articles_list/ArticleListActivity.kt	Fri Jul 08 21:01:52 2022 -0400
+++ b/app/src/main/java/com/geekorum/ttrss/articles_list/ArticleListActivity.kt	Thu Jul 14 18:19:40 2022 -0400
@@ -22,10 +22,7 @@
 
 import android.os.Bundle
 import androidx.activity.viewModels
-import androidx.core.graphics.Insets
-import androidx.core.view.ViewCompat
 import androidx.core.view.WindowCompat
-import androidx.core.view.WindowInsetsCompat
 import androidx.core.view.doOnLayout
 import androidx.databinding.DataBindingUtil
 import androidx.drawerlayout.widget.DrawerLayout
@@ -136,21 +133,6 @@
             AppBarLayout.OnOffsetChangedListener { _, _ ->
                 binding.appBar.invalidate()
             })
-
-        // banner container
-        ViewCompat.setOnApplyWindowInsetsListener(binding.bannerContainer) { view, windowInsets ->
-            // consume top padding since we are not on top of screen
-            val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
-            val noTopInsets = WindowInsetsCompat.Builder().setInsets(WindowInsetsCompat.Type.systemBars(),
-                Insets.of(insets.left,
-                    0,
-                    insets.right,
-                    insets.bottom)
-            ).build()
-            WindowInsetsCompat.toWindowInsetsCompat(
-                view.onApplyWindowInsets(noTopInsets.toWindowInsets())
-            )
-        }
     }
 
     private fun setupToolbar() {
--- a/app/src/main/java/com/geekorum/ttrss/articles_list/InAppUpdatePresenter.kt	Fri Jul 08 21:01:52 2022 -0400
+++ b/app/src/main/java/com/geekorum/ttrss/articles_list/InAppUpdatePresenter.kt	Thu Jul 14 18:19:40 2022 -0400
@@ -31,21 +31,37 @@
 import androidx.activity.result.IntentSenderRequest
 import androidx.activity.result.contract.ActivityResultContract
 import androidx.activity.result.contract.ActivityResultContracts
-import androidx.core.graphics.drawable.IconCompat
+import androidx.annotation.DrawableRes
+import androidx.appcompat.content.res.AppCompatResources
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.slideInVertically
+import androidx.compose.animation.slideOutVertically
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.Card
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Text
+import androidx.compose.material.TextButton
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.painter.Painter
+import androidx.compose.ui.layout.layout
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.colorResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
 import androidx.core.view.doOnNextLayout
 import androidx.core.view.updatePadding
 import androidx.lifecycle.LifecycleOwner
-import androidx.lifecycle.lifecycleScope
-import com.geekorum.geekdroid.views.banners.BannerContainer
-import com.geekorum.geekdroid.views.banners.BannerSpec
-import com.geekorum.geekdroid.views.banners.buildBanner
+import com.geekorum.geekdroid.views.doOnApplyWindowInsets
 import com.geekorum.ttrss.R
 import com.geekorum.ttrss.in_app_update.InAppUpdateViewModel
 import com.geekorum.ttrss.in_app_update.IntentSenderForResultStarter
-import com.google.android.material.bottomsheet.BottomSheetBehavior
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onEach
-import timber.log.Timber
+import com.geekorum.ttrss.ui.AppTheme
+import com.google.accompanist.drawablepainter.rememberDrawablePainter
 
 private const val CODE_START_IN_APP_UPDATE = 1
 
@@ -54,7 +70,7 @@
  * Really tied to ArticleListActivity
  */
 class InAppUpdatePresenter(
-    private val bannerContainer: BannerContainer,
+    private val composeView: ComposeView,
     private val lifecyleOwner: LifecycleOwner,
     private val inAppUpdateViewModel: InAppUpdateViewModel,
     activityResultRegistry: ActivityResultRegistry,
@@ -80,9 +96,160 @@
     }
 
     init {
-        setUpViewModels()
+        composeView.fitsSystemWindows = true
+        composeView.doOnApplyWindowInsets { _, windowInsetsCompat, _ ->
+            windowInsetsCompat
+        }
+        composeView.setContent {
+            Content()
+        }
+    }
+
+    @Composable
+    fun Content() {
+        AppTheme {
+            val isUpdateAvailable by inAppUpdateViewModel.isUpdateAvailable.collectAsState(false )
+            val isUpdateReadyToInstall by inAppUpdateViewModel.isUpdateReadyToInstall.collectAsState(false )
+            var showBanner by remember { mutableStateOf(false) }
+            LaunchedEffect(isUpdateAvailable, isUpdateReadyToInstall) {
+                showBanner = isUpdateAvailable || isUpdateReadyToInstall
+            }
+
+            var sheetHeigth by remember { mutableStateOf(0) }
+            AnimatedVisibility(showBanner,
+                enter = slideInVertically(initialOffsetY = { it }),
+                exit = slideOutVertically(targetOffsetY = { it}),
+                modifier = Modifier.layout { measurable, constraints ->
+                    val placeable = measurable.measure(constraints)
+                    sheetHeigth = placeable.height
+                    layout(placeable.width, placeable.height) {
+                        placeable.placeRelative(0, 0)
+                    }
+                }
+            ) {
+                SheetContent(
+                    isUpdateAvailable = isUpdateAvailable,
+                    isUpdateReadyToInstalll = isUpdateReadyToInstall,
+                    dismissSheet = {
+                        showBanner = false
+                    }
+                )
+            }
+
+            // needed to add additional padding to articles list
+            LaunchedEffect(showBanner, sheetHeigth) {
+                val paddingBottom = if (showBanner) sheetHeigth else 0
+                (composeView.parent as? View)?.doOnNextLayout { parent ->
+                    val fragmentContainerView = parent.findViewById<View>(R.id.middle_pane_layout)
+                    fragmentContainerView?.updatePadding(bottom = paddingBottom)
+                }
+            }
+        }
     }
 
+    @Composable
+    private fun SheetContent(
+        isUpdateAvailable: Boolean,
+        isUpdateReadyToInstalll: Boolean,
+        dismissSheet: () -> Unit,
+    ) {
+        Card(shape = RoundedCornerShape(0.dp),
+            backgroundColor =  if (MaterialTheme.colors.isLight)
+                colorResource(R.color.material_blue_grey_100)
+            else MaterialTheme.colors.surface,
+            elevation = 8.dp,
+        ) {
+            Box(modifier = Modifier
+                .windowInsetsPadding(WindowInsets.navigationBars)
+                .fillMaxWidth(),
+                propagateMinConstraints = true
+            ) {
+                when {
+                    isUpdateReadyToInstalll -> {
+                        UpdateReadyBanner(onRestartClick = {
+                            inAppUpdateViewModel.completeUpdate()
+                            dismissSheet()
+                        })
+                    }
+                    isUpdateAvailable -> {
+                        UpdateAvailableBanner(onInstallClick = {
+                            inAppUpdateViewModel.startUpdateFlow(intentSenderForResultStarter,
+                                CODE_START_IN_APP_UPDATE)
+                            dismissSheet()
+                        },
+                            onDismissClick = {
+                                dismissSheet()
+                            })
+                    }
+                    else -> Unit
+                }
+            }
+        }
+    }
+
+    @Composable
+    private fun UpdateAvailableBanner(onInstallClick: () -> Unit, onDismissClick: () -> Unit) {
+        Column {
+            Row(Modifier
+                .padding(top = 24.dp)
+                .padding(horizontal = 16.dp)) {
+                val painter = rememberMipmapPainter(R.mipmap.ic_launcher)
+
+                Image(painter = painter, contentDescription = null,
+                    modifier = Modifier.size(56.dp)
+                )
+                Text(stringResource(R.string.banner_update_msg),
+                    style = MaterialTheme.typography.subtitle1,
+                    modifier = Modifier.padding(start = 16.dp))
+            }
+
+            AppTheme(
+                colors = MaterialTheme.colors.copy(primary = MaterialTheme.colors.secondary)
+            ) {
+                Row(Modifier
+                    .align(Alignment.End)
+                    .padding(top = 12.dp, end = 8.dp, bottom = 8.dp),
+                    horizontalArrangement = Arrangement.spacedBy(8.dp)
+                ) {
+                    TextButton(onClick = onDismissClick) {
+                        Text(stringResource(R.string.banner_dismiss_btn))
+                    }
+
+                    TextButton(onClick = onInstallClick) {
+                        Text(stringResource(R.string.banner_update_btn))
+                    }
+                }
+            }
+        }
+    }
+
+    @Composable
+    private fun UpdateReadyBanner(onRestartClick: () -> Unit) {
+        Column {
+            Row(Modifier
+                .padding(top = 24.dp)
+                .padding(horizontal = 16.dp)) {
+                Image(painter = rememberMipmapPainter(R.mipmap.ic_launcher), contentDescription = null,
+                    Modifier.size(56.dp))
+                Text(stringResource(R.string.banner_update_install_msg),
+                    style = MaterialTheme.typography.subtitle1,
+                    modifier = Modifier.padding(start = 16.dp))
+            }
+            AppTheme(
+                colors = MaterialTheme.colors.copy(primary = MaterialTheme.colors.secondary)
+            ) {
+                TextButton(onClick = onRestartClick,
+                    modifier = Modifier
+                        .align(Alignment.End)
+                        .padding(top = 12.dp, end = 8.dp, bottom = 8.dp)
+                ) {
+                    Text(stringResource(R.string.banner_install_btn))
+                }
+            }
+        }
+    }
+
+
     private fun <I, O> registerInAppUpdateLauncher(
         contract: ActivityResultContract<I, O>,
         registry: ActivityResultRegistry,
@@ -91,79 +258,9 @@
             "in_app_update_presenter", lifecyleOwner, contract, callback)
     }
 
-    private fun setUpViewModels() {
-        inAppUpdateViewModel.isUpdateAvailable.onEach {
-            if (it) {
-                Timber.d("Update available")
-                val context = bannerContainer.context
-                val banner = buildBanner(context) {
-                    messageId = R.string.banner_update_msg
-                    icon = IconCompat.createWithResource(context,
-                        R.mipmap.ic_launcher)
-                    setPositiveButton(R.string.banner_update_btn) {
-                        inAppUpdateViewModel.startUpdateFlow(intentSenderForResultStarter,
-                            CODE_START_IN_APP_UPDATE)
-                        hideBanner()
-                    }
-                    setNegativeButton(R.string.banner_dismiss_btn) {
-                        hideBanner()
-                    }
-                }
-
-                showBanner(banner)
-            }
-        }.launchIn(lifecyleOwner.lifecycleScope)
-
-        inAppUpdateViewModel.isUpdateReadyToInstall.onEach {
-            if (it) {
-                Timber.d("Update ready to install")
-                val context = bannerContainer.context
-                val banner = buildBanner(context) {
-                    message = "Update ready to install"
-                    icon = IconCompat.createWithResource(context,
-                        R.mipmap.ic_launcher)
-                    setPositiveButton("Restart") {
-                        hideBanner()
-                        inAppUpdateViewModel.completeUpdate()
-                    }
-                }
-
-                showBanner(banner)
-            }
-        }.launchIn(lifecyleOwner.lifecycleScope)
+    @Composable
+    private fun rememberMipmapPainter(@DrawableRes mipmapId: Int): Painter {
+        return rememberDrawablePainter(drawable = AppCompatResources.getDrawable(LocalContext.current, mipmapId))
     }
 
-    private fun showBanner(bannerSpec: BannerSpec) {
-        bannerContainer.show(bannerSpec)
-        val behavior = BottomSheetBehavior.from(bannerContainer)
-        behavior.state = BottomSheetBehavior.STATE_EXPANDED
-
-        // wait for expanded state to set non hideable
-        behavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
-            override fun onSlide(bottomSheet: View, slideOffset: Float) {
-                // nothing to do
-            }
-
-            override fun onStateChanged(bottomSheet: View, newState: Int) {
-                if (newState == BottomSheetBehavior.STATE_EXPANDED) {
-                    behavior.isHideable = false
-                    behavior.removeBottomSheetCallback(this)
-                }
-            }
-        })
-
-        (bannerContainer.parent as? View)?.doOnNextLayout { parent ->
-            val fragmentContainerView = parent.findViewById<View>(R.id.middle_pane_layout)
-            fragmentContainerView?.updatePadding(bottom = bannerContainer.height)
-        }
-    }
-
-    private fun hideBanner() {
-        val behavior = BottomSheetBehavior.from(bannerContainer)
-        behavior.isHideable = true
-        behavior.state = BottomSheetBehavior.STATE_HIDDEN
-        val parent = bannerContainer.parent as View?
-        val fragmentContainerView = parent?.findViewById<View>(R.id.middle_pane_layout)
-        fragmentContainerView?.updatePadding(bottom = 0)
-    }
 }
--- a/app/src/main/java/com/geekorum/ttrss/in_app_update/InAppUpdateViewModel.kt	Fri Jul 08 21:01:52 2022 -0400
+++ b/app/src/main/java/com/geekorum/ttrss/in_app_update/InAppUpdateViewModel.kt	Thu Jul 14 18:19:40 2022 -0400
@@ -23,15 +23,14 @@
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.viewModelScope
 import dagger.hilt.android.lifecycle.HiltViewModel
-import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.channels.Channel
 import kotlinx.coroutines.flow.*
 import kotlinx.coroutines.launch
+import timber.log.Timber
 import javax.inject.Inject
 
 
-@OptIn(ExperimentalCoroutinesApi::class)
 @HiltViewModel
 class InAppUpdateViewModel @Inject constructor(
     private val updateManager: InAppUpdateManager
@@ -39,6 +38,7 @@
 
     val isUpdateAvailable: Flow<Boolean> = flow {
         val result = updateManager.getUpdateAvailability()
+        Timber.d("Update availability $result")
         emit(result == UpdateAvailability.UPDATE_AVAILABLE)
     }
 
@@ -52,6 +52,7 @@
     }
 
     val isUpdateReadyToInstall = updateState.map {
+        Timber.d("Update status ${it.status}")
         it.status == UpdateState.Status.DOWNLOADED
     }.distinctUntilChanged()
 
--- a/app/src/main/res/layout-w800dp/activity_article_list.xml	Fri Jul 08 21:01:52 2022 -0400
+++ b/app/src/main/res/layout-w800dp/activity_article_list.xml	Thu Jul 14 18:19:40 2022 -0400
@@ -88,16 +88,13 @@
                         app:layout_behavior="@string/appbar_scrolling_view_behavior"
                     />
 
-                <com.geekorum.geekdroid.views.banners.BannerContainer
-                        android:id="@+id/banner_container"
-                        android:layout_width="match_parent"
-                        android:layout_height="wrap_content"
-                        app:layout_behavior="@string/bottom_sheet_behavior"
-                        app:behavior_skipCollapsed="true"
-                        app:layout_insetEdge="bottom"
-                        android:elevation="8dp"
-                        tools:layout_height="0dp"
-                        android:theme="@style/ThemeOverlay.AppTheme.BottomSheet" />
+                <androidx.compose.ui.platform.ComposeView
+                    android:id="@+id/banner_container"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:layout_gravity="bottom"
+                    app:layout_insetEdge="bottom"
+                    />
 
                 <com.google.android.material.floatingactionbutton.FloatingActionButton
                     android:id="@+id/fab"
--- a/app/src/main/res/layout/activity_article_list.xml	Fri Jul 08 21:01:52 2022 -0400
+++ b/app/src/main/res/layout/activity_article_list.xml	Thu Jul 14 18:19:40 2022 -0400
@@ -93,16 +93,13 @@
             android:onClick="@{() -> activityViewModel.refresh()}"
             />
 
-        <com.geekorum.geekdroid.views.banners.BannerContainer
-                android:id="@+id/banner_container"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                app:layout_behavior="@string/bottom_sheet_behavior"
-                app:behavior_skipCollapsed="true"
-                app:layout_insetEdge="bottom"
-                android:elevation="8dp"
-                tools:layout_height="150dp"
-                android:theme="@style/ThemeOverlay.AppTheme.BottomSheet" />
+        <androidx.compose.ui.platform.ComposeView
+            android:id="@+id/banner_container"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_gravity="bottom"
+            app:layout_insetEdge="bottom"
+            />
 
     </androidx.coordinatorlayout.widget.CoordinatorLayout>