Compare commits

..

31 Commits

Author SHA1 Message Date
8b2adbf301 GH Actions: Remove API 23 2023-05-27 13:26:11 -05:00
88cc3a2a12 Update CHANGELOG 2023-05-26 20:12:06 -05:00
26526a71a9 Update test screenshots 2023-05-26 19:56:01 -05:00
11eb3713e5 Reschedule reminders on resume 2023-05-26 19:55:53 -05:00
1df9cc7664 Widgets: Remove option to create StackWidgets
StackWidgets have been unfortunately been very unreliable on multiple phones,
and fixing it does not appear to be simple. This commit removes the ability
to create new StackWidgets, but existing ones should remain functional.
2023-05-01 18:52:53 -05:00
b76da35752 Widgets: Increase corner radius to match Android 12 2023-03-24 05:24:58 -05:00
abead88ceb GH Actions: Fix build.sh 2023-03-23 04:48:40 -05:00
908eb4ac99 Convert NumberDialog to AppCompatDialogFragment; remove unused classes 2023-03-18 05:04:43 -05:00
71a05d598a CheckmarkDialog: Switch to AppCompatDialogFragment
Fixes issues with the soft keyboard covering the popup.
2023-01-30 05:59:42 -06:00
2131fb3a3d EntryList: Copy notes from original entries
Fixes #1566
2023-01-24 05:59:23 -06:00
1470dcd560 Remove toggle delay 2023-01-23 03:50:38 -06:00
471f977209 Replace some incorrect usages of getToday by getTodayWithOffset
Fixes #1541
2022-10-22 17:11:59 -05:00
2ba5f5fb98 Dismiss current dialog onPause
Fixes #1545
2022-10-22 16:19:49 -05:00
4de67bd27a GH Actions: Remove API 27 2022-10-22 16:01:36 -05:00
0bb82a48a5 NumberPopup: Accept comma (instead of dot) in certain locales
Fixes #1532
2022-10-22 15:44:30 -05:00
d5a5273607 Bump version to 2.1.2 2022-10-22 15:25:42 -05:00
40a4d254f5 Merge branch 'hotfix/2.1.1' 2022-09-24 19:40:04 -05:00
177d01edd9 Fix Tasker plugin; bump version to 2.1.1
Fixes #1503
2022-09-24 19:25:42 -05:00
ec1f0c5356 StackWidget: Remove unused remoteViews; fix invalid check 2022-09-09 06:02:57 -05:00
961fb7618f splitLongs: Handle NumberFormatException 2022-09-09 05:39:15 -05:00
11f726064a StackWidget: Avoid storing RemoteViews reference 2022-09-09 05:39:15 -05:00
abced92a07 Update CHANGELOG 2022-09-08 20:09:47 -05:00
cheer
eeacc5eef8 Support for Android 13 Themed Icons (#1497) 2022-09-08 20:08:12 -05:00
Eduardo Esparza
16c65f19fd fix marker scaling for numerical habits in frequency display (#1489) 2022-09-08 20:08:05 -05:00
fc402fd81b Minor fix to BarChart 2022-09-03 20:26:21 -05:00
f7c6bc716c Fix ListHabitsScreen.showColorPicker 2022-09-03 19:52:13 -05:00
2535347d5a Always update widgets on launch
Helps mitigate situations where the widgets mysteriously fail to render.
The user can always force a refresh by opening the app.
2022-09-03 19:45:31 -05:00
13af054214 Use different request codes for showNumberPicker pendingIntent
Fixes #1468
2022-09-03 18:46:52 -05:00
fcbb586e80 Use pending intent templates on StackWidget
Fixes #1463
2022-09-03 17:51:55 -05:00
Jakub Kalinowski
428bf42e79 Reimplementing the multiple popups handling in the new popup solution. (#1454)
Co-authored-by: Jakub Kalinowski <kalj@netcompany.com>
2022-08-14 13:25:01 -05:00
838e13f30c Prevent crashes in AutoBackup from blocking app launch 2022-08-13 07:25:12 -05:00
62 changed files with 480 additions and 420 deletions

View File

@@ -17,7 +17,7 @@ jobs:
run: ./build.sh build
- name: Run Android tests
run: ./build.sh android-tests-parallel 23 24 25 26 27 28 30 31
run: ./build.sh android-tests-parallel 24 25 26 28 30 31
- name: Upload artifacts
if: always()

View File

@@ -1,6 +1,25 @@
# Changelog
## [2.1.0] -- Unreleased
## [2.1.2] -- 2023-05-26
### Fixed
- Fix bug that caused widget to enter checkmark on wrong date (@iSoron, #1541)
- Fix widget corners on Android 12 (@iSoron)
- Fix bug that caused notes to be lost when editing a checkmark (@iSoron, #1566)
- Prevent soft keyboard from covering entry popup (@iSoron)
- Accept comma (instead of dot) in certain locales (@iSoron)
### Changed
- Remove update delay after entering a checkmark (@iSoron)
### Removed
- Remove stack widgets (@iSoron)
## [2.1.1] -- 2022-09-24
### Fixed
- Fix Tasker plugin (@iSoron, #1503)
## [2.1.0] -- 2022-09-10
### Added
- Allow user to add notes to specific dates (@vbh, #1103)
- Allow user to track "at most" numerical habits (@KristianTashkov, #1101)
@@ -9,6 +28,7 @@
- Improve number picker (@hiqua, @iSoron, #1082, #1370)
- Add new checkmark and number picker (@iSoron, #1370)
- Allow user to import numerical habits from HabitBull (@hiqua, #1278)
- Add support for Android 13 themed icons (@cheeeeer, #1497)
### Removed
- Hide snooze button Android 12 notifications (@hiqua, #1226)
@@ -28,6 +48,7 @@
- Fix small issues in calendar chart (@kalina559, #1314)
- Resort habit list after edit (@hiqua, #1350)
- Fix marker scaling in frequency display (@eduebernal, #1425)
- Fix widgets not working correctly on API 33 (@iSoron, #1488)
### Refactoring & Testing
- Replace raster icons by vector assets (@kalina559)

View File

@@ -217,20 +217,28 @@ android_test_parallel() {
(
LOG=build/android-test-$API.log
log_info "API $API: Running tests..."
if android_test $API 1>$LOG 2>&1; then
android_test $API 1>$LOG 2>&1
ret_code=$?
if [ $ret_code = 0 ]; then
log_info "API $API: Passed"
else
log_error "API $API: Failed"
fi
pkill -9 -f ${AVD_PREFIX}${API}
exit $ret_code
)&
PIDS+=" $!"
done
# Check exit codes
RET_CODE=0
success=0
for pid in $PIDS; do
wait $pid || RET_CODE=1
wait $pid
ret_code=$?
if [ $ret_code != 0 ]; then
success=1
fi
echo pid=$pid ret_code=$ret_code success=$success
done
# Print all logs
@@ -240,7 +248,7 @@ android_test_parallel() {
echo "::endgroup::"
done
return $RET_CODE
return $success
}
android_build() {

View File

@@ -35,8 +35,8 @@ android {
compileSdk = 31
defaultConfig {
versionCode = 20100
versionName = "2.1.0"
versionCode = 20102
versionName = "2.1.2"
minSdk = 23
targetSdk = 31
applicationId = "org.isoron.uhabits"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -44,7 +44,7 @@ class EntryButtonViewTest : BaseViewTest() {
view = component.getEntryButtonViewFactory().create().apply {
value = Entry.NO
color = PaletteUtils.getAndroidTestColor(5)
onToggle = { _, _, _ -> toggled = true }
onToggle = { _, _ -> toggled = true }
onEdit = { edited = true }
}
measureView(view, dpToPixels(48), dpToPixels(48))

View File

@@ -77,7 +77,7 @@ class EntryPanelViewTest : BaseViewTest() {
@Test
fun testToggle() {
val timestamps = mutableListOf<Timestamp>()
view.onToggle = { t, _, _, _ -> timestamps.add(t) }
view.onToggle = { t, _, _ -> timestamps.add(t) }
view.buttons[0].performLongClick()
view.buttons[2].performLongClick()
view.buttons[3].performLongClick()
@@ -88,7 +88,7 @@ class EntryPanelViewTest : BaseViewTest() {
fun testToggle_withOffset() {
val timestamps = mutableListOf<Timestamp>()
view.dataOffset = 3
view.onToggle = { t, _, _, _ -> timestamps += t }
view.onToggle = { t, _, _ -> timestamps += t }
view.buttons[0].performLongClick()
view.buttons[2].performLongClick()
view.buttons[3].performLongClick()

View File

@@ -36,6 +36,7 @@ class CheckmarkWidgetViewTest : BaseViewTest() {
@Before
override fun setUp() {
super.setUp()
similarityCutoff = 0.00025
setTheme(R.style.WidgetTheme)
val habit = fixtures.createShortHabit()
val computedEntries = habit.computedEntries

View File

@@ -270,7 +270,7 @@
<!-- Locale/Tasker -->
<receiver
android:name=".automation.FireSettingReceiver"
android:exported="false">
android:exported="true">
<intent-filter>
<action android:name="com.twofortyfouram.locale.intent.action.FIRE_SETTING" />
</intent-filter>

View File

@@ -20,110 +20,60 @@
package org.isoron.uhabits.activities.common.dialogs
import android.app.Dialog
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE
import androidx.appcompat.app.AppCompatDialogFragment
import org.isoron.uhabits.HabitsApplication
import org.isoron.uhabits.R
import org.isoron.uhabits.activities.common.dialogs.MultipleDialogsHandler.Companion.dismissCurrent
import org.isoron.uhabits.core.models.Entry.Companion.NO
import org.isoron.uhabits.core.models.Entry.Companion.SKIP
import org.isoron.uhabits.core.models.Entry.Companion.UNKNOWN
import org.isoron.uhabits.core.models.Entry.Companion.YES_AUTO
import org.isoron.uhabits.core.models.Entry.Companion.YES_MANUAL
import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.databinding.CheckmarkPopupBinding
import org.isoron.uhabits.utils.InterfaceUtils.getFontAwesome
import org.isoron.uhabits.utils.dimBehind
import org.isoron.uhabits.utils.dp
import org.isoron.uhabits.utils.sres
const val POPUP_WIDTH = 4 * 48f + 16f
const val POPUP_HEIGHT = 48f * 2.5f + 8f
class CheckmarkPopup(
private val context: Context,
private val color: Int,
private var notes: String,
private var value: Int,
private val prefs: Preferences,
private val anchor: View,
) {
class CheckmarkDialog : AppCompatDialogFragment() {
var onToggle: (Int, String) -> Unit = { _, _ -> }
private lateinit var dialog: Dialog
private val view = CheckmarkPopupBinding.inflate(LayoutInflater.from(context)).apply {
// Required for round corners
container.clipToOutline = true
}
init {
view.booleanButtons.visibility = VISIBLE
initColors()
initTypefaces()
hideDisabledButtons()
populate()
}
private fun initColors() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val appComponent = (requireActivity().application as HabitsApplication).component
val prefs = appComponent.preferences
val view = CheckmarkPopupBinding.inflate(LayoutInflater.from(context))
arrayOf(view.yesBtn, view.skipBtn).forEach {
it.setTextColor(color)
it.setTextColor(requireArguments().getInt("color"))
}
arrayOf(view.noBtn, view.unknownBtn).forEach {
it.setTextColor(view.root.sres.getColor(R.attr.contrast60))
}
}
private fun initTypefaces() {
arrayOf(view.yesBtn, view.noBtn, view.skipBtn, view.unknownBtn).forEach {
it.typeface = getFontAwesome(context)
it.typeface = getFontAwesome(requireContext())
}
}
private fun hideDisabledButtons() {
view.notes.setText(requireArguments().getString("notes")!!)
if (!prefs.isSkipEnabled) view.skipBtn.visibility = GONE
if (!prefs.areQuestionMarksEnabled) view.unknownBtn.visibility = GONE
}
private fun populate() {
val selectedBtn = when (value) {
YES_MANUAL -> view.yesBtn
YES_AUTO -> view.noBtn
NO -> view.noBtn
UNKNOWN -> if (prefs.areQuestionMarksEnabled) view.unknownBtn else view.noBtn
SKIP -> if (prefs.isSkipEnabled) view.skipBtn else view.noBtn
else -> null
}
view.notes.setText(notes)
}
fun show() {
dialog = Dialog(context, android.R.style.Theme_NoTitleBar)
view.booleanButtons.visibility = VISIBLE
val dialog = Dialog(requireContext())
dialog.setContentView(view.root)
dialog.window?.apply {
setLayout(
view.root.dp(POPUP_WIDTH).toInt(),
view.root.dp(POPUP_HEIGHT).toInt()
)
setBackgroundDrawableResource(android.R.color.transparent)
}
fun onClick(v: Int) {
this.value = v
save()
val notes = view.notes.text.toString().trim()
onToggle(v, notes)
requireDialog().dismiss()
}
view.yesBtn.setOnClickListener { onClick(YES_MANUAL) }
view.noBtn.setOnClickListener { onClick(NO) }
view.skipBtn.setOnClickListener { onClick(SKIP) }
view.unknownBtn.setOnClickListener { onClick(UNKNOWN) }
dialog.setCanceledOnTouchOutside(true)
dialog.dimBehind()
dialog.dismissCurrent()
dialog.show()
view.notes.setOnEditorActionListener { v, actionId, event ->
onClick(requireArguments().getInt("value"))
true
}
fun save() {
onToggle(value, view.notes.text.toString().trim())
dialog.dismiss()
return dialog
}
}

View File

@@ -18,10 +18,7 @@
*/
package org.isoron.uhabits.activities.common.dialogs
import android.app.Dialog
import android.os.Bundle
import com.android.colorpicker.ColorPickerDialog
import org.isoron.uhabits.activities.common.dialogs.MultipleDialogsHandler.Companion.dismissCurrent
import org.isoron.uhabits.core.ui.callbacks.OnColorPickedCallback
import org.isoron.uhabits.utils.toPaletteColor
@@ -31,14 +28,8 @@ import org.isoron.uhabits.utils.toPaletteColor
class ColorPickerDialog : ColorPickerDialog() {
fun setListener(callback: OnColorPickedCallback) {
super.setOnColorSelectedListener { c: Int ->
val pc = c.toPaletteColor(requireContext())
val pc = c.toPaletteColor(context!!)
callback.onColorPicked(pc)
}
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val dialog = super.onCreateDialog(savedInstanceState)
dialog.dismissCurrent()
return dialog
}
}

View File

@@ -22,7 +22,6 @@ import android.content.Context
import android.content.DialogInterface
import androidx.appcompat.app.AlertDialog
import org.isoron.uhabits.R
import org.isoron.uhabits.activities.common.dialogs.MultipleDialogsHandler.Companion.dismissCurrent
import org.isoron.uhabits.core.ui.callbacks.OnConfirmedCallback
import org.isoron.uhabits.inject.ActivityContext
@@ -46,7 +45,5 @@ class ConfirmDeleteDialog(
BUTTON_NEGATIVE,
res.getString(R.string.no)
) { dialog: DialogInterface?, which: Int -> }
this.dismissCurrent()
}
}

View File

@@ -31,7 +31,6 @@ import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatDialogFragment
import kotlinx.android.synthetic.main.frequency_picker_dialog.view.*
import org.isoron.uhabits.R
import org.isoron.uhabits.activities.common.dialogs.MultipleDialogsHandler.Companion.dismissCurrent
class FrequencyPickerDialog(
var freqNumerator: Int,
@@ -112,12 +111,10 @@ class FrequencyPickerDialog(
if (hasFocus) check(contentView.xTimesPerYDaysRadioButton)
}
val dialog = AlertDialog.Builder(requireActivity())
return AlertDialog.Builder(requireActivity())
.setView(contentView)
.setPositiveButton(R.string.save) { _, _ -> onSaveClicked() }
.create()
dialog.dismissCurrent()
return dialog
}
private fun addBeforeAfterText(
@@ -152,8 +149,10 @@ class FrequencyPickerDialog(
}
contentView.xTimesPerYDaysRadioButton.isChecked -> {
if (contentView.xTimesPerYDaysXTextView.text.isNotEmpty() && contentView.xTimesPerYDaysYTextView.text.isNotEmpty()) {
numerator = Integer.parseInt(contentView.xTimesPerYDaysXTextView.text.toString())
denominator = Integer.parseInt(contentView.xTimesPerYDaysYTextView.text.toString())
numerator =
Integer.parseInt(contentView.xTimesPerYDaysXTextView.text.toString())
denominator =
Integer.parseInt(contentView.xTimesPerYDaysYTextView.text.toString())
}
}
else -> {

View File

@@ -51,12 +51,12 @@ class HistoryEditorDialog : AppCompatDialogFragment(), CommandRunner.Listener {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
clearCurrentDialog()
val component = (requireActivity().application as HabitsApplication).component
val component = (activity!!.application as HabitsApplication).component
commandRunner = component.commandRunner
habit = component.habitList.getById(requireArguments().getLong("habit"))!!
habit = component.habitList.getById(arguments!!.getLong("habit"))!!
preferences = component.preferences
val themeSwitcher = AndroidThemeSwitcher(requireActivity(), preferences)
val themeSwitcher = AndroidThemeSwitcher(activity!!, preferences)
themeSwitcher.apply()
chart = HistoryChart(
@@ -71,10 +71,10 @@ class HistoryEditorDialog : AppCompatDialogFragment(), CommandRunner.Listener {
onDateClickedListener = onDateClickedListener ?: object : OnDateClickedListener {},
padding = 10.0,
)
dataView = AndroidDataView(requireContext(), null)
dataView = AndroidDataView(context!!, null)
dataView.view = chart!!
val dialog = Dialog(requireContext()).apply {
val dialog = Dialog(context!!).apply {
val metrics = resources.displayMetrics
val maxHeight = resources.getDimensionPixelSize(R.dimen.history_editor_max_height)
setContentView(dataView)

View File

@@ -1,15 +0,0 @@
package org.isoron.uhabits.activities.common.dialogs
import android.app.Dialog
import java.lang.ref.WeakReference
class MultipleDialogsHandler {
companion object {
var currentDialog: WeakReference<Dialog> = WeakReference(null)
fun Dialog.dismissCurrent() {
currentDialog.get()?.dismiss()
currentDialog = WeakReference(this)
}
}
}

View File

@@ -0,0 +1,104 @@
package org.isoron.uhabits.activities.common.dialogs
import android.app.Dialog
import android.os.Bundle
import android.text.method.DigitsKeyListener
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import androidx.appcompat.app.AppCompatDialogFragment
import org.isoron.uhabits.HabitsApplication
import org.isoron.uhabits.R
import org.isoron.uhabits.core.models.Entry
import org.isoron.uhabits.databinding.CheckmarkPopupBinding
import org.isoron.uhabits.utils.InterfaceUtils
import org.isoron.uhabits.utils.requestFocusWithKeyboard
import org.isoron.uhabits.utils.sres
import java.text.DecimalFormat
import java.text.DecimalFormatSymbols
import java.text.NumberFormat
import java.text.ParseException
class NumberDialog : AppCompatDialogFragment() {
var onToggle: (Double, String) -> Unit = { _, _ -> }
var onDismiss: () -> Unit = {}
private var originalNotes: String = ""
private var originalValue: Double = 0.0
private lateinit var view: CheckmarkPopupBinding
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val appComponent = (requireActivity().application as HabitsApplication).component
val prefs = appComponent.preferences
view = CheckmarkPopupBinding.inflate(LayoutInflater.from(context))
arrayOf(view.yesBtn, view.skipBtn).forEach {
it.setTextColor(requireArguments().getInt("color"))
}
arrayOf(view.noBtn, view.unknownBtn).forEach {
it.setTextColor(view.root.sres.getColor(R.attr.contrast60))
}
arrayOf(view.yesBtn, view.noBtn, view.skipBtn, view.unknownBtn).forEach {
it.typeface = InterfaceUtils.getFontAwesome(requireContext())
}
if (!prefs.isSkipEnabled) view.skipBtnNumber.visibility = View.GONE
view.numberButtons.visibility = View.VISIBLE
fixDecimalSeparator(view)
originalNotes = requireArguments().getString("notes")!!
originalValue = requireArguments().getDouble("value")
view.notes.setText(originalNotes)
view.value.setText(
when {
originalValue < 0.01 -> "0"
else -> DecimalFormat("#.##").format(originalValue)
}
)
view.value.setOnKeyListener { _, keyCode, event ->
if (event.action == MotionEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_ENTER) {
save()
return@setOnKeyListener true
}
return@setOnKeyListener false
}
view.saveBtn.setOnClickListener {
save()
}
view.skipBtnNumber.setOnClickListener {
view.value.setText((Entry.SKIP.toDouble() / 1000).toString())
save()
}
view.notes.setOnEditorActionListener { v, actionId, event ->
save()
true
}
view.value.requestFocusWithKeyboard()
val dialog = Dialog(requireContext())
dialog.setContentView(view.root)
dialog.window?.apply {
setBackgroundDrawableResource(android.R.color.transparent)
}
dialog.setOnDismissListener { onDismiss() }
return dialog
}
private fun fixDecimalSeparator(view: CheckmarkPopupBinding) {
// https://stackoverflow.com/a/34256139
val separator = DecimalFormatSymbols.getInstance().decimalSeparator
view.value.keyListener = DigitsKeyListener.getInstance("0123456789$separator")
}
fun save() {
var value = originalValue
try {
val numberFormat = NumberFormat.getInstance()
val valueStr = view.value.text.toString()
value = numberFormat.parse(valueStr)!!.toDouble()
} catch (e: ParseException) {
// NOP
}
val notes = view.notes.text.toString()
onToggle(value, notes)
requireDialog().dismiss()
}
}

View File

@@ -1,117 +0,0 @@
/*
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker 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, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker 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 <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.common.dialogs
import android.app.Dialog
import android.content.Context
import android.view.KeyEvent.KEYCODE_ENTER
import android.view.LayoutInflater
import android.view.MotionEvent.ACTION_DOWN
import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE
import org.isoron.uhabits.activities.common.dialogs.MultipleDialogsHandler.Companion.dismissCurrent
import org.isoron.uhabits.core.models.Entry
import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.databinding.CheckmarkPopupBinding
import org.isoron.uhabits.utils.dimBehind
import org.isoron.uhabits.utils.dp
import org.isoron.uhabits.utils.requestFocusWithKeyboard
import java.text.DecimalFormat
class NumberPopup(
private val context: Context,
private var notes: String,
private var value: Double,
private val prefs: Preferences,
private val anchor: View,
) {
var onToggle: (Double, String) -> Unit = { _, _ -> }
var onDismiss: () -> Unit = {}
private val originalValue = value
private lateinit var dialog: Dialog
private val view = CheckmarkPopupBinding.inflate(LayoutInflater.from(context)).apply {
// Required for round corners
container.clipToOutline = true
}
init {
view.numberButtons.visibility = VISIBLE
hideDisabledButtons()
populate()
}
private fun hideDisabledButtons() {
if (!prefs.isSkipEnabled) view.skipBtnNumber.visibility = GONE
}
private fun populate() {
view.notes.setText(notes)
view.value.setText(
when {
value < 0.01 -> "0"
else -> DecimalFormat("#.##").format(value)
}
)
}
fun show() {
dialog = Dialog(context, android.R.style.Theme_NoTitleBar)
dialog.setContentView(view.root)
dialog.window?.apply {
setLayout(
view.root.dp(POPUP_WIDTH).toInt(),
view.root.dp(POPUP_HEIGHT).toInt()
)
setBackgroundDrawableResource(android.R.color.transparent)
}
dialog.setOnDismissListener {
onDismiss()
}
view.value.setOnKeyListener { _, keyCode, event ->
if (event.action == ACTION_DOWN && keyCode == KEYCODE_ENTER) {
save()
return@setOnKeyListener true
}
return@setOnKeyListener false
}
view.saveBtn.setOnClickListener {
save()
}
view.skipBtnNumber.setOnClickListener {
view.value.setText((Entry.SKIP.toDouble() / 1000).toString())
save()
}
view.value.requestFocusWithKeyboard()
dialog.setCanceledOnTouchOutside(true)
dialog.dimBehind()
dialog.dismissCurrent()
dialog.show()
}
fun save() {
val value = view.value.text.toString().toDoubleOrNull() ?: originalValue
val notes = view.notes.text.toString()
onToggle(value, notes)
dialog.dismiss()
}
}

View File

@@ -25,7 +25,6 @@ import android.os.Bundle
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatDialogFragment
import org.isoron.uhabits.R
import org.isoron.uhabits.activities.common.dialogs.MultipleDialogsHandler.Companion.dismissCurrent
import org.isoron.uhabits.core.models.WeekdayList
import org.isoron.uhabits.core.utils.DateUtils
import java.util.Calendar
@@ -61,7 +60,7 @@ class WeekdayPickerDialog :
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val builder = AlertDialog.Builder(
requireActivity()
activity!!
)
builder
.setTitle(R.string.select_weekdays)
@@ -75,9 +74,7 @@ class WeekdayPickerDialog :
android.R.string.cancel
) { _: DialogInterface?, _: Int -> dismiss() }
val dialog = builder.create()
dialog.dismissCurrent()
return dialog
return builder.create()
}
fun setListener(listener: OnWeekdaysPickedListener?) {

View File

@@ -63,7 +63,9 @@ class FrequencyChart : ScrollableChart {
private var primaryColor = 0
private var isBackgroundTransparent = false
private lateinit var frequency: HashMap<Timestamp, Array<Int>>
private var maxFreq = 0
private var firstWeekday = Calendar.SUNDAY
private var isNumerical: Boolean = false
constructor(context: Context?) : super(context) {
init()
@@ -80,8 +82,14 @@ class FrequencyChart : ScrollableChart {
postInvalidate()
}
fun setIsNumerical(type: Boolean) {
isNumerical = type
postInvalidate()
}
fun setFrequency(frequency: java.util.HashMap<Timestamp, Array<Int>>) {
this.frequency = frequency
maxFreq = getMaxFreq(frequency)
postInvalidate()
}
@@ -90,6 +98,15 @@ class FrequencyChart : ScrollableChart {
postInvalidate()
}
private fun getMaxFreq(frequency: HashMap<Timestamp, Array<Int>>): Int {
var maxValue = 1
for (values in frequency.values) for (value in values) maxValue = max(
value,
maxValue
)
return maxValue
}
fun setIsBackgroundTransparent(isBackgroundTransparent: Boolean) {
this.isBackgroundTransparent = isBackgroundTransparent
initColors()
@@ -213,7 +230,7 @@ class FrequencyChart : ScrollableChart {
canvas.drawLine(rGrid.left, rGrid.top, rGrid.right, rGrid.top, pGrid!!)
}
private fun drawMarker(canvas: Canvas, rect: RectF?, value: Int?, frequency: Int) {
private fun drawMarker(canvas: Canvas, rect: RectF?, value: Int?, weekdayFrequency: Int) {
// value can be negative when the entry is skipped
val valueCopy = value?.let { max(0, it) }
@@ -221,8 +238,8 @@ class FrequencyChart : ScrollableChart {
// maximal allowed mark radius
val maxRadius = (rect.height() - 2 * padding) / 2.0f
// the real mark radius is scaled down by a factor depending on the maximal frequency
val scale = 1.0f / frequency * valueCopy!!
val scalingFactor = if (isNumerical) maxFreq else weekdayFrequency
val scale = 1.0f / scalingFactor * valueCopy!!
val radius = maxRadius * scale
val colorIndex = min((colors.size - 1), ((colors.size - 1) * scale).roundToInt())
pGraph!!.color = colors[colorIndex]
@@ -285,5 +302,6 @@ class FrequencyChart : ScrollableChart {
frequency[Timestamp(date)] = values
date.add(Calendar.MONTH, -1)
}
maxFreq = getMaxFreq(frequency)
}
}

View File

@@ -46,7 +46,6 @@ import org.isoron.uhabits.R
import org.isoron.uhabits.activities.AndroidThemeSwitcher
import org.isoron.uhabits.activities.common.dialogs.ColorPickerDialogFactory
import org.isoron.uhabits.activities.common.dialogs.FrequencyPickerDialog
import org.isoron.uhabits.activities.common.dialogs.MultipleDialogsHandler.Companion.dismissCurrent
import org.isoron.uhabits.activities.common.dialogs.WeekdayPickerDialog
import org.isoron.uhabits.core.commands.CommandRunner
import org.isoron.uhabits.core.commands.CreateHabitCommand
@@ -60,6 +59,7 @@ import org.isoron.uhabits.core.models.Reminder
import org.isoron.uhabits.core.models.WeekdayList
import org.isoron.uhabits.databinding.ActivityEditHabitBinding
import org.isoron.uhabits.utils.ColorUtils
import org.isoron.uhabits.utils.dismissCurrentAndShow
import org.isoron.uhabits.utils.formatTime
import org.isoron.uhabits.utils.toFormattedString
@@ -157,23 +157,23 @@ class EditHabitActivity : AppCompatActivity() {
val colorPickerDialogFactory = ColorPickerDialogFactory(this)
binding.colorButton.setOnClickListener {
val dialog = colorPickerDialogFactory.create(color, themeSwitcher.currentTheme)
dialog.setListener { paletteColor ->
val picker = colorPickerDialogFactory.create(color, themeSwitcher.currentTheme)
picker.setListener { paletteColor ->
this.color = paletteColor
updateColors()
}
dialog.show(supportFragmentManager, "colorPicker")
picker.dismissCurrentAndShow(supportFragmentManager, "colorPicker")
}
populateFrequency()
binding.booleanFrequencyPicker.setOnClickListener {
val dialog = FrequencyPickerDialog(freqNum, freqDen)
dialog.onFrequencyPicked = { num, den ->
val picker = FrequencyPickerDialog(freqNum, freqDen)
picker.onFrequencyPicked = { num, den ->
freqNum = num
freqDen = den
populateFrequency()
}
dialog.show(supportFragmentManager, "frequencyPicker")
picker.dismissCurrentAndShow(supportFragmentManager, "frequencyPicker")
}
populateTargetType()
@@ -191,8 +191,7 @@ class EditHabitActivity : AppCompatActivity() {
dialog.dismiss()
}
val dialog = builder.create()
dialog.dismissCurrent()
dialog.show()
dialog.dismissCurrentAndShow()
}
binding.numericalFrequencyPicker.setOnClickListener {
@@ -238,7 +237,7 @@ class EditHabitActivity : AppCompatActivity() {
is24HourMode,
androidColor
)
dialog.show(supportFragmentManager, "timePicker")
dialog.dismissCurrentAndShow(supportFragmentManager, "timePicker")
}
binding.reminderDatePicker.setOnClickListener {
@@ -250,7 +249,7 @@ class EditHabitActivity : AppCompatActivity() {
populateReminder()
}
dialog.setSelectedDays(reminderDays)
dialog.show(supportFragmentManager, "dayPicker")
dialog.dismissCurrentAndShow(supportFragmentManager, "dayPicker")
}
binding.buttonSave.setOnClickListener {

View File

@@ -21,6 +21,7 @@ package org.isoron.uhabits.activities.habits.list
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity
@@ -39,6 +40,7 @@ import org.isoron.uhabits.inject.ActivityContextModule
import org.isoron.uhabits.inject.DaggerHabitsActivityComponent
import org.isoron.uhabits.inject.HabitsActivityComponent
import org.isoron.uhabits.inject.HabitsApplicationComponent
import org.isoron.uhabits.utils.dismissCurrentDialog
import org.isoron.uhabits.utils.restartWithFade
class ListHabitsActivity : AppCompatActivity(), Preferences.Listener {
@@ -84,13 +86,13 @@ class ListHabitsActivity : AppCompatActivity(), Preferences.Listener {
Thread.setDefaultUncaughtExceptionHandler(BaseExceptionHandler(this))
component.listHabitsBehavior.onStartup()
setContentView(rootView)
parseIntents()
}
override fun onPause() {
midnightTimer.onPause()
screen.onDetached()
adapter.cancelRefresh()
dismissCurrentDialog()
super.onPause()
}
@@ -99,12 +101,19 @@ class ListHabitsActivity : AppCompatActivity(), Preferences.Listener {
screen.onAttached()
rootView.postInvalidate()
midnightTimer.onResume()
appComponent.reminderScheduler.scheduleAll()
taskRunner.run {
try {
AutoBackup(this@ListHabitsActivity).run()
appComponent.widgetUpdater.updateWidgets()
} catch (e: Exception) {
Log.e("ListHabitActivity", "TaskRunner failed", e)
}
}
if (prefs.theme == THEME_DARK && prefs.isPureBlackEnabled != pureBlack) {
restartWithFade(ListHabitsActivity::class.java)
}
parseIntents()
super.onResume()
}
@@ -124,6 +133,7 @@ class ListHabitsActivity : AppCompatActivity(), Preferences.Listener {
}
private fun parseIntents() {
if (intent == null) return
if (intent.action == ACTION_EDIT) {
val habitId = intent.extras?.getLong("habit")
val timestamp = intent.extras?.getLong("timestamp")
@@ -132,6 +142,12 @@ class ListHabitsActivity : AppCompatActivity(), Preferences.Listener {
component.listHabitsBehavior.onEdit(habit, Timestamp(timestamp))
}
}
intent = null
}
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
setIntent(intent)
}
companion object {

View File

@@ -22,14 +22,15 @@ package org.isoron.uhabits.activities.habits.list
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import dagger.Lazy
import org.isoron.platform.gui.toInt
import org.isoron.uhabits.R
import org.isoron.uhabits.activities.common.dialogs.CheckmarkPopup
import org.isoron.uhabits.activities.common.dialogs.CheckmarkDialog
import org.isoron.uhabits.activities.common.dialogs.ColorPickerDialogFactory
import org.isoron.uhabits.activities.common.dialogs.ConfirmDeleteDialog
import org.isoron.uhabits.activities.common.dialogs.NumberPopup
import org.isoron.uhabits.activities.common.dialogs.NumberDialog
import org.isoron.uhabits.activities.habits.edit.HabitTypeDialog
import org.isoron.uhabits.activities.habits.list.views.HabitCardListAdapter
import org.isoron.uhabits.core.commands.ArchiveHabitsCommand
@@ -64,6 +65,7 @@ import org.isoron.uhabits.tasks.ImportDataTask
import org.isoron.uhabits.tasks.ImportDataTaskFactory
import org.isoron.uhabits.utils.copyTo
import org.isoron.uhabits.utils.currentTheme
import org.isoron.uhabits.utils.dismissCurrentAndShow
import org.isoron.uhabits.utils.restartWithFade
import org.isoron.uhabits.utils.showMessage
import org.isoron.uhabits.utils.showSendEmailScreen
@@ -163,7 +165,7 @@ class ListHabitsScreen
}
override fun showDeleteConfirmationScreen(callback: OnConfirmedCallback, quantity: Int) {
ConfirmDeleteDialog(activity, callback, quantity).show()
ConfirmDeleteDialog(activity, callback, quantity).dismissCurrentAndShow()
}
override fun showEditHabitsScreen(selected: List<Habit>) {
@@ -224,7 +226,7 @@ class ListHabitsScreen
override fun showColorPicker(defaultColor: PaletteColor, callback: OnColorPickedCallback) {
val picker = colorPickerFactory.create(defaultColor, themeSwitcher.currentTheme!!)
picker.setListener(callback)
picker.show(activity.supportFragmentManager, "picker")
picker.dismissCurrentAndShow(activity.supportFragmentManager, "picker")
}
override fun showNumberPopup(
@@ -232,17 +234,14 @@ class ListHabitsScreen
notes: String,
callback: ListHabitsBehavior.NumberPickerCallback
) {
val view = rootView.get()
NumberPopup(
context = context,
prefs = preferences,
anchor = view,
notes = notes,
value = value,
).apply {
onToggle = { value, notes -> callback.onNumberPicked(value, notes) }
show()
val fm = (context as AppCompatActivity).supportFragmentManager
val dialog = NumberDialog()
dialog.arguments = Bundle().apply {
putDouble("value", value)
putString("notes", notes)
}
dialog.onToggle = { v, n -> callback.onNumberPicked(v, n) }
dialog.dismissCurrentAndShow(fm, "numberDialog")
}
override fun showCheckmarkPopup(
@@ -251,18 +250,16 @@ class ListHabitsScreen
color: PaletteColor,
callback: ListHabitsBehavior.CheckMarkDialogCallback
) {
val view = rootView.get()
CheckmarkPopup(
context = context,
prefs = preferences,
anchor = view,
color = view.currentTheme().color(color).toInt(),
notes = notes,
value = selectedValue,
).apply {
onToggle = { value, notes -> callback.onNotesSaved(value, notes) }
show()
val theme = rootView.get().currentTheme()
val fm = (context as AppCompatActivity).supportFragmentManager
val dialog = CheckmarkDialog()
dialog.arguments = Bundle().apply {
putInt("color", theme.color(color).toInt())
putInt("value", selectedValue)
putString("notes", notes)
}
dialog.onToggle = { v, n -> callback.onNotesSaved(v, n) }
dialog.dismissCurrentAndShow(fm, "checkmarkDialog")
}
private fun getExecuteString(command: Command): String? {

View File

@@ -44,8 +44,6 @@ import org.isoron.uhabits.utils.sres
import org.isoron.uhabits.utils.toMeasureSpec
import javax.inject.Inject
const val TOGGLE_DELAY_MILLIS = 2000L
class CheckmarkButtonViewFactory
@Inject constructor(
@ActivityContext val context: Context,
@@ -79,7 +77,7 @@ class CheckmarkButtonView(
invalidate()
}
var onToggle: (Int, String, Long) -> Unit = { _, _, _ -> }
var onToggle: (Int, String) -> Unit = { _, _ -> }
var onEdit: () -> Unit = { }
@@ -90,25 +88,25 @@ class CheckmarkButtonView(
setOnLongClickListener(this)
}
fun performToggle(delay: Long) {
fun performToggle() {
value = Entry.nextToggleValue(
value = value,
isSkipEnabled = preferences.isSkipEnabled,
areQuestionMarksEnabled = preferences.areQuestionMarksEnabled
)
onToggle(value, notes, delay)
onToggle(value, notes)
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
invalidate()
}
override fun onClick(v: View) {
if (preferences.isShortToggleEnabled) performToggle(TOGGLE_DELAY_MILLIS)
if (preferences.isShortToggleEnabled) performToggle()
else onEdit()
}
override fun onLongClick(v: View): Boolean {
if (preferences.isShortToggleEnabled) onEdit()
else performToggle(TOGGLE_DELAY_MILLIS)
else performToggle()
return true
}

View File

@@ -60,7 +60,7 @@ class CheckmarkPanelView(
setupButtons()
}
var onToggle: (Timestamp, Int, String, Long) -> Unit = { _, _, _, _ -> }
var onToggle: (Timestamp, Int, String) -> Unit = { _, _, _ -> }
set(value) {
field = value
setupButtons()
@@ -89,7 +89,7 @@ class CheckmarkPanelView(
else -> ""
}
button.color = color
button.onToggle = { value, notes, delay -> onToggle(timestamp, value, notes, delay) }
button.onToggle = { value, notes -> onToggle(timestamp, value, notes) }
button.onEdit = { onEdit(timestamp) }
}
}

View File

@@ -57,13 +57,6 @@ class HabitCardViewFactory
fun create() = HabitCardView(context, checkmarkPanelFactory, numberPanelFactory, behavior)
}
data class DelayedToggle(
var habit: Habit,
var timestamp: Timestamp,
var value: Int,
var notes: String
)
class HabitCardView(
@ActivityContext context: Context,
checkmarkPanelFactory: CheckmarkPanelViewFactory,
@@ -136,7 +129,6 @@ class HabitCardView(
private var scoreRing: RingView
private var currentToggleTaskId = 0
private var queuedToggles = mutableListOf<DelayedToggle>()
init {
scoreRing = RingView(context).apply {
@@ -160,12 +152,9 @@ class HabitCardView(
}
checkmarkPanel = checkmarkPanelFactory.create().apply {
onToggle = { timestamp, value, notes, delay ->
if (delay > 0) triggerRipple(timestamp)
habit?.let {
val taskId = queueToggle(it, timestamp, value, notes);
{ runPendingToggles(taskId) }.delay(delay)
}
onToggle = { timestamp, value, notes ->
triggerRipple(timestamp)
habit?.let { behavior.onToggle(it, timestamp, value, notes) }
}
onEdit = { timestamp ->
triggerRipple(timestamp)
@@ -205,25 +194,6 @@ class HabitCardView(
addView(innerFrame)
}
@Synchronized
private fun runPendingToggles(id: Int) {
if (currentToggleTaskId != id) return
for ((h, t, v, n) in queuedToggles) behavior.onToggle(h, t, v, n)
queuedToggles.clear()
}
@Synchronized
private fun queueToggle(
it: Habit,
timestamp: Timestamp,
value: Int,
notes: String,
): Int {
currentToggleTaskId += 1
queuedToggles.add(DelayedToggle(it, timestamp, value, notes))
return currentToggleTaskId
}
override fun onModelChange() {
Handler(Looper.getMainLooper()).post {
habit?.let { copyAttributesFrom(it) }

View File

@@ -34,10 +34,10 @@ import org.isoron.uhabits.HabitsApplication
import org.isoron.uhabits.R
import org.isoron.uhabits.activities.AndroidThemeSwitcher
import org.isoron.uhabits.activities.HabitsDirFinder
import org.isoron.uhabits.activities.common.dialogs.CheckmarkPopup
import org.isoron.uhabits.activities.common.dialogs.CheckmarkDialog
import org.isoron.uhabits.activities.common.dialogs.ConfirmDeleteDialog
import org.isoron.uhabits.activities.common.dialogs.HistoryEditorDialog
import org.isoron.uhabits.activities.common.dialogs.NumberPopup
import org.isoron.uhabits.activities.common.dialogs.NumberDialog
import org.isoron.uhabits.core.commands.Command
import org.isoron.uhabits.core.commands.CommandRunner
import org.isoron.uhabits.core.models.Habit
@@ -50,6 +50,8 @@ import org.isoron.uhabits.core.ui.screens.habits.show.ShowHabitPresenter
import org.isoron.uhabits.core.ui.views.OnDateClickedListener
import org.isoron.uhabits.intents.IntentFactory
import org.isoron.uhabits.utils.currentTheme
import org.isoron.uhabits.utils.dismissCurrentAndShow
import org.isoron.uhabits.utils.dismissCurrentDialog
import org.isoron.uhabits.utils.showMessage
import org.isoron.uhabits.utils.showSendFileScreen
import org.isoron.uhabits.widgets.WidgetUpdater
@@ -128,6 +130,7 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
}
override fun onPause() {
dismissCurrentDialog()
commandRunner.removeListener(this)
super.onPause()
}
@@ -169,41 +172,32 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
override fun showNumberPopup(
value: Double,
notes: String,
preferences: Preferences,
callback: ListHabitsBehavior.NumberPickerCallback
) {
val anchor = getPopupAnchor() ?: return
NumberPopup(
context = this@ShowHabitActivity,
prefs = preferences,
notes = notes,
anchor = anchor,
value = value,
).apply {
onToggle = { v, n -> callback.onNumberPicked(v, n) }
show()
val dialog = NumberDialog()
dialog.arguments = Bundle().apply {
putDouble("value", value)
putString("notes", notes)
}
dialog.onToggle = { v, n -> callback.onNumberPicked(v, n) }
dialog.dismissCurrentAndShow(supportFragmentManager, "numberDialog")
}
override fun showCheckmarkPopup(
selectedValue: Int,
notes: String,
preferences: Preferences,
color: PaletteColor,
callback: ListHabitsBehavior.CheckMarkDialogCallback
) {
val anchor = getPopupAnchor() ?: return
CheckmarkPopup(
context = this@ShowHabitActivity,
prefs = preferences,
notes = notes,
color = view.currentTheme().color(color).toInt(),
anchor = anchor,
value = selectedValue,
).apply {
onToggle = { v, n -> callback.onNotesSaved(v, n) }
show()
val theme = view.currentTheme()
val dialog = CheckmarkDialog()
dialog.arguments = Bundle().apply {
putInt("color", theme.color(color).toInt())
putInt("value", selectedValue)
putString("notes", notes)
}
dialog.onToggle = { v, n -> callback.onNotesSaved(v, n) }
dialog.dismissCurrentAndShow(supportFragmentManager, "checkmarkDialog")
}
private fun getPopupAnchor(): View? {
@@ -228,7 +222,7 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
}
override fun showDeleteConfirmationScreen(callback: OnConfirmedCallback) {
ConfirmDeleteDialog(this@ShowHabitActivity, callback, 1).show()
ConfirmDeleteDialog(this@ShowHabitActivity, callback, 1).dismissCurrentAndShow()
}
override fun close() {

View File

@@ -33,6 +33,7 @@ class FrequencyCardView(context: Context, attrs: AttributeSet) : LinearLayout(co
fun setState(state: FrequencyCardState) {
val androidColor = state.theme.color(state.color).toInt()
binding.frequencyChart.setFrequency(state.frequency)
binding.frequencyChart.setIsNumerical(state.isNumerical)
binding.frequencyChart.setFirstWeekday(state.firstWeekday)
binding.title.setTextColor(androidColor)
binding.frequencyChart.setColor(androidColor)

View File

@@ -21,13 +21,16 @@ package org.isoron.uhabits.intents
import android.app.PendingIntent
import android.app.PendingIntent.FLAG_IMMUTABLE
import android.app.PendingIntent.FLAG_MUTABLE
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
import android.app.PendingIntent.getActivity
import android.app.PendingIntent.getBroadcast
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import org.isoron.uhabits.activities.habits.list.ListHabitsActivity
import org.isoron.uhabits.activities.habits.show.ShowHabitActivity
import org.isoron.uhabits.core.AppScope
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.Timestamp
@@ -89,6 +92,20 @@ class PendingIntentFactory
)
.getPendingIntent(0, FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT)!!
fun showHabitTemplate(): PendingIntent {
return getActivity(
context,
0,
Intent(context, ShowHabitActivity::class.java),
getIntentTemplateFlags()
)
}
fun showHabitFillIn(habit: Habit) =
Intent().apply {
data = Uri.parse(habit.uriString)
}
fun showReminder(
habit: Habit,
reminderTime: Long?,
@@ -142,7 +159,7 @@ class PendingIntentFactory
fun showNumberPicker(habit: Habit, timestamp: Timestamp): PendingIntent? {
return getActivity(
context,
0,
(habit.id!! % Integer.MAX_VALUE).toInt() + 1,
Intent(context, ListHabitsActivity::class.java).apply {
action = ListHabitsActivity.ACTION_EDIT
putExtra("habit", habit.id)
@@ -151,4 +168,43 @@ class PendingIntentFactory
FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT
)
}
fun showNumberPickerTemplate(): PendingIntent {
return getActivity(
context,
1,
Intent(context, ListHabitsActivity::class.java).apply {
action = ListHabitsActivity.ACTION_EDIT
},
getIntentTemplateFlags()
)
}
fun showNumberPickerFillIn(habit: Habit, timestamp: Timestamp) = Intent().apply {
putExtra("habit", habit.id)
putExtra("timestamp", timestamp.unixTime)
}
private fun getIntentTemplateFlags(): Int {
var flags = 0
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
flags = flags or FLAG_MUTABLE
}
return flags
}
fun toggleCheckmarkTemplate(): PendingIntent =
getBroadcast(
context,
2,
Intent(context, WidgetReceiver::class.java).apply {
action = WidgetReceiver.ACTION_TOGGLE_REPETITION
},
getIntentTemplateFlags()
)
fun toggleCheckmarkFillIn(habit: Habit, timestamp: Timestamp) = Intent().apply {
data = Uri.parse(habit.uriString)
putExtra("timestamp", timestamp.unixTime)
}
}

View File

@@ -0,0 +1,29 @@
package org.isoron.uhabits.utils
import android.app.Dialog
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.FragmentManager
import java.lang.ref.WeakReference
var currentDialog: WeakReference<Dialog> = WeakReference(null)
var currentDialogFragment: WeakReference<DialogFragment> = WeakReference(null)
fun dismissCurrentDialog() {
currentDialog.get()?.dismiss()
currentDialog = WeakReference(null)
currentDialogFragment.get()?.dismiss()
currentDialogFragment = WeakReference(null)
}
fun Dialog.dismissCurrentAndShow() {
dismissCurrentDialog()
currentDialog = WeakReference(this)
show()
}
fun DialogFragment.dismissCurrentAndShow(fragmentManager: FragmentManager, tag: String) {
dismissCurrentDialog()
currentDialogFragment = WeakReference(this)
show(fragmentManager, tag)
fragmentManager.executePendingTransactions()
}

View File

@@ -43,7 +43,7 @@ open class CheckmarkWidget(
override fun getOnClickPendingIntent(context: Context): PendingIntent? {
return if (habit.isNumerical) {
pendingIntentFactory.showNumberPicker(habit, DateUtils.getToday())
pendingIntentFactory.showNumberPicker(habit, DateUtils.getTodayWithOffset())
} else {
pendingIntentFactory.toggleCheckmark(habit, null)
}

View File

@@ -49,6 +49,7 @@ class FrequencyWidget(
(widgetView.dataView as FrequencyChart).apply {
setFirstWeekday(firstWeekday)
setColor(WidgetTheme().color(habit.color).toInt())
setIsNumerical(habit.isNumerical)
setFrequency(habit.originalEntries.computeWeekdayFrequency(habit.isNumerical))
}
}

View File

@@ -73,6 +73,10 @@ class StackWidget(
StackWidgetType.getStackWidgetAdapterViewId(widgetType),
StackWidgetType.getStackWidgetEmptyViewId(widgetType)
)
remoteViews.setPendingIntentTemplate(
StackWidgetType.getStackWidgetAdapterViewId(widgetType),
StackWidgetType.getPendingIntentTemplate(pendingIntentFactory, widgetType, habits)
)
return remoteViews
}
}

View File

@@ -29,11 +29,14 @@ import android.widget.RemoteViewsService
import android.widget.RemoteViewsService.RemoteViewsFactory
import org.isoron.platform.utils.StringUtils.Companion.splitLongs
import org.isoron.uhabits.HabitsApplication
import org.isoron.uhabits.R
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.HabitNotFoundException
import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.core.utils.DateUtils.Companion.getTodayWithOffset
import org.isoron.uhabits.intents.IntentFactory
import org.isoron.uhabits.intents.PendingIntentFactory
import org.isoron.uhabits.utils.InterfaceUtils.dpToPixels
import java.util.ArrayList
class StackWidgetService : RemoteViewsService() {
override fun onGetViewFactory(intent: Intent): RemoteViewsFactory {
@@ -54,7 +57,6 @@ internal class StackRemoteViewsFactory(private val context: Context, intent: Int
)
private val habitIds: LongArray
private val widgetType: StackWidgetType
private var remoteViews = ArrayList<RemoteViews>()
override fun onCreate() {}
override fun onDestroy() {}
override fun getCount(): Int {
@@ -85,8 +87,26 @@ internal class StackRemoteViewsFactory(private val context: Context, intent: Int
}
override fun getViewAt(position: Int): RemoteViews? {
Log.i("StackRemoteViewsFactory", "getViewAt $position")
return if (0 <= position && position < remoteViews.size) remoteViews[position] else null
Log.i("StackRemoteViewsFactory", "getViewAt $position started")
if (position < 0 || position >= habitIds.size) return null
val app = context.applicationContext as HabitsApplication
val prefs = app.component.preferences
val habitList = app.component.habitList
val options = AppWidgetManager.getInstance(context).getAppWidgetOptions(widgetId)
if (Looper.myLooper() == null) Looper.prepare()
val habits = habitIds.map { habitList.getById(it) ?: throw HabitNotFoundException() }
val h = habits[position]
val widget = constructWidget(h, prefs)
widget.setDimensions(getDimensionsFromOptions(context, options))
val landscapeViews = widget.landscapeRemoteViews
val portraitViews = widget.portraitRemoteViews
val factory = PendingIntentFactory(context, IntentFactory())
val intent = StackWidgetType.getIntentFillIn(factory, widgetType, h, habits, getTodayWithOffset())
landscapeViews.setOnClickFillInIntent(R.id.button, intent)
portraitViews.setOnClickFillInIntent(R.id.button, intent)
val remoteViews = RemoteViews(landscapeViews, portraitViews)
Log.i("StackRemoteViewsFactory", "getViewAt $position ended")
return remoteViews
}
private fun constructWidget(
@@ -131,24 +151,6 @@ internal class StackRemoteViewsFactory(private val context: Context, intent: Int
}
override fun onDataSetChanged() {
Log.i("StackRemoteViewsFactory", "onDataSetChanged started")
val app = context.applicationContext as HabitsApplication
val prefs = app.component.preferences
val habitList = app.component.habitList
val options = AppWidgetManager.getInstance(context).getAppWidgetOptions(widgetId)
val newRemoteViews = ArrayList<RemoteViews>()
if (Looper.myLooper() == null) Looper.prepare()
for (id in habitIds) {
val h = habitList.getById(id) ?: throw HabitNotFoundException()
val widget = constructWidget(h, prefs)
widget.setDimensions(getDimensionsFromOptions(context, options))
val landscapeViews = widget.landscapeRemoteViews
val portraitViews = widget.portraitRemoteViews
newRemoteViews.add(RemoteViews(landscapeViews, portraitViews))
Log.i("StackRemoteViewsFactory", "onDataSetChanged constructed widget $id")
}
remoteViews = newRemoteViews
Log.i("StackRemoteViewsFactory", "onDataSetChanged ended")
}
init {

View File

@@ -18,7 +18,12 @@
*/
package org.isoron.uhabits.widgets
import android.app.PendingIntent
import android.content.Intent
import org.isoron.uhabits.R
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.Timestamp
import org.isoron.uhabits.intents.PendingIntentFactory
import java.lang.IllegalStateException
enum class StackWidgetType(val value: Int) {
@@ -73,5 +78,39 @@ enum class StackWidgetType(val value: Int) {
else -> throw IllegalStateException()
}
}
fun getPendingIntentTemplate(
factory: PendingIntentFactory,
widgetType: StackWidgetType,
habits: List<Habit>
): PendingIntent {
val containsNumerical = habits.any { it.isNumerical }
return when (widgetType) {
CHECKMARK -> if (containsNumerical) {
factory.showNumberPickerTemplate()
} else {
factory.toggleCheckmarkTemplate()
}
FREQUENCY, SCORE, HISTORY, STREAKS, TARGET -> factory.showHabitTemplate()
}
}
fun getIntentFillIn(
factory: PendingIntentFactory,
widgetType: StackWidgetType,
habit: Habit,
allHabitsInStackWidget: List<Habit>,
timestamp: Timestamp
): Intent {
val containsNumerical = allHabitsInStackWidget.any { it.isNumerical }
return when (widgetType) {
CHECKMARK -> if (containsNumerical) {
factory.showNumberPickerFillIn(habit, timestamp)
} else {
factory.toggleCheckmarkFillIn(habit, timestamp)
}
FREQUENCY, SCORE, HISTORY, STREAKS, TARGET -> factory.showHabitFillIn(habit)
}
}
}
}

View File

@@ -24,7 +24,6 @@ import android.appwidget.AppWidgetManager.EXTRA_APPWIDGET_ID
import android.appwidget.AppWidgetManager.INVALID_APPWIDGET_ID
import android.content.Intent
import android.os.Bundle
import android.widget.AbsListView.CHOICE_MODE_MULTIPLE
import android.widget.ArrayAdapter
import android.widget.Button
import android.widget.ListView
@@ -34,7 +33,6 @@ import org.isoron.uhabits.R
import org.isoron.uhabits.activities.AndroidThemeSwitcher
import org.isoron.uhabits.core.preferences.WidgetPreferences
import org.isoron.uhabits.widgets.WidgetUpdater
import java.util.ArrayList
class BooleanHabitPickerDialog : HabitPickerDialog() {
override fun shouldHideNumerical() = true
@@ -88,20 +86,12 @@ open class HabitPickerDialog : Activity() {
with(listView) {
adapter = ArrayAdapter(
context,
android.R.layout.simple_list_item_multiple_choice,
android.R.layout.simple_list_item_1,
habitNames
)
choiceMode = CHOICE_MODE_MULTIPLE
itemsCanFocus = false
setOnItemClickListener { parent, view, position, id ->
confirm(mutableListOf(habitIds[position]))
}
saveButton.setOnClickListener {
val selectedIds = mutableListOf<Long>()
for (i in 0..listView.count) {
if (listView.isItemChecked(i)) {
selectedIds.add(habitIds[i])
}
}
confirm(selectedIds)
}
}

View File

@@ -66,23 +66,21 @@ class CheckmarkWidgetView : HabitWidgetView {
val res = StyledResources(context)
val bgColor: Int
val fgColor: Int
setShadowAlpha(0x4f)
when (entryState) {
YES_MANUAL, SKIP -> {
bgColor = activeColor
fgColor = res.getColor(R.attr.contrast0)
setShadowAlpha(0x4f)
backgroundPaint!!.color = bgColor
frame!!.setBackgroundDrawable(background)
}
YES_AUTO, NO, UNKNOWN -> {
bgColor = res.getColor(R.attr.cardBgColor)
fgColor = res.getColor(R.attr.contrast60)
setShadowAlpha(0x00)
}
else -> {
bgColor = res.getColor(R.attr.cardBgColor)
fgColor = res.getColor(R.attr.contrast60)
setShadowAlpha(0x00)
}
}
ring.setPercentage(percentage)
@@ -126,7 +124,7 @@ class CheckmarkWidgetView : HabitWidgetView {
} else {
width = min(width, height)
}
val textSize = min(0.2f * width, getDimension(context, R.dimen.smallerTextSize))
val textSize = min(0.175f * width, getDimension(context, R.dimen.smallTextSize))
label.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
if (isNumerical) {
ring.setTextSize(textSize * 0.9f)
@@ -141,7 +139,8 @@ class CheckmarkWidgetView : HabitWidgetView {
}
private fun init() {
val appComponent: HabitsApplicationComponent = (context.applicationContext as HabitsApplication).component
val appComponent: HabitsApplicationComponent =
(context.applicationContext as HabitsApplication).component
preferences = appComponent.preferences
ring = findViewById<View>(R.id.scoreRing) as RingView
label = findViewById<View>(R.id.label) as TextView

View File

@@ -69,7 +69,7 @@ abstract class HabitWidgetView : FrameLayout {
val shadowRadius = dpToPixels(context, 2f).toInt()
val shadowOffset = dpToPixels(context, 1f).toInt()
val shadowColor = Color.argb(shadowAlpha, 0, 0, 0)
val cornerRadius = dpToPixels(context, 5f)
val cornerRadius = dpToPixels(context, 12f)
val radii = FloatArray(8)
Arrays.fill(radii, cornerRadius)
val shape = RoundRectShape(radii, null, null)

View File

@@ -1,3 +1,6 @@
2.1.1:
* Fix Tasker plugin
2.1:
* Add notes to specific dates
* Track at-most measurable habits

View File

@@ -28,7 +28,7 @@
<item android:id="@android:id/mask">
<shape android:shape="rectangle">
<corners android:radius="5dp"/>
<corners android:radius="12dp"/>
<solid android:color="?android:colorPrimary"/>
</shape>
<color android:color="@color/white"/>

View File

@@ -21,8 +21,10 @@
<androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minHeight="128dp"
android:minWidth="208dp"
app:divider="@drawable/checkmark_dialog_divider"
app:showDividers="middle"
android:orientation="vertical"
@@ -34,7 +36,7 @@
android:layout_height="0dp"
android:layout_weight="1"
android:gravity="center"
android:inputType="textCapSentences|textMultiLine"
android:inputType="textCapSentences"
android:textSize="@dimen/smallTextSize"
android:padding="4dp"
android:background="@color/transparent"

View File

@@ -30,10 +30,4 @@
android:layout_weight="1">
</ListView>
<Button
android:id="@+id/buttonSave"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:text="@string/save"/>
</LinearLayout>

View File

@@ -35,7 +35,7 @@
android:paddingTop="4dp"
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:paddingBottom="4dp"
android:paddingBottom="8dp"
tools:ignore="UselessParent">
<TextView

View File

@@ -21,4 +21,5 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<monochrome android:drawable="@mipmap/ic_launcher_monochrome"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

View File

@@ -25,6 +25,12 @@ class StringUtils {
fun joinLongs(values: LongArray): String = values.joinToString(separator = ",")
fun splitLongs(str: String): LongArray = str.split(",").map { it.toLong() }.toLongArray()
fun splitLongs(str: String): LongArray {
return try {
str.split(",").map { it.toLong() }.toLongArray()
} catch (e: NumberFormatException) {
LongArray(0)
}
}
}
}

View File

@@ -149,7 +149,7 @@ class HabitsCSVExporter(
val timeframe = getTimeframe()
val oldest = timeframe[0]
val newest = DateUtils.getToday()
val newest = DateUtils.getTodayWithOffset()
val checkmarks: MutableList<ArrayList<Entry>> = ArrayList()
val scores: MutableList<ArrayList<Score>> = ArrayList()
for (habit in selectedHabits) {

View File

@@ -204,9 +204,16 @@ open class EntryList {
// Copy original entries
original.forEach { entry ->
val offset = entry.timestamp.daysUntil(to)
if (result[offset].value == UNKNOWN || entry.value == SKIP || entry.value == YES_MANUAL) {
result[offset] = entry
val value = if (
result[offset].value == UNKNOWN ||
entry.value == SKIP ||
entry.value == YES_MANUAL
) {
entry.value
} else {
YES_AUTO
}
result[offset] = Entry(entry.timestamp, value, entry.notes)
}
return result

View File

@@ -57,7 +57,7 @@ class BarCardPresenter(
} else {
boolBucketSizes[boolSpinnerPosition]
}
val today = DateUtils.getToday()
val today = DateUtils.getTodayWithOffset()
val oldest = habit.computedEntries.getKnown().lastOrNull()?.timestamp ?: today
val entries = habit.computedEntries.getByInterval(oldest, today).groupedSum(
truncateField = ScoreCardPresenter.getTruncateField(bucketSize),

View File

@@ -30,6 +30,7 @@ data class FrequencyCardState(
val firstWeekday: Int,
val frequency: HashMap<Timestamp, Array<Int>>,
val theme: Theme,
val isNumerical: Boolean
)
class FrequencyCardPresenter {
@@ -40,6 +41,7 @@ class FrequencyCardPresenter {
theme: Theme
) = FrequencyCardState(
color = habit.color,
isNumerical = habit.isNumerical,
frequency = habit.originalEntries.computeWeekdayFrequency(
isNumerical = habit.isNumerical
),

View File

@@ -91,7 +91,6 @@ class HistoryCardPresenter(
screen.showCheckmarkPopup(
entry.value,
entry.notes,
preferences,
habit.color,
) { newValue, newNotes ->
commandRunner.run(
@@ -130,7 +129,6 @@ class HistoryCardPresenter(
screen.showNumberPopup(
value = oldValue / 1000.0,
notes = entry.notes,
preferences = preferences,
) { newValue: Double, newNotes: String ->
val thousands = (newValue * 1000).roundToInt()
commandRunner.run(
@@ -203,13 +201,11 @@ class HistoryCardPresenter(
fun showNumberPopup(
value: Double,
notes: String,
preferences: Preferences,
callback: ListHabitsBehavior.NumberPickerCallback,
)
fun showCheckmarkPopup(
selectedValue: Int,
notes: String,
preferences: Preferences,
color: PaletteColor,
callback: ListHabitsBehavior.CheckMarkDialogCallback,
)

View File

@@ -92,7 +92,7 @@ class BarChart(
val r = round(barWidth * 0.15)
if (2 * r < barHeight) {
canvas.fillRect(x, y + r, barWidth, barHeight - r)
canvas.fillRect(x + r, y, barWidth - 2 * r, r)
canvas.fillRect(x + r, y, barWidth - 2 * r, r + 1)
canvas.fillCircle(x + r, y + r, r)
canvas.fillCircle(x + barWidth - r, y + r, r)
} else {

View File

@@ -217,7 +217,7 @@ class EntryListTest {
fun testAddFromInterval() {
val entries = listOf(
Entry(day(1), YES_MANUAL),
Entry(day(2), NO),
Entry(day(2), NO, "Test"),
Entry(day(4), NO),
Entry(day(5), YES_MANUAL),
Entry(day(10), YES_MANUAL),
@@ -230,7 +230,7 @@ class EntryListTest {
)
val expected = listOf(
Entry(day(1), YES_MANUAL),
Entry(day(2), YES_AUTO),
Entry(day(2), YES_AUTO, "Test"),
Entry(day(3), UNKNOWN),
Entry(day(4), YES_AUTO),
Entry(day(5), YES_MANUAL),