App udpate activity #880

master
Koitharu 2 years ago
parent e25ccf6b25
commit 3691db8e8e
Signed by: Koitharu
GPG Key ID: 676DEE768C17A9D7

@ -245,6 +245,9 @@
<activity <activity
android:name="org.koitharu.kotatsu.alternatives.ui.AlternativesActivity" android:name="org.koitharu.kotatsu.alternatives.ui.AlternativesActivity"
android:label="@string/alternatives" /> android:label="@string/alternatives" />
<activity
android:name="org.koitharu.kotatsu.settings.about.AppUpdateActivity"
android:label="@string/app_update_available" />
<service <service
android:name="androidx.work.impl.foreground.SystemForegroundService" android:name="androidx.work.impl.foreground.SystemForegroundService"
@ -357,13 +360,6 @@
android:name="android.appwidget.provider" android:name="android.appwidget.provider"
android:resource="@xml/widget_recent" /> android:resource="@xml/widget_recent" />
</receiver> </receiver>
<receiver
android:name="org.koitharu.kotatsu.settings.about.UpdateDownloadReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.DOWNLOAD_COMPLETE" />
</intent-filter>
</receiver>
<receiver <receiver
android:name="org.koitharu.kotatsu.core.ErrorReporterReceiver" android:name="org.koitharu.kotatsu.core.ErrorReporterReceiver"
android:exported="false"> android:exported="false">

@ -1,6 +1,7 @@
package org.koitharu.kotatsu.main.ui package org.koitharu.kotatsu.main.ui
import android.Manifest import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager.PERMISSION_GRANTED import android.content.pm.PackageManager.PERMISSION_GRANTED
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
@ -66,7 +67,7 @@ import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionFragment
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionViewModel import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionViewModel
import org.koitharu.kotatsu.settings.SettingsActivity import org.koitharu.kotatsu.settings.SettingsActivity
import org.koitharu.kotatsu.settings.about.AppUpdateDialog import org.koitharu.kotatsu.settings.about.AppUpdateActivity
import javax.inject.Inject import javax.inject.Inject
import com.google.android.material.R as materialR import com.google.android.material.R as materialR
@ -84,7 +85,6 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), AppBarOwner, BottomNav
private val viewModel by viewModels<MainViewModel>() private val viewModel by viewModels<MainViewModel>()
private val searchSuggestionViewModel by viewModels<SearchSuggestionViewModel>() private val searchSuggestionViewModel by viewModels<SearchSuggestionViewModel>()
private val closeSearchCallback = CloseSearchCallback() private val closeSearchCallback = CloseSearchCallback()
private val appUpdateDialog = AppUpdateDialog(this)
private lateinit var navigationDelegate: MainNavigationDelegate private lateinit var navigationDelegate: MainNavigationDelegate
private lateinit var appUpdateBadge: OptionsMenuBadgeHelper private lateinit var appUpdateBadge: OptionsMenuBadgeHelper
@ -190,9 +190,8 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), AppBarOwner, BottomNav
} }
R.id.action_app_update -> { R.id.action_app_update -> {
viewModel.appUpdate.value?.also { startActivity(Intent(this, AppUpdateActivity::class.java))
appUpdateDialog.show(it) true
} != null
} }
else -> super.onOptionsItemSelected(item) else -> super.onOptionsItemSelected(item)

@ -29,7 +29,6 @@ import org.koitharu.kotatsu.databinding.ActivitySettingsBinding
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.settings.about.AboutSettingsFragment import org.koitharu.kotatsu.settings.about.AboutSettingsFragment
import org.koitharu.kotatsu.settings.about.AppUpdateDialog
import org.koitharu.kotatsu.settings.sources.SourceSettingsFragment import org.koitharu.kotatsu.settings.sources.SourceSettingsFragment
import org.koitharu.kotatsu.settings.sources.SourcesSettingsFragment import org.koitharu.kotatsu.settings.sources.SourcesSettingsFragment
import org.koitharu.kotatsu.settings.sources.manage.SourcesManageFragment import org.koitharu.kotatsu.settings.sources.manage.SourcesManageFragment
@ -43,8 +42,6 @@ class SettingsActivity :
AppBarOwner, AppBarOwner,
FragmentManager.OnBackStackChangedListener { FragmentManager.OnBackStackChangedListener {
val appUpdateDialog = AppUpdateDialog(this)
override val appBar: AppBarLayout override val appBar: AppBarLayout
get() = viewBinding.appbar get() = viewBinding.appbar

@ -20,7 +20,6 @@ import org.koitharu.kotatsu.core.ui.BasePreferenceFragment
import org.koitharu.kotatsu.core.util.ShareHelper import org.koitharu.kotatsu.core.util.ShareHelper
import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.settings.SettingsActivity
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
@ -77,7 +76,7 @@ class AboutSettingsFragment : BasePreferenceFragment(R.string.about) {
Snackbar.make(listView, R.string.no_update_available, Snackbar.LENGTH_SHORT).show() Snackbar.make(listView, R.string.no_update_available, Snackbar.LENGTH_SHORT).show()
return return
} }
(activity as SettingsActivity).appUpdateDialog.show(version) startActivity(Intent(requireContext(), AppUpdateActivity::class.java))
} }
private fun openLink(url: String, title: CharSequence?) { private fun openLink(url: String, title: CharSequence?) {

@ -0,0 +1,180 @@
package org.koitharu.kotatsu.settings.about
import android.Manifest
import android.app.DownloadManager
import android.content.ActivityNotFoundException
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Build
import android.os.Bundle
import android.view.View
import android.widget.TextView
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.core.content.ContextCompat
import androidx.core.graphics.Insets
import androidx.core.net.toUri
import androidx.core.text.buildSpannedString
import dagger.hilt.android.AndroidEntryPoint
import io.noties.markwon.Markwon
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.github.AppVersion
import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.core.util.FileSize
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.core.util.ext.setTextAndVisible
import org.koitharu.kotatsu.core.util.ext.showOrHide
import org.koitharu.kotatsu.core.util.ext.textAndVisible
import org.koitharu.kotatsu.databinding.ActivityAppUpdateBinding
@AndroidEntryPoint
class AppUpdateActivity : BaseActivity<ActivityAppUpdateBinding>(), View.OnClickListener {
private val viewModel: AppUpdateViewModel by viewModels()
private lateinit var downloadReceiver: UpdateDownloadReceiver
private val permissionRequest = registerForActivityResult(
ActivityResultContracts.RequestPermission(),
) {
if (it) {
viewModel.startDownload()
} else {
openInBrowser()
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(ActivityAppUpdateBinding.inflate(layoutInflater))
downloadReceiver = UpdateDownloadReceiver(viewModel)
viewModel.nextVersion.observe(this, ::onNextVersionChanged)
viewBinding.buttonCancel.setOnClickListener(this)
viewBinding.buttonUpdate.setOnClickListener(this)
ContextCompat.registerReceiver(
this,
downloadReceiver,
IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE),
ContextCompat.RECEIVER_EXPORTED,
)
combine(viewModel.isLoading, viewModel.downloadProgress, ::Pair)
.observe(this, ::onProgressChanged)
viewModel.downloadState.observe(this, ::onDownloadStateChanged)
viewModel.onError.observeEvent(this, ::onError)
viewModel.onDownloadDone.observeEvent(this) { intent ->
try {
startActivity(intent)
} catch (e: ActivityNotFoundException) {
e.printStackTraceDebug()
}
}
}
override fun onDestroy() {
unregisterReceiver(downloadReceiver)
super.onDestroy()
}
override fun onClick(v: View) {
when (v.id) {
R.id.button_cancel -> finishAfterTransition()
R.id.button_update -> doUpdate()
}
}
override fun onWindowInsetsChanged(insets: Insets) {
val basePadding = resources.getDimensionPixelOffset(R.dimen.screen_padding)
viewBinding.root.setPadding(
basePadding + insets.left,
basePadding + insets.top,
basePadding + insets.right,
basePadding + insets.bottom,
)
}
private suspend fun onNextVersionChanged(version: AppVersion?) {
viewBinding.buttonUpdate.isEnabled = version != null && !viewModel.isLoading.value
if (version == null) {
viewBinding.textViewContent.setText(R.string.loading_)
return
}
val message = withContext(Dispatchers.Default) {
buildSpannedString {
append(getString(R.string.new_version_s, version.name))
appendLine()
append(getString(R.string.size_s, FileSize.BYTES.format(this@AppUpdateActivity, version.apkSize)))
appendLine()
appendLine()
append(Markwon.create(this@AppUpdateActivity).toMarkdown(version.description))
}
}
viewBinding.textViewContent.setText(message, TextView.BufferType.SPANNABLE)
}
private fun doUpdate() {
viewModel.installIntent.value?.let { intent ->
try {
startActivity(intent)
} catch (e: ActivityNotFoundException) {
onError(e)
}
return
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
permissionRequest.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)
} else {
viewModel.startDownload()
}
}
private fun openInBrowser() {
val latestVersion = viewModel.nextVersion.value ?: return
val intent = Intent(Intent.ACTION_VIEW, latestVersion.url.toUri())
startActivity(Intent.createChooser(intent, getString(R.string.open_in_browser)))
}
private fun onProgressChanged(value: Pair<Boolean, Float>) {
val (isLoading, downloadProgress) = value
val indicator = viewBinding.progressBar
indicator.showOrHide(isLoading)
indicator.isIndeterminate = downloadProgress <= 0f
if (downloadProgress > 0f) {
indicator.setProgressCompat((indicator.max * downloadProgress).toInt(), true)
}
viewBinding.buttonUpdate.isEnabled = !isLoading && viewModel.nextVersion.value != null
}
private fun onDownloadStateChanged(state: Int) {
val message = when (state) {
DownloadManager.STATUS_FAILED -> R.string.error_occurred
DownloadManager.STATUS_PAUSED -> R.string.downloads_paused
else -> 0
}
viewBinding.textViewError.setTextAndVisible(message)
}
private fun onError(e: Throwable) {
viewBinding.textViewError.textAndVisible = e.getDisplayMessage(resources)
}
private class UpdateDownloadReceiver(
private val viewModel: AppUpdateViewModel,
) : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
when (intent.action) {
DownloadManager.ACTION_DOWNLOAD_COMPLETE -> {
viewModel.onDownloadComplete(intent)
}
}
}
}
}

@ -1,89 +0,0 @@
package org.koitharu.kotatsu.settings.about
import android.Manifest
import android.app.DownloadManager
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Environment
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.core.net.toUri
import androidx.core.text.buildSpannedString
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import io.noties.markwon.Markwon
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.github.AppVersion
import org.koitharu.kotatsu.core.util.FileSize
import org.koitharu.kotatsu.core.util.ext.DIALOG_THEME_CENTERED
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
class AppUpdateDialog(private val activity: AppCompatActivity) {
private lateinit var latestVersion: AppVersion
private val permissionRequest = activity.registerForActivityResult(
ActivityResultContracts.RequestPermission(),
) {
if (it) {
downloadUpdateImpl()
} else {
openInBrowser()
}
}
fun show(version: AppVersion) {
latestVersion = version
val message = buildSpannedString {
append(activity.getString(R.string.new_version_s, version.name))
appendLine()
append(activity.getString(R.string.size_s, FileSize.BYTES.format(activity, version.apkSize)))
appendLine()
appendLine()
append(Markwon.create(activity).toMarkdown(version.description))
}
MaterialAlertDialogBuilder(activity, DIALOG_THEME_CENTERED)
.setTitle(R.string.app_update_available)
.setMessage(message)
.setIcon(R.drawable.ic_app_update)
.setNeutralButton(R.string.open_in_browser) { _, _ ->
val intent = Intent(Intent.ACTION_VIEW, version.url.toUri())
activity.startActivity(Intent.createChooser(intent, activity.getString(R.string.open_in_browser)))
}.setPositiveButton(R.string.update) { _, _ ->
downloadUpdate()
}.setNegativeButton(android.R.string.cancel, null)
.setCancelable(false)
.create()
.show()
}
private fun downloadUpdate() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
permissionRequest.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)
} else {
downloadUpdateImpl()
}
}
private fun downloadUpdateImpl() = runCatching {
val version = latestVersion
val url = version.apkUrl.toUri()
val dm = activity.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
val request = DownloadManager.Request(url)
.setTitle("${activity.getString(R.string.app_name)} v${version.name}")
.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, url.lastPathSegment)
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
.setMimeType("application/vnd.android.package-archive")
dm.enqueue(request)
}.onSuccess {
Toast.makeText(activity, R.string.download_started, Toast.LENGTH_SHORT).show()
}.onFailure { e ->
Toast.makeText(activity, e.getDisplayMessage(activity.resources), Toast.LENGTH_SHORT).show()
}
private fun openInBrowser() {
val intent = Intent(Intent.ACTION_VIEW, latestVersion.url.toUri())
activity.startActivity(Intent.createChooser(intent, activity.getString(R.string.open_in_browser)))
}
}

@ -0,0 +1,102 @@
package org.koitharu.kotatsu.settings.about
import android.app.DownloadManager
import android.content.Context
import android.content.Intent
import android.os.Environment
import androidx.core.net.toUri
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.isActive
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.github.AppUpdateRepository
import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.core.util.ext.requireValue
import javax.inject.Inject
import kotlin.coroutines.coroutineContext
@HiltViewModel
class AppUpdateViewModel @Inject constructor(
private val repository: AppUpdateRepository,
@ApplicationContext context: Context,
) : BaseViewModel() {
val nextVersion = repository.observeAvailableUpdate()
val downloadProgress = MutableStateFlow(-1f)
val downloadState = MutableStateFlow(DownloadManager.STATUS_PENDING)
val installIntent = MutableStateFlow<Intent?>(null)
val onDownloadDone = MutableEventFlow<Intent>()
private val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
private val appName = context.getString(R.string.app_name)
init {
if (nextVersion.value == null) {
launchLoadingJob(Dispatchers.Default) {
repository.fetchUpdate()
}
}
}
fun startDownload() {
launchLoadingJob(Dispatchers.Default) {
val version = nextVersion.requireValue()
val url = version.apkUrl.toUri()
val request = DownloadManager.Request(url)
.setTitle("$appName v${version.name}")
.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, url.lastPathSegment)
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
.setMimeType("application/vnd.android.package-archive")
val downloadId = downloadManager.enqueue(request)
observeDownload(downloadId)
}
}
fun onDownloadComplete(intent: Intent) {
launchLoadingJob(Dispatchers.Default) {
val downloadId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, 0L)
if (downloadId == 0L) {
return@launchLoadingJob
}
@Suppress("DEPRECATION")
val installerIntent = Intent(Intent.ACTION_INSTALL_PACKAGE)
installerIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION)
installerIntent.setDataAndType(
downloadManager.getUriForDownloadedFile(downloadId),
downloadManager.getMimeTypeForDownloadedFile(downloadId),
)
installerIntent.putExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, true)
installIntent.value = installerIntent
onDownloadDone.call(installerIntent)
}
}
private suspend fun observeDownload(id: Long) {
val query = DownloadManager.Query()
query.setFilterById(id)
while (coroutineContext.isActive) {
downloadManager.query(query).use { cursor ->
if (cursor.moveToFirst()) {
val bytesDownloaded = cursor.getLong(
cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR),
)
val bytesTotal = cursor.getLong(
cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES),
)
downloadProgress.value = bytesDownloaded.toFloat() / bytesTotal
val state = cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS))
downloadState.value = state
if (state == DownloadManager.STATUS_SUCCESSFUL || state == DownloadManager.STATUS_FAILED) {
return
}
}
}
delay(100)
}
}
}

@ -1,38 +0,0 @@
package org.koitharu.kotatsu.settings.about
import android.app.DownloadManager
import android.content.ActivityNotFoundException
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
class UpdateDownloadReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
when (intent.action) {
DownloadManager.ACTION_DOWNLOAD_COMPLETE -> {
val downloadId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, 0L)
if (downloadId == 0L) {
return
}
val dm = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
@Suppress("DEPRECATION")
val installIntent = Intent(Intent.ACTION_INSTALL_PACKAGE)
installIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION)
installIntent.setDataAndType(
dm.getUriForDownloadedFile(downloadId),
dm.getMimeTypeForDownloadedFile(downloadId),
)
installIntent.putExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, true)
try {
context.startActivity(installIntent)
} catch (e: ActivityNotFoundException) {
e.printStackTraceDebug()
}
}
}
}
}

@ -0,0 +1,95 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="@dimen/screen_padding">
<TextView
android:id="@+id/textView_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:drawablePadding="16dp"
android:gravity="center_horizontal"
android:text="@string/app_update_available"
android:textAppearance="?textAppearanceHeadline5"
app:drawableTint="?colorPrimary"
app:drawableTopCompat="@drawable/ic_app_update"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/progressBar"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/screen_padding"
android:max="100"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/textView_title"
tools:visibility="visible" />
<TextView
android:id="@+id/textView_error"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textColor="?colorError"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/progressBar"
tools:text="@string/error_corrupted_file"
tools:visibility="visible" />
<ScrollView
android:id="@+id/scrollView"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginVertical="@dimen/screen_padding"
app:layout_constraintBottom_toTopOf="@id/barrier"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/textView_error">
<TextView
android:id="@+id/textView_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?textAppearanceBodyMedium"
tools:text="@tools:sample/lorem/random" />
</ScrollView>
<com.google.android.material.button.MaterialButton
android:id="@+id/button_cancel"
style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@android:string/cancel"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<com.google.android.material.button.MaterialButton
android:id="@+id/button_update"
style="@style/Widget.Material3.Button.TonalButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:enabled="false"
android:text="@string/update"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/barrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="top"
app:constraint_referenced_ids="button_cancel,button_update" />
</androidx.constraintlayout.widget.ConstraintLayout>
Loading…
Cancel
Save