From 035fb9e75523cefa981b2503001fd5220f46d9e2 Mon Sep 17 00:00:00 2001 From: Jobobby04 Date: Sun, 6 Sep 2020 21:30:22 -0400 Subject: [PATCH] Add biometric lock times --- .../data/preference/PreferenceKeys.kt | 2 + .../data/preference/PreferencesHelper.kt | 2 + .../biometric/BiometricTimesAdapter.kt | 30 ++ .../biometric/BiometricTimesController.kt | 316 ++++++++++++++++++ .../biometric/BiometricTimesCreateDialog.kt | 75 +++++ .../biometric/BiometricTimesHolder.kt | 28 ++ .../category/biometric/BiometricTimesItem.kt | 64 ++++ .../biometric/BiometricTimesPresenter.kt | 87 +++++ .../ui/category/biometric/TimeRange.kt | 53 +++ .../ui/security/SecureActivityDelegate.kt | 19 +- .../ui/setting/SettingsSecurityController.kt | 14 + app/src/main/res/values/strings_sy.xml | 14 + 12 files changed, 702 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/category/biometric/BiometricTimesAdapter.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/category/biometric/BiometricTimesController.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/category/biometric/BiometricTimesCreateDialog.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/category/biometric/BiometricTimesHolder.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/category/biometric/BiometricTimesItem.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/category/biometric/BiometricTimesPresenter.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/category/biometric/TimeRange.kt diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt index 75ab0f9a8..9970fdaea 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt @@ -310,4 +310,6 @@ object PreferenceKeys { const val saveChaptersAsCBZLevel = "save_chapter_as_cbz_level" const val allowLocalSourceHiddenFolders = "allow_local_source_hidden_folders" + + const val biometricTimeRanges = "biometric_time_ranges" } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt index 0b21a5a83..081273b95 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt @@ -415,4 +415,6 @@ class PreferencesHelper(val context: Context) { fun saveChaptersAsCBZLevel() = flowPrefs.getInt(Keys.saveChaptersAsCBZLevel, 0) fun allowLocalSourceHiddenFolders() = flowPrefs.getBoolean(Keys.allowLocalSourceHiddenFolders, false) + + fun biometricTimeRanges() = flowPrefs.getStringSet(Keys.biometricTimeRanges, mutableSetOf()) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/biometric/BiometricTimesAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/biometric/BiometricTimesAdapter.kt new file mode 100644 index 000000000..7e278c39d --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/category/biometric/BiometricTimesAdapter.kt @@ -0,0 +1,30 @@ +package eu.kanade.tachiyomi.ui.category.biometric + +import eu.davidea.flexibleadapter.FlexibleAdapter + +/** + * Custom adapter for categories. + * + * @param controller The containing controller. + */ +class BiometricTimesAdapter(controller: BiometricTimesController) : + FlexibleAdapter(null, controller, true) { + + /** + * Clears the active selections from the list and the model. + */ + override fun clearSelection() { + super.clearSelection() + (0 until itemCount).forEach { getItem(it)?.isSelected = false } + } + + /** + * Toggles the selection of the given position. + * + * @param position The position to toggle. + */ + override fun toggleSelection(position: Int) { + super.toggleSelection(position) + getItem(position)?.isSelected = isSelected(position) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/biometric/BiometricTimesController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/biometric/BiometricTimesController.kt new file mode 100644 index 000000000..243815534 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/category/biometric/BiometricTimesController.kt @@ -0,0 +1,316 @@ +package eu.kanade.tachiyomi.ui.category.biometric + +import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.view.ActionMode +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton +import com.google.android.material.snackbar.Snackbar +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.SelectableAdapter +import eu.davidea.flexibleadapter.helpers.UndoHelper +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.databinding.CategoriesControllerBinding +import eu.kanade.tachiyomi.ui.base.controller.FabController +import eu.kanade.tachiyomi.ui.base.controller.NucleusController +import eu.kanade.tachiyomi.util.system.toast +import eu.kanade.tachiyomi.util.view.shrinkOnScroll +import kotlin.time.Duration +import kotlin.time.ExperimentalTime +import kotlinx.android.synthetic.main.main_activity.root_coordinator +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import reactivecircus.flowbinding.android.view.clicks + +/** + * Controller to manage the lock times for the biometric lock. + */ +@OptIn(ExperimentalTime::class) +class BiometricTimesController : + NucleusController(), + FabController, + ActionMode.Callback, + FlexibleAdapter.OnItemClickListener, + FlexibleAdapter.OnItemLongClickListener, + BiometricTimesCreateDialog.Listener, + UndoHelper.OnActionListener { + + /** + * Object used to show ActionMode toolbar. + */ + private var actionMode: ActionMode? = null + + /** + * Adapter containing biometric lock time items. + */ + private var adapter: BiometricTimesAdapter? = null + + private var actionFab: ExtendedFloatingActionButton? = null + private var actionFabScrollListener: RecyclerView.OnScrollListener? = null + + /** + * Undo helper used for restoring a deleted lock time. + */ + private var undoHelper: UndoHelper? = null + + /** + * Creates the presenter for this controller. Not to be manually called. + */ + override fun createPresenter() = BiometricTimesPresenter() + + /** + * Returns the toolbar title to show when this controller is attached. + */ + override fun getTitle(): String? { + return resources?.getString(R.string.biometric_lock_times) + } + + /** + * Returns the view of this controller. + * + * @param inflater The layout inflater to create the view from XML. + * @param container The parent view for this one. + */ + override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { + binding = CategoriesControllerBinding.inflate(inflater) + return binding.root + } + + /** + * Called after view inflation. Used to initialize the view. + * + * @param view The view of this controller. + */ + override fun onViewCreated(view: View) { + super.onViewCreated(view) + + adapter = BiometricTimesAdapter(this@BiometricTimesController) + binding.recycler.layoutManager = LinearLayoutManager(view.context) + binding.recycler.setHasFixedSize(true) + binding.recycler.adapter = adapter + adapter?.isPermanentDelete = false + + actionFabScrollListener = actionFab?.shrinkOnScroll(binding.recycler) + } + + override fun configureFab(fab: ExtendedFloatingActionButton) { + actionFab = fab + fab.setText(R.string.action_add) + fab.setIconResource(R.drawable.ic_add_24dp) + fab.clicks() + .onEach { + BiometricTimesCreateDialog(this@BiometricTimesController).showDialog(router, null) + } + .launchIn(scope) + } + + override fun cleanupFab(fab: ExtendedFloatingActionButton) { + actionFabScrollListener?.let { binding.recycler.removeOnScrollListener(it) } + actionFab = null + } + + /** + * Called when the view is being destroyed. Used to release references and remove callbacks. + * + * @param view The view of this controller. + */ + override fun onDestroyView(view: View) { + // Manually call callback to delete lock times if required + undoHelper?.onDeleteConfirmed(Snackbar.Callback.DISMISS_EVENT_MANUAL) + undoHelper = null + actionMode = null + adapter = null + super.onDestroyView(view) + } + + /** + * Called from the presenter when the biometric lock times are updated. + * + * @param biometricTimeItems The new list of lock times to display. + */ + fun setBiometricTimeItems(biometricTimeItems: List) { + actionMode?.finish() + adapter?.updateDataSet(biometricTimeItems) + if (biometricTimeItems.isNotEmpty()) { + binding.emptyView.hide() + val selected = biometricTimeItems.filter { it.isSelected } + if (selected.isNotEmpty()) { + selected.forEach { onItemLongClick(biometricTimeItems.indexOf(it)) } + } + } else { + binding.emptyView.show(R.string.biometric_lock_times_empty) + } + } + + /** + * Called when action mode is first created. The menu supplied will be used to generate action + * buttons for the action mode. + * + * @param mode ActionMode being created. + * @param menu Menu used to populate action buttons. + * @return true if the action mode should be created, false if entering this mode should be + * aborted. + */ + override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { + // Inflate menu. + mode.menuInflater.inflate(R.menu.category_selection, menu) + // Enable adapter multi selection. + adapter?.mode = SelectableAdapter.Mode.MULTI + return true + } + + /** + * Called to refresh an action mode's action menu whenever it is invalidated. + * + * @param mode ActionMode being prepared. + * @param menu Menu used to populate action buttons. + * @return true if the menu or action mode was updated, false otherwise. + */ + override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean { + val adapter = adapter ?: return false + val count = adapter.selectedItemCount + mode.title = count.toString() + + // Show edit button only when one item is selected + val editItem = mode.menu.findItem(R.id.action_edit) + editItem.isVisible = false + return true + } + + /** + * Called to report a user click on an action button. + * + * @param mode The current ActionMode. + * @param item The item that was clicked. + * @return true if this callback handled the event, false if the standard MenuItem invocation + * should continue. + */ + override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean { + val adapter = adapter ?: return false + + when (item.itemId) { + R.id.action_delete -> { + undoHelper = UndoHelper(adapter, this) + undoHelper?.start( + adapter.selectedPositions, activity!!.root_coordinator, + R.string.biometric_lock_time_deleted_snack, R.string.action_undo, 3000 + ) + + mode.finish() + } + else -> return false + } + return true + } + + /** + * Called when an action mode is about to be exited and destroyed. + * + * @param mode The current ActionMode being destroyed. + */ + override fun onDestroyActionMode(mode: ActionMode) { + // Reset adapter to single selection + adapter?.mode = SelectableAdapter.Mode.IDLE + adapter?.clearSelection() + actionMode = null + } + + /** + * Called when an item in the list is clicked. + * + * @param position The position of the clicked item. + * @return true if this click should enable selection mode. + */ + override fun onItemClick(view: View, position: Int): Boolean { + // Check if action mode is initialized and selected item exist. + return if (actionMode != null && position != RecyclerView.NO_POSITION) { + toggleSelection(position) + true + } else { + false + } + } + + /** + * Called when an item in the list is long clicked. + * + * @param position The position of the clicked item. + */ + override fun onItemLongClick(position: Int) { + val activity = activity as? AppCompatActivity ?: return + + // Check if action mode is initialized. + if (actionMode == null) { + // Initialize action mode + actionMode = activity.startSupportActionMode(this) + } + + // Set item as selected + toggleSelection(position) + } + + /** + * Toggle the selection state of an item. + * If the item was the last one in the selection and is unselected, the ActionMode is finished. + * + * @param position The position of the item to toggle. + */ + private fun toggleSelection(position: Int) { + val adapter = adapter ?: return + + // Mark the position selected + adapter.toggleSelection(position) + + if (adapter.selectedItemCount == 0) { + actionMode?.finish() + } else { + actionMode?.invalidate() + } + } + + /** + * Called when the undo action is clicked in the snackbar. + * + * @param action The action performed. + */ + override fun onActionCanceled(action: Int, positions: MutableList?) { + adapter?.restoreDeletedItems() + undoHelper = null + } + + /** + * Called when the time to restore the items expires. + * + * @param action The action performed. + * @param event The event that triggered the action + */ + override fun onActionConfirmed(action: Int, event: Int) { + val adapter = adapter ?: return + presenter.deleteTimeRanges(adapter.deletedItems.map { it.timeRange }) + undoHelper = null + } + + /** + * Called from the presenter when a time range conflicts with another. + */ + fun onTimeRangeConflictsError() { + activity?.toast(R.string.biometric_lock_time_conflicts) + } + + override fun startNextDialog(startTime: Duration?) { + if (startTime != null) { + BiometricTimesCreateDialog(this@BiometricTimesController, startTime).showDialog(router, null) + } else activity?.toast(R.string.biometric_lock_invalid_time_selected) + } + + override fun createTimeRange(startTime: Duration?, endTime: Duration?) { + if (startTime != null && endTime != null) { + presenter.createTimeRange(TimeRange(startTime, endTime)) + } else activity?.toast(R.string.biometric_lock_invalid_time_selected) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/biometric/BiometricTimesCreateDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/biometric/BiometricTimesCreateDialog.kt new file mode 100644 index 000000000..23022c518 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/category/biometric/BiometricTimesCreateDialog.kt @@ -0,0 +1,75 @@ +package eu.kanade.tachiyomi.ui.category.biometric + +import android.app.Dialog +import android.os.Bundle +import com.afollestad.materialdialogs.MaterialDialog +import com.afollestad.materialdialogs.datetime.timePicker +import com.bluelinelabs.conductor.Controller +import com.elvishew.xlog.XLog +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.ui.base.controller.DialogController +import java.util.Calendar +import kotlin.time.Duration +import kotlin.time.ExperimentalTime +import kotlin.time.hours +import kotlin.time.minutes + +/** + * Dialog to create a new category for the library. + */ +@OptIn(ExperimentalTime::class) +class BiometricTimesCreateDialog(bundle: Bundle? = null) : DialogController(bundle) + where T : Controller, T : BiometricTimesCreateDialog.Listener { + + /** + * Name of the new category. Value updated with each input from the user. + */ + private var startTime: Duration? = null + + private var endTime: Duration? = null + + constructor(target: T) : this() { + targetController = target + } + + constructor(target: T, startTime: Duration) : this() { + targetController = target + this.startTime = startTime + } + + /** + * Called when creating the dialog for this controller. + * + * @param savedViewState The saved state of this dialog. + * @return a new dialog instance. + */ + override fun onCreateDialog(savedViewState: Bundle?): Dialog { + return MaterialDialog(activity!!) + .title(if (startTime == null) R.string.biometric_lock_start_time else R.string.biometric_lock_end_time) + .timePicker(show24HoursView = false) { _, datetime -> + val hour = datetime.get(Calendar.HOUR_OF_DAY) + XLog.nst().d(hour) + val minute = datetime.get(Calendar.MINUTE) + XLog.nst().d(minute) + if (hour !in 0..24 || minute !in 0..60) return@timePicker + if (startTime != null) { + endTime = hour.hours + minute.minutes + } else { + startTime = hour.hours + minute.minutes + } + } + .negativeButton(android.R.string.cancel) + .positiveButton(android.R.string.ok) { + if (endTime != null) { + (targetController as? Listener)?.createTimeRange(startTime, endTime) + } else { + (targetController as? Listener)?.startNextDialog(startTime) + } + } + } + + interface Listener { + fun startNextDialog(startTime: Duration?) + fun createTimeRange(startTime: Duration?, endTime: Duration?) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/biometric/BiometricTimesHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/biometric/BiometricTimesHolder.kt new file mode 100644 index 000000000..b9abdc8cb --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/category/biometric/BiometricTimesHolder.kt @@ -0,0 +1,28 @@ +package eu.kanade.tachiyomi.ui.category.biometric + +import android.view.View +import androidx.core.view.isVisible +import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder +import kotlin.time.ExperimentalTime +import kotlinx.android.synthetic.main.categories_item.reorder +import kotlinx.android.synthetic.main.categories_item.title + +/** + * Holder used to display category items. + * + * @param view The view used by category items. + * @param adapter The adapter containing this holder. + */ +class BiometricTimesHolder(view: View, val adapter: BiometricTimesAdapter) : BaseFlexibleViewHolder(view, adapter) { + /** + * Binds this holder with the given category. + * + * @param timeRange The category to bind. + */ + @OptIn(ExperimentalTime::class) + fun bind(timeRange: TimeRange) { + // Set capitalized title. + title.text = timeRange.getFormattedString(itemView.context) + reorder.isVisible = false + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/biometric/BiometricTimesItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/biometric/BiometricTimesItem.kt new file mode 100644 index 000000000..61362735c --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/category/biometric/BiometricTimesItem.kt @@ -0,0 +1,64 @@ +package eu.kanade.tachiyomi.ui.category.biometric + +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.items.AbstractFlexibleItem +import eu.davidea.flexibleadapter.items.IFlexible +import eu.kanade.tachiyomi.R +import kotlin.time.ExperimentalTime + +/** + * Category item for a recycler view. + */ +@OptIn(ExperimentalTime::class) +class BiometricTimesItem(val timeRange: TimeRange) : AbstractFlexibleItem() { + + /** + * Whether this item is currently selected. + */ + var isSelected = false + + /** + * Returns the layout resource for this item. + */ + override fun getLayoutRes(): Int { + return R.layout.categories_item + } + + /** + * Returns a new view holder for this item. + * + * @param view The view of this item. + * @param adapter The adapter of this item. + */ + override fun createViewHolder(view: View, adapter: FlexibleAdapter>): BiometricTimesHolder { + return BiometricTimesHolder(view, adapter as BiometricTimesAdapter) + } + + /** + * Binds the given view holder with this item. + * + * @param adapter The adapter of this item. + * @param holder The holder to bind. + * @param position The position of this item in the adapter. + * @param payloads List of partial changes. + */ + override fun bindViewHolder( + adapter: FlexibleAdapter>, + holder: BiometricTimesHolder, + position: Int, + payloads: List? + ) { + holder.bind(timeRange) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + return false + } + + override fun hashCode(): Int { + return timeRange.hashCode() + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/biometric/BiometricTimesPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/biometric/BiometricTimesPresenter.kt new file mode 100644 index 000000000..f399100bf --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/category/biometric/BiometricTimesPresenter.kt @@ -0,0 +1,87 @@ +package eu.kanade.tachiyomi.ui.category.biometric + +import android.os.Bundle +import com.elvishew.xlog.XLog +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.data.preference.plusAssign +import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter +import kotlin.time.ExperimentalTime +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import rx.Observable +import rx.android.schedulers.AndroidSchedulers +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +/** + * Presenter of [BiometricTimesController]. Used to manage the categories of the library. + */ +@OptIn(ExperimentalTime::class) +class BiometricTimesPresenter : BasePresenter() { + + /** + * List containing categories. + */ + private var timeRanges: List = emptyList() + + val preferences: PreferencesHelper = Injekt.get() + + val scope = CoroutineScope(Job() + Dispatchers.Main) + + /** + * Called when the presenter is created. + * + * @param savedState The saved state of this presenter. + */ + override fun onCreate(savedState: Bundle?) { + super.onCreate(savedState) + + preferences.biometricTimeRanges().asFlow().onEach { prefTimeRanges -> + timeRanges = prefTimeRanges.toList() + .mapNotNull { TimeRange.fromPreferenceString(it) }.onEach { XLog.nst().d(it) } + + Observable.just(timeRanges) + .map { it.map(::BiometricTimesItem) } + .observeOn(AndroidSchedulers.mainThread()) + .subscribeLatestCache(BiometricTimesController::setBiometricTimeItems) + }.launchIn(scope) + } + + /** + * Creates and adds a new category to the database. + * + * @param name The name of the category to create. + */ + fun createTimeRange(timeRange: TimeRange) { + // Do not allow duplicate categories. + if (timeRangeConflicts(timeRange)) { + Observable.just(Unit).subscribeFirst({ view, _ -> view.onTimeRangeConflictsError() }) + return + } + + XLog.nst().d(timeRange) + + preferences.biometricTimeRanges() += timeRange.toPreferenceString() + } + + /** + * Deletes the given categories from the database. + * + * @param timeRanges The list of categories to delete. + */ + fun deleteTimeRanges(timeRanges: List) { + preferences.biometricTimeRanges().set( + this.timeRanges.filterNot { it in timeRanges }.map { it.toPreferenceString() }.toSet() + ) + } + + /** + * Returns true if a category with the given name already exists. + */ + private fun timeRangeConflicts(timeRange: TimeRange): Boolean { + return timeRanges.any { timeRange.conflictsWith(it) } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/biometric/TimeRange.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/biometric/TimeRange.kt new file mode 100644 index 000000000..b04cd5ef7 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/category/biometric/TimeRange.kt @@ -0,0 +1,53 @@ +package eu.kanade.tachiyomi.ui.category.biometric + +import android.content.Context +import android.text.format.DateFormat +import com.elvishew.xlog.XLog +import java.util.Calendar +import java.util.Date +import java.util.SimpleTimeZone +import kotlin.math.floor +import kotlin.math.roundToInt +import kotlin.time.Duration +import kotlin.time.ExperimentalTime +import kotlin.time.hours +import kotlin.time.milliseconds +import kotlin.time.minutes + +@ExperimentalTime +data class TimeRange(val startTime: Duration, val endTime: Duration) { + override fun toString(): String { + val startHour = floor(startTime.inHours).roundToInt() + val startMinute = (startTime - floor(startTime.inHours).hours).inMinutes.roundToInt() + val endHour = floor(endTime.inHours).roundToInt() + val endMinute = (endTime - floor(endTime.inHours).hours).inMinutes.roundToInt() + return String.format("%02d:%02d - %02d:%02d", startHour, startMinute, endHour, endMinute) + } + + fun getFormattedString(context: Context): String { + val startDate = Date(startTime.toLongMilliseconds()) + val endDate = Date(endTime.toLongMilliseconds()) + val format = DateFormat.getTimeFormat(context) + format.timeZone = SimpleTimeZone(0, "UTC") + + return format.format(startDate) + " - " + format.format(endDate) + } + + fun toPreferenceString(): String { + return "${startTime.inMinutes},${endTime.inMinutes}" + } + + fun conflictsWith(other: TimeRange): Boolean { + return startTime in other.startTime..other.endTime || endTime in other.startTime..other.endTime + } + + companion object { + fun fromPreferenceString(timeRange: String): TimeRange? { + return timeRange.split(",").mapNotNull { it.toDoubleOrNull() }.let { + if (it.size != 2) null else { + TimeRange(it[0].minutes, it[1].minutes) + } + } + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/security/SecureActivityDelegate.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/security/SecureActivityDelegate.kt index a34a13819..bbda30b81 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/security/SecureActivityDelegate.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/security/SecureActivityDelegate.kt @@ -6,7 +6,12 @@ import androidx.biometric.BiometricManager import androidx.fragment.app.FragmentActivity import androidx.lifecycle.lifecycleScope import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.ui.category.biometric.TimeRange +import java.util.Calendar import java.util.Date +import kotlin.time.ExperimentalTime +import kotlin.time.hours +import kotlin.time.minutes import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import uy.kohesive.injekt.injectLazy @@ -19,7 +24,10 @@ class SecureActivityDelegate(private val activity: FragmentActivity) { preferences.secureScreen().asFlow() .onEach { if (it) { - activity.window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE) + activity.window.setFlags( + WindowManager.LayoutParams.FLAG_SECURE, + WindowManager.LayoutParams.FLAG_SECURE + ) } else { activity.window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE) } @@ -40,12 +48,19 @@ class SecureActivityDelegate(private val activity: FragmentActivity) { } } + @OptIn(ExperimentalTime::class) private fun isAppLocked(): Boolean { return locked && ( preferences.lockAppAfter().get() <= 0 || Date().time >= preferences.lastAppUnlock().get() + 60 * 1000 * preferences.lockAppAfter().get() - ) + ) && preferences.biometricTimeRanges().get().mapNotNull { TimeRange.fromPreferenceString(it) }.let { timeRanges -> + if (timeRanges.isNotEmpty()) { + val today: Calendar = Calendar.getInstance() + val now = today.get(Calendar.HOUR_OF_DAY).hours + today.get(Calendar.MINUTE).minutes + timeRanges.any { now in it.startTime..it.endTime } + } else true + } } companion object { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsSecurityController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsSecurityController.kt index 6fb3a1e66..fd2cae681 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsSecurityController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsSecurityController.kt @@ -5,8 +5,12 @@ import androidx.preference.PreferenceScreen import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys import eu.kanade.tachiyomi.data.preference.asImmediateFlow +import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction +import eu.kanade.tachiyomi.ui.category.biometric.BiometricTimesController import eu.kanade.tachiyomi.util.preference.defaultValue import eu.kanade.tachiyomi.util.preference.intListPreference +import eu.kanade.tachiyomi.util.preference.onClick +import eu.kanade.tachiyomi.util.preference.preference import eu.kanade.tachiyomi.util.preference.summaryRes import eu.kanade.tachiyomi.util.preference.switchPreference import eu.kanade.tachiyomi.util.preference.titleRes @@ -54,5 +58,15 @@ class SettingsSecurityController : SettingsController() { titleRes = R.string.hide_notification_content defaultValue = false } + preference { + titleRes = R.string.action_edit_biometric_lock_times + + val timeRanges = preferences.biometricTimeRanges().get().count() + summary = context.resources.getQuantityString(R.plurals.num_lock_times, timeRanges, timeRanges) + + onClick { + router.pushController(BiometricTimesController().withFadeTransaction()) + } + } } } diff --git a/app/src/main/res/values/strings_sy.xml b/app/src/main/res/values/strings_sy.xml index dea0d5c98..4e565f55a 100644 --- a/app/src/main/res/values/strings_sy.xml +++ b/app/src/main/res/values/strings_sy.xml @@ -187,6 +187,20 @@ Save Chapters as CBZ CBZ Compression level + + Biometric lock times + Edit lock times + You have biometric lock times. Tap the plus button to create one. + A lock time conflicts with one that already exists! + + %d lock time + %d lock times + + Enter starting time + Enter end time + Biometric lock time completed + Invalid time selected + Download threads Higher values can speed up image downloading significantly, but can also trigger bans. Recommended value is 2 or 3. Current value is: %s