App udpate activity #880
parent
e25ccf6b25
commit
3691db8e8e
@ -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…
Reference in New Issue