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'
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"

View file

@ -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<Boolean>().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<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 }
@ -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) {

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.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,40 +89,20 @@ class AnnoyActivity : AppCompatActivity(), ActivityViewModelHolder, U2fManager.D
)
)
U2fManager.setupActivity(this)
supportActionBar!!.hide()
val logic = DefaultAppLogic.with(this)
val binding = AnnoyActivityBinding.inflate(layoutInflater)
setContentView(binding.root)
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view, windowInsets ->
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
view.updatePadding(insets.left, insets.top, insets.right, insets.bottom)
WindowInsetsCompat.CONSUMED
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
val systemImageApps = packageManager.getInstalledApplications(0)
.filter { it.flags and ApplicationInfo.FLAG_SYSTEM == ApplicationInfo.FLAG_SYSTEM }
.map { it.packageName }.toSet()
val lockTaskPackages = AndroidIntegrationApps.appsToIncludeInLockTasks + setOf(packageName) + systemImageApps
if (BuildConfig.DEBUG) {
Log.d(LOG_TAG, "setLockTaskPackages: $lockTaskPackages")
}
if (logic.platformIntegration.setLockTaskPackages(lockTaskPackages.toList())) {
startLockTask()
}
}
logic.annoyLogic.shouldAnnoyRightNow.observe(this) { shouldRun ->
if (!shouldRun) shutdown()
}
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))
logic.annoyLogic.nextManualUnblockCountdown.observe(this) { countdown ->
binding.canRequestUnlock = countdown == 0L
@ -146,6 +133,39 @@ class AnnoyActivity : AppCompatActivity(), ActivityViewModelHolder, U2fManager.D
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 }
.map { it.packageName }.toSet()
val lockTaskPackages = AndroidIntegrationApps.appsToIncludeInLockTasks + setOf(packageName) + systemImageApps
if (BuildConfig.DEBUG) {
Log.d(LOG_TAG, "setLockTaskPackages: $lockTaskPackages")
}
if (logic.platformIntegration.setLockTaskPackages(lockTaskPackages.toList())) {
startLockTask()
}
}
logic.annoyLogic.shouldAnnoyRightNow.observe(this) { shouldRun ->
if (!shouldRun) shutdown()
}
model.authenticatedUser.observe(this) { user ->
if (user?.second?.type == UserType.Parent) {
logic.annoyLogic.doParentTempUnlock()

View file

@ -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,15 +47,20 @@ class UpdateActivity: AppCompatActivity() {
)
)
val binding = DataBindingUtil.setContentView<UpdateActivityBinding>(this, R.layout.update_activity)
supportActionBar!!.hide()
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view, windowInsets ->
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
view.updatePadding(insets.left, insets.top, insets.right, insets.bottom)
WindowInsetsCompat.CONSUMED
}
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))
UpdateView.bind(
view = binding.update,
@ -59,5 +68,16 @@ class UpdateActivity: AppCompatActivity() {
fragmentManager = supportFragmentManager,
appLogic = DefaultAppLogic.with(this)
)
binding.root
},
modifier = Modifier.fillMaxSize().padding(padding)
)
},
executeCommand = {},
showAuthenticationDialog = null
)
}
}
}
}

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>