Reformat all Kotlin files with ktlint; add check to build script

pull/699/head
Alinson S. Xavier 5 years ago
parent 9a5263e508
commit 9087025418

@ -2,6 +2,9 @@ buildscript {
repositories {
google()
jcenter()
maven {
url "https://plugins.gradle.org/m2/"
}
}
dependencies {
@ -9,6 +12,7 @@ buildscript {
classpath "com.neenbedankt.gradle.plugins:android-apt:1.8"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$KOTLIN_VERSION"
classpath "org.ajoberstar:grgit:1.5.0"
classpath "org.jlleitschuh.gradle:ktlint-gradle:9.4.1"
}
}
@ -18,4 +22,5 @@ allprojects {
maven { url "https://oss.sonatype.org/content/repositories/snapshots/" }
jcenter()
}
apply plugin: "org.jlleitschuh.gradle.ktlint"
}

@ -67,6 +67,11 @@ run_adb_as_root() {
$ADB root
}
lint() {
log_info "Running ktlint..."
$GRADLE ktlintCheck || fail
}
build_apk() {
log_info "Removing old APKs..."
rm -vf build/*.apk
@ -258,6 +263,7 @@ case "$1" in
build)
shift; parse_opts $*
lint
build_apk
build_instrumentation_apk
run_jvm_tests

@ -19,12 +19,22 @@
package org.isoron.uhabits
import dagger.*
import org.isoron.uhabits.activities.habits.list.*
import org.isoron.uhabits.activities.habits.list.views.*
import org.isoron.uhabits.core.ui.screens.habits.list.*
import org.isoron.uhabits.inject.*
import org.mockito.Mockito.*
import dagger.Component
import dagger.Module
import dagger.Provides
import org.isoron.uhabits.activities.habits.list.ListHabitsModule
import org.isoron.uhabits.activities.habits.list.views.CheckmarkButtonViewFactory
import org.isoron.uhabits.activities.habits.list.views.CheckmarkPanelViewFactory
import org.isoron.uhabits.activities.habits.list.views.HabitCardViewFactory
import org.isoron.uhabits.activities.habits.list.views.NumberButtonViewFactory
import org.isoron.uhabits.activities.habits.list.views.NumberPanelViewFactory
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior
import org.isoron.uhabits.inject.ActivityContextModule
import org.isoron.uhabits.inject.ActivityScope
import org.isoron.uhabits.inject.HabitModule
import org.isoron.uhabits.inject.HabitsActivityModule
import org.isoron.uhabits.inject.HabitsApplicationComponent
import org.mockito.Mockito.mock
@Module
class TestModule {
@ -32,17 +42,20 @@ class TestModule {
}
@ActivityScope
@Component(modules = arrayOf(
@Component(
modules = arrayOf(
ActivityContextModule::class,
HabitsActivityModule::class,
ListHabitsModule::class,
HabitModule::class,
TestModule::class
), dependencies = arrayOf(HabitsApplicationComponent::class))
),
dependencies = arrayOf(HabitsApplicationComponent::class)
)
interface HabitsActivityTestComponent {
fun getCheckmarkPanelViewFactory(): CheckmarkPanelViewFactory
fun getHabitCardViewFactory(): HabitCardViewFactory
fun getEntryButtonViewFactory(): CheckmarkButtonViewFactory
fun getNumberButtonViewFactory(): NumberButtonViewFactory
fun getNumberPanelViewFactory(): NumberPanelViewFactory
}
}

@ -19,11 +19,20 @@
package org.isoron.uhabits.acceptance
import androidx.test.filters.*
import org.isoron.uhabits.*
import org.isoron.uhabits.acceptance.steps.*
import org.isoron.uhabits.acceptance.steps.CommonSteps.*
import org.junit.*
import androidx.test.filters.LargeTest
import org.isoron.uhabits.BaseUserInterfaceTest
import org.isoron.uhabits.acceptance.steps.CommonSteps.clickText
import org.isoron.uhabits.acceptance.steps.CommonSteps.launchApp
import org.isoron.uhabits.acceptance.steps.CommonSteps.longClickText
import org.isoron.uhabits.acceptance.steps.CommonSteps.verifyDisplaysText
import org.isoron.uhabits.acceptance.steps.CommonSteps.verifyDoesNotDisplayText
import org.isoron.uhabits.acceptance.steps.ListHabitsSteps
import org.isoron.uhabits.acceptance.steps.clearBackupFolder
import org.isoron.uhabits.acceptance.steps.clearDownloadFolder
import org.isoron.uhabits.acceptance.steps.copyBackupToDownloadFolder
import org.isoron.uhabits.acceptance.steps.exportFullBackup
import org.isoron.uhabits.acceptance.steps.importBackupFromDownloadFolder
import org.junit.Test
@LargeTest
class BackupTest : BaseUserInterfaceTest() {
@ -44,4 +53,4 @@ class BackupTest : BaseUserInterfaceTest() {
verifyDisplaysText("Wake up early")
}
}
}

@ -19,10 +19,11 @@
package org.isoron.uhabits.acceptance.steps
import androidx.test.uiautomator.*
import org.isoron.uhabits.acceptance.steps.CommonSteps.*
import org.isoron.uhabits.acceptance.steps.ListHabitsSteps.*
import org.isoron.uhabits.acceptance.steps.ListHabitsSteps.MenuItem.*
import androidx.test.uiautomator.UiSelector
import org.isoron.uhabits.acceptance.steps.CommonSteps.clickText
import org.isoron.uhabits.acceptance.steps.CommonSteps.device
import org.isoron.uhabits.acceptance.steps.ListHabitsSteps.MenuItem.SETTINGS
import org.isoron.uhabits.acceptance.steps.ListHabitsSteps.clickMenu
const val BACKUP_FOLDER = "/sdcard/Android/data/org.isoron.uhabits/files/Backups/"
const val DOWNLOAD_FOLDER = "/sdcard/Download/"

@ -20,12 +20,13 @@
package org.isoron.uhabits.activities.habits.list.views
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.*
import org.isoron.uhabits.*
import org.isoron.uhabits.core.models.*
import org.isoron.uhabits.utils.*
import org.junit.*
import org.junit.runner.*
import androidx.test.filters.MediumTest
import org.isoron.uhabits.BaseViewTest
import org.isoron.uhabits.core.models.Entry
import org.isoron.uhabits.utils.PaletteUtils
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
@MediumTest
@ -96,4 +97,4 @@ class EntryButtonViewTest : BaseViewTest() {
private fun assertRendersUnchecked() {
assertRenders(view, "$PATH/render_unchecked.png")
}
}
}

@ -19,18 +19,20 @@
package org.isoron.uhabits.activities.habits.list.views
import androidx.test.ext.junit.runners.*
import androidx.test.filters.*
import org.hamcrest.CoreMatchers.*
import org.hamcrest.MatcherAssert.*
import org.isoron.uhabits.*
import org.isoron.uhabits.core.models.*
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import org.hamcrest.CoreMatchers.equalTo
import org.hamcrest.MatcherAssert.assertThat
import org.isoron.uhabits.BaseViewTest
import org.isoron.uhabits.core.models.Entry.Companion.NO
import org.isoron.uhabits.core.models.Entry.Companion.YES_AUTO
import org.isoron.uhabits.core.models.Entry.Companion.YES_MANUAL
import org.isoron.uhabits.utils.*
import org.junit.*
import org.junit.runner.*
import org.isoron.uhabits.core.models.Timestamp
import org.isoron.uhabits.utils.PaletteUtils
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
@MediumTest
@ -44,13 +46,15 @@ class EntryPanelViewTest : BaseViewTest() {
super.setUp()
prefs.isCheckmarkSequenceReversed = false
val checkmarks = intArrayOf(YES_MANUAL,
YES_MANUAL,
YES_AUTO,
NO,
NO,
NO,
YES_MANUAL)
val checkmarks = intArrayOf(
YES_MANUAL,
YES_MANUAL,
YES_AUTO,
NO,
NO,
NO,
YES_MANUAL
)
view = component.getCheckmarkPanelViewFactory().create().apply {
values = checkmarks
@ -112,4 +116,4 @@ class EntryPanelViewTest : BaseViewTest() {
view.buttons[3].performLongClick()
assertThat(timestamps, equalTo(listOf(day(3), day(5), day(6))))
}
}
}

@ -19,22 +19,24 @@
package org.isoron.uhabits.activities.habits.list.views
import androidx.test.ext.junit.runners.*
import androidx.test.filters.*
import org.isoron.uhabits.*
import org.isoron.uhabits.core.models.*
import org.isoron.uhabits.core.utils.*
import org.junit.*
import org.junit.runner.*
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import org.isoron.uhabits.BaseViewTest
import org.isoron.uhabits.R
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.utils.DateUtils
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
@MediumTest
class HabitCardViewTest : BaseViewTest() {
val PATH = "habits/list/HabitCardView"
lateinit private var view: HabitCardView
lateinit private var habit1: Habit
lateinit private var habit2: Habit
private lateinit var view: HabitCardView
private lateinit var habit1: Habit
private lateinit var habit2: Habit
override fun setUp() {
super.setUp()

@ -20,14 +20,14 @@
package org.isoron.uhabits.activities.habits.list.views
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.*
import androidx.test.runner.*
import org.hamcrest.CoreMatchers.*
import org.hamcrest.MatcherAssert.*
import org.isoron.uhabits.*
import org.isoron.uhabits.utils.*
import org.junit.*
import org.junit.runner.*
import androidx.test.filters.MediumTest
import org.hamcrest.CoreMatchers.equalTo
import org.hamcrest.MatcherAssert.assertThat
import org.isoron.uhabits.BaseViewTest
import org.isoron.uhabits.utils.PaletteUtils
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
@MediumTest
@ -104,4 +104,4 @@ class NumberButtonViewTest : BaseViewTest() {
view.performLongClick()
assertTrue(edited)
}
}
}

@ -20,15 +20,16 @@
package org.isoron.uhabits.activities.habits.list.views
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.*
import androidx.test.runner.*
import org.hamcrest.CoreMatchers.*
import org.hamcrest.MatcherAssert.*
import org.isoron.uhabits.*
import org.isoron.uhabits.core.models.*
import org.isoron.uhabits.utils.*
import org.junit.*
import org.junit.runner.*
import androidx.test.filters.MediumTest
import org.hamcrest.CoreMatchers.equalTo
import org.hamcrest.MatcherAssert.assertThat
import org.isoron.uhabits.BaseViewTest
import org.isoron.uhabits.core.models.Timestamp
import org.isoron.uhabits.utils.PaletteUtils
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
@MediumTest
@ -42,8 +43,14 @@ class NumberPanelViewTest : BaseViewTest() {
super.setUp()
prefs.isCheckmarkSequenceReversed = false
val checkmarks = doubleArrayOf(1400.0, 5300.0, 0.0,
14600.0, 2500.0, 45000.0)
val checkmarks = doubleArrayOf(
1400.0,
5300.0,
0.0,
14600.0,
2500.0,
45000.0
)
view = component.getNumberPanelViewFactory().create().apply {
values = checkmarks
@ -107,4 +114,4 @@ class NumberPanelViewTest : BaseViewTest() {
view.buttons[3].performLongClick()
assertThat(timestamps, equalTo(listOf(day(3), day(5), day(6))))
}
}
}

@ -18,12 +18,15 @@
*/
package org.isoron.uhabits.activities.habits.show.views
import android.view.*
import androidx.test.ext.junit.runners.*
import androidx.test.filters.*
import org.isoron.uhabits.*
import org.junit.*
import org.junit.runner.*
import android.view.LayoutInflater
import android.view.View
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import org.isoron.uhabits.BaseViewTest
import org.isoron.uhabits.R
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
@MediumTest
@ -36,9 +39,9 @@ class FrequencyCardTest : BaseViewTest() {
super.setUp()
val habit = fixtures.createLongHabit()
view = LayoutInflater
.from(targetContext)
.inflate(R.layout.show_habit, null)
.findViewById<View>(R.id.frequencyCard) as FrequencyCard
.from(targetContext)
.inflate(R.layout.show_habit, null)
.findViewById<View>(R.id.frequencyCard) as FrequencyCard
view.update(FrequencyCardPresenter(habit, 0).present())
measureView(view, 800f, 600f)
}
@ -47,4 +50,4 @@ class FrequencyCardTest : BaseViewTest() {
fun testRender() {
assertRenders(view, PATH + "render.png")
}
}
}

@ -18,12 +18,15 @@
*/
package org.isoron.uhabits.activities.habits.show.views
import android.view.*
import androidx.test.ext.junit.runners.*
import androidx.test.filters.*
import org.isoron.uhabits.*
import org.junit.*
import org.junit.runner.*
import android.view.LayoutInflater
import android.view.View
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import org.isoron.uhabits.BaseViewTest
import org.isoron.uhabits.R
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
@MediumTest
@ -36,12 +39,16 @@ class HistoryCardTest : BaseViewTest() {
super.setUp()
val habit = fixtures.createLongHabit()
view = LayoutInflater
.from(targetContext)
.inflate(R.layout.show_habit, null)
.findViewById<View>(R.id.historyCard) as HistoryCard
view.update(HistoryCardPresenter(habit = habit,
firstWeekday = 1,
isSkipEnabled = false).present())
.from(targetContext)
.inflate(R.layout.show_habit, null)
.findViewById<View>(R.id.historyCard) as HistoryCard
view.update(
HistoryCardPresenter(
habit = habit,
firstWeekday = 1,
isSkipEnabled = false
).present()
)
measureView(view, 800f, 600f)
}
@ -49,4 +56,4 @@ class HistoryCardTest : BaseViewTest() {
fun testRender() {
assertRenders(view, PATH + "render.png")
}
}
}

@ -18,15 +18,17 @@
*/
package org.isoron.uhabits.activities.habits.show.views
import android.view.*
import android.view.View.*
import androidx.test.ext.junit.runners.*
import androidx.test.filters.*
import org.hamcrest.Matchers.*
import org.isoron.uhabits.*
import org.junit.*
import org.junit.Assert.*
import org.junit.runner.*
import android.view.LayoutInflater
import android.view.View.GONE
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import org.hamcrest.Matchers.equalTo
import org.isoron.uhabits.BaseViewTest
import org.isoron.uhabits.R
import org.junit.Assert.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
@MediumTest
@ -38,9 +40,9 @@ class NotesCardViewTest : BaseViewTest() {
override fun setUp() {
super.setUp()
view = LayoutInflater
.from(targetContext)
.inflate(R.layout.show_habit, null)
.findViewById(R.id.notesCard)
.from(targetContext)
.inflate(R.layout.show_habit, null)
.findViewById(R.id.notesCard)
view.update(NotesCardViewModel(description = "This is a test description"))
measureView(view, 800f, 200f)
}
@ -55,4 +57,4 @@ class NotesCardViewTest : BaseViewTest() {
view.update(NotesCardViewModel(description = ""))
assertThat(view.visibility, equalTo(GONE))
}
}
}

@ -18,13 +18,16 @@
*/
package org.isoron.uhabits.activities.habits.show.views
import android.view.*
import androidx.test.ext.junit.runners.*
import androidx.test.filters.*
import org.isoron.uhabits.*
import org.isoron.uhabits.core.models.*
import org.junit.*
import org.junit.runner.*
import android.view.LayoutInflater
import android.view.View
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import org.isoron.uhabits.BaseViewTest
import org.isoron.uhabits.R
import org.isoron.uhabits.core.models.PaletteColor
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
@MediumTest
@ -36,16 +39,18 @@ class OverviewCardViewTest : BaseViewTest() {
override fun setUp() {
super.setUp()
view = LayoutInflater
.from(targetContext)
.inflate(R.layout.show_habit, null)
.findViewById<View>(R.id.overviewCard) as OverviewCardView
view.update(OverviewCardViewModel(
.from(targetContext)
.inflate(R.layout.show_habit, null)
.findViewById<View>(R.id.overviewCard) as OverviewCardView
view.update(
OverviewCardViewModel(
scoreToday = 0.74f,
scoreMonthDiff = 0.23f,
scoreYearDiff = 0.74f,
totalCount = 44,
color = PaletteColor(7),
))
)
)
measureView(view, 800f, 300f)
}
@ -53,4 +58,4 @@ class OverviewCardViewTest : BaseViewTest() {
fun testRender() {
assertRenders(view, PATH + "render.png")
}
}
}

@ -18,12 +18,15 @@
*/
package org.isoron.uhabits.activities.habits.show.views
import android.view.*
import androidx.test.ext.junit.runners.*
import androidx.test.filters.*
import org.isoron.uhabits.*
import org.junit.*
import org.junit.runner.*
import android.view.LayoutInflater
import android.view.View
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import org.isoron.uhabits.BaseViewTest
import org.isoron.uhabits.R
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
@MediumTest
@ -36,9 +39,9 @@ class ScoreCardTest : BaseViewTest() {
super.setUp()
val habit = fixtures.createLongHabit()
view = LayoutInflater
.from(targetContext)
.inflate(R.layout.show_habit, null)
.findViewById<View>(R.id.scoreCard) as ScoreCard
.from(targetContext)
.inflate(R.layout.show_habit, null)
.findViewById<View>(R.id.scoreCard) as ScoreCard
view.update(ScoreCardPresenter(habit, 0).present(0))
measureView(view, 800f, 600f)
}
@ -47,4 +50,4 @@ class ScoreCardTest : BaseViewTest() {
fun testRender() {
assertRenders(view, PATH + "render.png")
}
}
}

@ -18,17 +18,15 @@
*/
package org.isoron.uhabits.activities.habits.show.views
import android.view.*
import androidx.test.ext.junit.runners.*
import org.junit.runner.RunWith
import android.view.LayoutInflater
import android.view.View
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import org.isoron.uhabits.BaseViewTest
import org.isoron.uhabits.activities.habits.show.views.StreakCardView
import org.isoron.uhabits.R
import org.isoron.uhabits.activities.habits.show.views.StreakCardViewTest
import org.isoron.uhabits.core.models.*
import org.junit.*
import java.lang.Exception
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
@MediumTest
@ -41,13 +39,15 @@ class StreakCardViewTest : BaseViewTest() {
super.setUp()
val habit = fixtures.createLongHabit()
view = LayoutInflater
.from(targetContext)
.inflate(R.layout.show_habit, null)
.findViewById<View>(R.id.streakCard) as StreakCardView
view.update(StreakCardViewModel(
.from(targetContext)
.inflate(R.layout.show_habit, null)
.findViewById<View>(R.id.streakCard) as StreakCardView
view.update(
StreakCardViewModel(
bestStreaks = habit.streaks.getBest(10),
color = habit.color,
))
)
)
measureView(view, 800f, 600f)
}
@ -55,4 +55,4 @@ class StreakCardViewTest : BaseViewTest() {
fun testRender() {
assertRenders(view, PATH + "render.png")
}
}
}

@ -18,13 +18,15 @@
*/
package org.isoron.uhabits.activities.habits.show.views
import android.view.*
import androidx.test.ext.junit.runners.*
import androidx.test.filters.*
import org.isoron.uhabits.*
import org.isoron.uhabits.core.models.*
import org.junit.*
import org.junit.runner.*
import android.view.LayoutInflater
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import org.isoron.uhabits.BaseViewTest
import org.isoron.uhabits.R
import org.isoron.uhabits.core.models.PaletteColor
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
@MediumTest
@ -36,17 +38,19 @@ class SubtitleCardViewTest : BaseViewTest() {
override fun setUp() {
super.setUp()
view = LayoutInflater
.from(targetContext)
.inflate(R.layout.show_habit, null)
.findViewById(R.id.subtitleCard)
view.update(SubtitleCardViewModel(
.from(targetContext)
.inflate(R.layout.show_habit, null)
.findViewById(R.id.subtitleCard)
view.update(
SubtitleCardViewModel(
color = PaletteColor(7),
frequencyText = "3 times in 7 days",
isNumerical = false,
question = "Did you meditate this morning?",
reminderText = "8:30 AM",
targetText = "",
))
)
)
measureView(view, 800f, 200f)
}
@ -54,4 +58,4 @@ class SubtitleCardViewTest : BaseViewTest() {
fun testRender() {
assertRenders(view, PATH + "render.png")
}
}
}

@ -19,10 +19,12 @@
package org.isoron.uhabits.database
import org.isoron.uhabits.*
import org.isoron.uhabits.core.utils.*
import org.junit.*
import java.io.*
import org.isoron.uhabits.AndroidDirFinder
import org.isoron.uhabits.BaseAndroidTest
import org.isoron.uhabits.core.utils.DateUtils
import org.junit.Test
import java.io.File
import java.io.FileOutputStream
class AutoBackupTest : BaseAndroidTest() {
@Test
@ -32,7 +34,7 @@ class AutoBackupTest : BaseAndroidTest() {
createTestFiles(basedir, 30)
val autoBackup = AutoBackup(targetContext)
autoBackup.run(keep=5)
autoBackup.run(keep = 5)
for (k in 1..25) assertDoesNotExist("${basedir.path}/test-$k.txt")
for (k in 26..30) assertExists("${basedir.path}/test-$k.txt")
@ -77,4 +79,4 @@ class AutoBackupTest : BaseAndroidTest() {
assertTrue(file.delete())
}
}
}
}

@ -18,18 +18,27 @@
*/
package org.isoron.uhabits.intents
import android.content.ContentUris.*
import androidx.test.ext.junit.runners.*
import androidx.test.filters.*
import org.hamcrest.Matchers.*
import org.isoron.uhabits.*
import org.isoron.uhabits.core.reminders.ReminderScheduler.SchedulerResult.*
import org.isoron.uhabits.receivers.*
import org.junit.*
import org.junit.Assert.*
import org.junit.runner.*
import java.util.*
import java.util.Calendar.*
import android.content.ContentUris.parseId
import androidx.test.filters.MediumTest
import org.hamcrest.Matchers.equalTo
import org.isoron.uhabits.BaseAndroidTest
import org.isoron.uhabits.core.reminders.ReminderScheduler.SchedulerResult.OK
import org.isoron.uhabits.receivers.ReminderReceiver
import org.isoron.uhabits.receivers.WidgetReceiver
import org.junit.After
import org.junit.Assert.assertThat
import org.junit.Before
import org.junit.Test
import java.util.Calendar.DAY_OF_MONTH
import java.util.Calendar.DECEMBER
import java.util.Calendar.HOUR_OF_DAY
import java.util.Calendar.JUNE
import java.util.Calendar.MAY
import java.util.Calendar.MINUTE
import java.util.Calendar.MONTH
import java.util.Calendar.YEAR
import java.util.GregorianCalendar
import java.util.TimeZone
class IntentSchedulerTest : BaseAndroidTest() {
@ -117,4 +126,4 @@ class IntentSchedulerTest : BaseAndroidTest() {
val intent = WidgetReceiver.getLastReceivedIntent()
assertNotNull(intent)
}
}
}

@ -19,15 +19,23 @@
package org.isoron.uhabits.regression
import androidx.test.filters.*
import org.isoron.uhabits.*
import org.isoron.uhabits.acceptance.steps.CommonSteps.*
import org.isoron.uhabits.acceptance.steps.CommonSteps.Screen.*
import org.isoron.uhabits.acceptance.steps.EditHabitSteps.*
import org.isoron.uhabits.acceptance.steps.ListHabitsSteps.*
import org.isoron.uhabits.acceptance.steps.ListHabitsSteps.MenuItem.*
import org.junit.*
import androidx.test.filters.LargeTest
import org.isoron.uhabits.BaseUserInterfaceTest
import org.isoron.uhabits.acceptance.steps.CommonSteps.Screen.EDIT_HABIT
import org.isoron.uhabits.acceptance.steps.CommonSteps.Screen.LIST_HABITS
import org.isoron.uhabits.acceptance.steps.CommonSteps.Screen.SELECT_HABIT_TYPE
import org.isoron.uhabits.acceptance.steps.CommonSteps.clickText
import org.isoron.uhabits.acceptance.steps.CommonSteps.launchApp
import org.isoron.uhabits.acceptance.steps.CommonSteps.longClickText
import org.isoron.uhabits.acceptance.steps.CommonSteps.verifyDisplaysText
import org.isoron.uhabits.acceptance.steps.CommonSteps.verifyShowsScreen
import org.isoron.uhabits.acceptance.steps.EditHabitSteps.clickSave
import org.isoron.uhabits.acceptance.steps.EditHabitSteps.typeName
import org.isoron.uhabits.acceptance.steps.ListHabitsSteps.MenuItem.ADD
import org.isoron.uhabits.acceptance.steps.ListHabitsSteps.MenuItem.DELETE
import org.isoron.uhabits.acceptance.steps.ListHabitsSteps.clickMenu
import org.isoron.uhabits.acceptance.steps.ListHabitsSteps.longPressCheckmarks
import org.junit.Test
@LargeTest
class ListHabitsRegressionTest : BaseUserInterfaceTest() {
@ -54,4 +62,4 @@ class ListHabitsRegressionTest : BaseUserInterfaceTest() {
verifyDisplaysText("Hello world")
longPressCheckmarks("Hello world", 3)
}
}
}

@ -19,12 +19,12 @@
package org.isoron.uhabits.regression
import androidx.test.filters.*
import org.isoron.uhabits.*
import org.isoron.uhabits.acceptance.steps.CommonSteps.*
import org.isoron.uhabits.activities.about.*
import org.junit.*
import java.lang.Thread.*
import androidx.test.filters.LargeTest
import org.isoron.uhabits.BaseUserInterfaceTest
import org.isoron.uhabits.acceptance.steps.CommonSteps.launchApp
import org.isoron.uhabits.activities.about.AboutActivity
import org.junit.Test
import java.lang.Thread.sleep
@LargeTest
class SavedStateTest : BaseUserInterfaceTest() {

@ -19,18 +19,29 @@
package org.isoron.uhabits.sync
import androidx.test.filters.*
import com.fasterxml.jackson.databind.*
import io.ktor.client.*
import io.ktor.client.engine.mock.*
import io.ktor.client.features.json.*
import io.ktor.client.request.*
import io.ktor.http.*
import junit.framework.Assert.*
import kotlinx.coroutines.*
import org.isoron.uhabits.*
import org.isoron.uhabits.core.sync.*
import org.junit.*
import androidx.test.filters.MediumTest
import com.fasterxml.jackson.databind.ObjectMapper
import io.ktor.client.HttpClient
import io.ktor.client.engine.mock.MockEngine
import io.ktor.client.engine.mock.MockRequestHandleScope
import io.ktor.client.engine.mock.respond
import io.ktor.client.engine.mock.respondError
import io.ktor.client.engine.mock.respondOk
import io.ktor.client.features.json.JsonFeature
import io.ktor.client.request.HttpRequestData
import io.ktor.client.request.HttpResponseData
import io.ktor.http.HttpStatusCode
import io.ktor.http.fullPath
import io.ktor.http.headersOf
import kotlinx.coroutines.runBlocking
import org.isoron.uhabits.BaseAndroidTest
import org.isoron.uhabits.core.sync.AbstractSyncServer
import org.isoron.uhabits.core.sync.GetDataVersionResponse
import org.isoron.uhabits.core.sync.KeyNotFoundException
import org.isoron.uhabits.core.sync.RegisterReponse
import org.isoron.uhabits.core.sync.ServiceUnavailable
import org.isoron.uhabits.core.sync.SyncData
import org.junit.Test
@MediumTest
class RemoteSyncServerTest : BaseAndroidTest() {
@ -115,23 +126,29 @@ class RemoteSyncServerTest : BaseAndroidTest() {
return@runBlocking
}
private fun server(expectedPath: String,
action: MockRequestHandleScope.(HttpRequestData) -> HttpResponseData
private fun server(
expectedPath: String,
action: MockRequestHandleScope.(HttpRequestData) -> HttpResponseData
): AbstractSyncServer {
return RemoteSyncServer(httpClient = HttpClient(MockEngine) {
install(JsonFeature)
engine {
addHandler { request ->
when (request.url.fullPath) {
expectedPath -> action(request)
else -> error("unexpected call: ${request.url.fullPath}")
return RemoteSyncServer(
httpClient = HttpClient(MockEngine) {
install(JsonFeature)
engine {
addHandler { request ->
when (request.url.fullPath) {
expectedPath -> action(request)
else -> error("unexpected call: ${request.url.fullPath}")
}
}
}
}
}, preferences = prefs)
},
preferences = prefs
)
}
private fun MockRequestHandleScope.respondWithJson(content: Any) =
respond(mapper.writeValueAsBytes(content),
headers = headersOf("Content-Type" to listOf("application/json")))
}
respond(
mapper.writeValueAsBytes(content),
headers = headersOf("Content-Type" to listOf("application/json"))
)
}

@ -18,14 +18,21 @@
*/
package org.isoron.uhabits
import android.content.*
import android.os.*
import android.view.*
import org.isoron.uhabits.inject.*
import java.io.*
import java.text.*
import java.util.*
import javax.inject.*
import android.content.Context
import android.os.Build
import android.os.Environment
import android.view.WindowManager
import org.isoron.uhabits.inject.AppContext
import java.io.BufferedReader
import java.io.File
import java.io.FileWriter
import java.io.IOException
import java.io.InputStreamReader
import java.text.SimpleDateFormat
import java.util.Date
import java.util.LinkedList
import java.util.Locale
import javax.inject.Inject
open class AndroidBugReporter @Inject constructor(@AppContext private val context: Context) {
@ -56,7 +63,7 @@ open class AndroidBugReporter @Inject constructor(@AppContext private val contex
var line: String?
while (true) {
line = bufferedReader.readLine()
if (line == null) break;
if (line == null) break
log.addLast(line)
if (log.size > maxLineCount) log.removeFirst()
}
@ -79,7 +86,7 @@ open class AndroidBugReporter @Inject constructor(@AppContext private val contex
try {
val date = SimpleDateFormat("yyyy-MM-dd HHmmss", Locale.US).format(Date())
val dir = AndroidDirFinder(context).getFilesDir("Logs")
?: throw IOException("log dir should not be null")
?: throw IOException("log dir should not be null")
val logFile = File(String.format("%s/Log %s.txt", dir.path, date))
val output = FileWriter(logFile)
output.write(getBugReport())
@ -106,5 +113,4 @@ open class AndroidBugReporter @Inject constructor(@AppContext private val contex
appendln()
}
}
}
}

@ -20,7 +20,7 @@ package org.isoron.uhabits
import android.content.Context
import androidx.core.content.ContextCompat
import org.isoron.uhabits.inject.*
import org.isoron.uhabits.inject.AppContext
import org.isoron.uhabits.utils.FileUtils
import java.io.File
import javax.inject.Inject
@ -28,8 +28,8 @@ import javax.inject.Inject
class AndroidDirFinder @Inject constructor(@param:AppContext private val context: Context) {
fun getFilesDir(relativePath: String): File? {
return FileUtils.getDir(
ContextCompat.getExternalFilesDirs(context, null),
relativePath
ContextCompat.getExternalFilesDirs(context, null),
relativePath
)
}
}
}

@ -18,12 +18,12 @@
*/
package org.isoron.uhabits
import android.app.*
import android.app.Activity
class BaseExceptionHandler(private val activity: Activity) : Thread.UncaughtExceptionHandler {
private val originalHandler: Thread.UncaughtExceptionHandler? =
Thread.getDefaultUncaughtExceptionHandler()
Thread.getDefaultUncaughtExceptionHandler()
override fun uncaughtException(thread: Thread?, ex: Throwable?) {
if (ex == null) return
@ -36,4 +36,4 @@ class BaseExceptionHandler(private val activity: Activity) : Thread.UncaughtExce
}
originalHandler?.uncaughtException(thread, ex)
}
}
}

@ -19,16 +19,19 @@
package org.isoron.uhabits
import android.app.*
import android.content.*
import org.isoron.uhabits.core.database.*
import org.isoron.uhabits.core.reminders.*
import org.isoron.uhabits.core.ui.*
import org.isoron.uhabits.core.utils.*
import org.isoron.uhabits.inject.*
import org.isoron.uhabits.utils.*
import org.isoron.uhabits.widgets.*
import java.io.*
import android.app.Application
import android.content.Context
import org.isoron.uhabits.core.database.UnsupportedDatabaseVersionException
import org.isoron.uhabits.core.reminders.ReminderScheduler
import org.isoron.uhabits.core.ui.NotificationTray
import org.isoron.uhabits.core.utils.DateUtils
import org.isoron.uhabits.inject.AppContextModule
import org.isoron.uhabits.inject.DaggerHabitsApplicationComponent
import org.isoron.uhabits.inject.HabitsApplicationComponent
import org.isoron.uhabits.inject.HabitsModule
import org.isoron.uhabits.utils.DatabaseUtils
import org.isoron.uhabits.widgets.WidgetUpdater
import java.io.File
/**
* The Android application for Loop Habit Tracker.
@ -59,10 +62,10 @@ class HabitsApplication : Application() {
val db = DatabaseUtils.getDatabaseFile(this)
HabitsApplication.component = DaggerHabitsApplicationComponent
.builder()
.appContextModule(AppContextModule(context))
.habitsModule(HabitsModule(db))
.build()
.builder()
.appContextModule(AppContextModule(context))
.habitsModule(HabitsModule(db))
.build()
DateUtils.setStartDayOffset(3, 0)

@ -19,7 +19,9 @@
package org.isoron.uhabits
import android.app.backup.*
import android.app.backup.BackupAgentHelper
import android.app.backup.FileBackupHelper
import android.app.backup.SharedPreferencesBackupHelper
/**
* An Android BackupAgentHelper customized for this application.

@ -21,17 +21,18 @@
package org.isoron.uhabits
import android.content.*
import android.database.sqlite.*
import org.isoron.uhabits.core.database.*
import org.isoron.uhabits.database.*
import java.io.*
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
import org.isoron.uhabits.core.database.MigrationHelper
import org.isoron.uhabits.core.database.UnsupportedDatabaseVersionException
import org.isoron.uhabits.database.AndroidDatabase
import java.io.File
class HabitsDatabaseOpener(
context: Context,
private val databaseFilename: String,
private val version: Int
context: Context,
private val databaseFilename: String,
private val version: Int
) : SQLiteOpenHelper(context, databaseFilename, null, version) {
override fun onCreate(db: SQLiteDatabase) {
@ -45,18 +46,22 @@ class HabitsDatabaseOpener(
db.disableWriteAheadLogging()
}
override fun onUpgrade(db: SQLiteDatabase,
oldVersion: Int,
newVersion: Int) {
override fun onUpgrade(
db: SQLiteDatabase,
oldVersion: Int,
newVersion: Int
) {
db.disableWriteAheadLogging()
if (db.version < 8) throw UnsupportedDatabaseVersionException()
val helper = MigrationHelper(AndroidDatabase(db, File(databaseFilename)))
helper.migrateTo(newVersion)
}
override fun onDowngrade(db: SQLiteDatabase,
oldVersion: Int,
newVersion: Int) {
override fun onDowngrade(
db: SQLiteDatabase,
oldVersion: Int,
newVersion: Int
) {
throw UnsupportedDatabaseVersionException()
}
}

@ -19,37 +19,39 @@
package org.isoron.uhabits.activities
import android.app.*
import android.content.*
import android.content.res.Configuration.*
import android.os.Build.VERSION.*
import androidx.core.content.*
import android.app.Activity
import android.content.Context
import android.content.res.Configuration.UI_MODE_NIGHT_MASK
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import android.os.Build.VERSION.SDK_INT
import androidx.core.content.ContextCompat
import org.isoron.uhabits.R
import org.isoron.uhabits.core.preferences.*
import org.isoron.uhabits.core.ui.*
import org.isoron.uhabits.inject.*
import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.core.ui.ThemeSwitcher
import org.isoron.uhabits.inject.ActivityContext
import org.isoron.uhabits.inject.ActivityScope
@ActivityScope
class AndroidThemeSwitcher
constructor(
@ActivityContext val context: Context,
preferences: Preferences
@ActivityContext val context: Context,
preferences: Preferences
) : ThemeSwitcher(preferences) {
override fun getSystemTheme(): Int {
if (SDK_INT < 29) return THEME_LIGHT;
if (SDK_INT < 29) return THEME_LIGHT
val uiMode = context.resources.configuration.uiMode
return if ((uiMode and UI_MODE_NIGHT_MASK) == UI_MODE_NIGHT_YES) {
THEME_DARK;
THEME_DARK
} else {
THEME_LIGHT;
THEME_LIGHT
}
}
override fun applyDarkTheme() {
context.setTheme(R.style.AppBaseThemeDark)
(context as Activity).window.navigationBarColor =
ContextCompat.getColor(context, R.color.grey_900)
ContextCompat.getColor(context, R.color.grey_900)
}
override fun applyLightTheme() {
@ -59,6 +61,6 @@ constructor(
override fun applyPureBlackTheme() {
context.setTheme(R.style.AppBaseThemeDark_PureBlack)
(context as Activity).window.navigationBarColor =
ContextCompat.getColor(context, R.color.black)
ContextCompat.getColor(context, R.color.black)
}
}

@ -18,15 +18,15 @@
*/
package org.isoron.uhabits.activities
import org.isoron.uhabits.*
import org.isoron.uhabits.core.ui.screens.habits.list.*
import org.isoron.uhabits.core.ui.screens.habits.show.*
import java.io.*
import javax.inject.*
import org.isoron.uhabits.AndroidDirFinder
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior
import org.isoron.uhabits.core.ui.screens.habits.show.ShowHabitMenuBehavior
import java.io.File
import javax.inject.Inject
class HabitsDirFinder @Inject
constructor(
private val androidDirFinder: AndroidDirFinder
private val androidDirFinder: AndroidDirFinder
) : ShowHabitMenuBehavior.System, ListHabitsBehavior.DirFinder {
override fun getCSVOutputDir(): File {

@ -18,10 +18,10 @@
*/
package org.isoron.uhabits.activities.about
import android.os.*
import androidx.appcompat.app.*
import org.isoron.uhabits.*
import org.isoron.uhabits.activities.*
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import org.isoron.uhabits.HabitsApplication
import org.isoron.uhabits.activities.AndroidThemeSwitcher
/**
* Activity that allows the user to see information about the app itself.
@ -31,10 +31,12 @@ class AboutActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val app = application as HabitsApplication
val screen = AboutScreen(this,
app.component.intentFactory,
app.component.preferences)
val screen = AboutScreen(
this,
app.component.intentFactory,
app.component.preferences
)
AndroidThemeSwitcher(this, app.component.preferences).apply()
setContentView(AboutView(this, screen))
}
}
}

@ -18,36 +18,37 @@
*/
package org.isoron.uhabits.activities.about
import org.isoron.uhabits.*
import org.isoron.uhabits.core.preferences.*
import org.isoron.uhabits.intents.*
import org.isoron.uhabits.utils.*
import org.isoron.uhabits.R
import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.intents.IntentFactory
import org.isoron.uhabits.utils.showMessage
import org.isoron.uhabits.utils.startActivitySafely
class AboutScreen(
private val activity: AboutActivity,
private val intents: IntentFactory,
private val prefs: Preferences,
private val activity: AboutActivity,
private val intents: IntentFactory,
private val prefs: Preferences,
) {
private var developerCountdown = 5
fun showRateAppWebsite() =
activity.startActivitySafely(intents.rateApp(activity))
activity.startActivitySafely(intents.rateApp(activity))
fun showSendFeedbackScreen() =
activity.startActivitySafely(intents.sendFeedback(activity))
activity.startActivitySafely(intents.sendFeedback(activity))
fun showSourceCodeWebsite() =
activity.startActivitySafely(intents.viewSourceCode(activity))
activity.startActivitySafely(intents.viewSourceCode(activity))
fun showTranslationWebsite() =
activity.startActivitySafely(intents.helpTranslate(activity))
activity.startActivitySafely(intents.helpTranslate(activity))
fun showPrivacyPolicyWebsite() =
activity.startActivitySafely(intents.privacyPolicy(activity))
activity.startActivitySafely(intents.privacyPolicy(activity))
fun showCodeContributorsWebsite() =
activity.startActivitySafely(intents.codeContributors(activity))
activity.startActivitySafely(intents.codeContributors(activity))
fun onPressDeveloperCountdown() {
developerCountdown--
@ -56,4 +57,4 @@ class AboutScreen(
activity.showMessage(activity.resources.getString(R.string.you_are_now_a_developer))
}
}
}
}

@ -18,19 +18,20 @@
*/
package org.isoron.uhabits.activities.about
import android.annotation.*
import android.content.*
import android.view.*
import android.widget.*
import org.isoron.uhabits.*
import org.isoron.uhabits.core.models.*
import org.isoron.uhabits.databinding.*
import org.isoron.uhabits.utils.*
import android.annotation.SuppressLint
import android.content.Context
import android.view.LayoutInflater
import android.widget.FrameLayout
import org.isoron.uhabits.BuildConfig
import org.isoron.uhabits.R
import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.databinding.AboutBinding
import org.isoron.uhabits.utils.setupToolbar
@SuppressLint("ViewConstructor")
class AboutView(
context: Context,
private val screen: AboutScreen,
context: Context,
private val screen: AboutScreen,
) : FrameLayout(context) {
private var binding = AboutBinding.inflate(LayoutInflater.from(context))
@ -38,9 +39,9 @@ class AboutView(
init {
addView(binding.root)
setupToolbar(
toolbar = binding.toolbar,
color = PaletteColor(11),
title = resources.getString(R.string.about)
toolbar = binding.toolbar,
color = PaletteColor(11),
title = resources.getString(R.string.about)
)
val version = resources.getString(R.string.version_n)
binding.tvContributors.setOnClickListener { screen.showCodeContributorsWebsite() }
@ -52,4 +53,4 @@ class AboutView(
binding.tvVersion.setOnClickListener { screen.onPressDeveloperCountdown() }
binding.tvVersion.text = String.format(version, BuildConfig.VERSION_NAME)
}
}
}

@ -19,23 +19,25 @@
package org.isoron.uhabits.activities.common.dialogs
import android.app.*
import android.os.*
import android.util.*
import android.view.*
import android.widget.*
import androidx.appcompat.app.*
import android.app.Dialog
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.widget.EditText
import android.widget.RadioButton
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatDialogFragment
import kotlinx.android.synthetic.main.frequency_picker_dialog.view.*
import org.isoron.uhabits.*
import org.isoron.uhabits.R
class FrequencyPickerDialog(var freqNumerator: Int,
var freqDenominator: Int
) : AppCompatDialogFragment() {
class FrequencyPickerDialog(
var freqNumerator: Int,
var freqDenominator: Int
) : AppCompatDialogFragment() {
lateinit var contentView: View
var onFrequencyPicked: (num: Int, den: Int) -> Unit = {_,_ -> }
var onFrequencyPicked: (num: Int, den: Int) -> Unit = { _, _ -> }
constructor() : this(1, 1)
@ -55,7 +57,7 @@ class FrequencyPickerDialog(var freqNumerator: Int,
}
contentView.everyXDaysTextView.setOnFocusChangeListener { v, hasFocus ->
if(hasFocus) check(contentView.everyXDaysRadioButton)
if (hasFocus) check(contentView.everyXDaysRadioButton)
}
contentView.xTimesPerWeekRadioButton.setOnClickListener {
@ -64,7 +66,7 @@ class FrequencyPickerDialog(var freqNumerator: Int,
}
contentView.xTimesPerWeekTextView.setOnFocusChangeListener { v, hasFocus ->
if(hasFocus) check(contentView.xTimesPerWeekRadioButton)
if (hasFocus) check(contentView.xTimesPerWeekRadioButton)
}
contentView.xTimesPerMonthRadioButton.setOnClickListener {
@ -73,13 +75,13 @@ class FrequencyPickerDialog(var freqNumerator: Int,
}
contentView.xTimesPerMonthTextView.setOnFocusChangeListener { v, hasFocus ->
if(hasFocus) check(contentView.xTimesPerMonthRadioButton)
if (hasFocus) check(contentView.xTimesPerMonthRadioButton)
}
return AlertDialog.Builder(activity!!)
.setView(contentView)
.setPositiveButton(R.string.save) { _, _ -> onSaveClicked() }
.create()
.setView(contentView)
.setPositiveButton(R.string.save) { _, _ -> onSaveClicked() }
.create()
}
private fun onSaveClicked() {
@ -129,7 +131,7 @@ class FrequencyPickerDialog(var freqNumerator: Int,
private fun populateViews() {
uncheckAll()
if (freqNumerator == 1) {
if(freqDenominator == 1) {
if (freqDenominator == 1) {
contentView.everyDayRadioButton.isChecked = true
} else {
contentView.everyXDaysRadioButton.isChecked = true
@ -137,7 +139,7 @@ class FrequencyPickerDialog(var freqNumerator: Int,
focus(contentView.everyXDaysTextView)
}
} else {
if(freqDenominator == 7) {
if (freqDenominator == 7) {
contentView.xTimesPerWeekRadioButton.isChecked = true
contentView.xTimesPerWeekTextView.setText(freqNumerator.toString())
focus(contentView.xTimesPerWeekTextView)
@ -169,4 +171,4 @@ class FrequencyPickerDialog(var freqNumerator: Int,
contentView.xTimesPerWeekTextView.clearFocus()
contentView.xTimesPerMonthTextView.clearFocus()
}
}
}

@ -19,26 +19,31 @@
package org.isoron.uhabits.activities.common.dialogs
import android.content.*
import androidx.appcompat.app.*
import android.text.*
import android.view.*
import android.view.WindowManager.LayoutParams.*
import android.view.inputmethod.*
import android.widget.*
import android.content.Context
import android.content.DialogInterface
import android.text.InputFilter
import android.view.LayoutInflater
import android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE
import android.view.inputmethod.EditorInfo
import android.widget.EditText
import android.widget.NumberPicker
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import org.isoron.uhabits.R
import org.isoron.uhabits.core.ui.screens.habits.list.*
import org.isoron.uhabits.inject.*
import org.isoron.uhabits.utils.*
import javax.inject.*
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior
import org.isoron.uhabits.inject.ActivityContext
import org.isoron.uhabits.utils.InterfaceUtils
import javax.inject.Inject
class NumberPickerFactory
@Inject constructor(
@ActivityContext private val context: Context
@ActivityContext private val context: Context
) {
fun create(value: Double,
unit: String,
callback: ListHabitsBehavior.NumberPickerCallback): AlertDialog {
fun create(
value: Double,
unit: String,
callback: ListHabitsBehavior.NumberPickerCallback
): AlertDialog {
val inflater = LayoutInflater.from(context)
val view = inflater.inflate(R.layout.number_picker_dialog, null)
@ -63,28 +68,31 @@ class NumberPickerFactory
tvUnit.text = unit
val dialog = AlertDialog.Builder(context)
.setView(view)
.setTitle(R.string.change_value)
.setPositiveButton(android.R.string.ok) { _, _ ->
picker.clearFocus()
val v = picker.value + 0.05 * picker2.value
callback.onNumberPicked(v)
}
.setOnDismissListener{
callback.onNumberPickerDismissed()
}
.create()
.setView(view)
.setTitle(R.string.change_value)
.setPositiveButton(android.R.string.ok) { _, _ ->
picker.clearFocus()
val v = picker.value + 0.05 * picker2.value
callback.onNumberPicked(v)
}
.setOnDismissListener {
callback.onNumberPickerDismissed()
}
.create()
dialog.setOnShowListener {
picker.getChildAt(0)?.requestFocus()
dialog.window?.setSoftInputMode(SOFT_INPUT_STATE_ALWAYS_VISIBLE)
}
InterfaceUtils.setupEditorAction(picker, TextView.OnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_DONE)
dialog.getButton(DialogInterface.BUTTON_POSITIVE).performClick()
false
})
InterfaceUtils.setupEditorAction(
picker,
TextView.OnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_DONE)
dialog.getButton(DialogInterface.BUTTON_POSITIVE).performClick()
false
}
)
return dialog
}

@ -19,19 +19,21 @@
package org.isoron.uhabits.activities.common.views
import android.content.*
import android.view.*
import android.widget.*
import org.isoron.uhabits.core.tasks.*
import android.content.Context
import android.view.View
import android.widget.ProgressBar
import org.isoron.uhabits.core.tasks.Task
import org.isoron.uhabits.core.tasks.TaskRunner
class TaskProgressBar(
context: Context,
private val runner: TaskRunner
context: Context,
private val runner: TaskRunner
) : ProgressBar(
context,
null,
android.R.attr.progressBarStyleHorizontal
), TaskRunner.Listener {
context,
null,
android.R.attr.progressBarStyleHorizontal
),
TaskRunner.Listener {
init {
visibility = View.GONE

@ -19,25 +19,38 @@
package org.isoron.uhabits.activities.habits.edit
import android.annotation.*
import android.content.res.*
import android.graphics.*
import android.os.*
import android.text.format.*
import android.view.*
import android.widget.*
import androidx.appcompat.app.*
import androidx.fragment.app.*
import com.android.datetimepicker.time.*
import android.annotation.SuppressLint
import android.content.res.ColorStateList
import android.graphics.Color
import android.os.Bundle
import android.text.format.DateFormat
import android.view.View
import android.widget.ArrayAdapter
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.DialogFragment
import com.android.datetimepicker.time.RadialPickerLayout
import com.android.datetimepicker.time.TimePickerDialog
import kotlinx.android.synthetic.main.activity_edit_habit.*
import org.isoron.uhabits.*
import org.isoron.uhabits.activities.*
import org.isoron.uhabits.activities.common.dialogs.*
import org.isoron.uhabits.core.commands.*
import org.isoron.uhabits.core.models.*
import org.isoron.uhabits.databinding.*
import org.isoron.uhabits.utils.*
import org.isoron.uhabits.HabitsApplication
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.WeekdayPickerDialog
import org.isoron.uhabits.core.commands.CommandRunner
import org.isoron.uhabits.core.commands.CreateHabitCommand
import org.isoron.uhabits.core.commands.EditHabitCommand
import org.isoron.uhabits.core.models.Frequency
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.PaletteColor
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.formatTime
import org.isoron.uhabits.utils.toFormattedString
import org.isoron.uhabits.utils.toThemedAndroidColor
class EditHabitActivity : AppCompatActivity() {
@ -143,7 +156,7 @@ class EditHabitActivity : AppCompatActivity() {
arrayAdapter.add(getString(R.string.every_week))
arrayAdapter.add(getString(R.string.every_month))
builder.setAdapter(arrayAdapter) { dialog, which ->
freqDen = when(which) {
freqDen = when (which) {
1 -> 7
2 -> 30
else -> 1
@ -159,20 +172,26 @@ class EditHabitActivity : AppCompatActivity() {
val currentHour = if (reminderHour >= 0) reminderHour else 8
val currentMin = if (reminderMin >= 0) reminderMin else 0
val is24HourMode = DateFormat.is24HourFormat(this)
val dialog = TimePickerDialog.newInstance(object : TimePickerDialog.OnTimeSetListener {
override fun onTimeSet(view: RadialPickerLayout?, hourOfDay: Int, minute: Int) {
reminderHour = hourOfDay
reminderMin = minute
populateReminder()
}
override fun onTimeCleared(view: RadialPickerLayout?) {
reminderHour = -1
reminderMin = -1
reminderDays = WeekdayList.EVERY_DAY
populateReminder()
}
}, currentHour, currentMin, is24HourMode, androidColor)
val dialog = TimePickerDialog.newInstance(
object : TimePickerDialog.OnTimeSetListener {
override fun onTimeSet(view: RadialPickerLayout?, hourOfDay: Int, minute: Int) {
reminderHour = hourOfDay
reminderMin = minute
populateReminder()
}
override fun onTimeCleared(view: RadialPickerLayout?) {
reminderHour = -1
reminderMin = -1
reminderDays = WeekdayList.EVERY_DAY
populateReminder()
}
},
currentHour,
currentMin,
is24HourMode,
androidColor
)
dialog.show(supportFragmentManager, "timePicker")
}
@ -188,7 +207,7 @@ class EditHabitActivity : AppCompatActivity() {
}
binding.buttonSave.setOnClickListener {
if(validate()) save()
if (validate()) save()
}
for (fragment in supportFragmentManager.fragments) {
@ -226,14 +245,16 @@ class EditHabitActivity : AppCompatActivity() {
val command = if (habitId >= 0) {
EditHabitCommand(
component.habitList,
habitId,
habit)
component.habitList,
habitId,
habit
)
} else {
CreateHabitCommand(
component.modelFactory,
component.habitList,
habit)
component.modelFactory,
component.habitList,
habit
)
}
component.commandRunner.run(command)
finish()
@ -246,11 +267,11 @@ class EditHabitActivity : AppCompatActivity() {
isValid = false
}
if (habitType == Habit.NUMBER_HABIT) {
if(unitInput.text.isEmpty()) {
if (unitInput.text.isEmpty()) {
unitInput.error = getString(R.string.validation_cannot_be_blank)
isValid = false
}
if(targetInput.text.isEmpty()) {
if (targetInput.text.isEmpty()) {
targetInput.error = getString(R.string.validation_cannot_be_blank)
isValid = false
}
@ -282,7 +303,7 @@ class EditHabitActivity : AppCompatActivity() {
freqDen == 31 -> getString(R.string.x_times_per_month, freqNum)
else -> "Unknown"
}
binding.numericalFrequencyPicker.text = when(freqDen) {
binding.numericalFrequencyPicker.text = when (freqDen) {
1 -> getString(R.string.every_day)
7 -> getString(R.string.every_week)
30 -> getString(R.string.every_month)

@ -19,20 +19,24 @@
package org.isoron.uhabits.activities.habits.edit
import android.os.*
import android.view.*
import androidx.appcompat.app.*
import org.isoron.uhabits.*
import org.isoron.uhabits.core.models.*
import org.isoron.uhabits.databinding.*
import org.isoron.uhabits.intents.*
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatDialogFragment
import org.isoron.uhabits.R
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.databinding.SelectHabitTypeBinding
import org.isoron.uhabits.intents.IntentFactory
class HabitTypeDialog : AppCompatDialogFragment() {
override fun getTheme() = R.style.Translucent
override fun onCreateView(inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?): View? {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val binding = SelectHabitTypeBinding.inflate(inflater, container, false)
binding.buttonYesNo.setOnClickListener {
@ -53,4 +57,4 @@ class HabitTypeDialog : AppCompatDialogFragment() {
return binding.root
}
}
}

@ -19,22 +19,26 @@
package org.isoron.uhabits.activities.habits.list
import android.content.*
import android.os.*
import android.view.*
import androidx.appcompat.app.*
import kotlinx.coroutines.*
import org.isoron.uhabits.*
import org.isoron.uhabits.activities.*
import org.isoron.uhabits.activities.habits.list.views.*
import org.isoron.uhabits.core.preferences.*
import org.isoron.uhabits.core.sync.*
import org.isoron.uhabits.core.tasks.*
import org.isoron.uhabits.core.ui.ThemeSwitcher.*
import org.isoron.uhabits.core.utils.*
import org.isoron.uhabits.database.*
import org.isoron.uhabits.inject.*
import org.isoron.uhabits.utils.*
import android.content.Intent
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.isoron.uhabits.BaseExceptionHandler
import org.isoron.uhabits.HabitsApplication
import org.isoron.uhabits.activities.habits.list.views.HabitCardListAdapter
import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.core.sync.SyncManager
import org.isoron.uhabits.core.tasks.TaskRunner
import org.isoron.uhabits.core.ui.ThemeSwitcher.THEME_DARK
import org.isoron.uhabits.core.utils.MidnightTimer
import org.isoron.uhabits.database.AutoBackup
import org.isoron.uhabits.inject.ActivityContextModule
import org.isoron.uhabits.inject.DaggerHabitsActivityComponent
import org.isoron.uhabits.utils.restartWithFade
class ListHabitsActivity : AppCompatActivity() {
@ -55,10 +59,10 @@ class ListHabitsActivity : AppCompatActivity() {
val appComponent = (applicationContext as HabitsApplication).component
val component = DaggerHabitsActivityComponent
.builder()
.activityContextModule(ActivityContextModule(this))
.habitsApplicationComponent(appComponent)
.build()
.builder()
.activityContextModule(ActivityContextModule(this))
.habitsApplicationComponent(appComponent)
.build()
component.themeSwitcher.apply()
prefs = appComponent.preferences

@ -19,22 +19,25 @@
package org.isoron.uhabits.activities.habits.list
import android.content.*
import android.view.*
import androidx.appcompat.app.*
import android.content.Context
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity
import org.isoron.uhabits.R
import org.isoron.uhabits.core.preferences.*
import org.isoron.uhabits.core.ui.*
import org.isoron.uhabits.core.ui.screens.habits.list.*
import org.isoron.uhabits.inject.*
import javax.inject.*
import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.core.ui.ThemeSwitcher
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsMenuBehavior
import org.isoron.uhabits.inject.ActivityContext
import org.isoron.uhabits.inject.ActivityScope
import javax.inject.Inject
@ActivityScope
class ListHabitsMenu @Inject constructor(
@ActivityContext context: Context,
private val preferences: Preferences,
private val themeSwitcher: ThemeSwitcher,
private val behavior: ListHabitsMenuBehavior
@ActivityContext context: Context,
private val preferences: Preferences,
private val themeSwitcher: ThemeSwitcher,
private val behavior: ListHabitsMenuBehavior
) {
val activity = (context as AppCompatActivity)

@ -19,18 +19,21 @@
package org.isoron.uhabits.activities.habits.list
import android.content.*
import dagger.*
import org.isoron.uhabits.*
import org.isoron.uhabits.activities.*
import org.isoron.uhabits.activities.habits.list.views.*
import org.isoron.uhabits.core.ui.screens.habits.list.*
import org.isoron.uhabits.inject.*
import javax.inject.*
import android.content.Context
import dagger.Binds
import dagger.Module
import org.isoron.uhabits.AndroidBugReporter
import org.isoron.uhabits.activities.HabitsDirFinder
import org.isoron.uhabits.activities.habits.list.views.HabitCardListAdapter
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsMenuBehavior
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsSelectionMenuBehavior
import org.isoron.uhabits.inject.AppContext
import javax.inject.Inject
class BugReporterProxy
@Inject constructor(
@AppContext context: Context
@AppContext context: Context
) : AndroidBugReporter(context), ListHabitsBehavior.BugReporter
@Module

@ -19,33 +19,50 @@
package org.isoron.uhabits.activities.habits.list
import android.content.*
import android.view.ViewGroup.LayoutParams.*
import android.widget.*
import android.content.Context
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.widget.FrameLayout
import android.widget.RelativeLayout
import org.isoron.uhabits.R
import org.isoron.uhabits.activities.common.views.*
import org.isoron.uhabits.activities.habits.list.views.*
import org.isoron.uhabits.core.models.*
import org.isoron.uhabits.core.preferences.*
import org.isoron.uhabits.core.tasks.*
import org.isoron.uhabits.core.ui.screens.habits.list.*
import org.isoron.uhabits.core.utils.*
import org.isoron.uhabits.inject.*
import org.isoron.uhabits.utils.*
import java.lang.Math.*
import javax.inject.*
import org.isoron.uhabits.activities.common.views.ScrollableChart
import org.isoron.uhabits.activities.common.views.TaskProgressBar
import org.isoron.uhabits.activities.habits.list.views.EmptyListView
import org.isoron.uhabits.activities.habits.list.views.HabitCardListAdapter
import org.isoron.uhabits.activities.habits.list.views.HabitCardListView
import org.isoron.uhabits.activities.habits.list.views.HabitCardListViewFactory
import org.isoron.uhabits.activities.habits.list.views.HeaderView
import org.isoron.uhabits.activities.habits.list.views.HintView
import org.isoron.uhabits.core.models.ModelObservable
import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.core.tasks.TaskRunner
import org.isoron.uhabits.core.ui.screens.habits.list.HintListFactory
import org.isoron.uhabits.core.utils.MidnightTimer
import org.isoron.uhabits.inject.ActivityContext
import org.isoron.uhabits.inject.ActivityScope
import org.isoron.uhabits.utils.addAtBottom
import org.isoron.uhabits.utils.addAtTop
import org.isoron.uhabits.utils.addBelow
import org.isoron.uhabits.utils.buildToolbar
import org.isoron.uhabits.utils.dim
import org.isoron.uhabits.utils.dp
import org.isoron.uhabits.utils.setupToolbar
import org.isoron.uhabits.utils.sres
import java.lang.Math.max
import java.lang.Math.min
import javax.inject.Inject
const val MAX_CHECKMARK_COUNT = 60
@ActivityScope
class ListHabitsRootView @Inject constructor(
@ActivityContext context: Context,
hintListFactory: HintListFactory,
preferences: Preferences,
midnightTimer: MidnightTimer,
runner: TaskRunner,
private val listAdapter: HabitCardListAdapter,
habitCardListViewFactory: HabitCardListViewFactory
@ActivityContext context: Context,
hintListFactory: HintListFactory,
preferences: Preferences,
midnightTimer: MidnightTimer,
runner: TaskRunner,
private val listAdapter: HabitCardListAdapter,
habitCardListViewFactory: HabitCardListViewFactory
) : FrameLayout(context), ModelObservable.Listener {
val listView: HabitCardListView = habitCardListViewFactory.create()
@ -72,10 +89,10 @@ class ListHabitsRootView @Inject constructor(
addAtBottom(hintView)
}
rootView.setupToolbar(
toolbar = tbar,
title = resources.getString(R.string.main_activity_title),
color = PaletteColor(17),
displayHomeAsUpEnabled = false,
toolbar = tbar,
title = resources.getString(R.string.main_activity_title),
color = PaletteColor(17),
displayHomeAsUpEnabled = false,
)
addView(rootView, MATCH_PARENT, MATCH_PARENT)
listAdapter.setListView(listView)
@ -86,11 +103,13 @@ class ListHabitsRootView @Inject constructor(
}
private fun setupControllers() {
header.setScrollController(object : ScrollableChart.ScrollController {
override fun onDataOffsetChanged(newDataOffset: Int) {
listView.dataOffset = newDataOffset
header.setScrollController(
object : ScrollableChart.ScrollController {
override fun onDataOffsetChanged(newDataOffset: Int) {
listView.dataOffset = newDataOffset
}
}
})
)
}
override fun onAttachedToWindow() {

@ -19,29 +19,58 @@
package org.isoron.uhabits.activities.habits.list
import android.app.*
import android.content.*
import android.util.*
import androidx.annotation.*
import androidx.appcompat.app.*
import dagger.*
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import dagger.Lazy
import org.isoron.uhabits.R
import org.isoron.uhabits.activities.common.dialogs.*
import org.isoron.uhabits.activities.habits.edit.*
import org.isoron.uhabits.activities.habits.list.views.*
import org.isoron.uhabits.core.commands.*
import org.isoron.uhabits.core.models.*
import org.isoron.uhabits.core.tasks.*
import org.isoron.uhabits.core.ui.*
import org.isoron.uhabits.core.ui.callbacks.*
import org.isoron.uhabits.core.ui.screens.habits.list.*
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior.Message.*
import org.isoron.uhabits.inject.*
import org.isoron.uhabits.intents.*
import org.isoron.uhabits.tasks.*
import org.isoron.uhabits.utils.*
import java.io.*
import javax.inject.*
import org.isoron.uhabits.activities.common.dialogs.ColorPickerDialogFactory
import org.isoron.uhabits.activities.common.dialogs.ConfirmDeleteDialogFactory
import org.isoron.uhabits.activities.common.dialogs.ConfirmSyncKeyDialogFactory
import org.isoron.uhabits.activities.common.dialogs.NumberPickerFactory
import org.isoron.uhabits.activities.habits.edit.HabitTypeDialog
import org.isoron.uhabits.activities.habits.list.views.HabitCardListAdapter
import org.isoron.uhabits.core.commands.ArchiveHabitsCommand
import org.isoron.uhabits.core.commands.ChangeHabitColorCommand
import org.isoron.uhabits.core.commands.Command
import org.isoron.uhabits.core.commands.CommandRunner
import org.isoron.uhabits.core.commands.CreateHabitCommand
import org.isoron.uhabits.core.commands.DeleteHabitsCommand
import org.isoron.uhabits.core.commands.EditHabitCommand
import org.isoron.uhabits.core.commands.UnarchiveHabitsCommand
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.tasks.TaskRunner
import org.isoron.uhabits.core.ui.ThemeSwitcher
import org.isoron.uhabits.core.ui.callbacks.OnColorPickedCallback
import org.isoron.uhabits.core.ui.callbacks.OnConfirmedCallback
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior.Message.COULD_NOT_EXPORT
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior.Message.COULD_NOT_GENERATE_BUG_REPORT
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior.Message.DATABASE_REPAIRED
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior.Message.FILE_NOT_RECOGNIZED
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior.Message.IMPORT_FAILED
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior.Message.IMPORT_SUCCESSFUL
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior.Message.SYNC_ENABLED
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior.Message.SYNC_KEY_ALREADY_INSTALLED
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsMenuBehavior
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsSelectionMenuBehavior
import org.isoron.uhabits.inject.ActivityContext
import org.isoron.uhabits.inject.ActivityScope
import org.isoron.uhabits.intents.IntentFactory
import org.isoron.uhabits.tasks.ExportDBTaskFactory
import org.isoron.uhabits.tasks.ImportDataTask
import org.isoron.uhabits.tasks.ImportDataTaskFactory
import org.isoron.uhabits.utils.copyTo
import org.isoron.uhabits.utils.restartWithFade
import org.isoron.uhabits.utils.showMessage
import org.isoron.uhabits.utils.showSendEmailScreen
import org.isoron.uhabits.utils.showSendFileScreen
import java.io.File
import java.io.IOException
import javax.inject.Inject
const val RESULT_IMPORT_DATA = 101
const val RESULT_EXPORT_CSV = 102
@ -54,19 +83,19 @@ const val REQUEST_SETTINGS = 107
@ActivityScope
class ListHabitsScreen
@Inject constructor(
@ActivityContext val context: Context,
private val commandRunner: CommandRunner,
private val intentFactory: IntentFactory,
private val themeSwitcher: ThemeSwitcher,
private val adapter: HabitCardListAdapter,
private val taskRunner: TaskRunner,
private val exportDBFactory: ExportDBTaskFactory,
private val importTaskFactory: ImportDataTaskFactory,
private val confirmDeleteDialogFactory: ConfirmDeleteDialogFactory,
private val confirmSyncKeyDialogFactory: ConfirmSyncKeyDialogFactory,
private val colorPickerFactory: ColorPickerDialogFactory,
private val numberPickerFactory: NumberPickerFactory,
private val behavior: Lazy<ListHabitsBehavior>
@ActivityContext val context: Context,
private val commandRunner: CommandRunner,
private val intentFactory: IntentFactory,
private val themeSwitcher: ThemeSwitcher,
private val adapter: HabitCardListAdapter,
private val taskRunner: TaskRunner,
private val exportDBFactory: ExportDBTaskFactory,
private val importTaskFactory: ImportDataTaskFactory,
private val confirmDeleteDialogFactory: ConfirmDeleteDialogFactory,
private val confirmSyncKeyDialogFactory: ConfirmSyncKeyDialogFactory,
private val colorPickerFactory: ColorPickerDialogFactory,
private val numberPickerFactory: NumberPickerFactory,
private val behavior: Lazy<ListHabitsBehavior>
) : CommandRunner.Listener,
ListHabitsBehavior.Screen,
ListHabitsMenuBehavior.Screen,
@ -76,7 +105,7 @@ class ListHabitsScreen
fun onAttached() {
commandRunner.addListener(this)
if(activity.intent.action == "android.intent.action.VIEW") {
if (activity.intent.action == "android.intent.action.VIEW") {
val uri = activity.intent.data!!.toString()
val parts = uri.replace(Regex("^.*sync/"), "").split("#")
val syncKey = parts[0]
@ -172,16 +201,20 @@ class ListHabitsScreen
}
override fun showMessage(m: ListHabitsBehavior.Message) {
activity.showMessage(activity.resources.getString(when (m) {
COULD_NOT_EXPORT -> R.string.could_not_export
IMPORT_SUCCESSFUL -> R.string.habits_imported
IMPORT_FAILED -> R.string.could_not_import
DATABASE_REPAIRED -> R.string.database_repaired
COULD_NOT_GENERATE_BUG_REPORT -> R.string.bug_report_failed
FILE_NOT_RECOGNIZED -> R.string.file_not_recognized
SYNC_ENABLED -> R.string.sync_enabled
SYNC_KEY_ALREADY_INSTALLED -> R.string.sync_key_already_installed
}))
activity.showMessage(
activity.resources.getString(
when (m) {
COULD_NOT_EXPORT -> R.string.could_not_export
IMPORT_SUCCESSFUL -> R.string.habits_imported
IMPORT_FAILED -> R.string.could_not_import
DATABASE_REPAIRED -> R.string.database_repaired
COULD_NOT_GENERATE_BUG_REPORT -> R.string.bug_report_failed
FILE_NOT_RECOGNIZED -> R.string.file_not_recognized
SYNC_ENABLED -> R.string.sync_enabled
SYNC_KEY_ALREADY_INSTALLED -> R.string.sync_key_already_installed
}
)
)
}
override fun showSendBugReportToDeveloperScreen(log: String) {
@ -199,16 +232,20 @@ class ListHabitsScreen
activity.startActivityForResult(intent, REQUEST_SETTINGS)
}
override fun showColorPicker(defaultColor: PaletteColor,
callback: OnColorPickedCallback) {
override fun showColorPicker(
defaultColor: PaletteColor,
callback: OnColorPickedCallback
) {
val picker = colorPickerFactory.create(defaultColor)
picker.setListener(callback)
picker.show(activity.supportFragmentManager, "picker")
}
override fun showNumberPicker(value: Double,
unit: String,
callback: ListHabitsBehavior.NumberPickerCallback) {
override fun showNumberPicker(
value: Double,
unit: String,
callback: ListHabitsBehavior.NumberPickerCallback
) {
numberPickerFactory.create(value, unit, callback).show()
}
@ -219,49 +256,61 @@ class ListHabitsScreen
private fun getExecuteString(command: Command): String? {
when (command) {
is ArchiveHabitsCommand -> {
return activity.resources.getQuantityString(R.plurals.toast_habits_archived,
command.selected.size)
return activity.resources.getQuantityString(
R.plurals.toast_habits_archived,
command.selected.size
)
}
is ChangeHabitColorCommand -> {
return activity.resources.getQuantityString(R.plurals.toast_habits_changed,
command.selected.size)
return activity.resources.getQuantityString(
R.plurals.toast_habits_changed,
command.selected.size
)
}
is CreateHabitCommand -> {
return activity.resources.getString(R.string.toast_habit_created)
}
is DeleteHabitsCommand -> {
return activity.resources.getQuantityString(R.plurals.toast_habits_deleted,
command.selected.size)
return activity.resources.getQuantityString(
R.plurals.toast_habits_deleted,
command.selected.size
)
}
is EditHabitCommand -> {
return activity.resources.getQuantityString(R.plurals.toast_habits_changed, 1)
}
is UnarchiveHabitsCommand -> {
return activity.resources.getQuantityString(R.plurals.toast_habits_unarchived,
command.selected.size)
return activity.resources.getQuantityString(
R.plurals.toast_habits_unarchived,
command.selected.size
)
}
else -> return null
}
}
private fun onImportData(file: File, onFinished: () -> Unit) {
taskRunner.execute(importTaskFactory.create(file) { result ->
if (result == ImportDataTask.SUCCESS) {
adapter.refresh()
activity.showMessage(activity.resources.getString(R.string.habits_imported))
} else if (result == ImportDataTask.NOT_RECOGNIZED) {
activity.showMessage(activity.resources.getString(R.string.file_not_recognized))
} else {
activity.showMessage(activity.resources.getString(R.string.could_not_import))
taskRunner.execute(
importTaskFactory.create(file) { result ->
if (result == ImportDataTask.SUCCESS) {
adapter.refresh()
activity.showMessage(activity.resources.getString(R.string.habits_imported))
} else if (result == ImportDataTask.NOT_RECOGNIZED) {
activity.showMessage(activity.resources.getString(R.string.file_not_recognized))
} else {
activity.showMessage(activity.resources.getString(R.string.could_not_import))
}
onFinished()
}
onFinished()
})
)
}
private fun onExportDB() {
taskRunner.execute(exportDBFactory.create { filename ->
if (filename != null) activity.showSendFileScreen(filename)
else activity.showMessage(activity.resources.getString(R.string.could_not_export))
})
taskRunner.execute(
exportDBFactory.create { filename ->
if (filename != null) activity.showSendFileScreen(filename)
else activity.showMessage(activity.resources.getString(R.string.could_not_export))
}
)
}
}

@ -19,30 +19,33 @@
package org.isoron.uhabits.activities.habits.list
import android.content.*
import android.view.*
import androidx.appcompat.app.*
import android.content.Context
import android.view.Menu
import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import dagger.*
import dagger.Lazy
import org.isoron.uhabits.R
import org.isoron.uhabits.activities.habits.list.views.*
import org.isoron.uhabits.core.commands.*
import org.isoron.uhabits.core.preferences.*
import org.isoron.uhabits.core.ui.*
import org.isoron.uhabits.core.ui.screens.habits.list.*
import org.isoron.uhabits.core.utils.*
import org.isoron.uhabits.inject.*
import javax.inject.*
import org.isoron.uhabits.activities.habits.list.views.HabitCardListAdapter
import org.isoron.uhabits.activities.habits.list.views.HabitCardListController
import org.isoron.uhabits.core.commands.CommandRunner
import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.core.ui.NotificationTray
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsSelectionMenuBehavior
import org.isoron.uhabits.core.utils.DateUtils
import org.isoron.uhabits.inject.ActivityContext
import org.isoron.uhabits.inject.ActivityScope
import javax.inject.Inject
@ActivityScope
class ListHabitsSelectionMenu @Inject constructor(
@ActivityContext context: Context,
private val listAdapter: HabitCardListAdapter,
var commandRunner: CommandRunner,
private val prefs: Preferences,
private val behavior: ListHabitsSelectionMenuBehavior,
private val listController: Lazy<HabitCardListController>,
private val notificationTray: NotificationTray
@ActivityContext context: Context,
private val listAdapter: HabitCardListAdapter,
var commandRunner: CommandRunner,
private val prefs: Preferences,
private val behavior: ListHabitsSelectionMenuBehavior,
private val listController: Lazy<HabitCardListController>,
private val notificationTray: NotificationTray
) : ActionMode.Callback {
val activity = (context as AppCompatActivity)

@ -19,17 +19,18 @@
package org.isoron.uhabits.activities.habits.list.views
import android.content.*
import android.view.*
import android.view.View.MeasureSpec.*
import android.widget.*
import org.isoron.uhabits.*
import org.isoron.uhabits.core.preferences.*
import org.isoron.uhabits.utils.*
import android.content.Context
import android.view.View
import android.view.View.MeasureSpec.EXACTLY
import android.widget.LinearLayout
import org.isoron.uhabits.R
import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.utils.dim
import org.isoron.uhabits.utils.toMeasureSpec
abstract class ButtonPanelView<T : View>(
context: Context,
val preferences: Preferences
context: Context,
val preferences: Preferences
) : LinearLayout(context),
Preferences.Listener {
@ -79,10 +80,12 @@ abstract class ButtonPanelView<T : View>(
val buttonWidth = dim(R.dimen.checkmarkWidth)
val buttonHeight = dim(R.dimen.checkmarkHeight)
val width = (buttonWidth * buttonCount)
super.onMeasure(width.toMeasureSpec(EXACTLY),
buttonHeight.toMeasureSpec(EXACTLY))
super.onMeasure(
width.toMeasureSpec(EXACTLY),
buttonHeight.toMeasureSpec(EXACTLY)
)
}
protected abstract fun setupButtons()
protected abstract fun createButton(): T
}
}

@ -19,26 +19,35 @@
package org.isoron.uhabits.activities.habits.list.views
import android.content.*
import android.graphics.*
import android.text.*
import android.view.*
import android.view.View.MeasureSpec.*
import com.google.auto.factory.*
import org.isoron.uhabits.*
import org.isoron.uhabits.core.models.*
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.RectF
import android.text.TextPaint
import android.view.HapticFeedbackConstants
import android.view.View
import android.view.View.MeasureSpec.EXACTLY
import com.google.auto.factory.AutoFactory
import com.google.auto.factory.Provided
import org.isoron.uhabits.R
import org.isoron.uhabits.core.models.Entry
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_MANUAL
import org.isoron.uhabits.core.preferences.*
import org.isoron.uhabits.inject.*
import org.isoron.uhabits.utils.*
import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.inject.ActivityContext
import org.isoron.uhabits.utils.dim
import org.isoron.uhabits.utils.getFontAwesome
import org.isoron.uhabits.utils.showMessage
import org.isoron.uhabits.utils.sres
import org.isoron.uhabits.utils.toMeasureSpec
@AutoFactory
class CheckmarkButtonView(
@Provided @ActivityContext context: Context,
@Provided val preferences: Preferences
@Provided @ActivityContext context: Context,
@Provided val preferences: Preferences
) : View(context),
View.OnClickListener,
View.OnLongClickListener {
@ -65,7 +74,7 @@ class CheckmarkButtonView(
}
fun performToggle() {
value = if(preferences.isSkipEnabled) {
value = if (preferences.isSkipEnabled) {
Entry.nextToggleValueWithSkip(value)
} else {
Entry.nextToggleValueWithoutSkip(value)
@ -93,8 +102,10 @@ class CheckmarkButtonView(
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val height = resources.getDimensionPixelSize(R.dimen.checkmarkHeight)
val width = resources.getDimensionPixelSize(R.dimen.checkmarkWidth)
super.onMeasure(width.toMeasureSpec(EXACTLY),
height.toMeasureSpec(EXACTLY))
super.onMeasure(
width.toMeasureSpec(EXACTLY),
height.toMeasureSpec(EXACTLY)
)
}
private inner class Drawer {
@ -118,7 +129,7 @@ class CheckmarkButtonView(
SKIP -> R.string.fa_skipped
NO -> R.string.fa_times
UNKNOWN -> {
if(preferences.areQuestionMarksEnabled()) R.string.fa_question
if (preferences.areQuestionMarksEnabled()) R.string.fa_question
else R.string.fa_times
}
else -> R.string.fa_check

@ -19,19 +19,20 @@
package org.isoron.uhabits.activities.habits.list.views
import android.content.*
import com.google.auto.factory.*
import org.isoron.uhabits.core.models.*
import android.content.Context
import com.google.auto.factory.AutoFactory
import com.google.auto.factory.Provided
import org.isoron.uhabits.core.models.Entry.Companion.UNKNOWN
import org.isoron.uhabits.core.preferences.*
import org.isoron.uhabits.core.utils.*
import org.isoron.uhabits.inject.*
import org.isoron.uhabits.core.models.Timestamp
import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.core.utils.DateUtils
import org.isoron.uhabits.inject.ActivityContext
@AutoFactory
class CheckmarkPanelView(
@Provided @ActivityContext context: Context,
@Provided preferences: Preferences,
@Provided private val buttonFactory: CheckmarkButtonViewFactory
@Provided @ActivityContext context: Context,
@Provided preferences: Preferences,
@Provided private val buttonFactory: CheckmarkButtonViewFactory
) : ButtonPanelView<CheckmarkButtonView>(context, preferences) {
var values = IntArray(0)
@ -46,7 +47,7 @@ class CheckmarkPanelView(
setupButtons()
}
var onToggle: (Timestamp, Int) -> Unit = {_, _ ->}
var onToggle: (Timestamp, Int) -> Unit = { _, _ -> }
set(value) {
field = value
setupButtons()

@ -19,13 +19,19 @@
package org.isoron.uhabits.activities.habits.list.views
import android.content.*
import android.view.*
import android.view.Gravity.*
import android.view.ViewGroup.LayoutParams.*
import android.widget.*
import org.isoron.uhabits.*
import org.isoron.uhabits.utils.*
import android.content.Context
import android.view.Gravity.CENTER
import android.view.View
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.widget.LinearLayout
import android.widget.TextView
import org.isoron.uhabits.R
import org.isoron.uhabits.utils.dp
import org.isoron.uhabits.utils.getFontAwesome
import org.isoron.uhabits.utils.sp
import org.isoron.uhabits.utils.sres
import org.isoron.uhabits.utils.str
class EmptyListView(context: Context) : LinearLayout(context) {
init {
@ -33,19 +39,27 @@ class EmptyListView(context: Context) : LinearLayout(context) {
gravity = CENTER
visibility = View.GONE
addView(TextView(context).apply {
text = str(R.string.fa_star_half_o)
typeface = getFontAwesome()
textSize = sp(40.0f)
gravity = CENTER
setTextColor(sres.getColor(R.attr.mediumContrastTextColor))
}, MATCH_PARENT, WRAP_CONTENT)
addView(
TextView(context).apply {
text = str(R.string.fa_star_half_o)
typeface = getFontAwesome()
textSize = sp(40.0f)
gravity = CENTER
setTextColor(sres.getColor(R.attr.mediumContrastTextColor))
},
MATCH_PARENT,
WRAP_CONTENT
)
addView(TextView(context).apply {
text = str(R.string.no_habits_found)
gravity = CENTER
setPadding(0, dp(20.0f).toInt(), 0, 0)
setTextColor(sres.getColor(R.attr.mediumContrastTextColor))
}, MATCH_PARENT, WRAP_CONTENT)
addView(
TextView(context).apply {
text = str(R.string.no_habits_found)
gravity = CENTER
setPadding(0, dp(20.0f).toInt(), 0, 0)
setTextColor(sres.getColor(R.attr.mediumContrastTextColor))
},
MATCH_PARENT,
WRAP_CONTENT
)
}
}
}

@ -19,12 +19,13 @@
package org.isoron.uhabits.activities.habits.list.views
import dagger.*
import org.isoron.uhabits.activities.habits.list.*
import org.isoron.uhabits.core.models.*
import org.isoron.uhabits.core.ui.screens.habits.list.*
import org.isoron.uhabits.inject.*
import javax.inject.*
import dagger.Lazy
import org.isoron.uhabits.activities.habits.list.ListHabitsSelectionMenu
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.ModelObservable
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior
import org.isoron.uhabits.inject.ActivityScope
import javax.inject.Inject
/**
* Controller responsible for receiving and processing the events generated by a
@ -33,9 +34,9 @@ import javax.inject.*
*/
@ActivityScope
class HabitCardListController @Inject constructor(
private val adapter: HabitCardListAdapter,
private val behavior: ListHabitsBehavior,
private val selectionMenu: Lazy<ListHabitsSelectionMenu>
private val adapter: HabitCardListAdapter,
private val behavior: ListHabitsBehavior,
private val selectionMenu: Lazy<ListHabitsSelectionMenu>
) : HabitCardListView.Controller, ModelObservable.Listener {
private val NORMAL_MODE = NormalMode()
@ -53,7 +54,7 @@ class HabitCardListController @Inject constructor(
val habitFrom = adapter.getItem(from)
val habitTo = adapter.getItem(to)
if(habitFrom == null || habitTo == null) return
if (habitFrom == null || habitTo == null) return
adapter.performReorder(from, to)
behavior.onReorderHabit(habitFrom, habitTo)

@ -19,24 +19,33 @@
package org.isoron.uhabits.activities.habits.list.views
import android.content.*
import android.os.*
import androidx.recyclerview.widget.*
import androidx.recyclerview.widget.ItemTouchHelper.*
import android.view.*
import com.google.auto.factory.*
import dagger.*
import android.content.Context
import android.os.Bundle
import android.os.Parcelable
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.View
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.ItemTouchHelper.DOWN
import androidx.recyclerview.widget.ItemTouchHelper.END
import androidx.recyclerview.widget.ItemTouchHelper.START
import androidx.recyclerview.widget.ItemTouchHelper.UP
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.auto.factory.AutoFactory
import com.google.auto.factory.Provided
import dagger.Lazy
import org.isoron.uhabits.R
import org.isoron.uhabits.activities.common.views.*
import org.isoron.uhabits.core.models.*
import org.isoron.uhabits.inject.*
import org.isoron.uhabits.activities.common.views.BundleSavedState
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.inject.ActivityContext
@AutoFactory
class HabitCardListView(
@Provided @ActivityContext context: Context,
@Provided private val adapter: HabitCardListAdapter,
@Provided private val cardViewFactory: HabitCardViewFactory,
@Provided private val controller: Lazy<HabitCardListController>
@Provided @ActivityContext context: Context,
@Provided private val adapter: HabitCardListAdapter,
@Provided private val cardViewFactory: HabitCardViewFactory,
@Provided private val controller: Lazy<HabitCardListController>
) : RecyclerView(context, null, R.attr.scrollableRecyclerViewStyle) {
var checkmarkCount: Int = 0
@ -45,8 +54,8 @@ class HabitCardListView(
set(value) {
field = value
attachedHolders
.map { it.itemView as HabitCardView }
.forEach { it.dataOffset = value }
.map { it.itemView as HabitCardView }
.forEach { it.dataOffset = value }
}
private val attachedHolders = mutableListOf<HabitCardViewHolder>()
@ -65,11 +74,13 @@ class HabitCardListView(
return cardViewFactory.create()
}
fun bindCardView(holder: HabitCardViewHolder,
habit: Habit,
score: Double,
checkmarks: IntArray,
selected: Boolean): View {
fun bindCardView(
holder: HabitCardViewHolder,
habit: Habit,
score: Double,
checkmarks: IntArray,
selected: Boolean
): View {
val cardView = holder.itemView as HabitCardView
cardView.habit = habit
cardView.isSelected = selected
@ -132,7 +143,7 @@ class HabitCardListView(
}
private inner class CardViewGestureDetector(
private val holder: HabitCardViewHolder
private val holder: HabitCardViewHolder
) : GestureDetector.SimpleOnGestureListener() {
override fun onLongPress(e: MotionEvent) {
@ -149,20 +160,26 @@ class HabitCardListView(
}
inner class TouchHelperCallback : ItemTouchHelper.Callback() {
override fun getMovementFlags(recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder): Int {
override fun getMovementFlags(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
): Int {
return makeMovementFlags(UP or DOWN, START or END)
}
override fun onMove(recyclerView: RecyclerView,
from: RecyclerView.ViewHolder,
to: RecyclerView.ViewHolder): Boolean {
override fun onMove(
recyclerView: RecyclerView,
from: RecyclerView.ViewHolder,
to: RecyclerView.ViewHolder
): Boolean {
controller.get().drop(from.adapterPosition, to.adapterPosition)
return true
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder,
direction: Int) {
override fun onSwiped(
viewHolder: RecyclerView.ViewHolder,
direction: Int
) {
}
override fun isItemViewSwipeEnabled() = false

@ -19,29 +19,41 @@
package org.isoron.uhabits.activities.habits.list.views
import android.content.*
import android.os.*
import android.os.Build.VERSION.*
import android.os.Build.VERSION_CODES.*
import android.text.*
import android.view.*
import android.view.ViewGroup.LayoutParams.*
import android.widget.*
import com.google.auto.factory.*
import android.content.Context
import android.os.Build.VERSION.SDK_INT
import android.os.Build.VERSION_CODES.LOLLIPOP
import android.os.Build.VERSION_CODES.M
import android.os.Handler
import android.os.Looper
import android.text.Layout
import android.text.TextUtils
import android.view.Gravity
import android.view.View
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.widget.FrameLayout
import android.widget.LinearLayout
import android.widget.TextView
import com.google.auto.factory.AutoFactory
import com.google.auto.factory.Provided
import org.isoron.uhabits.R
import org.isoron.uhabits.activities.common.views.*
import org.isoron.uhabits.core.models.*
import org.isoron.uhabits.core.ui.screens.habits.list.*
import org.isoron.uhabits.core.utils.*
import org.isoron.uhabits.inject.*
import org.isoron.uhabits.utils.*
import org.isoron.uhabits.activities.common.views.RingView
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.ModelObservable
import org.isoron.uhabits.core.models.Timestamp
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior
import org.isoron.uhabits.core.utils.DateUtils
import org.isoron.uhabits.inject.ActivityContext
import org.isoron.uhabits.utils.dp
import org.isoron.uhabits.utils.sres
import org.isoron.uhabits.utils.toThemedAndroidColor
@AutoFactory
class HabitCardView(
@Provided @ActivityContext context: Context,
@Provided private val checkmarkPanelFactory: CheckmarkPanelViewFactory,
@Provided private val numberPanelFactory: NumberPanelViewFactory,
@Provided private val behavior: ListHabitsBehavior
@Provided @ActivityContext context: Context,
@Provided private val checkmarkPanelFactory: CheckmarkPanelViewFactory,
@Provided private val numberPanelFactory: NumberPanelViewFactory,
@Provided private val behavior: ListHabitsBehavior
) : FrameLayout(context),
ModelObservable.Listener {
@ -228,8 +240,10 @@ class HabitCardView(
private fun triggerRipple(x: Float, y: Float) {
val background = innerFrame.background
if (SDK_INT >= LOLLIPOP) background.setHotspot(x, y)
background.state = intArrayOf(android.R.attr.state_pressed,
android.R.attr.state_enabled)
background.state = intArrayOf(
android.R.attr.state_pressed,
android.R.attr.state_enabled
)
Handler().postDelayed({ background.state = intArrayOf() }, 25)
}

@ -19,8 +19,7 @@
package org.isoron.uhabits.activities.habits.list.views
import androidx.appcompat.widget.*
import android.view.*
import android.view.View
import androidx.recyclerview.widget.RecyclerView
class HabitCardViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)

@ -19,24 +19,33 @@
package org.isoron.uhabits.activities.habits.list.views
import android.content.*
import android.graphics.*
import android.os.Build.VERSION.*
import android.os.Build.VERSION_CODES.*
import android.text.*
import android.view.View.MeasureSpec.*
import org.isoron.uhabits.*
import org.isoron.uhabits.activities.common.views.*
import org.isoron.uhabits.core.preferences.*
import org.isoron.uhabits.core.utils.*
import org.isoron.uhabits.core.utils.DateUtils.*
import org.isoron.uhabits.utils.*
import java.util.*
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.RectF
import android.graphics.Typeface
import android.os.Build.VERSION.SDK_INT
import android.os.Build.VERSION_CODES.LOLLIPOP
import android.text.TextPaint
import android.view.View.MeasureSpec.EXACTLY
import org.isoron.uhabits.R
import org.isoron.uhabits.activities.common.views.ScrollableChart
import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.core.utils.DateUtils.formatHeaderDate
import org.isoron.uhabits.core.utils.DateUtils.getStartOfTodayCalendarWithOffset
import org.isoron.uhabits.core.utils.MidnightTimer
import org.isoron.uhabits.utils.dim
import org.isoron.uhabits.utils.dp
import org.isoron.uhabits.utils.isRTL
import org.isoron.uhabits.utils.sres
import org.isoron.uhabits.utils.toMeasureSpec
import java.util.GregorianCalendar
class HeaderView(
context: Context,
val prefs: Preferences,
val midnightTimer: MidnightTimer
context: Context,
val prefs: Preferences,
val midnightTimer: MidnightTimer
) : ScrollableChart(context),
Preferences.Listener,
MidnightTimer.MidnightListener {
@ -121,8 +130,12 @@ class HeaderView(
if (isReversed) rect.offset(-(index + 1) * width, 0f)
else rect.offset((index - buttonCount) * width, 0f)
if (isRTL()) rect.set(canvas.width - rect.right, rect.top,
canvas.width - rect.left, rect.bottom)
if (isRTL()) rect.set(
canvas.width - rect.right,
rect.top,
canvas.width - rect.left,
rect.bottom
)
val y1 = rect.centerY() - 0.25 * em
val y2 = rect.centerY() + 1.25 * em

@ -19,20 +19,21 @@
package org.isoron.uhabits.activities.habits.list.views
import android.animation.*
import android.content.*
import android.graphics.*
import android.graphics.Color.*
import android.view.*
import android.view.ViewGroup.LayoutParams.*
import android.widget.*
import org.isoron.uhabits.*
import org.isoron.uhabits.core.ui.screens.habits.list.*
import org.isoron.uhabits.utils.*
import android.animation.AnimatorListenerAdapter
import android.content.Context
import android.graphics.Color.WHITE
import android.graphics.Typeface
import android.view.View
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.widget.LinearLayout
import android.widget.TextView
import org.isoron.uhabits.R
import org.isoron.uhabits.core.ui.screens.habits.list.HintList
import org.isoron.uhabits.utils.dp
class HintView(
context: Context,
private val hintList: HintList
context: Context,
private val hintList: HintList
) : LinearLayout(context) {
val hintContent: TextView

@ -19,18 +19,25 @@
package org.isoron.uhabits.activities.habits.list.views
import android.content.*
import android.graphics.*
import android.text.*
import android.view.*
import android.view.View.*
import com.google.auto.factory.*
import org.isoron.uhabits.*
import org.isoron.uhabits.core.preferences.*
import org.isoron.uhabits.inject.*
import org.isoron.uhabits.utils.*
import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.RectF
import android.graphics.Typeface
import android.text.TextPaint
import android.view.View
import android.view.View.OnClickListener
import android.view.View.OnLongClickListener
import com.google.auto.factory.AutoFactory
import com.google.auto.factory.Provided
import org.isoron.uhabits.R
import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.inject.ActivityContext
import org.isoron.uhabits.utils.InterfaceUtils.getDimension
import java.text.*
import org.isoron.uhabits.utils.StyledResources
import org.isoron.uhabits.utils.getFontAwesome
import org.isoron.uhabits.utils.showMessage
import java.text.DecimalFormat
private val BOLD_TYPEFACE = Typeface.create("sans-serif-condensed", Typeface.BOLD)
private val NORMAL_TYPEFACE = Typeface.create("sans-serif-condensed", Typeface.NORMAL)
@ -50,8 +57,8 @@ fun Double.toShortString(): String = when {
@AutoFactory
class NumberButtonView(
@Provided @ActivityContext context: Context,
@Provided val preferences: Preferences
@Provided @ActivityContext context: Context,
@Provided val preferences: Preferences
) : View(context),
OnClickListener,
OnLongClickListener {
@ -148,10 +155,10 @@ class NumberButtonView(
val label: String
val typeface: Typeface
if(value >= 0) {
if (value >= 0) {
label = value.toShortString()
typeface = BOLD_TYPEFACE
} else if(preferences.areQuestionMarksEnabled()) {
} else if (preferences.areQuestionMarksEnabled()) {
label = resources.getString(R.string.fa_question)
typeface = getFontAwesome()
} else {

@ -19,18 +19,19 @@
package org.isoron.uhabits.activities.habits.list.views
import android.content.*
import com.google.auto.factory.*
import org.isoron.uhabits.core.models.*
import org.isoron.uhabits.core.preferences.*
import org.isoron.uhabits.core.utils.*
import org.isoron.uhabits.inject.*
import android.content.Context
import com.google.auto.factory.AutoFactory
import com.google.auto.factory.Provided
import org.isoron.uhabits.core.models.Timestamp
import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.core.utils.DateUtils
import org.isoron.uhabits.inject.ActivityContext
@AutoFactory
class NumberPanelView(
@Provided @ActivityContext context: Context,
@Provided preferences: Preferences,
@Provided private val buttonFactory: NumberButtonViewFactory
@Provided @ActivityContext context: Context,
@Provided preferences: Preferences,
@Provided private val buttonFactory: NumberButtonViewFactory
) : ButtonPanelView<NumberButtonView>(context, preferences) {
var values = DoubleArray(0)

@ -19,11 +19,12 @@
package org.isoron.uhabits.activities.habits.list.views
import android.content.*
import android.view.*
import android.view.View.MeasureSpec.*
import org.isoron.uhabits.*
import org.isoron.uhabits.utils.*
import android.content.Context
import android.view.View
import android.view.View.MeasureSpec.EXACTLY
import org.isoron.uhabits.R
import org.isoron.uhabits.utils.dp
import org.isoron.uhabits.utils.toMeasureSpec
@Suppress("DEPRECATION")
class ShadowView(context: Context) : View(context) {
@ -33,7 +34,9 @@ class ShadowView(context: Context) : View(context) {
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec,
dp(2.0f).toInt().toMeasureSpec(EXACTLY))
super.onMeasure(
widthMeasureSpec,
dp(2.0f).toInt().toMeasureSpec(EXACTLY)
)
}
}
}

@ -18,17 +18,25 @@
*/
package org.isoron.uhabits.activities.habits.show
import android.content.*
import android.os.*
import android.view.*
import androidx.appcompat.app.*
import kotlinx.coroutines.*
import org.isoron.uhabits.*
import org.isoron.uhabits.activities.*
import org.isoron.uhabits.activities.common.dialogs.*
import org.isoron.uhabits.core.commands.*
import org.isoron.uhabits.core.ui.screens.habits.show.*
import org.isoron.uhabits.intents.*
import android.content.ContentUris
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.isoron.uhabits.AndroidDirFinder
import org.isoron.uhabits.HabitsApplication
import org.isoron.uhabits.activities.AndroidThemeSwitcher
import org.isoron.uhabits.activities.HabitsDirFinder
import org.isoron.uhabits.activities.common.dialogs.ConfirmDeleteDialogFactory
import org.isoron.uhabits.activities.common.dialogs.NumberPickerFactory
import org.isoron.uhabits.core.commands.Command
import org.isoron.uhabits.core.commands.CommandRunner
import org.isoron.uhabits.core.ui.screens.habits.show.ShowHabitBehavior
import org.isoron.uhabits.core.ui.screens.habits.show.ShowHabitMenuBehavior
import org.isoron.uhabits.intents.IntentFactory
class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
@ -51,41 +59,41 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
view = ShowHabitView(this)
presenter = ShowHabitPresenter(
context = this,
habit = habit,
preferences = appComponent.preferences,
context = this,
habit = habit,
preferences = appComponent.preferences,
)
val screen = ShowHabitScreen(
activity = this,
confirmDeleteDialogFactory = ConfirmDeleteDialogFactory { this },
habit = habit,
intentFactory = IntentFactory(),
numberPickerFactory = NumberPickerFactory(this),
widgetUpdater = appComponent.widgetUpdater,
activity = this,
confirmDeleteDialogFactory = ConfirmDeleteDialogFactory { this },
habit = habit,
intentFactory = IntentFactory(),
numberPickerFactory = NumberPickerFactory(this),
widgetUpdater = appComponent.widgetUpdater,
)
val behavior = ShowHabitBehavior(
commandRunner = commandRunner,
habit = habit,
habitList = habitList,
preferences = preferences,
screen = screen,
commandRunner = commandRunner,
habit = habit,
habitList = habitList,
preferences = preferences,
screen = screen,
)
val menuBehavior = ShowHabitMenuBehavior(
commandRunner = commandRunner,
habit = habit,
habitList = habitList,
screen = screen,
system = HabitsDirFinder(AndroidDirFinder(this)),
taskRunner = appComponent.taskRunner,
commandRunner = commandRunner,
habit = habit,
habitList = habitList,
screen = screen,
system = HabitsDirFinder(AndroidDirFinder(this)),
taskRunner = appComponent.taskRunner,
)
menu = ShowHabitMenu(
activity = this,
behavior = menuBehavior,
preferences = preferences,
activity = this,
behavior = menuBehavior,
preferences = preferences,
)
view.onScoreCardSpinnerPosition = behavior::onScoreCardSpinnerPosition
@ -125,4 +133,3 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
}
}
}

@ -19,15 +19,16 @@
package org.isoron.uhabits.activities.habits.show
import android.view.*
import org.isoron.uhabits.*
import org.isoron.uhabits.core.preferences.*
import org.isoron.uhabits.core.ui.screens.habits.show.*
import android.view.Menu
import android.view.MenuItem
import org.isoron.uhabits.R
import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.core.ui.screens.habits.show.ShowHabitMenuBehavior
class ShowHabitMenu(
val activity: ShowHabitActivity,
val behavior: ShowHabitMenuBehavior,
val preferences: Preferences,
val activity: ShowHabitActivity,
val behavior: ShowHabitMenuBehavior,
val preferences: Preferences,
) {
fun onCreateOptionsMenu(menu: Menu): Boolean {
activity.menuInflater.inflate(R.menu.show_habit, menu)
@ -38,7 +39,7 @@ class ShowHabitMenu(
}
fun onOptionsItemSelected(item: MenuItem): Boolean {
when(item.itemId) {
when (item.itemId) {
R.id.action_edit_habit -> {
behavior.onEditHabit()
return true
@ -58,4 +59,4 @@ class ShowHabitMenu(
}
return false
}
}
}

@ -19,28 +19,32 @@
package org.isoron.uhabits.activities.habits.show
import org.isoron.uhabits.*
import org.isoron.uhabits.activities.common.dialogs.*
import org.isoron.uhabits.core.models.*
import org.isoron.uhabits.core.ui.callbacks.*
import org.isoron.uhabits.core.ui.screens.habits.list.*
import org.isoron.uhabits.core.ui.screens.habits.show.*
import org.isoron.uhabits.intents.*
import org.isoron.uhabits.utils.*
import org.isoron.uhabits.widgets.*
import org.isoron.uhabits.R
import org.isoron.uhabits.activities.common.dialogs.ConfirmDeleteDialogFactory
import org.isoron.uhabits.activities.common.dialogs.HistoryEditorDialog
import org.isoron.uhabits.activities.common.dialogs.NumberPickerFactory
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.ui.callbacks.OnConfirmedCallback
import org.isoron.uhabits.core.ui.callbacks.OnToggleCheckmarkListener
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior
import org.isoron.uhabits.core.ui.screens.habits.show.ShowHabitBehavior
import org.isoron.uhabits.core.ui.screens.habits.show.ShowHabitMenuBehavior
import org.isoron.uhabits.intents.IntentFactory
import org.isoron.uhabits.utils.showMessage
import org.isoron.uhabits.utils.showSendFileScreen
import org.isoron.uhabits.widgets.WidgetUpdater
class ShowHabitScreen(
val activity: ShowHabitActivity,
val confirmDeleteDialogFactory: ConfirmDeleteDialogFactory,
val habit: Habit,
val intentFactory: IntentFactory,
val numberPickerFactory: NumberPickerFactory,
val widgetUpdater: WidgetUpdater,
val activity: ShowHabitActivity,
val confirmDeleteDialogFactory: ConfirmDeleteDialogFactory,
val habit: Habit,
val intentFactory: IntentFactory,
val numberPickerFactory: NumberPickerFactory,
val widgetUpdater: WidgetUpdater,
) : ShowHabitBehavior.Screen, ShowHabitMenuBehavior.Screen {
override fun showNumberPicker(value: Double, unit: String, callback: ListHabitsBehavior.NumberPickerCallback) {
numberPickerFactory.create(value, unit, callback).show();
numberPickerFactory.create(value, unit, callback).show()
}
override fun updateWidgets() {
@ -81,4 +85,4 @@ class ShowHabitScreen(
override fun close() {
activity.finish()
}
}
}

@ -19,28 +19,46 @@
package org.isoron.uhabits.activities.habits.show
import android.content.*
import android.view.*
import android.widget.*
import org.isoron.uhabits.activities.habits.show.views.*
import org.isoron.uhabits.core.models.*
import org.isoron.uhabits.core.preferences.*
import org.isoron.uhabits.databinding.*
import org.isoron.uhabits.utils.*
import android.content.Context
import android.view.LayoutInflater
import android.widget.FrameLayout
import org.isoron.uhabits.activities.habits.show.views.BarCardPresenter
import org.isoron.uhabits.activities.habits.show.views.BarCardViewModel
import org.isoron.uhabits.activities.habits.show.views.FrequencyCardPresenter
import org.isoron.uhabits.activities.habits.show.views.FrequencyCardViewModel
import org.isoron.uhabits.activities.habits.show.views.HistoryCardPresenter
import org.isoron.uhabits.activities.habits.show.views.HistoryCardViewModel
import org.isoron.uhabits.activities.habits.show.views.NotesCardPresenter
import org.isoron.uhabits.activities.habits.show.views.NotesCardViewModel
import org.isoron.uhabits.activities.habits.show.views.OverviewCardPresenter
import org.isoron.uhabits.activities.habits.show.views.OverviewCardViewModel
import org.isoron.uhabits.activities.habits.show.views.ScoreCardPresenter
import org.isoron.uhabits.activities.habits.show.views.ScoreCardViewModel
import org.isoron.uhabits.activities.habits.show.views.StreakCardViewModel
import org.isoron.uhabits.activities.habits.show.views.StreakCartPresenter
import org.isoron.uhabits.activities.habits.show.views.SubtitleCardPresenter
import org.isoron.uhabits.activities.habits.show.views.SubtitleCardViewModel
import org.isoron.uhabits.activities.habits.show.views.TargetCardPresenter
import org.isoron.uhabits.activities.habits.show.views.TargetCardViewModel
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.databinding.ShowHabitBinding
import org.isoron.uhabits.utils.setupToolbar
data class ShowHabitViewModel(
val title: String = "",
val isNumerical: Boolean = false,
val color: PaletteColor = PaletteColor(1),
val subtitle: SubtitleCardViewModel,
val overview: OverviewCardViewModel,
val notes: NotesCardViewModel,
val target: TargetCardViewModel,
val streaks: StreakCardViewModel,
val scores: ScoreCardViewModel,
val frequency: FrequencyCardViewModel,
val history: HistoryCardViewModel,
val bar: BarCardViewModel,
val title: String = "",
val isNumerical: Boolean = false,
val color: PaletteColor = PaletteColor(1),
val subtitle: SubtitleCardViewModel,
val overview: OverviewCardViewModel,
val notes: NotesCardViewModel,
val target: TargetCardViewModel,
val streaks: StreakCardViewModel,
val scores: ScoreCardViewModel,
val frequency: FrequencyCardViewModel,
val history: HistoryCardViewModel,
val bar: BarCardViewModel,
)
class ShowHabitView(context: Context) : FrameLayout(context) {
@ -80,56 +98,56 @@ class ShowHabitView(context: Context) : FrameLayout(context) {
}
class ShowHabitPresenter(
val habit: Habit,
val context: Context,
val preferences: Preferences,
val habit: Habit,
val context: Context,
val preferences: Preferences,
) {
private val subtitleCardPresenter = SubtitleCardPresenter(habit, context)
private val overviewCardPresenter = OverviewCardPresenter(habit)
private val notesCardPresenter = NotesCardPresenter(habit)
private val targetCardPresenter = TargetCardPresenter(
habit = habit,
firstWeekday = preferences.firstWeekday,
resources = context.resources,
habit = habit,
firstWeekday = preferences.firstWeekday,
resources = context.resources,
)
private val streakCartPresenter = StreakCartPresenter(habit)
private val scoreCardPresenter = ScoreCardPresenter(
habit = habit,
firstWeekday = preferences.firstWeekday,
habit = habit,
firstWeekday = preferences.firstWeekday,
)
private val frequencyCardPresenter = FrequencyCardPresenter(
habit = habit,
firstWeekday = preferences.firstWeekday,
habit = habit,
firstWeekday = preferences.firstWeekday,
)
private val historyCardViewModel = HistoryCardPresenter(
habit = habit,
firstWeekday = preferences.firstWeekday,
isSkipEnabled = preferences.isSkipEnabled,
habit = habit,
firstWeekday = preferences.firstWeekday,
isSkipEnabled = preferences.isSkipEnabled,
)
private val barCardPresenter = BarCardPresenter(
habit = habit,
firstWeekday = preferences.firstWeekday,
habit = habit,
firstWeekday = preferences.firstWeekday,
)
suspend fun present(): ShowHabitViewModel {
return ShowHabitViewModel(
title = habit.name,
color = habit.color,
isNumerical = habit.isNumerical,
subtitle = subtitleCardPresenter.present(),
overview = overviewCardPresenter.present(),
notes = notesCardPresenter.present(),
target = targetCardPresenter.present(),
streaks = streakCartPresenter.present(),
scores = scoreCardPresenter.present(
spinnerPosition = preferences.scoreCardSpinnerPosition
),
frequency = frequencyCardPresenter.present(),
history = historyCardViewModel.present(),
bar = barCardPresenter.present(
boolSpinnerPosition = preferences.barCardBoolSpinnerPosition,
numericalSpinnerPosition = preferences.barCardNumericalSpinnerPosition,
),
title = habit.name,
color = habit.color,
isNumerical = habit.isNumerical,
subtitle = subtitleCardPresenter.present(),
overview = overviewCardPresenter.present(),
notes = notesCardPresenter.present(),
target = targetCardPresenter.present(),
streaks = streakCartPresenter.present(),
scores = scoreCardPresenter.present(
spinnerPosition = preferences.scoreCardSpinnerPosition
),
frequency = frequencyCardPresenter.present(),
history = historyCardViewModel.present(),
bar = barCardPresenter.present(
boolSpinnerPosition = preferences.barCardBoolSpinnerPosition,
numericalSpinnerPosition = preferences.barCardNumericalSpinnerPosition,
),
)
}
}
}

@ -18,23 +18,27 @@
*/
package org.isoron.uhabits.activities.habits.show.views
import android.content.*
import android.util.*
import android.view.*
import android.widget.*
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.widget.AdapterView
import android.widget.LinearLayout
import org.isoron.uhabits.activities.habits.show.views.ScoreCardPresenter.Companion.getTruncateField
import org.isoron.uhabits.core.models.*
import org.isoron.uhabits.core.utils.*
import org.isoron.uhabits.databinding.*
import org.isoron.uhabits.utils.*
import org.isoron.uhabits.core.models.Entry
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.utils.DateUtils
import org.isoron.uhabits.databinding.ShowHabitBarBinding
import org.isoron.uhabits.utils.toThemedAndroidColor
data class BarCardViewModel(
val entries: List<Entry>,
val bucketSize: Int,
val color: PaletteColor,
val isNumerical: Boolean,
val numericalSpinnerPosition: Int,
val boolSpinnerPosition: Int,
val entries: List<Entry>,
val bucketSize: Int,
val color: PaletteColor,
val isNumerical: Boolean,
val numericalSpinnerPosition: Int,
val boolSpinnerPosition: Int,
)
class BarCard(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) {
@ -78,17 +82,17 @@ class BarCard(context: Context, attrs: AttributeSet) : LinearLayout(context, att
}
class BarCardPresenter(
val habit: Habit,
val firstWeekday: Int,
val habit: Habit,
val firstWeekday: Int,
) {
val numericalBucketSizes = intArrayOf(1, 7, 31, 92, 365)
val boolBucketSizes = intArrayOf(7, 31, 92, 365)
fun present(
numericalSpinnerPosition: Int,
boolSpinnerPosition: Int,
numericalSpinnerPosition: Int,
boolSpinnerPosition: Int,
): BarCardViewModel {
val bucketSize = if(habit.isNumerical) {
val bucketSize = if (habit.isNumerical) {
numericalBucketSizes[numericalSpinnerPosition]
} else {
boolBucketSizes[boolSpinnerPosition]
@ -101,19 +105,19 @@ class BarCardPresenter(
}
} else {
habit.computedEntries.groupBy(
original = habit.computedEntries.getByInterval(oldest, today),
field = getTruncateField(bucketSize),
firstWeekday = firstWeekday,
isNumerical = habit.isNumerical,
original = habit.computedEntries.getByInterval(oldest, today),
field = getTruncateField(bucketSize),
firstWeekday = firstWeekday,
isNumerical = habit.isNumerical,
)
}
return BarCardViewModel(
entries = entries,
bucketSize = bucketSize,
color = habit.color,
isNumerical = habit.isNumerical,
numericalSpinnerPosition = numericalSpinnerPosition,
boolSpinnerPosition = boolSpinnerPosition,
entries = entries,
bucketSize = bucketSize,
color = habit.color,
isNumerical = habit.isNumerical,
numericalSpinnerPosition = numericalSpinnerPosition,
boolSpinnerPosition = boolSpinnerPosition,
)
}
}
}

@ -18,19 +18,21 @@
*/
package org.isoron.uhabits.activities.habits.show.views
import android.content.*
import android.util.*
import android.view.*
import android.widget.*
import org.isoron.uhabits.core.models.*
import org.isoron.uhabits.databinding.*
import org.isoron.uhabits.utils.*
import java.util.*
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.LinearLayout
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.models.Timestamp
import org.isoron.uhabits.databinding.ShowHabitFrequencyBinding
import org.isoron.uhabits.utils.toThemedAndroidColor
import java.util.HashMap
data class FrequencyCardViewModel(
val frequency: HashMap<Timestamp, Array<Int>>,
val firstWeekday: Int,
val color: PaletteColor,
val frequency: HashMap<Timestamp, Array<Int>>,
val firstWeekday: Int,
val color: PaletteColor,
)
class FrequencyCard(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) {
@ -47,14 +49,14 @@ class FrequencyCard(context: Context, attrs: AttributeSet) : LinearLayout(contex
}
class FrequencyCardPresenter(
val habit: Habit,
val firstWeekday: Int,
val habit: Habit,
val firstWeekday: Int,
) {
fun present() = FrequencyCardViewModel(
color = habit.color,
frequency = habit.originalEntries.computeWeekdayFrequency(
isNumerical = habit.isNumerical
),
firstWeekday = firstWeekday,
color = habit.color,
frequency = habit.originalEntries.computeWeekdayFrequency(
isNumerical = habit.isNumerical
),
firstWeekday = firstWeekday,
)
}
}

@ -18,20 +18,21 @@
*/
package org.isoron.uhabits.activities.habits.show.views
import android.content.*
import android.util.*
import android.view.*
import android.widget.*
import org.isoron.uhabits.core.models.*
import org.isoron.uhabits.databinding.*
import org.isoron.uhabits.utils.*
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.LinearLayout
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.databinding.ShowHabitHistoryBinding
import org.isoron.uhabits.utils.toThemedAndroidColor
data class HistoryCardViewModel(
val entries: IntArray,
val color: PaletteColor,
val firstWeekday: Int,
val isNumerical: Boolean,
val isSkipEnabled: Boolean,
val entries: IntArray,
val color: PaletteColor,
val firstWeekday: Int,
val isNumerical: Boolean,
val isSkipEnabled: Boolean,
)
class HistoryCard(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) {
@ -54,20 +55,19 @@ class HistoryCard(context: Context, attrs: AttributeSet) : LinearLayout(context,
if (data.isNumerical) {
binding.historyChart.setNumerical(true)
}
}
}
class HistoryCardPresenter(
val habit: Habit,
val firstWeekday: Int,
val isSkipEnabled: Boolean,
val habit: Habit,
val firstWeekday: Int,
val isSkipEnabled: Boolean,
) {
fun present() = HistoryCardViewModel(
entries = habit.computedEntries.getAllValues(),
color = habit.color,
firstWeekday = firstWeekday,
isNumerical = habit.isNumerical,
isSkipEnabled = isSkipEnabled,
entries = habit.computedEntries.getAllValues(),
color = habit.color,
firstWeekday = firstWeekday,
isNumerical = habit.isNumerical,
isSkipEnabled = isSkipEnabled,
)
}
}

@ -19,12 +19,12 @@
package org.isoron.uhabits.activities.habits.show.views
import android.content.*
import android.util.*
import android.view.*
import android.widget.*
import org.isoron.uhabits.core.models.*
import org.isoron.uhabits.databinding.*
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.LinearLayout
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.databinding.ShowHabitNotesBinding
data class NotesCardViewModel(val description: String)
@ -43,6 +43,6 @@ class NotesCardView(context: Context, attrs: AttributeSet) : LinearLayout(contex
class NotesCardPresenter(val habit: Habit) {
fun present() = NotesCardViewModel(
description = habit.description,
description = habit.description,
)
}
}

@ -18,24 +18,27 @@
*/
package org.isoron.uhabits.activities.habits.show.views
import android.content.*
import android.util.*
import android.view.*
import android.widget.*
import kotlinx.coroutines.*
import org.isoron.uhabits.*
import org.isoron.uhabits.core.models.*
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.LinearLayout
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.invoke
import org.isoron.uhabits.R
import org.isoron.uhabits.core.models.Entry.Companion.YES_MANUAL
import org.isoron.uhabits.core.utils.*
import org.isoron.uhabits.databinding.*
import org.isoron.uhabits.utils.*
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.utils.DateUtils
import org.isoron.uhabits.databinding.ShowHabitOverviewBinding
import org.isoron.uhabits.utils.StyledResources
import org.isoron.uhabits.utils.toThemedAndroidColor
data class OverviewCardViewModel(
val color: PaletteColor,
val scoreMonthDiff: Float,
val scoreYearDiff: Float,
val scoreToday: Float,
val totalCount: Long,
val color: PaletteColor,
val scoreMonthDiff: Float,
val scoreYearDiff: Float,
val scoreToday: Float,
val totalCount: Long,
)
class OverviewCardView(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) {
@ -43,8 +46,11 @@ class OverviewCardView(context: Context, attrs: AttributeSet) : LinearLayout(con
private val binding = ShowHabitOverviewBinding.inflate(LayoutInflater.from(context), this)
private fun formatPercentageDiff(percentageDiff: Float): String {
return String.format("%s%.0f%%", if (percentageDiff >= 0) "+" else "\u2212",
Math.abs(percentageDiff) * 100)
return String.format(
"%s%.0f%%",
if (percentageDiff >= 0) "+" else "\u2212",
Math.abs(percentageDiff) * 100
)
}
fun update(data: OverviewCardViewModel) {
@ -76,15 +82,15 @@ class OverviewCardPresenter(val habit: Habit) {
val scoreLastMonth = scores.get(lastMonth).value.toFloat()
val scoreLastYear = scores.get(lastYear).value.toFloat()
val totalCount = habit.originalEntries.getKnown()
.filter { it.value == YES_MANUAL }
.count()
.toLong()
.filter { it.value == YES_MANUAL }
.count()
.toLong()
return@IO OverviewCardViewModel(
color = habit.color,
scoreToday = scoreToday,
scoreMonthDiff = scoreToday - scoreLastMonth,
scoreYearDiff = scoreToday - scoreLastYear,
totalCount = totalCount,
color = habit.color,
scoreToday = scoreToday,
scoreMonthDiff = scoreToday - scoreLastMonth,
scoreYearDiff = scoreToday - scoreLastYear,
totalCount = totalCount,
)
}
}
}

@ -18,21 +18,29 @@
*/
package org.isoron.uhabits.activities.habits.show.views
import android.content.*
import android.util.*
import android.view.*
import android.widget.*
import org.isoron.uhabits.core.models.*
import org.isoron.uhabits.core.utils.*
import org.isoron.uhabits.core.utils.DateUtils.TruncateField.*
import org.isoron.uhabits.databinding.*
import org.isoron.uhabits.utils.*
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.widget.AdapterView
import android.widget.LinearLayout
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.models.Score
import org.isoron.uhabits.core.utils.DateUtils
import org.isoron.uhabits.core.utils.DateUtils.TruncateField.DAY
import org.isoron.uhabits.core.utils.DateUtils.TruncateField.MONTH
import org.isoron.uhabits.core.utils.DateUtils.TruncateField.QUARTER
import org.isoron.uhabits.core.utils.DateUtils.TruncateField.WEEK_NUMBER
import org.isoron.uhabits.core.utils.DateUtils.TruncateField.YEAR
import org.isoron.uhabits.databinding.ShowHabitScoreBinding
import org.isoron.uhabits.utils.toThemedAndroidColor
data class ScoreCardViewModel(
val scores: List<Score>,
val bucketSize: Int,
val spinnerPosition: Int,
val color: PaletteColor,
val scores: List<Score>,
val bucketSize: Int,
val spinnerPosition: Int,
val color: PaletteColor,
)
class ScoreCard(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) {
@ -60,8 +68,8 @@ class ScoreCard(context: Context, attrs: AttributeSet) : LinearLayout(context, a
}
class ScoreCardPresenter(
val habit: Habit,
val firstWeekday: Int,
val habit: Habit,
val firstWeekday: Int,
) {
companion object {
val BUCKET_SIZES = intArrayOf(1, 7, 31, 92, 365)
@ -86,18 +94,21 @@ class ScoreCardPresenter(
val scores = habit.scores.getByInterval(oldest, today).groupBy {
DateUtils.truncate(field, it.timestamp, firstWeekday)
}.map { (timestamp, scores) ->
Score(timestamp, scores.map {
it.value
}.average())
Score(
timestamp,
scores.map {
it.value
}.average()
)
}.sortedBy {
it.timestamp
}.reversed()
return ScoreCardViewModel(
color = habit.color,
scores = scores,
bucketSize = bucketSize,
spinnerPosition = spinnerPosition,
color = habit.color,
scores = scores,
bucketSize = bucketSize,
spinnerPosition = spinnerPosition,
)
}
}
}

@ -18,18 +18,21 @@
*/
package org.isoron.uhabits.activities.habits.show.views
import android.content.*
import android.util.*
import android.view.*
import android.widget.*
import kotlinx.coroutines.*
import org.isoron.uhabits.core.models.*
import org.isoron.uhabits.databinding.*
import org.isoron.uhabits.utils.*
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.LinearLayout
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.invoke
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.models.Streak
import org.isoron.uhabits.databinding.ShowHabitStreakBinding
import org.isoron.uhabits.utils.toThemedAndroidColor
data class StreakCardViewModel(
val color: PaletteColor,
val bestStreaks: List<Streak>
val color: PaletteColor,
val bestStreaks: List<Streak>
)
class StreakCardView(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) {
@ -46,8 +49,8 @@ class StreakCardView(context: Context, attrs: AttributeSet) : LinearLayout(conte
class StreakCartPresenter(val habit: Habit) {
suspend fun present(): StreakCardViewModel = Dispatchers.IO {
return@IO StreakCardViewModel(
color = habit.color,
bestStreaks = habit.streaks.getBest(10),
color = habit.color,
bestStreaks = habit.streaks.getBest(10),
)
}
}
}

@ -18,26 +18,31 @@
*/
package org.isoron.uhabits.activities.habits.show.views
import android.annotation.*
import android.content.*
import android.content.res.*
import android.util.*
import android.view.*
import android.widget.*
import org.isoron.uhabits.*
import org.isoron.uhabits.activities.habits.list.views.*
import org.isoron.uhabits.core.models.*
import org.isoron.uhabits.databinding.*
import org.isoron.uhabits.utils.*
import java.util.*
import android.annotation.SuppressLint
import android.content.Context
import android.content.res.Resources
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.widget.LinearLayout
import org.isoron.uhabits.R
import org.isoron.uhabits.activities.habits.list.views.toShortString
import org.isoron.uhabits.core.models.Frequency
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.databinding.ShowHabitSubtitleBinding
import org.isoron.uhabits.utils.InterfaceUtils
import org.isoron.uhabits.utils.formatTime
import org.isoron.uhabits.utils.toThemedAndroidColor
import java.util.Locale
data class SubtitleCardViewModel(
val color: PaletteColor,
val frequencyText: String,
val isNumerical: Boolean,
val question: String,
val reminderText: String,
val targetText: String,
val color: PaletteColor,
val frequencyText: String,
val isNumerical: Boolean,
val question: String,
val reminderText: String,
val targetText: String,
)
class SubtitleCardView(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) {
@ -75,8 +80,8 @@ class SubtitleCardView(context: Context, attrs: AttributeSet) : LinearLayout(con
}
class SubtitleCardPresenter(
val habit: Habit,
val context: Context,
val habit: Habit,
val context: Context,
) {
val resources: Resources = context.resources
@ -87,12 +92,12 @@ class SubtitleCardPresenter(
resources.getString(R.string.reminder_off)
}
return SubtitleCardViewModel(
color = habit.color,
frequencyText = habit.frequency.format(),
isNumerical = habit.isNumerical,
question = habit.question,
reminderText = reminderText,
targetText = "${habit.targetValue.toShortString()} ${habit.unit}",
color = habit.color,
frequencyText = habit.frequency.format(),
isNumerical = habit.isNumerical,
question = habit.question,
reminderText = reminderText,
targetText = "${habit.targetValue.toShortString()} ${habit.unit}",
)
}
@ -122,12 +127,12 @@ class SubtitleCardPresenter(
return resources.getString(R.string.every_x_days, den)
}
return String.format(
Locale.US,
"%d %s %d %s",
num,
resources.getString(R.string.times_every),
den,
resources.getString(R.string.days),
Locale.US,
"%d %s %d %s",
num,
resources.getString(R.string.times_every),
den,
resources.getString(R.string.days),
)
}
}
}

@ -18,24 +18,27 @@
*/
package org.isoron.uhabits.activities.habits.show.views
import android.content.*
import android.content.res.*
import android.util.*
import android.view.*
import android.widget.*
import kotlinx.coroutines.*
import org.isoron.uhabits.*
import org.isoron.uhabits.core.models.*
import org.isoron.uhabits.core.utils.*
import org.isoron.uhabits.databinding.*
import org.isoron.uhabits.utils.*
import java.util.*
import android.content.Context
import android.content.res.Resources
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.LinearLayout
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.invoke
import org.isoron.uhabits.R
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.utils.DateUtils
import org.isoron.uhabits.databinding.ShowHabitTargetBinding
import org.isoron.uhabits.utils.toThemedAndroidColor
import java.util.ArrayList
import java.util.Calendar
data class TargetCardViewModel(
val color: PaletteColor,
val values: List<Double> = listOf(),
val targets: List<Double> = listOf(),
val labels: List<String> = listOf(),
val color: PaletteColor,
val values: List<Double> = listOf(),
val targets: List<Double> = listOf(),
val labels: List<String> = listOf(),
)
class TargetCardView(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) {
@ -52,9 +55,9 @@ class TargetCardView(context: Context, attrs: AttributeSet) : LinearLayout(conte
}
class TargetCardPresenter(
val habit: Habit,
val firstWeekday: Int,
val resources: Resources,
val habit: Habit,
val firstWeekday: Int,
val resources: Resources,
) {
suspend fun present(): TargetCardViewModel = Dispatchers.IO {
val today = DateUtils.getTodayWithOffset()
@ -98,10 +101,10 @@ class TargetCardPresenter(
labels.add(resources.getString(R.string.year))
return@IO TargetCardViewModel(
color = habit.color,
values = values,
labels = labels,
targets = targets,
color = habit.color,
values = values,
labels = labels,
targets = targets,
)
}
}

@ -19,11 +19,10 @@
package org.isoron.uhabits.activities.intro
import android.graphics.*
import android.os.*
import com.github.paolorotolo.appintro.*
import android.graphics.Color
import android.os.Bundle
import com.github.paolorotolo.appintro.AppIntro2
import com.github.paolorotolo.appintro.AppIntroFragment
import org.isoron.uhabits.R
/**
@ -34,17 +33,32 @@ class IntroActivity : AppIntro2() {
override fun init(savedInstanceState: Bundle?) {
showStatusBar(false)
addSlide(AppIntroFragment.newInstance(getString(R.string.intro_title_1),
getString(R.string.intro_description_1), R.drawable.intro_icon_1,
Color.parseColor("#194673")))
addSlide(
AppIntroFragment.newInstance(
getString(R.string.intro_title_1),
getString(R.string.intro_description_1),
R.drawable.intro_icon_1,
Color.parseColor("#194673")
)
)
addSlide(AppIntroFragment.newInstance(getString(R.string.intro_title_2),
getString(R.string.intro_description_2), R.drawable.intro_icon_2,
Color.parseColor("#ffa726")))
addSlide(
AppIntroFragment.newInstance(
getString(R.string.intro_title_2),
getString(R.string.intro_description_2),
R.drawable.intro_icon_2,
Color.parseColor("#ffa726")
)
)
addSlide(AppIntroFragment.newInstance(getString(R.string.intro_title_4),
getString(R.string.intro_description_4), R.drawable.intro_icon_4,
Color.parseColor("#9575cd")))
addSlide(
AppIntroFragment.newInstance(
getString(R.string.intro_title_4),
getString(R.string.intro_description_4),
R.drawable.intro_icon_4,
Color.parseColor("#9575cd")
)
)
}
override fun onNextPressed() {}

@ -18,14 +18,15 @@
*/
package org.isoron.uhabits.activities.settings
import android.os.*
import android.view.*
import androidx.appcompat.app.*
import org.isoron.uhabits.*
import org.isoron.uhabits.activities.*
import org.isoron.uhabits.core.models.*
import org.isoron.uhabits.databinding.*
import org.isoron.uhabits.utils.*
import android.os.Bundle
import android.view.LayoutInflater
import androidx.appcompat.app.AppCompatActivity
import org.isoron.uhabits.HabitsApplication
import org.isoron.uhabits.R
import org.isoron.uhabits.activities.AndroidThemeSwitcher
import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.databinding.SettingsActivityBinding
import org.isoron.uhabits.utils.setupToolbar
class SettingsActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
@ -35,10 +36,10 @@ class SettingsActivity : AppCompatActivity() {
val binding = SettingsActivityBinding.inflate(LayoutInflater.from(this))
binding.root.setupToolbar(
toolbar = binding.toolbar,
title = resources.getString(R.string.settings),
color = PaletteColor(11),
toolbar = binding.toolbar,
title = resources.getString(R.string.settings),
color = PaletteColor(11),
)
setContentView(binding.root)
}
}
}

@ -19,25 +19,31 @@
package org.isoron.uhabits.activities.sync
import android.content.*
import android.content.ClipData
import android.content.ClipboardManager
import android.graphics.*
import android.os.*
import android.text.*
import android.view.*
import androidx.appcompat.app.*
import com.google.zxing.*
import com.google.zxing.qrcode.*
import kotlinx.coroutines.*
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Color
import android.os.Bundle
import android.text.Html
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import com.google.zxing.BarcodeFormat
import com.google.zxing.qrcode.QRCodeWriter
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.invoke
import kotlinx.coroutines.launch
import org.isoron.uhabits.HabitsApplication
import org.isoron.uhabits.R
import org.isoron.uhabits.activities.AndroidThemeSwitcher
import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.ui.screens.sync.SyncBehavior
import org.isoron.uhabits.databinding.ActivitySyncBinding
import org.isoron.uhabits.sync.RemoteSyncServer
import org.isoron.uhabits.utils.InterfaceUtils.getFontAwesome
import org.isoron.uhabits.*
import org.isoron.uhabits.activities.*
import org.isoron.uhabits.core.models.*
import org.isoron.uhabits.core.ui.screens.sync.*
import org.isoron.uhabits.databinding.*
import org.isoron.uhabits.sync.*
import org.isoron.uhabits.utils.*
import org.isoron.uhabits.utils.setupToolbar
import org.isoron.uhabits.utils.showMessage
class SyncActivity : AppCompatActivity(), SyncBehavior.Screen {
@ -57,9 +63,9 @@ class SyncActivity : AppCompatActivity(), SyncBehavior.Screen {
binding = ActivitySyncBinding.inflate(layoutInflater)
binding.errorIcon.typeface = getFontAwesome(this)
binding.root.setupToolbar(
toolbar = binding.toolbar,
color = PaletteColor(11),
title = resources.getString(R.string.device_sync),
toolbar = binding.toolbar,
color = PaletteColor(11),
title = resources.getString(R.string.device_sync),
)
binding.syncLink.setOnClickListener { copyToClipboard() }
binding.instructions.setText(Html.fromHtml(resources.getString(R.string.sync_instructions)))
@ -100,7 +106,6 @@ class SyncActivity : AppCompatActivity(), SyncBehavior.Screen {
binding.progress.visibility = View.GONE
binding.qrCode.visibility = View.VISIBLE
binding.qrCode.setImageBitmap(generateQR(msg))
}
override suspend fun showLoadingScreen() {
@ -122,4 +127,4 @@ class SyncActivity : AppCompatActivity(), SyncBehavior.Screen {
binding.syncLink.text = link
showQR(link)
}
}
}

@ -19,30 +19,31 @@
package org.isoron.uhabits.automation
import android.os.*
import androidx.appcompat.app.*
import org.isoron.uhabits.*
import org.isoron.uhabits.activities.*
import org.isoron.uhabits.core.models.*
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import org.isoron.uhabits.HabitsApplication
import org.isoron.uhabits.activities.AndroidThemeSwitcher
import org.isoron.uhabits.core.models.HabitMatcherBuilder
class EditSettingActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val app = applicationContext as HabitsApplication
val habits = app.component.habitList.getFiltered(
HabitMatcherBuilder()
.setArchivedAllowed(false)
.setCompletedAllowed(true)
.build())
HabitMatcherBuilder()
.setArchivedAllowed(false)
.setCompletedAllowed(true)
.build()
)
AndroidThemeSwitcher(this, app.component.preferences).apply()
val args = SettingUtils.parseIntent(this.intent, habits)
val controller = EditSettingController(this)
val view = EditSettingRootView(
context = this,
habitList = app.component.habitList,
onSave = controller::onSave,
args = args,
context = this,
habitList = app.component.habitList,
onSave = controller::onSave,
args = args,
)
setContentView(view)
}

@ -19,11 +19,11 @@
package org.isoron.uhabits.automation
import android.app.*
import android.content.*
import android.os.*
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import org.isoron.uhabits.R
import org.isoron.uhabits.core.models.*
import org.isoron.uhabits.core.models.Habit
class EditSettingController(private val activity: Activity) {
@ -36,10 +36,13 @@ class EditSettingController(private val activity: Activity) {
bundle.putInt("action", action)
bundle.putLong("habit", habit.id!!)
activity.setResult(Activity.RESULT_OK, Intent().apply {
putExtra(EXTRA_STRING_BLURB, blurb)
putExtra(EXTRA_BUNDLE, bundle)
})
activity.setResult(
Activity.RESULT_OK,
Intent().apply {
putExtra(EXTRA_STRING_BLURB, blurb)
putExtra(EXTRA_BUNDLE, bundle)
}
)
activity.finish()
}

@ -19,23 +19,29 @@
package org.isoron.uhabits.automation
import android.R.layout.*
import android.annotation.*
import android.content.*
import android.view.*
import android.widget.*
import org.isoron.uhabits.*
import org.isoron.uhabits.core.models.*
import org.isoron.uhabits.databinding.*
import org.isoron.uhabits.utils.*
import java.util.*
import android.R.layout.simple_spinner_dropdown_item
import android.R.layout.simple_spinner_item
import android.annotation.SuppressLint
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.FrameLayout
import org.isoron.uhabits.R
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.HabitList
import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.databinding.AutomationBinding
import org.isoron.uhabits.utils.setupToolbar
import java.util.LinkedList
@SuppressLint("ViewConstructor")
class EditSettingRootView(
context: Context,
private val habitList: HabitList,
private val onSave: (habit: Habit, action: Int) -> Unit,
args: SettingUtils.Arguments?
context: Context,
private val habitList: HabitList,
private val onSave: (habit: Habit, action: Int) -> Unit,
args: SettingUtils.Arguments?
) : FrameLayout(context) {
private var binding = AutomationBinding.inflate(LayoutInflater.from(context))
@ -43,10 +49,10 @@ class EditSettingRootView(
init {
addView(binding.root)
setupToolbar(
toolbar = binding.toolbar,
title = resources.getString(R.string.app_name),
color = PaletteColor(11),
displayHomeAsUpEnabled = false,
toolbar = binding.toolbar,
title = resources.getString(R.string.app_name),
color = PaletteColor(11),
displayHomeAsUpEnabled = false,
)
populateHabitSpinner()
binding.habitSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
@ -60,8 +66,8 @@ class EditSettingRootView(
binding.buttonSave.setOnClickListener {
val habit = habitList.getByPosition(binding.habitSpinner.selectedItemPosition)
val action = mapSpinnerPositionToAction(
isNumerical = habit.isNumerical,
itemPosition = binding.actionSpinner.selectedItemPosition,
isNumerical = habit.isNumerical,
itemPosition = binding.actionSpinner.selectedItemPosition,
)
onSave(habit, action)
}

@ -19,14 +19,16 @@
package org.isoron.uhabits.automation
import android.content.*
import dagger.*
import org.isoron.uhabits.*
import org.isoron.uhabits.core.models.*
import org.isoron.uhabits.core.ui.widgets.*
import org.isoron.uhabits.core.utils.*
import org.isoron.uhabits.inject.*
import org.isoron.uhabits.receivers.*
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import dagger.Component
import org.isoron.uhabits.HabitsApplication
import org.isoron.uhabits.core.models.HabitList
import org.isoron.uhabits.core.ui.widgets.WidgetBehavior
import org.isoron.uhabits.core.utils.DateUtils
import org.isoron.uhabits.inject.HabitsApplicationComponent
import org.isoron.uhabits.receivers.ReceiverScope
const val ACTION_CHECK = 0
const val ACTION_UNCHECK = 1
@ -44,9 +46,9 @@ class FireSettingReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val app = context.applicationContext as HabitsApplication
val component = DaggerFireSettingReceiver_ReceiverComponent
.builder()
.habitsApplicationComponent(app.component)
.build()
.builder()
.habitsApplicationComponent(app.component)
.build()
allHabits = app.component.habitList
val args = SettingUtils.parseIntent(intent, allHabits) ?: return
val timestamp = DateUtils.getTodayWithOffset()

@ -15,4 +15,4 @@ object SettingUtils {
}
class Arguments(var action: Int, var habit: Habit)
}
}

@ -19,7 +19,7 @@
package org.isoron.uhabits.database
import org.isoron.uhabits.core.database.*
import org.isoron.uhabits.core.database.Cursor
class AndroidCursor(private val cursor: android.database.Cursor) : Cursor {

@ -19,14 +19,14 @@
package org.isoron.uhabits.database
import android.content.*
import android.database.sqlite.*
import org.isoron.uhabits.core.database.*
import java.io.*
import android.content.ContentValues
import android.database.sqlite.SQLiteDatabase
import org.isoron.uhabits.core.database.Database
import java.io.File
class AndroidDatabase(
private val db: SQLiteDatabase,
override val file: File?,
private val db: SQLiteDatabase,
override val file: File?,
) : Database {
override fun beginTransaction() = db.beginTransaction()
@ -42,10 +42,10 @@ class AndroidDatabase(
override fun execute(query: String, vararg params: Any) = db.execSQL(query, params)
override fun update(
tableName: String,
values: Map<String, Any?>,
where: String,
vararg params: String,
tableName: String,
values: Map<String, Any?>,
where: String,
vararg params: String,
): Int {
val contValues = mapToContentValues(values)
return db.update(tableName, contValues, where, params)
@ -57,9 +57,9 @@ class AndroidDatabase(
}
override fun delete(
tableName: String,
where: String,
vararg params: String,
tableName: String,
where: String,
vararg params: String,
) {
db.delete(tableName, where, params)
}

@ -19,19 +19,20 @@
package org.isoron.uhabits.database
import android.database.sqlite.*
import org.isoron.uhabits.core.database.*
import java.io.*
import javax.inject.*
import android.database.sqlite.SQLiteDatabase
import org.isoron.uhabits.core.database.DatabaseOpener
import java.io.File
import javax.inject.Inject
class AndroidDatabaseOpener @Inject constructor() : DatabaseOpener {
override fun open(file: File): AndroidDatabase {
return AndroidDatabase(
db = SQLiteDatabase.openDatabase(
file.absolutePath,
null,
SQLiteDatabase.OPEN_READWRITE,
),
file = file)
db = SQLiteDatabase.openDatabase(
file.absolutePath,
null,
SQLiteDatabase.OPEN_READWRITE,
),
file = file
)
}
}

@ -19,12 +19,12 @@
package org.isoron.uhabits.database
import android.content.*
import android.util.*
import org.isoron.uhabits.*
import org.isoron.uhabits.core.utils.*
import org.isoron.uhabits.utils.*
import java.io.*
import android.content.Context
import android.util.Log
import org.isoron.uhabits.AndroidDirFinder
import org.isoron.uhabits.core.utils.DateUtils
import org.isoron.uhabits.utils.DatabaseUtils
import java.io.File
class AutoBackup(private val context: Context) {
@ -33,7 +33,7 @@ class AutoBackup(private val context: Context) {
fun run(keep: Int = 5) {
Log.i("AutoBackup", "Starting automatic backups...")
val files = listBackupFiles()
var newestTimestamp = 0L;
var newestTimestamp = 0L
if (files.isNotEmpty()) {
newestTimestamp = files.last().lastModified()
}
@ -61,4 +61,4 @@ class AutoBackup(private val context: Context) {
}
return files
}
}
}

@ -18,9 +18,9 @@
*/
package org.isoron.uhabits.inject
import javax.inject.*
import javax.inject.Qualifier
@Qualifier
@MustBeDocumented
@kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
annotation class ActivityContext
annotation class ActivityContext

@ -18,10 +18,10 @@
*/
package org.isoron.uhabits.inject
import javax.inject.*
import javax.inject.Scope
/**
* Scope used by objects that live as long as the activity is alive.
*/
@Scope
annotation class ActivityScope
annotation class ActivityScope

@ -23,4 +23,4 @@ import javax.inject.Qualifier
@Qualifier
@MustBeDocumented
@Retention(AnnotationRetention.RUNTIME)
annotation class AppContext
annotation class AppContext

@ -19,8 +19,9 @@
package org.isoron.uhabits.inject
import dagger.*
import org.isoron.uhabits.core.models.*
import dagger.Module
import dagger.Provides
import org.isoron.uhabits.core.models.Habit
@Module
class HabitModule(private val habit: Habit) {

@ -19,20 +19,27 @@
package org.isoron.uhabits.inject
import dagger.*
import org.isoron.uhabits.activities.common.dialogs.*
import org.isoron.uhabits.activities.habits.list.*
import org.isoron.uhabits.activities.habits.list.views.*
import org.isoron.uhabits.core.ui.*
import org.isoron.uhabits.core.ui.screens.habits.list.*
import dagger.Component
import org.isoron.uhabits.activities.common.dialogs.ColorPickerDialogFactory
import org.isoron.uhabits.activities.habits.list.ListHabitsMenu
import org.isoron.uhabits.activities.habits.list.ListHabitsModule
import org.isoron.uhabits.activities.habits.list.ListHabitsRootView
import org.isoron.uhabits.activities.habits.list.ListHabitsScreen
import org.isoron.uhabits.activities.habits.list.ListHabitsSelectionMenu
import org.isoron.uhabits.activities.habits.list.views.HabitCardListAdapter
import org.isoron.uhabits.core.ui.ThemeSwitcher
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior
@ActivityScope
@Component(modules = arrayOf(
@Component(
modules = arrayOf(
ActivityContextModule::class,
HabitsActivityModule::class,
ListHabitsModule::class,
HabitModule::class
), dependencies = arrayOf(HabitsApplicationComponent::class))
),
dependencies = arrayOf(HabitsApplicationComponent::class)
)
interface HabitsActivityComponent {
val colorPickerDialogFactory: ColorPickerDialogFactory
val habitCardListAdapter: HabitCardListAdapter

@ -19,11 +19,12 @@
package org.isoron.uhabits.inject
import android.content.*
import dagger.*
import org.isoron.uhabits.activities.*
import org.isoron.uhabits.core.preferences.*
import org.isoron.uhabits.core.ui.*
import android.content.Context
import dagger.Module
import dagger.Provides
import org.isoron.uhabits.activities.AndroidThemeSwitcher
import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.core.ui.ThemeSwitcher
@Module
class HabitsActivityModule {
@ -31,8 +32,8 @@ class HabitsActivityModule {
@Provides
@ActivityScope
fun getThemeSwitcher(
@ActivityContext context: Context,
prefs: Preferences
@ActivityContext context: Context,
prefs: Preferences
): ThemeSwitcher {
return AndroidThemeSwitcher(context, prefs)
}

@ -19,28 +19,35 @@
package org.isoron.uhabits.inject
import android.content.*
import dagger.*
import org.isoron.uhabits.core.*
import org.isoron.uhabits.core.commands.*
import org.isoron.uhabits.core.database.*
import org.isoron.uhabits.core.io.*
import org.isoron.uhabits.core.models.*
import org.isoron.uhabits.core.models.sqlite.*
import org.isoron.uhabits.core.preferences.*
import org.isoron.uhabits.core.reminders.*
import org.isoron.uhabits.core.sync.*
import org.isoron.uhabits.core.tasks.*
import org.isoron.uhabits.core.ui.*
import org.isoron.uhabits.database.*
import org.isoron.uhabits.inject.*
import org.isoron.uhabits.intents.*
import org.isoron.uhabits.io.*
import org.isoron.uhabits.notifications.*
import org.isoron.uhabits.preferences.*
import org.isoron.uhabits.sync.*
import org.isoron.uhabits.utils.*
import java.io.*
import android.content.Context
import dagger.Module
import dagger.Provides
import org.isoron.uhabits.core.AppScope
import org.isoron.uhabits.core.commands.CommandRunner
import org.isoron.uhabits.core.database.Database
import org.isoron.uhabits.core.database.DatabaseOpener
import org.isoron.uhabits.core.io.Logging
import org.isoron.uhabits.core.models.HabitList
import org.isoron.uhabits.core.models.ModelFactory
import org.isoron.uhabits.core.models.sqlite.SQLModelFactory
import org.isoron.uhabits.core.models.sqlite.SQLiteHabitList
import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.core.preferences.WidgetPreferences
import org.isoron.uhabits.core.reminders.ReminderScheduler
import org.isoron.uhabits.core.sync.AbstractSyncServer
import org.isoron.uhabits.core.sync.NetworkManager
import org.isoron.uhabits.core.tasks.TaskRunner
import org.isoron.uhabits.core.ui.NotificationTray
import org.isoron.uhabits.database.AndroidDatabase
import org.isoron.uhabits.database.AndroidDatabaseOpener
import org.isoron.uhabits.intents.IntentScheduler
import org.isoron.uhabits.io.AndroidLogging
import org.isoron.uhabits.notifications.AndroidNotificationTray
import org.isoron.uhabits.preferences.SharedPreferencesStorage
import org.isoron.uhabits.sync.AndroidNetworkManager
import org.isoron.uhabits.sync.RemoteSyncServer
import org.isoron.uhabits.utils.DatabaseUtils
import java.io.File
@Module
class HabitsModule(dbFile: File) {
@ -56,10 +63,10 @@ class HabitsModule(dbFile: File) {
@Provides
@AppScope
fun getReminderScheduler(
sys: IntentScheduler,
commandRunner: CommandRunner,
habitList: HabitList,
widgetPreferences: WidgetPreferences
sys: IntentScheduler,
commandRunner: CommandRunner,
habitList: HabitList,
widgetPreferences: WidgetPreferences
): ReminderScheduler {
return ReminderScheduler(commandRunner, habitList, sys, widgetPreferences)
}
@ -67,10 +74,10 @@ class HabitsModule(dbFile: File) {
@Provides
@AppScope
fun getTray(
taskRunner: TaskRunner,
commandRunner: CommandRunner,
preferences: Preferences,
screen: AndroidNotificationTray
taskRunner: TaskRunner,
commandRunner: CommandRunner,
preferences: Preferences,
screen: AndroidNotificationTray
): NotificationTray {
return NotificationTray(taskRunner, commandRunner, preferences, screen)
}
@ -78,7 +85,7 @@ class HabitsModule(dbFile: File) {
@Provides
@AppScope
fun getWidgetPreferences(
storage: SharedPreferencesStorage
storage: SharedPreferencesStorage
): WidgetPreferences {
return WidgetPreferences(storage)
}
@ -115,7 +122,7 @@ class HabitsModule(dbFile: File) {
@Provides
@AppScope
fun getSyncServer(preferences: Preferences) : AbstractSyncServer {
fun getSyncServer(preferences: Preferences): AbstractSyncServer {
return RemoteSyncServer(preferences)
}
@ -125,4 +132,3 @@ class HabitsModule(dbFile: File) {
return db
}
}

@ -19,23 +19,24 @@
package org.isoron.uhabits.intents
import android.content.*
import android.net.*
import org.isoron.uhabits.*
import org.isoron.uhabits.activities.about.*
import org.isoron.uhabits.activities.habits.edit.*
import org.isoron.uhabits.activities.habits.show.*
import org.isoron.uhabits.activities.intro.*
import org.isoron.uhabits.activities.settings.*
import org.isoron.uhabits.activities.sync.*
import org.isoron.uhabits.core.models.*
import javax.inject.*
import android.content.Context
import android.content.Intent
import android.net.Uri
import org.isoron.uhabits.R
import org.isoron.uhabits.activities.about.AboutActivity
import org.isoron.uhabits.activities.habits.edit.EditHabitActivity
import org.isoron.uhabits.activities.habits.show.ShowHabitActivity
import org.isoron.uhabits.activities.intro.IntroActivity
import org.isoron.uhabits.activities.settings.SettingsActivity
import org.isoron.uhabits.activities.sync.SyncActivity
import org.isoron.uhabits.core.models.Habit
import javax.inject.Inject
class IntentFactory
@Inject constructor() {
fun helpTranslate(context: Context) =
buildViewIntent(context.getString(R.string.translateURL))
buildViewIntent(context.getString(R.string.translateURL))
fun openDocument() = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
@ -43,33 +44,33 @@ class IntentFactory
}
fun rateApp(context: Context) =
buildViewIntent(context.getString(R.string.playStoreURL))
buildViewIntent(context.getString(R.string.playStoreURL))
fun sendFeedback(context: Context) =
buildSendToIntent(context.getString(R.string.feedbackURL))
buildSendToIntent(context.getString(R.string.feedbackURL))
fun privacyPolicy(context: Context) =
buildViewIntent(context.getString(R.string.privacyPolicyURL))
buildViewIntent(context.getString(R.string.privacyPolicyURL))
fun startAboutActivity(context: Context) =
Intent(context, AboutActivity::class.java)
Intent(context, AboutActivity::class.java)
fun startIntroActivity(context: Context) =
Intent(context, IntroActivity::class.java)
Intent(context, IntroActivity::class.java)
fun startSettingsActivity(context: Context) =
Intent(context, SettingsActivity::class.java)
Intent(context, SettingsActivity::class.java)
fun startShowHabitActivity(context: Context, habit: Habit) =
Intent(context, ShowHabitActivity::class.java).apply {
data = Uri.parse(habit.uriString)
}
Intent(context, ShowHabitActivity::class.java).apply {
data = Uri.parse(habit.uriString)
}
fun viewFAQ(context: Context) =
buildViewIntent(context.getString(R.string.helpURL))
buildViewIntent(context.getString(R.string.helpURL))
fun viewSourceCode(context: Context) =
buildViewIntent(context.getString(R.string.sourceCodeURL))
buildViewIntent(context.getString(R.string.sourceCodeURL))
private fun buildSendToIntent(url: String) = Intent().apply {
action = Intent.ACTION_SENDTO
@ -82,7 +83,7 @@ class IntentFactory
}
fun codeContributors(context: Context) =
buildViewIntent(context.getString(R.string.codeContributorsURL))
buildViewIntent(context.getString(R.string.codeContributorsURL))
private fun startEditActivity(context: Context): Intent {
return Intent(context, EditHabitActivity::class.java)

@ -19,13 +19,15 @@
package org.isoron.uhabits.intents
import android.content.*
import android.content.ContentUris.*
import android.net.*
import org.isoron.uhabits.core.*
import org.isoron.uhabits.core.models.*
import org.isoron.uhabits.core.utils.*
import javax.inject.*
import android.content.ContentUris.parseId
import android.content.Intent
import android.net.Uri
import org.isoron.uhabits.core.AppScope
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.HabitList
import org.isoron.uhabits.core.models.Timestamp
import org.isoron.uhabits.core.utils.DateUtils
import javax.inject.Inject
@AppScope
class IntentParser
@ -37,13 +39,13 @@ class IntentParser
}
fun copyIntentData(source: Intent, destination: Intent) {
destination.data = source.data;
destination.data = source.data
destination.putExtra("timestamp", source.getLongExtra("timestamp", DateUtils.getTodayWithOffset().unixTime))
}
private fun parseHabit(uri: Uri): Habit {
val habit = habits.getById(parseId(uri)) ?:
throw IllegalArgumentException("habit not found")
val habit = habits.getById(parseId(uri))
?: throw IllegalArgumentException("habit not found")
return habit
}

@ -19,37 +19,42 @@
package org.isoron.uhabits.intents
import android.app.*
import android.app.AlarmManager.*
import android.content.*
import android.content.Context.*
import android.os.Build.VERSION.*
import android.os.Build.VERSION_CODES.*
import android.util.*
import org.isoron.uhabits.core.*
import org.isoron.uhabits.core.models.*
import org.isoron.uhabits.core.reminders.ReminderScheduler.*
import org.isoron.uhabits.core.utils.*
import org.isoron.uhabits.inject.*
import java.util.*
import javax.inject.*
import android.app.AlarmManager
import android.app.AlarmManager.RTC
import android.app.AlarmManager.RTC_WAKEUP
import android.app.PendingIntent
import android.content.Context
import android.content.Context.ALARM_SERVICE
import android.os.Build.VERSION.SDK_INT
import android.os.Build.VERSION_CODES.M
import android.util.Log
import org.isoron.uhabits.core.AppScope
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.reminders.ReminderScheduler.SchedulerResult
import org.isoron.uhabits.core.reminders.ReminderScheduler.SystemScheduler
import org.isoron.uhabits.core.utils.DateFormats
import org.isoron.uhabits.inject.AppContext
import java.util.Date
import javax.inject.Inject
@AppScope
class IntentScheduler
@Inject constructor(
@AppContext context: Context,
private val pendingIntents: PendingIntentFactory
@AppContext context: Context,
private val pendingIntents: PendingIntentFactory
) : SystemScheduler {
private val manager =
context.getSystemService(ALARM_SERVICE) as AlarmManager
context.getSystemService(ALARM_SERVICE) as AlarmManager
private fun schedule(timestamp: Long, intent: PendingIntent, alarmType: Int): SchedulerResult {
val now = System.currentTimeMillis()
Log.d("IntentScheduler", "timestamp=$timestamp now=$now")
if (timestamp < now) {
Log.e("IntentScheduler",
"Ignoring attempt to schedule intent in the past.")
Log.e(
"IntentScheduler",
"Ignoring attempt to schedule intent in the past."
)
return SchedulerResult.IGNORED
}
if (SDK_INT >= M)
@ -59,9 +64,11 @@ class IntentScheduler
return SchedulerResult.OK
}
override fun scheduleShowReminder(reminderTime: Long,
habit: Habit,
timestamp: Long): SchedulerResult {
override fun scheduleShowReminder(
reminderTime: Long,
habit: Habit,
timestamp: Long
): SchedulerResult {
val intent = pendingIntents.showReminder(habit, reminderTime, timestamp)
logReminderScheduled(habit, reminderTime)
return schedule(reminderTime, intent, RTC_WAKEUP)
@ -81,7 +88,9 @@ class IntentScheduler
val name = habit.name.substring(0, min)
val df = DateFormats.getBackupDateFormat()
val time = df.format(Date(reminderTime))
Log.i("ReminderHelper",
String.format("Setting alarm (%s): %s", time, name))
Log.i(
"ReminderHelper",
String.format("Setting alarm (%s): %s", time, name)
)
}
}

@ -19,111 +19,138 @@
package org.isoron.uhabits.intents
import android.app.*
import android.app.PendingIntent.*
import android.content.*
import android.net.*
import org.isoron.uhabits.core.*
import org.isoron.uhabits.core.models.*
import org.isoron.uhabits.inject.*
import org.isoron.uhabits.receivers.*
import javax.inject.*
import android.app.PendingIntent
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
import android.app.PendingIntent.getBroadcast
import android.content.Context
import android.content.Intent
import android.net.Uri
import org.isoron.uhabits.core.AppScope
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.Timestamp
import org.isoron.uhabits.inject.AppContext
import org.isoron.uhabits.receivers.ReminderReceiver
import org.isoron.uhabits.receivers.WidgetReceiver
import javax.inject.Inject
@AppScope
class PendingIntentFactory
@Inject constructor(
@AppContext private val context: Context,
private val intentFactory: IntentFactory) {
@AppContext private val context: Context,
private val intentFactory: IntentFactory
) {
fun addCheckmark(habit: Habit, timestamp: Timestamp?): PendingIntent =
PendingIntent.getBroadcast(
context, 1,
Intent(context, WidgetReceiver::class.java).apply {
data = Uri.parse(habit.uriString)
action = WidgetReceiver.ACTION_ADD_REPETITION
if (timestamp != null) putExtra("timestamp", timestamp.unixTime)
},
FLAG_UPDATE_CURRENT)
PendingIntent.getBroadcast(
context,
1,
Intent(context, WidgetReceiver::class.java).apply {
data = Uri.parse(habit.uriString)
action = WidgetReceiver.ACTION_ADD_REPETITION
if (timestamp != null) putExtra("timestamp", timestamp.unixTime)
},
FLAG_UPDATE_CURRENT
)
fun dismissNotification(habit: Habit): PendingIntent =
PendingIntent.getBroadcast(
context, 0,
Intent(context, ReminderReceiver::class.java).apply {
action = WidgetReceiver.ACTION_DISMISS_REMINDER
data = Uri.parse(habit.uriString)
},
FLAG_UPDATE_CURRENT)
PendingIntent.getBroadcast(
context,
0,
Intent(context, ReminderReceiver::class.java).apply {
action = WidgetReceiver.ACTION_DISMISS_REMINDER
data = Uri.parse(habit.uriString)
},
FLAG_UPDATE_CURRENT
)
fun removeRepetition(habit: Habit): PendingIntent =
PendingIntent.getBroadcast(
context, 3,
Intent(context, WidgetReceiver::class.java).apply {
action = WidgetReceiver.ACTION_REMOVE_REPETITION
data = Uri.parse(habit.uriString)
},
FLAG_UPDATE_CURRENT)
PendingIntent.getBroadcast(
context,
3,
Intent(context, WidgetReceiver::class.java).apply {
action = WidgetReceiver.ACTION_REMOVE_REPETITION
data = Uri.parse(habit.uriString)
},
FLAG_UPDATE_CURRENT
)
fun showHabit(habit: Habit): PendingIntent =
androidx.core.app.TaskStackBuilder
.create(context)
.addNextIntentWithParentStack(
intentFactory.startShowHabitActivity(
context, habit))
.getPendingIntent(0, FLAG_UPDATE_CURRENT)!!
fun showReminder(habit: Habit,
reminderTime: Long?,
timestamp: Long): PendingIntent =
PendingIntent.getBroadcast(
androidx.core.app.TaskStackBuilder
.create(context)
.addNextIntentWithParentStack(
intentFactory.startShowHabitActivity(
context,
(habit.id!! % Integer.MAX_VALUE).toInt() + 1,
Intent(context, ReminderReceiver::class.java).apply {
action = ReminderReceiver.ACTION_SHOW_REMINDER
data = Uri.parse(habit.uriString)
putExtra("timestamp", timestamp)
putExtra("reminderTime", reminderTime)
},
FLAG_UPDATE_CURRENT)
habit
)
)
.getPendingIntent(0, FLAG_UPDATE_CURRENT)!!
fun showReminder(
habit: Habit,
reminderTime: Long?,
timestamp: Long
): PendingIntent =
PendingIntent.getBroadcast(
context,
(habit.id!! % Integer.MAX_VALUE).toInt() + 1,
Intent(context, ReminderReceiver::class.java).apply {
action = ReminderReceiver.ACTION_SHOW_REMINDER
data = Uri.parse(habit.uriString)
putExtra("timestamp", timestamp)
putExtra("reminderTime", reminderTime)
},
FLAG_UPDATE_CURRENT
)
fun snoozeNotification(habit: Habit): PendingIntent =
PendingIntent.getBroadcast(
context, 0,
Intent(context, ReminderReceiver::class.java).apply {
data = Uri.parse(habit.uriString)
action = ReminderReceiver.ACTION_SNOOZE_REMINDER
},
FLAG_UPDATE_CURRENT)
PendingIntent.getBroadcast(
context,
0,
Intent(context, ReminderReceiver::class.java).apply {
data = Uri.parse(habit.uriString)
action = ReminderReceiver.ACTION_SNOOZE_REMINDER
},
FLAG_UPDATE_CURRENT
)
fun toggleCheckmark(habit: Habit, timestamp: Long?): PendingIntent =
PendingIntent.getBroadcast(
context, 2,
Intent(context, WidgetReceiver::class.java).apply {
data = Uri.parse(habit.uriString)
action = WidgetReceiver.ACTION_TOGGLE_REPETITION
if (timestamp != null) putExtra("timestamp", timestamp)
},
FLAG_UPDATE_CURRENT)
PendingIntent.getBroadcast(
context,
2,
Intent(context, WidgetReceiver::class.java).apply {
data = Uri.parse(habit.uriString)
action = WidgetReceiver.ACTION_TOGGLE_REPETITION
if (timestamp != null) putExtra("timestamp", timestamp)
},
FLAG_UPDATE_CURRENT
)
fun setNumericalValue(widgetContext: Context,
habit: Habit,
numericalValue: Int,
timestamp: Long?):
PendingIntent =
fun setNumericalValue(
widgetContext: Context,
habit: Habit,
numericalValue: Int,
timestamp: Long?
):
PendingIntent =
getBroadcast(
widgetContext, 2,
Intent(widgetContext, WidgetReceiver::class.java).apply {
data = Uri.parse(habit.uriString)
action = WidgetReceiver.ACTION_SET_NUMERICAL_VALUE
putExtra("numericalValue", numericalValue);
if (timestamp != null) putExtra("timestamp", timestamp)
},
FLAG_UPDATE_CURRENT)
widgetContext,
2,
Intent(widgetContext, WidgetReceiver::class.java).apply {
data = Uri.parse(habit.uriString)
action = WidgetReceiver.ACTION_SET_NUMERICAL_VALUE
putExtra("numericalValue", numericalValue)
if (timestamp != null) putExtra("timestamp", timestamp)
},
FLAG_UPDATE_CURRENT
)
fun updateWidgets(): PendingIntent =
PendingIntent.getBroadcast(
context, 0,
Intent(context, WidgetReceiver::class.java).apply {
action = WidgetReceiver.ACTION_UPDATE_WIDGETS_VALUE
},
FLAG_UPDATE_CURRENT)
PendingIntent.getBroadcast(
context,
0,
Intent(context, WidgetReceiver::class.java).apply {
action = WidgetReceiver.ACTION_UPDATE_WIDGETS_VALUE
},
FLAG_UPDATE_CURRENT
)
}

@ -19,8 +19,9 @@
package org.isoron.uhabits.io
import android.util.*
import org.isoron.uhabits.core.io.*
import android.util.Log
import org.isoron.uhabits.core.io.Logger
import org.isoron.uhabits.core.io.Logging
class AndroidLogging : Logging {
override fun getLogger(name: String): Logger {
@ -44,5 +45,4 @@ class AndroidLogger(val name: String) : Logger {
override fun error(exception: Exception) {
Log.e(name, "Exception", exception)
}
}
}

@ -19,31 +19,37 @@
package org.isoron.uhabits.notifications
import android.app.*
import android.content.*
import android.graphics.*
import android.graphics.BitmapFactory.*
import android.os.*
import android.os.Build.VERSION.*
import android.util.*
import androidx.core.app.*
import androidx.core.app.NotificationCompat.*
import android.app.Activity
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.graphics.BitmapFactory.decodeResource
import android.graphics.Color
import android.os.Build
import android.os.Build.VERSION.SDK_INT
import android.util.Log
import androidx.core.app.NotificationCompat.Action
import androidx.core.app.NotificationCompat.Builder
import androidx.core.app.NotificationCompat.WearableExtender
import androidx.core.app.NotificationManagerCompat
import org.isoron.uhabits.R
import org.isoron.uhabits.core.*
import org.isoron.uhabits.core.models.*
import org.isoron.uhabits.core.preferences.*
import org.isoron.uhabits.core.ui.*
import org.isoron.uhabits.inject.*
import org.isoron.uhabits.intents.*
import javax.inject.*
import org.isoron.uhabits.core.AppScope
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.Timestamp
import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.core.ui.NotificationTray
import org.isoron.uhabits.inject.AppContext
import org.isoron.uhabits.intents.PendingIntentFactory
import javax.inject.Inject
@AppScope
class AndroidNotificationTray
@Inject constructor(
@AppContext private val context: Context,
private val pendingIntents: PendingIntentFactory,
private val preferences: Preferences,
private val ringtoneManager: RingtoneManager
@AppContext private val context: Context,
private val pendingIntents: PendingIntentFactory,
private val preferences: Preferences,
private val ringtoneManager: RingtoneManager
) : NotificationTray.SystemTray {
private var active = HashSet<Int>()
@ -57,10 +63,12 @@ class AndroidNotificationTray
active.remove(id)
}
override fun showNotification(habit: Habit,
notificationId: Int,
timestamp: Timestamp,
reminderTime: Long) {
override fun showNotification(
habit: Habit,
notificationId: Int,
timestamp: Timestamp,
reminderTime: Long
) {
val notificationManager = NotificationManagerCompat.from(context)
val notification = buildNotification(habit, reminderTime, timestamp)
createAndroidNotificationChannel(context)
@ -68,37 +76,45 @@ class AndroidNotificationTray
notificationManager.notify(notificationId, notification)
} catch (e: RuntimeException) {
// Some Xiaomi phones produce a RuntimeException if custom notification sounds are used.
Log.i("AndroidNotificationTray",
"Failed to show notification. Retrying without sound.")
val n = buildNotification(habit,
reminderTime,
timestamp,
disableSound = true)
Log.i(
"AndroidNotificationTray",
"Failed to show notification. Retrying without sound."
)
val n = buildNotification(
habit,
reminderTime,
timestamp,
disableSound = true
)
notificationManager.notify(notificationId, n)
}
active.add(notificationId)
}
fun buildNotification(habit: Habit,
reminderTime: Long,
timestamp: Timestamp,
disableSound: Boolean = false): Notification {
fun buildNotification(
habit: Habit,
reminderTime: Long,
timestamp: Timestamp,
disableSound: Boolean = false
): Notification {
val addRepetitionAction = Action(
R.drawable.ic_action_check,
context.getString(R.string.yes),
pendingIntents.addCheckmark(habit, timestamp))
R.drawable.ic_action_check,
context.getString(R.string.yes),
pendingIntents.addCheckmark(habit, timestamp)
)
val removeRepetitionAction = Action(
R.drawable.ic_action_cancel,
context.getString(R.string.no),
pendingIntents.removeRepetition(habit))
R.drawable.ic_action_cancel,
context.getString(R.string.no),
pendingIntents.removeRepetition(habit)
)
val enterAction = Action(
R.drawable.ic_action_check,
context.getString(R.string.enter),
pendingIntents.setNumericalValue(context, habit, 0, null))
R.drawable.ic_action_check,
context.getString(R.string.enter),
pendingIntents.setNumericalValue(context, habit, 0, null)
)
val wearableBg = decodeResource(context.resources, R.drawable.stripe)
@ -109,26 +125,26 @@ class AndroidNotificationTray
val defaultText = context.getString(R.string.default_reminder_question)
val builder = Builder(context, REMINDERS_CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle(habit.name)
.setContentText(if(habit.question.isBlank()) defaultText else habit.question)
.setContentIntent(pendingIntents.showHabit(habit))
.setDeleteIntent(pendingIntents.dismissNotification(habit))
.setSound(null)
.setWhen(reminderTime)
.setShowWhen(true)
.setOngoing(preferences.shouldMakeNotificationsSticky())
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle(habit.name)
.setContentText(if (habit.question.isBlank()) defaultText else habit.question)
.setContentIntent(pendingIntents.showHabit(habit))
.setDeleteIntent(pendingIntents.dismissNotification(habit))
.setSound(null)
.setWhen(reminderTime)
.setShowWhen(true)
.setOngoing(preferences.shouldMakeNotificationsSticky())
if (habit.isNumerical) {
wearableExtender.addAction(enterAction)
builder.addAction(enterAction)
} else {
wearableExtender
.addAction(addRepetitionAction)
.addAction(removeRepetitionAction)
.addAction(addRepetitionAction)
.addAction(removeRepetitionAction)
builder
.addAction(addRepetitionAction)
.addAction(removeRepetitionAction)
.addAction(addRepetitionAction)
.addAction(removeRepetitionAction)
}
if (!disableSound)
@ -137,9 +153,11 @@ class AndroidNotificationTray
if (preferences.shouldMakeNotificationsLed())
builder.setLights(Color.RED, 1000, 1000)
val snoozeAction = Action(R.drawable.ic_action_snooze,
context.getString(R.string.snooze),
pendingIntents.snoozeNotification(habit))
val snoozeAction = Action(
R.drawable.ic_action_snooze,
context.getString(R.string.snooze),
pendingIntents.snoozeNotification(habit)
)
wearableExtender.addAction(snoozeAction)
builder.addAction(snoozeAction)
@ -151,14 +169,15 @@ class AndroidNotificationTray
private const val REMINDERS_CHANNEL_ID = "REMINDERS"
fun createAndroidNotificationChannel(context: Context) {
val notificationManager = context.getSystemService(Activity.NOTIFICATION_SERVICE)
as NotificationManager
as NotificationManager
if (SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(REMINDERS_CHANNEL_ID,
context.resources.getString(R.string.reminder),
NotificationManager.IMPORTANCE_DEFAULT)
val channel = NotificationChannel(
REMINDERS_CHANNEL_ID,
context.resources.getString(R.string.reminder),
NotificationManager.IMPORTANCE_DEFAULT
)
notificationManager.createNotificationChannel(channel)
}
}
}
}

@ -19,22 +19,25 @@
package org.isoron.uhabits.notifications
import android.content.*
import android.media.RingtoneManager.*
import android.net.*
import android.preference.*
import android.provider.*
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.media.RingtoneManager.EXTRA_RINGTONE_PICKED_URI
import android.media.RingtoneManager.getRingtone
import android.net.Uri
import android.preference.PreferenceManager
import android.provider.Settings
import org.isoron.uhabits.R
import org.isoron.uhabits.core.*
import org.isoron.uhabits.inject.*
import javax.inject.*
import org.isoron.uhabits.core.AppScope
import org.isoron.uhabits.inject.AppContext
import javax.inject.Inject
@AppScope
class RingtoneManager
@Inject constructor(@AppContext private val context: Context) {
val prefs: SharedPreferences =
PreferenceManager.getDefaultSharedPreferences(context)
PreferenceManager.getDefaultSharedPreferences(context)
fun getName(): String? {
try {
@ -54,8 +57,10 @@ class RingtoneManager
fun getURI(): Uri? {
var ringtoneUri: Uri? = null
val defaultRingtoneUri = Settings.System.DEFAULT_NOTIFICATION_URI
val prefRingtoneUri = prefs.getString("pref_ringtone_uri",
defaultRingtoneUri.toString())!!
val prefRingtoneUri = prefs.getString(
"pref_ringtone_uri",
defaultRingtoneUri.toString()
)!!
if (prefRingtoneUri.isNotEmpty())
ringtoneUri = Uri.parse(prefRingtoneUri)

@ -19,22 +19,23 @@
package org.isoron.uhabits.preferences
import android.content.*
import android.preference.*
import android.content.Context
import android.content.SharedPreferences
import android.preference.PreferenceManager
import org.isoron.uhabits.R
import org.isoron.uhabits.core.*
import org.isoron.uhabits.core.preferences.*
import org.isoron.uhabits.inject.*
import javax.inject.*
import org.isoron.uhabits.core.AppScope
import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.inject.AppContext
import javax.inject.Inject
@AppScope
class SharedPreferencesStorage
@Inject constructor(
@AppContext context: Context
@AppContext context: Context
) : SharedPreferences.OnSharedPreferenceChangeListener, Preferences.Storage {
private val sharedPrefs: SharedPreferences =
PreferenceManager.getDefaultSharedPreferences(context)
PreferenceManager.getDefaultSharedPreferences(context)
private var preferences: Preferences? = null
@ -46,38 +47,40 @@ class SharedPreferencesStorage
override fun clear() = sharedPrefs.edit().clear().apply()
override fun getBoolean(key: String, defValue: Boolean) =
sharedPrefs.getBoolean(key, defValue)
sharedPrefs.getBoolean(key, defValue)
override fun getInt(key: String, defValue: Int) =
sharedPrefs.getInt(key, defValue)
sharedPrefs.getInt(key, defValue)
override fun getLong(key: String, defValue: Long) =
sharedPrefs.getLong(key, defValue)
sharedPrefs.getLong(key, defValue)
override fun getString(key: String, defValue: String): String =
sharedPrefs.getString(key, defValue)!!
sharedPrefs.getString(key, defValue)!!
override fun onAttached(preferences: Preferences) {
this.preferences = preferences
}
override fun putBoolean(key: String, value: Boolean) =
sharedPrefs.edit().putBoolean(key, value).apply()
sharedPrefs.edit().putBoolean(key, value).apply()
override fun putInt(key: String, value: Int) =
sharedPrefs.edit().putInt(key, value).apply()
sharedPrefs.edit().putInt(key, value).apply()
override fun putLong(key: String, value: Long) =
sharedPrefs.edit().putLong(key, value).apply()
sharedPrefs.edit().putLong(key, value).apply()
override fun putString(key: String, value: String) =
sharedPrefs.edit().putString(key, value).apply()
sharedPrefs.edit().putString(key, value).apply()
override fun remove(key: String) =
sharedPrefs.edit().remove(key).apply()
sharedPrefs.edit().remove(key).apply()
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences,
key: String) {
override fun onSharedPreferenceChanged(
sharedPreferences: SharedPreferences,
key: String
) {
val preferences = this.preferences ?: return
sharedPreferences.unregisterOnSharedPreferenceChangeListener(this)
when (key) {

@ -19,12 +19,14 @@
package org.isoron.uhabits.sync
import android.content.*
import android.net.*
import org.isoron.uhabits.core.sync.*
import android.content.Context
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkRequest
import org.isoron.uhabits.core.sync.NetworkManager
class AndroidNetworkManager(
val context: Context,
val context: Context,
) : NetworkManager, ConnectivityManager.NetworkCallback() {
val listeners = mutableListOf<NetworkManager.Listener>()
@ -54,4 +56,4 @@ class AndroidNetworkManager(
connected = false
for (l in listeners) l.onNetworkLost()
}
}
}

@ -19,21 +19,32 @@
package org.isoron.uhabits.sync
import android.util.*
import io.ktor.client.*
import io.ktor.client.engine.android.*
import io.ktor.client.features.*
import io.ktor.client.features.json.*
import io.ktor.client.request.*
import kotlinx.coroutines.*
import org.isoron.uhabits.core.preferences.*
import org.isoron.uhabits.core.sync.*
import android.util.Log
import io.ktor.client.HttpClient
import io.ktor.client.engine.android.Android
import io.ktor.client.features.ClientRequestException
import io.ktor.client.features.ServerResponseException
import io.ktor.client.features.json.JsonFeature
import io.ktor.client.request.get
import io.ktor.client.request.header
import io.ktor.client.request.post
import io.ktor.client.request.put
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.invoke
import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.core.sync.AbstractSyncServer
import org.isoron.uhabits.core.sync.EditConflictException
import org.isoron.uhabits.core.sync.GetDataVersionResponse
import org.isoron.uhabits.core.sync.KeyNotFoundException
import org.isoron.uhabits.core.sync.RegisterReponse
import org.isoron.uhabits.core.sync.ServiceUnavailable
import org.isoron.uhabits.core.sync.SyncData
class RemoteSyncServer(
private val preferences: Preferences,
private val httpClient: HttpClient = HttpClient(Android) {
install(JsonFeature)
}
private val preferences: Preferences,
private val httpClient: HttpClient = HttpClient(Android) {
install(JsonFeature)
}
) : AbstractSyncServer {
override suspend fun register(): String = Dispatchers.IO {
@ -42,7 +53,7 @@ class RemoteSyncServer(
Log.i("RemoteSyncServer", "POST $url")
val response: RegisterReponse = httpClient.post(url)
return@IO response.key
} catch(e: ServerResponseException) {
} catch (e: ServerResponseException) {
throw ServiceUnavailable()
}
}
@ -59,8 +70,8 @@ class RemoteSyncServer(
throw ServiceUnavailable()
} catch (e: ClientRequestException) {
Log.w("RemoteSyncServer", "ClientRequestException", e)
if(e.message!!.contains("409")) throw EditConflictException()
if(e.message!!.contains("404")) throw KeyNotFoundException()
if (e.message!!.contains("409")) throw EditConflictException()
if (e.message!!.contains("404")) throw KeyNotFoundException()
throw e
}
}
@ -85,7 +96,7 @@ class RemoteSyncServer(
Log.i("RemoteSyncServer", "GET $url")
val response: GetDataVersionResponse = httpClient.get(url)
return@IO response.version
} catch(e: ServerResponseException) {
} catch (e: ServerResponseException) {
throw ServiceUnavailable()
} catch (e: ClientRequestException) {
Log.w("RemoteSyncServer", "ClientRequestException", e)

@ -55,4 +55,4 @@ object ColorUtils {
val f2 = (color2 shr channel and 0xff).toFloat() * (1.0f - amount)
return (fl + f2).toInt() and 0xff shl channel
}
}
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save