Improve edge to edge support

This commit is contained in:
Jonas Lochmann 2024-10-28 01:00:00 +01:00
parent 679276e3cf
commit bc0e83b916
No known key found for this signature in database
GPG key ID: 8B8C9AEE10FA5B36
6 changed files with 209 additions and 190 deletions

View file

@ -183,6 +183,7 @@ dependencies {
implementation 'androidx.compose.material:material-icons-extended:1.7.5' implementation 'androidx.compose.material:material-icons-extended:1.7.5'
debugImplementation "androidx.compose.ui:ui-tooling:1.7.5" debugImplementation "androidx.compose.ui:ui-tooling:1.7.5"
implementation 'androidx.fragment:fragment-ktx:1.8.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-fragment-ktx:$nav_version"
implementation "androidx.navigation:navigation-ui:$nav_version" implementation "androidx.navigation:navigation-ui:$nav_version"

View file

@ -23,16 +23,30 @@ import android.os.Build
import android.os.Bundle import android.os.Bundle
import androidx.activity.OnBackPressedCallback import androidx.activity.OnBackPressedCallback
import androidx.activity.SystemBarStyle import androidx.activity.SystemBarStyle
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat import androidx.compose.foundation.layout.Column
import androidx.core.view.WindowInsetsCompat import androidx.compose.foundation.layout.fillMaxSize
import androidx.core.view.updatePadding import androidx.compose.foundation.layout.padding
import androidx.lifecycle.MutableLiveData import androidx.compose.foundation.pager.HorizontalPager
import androidx.viewpager.widget.ViewPager 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.R
import io.timelimit.android.databinding.LockActivityBinding import io.timelimit.android.data.model.UserType
import io.timelimit.android.extensions.showSafe import io.timelimit.android.extensions.showSafe
import io.timelimit.android.logic.BlockingReason import io.timelimit.android.logic.BlockingReason
import io.timelimit.android.logic.DefaultAppLogic 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.U2fManager
import io.timelimit.android.u2f.protocol.U2FDevice import io.timelimit.android.u2f.protocol.U2FDevice
import io.timelimit.android.ui.IsAppInForeground 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.AuthTokenLoginProcessor
import io.timelimit.android.ui.login.NewLoginFragment import io.timelimit.android.ui.login.NewLoginFragment
import io.timelimit.android.ui.main.ActivityViewModel import io.timelimit.android.ui.main.ActivityViewModel
import io.timelimit.android.ui.main.ActivityViewModelHolder 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.manage.child.primarydevice.UpdatePrimaryDeviceDialogFragment
import io.timelimit.android.ui.util.SyncStatusModel import io.timelimit.android.ui.util.SyncStatusModel
@ -91,8 +106,6 @@ class LockActivity : AppCompatActivity(), ActivityViewModelHolder, U2fManager.De
null null
} }
private val showAuth = MutableLiveData<Boolean>().apply { value = false }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -107,19 +120,98 @@ class LockActivity : AppCompatActivity(), ActivityViewModelHolder, U2fManager.De
) )
) )
supportActionBar!!.hide()
U2fManager.setupActivity(this) 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) isTimeOver
setContentView(binding.root) }.asFlow()
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view, windowInsets -> setContent {
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) 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<LockReasonFragment>(Modifier.fillMaxSize())
1 -> AndroidFragment<LockActionFragment>(Modifier.fillMaxSize())
2 -> AndroidFragment<LockTaskFragment>(Modifier.fillMaxSize())
}
}
)
}
},
executeCommand = {},
showAuthenticationDialog =
if (pager.currentPage == 1 && !isAuthenticated) ({ showAuthenticationScreen() })
else null
)
}
} }
syncModel.statusText.observe(this) { supportActionBar?.subtitle = it } syncModel.statusText.observe(this) { supportActionBar?.subtitle = it }
@ -128,8 +220,6 @@ class LockActivity : AppCompatActivity(), ActivityViewModelHolder, U2fManager.De
model.init(blockedPackageName, blockedActivityName) model.init(blockedPackageName, blockedActivityName)
binding.pager.adapter = adapter
model.content.observe(this) { model.content.observe(this) {
if (isResumed && it is LockscreenContent.Blocked.BlockedCategory && it.reason == BlockingReason.RequiresCurrentDevice && !model.didOpenSetCurrentDeviceScreen) { if (isResumed && it is LockscreenContent.Blocked.BlockedCategory && it.reason == BlockingReason.RequiresCurrentDevice && !model.didOpenSetCurrentDeviceScreen) {
model.didOpenSetCurrentDeviceScreen = true model.didOpenSetCurrentDeviceScreen = true
@ -140,30 +230,12 @@ class LockActivity : AppCompatActivity(), ActivityViewModelHolder, U2fManager.De
} }
} }
AuthenticationFab.manageAuthenticationFab( activityModel.shouldHighlightAuthenticationButton.observe(this) {
fab = binding.fab, if (it) {
shouldHighlight = activityModel.shouldHighlightAuthenticationButton, activityModel.shouldHighlightAuthenticationButton.postValue(false)
authenticatedUser = activityModel.authenticatedUser,
activity = this,
doesSupportAuth = showAuth
)
binding.fab.setOnClickListener { showAuthenticationScreen() } showAuthenticationScreen()
binding.pager.addOnPageChangeListener(object: ViewPager.SimpleOnPageChangeListener() {
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
showAuth.value = position == 1
} }
})
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) { onBackPressedDispatcher.addCallback(object: OnBackPressedCallback(true) {

View file

@ -1,44 +0,0 @@
/*
* TimeLimit Copyright <C> 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 <https://www.gnu.org/licenses/>.
*/
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()
})
}

View file

@ -22,14 +22,17 @@ import android.content.res.Configuration
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import android.view.LayoutInflater
import androidx.activity.OnBackPressedCallback import androidx.activity.OnBackPressedCallback
import androidx.activity.SystemBarStyle import androidx.activity.SystemBarStyle
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat import androidx.compose.foundation.layout.fillMaxSize
import androidx.core.view.WindowInsetsCompat import androidx.compose.foundation.layout.padding
import androidx.core.view.updatePadding import androidx.compose.ui.Modifier
import androidx.compose.ui.viewinterop.AndroidView
import androidx.lifecycle.map import androidx.lifecycle.map
import io.timelimit.android.BuildConfig import io.timelimit.android.BuildConfig
import io.timelimit.android.R 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.logic.DefaultAppLogic
import io.timelimit.android.u2f.U2fManager import io.timelimit.android.u2f.U2fManager
import io.timelimit.android.u2f.protocol.U2FDevice 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.backdoor.BackdoorDialogFragment
import io.timelimit.android.ui.login.AuthTokenLoginProcessor import io.timelimit.android.ui.login.AuthTokenLoginProcessor
import io.timelimit.android.ui.login.NewLoginFragment import io.timelimit.android.ui.login.NewLoginFragment
@ -71,6 +76,8 @@ class AnnoyActivity : AppCompatActivity(), ActivityViewModelHolder, U2fManager.D
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val logic = DefaultAppLogic.with(this)
val isNightMode = val isNightMode =
(resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) ==
Configuration.UI_MODE_NIGHT_YES 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) logic.annoyLogic.nextManualUnblockCountdown.observe(this) { countdown ->
setContentView(binding.root) binding.canRequestUnlock = countdown == 0L
binding.countdownText = getString(R.string.annoy_timer, TimeTextUtil.seconds((countdown / 1000).toInt(), this@AnnoyActivity))
}
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view, windowInsets -> logic.deviceEntry.map {
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) 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) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
val systemImageApps = packageManager.getInstalledApplications(0) val systemImageApps = packageManager.getInstalledApplications(0)
.filter { it.flags and ApplicationInfo.FLAG_SYSTEM == ApplicationInfo.FLAG_SYSTEM } .filter { it.flags and ApplicationInfo.FLAG_SYSTEM == ApplicationInfo.FLAG_SYSTEM }
@ -117,35 +166,6 @@ class AnnoyActivity : AppCompatActivity(), ActivityViewModelHolder, U2fManager.D
if (!shouldRun) shutdown() 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 -> model.authenticatedUser.observe(this) { user ->
if (user?.second?.type == UserType.Parent) { if (user?.second?.type == UserType.Parent) {
logic.annoyLogic.doParentTempUnlock() logic.annoyLogic.doParentTempUnlock()

View file

@ -17,16 +17,20 @@ package io.timelimit.android.ui.update
import android.content.res.Configuration import android.content.res.Configuration
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater
import androidx.activity.SystemBarStyle import androidx.activity.SystemBarStyle
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat import androidx.compose.foundation.layout.fillMaxSize
import androidx.core.view.WindowInsetsCompat import androidx.compose.foundation.layout.padding
import androidx.core.view.updatePadding import androidx.compose.ui.Modifier
import androidx.databinding.DataBindingUtil import androidx.compose.ui.viewinterop.AndroidView
import io.timelimit.android.R import io.timelimit.android.R
import io.timelimit.android.databinding.UpdateActivityBinding import io.timelimit.android.databinding.UpdateActivityBinding
import io.timelimit.android.logic.DefaultAppLogic import io.timelimit.android.logic.DefaultAppLogic
import io.timelimit.android.ui.ScreenScaffold
import io.timelimit.android.ui.Theme
class UpdateActivity: AppCompatActivity() { class UpdateActivity: AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@ -43,21 +47,37 @@ class UpdateActivity: AppCompatActivity() {
) )
) )
val binding = DataBindingUtil.setContentView<UpdateActivityBinding>(this, R.layout.update_activity) supportActionBar!!.hide()
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view, windowInsets -> setContent {
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) 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)
)
} }
} }

View file

@ -1,50 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
TimeLimit Copyright <C> 2019 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 <https://www.gnu.org/licenses/>.
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".ui.lock.LockActivity" >
<com.google.android.material.tabs.TabLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/tabs"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" />
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.viewpager.widget.ViewPager
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/pager" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
app:fabSize="normal"
android:src="@drawable/ic_lock_open_white_24dp"
android:layout_margin="16dp"
android:layout_gravity="end|bottom"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</LinearLayout>