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 { repositories {
google() google()
jcenter() jcenter()
maven {
url "https://plugins.gradle.org/m2/"
}
} }
dependencies { dependencies {
@ -9,6 +12,7 @@ buildscript {
classpath "com.neenbedankt.gradle.plugins:android-apt:1.8" classpath "com.neenbedankt.gradle.plugins:android-apt:1.8"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$KOTLIN_VERSION" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$KOTLIN_VERSION"
classpath "org.ajoberstar:grgit:1.5.0" 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/" } maven { url "https://oss.sonatype.org/content/repositories/snapshots/" }
jcenter() jcenter()
} }
apply plugin: "org.jlleitschuh.gradle.ktlint"
} }

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

@ -19,12 +19,22 @@
package org.isoron.uhabits package org.isoron.uhabits
import dagger.* import dagger.Component
import org.isoron.uhabits.activities.habits.list.* import dagger.Module
import org.isoron.uhabits.activities.habits.list.views.* import dagger.Provides
import org.isoron.uhabits.core.ui.screens.habits.list.* import org.isoron.uhabits.activities.habits.list.ListHabitsModule
import org.isoron.uhabits.inject.* import org.isoron.uhabits.activities.habits.list.views.CheckmarkButtonViewFactory
import org.mockito.Mockito.* 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 @Module
class TestModule { class TestModule {
@ -32,13 +42,16 @@ class TestModule {
} }
@ActivityScope @ActivityScope
@Component(modules = arrayOf( @Component(
modules = arrayOf(
ActivityContextModule::class, ActivityContextModule::class,
HabitsActivityModule::class, HabitsActivityModule::class,
ListHabitsModule::class, ListHabitsModule::class,
HabitModule::class, HabitModule::class,
TestModule::class TestModule::class
), dependencies = arrayOf(HabitsApplicationComponent::class)) ),
dependencies = arrayOf(HabitsApplicationComponent::class)
)
interface HabitsActivityTestComponent { interface HabitsActivityTestComponent {
fun getCheckmarkPanelViewFactory(): CheckmarkPanelViewFactory fun getCheckmarkPanelViewFactory(): CheckmarkPanelViewFactory
fun getHabitCardViewFactory(): HabitCardViewFactory fun getHabitCardViewFactory(): HabitCardViewFactory

@ -19,11 +19,20 @@
package org.isoron.uhabits.acceptance package org.isoron.uhabits.acceptance
import androidx.test.filters.* import androidx.test.filters.LargeTest
import org.isoron.uhabits.* import org.isoron.uhabits.BaseUserInterfaceTest
import org.isoron.uhabits.acceptance.steps.* import org.isoron.uhabits.acceptance.steps.CommonSteps.clickText
import org.isoron.uhabits.acceptance.steps.CommonSteps.* import org.isoron.uhabits.acceptance.steps.CommonSteps.launchApp
import org.junit.* 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 @LargeTest
class BackupTest : BaseUserInterfaceTest() { class BackupTest : BaseUserInterfaceTest() {

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

@ -20,12 +20,13 @@
package org.isoron.uhabits.activities.habits.list.views package org.isoron.uhabits.activities.habits.list.views
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.* import androidx.test.filters.MediumTest
import org.isoron.uhabits.* import org.isoron.uhabits.BaseViewTest
import org.isoron.uhabits.core.models.* import org.isoron.uhabits.core.models.Entry
import org.isoron.uhabits.utils.* import org.isoron.uhabits.utils.PaletteUtils
import org.junit.* import org.junit.Before
import org.junit.runner.* import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@MediumTest @MediumTest

@ -19,18 +19,20 @@
package org.isoron.uhabits.activities.habits.list.views package org.isoron.uhabits.activities.habits.list.views
import androidx.test.ext.junit.runners.* import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.* import androidx.test.filters.MediumTest
import org.hamcrest.CoreMatchers.* import org.hamcrest.CoreMatchers.equalTo
import org.hamcrest.MatcherAssert.* import org.hamcrest.MatcherAssert.assertThat
import org.isoron.uhabits.* import org.isoron.uhabits.BaseViewTest
import org.isoron.uhabits.core.models.*
import org.isoron.uhabits.core.models.Entry.Companion.NO 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_AUTO
import org.isoron.uhabits.core.models.Entry.Companion.YES_MANUAL import org.isoron.uhabits.core.models.Entry.Companion.YES_MANUAL
import org.isoron.uhabits.utils.* import org.isoron.uhabits.core.models.Timestamp
import org.junit.* import org.isoron.uhabits.utils.PaletteUtils
import org.junit.runner.* import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@MediumTest @MediumTest
@ -44,13 +46,15 @@ class EntryPanelViewTest : BaseViewTest() {
super.setUp() super.setUp()
prefs.isCheckmarkSequenceReversed = false prefs.isCheckmarkSequenceReversed = false
val checkmarks = intArrayOf(YES_MANUAL, val checkmarks = intArrayOf(
YES_MANUAL,
YES_MANUAL, YES_MANUAL,
YES_AUTO, YES_AUTO,
NO, NO,
NO, NO,
NO, NO,
YES_MANUAL) YES_MANUAL
)
view = component.getCheckmarkPanelViewFactory().create().apply { view = component.getCheckmarkPanelViewFactory().create().apply {
values = checkmarks values = checkmarks

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

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

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

@ -18,12 +18,15 @@
*/ */
package org.isoron.uhabits.activities.habits.show.views package org.isoron.uhabits.activities.habits.show.views
import android.view.* import android.view.LayoutInflater
import androidx.test.ext.junit.runners.* import android.view.View
import androidx.test.filters.* import androidx.test.ext.junit.runners.AndroidJUnit4
import org.isoron.uhabits.* import androidx.test.filters.MediumTest
import org.junit.* import org.isoron.uhabits.BaseViewTest
import org.junit.runner.* import org.isoron.uhabits.R
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@MediumTest @MediumTest

@ -18,12 +18,15 @@
*/ */
package org.isoron.uhabits.activities.habits.show.views package org.isoron.uhabits.activities.habits.show.views
import android.view.* import android.view.LayoutInflater
import androidx.test.ext.junit.runners.* import android.view.View
import androidx.test.filters.* import androidx.test.ext.junit.runners.AndroidJUnit4
import org.isoron.uhabits.* import androidx.test.filters.MediumTest
import org.junit.* import org.isoron.uhabits.BaseViewTest
import org.junit.runner.* import org.isoron.uhabits.R
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@MediumTest @MediumTest
@ -39,9 +42,13 @@ class HistoryCardTest : BaseViewTest() {
.from(targetContext) .from(targetContext)
.inflate(R.layout.show_habit, null) .inflate(R.layout.show_habit, null)
.findViewById<View>(R.id.historyCard) as HistoryCard .findViewById<View>(R.id.historyCard) as HistoryCard
view.update(HistoryCardPresenter(habit = habit, view.update(
HistoryCardPresenter(
habit = habit,
firstWeekday = 1, firstWeekday = 1,
isSkipEnabled = false).present()) isSkipEnabled = false
).present()
)
measureView(view, 800f, 600f) measureView(view, 800f, 600f)
} }

@ -18,15 +18,17 @@
*/ */
package org.isoron.uhabits.activities.habits.show.views package org.isoron.uhabits.activities.habits.show.views
import android.view.* import android.view.LayoutInflater
import android.view.View.* import android.view.View.GONE
import androidx.test.ext.junit.runners.* import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.* import androidx.test.filters.MediumTest
import org.hamcrest.Matchers.* import org.hamcrest.Matchers.equalTo
import org.isoron.uhabits.* import org.isoron.uhabits.BaseViewTest
import org.junit.* import org.isoron.uhabits.R
import org.junit.Assert.* import org.junit.Assert.assertThat
import org.junit.runner.* import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@MediumTest @MediumTest

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

@ -18,12 +18,15 @@
*/ */
package org.isoron.uhabits.activities.habits.show.views package org.isoron.uhabits.activities.habits.show.views
import android.view.* import android.view.LayoutInflater
import androidx.test.ext.junit.runners.* import android.view.View
import androidx.test.filters.* import androidx.test.ext.junit.runners.AndroidJUnit4
import org.isoron.uhabits.* import androidx.test.filters.MediumTest
import org.junit.* import org.isoron.uhabits.BaseViewTest
import org.junit.runner.* import org.isoron.uhabits.R
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@MediumTest @MediumTest

@ -18,17 +18,15 @@
*/ */
package org.isoron.uhabits.activities.habits.show.views package org.isoron.uhabits.activities.habits.show.views
import android.view.* import android.view.LayoutInflater
import androidx.test.ext.junit.runners.* import android.view.View
import org.junit.runner.RunWith import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest import androidx.test.filters.MediumTest
import org.isoron.uhabits.BaseViewTest import org.isoron.uhabits.BaseViewTest
import org.isoron.uhabits.activities.habits.show.views.StreakCardView
import org.isoron.uhabits.R import org.isoron.uhabits.R
import org.isoron.uhabits.activities.habits.show.views.StreakCardViewTest import org.junit.Before
import org.isoron.uhabits.core.models.* import org.junit.Test
import org.junit.* import org.junit.runner.RunWith
import java.lang.Exception
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@MediumTest @MediumTest
@ -44,10 +42,12 @@ class StreakCardViewTest : BaseViewTest() {
.from(targetContext) .from(targetContext)
.inflate(R.layout.show_habit, null) .inflate(R.layout.show_habit, null)
.findViewById<View>(R.id.streakCard) as StreakCardView .findViewById<View>(R.id.streakCard) as StreakCardView
view.update(StreakCardViewModel( view.update(
StreakCardViewModel(
bestStreaks = habit.streaks.getBest(10), bestStreaks = habit.streaks.getBest(10),
color = habit.color, color = habit.color,
)) )
)
measureView(view, 800f, 600f) measureView(view, 800f, 600f)
} }

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

@ -19,10 +19,12 @@
package org.isoron.uhabits.database package org.isoron.uhabits.database
import org.isoron.uhabits.* import org.isoron.uhabits.AndroidDirFinder
import org.isoron.uhabits.core.utils.* import org.isoron.uhabits.BaseAndroidTest
import org.junit.* import org.isoron.uhabits.core.utils.DateUtils
import java.io.* import org.junit.Test
import java.io.File
import java.io.FileOutputStream
class AutoBackupTest : BaseAndroidTest() { class AutoBackupTest : BaseAndroidTest() {
@Test @Test
@ -32,7 +34,7 @@ class AutoBackupTest : BaseAndroidTest() {
createTestFiles(basedir, 30) createTestFiles(basedir, 30)
val autoBackup = AutoBackup(targetContext) 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 1..25) assertDoesNotExist("${basedir.path}/test-$k.txt")
for (k in 26..30) assertExists("${basedir.path}/test-$k.txt") for (k in 26..30) assertExists("${basedir.path}/test-$k.txt")

@ -18,18 +18,27 @@
*/ */
package org.isoron.uhabits.intents package org.isoron.uhabits.intents
import android.content.ContentUris.* import android.content.ContentUris.parseId
import androidx.test.ext.junit.runners.* import androidx.test.filters.MediumTest
import androidx.test.filters.* import org.hamcrest.Matchers.equalTo
import org.hamcrest.Matchers.* import org.isoron.uhabits.BaseAndroidTest
import org.isoron.uhabits.* import org.isoron.uhabits.core.reminders.ReminderScheduler.SchedulerResult.OK
import org.isoron.uhabits.core.reminders.ReminderScheduler.SchedulerResult.* import org.isoron.uhabits.receivers.ReminderReceiver
import org.isoron.uhabits.receivers.* import org.isoron.uhabits.receivers.WidgetReceiver
import org.junit.* import org.junit.After
import org.junit.Assert.* import org.junit.Assert.assertThat
import org.junit.runner.* import org.junit.Before
import java.util.* import org.junit.Test
import java.util.Calendar.* 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() { class IntentSchedulerTest : BaseAndroidTest() {

@ -19,15 +19,23 @@
package org.isoron.uhabits.regression package org.isoron.uhabits.regression
import androidx.test.filters.* import androidx.test.filters.LargeTest
import org.isoron.uhabits.* import org.isoron.uhabits.BaseUserInterfaceTest
import org.isoron.uhabits.acceptance.steps.CommonSteps.* import org.isoron.uhabits.acceptance.steps.CommonSteps.Screen.EDIT_HABIT
import org.isoron.uhabits.acceptance.steps.CommonSteps.Screen.* import org.isoron.uhabits.acceptance.steps.CommonSteps.Screen.LIST_HABITS
import org.isoron.uhabits.acceptance.steps.EditHabitSteps.* import org.isoron.uhabits.acceptance.steps.CommonSteps.Screen.SELECT_HABIT_TYPE
import org.isoron.uhabits.acceptance.steps.ListHabitsSteps.* import org.isoron.uhabits.acceptance.steps.CommonSteps.clickText
import org.isoron.uhabits.acceptance.steps.ListHabitsSteps.MenuItem.* import org.isoron.uhabits.acceptance.steps.CommonSteps.launchApp
import org.junit.* 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 @LargeTest
class ListHabitsRegressionTest : BaseUserInterfaceTest() { class ListHabitsRegressionTest : BaseUserInterfaceTest() {

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

@ -19,18 +19,29 @@
package org.isoron.uhabits.sync package org.isoron.uhabits.sync
import androidx.test.filters.* import androidx.test.filters.MediumTest
import com.fasterxml.jackson.databind.* import com.fasterxml.jackson.databind.ObjectMapper
import io.ktor.client.* import io.ktor.client.HttpClient
import io.ktor.client.engine.mock.* import io.ktor.client.engine.mock.MockEngine
import io.ktor.client.features.json.* import io.ktor.client.engine.mock.MockRequestHandleScope
import io.ktor.client.request.* import io.ktor.client.engine.mock.respond
import io.ktor.http.* import io.ktor.client.engine.mock.respondError
import junit.framework.Assert.* import io.ktor.client.engine.mock.respondOk
import kotlinx.coroutines.* import io.ktor.client.features.json.JsonFeature
import org.isoron.uhabits.* import io.ktor.client.request.HttpRequestData
import org.isoron.uhabits.core.sync.* import io.ktor.client.request.HttpResponseData
import org.junit.* 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 @MediumTest
class RemoteSyncServerTest : BaseAndroidTest() { class RemoteSyncServerTest : BaseAndroidTest() {
@ -115,10 +126,12 @@ class RemoteSyncServerTest : BaseAndroidTest() {
return@runBlocking return@runBlocking
} }
private fun server(expectedPath: String, private fun server(
expectedPath: String,
action: MockRequestHandleScope.(HttpRequestData) -> HttpResponseData action: MockRequestHandleScope.(HttpRequestData) -> HttpResponseData
): AbstractSyncServer { ): AbstractSyncServer {
return RemoteSyncServer(httpClient = HttpClient(MockEngine) { return RemoteSyncServer(
httpClient = HttpClient(MockEngine) {
install(JsonFeature) install(JsonFeature)
engine { engine {
addHandler { request -> addHandler { request ->
@ -128,10 +141,14 @@ class RemoteSyncServerTest : BaseAndroidTest() {
} }
} }
} }
}, preferences = prefs) },
preferences = prefs
)
} }
private fun MockRequestHandleScope.respondWithJson(content: Any) = private fun MockRequestHandleScope.respondWithJson(content: Any) =
respond(mapper.writeValueAsBytes(content), respond(
headers = headersOf("Content-Type" to listOf("application/json"))) mapper.writeValueAsBytes(content),
headers = headersOf("Content-Type" to listOf("application/json"))
)
} }

@ -18,14 +18,21 @@
*/ */
package org.isoron.uhabits package org.isoron.uhabits
import android.content.* import android.content.Context
import android.os.* import android.os.Build
import android.view.* import android.os.Environment
import org.isoron.uhabits.inject.* import android.view.WindowManager
import java.io.* import org.isoron.uhabits.inject.AppContext
import java.text.* import java.io.BufferedReader
import java.util.* import java.io.File
import javax.inject.* 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) { 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? var line: String?
while (true) { while (true) {
line = bufferedReader.readLine() line = bufferedReader.readLine()
if (line == null) break; if (line == null) break
log.addLast(line) log.addLast(line)
if (log.size > maxLineCount) log.removeFirst() if (log.size > maxLineCount) log.removeFirst()
} }
@ -106,5 +113,4 @@ open class AndroidBugReporter @Inject constructor(@AppContext private val contex
appendln() appendln()
} }
} }
} }

@ -20,7 +20,7 @@ package org.isoron.uhabits
import android.content.Context import android.content.Context
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import org.isoron.uhabits.inject.* import org.isoron.uhabits.inject.AppContext
import org.isoron.uhabits.utils.FileUtils import org.isoron.uhabits.utils.FileUtils
import java.io.File import java.io.File
import javax.inject.Inject import javax.inject.Inject

@ -18,7 +18,7 @@
*/ */
package org.isoron.uhabits package org.isoron.uhabits
import android.app.* import android.app.Activity
class BaseExceptionHandler(private val activity: Activity) : Thread.UncaughtExceptionHandler { class BaseExceptionHandler(private val activity: Activity) : Thread.UncaughtExceptionHandler {

@ -19,16 +19,19 @@
package org.isoron.uhabits package org.isoron.uhabits
import android.app.* import android.app.Application
import android.content.* import android.content.Context
import org.isoron.uhabits.core.database.* import org.isoron.uhabits.core.database.UnsupportedDatabaseVersionException
import org.isoron.uhabits.core.reminders.* import org.isoron.uhabits.core.reminders.ReminderScheduler
import org.isoron.uhabits.core.ui.* import org.isoron.uhabits.core.ui.NotificationTray
import org.isoron.uhabits.core.utils.* import org.isoron.uhabits.core.utils.DateUtils
import org.isoron.uhabits.inject.* import org.isoron.uhabits.inject.AppContextModule
import org.isoron.uhabits.utils.* import org.isoron.uhabits.inject.DaggerHabitsApplicationComponent
import org.isoron.uhabits.widgets.* import org.isoron.uhabits.inject.HabitsApplicationComponent
import java.io.* 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. * The Android application for Loop Habit Tracker.

@ -19,7 +19,9 @@
package org.isoron.uhabits 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. * An Android BackupAgentHelper customized for this application.

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

@ -19,15 +19,17 @@
package org.isoron.uhabits.activities package org.isoron.uhabits.activities
import android.app.* import android.app.Activity
import android.content.* import android.content.Context
import android.content.res.Configuration.* import android.content.res.Configuration.UI_MODE_NIGHT_MASK
import android.os.Build.VERSION.* import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.core.content.* import android.os.Build.VERSION.SDK_INT
import androidx.core.content.ContextCompat
import org.isoron.uhabits.R import org.isoron.uhabits.R
import org.isoron.uhabits.core.preferences.* import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.core.ui.* import org.isoron.uhabits.core.ui.ThemeSwitcher
import org.isoron.uhabits.inject.* import org.isoron.uhabits.inject.ActivityContext
import org.isoron.uhabits.inject.ActivityScope
@ActivityScope @ActivityScope
class AndroidThemeSwitcher class AndroidThemeSwitcher
@ -37,12 +39,12 @@ constructor(
) : ThemeSwitcher(preferences) { ) : ThemeSwitcher(preferences) {
override fun getSystemTheme(): Int { override fun getSystemTheme(): Int {
if (SDK_INT < 29) return THEME_LIGHT; if (SDK_INT < 29) return THEME_LIGHT
val uiMode = context.resources.configuration.uiMode val uiMode = context.resources.configuration.uiMode
return if ((uiMode and UI_MODE_NIGHT_MASK) == UI_MODE_NIGHT_YES) { return if ((uiMode and UI_MODE_NIGHT_MASK) == UI_MODE_NIGHT_YES) {
THEME_DARK; THEME_DARK
} else { } else {
THEME_LIGHT; THEME_LIGHT
} }
} }

@ -18,11 +18,11 @@
*/ */
package org.isoron.uhabits.activities package org.isoron.uhabits.activities
import org.isoron.uhabits.* import org.isoron.uhabits.AndroidDirFinder
import org.isoron.uhabits.core.ui.screens.habits.list.* import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior
import org.isoron.uhabits.core.ui.screens.habits.show.* import org.isoron.uhabits.core.ui.screens.habits.show.ShowHabitMenuBehavior
import java.io.* import java.io.File
import javax.inject.* import javax.inject.Inject
class HabitsDirFinder @Inject class HabitsDirFinder @Inject
constructor( constructor(

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

@ -18,10 +18,11 @@
*/ */
package org.isoron.uhabits.activities.about package org.isoron.uhabits.activities.about
import org.isoron.uhabits.* import org.isoron.uhabits.R
import org.isoron.uhabits.core.preferences.* import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.intents.* import org.isoron.uhabits.intents.IntentFactory
import org.isoron.uhabits.utils.* import org.isoron.uhabits.utils.showMessage
import org.isoron.uhabits.utils.startActivitySafely
class AboutScreen( class AboutScreen(
private val activity: AboutActivity, private val activity: AboutActivity,

@ -18,14 +18,15 @@
*/ */
package org.isoron.uhabits.activities.about package org.isoron.uhabits.activities.about
import android.annotation.* import android.annotation.SuppressLint
import android.content.* import android.content.Context
import android.view.* import android.view.LayoutInflater
import android.widget.* import android.widget.FrameLayout
import org.isoron.uhabits.* import org.isoron.uhabits.BuildConfig
import org.isoron.uhabits.core.models.* import org.isoron.uhabits.R
import org.isoron.uhabits.databinding.* import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.utils.* import org.isoron.uhabits.databinding.AboutBinding
import org.isoron.uhabits.utils.setupToolbar
@SuppressLint("ViewConstructor") @SuppressLint("ViewConstructor")
class AboutView( class AboutView(

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

@ -19,26 +19,31 @@
package org.isoron.uhabits.activities.common.dialogs package org.isoron.uhabits.activities.common.dialogs
import android.content.* import android.content.Context
import androidx.appcompat.app.* import android.content.DialogInterface
import android.text.* import android.text.InputFilter
import android.view.* import android.view.LayoutInflater
import android.view.WindowManager.LayoutParams.* import android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE
import android.view.inputmethod.* import android.view.inputmethod.EditorInfo
import android.widget.* 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.R
import org.isoron.uhabits.core.ui.screens.habits.list.* import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior
import org.isoron.uhabits.inject.* import org.isoron.uhabits.inject.ActivityContext
import org.isoron.uhabits.utils.* import org.isoron.uhabits.utils.InterfaceUtils
import javax.inject.* import javax.inject.Inject
class NumberPickerFactory class NumberPickerFactory
@Inject constructor( @Inject constructor(
@ActivityContext private val context: Context @ActivityContext private val context: Context
) { ) {
fun create(value: Double, fun create(
value: Double,
unit: String, unit: String,
callback: ListHabitsBehavior.NumberPickerCallback): AlertDialog { callback: ListHabitsBehavior.NumberPickerCallback
): AlertDialog {
val inflater = LayoutInflater.from(context) val inflater = LayoutInflater.from(context)
val view = inflater.inflate(R.layout.number_picker_dialog, null) val view = inflater.inflate(R.layout.number_picker_dialog, null)
@ -70,7 +75,7 @@ class NumberPickerFactory
val v = picker.value + 0.05 * picker2.value val v = picker.value + 0.05 * picker2.value
callback.onNumberPicked(v) callback.onNumberPicked(v)
} }
.setOnDismissListener{ .setOnDismissListener {
callback.onNumberPickerDismissed() callback.onNumberPickerDismissed()
} }
.create() .create()
@ -80,11 +85,14 @@ class NumberPickerFactory
dialog.window?.setSoftInputMode(SOFT_INPUT_STATE_ALWAYS_VISIBLE) dialog.window?.setSoftInputMode(SOFT_INPUT_STATE_ALWAYS_VISIBLE)
} }
InterfaceUtils.setupEditorAction(picker, TextView.OnEditorActionListener { _, actionId, _ -> InterfaceUtils.setupEditorAction(
picker,
TextView.OnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_DONE) if (actionId == EditorInfo.IME_ACTION_DONE)
dialog.getButton(DialogInterface.BUTTON_POSITIVE).performClick() dialog.getButton(DialogInterface.BUTTON_POSITIVE).performClick()
false false
}) }
)
return dialog return dialog
} }

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

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

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

@ -19,22 +19,26 @@
package org.isoron.uhabits.activities.habits.list package org.isoron.uhabits.activities.habits.list
import android.content.* import android.content.Intent
import android.os.* import android.os.Bundle
import android.view.* import android.view.Menu
import androidx.appcompat.app.* import android.view.MenuItem
import kotlinx.coroutines.* import androidx.appcompat.app.AppCompatActivity
import org.isoron.uhabits.* import kotlinx.coroutines.CoroutineScope
import org.isoron.uhabits.activities.* import kotlinx.coroutines.Dispatchers
import org.isoron.uhabits.activities.habits.list.views.* import kotlinx.coroutines.launch
import org.isoron.uhabits.core.preferences.* import org.isoron.uhabits.BaseExceptionHandler
import org.isoron.uhabits.core.sync.* import org.isoron.uhabits.HabitsApplication
import org.isoron.uhabits.core.tasks.* import org.isoron.uhabits.activities.habits.list.views.HabitCardListAdapter
import org.isoron.uhabits.core.ui.ThemeSwitcher.* import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.core.utils.* import org.isoron.uhabits.core.sync.SyncManager
import org.isoron.uhabits.database.* import org.isoron.uhabits.core.tasks.TaskRunner
import org.isoron.uhabits.inject.* import org.isoron.uhabits.core.ui.ThemeSwitcher.THEME_DARK
import org.isoron.uhabits.utils.* 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() { class ListHabitsActivity : AppCompatActivity() {

@ -19,15 +19,18 @@
package org.isoron.uhabits.activities.habits.list package org.isoron.uhabits.activities.habits.list
import android.content.* import android.content.Context
import android.view.* import android.view.Menu
import androidx.appcompat.app.* import android.view.MenuInflater
import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity
import org.isoron.uhabits.R import org.isoron.uhabits.R
import org.isoron.uhabits.core.preferences.* import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.core.ui.* import org.isoron.uhabits.core.ui.ThemeSwitcher
import org.isoron.uhabits.core.ui.screens.habits.list.* import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsMenuBehavior
import org.isoron.uhabits.inject.* import org.isoron.uhabits.inject.ActivityContext
import javax.inject.* import org.isoron.uhabits.inject.ActivityScope
import javax.inject.Inject
@ActivityScope @ActivityScope
class ListHabitsMenu @Inject constructor( class ListHabitsMenu @Inject constructor(

@ -19,14 +19,17 @@
package org.isoron.uhabits.activities.habits.list package org.isoron.uhabits.activities.habits.list
import android.content.* import android.content.Context
import dagger.* import dagger.Binds
import org.isoron.uhabits.* import dagger.Module
import org.isoron.uhabits.activities.* import org.isoron.uhabits.AndroidBugReporter
import org.isoron.uhabits.activities.habits.list.views.* import org.isoron.uhabits.activities.HabitsDirFinder
import org.isoron.uhabits.core.ui.screens.habits.list.* import org.isoron.uhabits.activities.habits.list.views.HabitCardListAdapter
import org.isoron.uhabits.inject.* import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior
import javax.inject.* 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 class BugReporterProxy
@Inject constructor( @Inject constructor(

@ -19,21 +19,38 @@
package org.isoron.uhabits.activities.habits.list package org.isoron.uhabits.activities.habits.list
import android.content.* import android.content.Context
import android.view.ViewGroup.LayoutParams.* import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.widget.* import android.widget.FrameLayout
import android.widget.RelativeLayout
import org.isoron.uhabits.R import org.isoron.uhabits.R
import org.isoron.uhabits.activities.common.views.* import org.isoron.uhabits.activities.common.views.ScrollableChart
import org.isoron.uhabits.activities.habits.list.views.* import org.isoron.uhabits.activities.common.views.TaskProgressBar
import org.isoron.uhabits.core.models.* import org.isoron.uhabits.activities.habits.list.views.EmptyListView
import org.isoron.uhabits.core.preferences.* import org.isoron.uhabits.activities.habits.list.views.HabitCardListAdapter
import org.isoron.uhabits.core.tasks.* import org.isoron.uhabits.activities.habits.list.views.HabitCardListView
import org.isoron.uhabits.core.ui.screens.habits.list.* import org.isoron.uhabits.activities.habits.list.views.HabitCardListViewFactory
import org.isoron.uhabits.core.utils.* import org.isoron.uhabits.activities.habits.list.views.HeaderView
import org.isoron.uhabits.inject.* import org.isoron.uhabits.activities.habits.list.views.HintView
import org.isoron.uhabits.utils.* import org.isoron.uhabits.core.models.ModelObservable
import java.lang.Math.* import org.isoron.uhabits.core.models.PaletteColor
import javax.inject.* 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 const val MAX_CHECKMARK_COUNT = 60
@ -86,11 +103,13 @@ class ListHabitsRootView @Inject constructor(
} }
private fun setupControllers() { private fun setupControllers() {
header.setScrollController(object : ScrollableChart.ScrollController { header.setScrollController(
object : ScrollableChart.ScrollController {
override fun onDataOffsetChanged(newDataOffset: Int) { override fun onDataOffsetChanged(newDataOffset: Int) {
listView.dataOffset = newDataOffset listView.dataOffset = newDataOffset
} }
}) }
)
} }
override fun onAttachedToWindow() { override fun onAttachedToWindow() {

@ -19,29 +19,58 @@
package org.isoron.uhabits.activities.habits.list package org.isoron.uhabits.activities.habits.list
import android.app.* import android.app.Activity
import android.content.* import android.content.Context
import android.util.* import android.content.Intent
import androidx.annotation.* import android.util.Log
import androidx.appcompat.app.* import androidx.appcompat.app.AppCompatActivity
import dagger.* import dagger.Lazy
import org.isoron.uhabits.R import org.isoron.uhabits.R
import org.isoron.uhabits.activities.common.dialogs.* import org.isoron.uhabits.activities.common.dialogs.ColorPickerDialogFactory
import org.isoron.uhabits.activities.habits.edit.* import org.isoron.uhabits.activities.common.dialogs.ConfirmDeleteDialogFactory
import org.isoron.uhabits.activities.habits.list.views.* import org.isoron.uhabits.activities.common.dialogs.ConfirmSyncKeyDialogFactory
import org.isoron.uhabits.core.commands.* import org.isoron.uhabits.activities.common.dialogs.NumberPickerFactory
import org.isoron.uhabits.core.models.* import org.isoron.uhabits.activities.habits.edit.HabitTypeDialog
import org.isoron.uhabits.core.tasks.* import org.isoron.uhabits.activities.habits.list.views.HabitCardListAdapter
import org.isoron.uhabits.core.ui.* import org.isoron.uhabits.core.commands.ArchiveHabitsCommand
import org.isoron.uhabits.core.ui.callbacks.* import org.isoron.uhabits.core.commands.ChangeHabitColorCommand
import org.isoron.uhabits.core.ui.screens.habits.list.* import org.isoron.uhabits.core.commands.Command
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior.Message.* import org.isoron.uhabits.core.commands.CommandRunner
import org.isoron.uhabits.inject.* import org.isoron.uhabits.core.commands.CreateHabitCommand
import org.isoron.uhabits.intents.* import org.isoron.uhabits.core.commands.DeleteHabitsCommand
import org.isoron.uhabits.tasks.* import org.isoron.uhabits.core.commands.EditHabitCommand
import org.isoron.uhabits.utils.* import org.isoron.uhabits.core.commands.UnarchiveHabitsCommand
import java.io.* import org.isoron.uhabits.core.models.Habit
import javax.inject.* 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_IMPORT_DATA = 101
const val RESULT_EXPORT_CSV = 102 const val RESULT_EXPORT_CSV = 102
@ -76,7 +105,7 @@ class ListHabitsScreen
fun onAttached() { fun onAttached() {
commandRunner.addListener(this) 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 uri = activity.intent.data!!.toString()
val parts = uri.replace(Regex("^.*sync/"), "").split("#") val parts = uri.replace(Regex("^.*sync/"), "").split("#")
val syncKey = parts[0] val syncKey = parts[0]
@ -172,7 +201,9 @@ class ListHabitsScreen
} }
override fun showMessage(m: ListHabitsBehavior.Message) { override fun showMessage(m: ListHabitsBehavior.Message) {
activity.showMessage(activity.resources.getString(when (m) { activity.showMessage(
activity.resources.getString(
when (m) {
COULD_NOT_EXPORT -> R.string.could_not_export COULD_NOT_EXPORT -> R.string.could_not_export
IMPORT_SUCCESSFUL -> R.string.habits_imported IMPORT_SUCCESSFUL -> R.string.habits_imported
IMPORT_FAILED -> R.string.could_not_import IMPORT_FAILED -> R.string.could_not_import
@ -181,7 +212,9 @@ class ListHabitsScreen
FILE_NOT_RECOGNIZED -> R.string.file_not_recognized FILE_NOT_RECOGNIZED -> R.string.file_not_recognized
SYNC_ENABLED -> R.string.sync_enabled SYNC_ENABLED -> R.string.sync_enabled
SYNC_KEY_ALREADY_INSTALLED -> R.string.sync_key_already_installed SYNC_KEY_ALREADY_INSTALLED -> R.string.sync_key_already_installed
})) }
)
)
} }
override fun showSendBugReportToDeveloperScreen(log: String) { override fun showSendBugReportToDeveloperScreen(log: String) {
@ -199,16 +232,20 @@ class ListHabitsScreen
activity.startActivityForResult(intent, REQUEST_SETTINGS) activity.startActivityForResult(intent, REQUEST_SETTINGS)
} }
override fun showColorPicker(defaultColor: PaletteColor, override fun showColorPicker(
callback: OnColorPickedCallback) { defaultColor: PaletteColor,
callback: OnColorPickedCallback
) {
val picker = colorPickerFactory.create(defaultColor) val picker = colorPickerFactory.create(defaultColor)
picker.setListener(callback) picker.setListener(callback)
picker.show(activity.supportFragmentManager, "picker") picker.show(activity.supportFragmentManager, "picker")
} }
override fun showNumberPicker(value: Double, override fun showNumberPicker(
value: Double,
unit: String, unit: String,
callback: ListHabitsBehavior.NumberPickerCallback) { callback: ListHabitsBehavior.NumberPickerCallback
) {
numberPickerFactory.create(value, unit, callback).show() numberPickerFactory.create(value, unit, callback).show()
} }
@ -219,33 +256,42 @@ class ListHabitsScreen
private fun getExecuteString(command: Command): String? { private fun getExecuteString(command: Command): String? {
when (command) { when (command) {
is ArchiveHabitsCommand -> { is ArchiveHabitsCommand -> {
return activity.resources.getQuantityString(R.plurals.toast_habits_archived, return activity.resources.getQuantityString(
command.selected.size) R.plurals.toast_habits_archived,
command.selected.size
)
} }
is ChangeHabitColorCommand -> { is ChangeHabitColorCommand -> {
return activity.resources.getQuantityString(R.plurals.toast_habits_changed, return activity.resources.getQuantityString(
command.selected.size) R.plurals.toast_habits_changed,
command.selected.size
)
} }
is CreateHabitCommand -> { is CreateHabitCommand -> {
return activity.resources.getString(R.string.toast_habit_created) return activity.resources.getString(R.string.toast_habit_created)
} }
is DeleteHabitsCommand -> { is DeleteHabitsCommand -> {
return activity.resources.getQuantityString(R.plurals.toast_habits_deleted, return activity.resources.getQuantityString(
command.selected.size) R.plurals.toast_habits_deleted,
command.selected.size
)
} }
is EditHabitCommand -> { is EditHabitCommand -> {
return activity.resources.getQuantityString(R.plurals.toast_habits_changed, 1) return activity.resources.getQuantityString(R.plurals.toast_habits_changed, 1)
} }
is UnarchiveHabitsCommand -> { is UnarchiveHabitsCommand -> {
return activity.resources.getQuantityString(R.plurals.toast_habits_unarchived, return activity.resources.getQuantityString(
command.selected.size) R.plurals.toast_habits_unarchived,
command.selected.size
)
} }
else -> return null else -> return null
} }
} }
private fun onImportData(file: File, onFinished: () -> Unit) { private fun onImportData(file: File, onFinished: () -> Unit) {
taskRunner.execute(importTaskFactory.create(file) { result -> taskRunner.execute(
importTaskFactory.create(file) { result ->
if (result == ImportDataTask.SUCCESS) { if (result == ImportDataTask.SUCCESS) {
adapter.refresh() adapter.refresh()
activity.showMessage(activity.resources.getString(R.string.habits_imported)) activity.showMessage(activity.resources.getString(R.string.habits_imported))
@ -255,13 +301,16 @@ class ListHabitsScreen
activity.showMessage(activity.resources.getString(R.string.could_not_import)) activity.showMessage(activity.resources.getString(R.string.could_not_import))
} }
onFinished() onFinished()
}) }
)
} }
private fun onExportDB() { private fun onExportDB() {
taskRunner.execute(exportDBFactory.create { filename -> taskRunner.execute(
exportDBFactory.create { filename ->
if (filename != null) activity.showSendFileScreen(filename) if (filename != null) activity.showSendFileScreen(filename)
else activity.showMessage(activity.resources.getString(R.string.could_not_export)) else activity.showMessage(activity.resources.getString(R.string.could_not_export))
}) }
)
} }
} }

@ -19,20 +19,23 @@
package org.isoron.uhabits.activities.habits.list package org.isoron.uhabits.activities.habits.list
import android.content.* import android.content.Context
import android.view.* import android.view.Menu
import androidx.appcompat.app.* import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import dagger.* import dagger.Lazy
import org.isoron.uhabits.R import org.isoron.uhabits.R
import org.isoron.uhabits.activities.habits.list.views.* import org.isoron.uhabits.activities.habits.list.views.HabitCardListAdapter
import org.isoron.uhabits.core.commands.* import org.isoron.uhabits.activities.habits.list.views.HabitCardListController
import org.isoron.uhabits.core.preferences.* import org.isoron.uhabits.core.commands.CommandRunner
import org.isoron.uhabits.core.ui.* import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.core.ui.screens.habits.list.* import org.isoron.uhabits.core.ui.NotificationTray
import org.isoron.uhabits.core.utils.* import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsSelectionMenuBehavior
import org.isoron.uhabits.inject.* import org.isoron.uhabits.core.utils.DateUtils
import javax.inject.* import org.isoron.uhabits.inject.ActivityContext
import org.isoron.uhabits.inject.ActivityScope
import javax.inject.Inject
@ActivityScope @ActivityScope
class ListHabitsSelectionMenu @Inject constructor( class ListHabitsSelectionMenu @Inject constructor(

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

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

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

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

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

@ -19,17 +19,26 @@
package org.isoron.uhabits.activities.habits.list.views package org.isoron.uhabits.activities.habits.list.views
import android.content.* import android.content.Context
import android.os.* import android.os.Bundle
import androidx.recyclerview.widget.* import android.os.Parcelable
import androidx.recyclerview.widget.ItemTouchHelper.* import android.view.GestureDetector
import android.view.* import android.view.MotionEvent
import com.google.auto.factory.* import android.view.View
import dagger.* 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.R
import org.isoron.uhabits.activities.common.views.* import org.isoron.uhabits.activities.common.views.BundleSavedState
import org.isoron.uhabits.core.models.* import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.inject.* import org.isoron.uhabits.inject.ActivityContext
@AutoFactory @AutoFactory
class HabitCardListView( class HabitCardListView(
@ -65,11 +74,13 @@ class HabitCardListView(
return cardViewFactory.create() return cardViewFactory.create()
} }
fun bindCardView(holder: HabitCardViewHolder, fun bindCardView(
holder: HabitCardViewHolder,
habit: Habit, habit: Habit,
score: Double, score: Double,
checkmarks: IntArray, checkmarks: IntArray,
selected: Boolean): View { selected: Boolean
): View {
val cardView = holder.itemView as HabitCardView val cardView = holder.itemView as HabitCardView
cardView.habit = habit cardView.habit = habit
cardView.isSelected = selected cardView.isSelected = selected
@ -149,20 +160,26 @@ class HabitCardListView(
} }
inner class TouchHelperCallback : ItemTouchHelper.Callback() { inner class TouchHelperCallback : ItemTouchHelper.Callback() {
override fun getMovementFlags(recyclerView: RecyclerView, override fun getMovementFlags(
viewHolder: RecyclerView.ViewHolder): Int { recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
): Int {
return makeMovementFlags(UP or DOWN, START or END) return makeMovementFlags(UP or DOWN, START or END)
} }
override fun onMove(recyclerView: RecyclerView, override fun onMove(
recyclerView: RecyclerView,
from: RecyclerView.ViewHolder, from: RecyclerView.ViewHolder,
to: RecyclerView.ViewHolder): Boolean { to: RecyclerView.ViewHolder
): Boolean {
controller.get().drop(from.adapterPosition, to.adapterPosition) controller.get().drop(from.adapterPosition, to.adapterPosition)
return true return true
} }
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, override fun onSwiped(
direction: Int) { viewHolder: RecyclerView.ViewHolder,
direction: Int
) {
} }
override fun isItemViewSwipeEnabled() = false override fun isItemViewSwipeEnabled() = false

@ -19,22 +19,34 @@
package org.isoron.uhabits.activities.habits.list.views package org.isoron.uhabits.activities.habits.list.views
import android.content.* import android.content.Context
import android.os.* import android.os.Build.VERSION.SDK_INT
import android.os.Build.VERSION.* import android.os.Build.VERSION_CODES.LOLLIPOP
import android.os.Build.VERSION_CODES.* import android.os.Build.VERSION_CODES.M
import android.text.* import android.os.Handler
import android.view.* import android.os.Looper
import android.view.ViewGroup.LayoutParams.* import android.text.Layout
import android.widget.* import android.text.TextUtils
import com.google.auto.factory.* 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.R
import org.isoron.uhabits.activities.common.views.* import org.isoron.uhabits.activities.common.views.RingView
import org.isoron.uhabits.core.models.* import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.ui.screens.habits.list.* import org.isoron.uhabits.core.models.ModelObservable
import org.isoron.uhabits.core.utils.* import org.isoron.uhabits.core.models.Timestamp
import org.isoron.uhabits.inject.* import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior
import org.isoron.uhabits.utils.* 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 @AutoFactory
class HabitCardView( class HabitCardView(
@ -228,8 +240,10 @@ class HabitCardView(
private fun triggerRipple(x: Float, y: Float) { private fun triggerRipple(x: Float, y: Float) {
val background = innerFrame.background val background = innerFrame.background
if (SDK_INT >= LOLLIPOP) background.setHotspot(x, y) if (SDK_INT >= LOLLIPOP) background.setHotspot(x, y)
background.state = intArrayOf(android.R.attr.state_pressed, background.state = intArrayOf(
android.R.attr.state_enabled) android.R.attr.state_pressed,
android.R.attr.state_enabled
)
Handler().postDelayed({ background.state = intArrayOf() }, 25) Handler().postDelayed({ background.state = intArrayOf() }, 25)
} }

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

@ -19,19 +19,28 @@
package org.isoron.uhabits.activities.habits.list.views package org.isoron.uhabits.activities.habits.list.views
import android.content.* import android.content.Context
import android.graphics.* import android.graphics.Canvas
import android.os.Build.VERSION.* import android.graphics.Color
import android.os.Build.VERSION_CODES.* import android.graphics.Paint
import android.text.* import android.graphics.RectF
import android.view.View.MeasureSpec.* import android.graphics.Typeface
import org.isoron.uhabits.* import android.os.Build.VERSION.SDK_INT
import org.isoron.uhabits.activities.common.views.* import android.os.Build.VERSION_CODES.LOLLIPOP
import org.isoron.uhabits.core.preferences.* import android.text.TextPaint
import org.isoron.uhabits.core.utils.* import android.view.View.MeasureSpec.EXACTLY
import org.isoron.uhabits.core.utils.DateUtils.* import org.isoron.uhabits.R
import org.isoron.uhabits.utils.* import org.isoron.uhabits.activities.common.views.ScrollableChart
import java.util.* 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( class HeaderView(
context: Context, context: Context,
@ -121,8 +130,12 @@ class HeaderView(
if (isReversed) rect.offset(-(index + 1) * width, 0f) if (isReversed) rect.offset(-(index + 1) * width, 0f)
else rect.offset((index - buttonCount) * width, 0f) else rect.offset((index - buttonCount) * width, 0f)
if (isRTL()) rect.set(canvas.width - rect.right, rect.top, if (isRTL()) rect.set(
canvas.width - rect.left, rect.bottom) canvas.width - rect.right,
rect.top,
canvas.width - rect.left,
rect.bottom
)
val y1 = rect.centerY() - 0.25 * em val y1 = rect.centerY() - 0.25 * em
val y2 = rect.centerY() + 1.25 * em val y2 = rect.centerY() + 1.25 * em

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

@ -19,18 +19,25 @@
package org.isoron.uhabits.activities.habits.list.views package org.isoron.uhabits.activities.habits.list.views
import android.content.* import android.content.Context
import android.graphics.* import android.graphics.Canvas
import android.text.* import android.graphics.Paint
import android.view.* import android.graphics.RectF
import android.view.View.* import android.graphics.Typeface
import com.google.auto.factory.* import android.text.TextPaint
import org.isoron.uhabits.* import android.view.View
import org.isoron.uhabits.core.preferences.* import android.view.View.OnClickListener
import org.isoron.uhabits.inject.* import android.view.View.OnLongClickListener
import org.isoron.uhabits.utils.* 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 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 BOLD_TYPEFACE = Typeface.create("sans-serif-condensed", Typeface.BOLD)
private val NORMAL_TYPEFACE = Typeface.create("sans-serif-condensed", Typeface.NORMAL) private val NORMAL_TYPEFACE = Typeface.create("sans-serif-condensed", Typeface.NORMAL)
@ -148,10 +155,10 @@ class NumberButtonView(
val label: String val label: String
val typeface: Typeface val typeface: Typeface
if(value >= 0) { if (value >= 0) {
label = value.toShortString() label = value.toShortString()
typeface = BOLD_TYPEFACE typeface = BOLD_TYPEFACE
} else if(preferences.areQuestionMarksEnabled()) { } else if (preferences.areQuestionMarksEnabled()) {
label = resources.getString(R.string.fa_question) label = resources.getString(R.string.fa_question)
typeface = getFontAwesome() typeface = getFontAwesome()
} else { } else {

@ -19,12 +19,13 @@
package org.isoron.uhabits.activities.habits.list.views package org.isoron.uhabits.activities.habits.list.views
import android.content.* import android.content.Context
import com.google.auto.factory.* import com.google.auto.factory.AutoFactory
import org.isoron.uhabits.core.models.* import com.google.auto.factory.Provided
import org.isoron.uhabits.core.preferences.* import org.isoron.uhabits.core.models.Timestamp
import org.isoron.uhabits.core.utils.* import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.inject.* import org.isoron.uhabits.core.utils.DateUtils
import org.isoron.uhabits.inject.ActivityContext
@AutoFactory @AutoFactory
class NumberPanelView( class NumberPanelView(

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

@ -18,17 +18,25 @@
*/ */
package org.isoron.uhabits.activities.habits.show package org.isoron.uhabits.activities.habits.show
import android.content.* import android.content.ContentUris
import android.os.* import android.os.Bundle
import android.view.* import android.view.Menu
import androidx.appcompat.app.* import android.view.MenuItem
import kotlinx.coroutines.* import androidx.appcompat.app.AppCompatActivity
import org.isoron.uhabits.* import kotlinx.coroutines.CoroutineScope
import org.isoron.uhabits.activities.* import kotlinx.coroutines.Dispatchers
import org.isoron.uhabits.activities.common.dialogs.* import kotlinx.coroutines.launch
import org.isoron.uhabits.core.commands.* import org.isoron.uhabits.AndroidDirFinder
import org.isoron.uhabits.core.ui.screens.habits.show.* import org.isoron.uhabits.HabitsApplication
import org.isoron.uhabits.intents.* 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 { class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
@ -125,4 +133,3 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
} }
} }
} }

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

@ -19,16 +19,20 @@
package org.isoron.uhabits.activities.habits.show package org.isoron.uhabits.activities.habits.show
import org.isoron.uhabits.* import org.isoron.uhabits.R
import org.isoron.uhabits.activities.common.dialogs.* import org.isoron.uhabits.activities.common.dialogs.ConfirmDeleteDialogFactory
import org.isoron.uhabits.core.models.* import org.isoron.uhabits.activities.common.dialogs.HistoryEditorDialog
import org.isoron.uhabits.core.ui.callbacks.* import org.isoron.uhabits.activities.common.dialogs.NumberPickerFactory
import org.isoron.uhabits.core.ui.screens.habits.list.* import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.ui.screens.habits.show.* import org.isoron.uhabits.core.ui.callbacks.OnConfirmedCallback
import org.isoron.uhabits.intents.* import org.isoron.uhabits.core.ui.callbacks.OnToggleCheckmarkListener
import org.isoron.uhabits.utils.* import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior
import org.isoron.uhabits.widgets.* 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( class ShowHabitScreen(
val activity: ShowHabitActivity, val activity: ShowHabitActivity,
@ -40,7 +44,7 @@ class ShowHabitScreen(
) : ShowHabitBehavior.Screen, ShowHabitMenuBehavior.Screen { ) : ShowHabitBehavior.Screen, ShowHabitMenuBehavior.Screen {
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(); numberPickerFactory.create(value, unit, callback).show()
} }
override fun updateWidgets() { override fun updateWidgets() {

@ -19,14 +19,32 @@
package org.isoron.uhabits.activities.habits.show package org.isoron.uhabits.activities.habits.show
import android.content.* import android.content.Context
import android.view.* import android.view.LayoutInflater
import android.widget.* import android.widget.FrameLayout
import org.isoron.uhabits.activities.habits.show.views.* import org.isoron.uhabits.activities.habits.show.views.BarCardPresenter
import org.isoron.uhabits.core.models.* import org.isoron.uhabits.activities.habits.show.views.BarCardViewModel
import org.isoron.uhabits.core.preferences.* import org.isoron.uhabits.activities.habits.show.views.FrequencyCardPresenter
import org.isoron.uhabits.databinding.* import org.isoron.uhabits.activities.habits.show.views.FrequencyCardViewModel
import org.isoron.uhabits.utils.* 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( data class ShowHabitViewModel(
val title: String = "", val title: String = "",

@ -18,15 +18,19 @@
*/ */
package org.isoron.uhabits.activities.habits.show.views package org.isoron.uhabits.activities.habits.show.views
import android.content.* import android.content.Context
import android.util.* import android.util.AttributeSet
import android.view.* import android.view.LayoutInflater
import android.widget.* 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.activities.habits.show.views.ScoreCardPresenter.Companion.getTruncateField
import org.isoron.uhabits.core.models.* import org.isoron.uhabits.core.models.Entry
import org.isoron.uhabits.core.utils.* import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.databinding.* import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.utils.* import org.isoron.uhabits.core.utils.DateUtils
import org.isoron.uhabits.databinding.ShowHabitBarBinding
import org.isoron.uhabits.utils.toThemedAndroidColor
data class BarCardViewModel( data class BarCardViewModel(
val entries: List<Entry>, val entries: List<Entry>,
@ -88,7 +92,7 @@ class BarCardPresenter(
numericalSpinnerPosition: Int, numericalSpinnerPosition: Int,
boolSpinnerPosition: Int, boolSpinnerPosition: Int,
): BarCardViewModel { ): BarCardViewModel {
val bucketSize = if(habit.isNumerical) { val bucketSize = if (habit.isNumerical) {
numericalBucketSizes[numericalSpinnerPosition] numericalBucketSizes[numericalSpinnerPosition]
} else { } else {
boolBucketSizes[boolSpinnerPosition] boolBucketSizes[boolSpinnerPosition]

@ -18,14 +18,16 @@
*/ */
package org.isoron.uhabits.activities.habits.show.views package org.isoron.uhabits.activities.habits.show.views
import android.content.* import android.content.Context
import android.util.* import android.util.AttributeSet
import android.view.* import android.view.LayoutInflater
import android.widget.* import android.widget.LinearLayout
import org.isoron.uhabits.core.models.* import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.databinding.* import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.utils.* import org.isoron.uhabits.core.models.Timestamp
import java.util.* import org.isoron.uhabits.databinding.ShowHabitFrequencyBinding
import org.isoron.uhabits.utils.toThemedAndroidColor
import java.util.HashMap
data class FrequencyCardViewModel( data class FrequencyCardViewModel(
val frequency: HashMap<Timestamp, Array<Int>>, val frequency: HashMap<Timestamp, Array<Int>>,

@ -18,13 +18,14 @@
*/ */
package org.isoron.uhabits.activities.habits.show.views package org.isoron.uhabits.activities.habits.show.views
import android.content.* import android.content.Context
import android.util.* import android.util.AttributeSet
import android.view.* import android.view.LayoutInflater
import android.widget.* import android.widget.LinearLayout
import org.isoron.uhabits.core.models.* import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.databinding.* import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.utils.* import org.isoron.uhabits.databinding.ShowHabitHistoryBinding
import org.isoron.uhabits.utils.toThemedAndroidColor
data class HistoryCardViewModel( data class HistoryCardViewModel(
val entries: IntArray, val entries: IntArray,
@ -54,7 +55,6 @@ class HistoryCard(context: Context, attrs: AttributeSet) : LinearLayout(context,
if (data.isNumerical) { if (data.isNumerical) {
binding.historyChart.setNumerical(true) binding.historyChart.setNumerical(true)
} }
} }
} }

@ -19,12 +19,12 @@
package org.isoron.uhabits.activities.habits.show.views package org.isoron.uhabits.activities.habits.show.views
import android.content.* import android.content.Context
import android.util.* import android.util.AttributeSet
import android.view.* import android.view.LayoutInflater
import android.widget.* import android.widget.LinearLayout
import org.isoron.uhabits.core.models.* import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.databinding.* import org.isoron.uhabits.databinding.ShowHabitNotesBinding
data class NotesCardViewModel(val description: String) data class NotesCardViewModel(val description: String)

@ -18,17 +18,20 @@
*/ */
package org.isoron.uhabits.activities.habits.show.views package org.isoron.uhabits.activities.habits.show.views
import android.content.* import android.content.Context
import android.util.* import android.util.AttributeSet
import android.view.* import android.view.LayoutInflater
import android.widget.* import android.widget.LinearLayout
import kotlinx.coroutines.* import kotlinx.coroutines.Dispatchers
import org.isoron.uhabits.* import kotlinx.coroutines.invoke
import org.isoron.uhabits.core.models.* import org.isoron.uhabits.R
import org.isoron.uhabits.core.models.Entry.Companion.YES_MANUAL import org.isoron.uhabits.core.models.Entry.Companion.YES_MANUAL
import org.isoron.uhabits.core.utils.* import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.databinding.* import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.utils.* 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( data class OverviewCardViewModel(
val color: PaletteColor, val color: PaletteColor,
@ -43,8 +46,11 @@ class OverviewCardView(context: Context, attrs: AttributeSet) : LinearLayout(con
private val binding = ShowHabitOverviewBinding.inflate(LayoutInflater.from(context), this) private val binding = ShowHabitOverviewBinding.inflate(LayoutInflater.from(context), this)
private fun formatPercentageDiff(percentageDiff: Float): String { private fun formatPercentageDiff(percentageDiff: Float): String {
return String.format("%s%.0f%%", if (percentageDiff >= 0) "+" else "\u2212", return String.format(
Math.abs(percentageDiff) * 100) "%s%.0f%%",
if (percentageDiff >= 0) "+" else "\u2212",
Math.abs(percentageDiff) * 100
)
} }
fun update(data: OverviewCardViewModel) { fun update(data: OverviewCardViewModel) {

@ -18,15 +18,23 @@
*/ */
package org.isoron.uhabits.activities.habits.show.views package org.isoron.uhabits.activities.habits.show.views
import android.content.* import android.content.Context
import android.util.* import android.util.AttributeSet
import android.view.* import android.view.LayoutInflater
import android.widget.* import android.view.View
import org.isoron.uhabits.core.models.* import android.widget.AdapterView
import org.isoron.uhabits.core.utils.* import android.widget.LinearLayout
import org.isoron.uhabits.core.utils.DateUtils.TruncateField.* import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.databinding.* import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.utils.* 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( data class ScoreCardViewModel(
val scores: List<Score>, val scores: List<Score>,
@ -86,9 +94,12 @@ class ScoreCardPresenter(
val scores = habit.scores.getByInterval(oldest, today).groupBy { val scores = habit.scores.getByInterval(oldest, today).groupBy {
DateUtils.truncate(field, it.timestamp, firstWeekday) DateUtils.truncate(field, it.timestamp, firstWeekday)
}.map { (timestamp, scores) -> }.map { (timestamp, scores) ->
Score(timestamp, scores.map { Score(
timestamp,
scores.map {
it.value it.value
}.average()) }.average()
)
}.sortedBy { }.sortedBy {
it.timestamp it.timestamp
}.reversed() }.reversed()

@ -18,14 +18,17 @@
*/ */
package org.isoron.uhabits.activities.habits.show.views package org.isoron.uhabits.activities.habits.show.views
import android.content.* import android.content.Context
import android.util.* import android.util.AttributeSet
import android.view.* import android.view.LayoutInflater
import android.widget.* import android.widget.LinearLayout
import kotlinx.coroutines.* import kotlinx.coroutines.Dispatchers
import org.isoron.uhabits.core.models.* import kotlinx.coroutines.invoke
import org.isoron.uhabits.databinding.* import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.utils.* 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( data class StreakCardViewModel(
val color: PaletteColor, val color: PaletteColor,

@ -18,18 +18,23 @@
*/ */
package org.isoron.uhabits.activities.habits.show.views package org.isoron.uhabits.activities.habits.show.views
import android.annotation.* import android.annotation.SuppressLint
import android.content.* import android.content.Context
import android.content.res.* import android.content.res.Resources
import android.util.* import android.util.AttributeSet
import android.view.* import android.view.LayoutInflater
import android.widget.* import android.view.View
import org.isoron.uhabits.* import android.widget.LinearLayout
import org.isoron.uhabits.activities.habits.list.views.* import org.isoron.uhabits.R
import org.isoron.uhabits.core.models.* import org.isoron.uhabits.activities.habits.list.views.toShortString
import org.isoron.uhabits.databinding.* import org.isoron.uhabits.core.models.Frequency
import org.isoron.uhabits.utils.* import org.isoron.uhabits.core.models.Habit
import java.util.* 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( data class SubtitleCardViewModel(
val color: PaletteColor, val color: PaletteColor,

@ -18,18 +18,21 @@
*/ */
package org.isoron.uhabits.activities.habits.show.views package org.isoron.uhabits.activities.habits.show.views
import android.content.* import android.content.Context
import android.content.res.* import android.content.res.Resources
import android.util.* import android.util.AttributeSet
import android.view.* import android.view.LayoutInflater
import android.widget.* import android.widget.LinearLayout
import kotlinx.coroutines.* import kotlinx.coroutines.Dispatchers
import org.isoron.uhabits.* import kotlinx.coroutines.invoke
import org.isoron.uhabits.core.models.* import org.isoron.uhabits.R
import org.isoron.uhabits.core.utils.* import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.databinding.* import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.utils.* import org.isoron.uhabits.core.utils.DateUtils
import java.util.* import org.isoron.uhabits.databinding.ShowHabitTargetBinding
import org.isoron.uhabits.utils.toThemedAndroidColor
import java.util.ArrayList
import java.util.Calendar
data class TargetCardViewModel( data class TargetCardViewModel(
val color: PaletteColor, val color: PaletteColor,

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

@ -18,14 +18,15 @@
*/ */
package org.isoron.uhabits.activities.settings package org.isoron.uhabits.activities.settings
import android.os.* import android.os.Bundle
import android.view.* import android.view.LayoutInflater
import androidx.appcompat.app.* import androidx.appcompat.app.AppCompatActivity
import org.isoron.uhabits.* import org.isoron.uhabits.HabitsApplication
import org.isoron.uhabits.activities.* import org.isoron.uhabits.R
import org.isoron.uhabits.core.models.* import org.isoron.uhabits.activities.AndroidThemeSwitcher
import org.isoron.uhabits.databinding.* import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.utils.* import org.isoron.uhabits.databinding.SettingsActivityBinding
import org.isoron.uhabits.utils.setupToolbar
class SettingsActivity : AppCompatActivity() { class SettingsActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {

@ -19,25 +19,31 @@
package org.isoron.uhabits.activities.sync package org.isoron.uhabits.activities.sync
import android.content.* import android.content.ClipData
import android.content.ClipboardManager import android.content.ClipboardManager
import android.graphics.* import android.content.Context
import android.os.* import android.graphics.Bitmap
import android.text.* import android.graphics.Color
import android.view.* import android.os.Bundle
import androidx.appcompat.app.* import android.text.Html
import com.google.zxing.* import android.view.View
import com.google.zxing.qrcode.* import androidx.appcompat.app.AppCompatActivity
import kotlinx.coroutines.* 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.utils.InterfaceUtils.getFontAwesome
import org.isoron.uhabits.* import org.isoron.uhabits.utils.setupToolbar
import org.isoron.uhabits.activities.* import org.isoron.uhabits.utils.showMessage
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.*
class SyncActivity : AppCompatActivity(), SyncBehavior.Screen { class SyncActivity : AppCompatActivity(), SyncBehavior.Screen {
@ -100,7 +106,6 @@ class SyncActivity : AppCompatActivity(), SyncBehavior.Screen {
binding.progress.visibility = View.GONE binding.progress.visibility = View.GONE
binding.qrCode.visibility = View.VISIBLE binding.qrCode.visibility = View.VISIBLE
binding.qrCode.setImageBitmap(generateQR(msg)) binding.qrCode.setImageBitmap(generateQR(msg))
} }
override suspend fun showLoadingScreen() { override suspend fun showLoadingScreen() {

@ -19,11 +19,11 @@
package org.isoron.uhabits.automation package org.isoron.uhabits.automation
import android.os.* import android.os.Bundle
import androidx.appcompat.app.* import androidx.appcompat.app.AppCompatActivity
import org.isoron.uhabits.* import org.isoron.uhabits.HabitsApplication
import org.isoron.uhabits.activities.* import org.isoron.uhabits.activities.AndroidThemeSwitcher
import org.isoron.uhabits.core.models.* import org.isoron.uhabits.core.models.HabitMatcherBuilder
class EditSettingActivity : AppCompatActivity() { class EditSettingActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@ -33,7 +33,8 @@ class EditSettingActivity : AppCompatActivity() {
HabitMatcherBuilder() HabitMatcherBuilder()
.setArchivedAllowed(false) .setArchivedAllowed(false)
.setCompletedAllowed(true) .setCompletedAllowed(true)
.build()) .build()
)
AndroidThemeSwitcher(this, app.component.preferences).apply() AndroidThemeSwitcher(this, app.component.preferences).apply()
val args = SettingUtils.parseIntent(this.intent, habits) val args = SettingUtils.parseIntent(this.intent, habits)

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

@ -19,16 +19,22 @@
package org.isoron.uhabits.automation package org.isoron.uhabits.automation
import android.R.layout.* import android.R.layout.simple_spinner_dropdown_item
import android.annotation.* import android.R.layout.simple_spinner_item
import android.content.* import android.annotation.SuppressLint
import android.view.* import android.content.Context
import android.widget.* import android.view.LayoutInflater
import org.isoron.uhabits.* import android.view.View
import org.isoron.uhabits.core.models.* import android.widget.AdapterView
import org.isoron.uhabits.databinding.* import android.widget.ArrayAdapter
import org.isoron.uhabits.utils.* import android.widget.FrameLayout
import java.util.* 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") @SuppressLint("ViewConstructor")
class EditSettingRootView( class EditSettingRootView(

@ -19,14 +19,16 @@
package org.isoron.uhabits.automation package org.isoron.uhabits.automation
import android.content.* import android.content.BroadcastReceiver
import dagger.* import android.content.Context
import org.isoron.uhabits.* import android.content.Intent
import org.isoron.uhabits.core.models.* import dagger.Component
import org.isoron.uhabits.core.ui.widgets.* import org.isoron.uhabits.HabitsApplication
import org.isoron.uhabits.core.utils.* import org.isoron.uhabits.core.models.HabitList
import org.isoron.uhabits.inject.* import org.isoron.uhabits.core.ui.widgets.WidgetBehavior
import org.isoron.uhabits.receivers.* 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_CHECK = 0
const val ACTION_UNCHECK = 1 const val ACTION_UNCHECK = 1

@ -19,7 +19,7 @@
package org.isoron.uhabits.database 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 { class AndroidCursor(private val cursor: android.database.Cursor) : Cursor {

@ -19,10 +19,10 @@
package org.isoron.uhabits.database package org.isoron.uhabits.database
import android.content.* import android.content.ContentValues
import android.database.sqlite.* import android.database.sqlite.SQLiteDatabase
import org.isoron.uhabits.core.database.* import org.isoron.uhabits.core.database.Database
import java.io.* import java.io.File
class AndroidDatabase( class AndroidDatabase(
private val db: SQLiteDatabase, private val db: SQLiteDatabase,

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

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

@ -18,7 +18,7 @@
*/ */
package org.isoron.uhabits.inject package org.isoron.uhabits.inject
import javax.inject.* import javax.inject.Qualifier
@Qualifier @Qualifier
@MustBeDocumented @MustBeDocumented

@ -18,7 +18,7 @@
*/ */
package org.isoron.uhabits.inject 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 used by objects that live as long as the activity is alive.

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

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

@ -19,11 +19,12 @@
package org.isoron.uhabits.inject package org.isoron.uhabits.inject
import android.content.* import android.content.Context
import dagger.* import dagger.Module
import org.isoron.uhabits.activities.* import dagger.Provides
import org.isoron.uhabits.core.preferences.* import org.isoron.uhabits.activities.AndroidThemeSwitcher
import org.isoron.uhabits.core.ui.* import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.core.ui.ThemeSwitcher
@Module @Module
class HabitsActivityModule { class HabitsActivityModule {

@ -19,28 +19,35 @@
package org.isoron.uhabits.inject package org.isoron.uhabits.inject
import android.content.* import android.content.Context
import dagger.* import dagger.Module
import org.isoron.uhabits.core.* import dagger.Provides
import org.isoron.uhabits.core.commands.* import org.isoron.uhabits.core.AppScope
import org.isoron.uhabits.core.database.* import org.isoron.uhabits.core.commands.CommandRunner
import org.isoron.uhabits.core.io.* import org.isoron.uhabits.core.database.Database
import org.isoron.uhabits.core.models.* import org.isoron.uhabits.core.database.DatabaseOpener
import org.isoron.uhabits.core.models.sqlite.* import org.isoron.uhabits.core.io.Logging
import org.isoron.uhabits.core.preferences.* import org.isoron.uhabits.core.models.HabitList
import org.isoron.uhabits.core.reminders.* import org.isoron.uhabits.core.models.ModelFactory
import org.isoron.uhabits.core.sync.* import org.isoron.uhabits.core.models.sqlite.SQLModelFactory
import org.isoron.uhabits.core.tasks.* import org.isoron.uhabits.core.models.sqlite.SQLiteHabitList
import org.isoron.uhabits.core.ui.* import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.database.* import org.isoron.uhabits.core.preferences.WidgetPreferences
import org.isoron.uhabits.inject.* import org.isoron.uhabits.core.reminders.ReminderScheduler
import org.isoron.uhabits.intents.* import org.isoron.uhabits.core.sync.AbstractSyncServer
import org.isoron.uhabits.io.* import org.isoron.uhabits.core.sync.NetworkManager
import org.isoron.uhabits.notifications.* import org.isoron.uhabits.core.tasks.TaskRunner
import org.isoron.uhabits.preferences.* import org.isoron.uhabits.core.ui.NotificationTray
import org.isoron.uhabits.sync.* import org.isoron.uhabits.database.AndroidDatabase
import org.isoron.uhabits.utils.* import org.isoron.uhabits.database.AndroidDatabaseOpener
import java.io.* 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 @Module
class HabitsModule(dbFile: File) { class HabitsModule(dbFile: File) {
@ -115,7 +122,7 @@ class HabitsModule(dbFile: File) {
@Provides @Provides
@AppScope @AppScope
fun getSyncServer(preferences: Preferences) : AbstractSyncServer { fun getSyncServer(preferences: Preferences): AbstractSyncServer {
return RemoteSyncServer(preferences) return RemoteSyncServer(preferences)
} }
@ -125,4 +132,3 @@ class HabitsModule(dbFile: File) {
return db return db
} }
} }

@ -19,17 +19,18 @@
package org.isoron.uhabits.intents package org.isoron.uhabits.intents
import android.content.* import android.content.Context
import android.net.* import android.content.Intent
import org.isoron.uhabits.* import android.net.Uri
import org.isoron.uhabits.activities.about.* import org.isoron.uhabits.R
import org.isoron.uhabits.activities.habits.edit.* import org.isoron.uhabits.activities.about.AboutActivity
import org.isoron.uhabits.activities.habits.show.* import org.isoron.uhabits.activities.habits.edit.EditHabitActivity
import org.isoron.uhabits.activities.intro.* import org.isoron.uhabits.activities.habits.show.ShowHabitActivity
import org.isoron.uhabits.activities.settings.* import org.isoron.uhabits.activities.intro.IntroActivity
import org.isoron.uhabits.activities.sync.* import org.isoron.uhabits.activities.settings.SettingsActivity
import org.isoron.uhabits.core.models.* import org.isoron.uhabits.activities.sync.SyncActivity
import javax.inject.* import org.isoron.uhabits.core.models.Habit
import javax.inject.Inject
class IntentFactory class IntentFactory
@Inject constructor() { @Inject constructor() {

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

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

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

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

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

@ -19,15 +19,18 @@
package org.isoron.uhabits.notifications package org.isoron.uhabits.notifications
import android.content.* import android.content.Context
import android.media.RingtoneManager.* import android.content.Intent
import android.net.* import android.content.SharedPreferences
import android.preference.* import android.media.RingtoneManager.EXTRA_RINGTONE_PICKED_URI
import android.provider.* 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.R
import org.isoron.uhabits.core.* import org.isoron.uhabits.core.AppScope
import org.isoron.uhabits.inject.* import org.isoron.uhabits.inject.AppContext
import javax.inject.* import javax.inject.Inject
@AppScope @AppScope
class RingtoneManager class RingtoneManager
@ -54,8 +57,10 @@ class RingtoneManager
fun getURI(): Uri? { fun getURI(): Uri? {
var ringtoneUri: Uri? = null var ringtoneUri: Uri? = null
val defaultRingtoneUri = Settings.System.DEFAULT_NOTIFICATION_URI val defaultRingtoneUri = Settings.System.DEFAULT_NOTIFICATION_URI
val prefRingtoneUri = prefs.getString("pref_ringtone_uri", val prefRingtoneUri = prefs.getString(
defaultRingtoneUri.toString())!! "pref_ringtone_uri",
defaultRingtoneUri.toString()
)!!
if (prefRingtoneUri.isNotEmpty()) if (prefRingtoneUri.isNotEmpty())
ringtoneUri = Uri.parse(prefRingtoneUri) ringtoneUri = Uri.parse(prefRingtoneUri)

@ -19,13 +19,14 @@
package org.isoron.uhabits.preferences package org.isoron.uhabits.preferences
import android.content.* import android.content.Context
import android.preference.* import android.content.SharedPreferences
import android.preference.PreferenceManager
import org.isoron.uhabits.R import org.isoron.uhabits.R
import org.isoron.uhabits.core.* import org.isoron.uhabits.core.AppScope
import org.isoron.uhabits.core.preferences.* import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.inject.* import org.isoron.uhabits.inject.AppContext
import javax.inject.* import javax.inject.Inject
@AppScope @AppScope
class SharedPreferencesStorage class SharedPreferencesStorage
@ -76,8 +77,10 @@ class SharedPreferencesStorage
override fun remove(key: String) = override fun remove(key: String) =
sharedPrefs.edit().remove(key).apply() sharedPrefs.edit().remove(key).apply()
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, override fun onSharedPreferenceChanged(
key: String) { sharedPreferences: SharedPreferences,
key: String
) {
val preferences = this.preferences ?: return val preferences = this.preferences ?: return
sharedPreferences.unregisterOnSharedPreferenceChangeListener(this) sharedPreferences.unregisterOnSharedPreferenceChangeListener(this)
when (key) { when (key) {

@ -19,9 +19,11 @@
package org.isoron.uhabits.sync package org.isoron.uhabits.sync
import android.content.* import android.content.Context
import android.net.* import android.net.ConnectivityManager
import org.isoron.uhabits.core.sync.* import android.net.Network
import android.net.NetworkRequest
import org.isoron.uhabits.core.sync.NetworkManager
class AndroidNetworkManager( class AndroidNetworkManager(
val context: Context, val context: Context,

@ -19,15 +19,26 @@
package org.isoron.uhabits.sync package org.isoron.uhabits.sync
import android.util.* import android.util.Log
import io.ktor.client.* import io.ktor.client.HttpClient
import io.ktor.client.engine.android.* import io.ktor.client.engine.android.Android
import io.ktor.client.features.* import io.ktor.client.features.ClientRequestException
import io.ktor.client.features.json.* import io.ktor.client.features.ServerResponseException
import io.ktor.client.request.* import io.ktor.client.features.json.JsonFeature
import kotlinx.coroutines.* import io.ktor.client.request.get
import org.isoron.uhabits.core.preferences.* import io.ktor.client.request.header
import org.isoron.uhabits.core.sync.* 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( class RemoteSyncServer(
private val preferences: Preferences, private val preferences: Preferences,
@ -42,7 +53,7 @@ class RemoteSyncServer(
Log.i("RemoteSyncServer", "POST $url") Log.i("RemoteSyncServer", "POST $url")
val response: RegisterReponse = httpClient.post(url) val response: RegisterReponse = httpClient.post(url)
return@IO response.key return@IO response.key
} catch(e: ServerResponseException) { } catch (e: ServerResponseException) {
throw ServiceUnavailable() throw ServiceUnavailable()
} }
} }
@ -59,8 +70,8 @@ class RemoteSyncServer(
throw ServiceUnavailable() throw ServiceUnavailable()
} catch (e: ClientRequestException) { } catch (e: ClientRequestException) {
Log.w("RemoteSyncServer", "ClientRequestException", e) Log.w("RemoteSyncServer", "ClientRequestException", e)
if(e.message!!.contains("409")) throw EditConflictException() if (e.message!!.contains("409")) throw EditConflictException()
if(e.message!!.contains("404")) throw KeyNotFoundException() if (e.message!!.contains("404")) throw KeyNotFoundException()
throw e throw e
} }
} }
@ -85,7 +96,7 @@ class RemoteSyncServer(
Log.i("RemoteSyncServer", "GET $url") Log.i("RemoteSyncServer", "GET $url")
val response: GetDataVersionResponse = httpClient.get(url) val response: GetDataVersionResponse = httpClient.get(url)
return@IO response.version return@IO response.version
} catch(e: ServerResponseException) { } catch (e: ServerResponseException) {
throw ServiceUnavailable() throw ServiceUnavailable()
} catch (e: ClientRequestException) { } catch (e: ClientRequestException) {
Log.w("RemoteSyncServer", "ClientRequestException", e) Log.w("RemoteSyncServer", "ClientRequestException", e)

@ -19,14 +19,17 @@
package org.isoron.uhabits.utils package org.isoron.uhabits.utils
import android.content.* import android.content.Context
import android.text.format.DateFormat import android.text.format.DateFormat
import org.isoron.uhabits.* import org.isoron.uhabits.R
import org.isoron.uhabits.core.models.* import org.isoron.uhabits.core.models.WeekdayList
import org.isoron.uhabits.core.utils.* import org.isoron.uhabits.core.utils.DateFormats
import org.isoron.uhabits.core.utils.DateUtils import org.isoron.uhabits.core.utils.DateUtils
import java.text.* import java.text.SimpleDateFormat
import java.util.* import java.util.Calendar
import java.util.Date
import java.util.Locale
import java.util.TimeZone
fun String.toSimpleDataFormat(): SimpleDateFormat { fun String.toSimpleDataFormat(): SimpleDateFormat {
val locale = Locale.getDefault() val locale = Locale.getDefault()

@ -20,8 +20,11 @@ package org.isoron.uhabits.utils
import android.os.Environment import android.os.Environment
import android.util.Log import android.util.Log
import java.io.* import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.InputStream
import java.io.OutputStream
fun File.copyTo(dst: File) { fun File.copyTo(dst: File) {
val inStream = FileInputStream(this) val inStream = FileInputStream(this)
@ -50,7 +53,7 @@ object FileUtils {
Log.e("FileUtils", "getDir: all potential parents are null or non-writable") Log.e("FileUtils", "getDir: all potential parents are null or non-writable")
return null return null
} }
val dir = File("${chosenDir.absolutePath}/${relativePath}/") val dir = File("${chosenDir.absolutePath}/$relativePath/")
if (!dir.exists() && !dir.mkdirs()) { if (!dir.exists() && !dir.mkdirs()) {
Log.e("FileUtils", "getDir: chosen dir does not exist and cannot be created") Log.e("FileUtils", "getDir: chosen dir does not exist and cannot be created")
return null return null

@ -18,13 +18,14 @@
*/ */
package org.isoron.uhabits.utils package org.isoron.uhabits.utils
import android.content.* import android.content.Context
import android.graphics.* import android.graphics.Typeface
import android.util.* import android.util.TypedValue
import android.view.* import android.view.View
import android.widget.* import android.view.ViewGroup
import android.widget.TextView.* import android.widget.TextView
import androidx.core.view.* import android.widget.TextView.OnEditorActionListener
import androidx.core.view.ViewCompat
object InterfaceUtils { object InterfaceUtils {
private var fontAwesome: Typeface? = null private var fontAwesome: Typeface? = null
@ -46,17 +47,21 @@ object InterfaceUtils {
@JvmStatic @JvmStatic
fun dpToPixels(context: Context, dp: Float): Float { fun dpToPixels(context: Context, dp: Float): Float {
if (fixedResolution != null) return dp * fixedResolution!! if (fixedResolution != null) return dp * fixedResolution!!
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, return TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
dp, dp,
context.resources.displayMetrics) context.resources.displayMetrics
)
} }
@JvmStatic @JvmStatic
fun spToPixels(context: Context, sp: Float): Float { fun spToPixels(context: Context, sp: Float): Float {
if (fixedResolution != null) return sp * fixedResolution!! if (fixedResolution != null) return sp * fixedResolution!!
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, return TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_SP,
sp, sp,
context.resources.displayMetrics) context.resources.displayMetrics
)
} }
@JvmStatic @JvmStatic
@ -69,8 +74,10 @@ object InterfaceUtils {
return dim return dim
} }
fun setupEditorAction(parent: ViewGroup, fun setupEditorAction(
listener: OnEditorActionListener) { parent: ViewGroup,
listener: OnEditorActionListener
) {
for (i in 0 until parent.childCount) { for (i in 0 until parent.childCount) {
val child = parent.getChildAt(i) val child = parent.getChildAt(i)
if (child is ViewGroup) setupEditorAction(child, listener) if (child is ViewGroup) setupEditorAction(child, listener)

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

Loading…
Cancel
Save