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