From bc0e83b916f5ef03ea974ea01bfed1abbec660ea Mon Sep 17 00:00:00 2001 From: Jonas Lochmann Date: Mon, 28 Oct 2024 01:00:00 +0100 Subject: [PATCH 1/3] Improve edge to edge support --- app/build.gradle | 1 + .../timelimit/android/ui/lock/LockActivity.kt | 152 +++++++++++++----- .../android/ui/lock/LockActivityAdapter.kt | 44 ----- .../android/ui/manipulation/AnnoyActivity.kt | 100 +++++++----- .../android/ui/update/UpdateActivity.kt | 52 ++++-- app/src/main/res/layout/lock_activity.xml | 50 ------ 6 files changed, 209 insertions(+), 190 deletions(-) delete mode 100644 app/src/main/java/io/timelimit/android/ui/lock/LockActivityAdapter.kt delete mode 100644 app/src/main/res/layout/lock_activity.xml diff --git a/app/build.gradle b/app/build.gradle index 775ce46..13e6668 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -183,6 +183,7 @@ dependencies { implementation 'androidx.compose.material:material-icons-extended:1.7.5' debugImplementation "androidx.compose.ui:ui-tooling:1.7.5" implementation 'androidx.fragment:fragment-ktx:1.8.5' + implementation 'androidx.fragment:fragment-compose:1.8.5' implementation "androidx.navigation:navigation-fragment-ktx:$nav_version" implementation "androidx.navigation:navigation-ui:$nav_version" diff --git a/app/src/main/java/io/timelimit/android/ui/lock/LockActivity.kt b/app/src/main/java/io/timelimit/android/ui/lock/LockActivity.kt index 18aa130..62e6c5d 100644 --- a/app/src/main/java/io/timelimit/android/ui/lock/LockActivity.kt +++ b/app/src/main/java/io/timelimit/android/ui/lock/LockActivity.kt @@ -23,16 +23,30 @@ import android.os.Build import android.os.Bundle import androidx.activity.OnBackPressedCallback import androidx.activity.SystemBarStyle +import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity -import androidx.core.view.ViewCompat -import androidx.core.view.WindowInsetsCompat -import androidx.core.view.updatePadding -import androidx.lifecycle.MutableLiveData -import androidx.viewpager.widget.ViewPager +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.material.Tab +import androidx.compose.material.TabRow +import androidx.compose.material.TabRowDefaults +import androidx.compose.material.TabRowDefaults.tabIndicatorOffset +import androidx.compose.material.Text +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.fragment.compose.AndroidFragment +import androidx.lifecycle.asFlow +import androidx.lifecycle.map import io.timelimit.android.R -import io.timelimit.android.databinding.LockActivityBinding +import io.timelimit.android.data.model.UserType import io.timelimit.android.extensions.showSafe import io.timelimit.android.logic.BlockingReason import io.timelimit.android.logic.DefaultAppLogic @@ -40,11 +54,12 @@ import io.timelimit.android.sync.network.UpdatePrimaryDeviceRequestType import io.timelimit.android.u2f.U2fManager import io.timelimit.android.u2f.protocol.U2FDevice import io.timelimit.android.ui.IsAppInForeground +import io.timelimit.android.ui.ScreenScaffold +import io.timelimit.android.ui.Theme import io.timelimit.android.ui.login.AuthTokenLoginProcessor import io.timelimit.android.ui.login.NewLoginFragment import io.timelimit.android.ui.main.ActivityViewModel import io.timelimit.android.ui.main.ActivityViewModelHolder -import io.timelimit.android.ui.main.AuthenticationFab import io.timelimit.android.ui.manage.child.primarydevice.UpdatePrimaryDeviceDialogFragment import io.timelimit.android.ui.util.SyncStatusModel @@ -91,8 +106,6 @@ class LockActivity : AppCompatActivity(), ActivityViewModelHolder, U2fManager.De null } - private val showAuth = MutableLiveData().apply { value = false } - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -107,19 +120,98 @@ class LockActivity : AppCompatActivity(), ActivityViewModelHolder, U2fManager.De ) ) + supportActionBar!!.hide() + U2fManager.setupActivity(this) - val adapter = LockActivityAdapter(supportFragmentManager, this) + val subtitleLive = syncModel.statusText.asFlow() + val showTasksLive = model.content.map { + val isTimeOver = it is LockscreenContent.Blocked.BlockedCategory && it.blockingHandling.activityBlockingReason == BlockingReason.TimeOver - val binding = LockActivityBinding.inflate(layoutInflater) - setContentView(binding.root) + isTimeOver + }.asFlow() - ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view, windowInsets -> - val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) + setContent { + val subtitle by subtitleLive.collectAsState(null) + val showTasks by showTasksLive.collectAsState(false) + val pager = rememberPagerState(initialPage = 0, pageCount = { + if (showTasks) 3 + else 2 + }) + val isAuthenticated by getActivityViewModel().authenticatedUser + .map { it?.second?.type == UserType.Parent } + .asFlow().collectAsState(initial = false) - view.updatePadding(insets.left, insets.top, insets.right, insets.bottom) + Theme { + ScreenScaffold( + screen = null, + title = getString(R.string.app_name), + subtitle = subtitle, + backStack = emptyList(), + snackbarHostState = null, + content = { padding -> + Column (Modifier.fillMaxSize().padding(padding)) { + TabRow( + pager.currentPage, + indicator = { tabPositions -> + // workaround for bug + TabRowDefaults.Indicator( + Modifier.tabIndicatorOffset(tabPositions[ + pager.currentPage.coerceAtMost(tabPositions.size - 1) + ]) + ) + } + ) { + Tab( + selected = pager.currentPage == 0, + onClick = { pager.requestScrollToPage(0) } + ) { + Text( + stringResource(R.string.lock_tab_reason), + Modifier.padding(16.dp) + ) + } - WindowInsetsCompat.CONSUMED + Tab( + selected = pager.currentPage == 1, + onClick = { pager.requestScrollToPage(1) } + ) { + Text( + stringResource(R.string.lock_tab_action), + Modifier.padding(16.dp) + ) + } + + if (showTasks) Tab( + selected = pager.currentPage == 2, + onClick = { pager.requestScrollToPage(2) } + ) { + Text( + stringResource(R.string.lock_tab_task), + Modifier.padding(16.dp) + ) + } + } + + HorizontalPager( + pager, + Modifier.weight(1.0F, fill = true), + pageContent = { index -> + when (index) { + 0 -> AndroidFragment(Modifier.fillMaxSize()) + 1 -> AndroidFragment(Modifier.fillMaxSize()) + 2 -> AndroidFragment(Modifier.fillMaxSize()) + } + } + ) + } + }, + executeCommand = {}, + showAuthenticationDialog = + if (pager.currentPage == 1 && !isAuthenticated) ({ showAuthenticationScreen() }) + else null + ) + } } syncModel.statusText.observe(this) { supportActionBar?.subtitle = it } @@ -128,8 +220,6 @@ class LockActivity : AppCompatActivity(), ActivityViewModelHolder, U2fManager.De model.init(blockedPackageName, blockedActivityName) - binding.pager.adapter = adapter - model.content.observe(this) { if (isResumed && it is LockscreenContent.Blocked.BlockedCategory && it.reason == BlockingReason.RequiresCurrentDevice && !model.didOpenSetCurrentDeviceScreen) { model.didOpenSetCurrentDeviceScreen = true @@ -140,30 +230,12 @@ class LockActivity : AppCompatActivity(), ActivityViewModelHolder, U2fManager.De } } - AuthenticationFab.manageAuthenticationFab( - fab = binding.fab, - shouldHighlight = activityModel.shouldHighlightAuthenticationButton, - authenticatedUser = activityModel.authenticatedUser, - activity = this, - doesSupportAuth = showAuth - ) + activityModel.shouldHighlightAuthenticationButton.observe(this) { + if (it) { + activityModel.shouldHighlightAuthenticationButton.postValue(false) - binding.fab.setOnClickListener { showAuthenticationScreen() } - - binding.pager.addOnPageChangeListener(object: ViewPager.SimpleOnPageChangeListener() { - override fun onPageSelected(position: Int) { - super.onPageSelected(position) - - showAuth.value = position == 1 + showAuthenticationScreen() } - }) - - binding.tabs.setupWithViewPager(binding.pager) - - model.content.observe(this) { - val isTimeOver = it is LockscreenContent.Blocked.BlockedCategory && it.blockingHandling.activityBlockingReason == BlockingReason.TimeOver - - adapter.showTasksFragment = isTimeOver } onBackPressedDispatcher.addCallback(object: OnBackPressedCallback(true) { diff --git a/app/src/main/java/io/timelimit/android/ui/lock/LockActivityAdapter.kt b/app/src/main/java/io/timelimit/android/ui/lock/LockActivityAdapter.kt deleted file mode 100644 index 04080e3..0000000 --- a/app/src/main/java/io/timelimit/android/ui/lock/LockActivityAdapter.kt +++ /dev/null @@ -1,44 +0,0 @@ -/* - * TimeLimit Copyright 2019 - 2020 Jonas Lochmann - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package io.timelimit.android.ui.lock - -import android.content.Context -import androidx.fragment.app.Fragment -import androidx.fragment.app.FragmentManager -import androidx.fragment.app.FragmentPagerAdapter -import io.timelimit.android.R -import kotlin.properties.Delegates - -class LockActivityAdapter(fragmentManager: FragmentManager, private val context: Context): FragmentPagerAdapter(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { - var showTasksFragment: Boolean by Delegates.observable(false) { _, _, _ -> notifyDataSetChanged() } - - override fun getCount(): Int = if (showTasksFragment) 3 else 2 - - override fun getItem(position: Int): Fragment = when (position) { - 0 -> LockReasonFragment() - 1 -> LockActionFragment() - 2 -> LockTaskFragment() - else -> throw IllegalArgumentException() - } - - override fun getPageTitle(position: Int): CharSequence? = context.getString(when (position) { - 0 -> R.string.lock_tab_reason - 1 -> R.string.lock_tab_action - 2 -> R.string.lock_tab_task - else -> throw IllegalArgumentException() - }) -} \ No newline at end of file diff --git a/app/src/main/java/io/timelimit/android/ui/manipulation/AnnoyActivity.kt b/app/src/main/java/io/timelimit/android/ui/manipulation/AnnoyActivity.kt index 0a57ef9..6bf0fa7 100644 --- a/app/src/main/java/io/timelimit/android/ui/manipulation/AnnoyActivity.kt +++ b/app/src/main/java/io/timelimit/android/ui/manipulation/AnnoyActivity.kt @@ -22,14 +22,17 @@ import android.content.res.Configuration import android.os.Build import android.os.Bundle import android.util.Log +import android.view.LayoutInflater import androidx.activity.OnBackPressedCallback import androidx.activity.SystemBarStyle +import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity -import androidx.core.view.ViewCompat -import androidx.core.view.WindowInsetsCompat -import androidx.core.view.updatePadding +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.ui.Modifier +import androidx.compose.ui.viewinterop.AndroidView import androidx.lifecycle.map import io.timelimit.android.BuildConfig import io.timelimit.android.R @@ -40,6 +43,8 @@ import io.timelimit.android.integration.platform.android.AndroidIntegrationApps import io.timelimit.android.logic.DefaultAppLogic import io.timelimit.android.u2f.U2fManager import io.timelimit.android.u2f.protocol.U2FDevice +import io.timelimit.android.ui.ScreenScaffold +import io.timelimit.android.ui.Theme import io.timelimit.android.ui.backdoor.BackdoorDialogFragment import io.timelimit.android.ui.login.AuthTokenLoginProcessor import io.timelimit.android.ui.login.NewLoginFragment @@ -71,6 +76,8 @@ class AnnoyActivity : AppCompatActivity(), ActivityViewModelHolder, U2fManager.D override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + val logic = DefaultAppLogic.with(this) + val isNightMode = (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES @@ -82,21 +89,63 @@ class AnnoyActivity : AppCompatActivity(), ActivityViewModelHolder, U2fManager.D ) ) - U2fManager.setupActivity(this) + supportActionBar!!.hide() - val logic = DefaultAppLogic.with(this) + setContent { + Theme { + ScreenScaffold( + screen = null, + title = getString(R.string.app_name), + subtitle = null, + backStack = emptyList(), + snackbarHostState = null, + content = { padding -> + AndroidView( + factory = { + val binding = AnnoyActivityBinding.inflate(LayoutInflater.from(it)) - val binding = AnnoyActivityBinding.inflate(layoutInflater) - setContentView(binding.root) + logic.annoyLogic.nextManualUnblockCountdown.observe(this) { countdown -> + binding.canRequestUnlock = countdown == 0L + binding.countdownText = getString(R.string.annoy_timer, TimeTextUtil.seconds((countdown / 1000).toInt(), this@AnnoyActivity)) + } - ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view, windowInsets -> - val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) + logic.deviceEntry.map { + val reasonItems = (it?.let { ManipulationWarnings.getFromDevice(it) } ?: ManipulationWarnings.empty) + .current + .map { getString(it.labelResourceId) } - view.updatePadding(insets.left, insets.top, insets.right, insets.bottom) + if (reasonItems.isEmpty()) { + null + } else { + getString(R.string.annoy_reason, reasonItems.joinToString(separator = ", ")) + } + }.observe(this) { binding.reasonText = it } - WindowInsetsCompat.CONSUMED + binding.unlockTemporarilyButton.setOnClickListener { + AnnoyUnlockDialogFragment.newInstance(AnnoyUnlockDialogFragment.UnlockDuration.Short) + .show(supportFragmentManager) + } + + binding.parentUnlockButton.setOnClickListener { + AnnoyUnlockDialogFragment.newInstance(AnnoyUnlockDialogFragment.UnlockDuration.Long) + .show(supportFragmentManager) + } + + binding.useBackdoorButton.setOnClickListener { BackdoorDialogFragment().show(supportFragmentManager) } + + binding.root + }, + modifier = Modifier.fillMaxSize().padding(padding) + ) + }, + executeCommand = {}, + showAuthenticationDialog = null + ) + } } + U2fManager.setupActivity(this) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { val systemImageApps = packageManager.getInstalledApplications(0) .filter { it.flags and ApplicationInfo.FLAG_SYSTEM == ApplicationInfo.FLAG_SYSTEM } @@ -117,35 +166,6 @@ class AnnoyActivity : AppCompatActivity(), ActivityViewModelHolder, U2fManager.D if (!shouldRun) shutdown() } - logic.annoyLogic.nextManualUnblockCountdown.observe(this) { countdown -> - binding.canRequestUnlock = countdown == 0L - binding.countdownText = getString(R.string.annoy_timer, TimeTextUtil.seconds((countdown / 1000).toInt(), this@AnnoyActivity)) - } - - logic.deviceEntry.map { - val reasonItems = (it?.let { ManipulationWarnings.getFromDevice(it) } ?: ManipulationWarnings.empty) - .current - .map { getString(it.labelResourceId) } - - if (reasonItems.isEmpty()) { - null - } else { - getString(R.string.annoy_reason, reasonItems.joinToString(separator = ", ")) - } - }.observe(this) { binding.reasonText = it } - - binding.unlockTemporarilyButton.setOnClickListener { - AnnoyUnlockDialogFragment.newInstance(AnnoyUnlockDialogFragment.UnlockDuration.Short) - .show(supportFragmentManager) - } - - binding.parentUnlockButton.setOnClickListener { - AnnoyUnlockDialogFragment.newInstance(AnnoyUnlockDialogFragment.UnlockDuration.Long) - .show(supportFragmentManager) - } - - binding.useBackdoorButton.setOnClickListener { BackdoorDialogFragment().show(supportFragmentManager) } - model.authenticatedUser.observe(this) { user -> if (user?.second?.type == UserType.Parent) { logic.annoyLogic.doParentTempUnlock() diff --git a/app/src/main/java/io/timelimit/android/ui/update/UpdateActivity.kt b/app/src/main/java/io/timelimit/android/ui/update/UpdateActivity.kt index 53e11c1..e213776 100644 --- a/app/src/main/java/io/timelimit/android/ui/update/UpdateActivity.kt +++ b/app/src/main/java/io/timelimit/android/ui/update/UpdateActivity.kt @@ -17,16 +17,20 @@ package io.timelimit.android.ui.update import android.content.res.Configuration import android.os.Bundle +import android.view.LayoutInflater import androidx.activity.SystemBarStyle +import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.appcompat.app.AppCompatActivity -import androidx.core.view.ViewCompat -import androidx.core.view.WindowInsetsCompat -import androidx.core.view.updatePadding -import androidx.databinding.DataBindingUtil +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.ui.Modifier +import androidx.compose.ui.viewinterop.AndroidView import io.timelimit.android.R import io.timelimit.android.databinding.UpdateActivityBinding import io.timelimit.android.logic.DefaultAppLogic +import io.timelimit.android.ui.ScreenScaffold +import io.timelimit.android.ui.Theme class UpdateActivity: AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { @@ -43,21 +47,37 @@ class UpdateActivity: AppCompatActivity() { ) ) - val binding = DataBindingUtil.setContentView(this, R.layout.update_activity) + supportActionBar!!.hide() - ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view, windowInsets -> - val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) + setContent { + Theme { + ScreenScaffold( + screen = null, + title = getString(R.string.app_name), + subtitle = null, + backStack = emptyList(), + snackbarHostState = null, + content = { padding -> + AndroidView( + factory = { + val binding = UpdateActivityBinding.inflate(LayoutInflater.from(it)) - view.updatePadding(insets.left, insets.top, insets.right, insets.bottom) + UpdateView.bind( + view = binding.update, + lifecycleOwner = this, + fragmentManager = supportFragmentManager, + appLogic = DefaultAppLogic.with(this) + ) - WindowInsetsCompat.CONSUMED + binding.root + }, + modifier = Modifier.fillMaxSize().padding(padding) + ) + }, + executeCommand = {}, + showAuthenticationDialog = null + ) + } } - - UpdateView.bind( - view = binding.update, - lifecycleOwner = this, - fragmentManager = supportFragmentManager, - appLogic = DefaultAppLogic.with(this) - ) } } \ No newline at end of file diff --git a/app/src/main/res/layout/lock_activity.xml b/app/src/main/res/layout/lock_activity.xml deleted file mode 100644 index 1ec69cf..0000000 --- a/app/src/main/res/layout/lock_activity.xml +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file From 13fe10b543b3fd57211a45c5f409a18be587dd74 Mon Sep 17 00:00:00 2001 From: Jonas Lochmann Date: Mon, 28 Oct 2024 01:00:00 +0100 Subject: [PATCH 2/3] Disable room warnings regarding internally used column --- app/src/main/java/io/timelimit/android/data/dao/UsedTimeDao.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/main/java/io/timelimit/android/data/dao/UsedTimeDao.kt b/app/src/main/java/io/timelimit/android/data/dao/UsedTimeDao.kt index e1d20d7..8b707f8 100644 --- a/app/src/main/java/io/timelimit/android/data/dao/UsedTimeDao.kt +++ b/app/src/main/java/io/timelimit/android/data/dao/UsedTimeDao.kt @@ -19,6 +19,7 @@ import androidx.lifecycle.LiveData import androidx.room.Dao import androidx.room.Insert import androidx.room.Query +import androidx.room.RoomWarnings import io.timelimit.android.data.model.UsedTimeItem import io.timelimit.android.data.model.UsedTimeListItem import io.timelimit.android.livedata.ignoreUnchanged @@ -68,11 +69,13 @@ abstract class UsedTimeDao { // breaking it into multiple lines causes issues during compilation ... // this warns about an unused column, but this column is used in the ORDER BY + @SuppressWarnings(RoomWarnings.CURSOR_MISMATCH) @Query("SELECT 2 AS type, start_time_of_day AS startMinuteOfDay, end_time_of_day AS endMinuteOfDay, used_time AS duration, day_of_epoch AS day, NULL AS lastUsage, NULL AS maxSessionDuration, NULL AS pauseDuration, category.id AS categoryId, category.title AS categoryTitle FROM used_time JOIN category ON (used_time.category_id = category.id) WHERE category.id = :categoryId UNION ALL SELECT 1 AS type, start_minute_of_day AS startMinuteOfDay, end_minute_of_day AS endMinuteOfDay, last_session_duration AS duration, NULL AS day, last_usage AS lastUsage, max_session_duration AS maxSessionDuration, session_pause_duration AS pauseDuration, category.id AS categoryId, category.title AS categoryTitle FROM session_duration JOIN category ON (session_duration.category_id = category.id) WHERE category.id = :categoryId ORDER BY type, day DESC, lastUsage DESC, startMinuteOfDay, endMinuteOfDay, categoryId") abstract fun getUsedTimeListItemsByCategoryId(categoryId: String): Flow> // breaking it into multiple lines causes issues during compilation ... // this warns about an unused column, but this column is used in the ORDER BY + @SuppressWarnings(RoomWarnings.CURSOR_MISMATCH) @Query("SELECT 2 AS type, start_time_of_day AS startMinuteOfDay, end_time_of_day AS endMinuteOfDay, used_time AS duration, day_of_epoch AS day, NULL AS lastUsage, NULL AS maxSessionDuration, NULL AS pauseDuration, category.id AS categoryId, category.title AS categoryTitle FROM used_time JOIN category ON (used_time.category_id = category.id) WHERE category.child_id = :userId UNION ALL SELECT 1 AS type, start_minute_of_day AS startMinuteOfDay, end_minute_of_day AS endMinuteOfDay, last_session_duration AS duration, NULL AS day, last_usage AS lastUsage, max_session_duration AS maxSessionDuration, session_pause_duration AS pauseDuration, category.id AS categoryId, category.title AS categoryTitle FROM session_duration JOIN category ON (session_duration.category_id = category.id) WHERE category.child_id = :userId ORDER BY type, day DESC, lastUsage DESC, startMinuteOfDay, endMinuteOfDay, categoryId") abstract fun getUsedTimeListItemsByUserId(userId: String): Flow> } From 2ed75c7f0feaf834ab210740085e9eb4c92bb134 Mon Sep 17 00:00:00 2001 From: Jonas Lochmann Date: Mon, 28 Oct 2024 01:00:00 +0100 Subject: [PATCH 3/3] Release 7.2.1 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 13e6668..ac9ed68 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -30,8 +30,8 @@ android { applicationId "io.timelimit.android" minSdkVersion 26 targetSdkVersion 35 - versionCode 219 - versionName "7.2.0" + versionCode 220 + versionName "7.2.1" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" kapt { arguments {