mirror of
https://github.com/iSoron/uhabits.git
synced 2025-12-07 09:38:52 -06:00
Rename legacy to android
This commit is contained in:
232
android/uhabits-android/src/main/AndroidManifest.xml
Normal file
232
android/uhabits-android/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,232 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
~
|
||||
~ This file is part of Loop Habit Tracker.
|
||||
~
|
||||
~ Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
~ it under the terms of the GNU General Public License as published by the
|
||||
~ Free Software Foundation, either version 3 of the License, or (at your
|
||||
~ option) any later version.
|
||||
~
|
||||
~ Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
~ WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
~ or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
~ more details.
|
||||
~
|
||||
~ You should have received a copy of the GNU General Public License along
|
||||
~ with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<manifest
|
||||
package="org.isoron.uhabits"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:versionCode="36"
|
||||
android:versionName="1.7.9">
|
||||
|
||||
<uses-permission android:name="android.permission.VIBRATE"/>
|
||||
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
|
||||
<application
|
||||
android:name=".HabitsApplication"
|
||||
android:allowBackup="true"
|
||||
android:backupAgent=".HabitsBackupAgent"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/main_activity_title"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppBaseTheme">
|
||||
<meta-data
|
||||
android:name="com.google.android.backup.api_key"
|
||||
android:value="AEdPqrEAAAAI6aeWncbnMNo8E5GWeZ44dlc5cQ7tCROwFhOtiw"/>
|
||||
|
||||
<activity
|
||||
android:name=".activities.habits.list.ListHabitsActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/main_activity_title"
|
||||
android:launchMode="singleTop"/>
|
||||
|
||||
<activity-alias
|
||||
android:name=".MainActivity"
|
||||
android:label="@string/main_activity_title"
|
||||
android:launchMode="singleTop"
|
||||
android:targetActivity=".activities.habits.list.ListHabitsActivity">
|
||||
<intent-filter android:label="@string/app_name">
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity-alias>
|
||||
|
||||
<activity
|
||||
android:name=".activities.habits.show.ShowHabitActivity"
|
||||
android:label="@string/title_activity_show_habit">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value=".activities.habits.list.ListHabitsActivity"/>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".activities.settings.SettingsActivity"
|
||||
android:label="@string/settings">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value=".activities.habits.list.ListHabitsActivity"/>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".activities.intro.IntroActivity"
|
||||
android:label=""
|
||||
android:theme="@style/Theme.AppCompat.Light.NoActionBar"/>
|
||||
<activity
|
||||
android:name=".widgets.HabitPickerDialog"
|
||||
android:theme="@style/Theme.AppCompat.Light.Dialog">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".activities.about.AboutActivity"
|
||||
android:label="@string/about">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value=".activities.habits.list.ListHabitsActivity"/>
|
||||
</activity>
|
||||
<activity android:name=".notifications.SnoozeDelayPickerActivity"
|
||||
android:excludeFromRecents="true"
|
||||
android:launchMode="singleInstance"
|
||||
android:theme="@android:style/Theme.Translucent.NoTitleBar">
|
||||
</activity>
|
||||
|
||||
<receiver
|
||||
android:name=".widgets.CheckmarkWidgetProvider"
|
||||
android:label="@string/checkmark">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.appwidget.provider"
|
||||
android:resource="@xml/widget_checkmark_info"/>
|
||||
</receiver>
|
||||
<service android:name=".widgets.StackWidgetService"
|
||||
android:permission="android.permission.BIND_REMOTEVIEWS"
|
||||
android:exported="false" />
|
||||
<receiver
|
||||
android:name=".widgets.HistoryWidgetProvider"
|
||||
android:label="@string/history">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.appwidget.provider"
|
||||
android:resource="@xml/widget_history_info"/>
|
||||
</receiver>
|
||||
<receiver
|
||||
android:name=".widgets.ScoreWidgetProvider"
|
||||
android:label="@string/habit_strength">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.appwidget.provider"
|
||||
android:resource="@xml/widget_score_info"/>
|
||||
</receiver>
|
||||
<receiver
|
||||
android:name=".widgets.StreakWidgetProvider"
|
||||
android:label="@string/streaks">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.appwidget.provider"
|
||||
android:resource="@xml/widget_streak_info"/>
|
||||
</receiver>
|
||||
<receiver
|
||||
android:name=".widgets.FrequencyWidgetProvider"
|
||||
android:label="@string/frequency">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.appwidget.provider"
|
||||
android:resource="@xml/widget_frequency_info"/>
|
||||
</receiver>
|
||||
<receiver android:name=".receivers.ReminderReceiver">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver android:name=".receivers.WidgetReceiver">
|
||||
<intent-filter>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
|
||||
<action android:name="org.isoron.uhabits.ACTION_TOGGLE_REPETITION"/>
|
||||
|
||||
<data
|
||||
android:host="org.isoron.uhabits"
|
||||
android:scheme="content"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
|
||||
<action android:name="org.isoron.uhabits.ACTION_ADD_REPETITION"/>
|
||||
|
||||
<data
|
||||
android:host="org.isoron.uhabits"
|
||||
android:scheme="content"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
|
||||
<action android:name="org.isoron.uhabits.ACTION_REMOVE_REPETITION"/>
|
||||
|
||||
<data
|
||||
android:host="org.isoron.uhabits"
|
||||
android:scheme="content"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<!-- Locale/Tasker -->
|
||||
<activity
|
||||
android:name=".automation.EditSettingActivity"
|
||||
android:exported="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name">
|
||||
<intent-filter>
|
||||
<action android:name="com.twofortyfouram.locale.intent.action.EDIT_SETTING"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- Locale/Tasker -->
|
||||
<receiver
|
||||
android:name=".automation.FireSettingReceiver"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="com.twofortyfouram.locale.intent.action.FIRE_SETTING"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<provider
|
||||
android:name="android.support.v4.content.FileProvider"
|
||||
android:authorities="org.isoron.uhabits"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths"/>
|
||||
</provider>
|
||||
|
||||
<service
|
||||
android:name=".sync.SyncService"
|
||||
android:enabled="true"
|
||||
android:exported="false">
|
||||
</service>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
41
android/uhabits-android/src/main/assets/cacert.pem
Normal file
41
android/uhabits-android/src/main/assets/cacert.pem
Normal file
@@ -0,0 +1,41 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIHPTCCBSWgAwIBAgIBADANBgkqhkiG9w0BAQQFADB5MRAwDgYDVQQKEwdSb290
|
||||
IENBMR4wHAYDVQQLExVodHRwOi8vd3d3LmNhY2VydC5vcmcxIjAgBgNVBAMTGUNB
|
||||
IENlcnQgU2lnbmluZyBBdXRob3JpdHkxITAfBgkqhkiG9w0BCQEWEnN1cHBvcnRA
|
||||
Y2FjZXJ0Lm9yZzAeFw0wMzAzMzAxMjI5NDlaFw0zMzAzMjkxMjI5NDlaMHkxEDAO
|
||||
BgNVBAoTB1Jvb3QgQ0ExHjAcBgNVBAsTFWh0dHA6Ly93d3cuY2FjZXJ0Lm9yZzEi
|
||||
MCAGA1UEAxMZQ0EgQ2VydCBTaWduaW5nIEF1dGhvcml0eTEhMB8GCSqGSIb3DQEJ
|
||||
ARYSc3VwcG9ydEBjYWNlcnQub3JnMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC
|
||||
CgKCAgEAziLA4kZ97DYoB1CW8qAzQIxL8TtmPzHlawI229Z89vGIj053NgVBlfkJ
|
||||
8BLPRoZzYLdufujAWGSuzbCtRRcMY/pnCujW0r8+55jE8Ez64AO7NV1sId6eINm6
|
||||
zWYyN3L69wj1x81YyY7nDl7qPv4coRQKFWyGhFtkZip6qUtTefWIonvuLwphK42y
|
||||
fk1WpRPs6tqSnqxEQR5YYGUFZvjARL3LlPdCfgv3ZWiYUQXw8wWRBB0bF4LsyFe7
|
||||
w2t6iPGwcswlWyCR7BYCEo8y6RcYSNDHBS4CMEK4JZwFaz+qOqfrU0j36NK2B5jc
|
||||
G8Y0f3/JHIJ6BVgrCFvzOKKrF11myZjXnhCLotLddJr3cQxyYN/Nb5gznZY0dj4k
|
||||
epKwDpUeb+agRThHqtdB7Uq3EvbXG4OKDy7YCbZZ16oE/9KTfWgu3YtLq1i6L43q
|
||||
laegw1SJpfvbi1EinbLDvhG+LJGGi5Z4rSDTii8aP8bQUWWHIbEZAWV/RRyH9XzQ
|
||||
QUxPKZgh/TMfdQwEUfoZd9vUFBzugcMd9Zi3aQaRIt0AUMyBMawSB3s42mhb5ivU
|
||||
fslfrejrckzzAeVLIL+aplfKkQABi6F1ITe1Yw1nPkZPcCBnzsXWWdsC4PDSy826
|
||||
YreQQejdIOQpvGQpQsgi3Hia/0PsmBsJUUtaWsJx8cTLc6nloQsCAwEAAaOCAc4w
|
||||
ggHKMB0GA1UdDgQWBBQWtTIb1Mfz4OaO873SsDrusjkY0TCBowYDVR0jBIGbMIGY
|
||||
gBQWtTIb1Mfz4OaO873SsDrusjkY0aF9pHsweTEQMA4GA1UEChMHUm9vdCBDQTEe
|
||||
MBwGA1UECxMVaHR0cDovL3d3dy5jYWNlcnQub3JnMSIwIAYDVQQDExlDQSBDZXJ0
|
||||
IFNpZ25pbmcgQXV0aG9yaXR5MSEwHwYJKoZIhvcNAQkBFhJzdXBwb3J0QGNhY2Vy
|
||||
dC5vcmeCAQAwDwYDVR0TAQH/BAUwAwEB/zAyBgNVHR8EKzApMCegJaAjhiFodHRw
|
||||
czovL3d3dy5jYWNlcnQub3JnL3Jldm9rZS5jcmwwMAYJYIZIAYb4QgEEBCMWIWh0
|
||||
dHBzOi8vd3d3LmNhY2VydC5vcmcvcmV2b2tlLmNybDA0BglghkgBhvhCAQgEJxYl
|
||||
aHR0cDovL3d3dy5jYWNlcnQub3JnL2luZGV4LnBocD9pZD0xMDBWBglghkgBhvhC
|
||||
AQ0ESRZHVG8gZ2V0IHlvdXIgb3duIGNlcnRpZmljYXRlIGZvciBGUkVFIGhlYWQg
|
||||
b3ZlciB0byBodHRwOi8vd3d3LmNhY2VydC5vcmcwDQYJKoZIhvcNAQEEBQADggIB
|
||||
ACjH7pyCArpcgBLKNQodgW+JapnM8mgPf6fhjViVPr3yBsOQWqy1YPaZQwGjiHCc
|
||||
nWKdpIevZ1gNMDY75q1I08t0AoZxPuIrA2jxNGJARjtT6ij0rPtmlVOKTV39O9lg
|
||||
18p5aTuxZZKmxoGCXJzN600BiqXfEVWqFcofN8CCmHBh22p8lqOOLlQ+TyGpkO/c
|
||||
gr/c6EWtTZBzCDyUZbAEmXZ/4rzCahWqlwQ3JNgelE5tDlG+1sSPypZt90Pf6DBl
|
||||
Jzt7u0NDY8RD97LsaMzhGY4i+5jhe1o+ATc7iwiwovOVThrLm82asduycPAtStvY
|
||||
sONvRUgzEv/+PDIqVPfE94rwiCPCR/5kenHA0R6mY7AHfqQv0wGP3J8rtsYIqQ+T
|
||||
SCX8Ev2fQtzzxD72V7DX3WnRBnc0CkvSyqD/HMaMyRa+xMwyN2hzXwj7UfdJUzYF
|
||||
CpUCTPJ5GhD22Dp1nPMd8aINcGeGG7MW9S/lpOt5hvk9C8JzC6WZrG/8Z7jlLwum
|
||||
GCSNe9FINSkYQKyTYOGWhlC0elnYjyELn8+CkcY7v2vcB5G5l1YjqrZslMZIBjzk
|
||||
zk6q5PYvCdxTby78dOs6Y5nCpqyJvKeyRKANihDjbPIky/qbn3BHLt4Ui9SyIAmW
|
||||
omTxJBzcoTWcFbLUvFUufQb1nA5V9FrWk9p2rSVzTMVD
|
||||
-----END CERTIFICATE-----
|
||||
BIN
android/uhabits-android/src/main/assets/fontawesome-webfont.ttf
Normal file
BIN
android/uhabits-android/src/main/assets/fontawesome-webfont.ttf
Normal file
Binary file not shown.
BIN
android/uhabits-android/src/main/ic_launcher-web.png
Normal file
BIN
android/uhabits-android/src/main/ic_launcher-web.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 49 KiB |
BIN
android/uhabits-android/src/main/ic_small_widget_preview-web.png
Normal file
BIN
android/uhabits-android/src/main/ic_small_widget_preview-web.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 35 KiB |
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits
|
||||
|
||||
import android.util.*
|
||||
import org.isoron.uhabits.core.*
|
||||
import org.isoron.uhabits.core.models.*
|
||||
import org.isoron.uhabits.core.utils.*
|
||||
import java.util.*
|
||||
import javax.inject.*
|
||||
|
||||
@AppScope
|
||||
class HabitLogger
|
||||
@Inject constructor() {
|
||||
|
||||
fun logReminderScheduled(habit: Habit, reminderTime: Long) {
|
||||
val min = Math.min(3, habit.name.length)
|
||||
val name = habit.name.substring(0, min)
|
||||
val df = DateFormats.getBackupDateFormat()
|
||||
val time = df.format(Date(reminderTime))
|
||||
Log.i("ReminderHelper",
|
||||
String.format("Setting alarm (%s): %s", time, name))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits
|
||||
|
||||
import android.app.*
|
||||
import android.content.*
|
||||
import org.isoron.androidbase.*
|
||||
import org.isoron.uhabits.core.database.*
|
||||
import org.isoron.uhabits.core.reminders.*
|
||||
import org.isoron.uhabits.core.ui.*
|
||||
import org.isoron.uhabits.utils.*
|
||||
import org.isoron.uhabits.widgets.*
|
||||
import java.io.*
|
||||
|
||||
/**
|
||||
* The Android application for Loop Habit Tracker.
|
||||
*/
|
||||
class HabitsApplication : Application() {
|
||||
|
||||
private lateinit var context: Context
|
||||
private lateinit var widgetUpdater: WidgetUpdater
|
||||
private lateinit var reminderScheduler: ReminderScheduler
|
||||
private lateinit var notificationTray: NotificationTray
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
context = this
|
||||
HabitsApplication.component = DaggerHabitsApplicationComponent
|
||||
.builder()
|
||||
.appContextModule(AppContextModule(context))
|
||||
.build()
|
||||
|
||||
if (isTestMode()) {
|
||||
val db = DatabaseUtils.getDatabaseFile(context)
|
||||
if (db.exists()) db.delete()
|
||||
}
|
||||
|
||||
try {
|
||||
DatabaseUtils.initializeDatabase(context)
|
||||
} catch (e: UnsupportedDatabaseVersionException) {
|
||||
val db = DatabaseUtils.getDatabaseFile(context)
|
||||
db.renameTo(File(db.absolutePath + ".invalid"))
|
||||
DatabaseUtils.initializeDatabase(context)
|
||||
}
|
||||
|
||||
widgetUpdater = component.widgetUpdater
|
||||
widgetUpdater.startListening()
|
||||
|
||||
reminderScheduler = component.reminderScheduler
|
||||
reminderScheduler.startListening()
|
||||
|
||||
notificationTray = component.notificationTray
|
||||
notificationTray.startListening()
|
||||
|
||||
val prefs = component.preferences
|
||||
prefs.setLastAppVersion(BuildConfig.VERSION_CODE)
|
||||
|
||||
val taskRunner = component.taskRunner
|
||||
taskRunner.execute {
|
||||
reminderScheduler.scheduleAll()
|
||||
widgetUpdater.updateWidgets()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTerminate() {
|
||||
reminderScheduler.stopListening()
|
||||
widgetUpdater.stopListening()
|
||||
notificationTray.stopListening()
|
||||
super.onTerminate()
|
||||
}
|
||||
|
||||
val component: HabitsApplicationComponent
|
||||
get() = HabitsApplication.component
|
||||
|
||||
companion object {
|
||||
lateinit var component: HabitsApplicationComponent
|
||||
|
||||
fun isTestMode(): Boolean {
|
||||
try {
|
||||
Class.forName("org.isoron.uhabits.BaseAndroidTest")
|
||||
return true
|
||||
} catch (e: ClassNotFoundException) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits;
|
||||
|
||||
import android.content.*;
|
||||
|
||||
import org.isoron.androidbase.*;
|
||||
import org.isoron.uhabits.core.*;
|
||||
import org.isoron.uhabits.core.commands.*;
|
||||
import org.isoron.uhabits.core.io.*;
|
||||
import org.isoron.uhabits.core.models.*;
|
||||
import org.isoron.uhabits.core.preferences.*;
|
||||
import org.isoron.uhabits.core.reminders.*;
|
||||
import org.isoron.uhabits.core.tasks.*;
|
||||
import org.isoron.uhabits.core.ui.*;
|
||||
import org.isoron.uhabits.core.ui.screens.habits.list.*;
|
||||
import org.isoron.uhabits.core.utils.*;
|
||||
import org.isoron.uhabits.intents.*;
|
||||
import org.isoron.uhabits.receivers.*;
|
||||
import org.isoron.uhabits.sync.*;
|
||||
import org.isoron.uhabits.tasks.*;
|
||||
import org.isoron.uhabits.widgets.*;
|
||||
|
||||
import dagger.*;
|
||||
|
||||
@AppScope
|
||||
@Component(modules = {
|
||||
AppContextModule.class,
|
||||
HabitsModule.class,
|
||||
AndroidTaskRunner.class,
|
||||
})
|
||||
public interface HabitsApplicationComponent
|
||||
{
|
||||
CommandRunner getCommandRunner();
|
||||
|
||||
@AppContext
|
||||
Context getContext();
|
||||
|
||||
CreateHabitCommandFactory getCreateHabitCommandFactory();
|
||||
|
||||
EditHabitCommandFactory getEditHabitCommandFactory();
|
||||
|
||||
GenericImporter getGenericImporter();
|
||||
|
||||
HabitCardListCache getHabitCardListCache();
|
||||
|
||||
HabitList getHabitList();
|
||||
|
||||
HabitLogger getHabitsLogger();
|
||||
|
||||
IntentFactory getIntentFactory();
|
||||
|
||||
IntentParser getIntentParser();
|
||||
|
||||
MidnightTimer getMidnightTimer();
|
||||
|
||||
ModelFactory getModelFactory();
|
||||
|
||||
NotificationTray getNotificationTray();
|
||||
|
||||
PendingIntentFactory getPendingIntentFactory();
|
||||
|
||||
Preferences getPreferences();
|
||||
|
||||
ReminderScheduler getReminderScheduler();
|
||||
|
||||
ReminderController getReminderController();
|
||||
|
||||
SyncManager getSyncManager();
|
||||
|
||||
TaskRunner getTaskRunner();
|
||||
|
||||
WidgetPreferences getWidgetPreferences();
|
||||
|
||||
WidgetUpdater getWidgetUpdater();
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits
|
||||
|
||||
import android.app.backup.*
|
||||
|
||||
/**
|
||||
* An Android BackupAgentHelper customized for this application.
|
||||
*/
|
||||
class HabitsBackupAgent : BackupAgentHelper() {
|
||||
override fun onCreate() {
|
||||
addHelper("preferences", SharedPreferencesBackupHelper(this, "preferences"))
|
||||
addHelper("database", FileBackupHelper(this, "../databases/uhabits.db"))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits
|
||||
|
||||
import android.content.*
|
||||
import android.database.sqlite.*
|
||||
|
||||
import org.isoron.uhabits.core.database.*
|
||||
import org.isoron.uhabits.database.*
|
||||
|
||||
class HabitsDatabaseOpener(
|
||||
context: Context,
|
||||
databaseFilename: String,
|
||||
private val version: Int
|
||||
) : SQLiteOpenHelper(context, databaseFilename, null, version) {
|
||||
|
||||
override fun onCreate(db: SQLiteDatabase) {
|
||||
db.version = 8
|
||||
onUpgrade(db, -1, version)
|
||||
}
|
||||
|
||||
override fun onUpgrade(db: SQLiteDatabase,
|
||||
oldVersion: Int,
|
||||
newVersion: Int) {
|
||||
if (db.version < 8) throw UnsupportedDatabaseVersionException()
|
||||
val helper = MigrationHelper(AndroidDatabase(db))
|
||||
helper.migrateTo(newVersion)
|
||||
}
|
||||
|
||||
override fun onDowngrade(db: SQLiteDatabase,
|
||||
oldVersion: Int,
|
||||
newVersion: Int) {
|
||||
throw UnsupportedDatabaseVersionException()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits
|
||||
|
||||
import dagger.*
|
||||
import org.isoron.uhabits.core.*
|
||||
import org.isoron.uhabits.core.commands.*
|
||||
import org.isoron.uhabits.core.database.*
|
||||
import org.isoron.uhabits.core.models.*
|
||||
import org.isoron.uhabits.core.models.sqlite.*
|
||||
import org.isoron.uhabits.core.preferences.*
|
||||
import org.isoron.uhabits.core.reminders.*
|
||||
import org.isoron.uhabits.core.tasks.*
|
||||
import org.isoron.uhabits.core.ui.*
|
||||
import org.isoron.uhabits.database.*
|
||||
import org.isoron.uhabits.intents.*
|
||||
import org.isoron.uhabits.notifications.*
|
||||
import org.isoron.uhabits.preferences.*
|
||||
import org.isoron.uhabits.utils.*
|
||||
|
||||
@Module
|
||||
class HabitsModule {
|
||||
@Provides
|
||||
@AppScope
|
||||
fun getPreferences(storage: SharedPreferencesStorage): Preferences {
|
||||
return Preferences(storage)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@AppScope
|
||||
fun getReminderScheduler(
|
||||
sys: IntentScheduler,
|
||||
commandRunner: CommandRunner,
|
||||
habitList: HabitList
|
||||
): ReminderScheduler {
|
||||
return ReminderScheduler(commandRunner, habitList, sys)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@AppScope
|
||||
fun getTray(
|
||||
taskRunner: TaskRunner,
|
||||
commandRunner: CommandRunner,
|
||||
preferences: Preferences,
|
||||
screen: AndroidNotificationTray
|
||||
): NotificationTray {
|
||||
return NotificationTray(taskRunner, commandRunner, preferences, screen)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@AppScope
|
||||
fun getWidgetPreferences(
|
||||
storage: SharedPreferencesStorage
|
||||
): WidgetPreferences {
|
||||
return WidgetPreferences(storage)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@AppScope
|
||||
fun getModelFactory(): ModelFactory {
|
||||
return SQLModelFactory(AndroidDatabase(DatabaseUtils.openDatabase()))
|
||||
}
|
||||
|
||||
@Provides
|
||||
@AppScope
|
||||
fun getHabitList(list: SQLiteHabitList): HabitList {
|
||||
return list
|
||||
}
|
||||
|
||||
@Provides
|
||||
@AppScope
|
||||
fun getDatabaseOpener(opener: AndroidDatabaseOpener): DatabaseOpener {
|
||||
return opener
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities
|
||||
|
||||
import org.isoron.androidbase.activities.*
|
||||
import org.isoron.uhabits.*
|
||||
import org.isoron.uhabits.core.preferences.*
|
||||
import org.isoron.uhabits.core.ui.*
|
||||
import javax.inject.*
|
||||
|
||||
@ActivityScope
|
||||
class AndroidThemeSwitcher
|
||||
@Inject constructor(
|
||||
private val activity: BaseActivity,
|
||||
preferences: Preferences
|
||||
) : ThemeSwitcher(preferences) {
|
||||
|
||||
override fun applyDarkTheme() {
|
||||
activity.setTheme(R.style.AppBaseThemeDark)
|
||||
}
|
||||
|
||||
override fun applyLightTheme() {
|
||||
activity.setTheme(R.style.AppBaseTheme)
|
||||
}
|
||||
|
||||
override fun applyPureBlackTheme() {
|
||||
activity.setTheme(R.style.AppBaseThemeDark_PureBlack)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities
|
||||
|
||||
import dagger.*
|
||||
import org.isoron.uhabits.core.models.*
|
||||
|
||||
@Module
|
||||
class HabitModule(private val habit: Habit) {
|
||||
@Provides fun getHabit() = habit
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities
|
||||
|
||||
import android.content.*
|
||||
import android.os.*
|
||||
import org.isoron.androidbase.activities.*
|
||||
import org.isoron.uhabits.*
|
||||
import org.isoron.uhabits.core.models.*
|
||||
|
||||
abstract class HabitsActivity : BaseActivity() {
|
||||
lateinit var component: HabitsActivityComponent
|
||||
lateinit var appComponent: HabitsApplicationComponent
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
appComponent = (applicationContext as HabitsApplication).component
|
||||
|
||||
val habit = getHabitFromIntent(appComponent.habitList)
|
||||
?: appComponent.modelFactory.buildHabit()
|
||||
|
||||
component = DaggerHabitsActivityComponent
|
||||
.builder()
|
||||
.activityContextModule(ActivityContextModule(this))
|
||||
.baseActivityModule(BaseActivityModule(this))
|
||||
.habitModule(HabitModule(habit))
|
||||
.habitsApplicationComponent(appComponent)
|
||||
.build()
|
||||
|
||||
component.themeSwitcher.apply()
|
||||
}
|
||||
|
||||
private fun getHabitFromIntent(habitList: HabitList): Habit? {
|
||||
val data = intent.data ?: return null
|
||||
val habit = habitList.getById(ContentUris.parseId(data))
|
||||
?: throw RuntimeException("habit not found")
|
||||
return habit
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities
|
||||
|
||||
import dagger.*
|
||||
import org.isoron.androidbase.activities.*
|
||||
import org.isoron.uhabits.*
|
||||
import org.isoron.uhabits.activities.about.*
|
||||
import org.isoron.uhabits.activities.common.dialogs.*
|
||||
import org.isoron.uhabits.activities.habits.list.*
|
||||
import org.isoron.uhabits.activities.habits.list.views.*
|
||||
import org.isoron.uhabits.activities.habits.show.*
|
||||
import org.isoron.uhabits.core.ui.*
|
||||
import org.isoron.uhabits.core.ui.screens.habits.list.*
|
||||
|
||||
@ActivityScope
|
||||
@Component(modules = arrayOf(
|
||||
ActivityContextModule::class,
|
||||
BaseActivityModule::class,
|
||||
AboutModule::class,
|
||||
HabitsActivityModule::class,
|
||||
ListHabitsModule::class,
|
||||
ShowHabitModule::class,
|
||||
HabitModule::class
|
||||
), dependencies = arrayOf(HabitsApplicationComponent::class))
|
||||
interface HabitsActivityComponent {
|
||||
val aboutRootView: AboutRootView
|
||||
val aboutScreen: AboutScreen
|
||||
val colorPickerDialogFactory: ColorPickerDialogFactory
|
||||
val habitCardListAdapter: HabitCardListAdapter
|
||||
val listHabitsBehavior: ListHabitsBehavior
|
||||
val listHabitsMenu: ListHabitsMenu
|
||||
val listHabitsRootView: ListHabitsRootView
|
||||
val listHabitsScreen: ListHabitsScreen
|
||||
val listHabitsSelectionMenu: ListHabitsSelectionMenu
|
||||
val showHabitScreen: ShowHabitScreen
|
||||
val themeSwitcher: ThemeSwitcher
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities
|
||||
|
||||
import dagger.*
|
||||
import org.isoron.androidbase.activities.*
|
||||
import org.isoron.uhabits.core.ui.*
|
||||
|
||||
@Module
|
||||
abstract class HabitsActivityModule {
|
||||
@Binds @ActivityScope
|
||||
internal abstract fun getThemeSwitcher(t: AndroidThemeSwitcher): ThemeSwitcher
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.isoron.uhabits.activities
|
||||
|
||||
import org.isoron.androidbase.*
|
||||
import org.isoron.uhabits.core.ui.screens.habits.list.*
|
||||
import org.isoron.uhabits.core.ui.screens.habits.show.*
|
||||
import java.io.*
|
||||
import javax.inject.*
|
||||
|
||||
class HabitsDirFinder @Inject
|
||||
constructor(
|
||||
private val androidDirFinder: AndroidDirFinder
|
||||
) : ShowHabitMenuBehavior.System, ListHabitsBehavior.DirFinder {
|
||||
|
||||
override fun getCSVOutputDir(): File {
|
||||
return androidDirFinder.getFilesDir("CSV")!!
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.about;
|
||||
|
||||
import android.os.*;
|
||||
|
||||
import org.isoron.uhabits.activities.*;
|
||||
|
||||
/**
|
||||
* Activity that allows the user to see information about the app itself.
|
||||
* Display current version, link to Google Play and list of contributors.
|
||||
*/
|
||||
public class AboutActivity extends HabitsActivity
|
||||
{
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState)
|
||||
{
|
||||
super.onCreate(savedInstanceState);
|
||||
AboutScreen screen = getComponent().getAboutScreen();
|
||||
screen.setRootView(getComponent().getAboutRootView());
|
||||
setScreen(screen);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.isoron.uhabits.activities.about;
|
||||
|
||||
import org.isoron.uhabits.core.ui.screens.about.*;
|
||||
|
||||
import dagger.*;
|
||||
|
||||
@Module
|
||||
public abstract class AboutModule
|
||||
{
|
||||
@Binds
|
||||
abstract AboutBehavior.Screen getScreen(AboutScreen screen);
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.about;
|
||||
|
||||
import android.content.*;
|
||||
import android.support.annotation.*;
|
||||
import android.widget.*;
|
||||
|
||||
import org.isoron.androidbase.activities.*;
|
||||
import org.isoron.androidbase.utils.*;
|
||||
import org.isoron.uhabits.BuildConfig;
|
||||
import org.isoron.uhabits.R;
|
||||
import org.isoron.uhabits.core.ui.screens.about.*;
|
||||
|
||||
import javax.inject.*;
|
||||
|
||||
import butterknife.*;
|
||||
|
||||
public class AboutRootView extends BaseRootView
|
||||
{
|
||||
@BindView(R.id.tvVersion)
|
||||
TextView tvVersion;
|
||||
|
||||
@NonNull
|
||||
private final AboutBehavior behavior;
|
||||
|
||||
@Inject
|
||||
public AboutRootView(@NonNull @ActivityContext Context context,
|
||||
@NonNull AboutBehavior behavior)
|
||||
{
|
||||
super(context);
|
||||
this.behavior = behavior;
|
||||
|
||||
addView(inflate(getContext(), R.layout.about, null));
|
||||
ButterKnife.bind(this);
|
||||
|
||||
String version = getResources().getString(R.string.version_n);
|
||||
tvVersion.setText(String.format(version, BuildConfig.VERSION_NAME));
|
||||
|
||||
setDisplayHomeAsUp(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getToolbarColor()
|
||||
{
|
||||
StyledResources res = new StyledResources(getContext());
|
||||
if (!res.getBoolean(R.attr.useHabitColorAsPrimary))
|
||||
return super.getToolbarColor();
|
||||
|
||||
return res.getColor(R.attr.aboutScreenColor);
|
||||
}
|
||||
|
||||
@OnClick(R.id.tvFeedback)
|
||||
public void onClickFeedback()
|
||||
{
|
||||
behavior.onSendFeedback();
|
||||
}
|
||||
|
||||
@OnClick(R.id.tvVersion)
|
||||
public void onClickIcon()
|
||||
{
|
||||
behavior.onPressDeveloperCountdown();
|
||||
}
|
||||
|
||||
@OnClick(R.id.tvRate)
|
||||
public void onClickRate()
|
||||
{
|
||||
behavior.onRateApp();
|
||||
}
|
||||
|
||||
@OnClick(R.id.tvSource)
|
||||
public void onClickSource()
|
||||
{
|
||||
behavior.onViewSourceCode();
|
||||
}
|
||||
|
||||
@OnClick(R.id.tvTranslate)
|
||||
public void onClickTranslate()
|
||||
{
|
||||
behavior.onTranslateApp();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initToolbar()
|
||||
{
|
||||
super.initToolbar();
|
||||
getToolbar().setTitle(getResources().getString(R.string.about));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.about;
|
||||
|
||||
import android.support.annotation.*;
|
||||
import android.widget.*;
|
||||
|
||||
import org.isoron.androidbase.activities.*;
|
||||
import org.isoron.uhabits.core.ui.screens.about.*;
|
||||
import org.isoron.uhabits.intents.*;
|
||||
|
||||
import javax.inject.*;
|
||||
|
||||
import static org.isoron.uhabits.core.ui.screens.about.AboutBehavior.Message.*;
|
||||
|
||||
public class AboutScreen extends BaseScreen implements AboutBehavior.Screen
|
||||
{
|
||||
@NonNull
|
||||
private final IntentFactory intents;
|
||||
|
||||
@Inject
|
||||
public AboutScreen(@NonNull BaseActivity activity,
|
||||
@NonNull IntentFactory intents)
|
||||
{
|
||||
super(activity);
|
||||
this.intents = intents;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showMessage(AboutBehavior.Message message)
|
||||
{
|
||||
if (message == YOU_ARE_NOW_A_DEVELOPER) Toast
|
||||
.makeText(activity, "You are now a developer", Toast.LENGTH_LONG)
|
||||
.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showRateAppWebsite()
|
||||
{
|
||||
activity.startActivity(intents.rateApp(activity));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showSendFeedbackScreen()
|
||||
{
|
||||
activity.startActivity(intents.sendFeedback(activity));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showSourceCodeWebsite()
|
||||
{
|
||||
activity.startActivity(intents.viewSourceCode(activity));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showTranslationWebsite()
|
||||
{
|
||||
activity.startActivity(intents.helpTranslate(activity));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.common.dialogs;
|
||||
|
||||
import org.isoron.uhabits.core.ui.callbacks.*;
|
||||
import org.isoron.uhabits.utils.*;
|
||||
|
||||
/**
|
||||
* Dialog that allows the user to choose a color.
|
||||
*/
|
||||
public class ColorPickerDialog extends com.android.colorpicker.ColorPickerDialog
|
||||
{
|
||||
public void setListener(OnColorPickedCallback callback)
|
||||
{
|
||||
super.setOnColorSelectedListener(c ->
|
||||
{
|
||||
c = PaletteUtils.colorToPaletteIndex(getContext(), c);
|
||||
callback.onColorPicked(c);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.common.dialogs;
|
||||
|
||||
import android.content.*;
|
||||
|
||||
import org.isoron.androidbase.activities.*;
|
||||
import org.isoron.androidbase.utils.*;
|
||||
import org.isoron.uhabits.R;
|
||||
import org.isoron.uhabits.utils.*;
|
||||
|
||||
import javax.inject.*;
|
||||
|
||||
@ActivityScope
|
||||
public class ColorPickerDialogFactory
|
||||
{
|
||||
private final Context context;
|
||||
|
||||
@Inject
|
||||
public ColorPickerDialogFactory(@ActivityContext Context context)
|
||||
{
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public ColorPickerDialog create(int paletteColor)
|
||||
{
|
||||
ColorPickerDialog dialog = new ColorPickerDialog();
|
||||
StyledResources res = new StyledResources(context);
|
||||
int color = PaletteUtils.getColor(context, paletteColor);
|
||||
|
||||
dialog.initialize(R.string.color_picker_default_title, res.getPalette(),
|
||||
color, 4, com.android.colorpicker.ColorPickerDialog.SIZE_SMALL);
|
||||
|
||||
return dialog;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.common.dialogs;
|
||||
|
||||
import android.content.*;
|
||||
import android.support.annotation.*;
|
||||
import android.support.v7.app.*;
|
||||
|
||||
import com.google.auto.factory.*;
|
||||
|
||||
import org.isoron.androidbase.activities.*;
|
||||
import org.isoron.uhabits.R;
|
||||
import org.isoron.uhabits.core.ui.callbacks.*;
|
||||
|
||||
import butterknife.*;
|
||||
|
||||
/**
|
||||
* Dialog that asks the user confirmation before executing a delete operation.
|
||||
*/
|
||||
@AutoFactory(allowSubclasses = true)
|
||||
public class ConfirmDeleteDialog extends AlertDialog
|
||||
{
|
||||
@BindString(R.string.delete_habits_message)
|
||||
protected String question;
|
||||
|
||||
@BindString(android.R.string.yes)
|
||||
protected String yes;
|
||||
|
||||
@BindString(android.R.string.no)
|
||||
protected String no;
|
||||
|
||||
protected ConfirmDeleteDialog(@Provided @ActivityContext Context context,
|
||||
@NonNull OnConfirmedCallback callback)
|
||||
{
|
||||
super(context);
|
||||
ButterKnife.bind(this);
|
||||
|
||||
setTitle(R.string.delete_habits);
|
||||
setMessage(question);
|
||||
setButton(BUTTON_POSITIVE, yes, (dialog, which) -> callback.onConfirmed());
|
||||
setButton(BUTTON_NEGATIVE, no, (dialog, which) -> {});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.common.dialogs;
|
||||
|
||||
import android.app.*;
|
||||
import android.content.*;
|
||||
import android.os.*;
|
||||
import android.support.annotation.*;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.support.v7.app.*;
|
||||
import android.util.*;
|
||||
|
||||
import org.isoron.uhabits.*;
|
||||
import org.isoron.uhabits.activities.common.views.*;
|
||||
import org.isoron.uhabits.core.models.*;
|
||||
import org.isoron.uhabits.core.tasks.*;
|
||||
import org.isoron.uhabits.utils.*;
|
||||
|
||||
import static org.isoron.androidbase.utils.InterfaceUtils.*;
|
||||
|
||||
public class HistoryEditorDialog extends AppCompatDialogFragment
|
||||
implements DialogInterface.OnClickListener, ModelObservable.Listener
|
||||
{
|
||||
@Nullable
|
||||
private Habit habit;
|
||||
|
||||
@Nullable
|
||||
HistoryChart historyChart;
|
||||
|
||||
@NonNull
|
||||
private Controller controller;
|
||||
|
||||
private HabitList habitList;
|
||||
|
||||
private TaskRunner taskRunner;
|
||||
|
||||
public HistoryEditorDialog()
|
||||
{
|
||||
this.controller = new Controller() {};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which)
|
||||
{
|
||||
dismiss();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState)
|
||||
{
|
||||
Context context = getActivity();
|
||||
|
||||
HabitsApplication app =
|
||||
(HabitsApplication) getActivity().getApplicationContext();
|
||||
habitList = app.getComponent().getHabitList();
|
||||
taskRunner = app.getComponent().getTaskRunner();
|
||||
|
||||
historyChart = new HistoryChart(context);
|
||||
historyChart.setController(controller);
|
||||
|
||||
if (savedInstanceState != null)
|
||||
{
|
||||
long id = savedInstanceState.getLong("habit", -1);
|
||||
if (id > 0) this.habit = habitList.getById(id);
|
||||
historyChart.onRestoreInstanceState(
|
||||
savedInstanceState.getParcelable("historyChart"));
|
||||
}
|
||||
|
||||
int padding =
|
||||
(int) getDimension(getContext(), R.dimen.history_editor_padding);
|
||||
|
||||
historyChart.setPadding(padding, 0, padding, 0);
|
||||
historyChart.setIsEditable(true);
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
||||
builder
|
||||
.setTitle(R.string.history)
|
||||
.setView(historyChart)
|
||||
.setPositiveButton(android.R.string.ok, this);
|
||||
|
||||
return builder.create();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onModelChange()
|
||||
{
|
||||
refreshData();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause()
|
||||
{
|
||||
habit.getCheckmarks().observable.removeListener(this);
|
||||
super.onPause();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume()
|
||||
{
|
||||
super.onResume();
|
||||
|
||||
DisplayMetrics metrics = getResources().getDisplayMetrics();
|
||||
int maxHeight = getResources().getDimensionPixelSize(
|
||||
R.dimen.history_editor_max_height);
|
||||
int width = metrics.widthPixels;
|
||||
int height = Math.min(metrics.heightPixels, maxHeight);
|
||||
|
||||
getDialog().getWindow().setLayout(width, height);
|
||||
|
||||
refreshData();
|
||||
habit.getCheckmarks().observable.addListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState)
|
||||
{
|
||||
outState.putLong("habit", habit.getId());
|
||||
outState.putParcelable("historyChart", historyChart.onSaveInstanceState());
|
||||
}
|
||||
|
||||
public void setController(@NonNull Controller controller)
|
||||
{
|
||||
this.controller = controller;
|
||||
if (historyChart != null) historyChart.setController(controller);
|
||||
}
|
||||
|
||||
public void setHabit(@Nullable Habit habit)
|
||||
{
|
||||
this.habit = habit;
|
||||
}
|
||||
|
||||
private void refreshData()
|
||||
{
|
||||
if (habit == null) return;
|
||||
taskRunner.execute(new RefreshTask());
|
||||
}
|
||||
|
||||
public interface Controller extends HistoryChart.Controller {}
|
||||
|
||||
private class RefreshTask implements Task
|
||||
{
|
||||
public int[] checkmarks;
|
||||
|
||||
@Override
|
||||
public void doInBackground()
|
||||
{
|
||||
checkmarks = habit.getCheckmarks().getAllValues();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPostExecute()
|
||||
{
|
||||
if (getContext() == null || habit == null || historyChart == null)
|
||||
return;
|
||||
|
||||
int color = PaletteUtils.getColor(getContext(), habit.getColor());
|
||||
historyChart.setColor(color);
|
||||
historyChart.setCheckmarks(checkmarks);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Álinson Santos Xavier
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see .
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.common.dialogs
|
||||
|
||||
import android.content.*
|
||||
import android.support.v7.app.*
|
||||
import android.text.*
|
||||
import android.view.*
|
||||
import android.view.inputmethod.*
|
||||
import android.widget.*
|
||||
import org.isoron.androidbase.activities.*
|
||||
import org.isoron.androidbase.utils.*
|
||||
import org.isoron.uhabits.*
|
||||
import org.isoron.uhabits.core.ui.screens.habits.list.*
|
||||
import javax.inject.*
|
||||
|
||||
class NumberPickerFactory
|
||||
@Inject constructor(
|
||||
@ActivityContext private val context: Context
|
||||
) {
|
||||
fun create(value: Double,
|
||||
unit: String,
|
||||
callback: ListHabitsBehavior.NumberPickerCallback): AlertDialog {
|
||||
|
||||
val inflater = LayoutInflater.from(context)
|
||||
val view = inflater.inflate(R.layout.number_picker_dialog, null)
|
||||
|
||||
val picker = view.findViewById<NumberPicker>(R.id.picker)
|
||||
val picker2 = view.findViewById<NumberPicker>(R.id.picker2)
|
||||
val tvUnit = view.findViewById<TextView>(R.id.tvUnit)
|
||||
|
||||
val intValue = Math.round(value * 100).toInt()
|
||||
|
||||
picker.minValue = 0
|
||||
picker.maxValue = Integer.MAX_VALUE / 100
|
||||
picker.value = intValue / 100
|
||||
picker.wrapSelectorWheel = false
|
||||
|
||||
picker2.minValue = 0
|
||||
picker2.maxValue = 19
|
||||
picker2.setFormatter { v -> String.format("%02d", 5 * v) }
|
||||
picker2.value = intValue % 100 / 5
|
||||
refreshInitialValue(picker2)
|
||||
|
||||
tvUnit.text = unit
|
||||
|
||||
val dialog = AlertDialog.Builder(context)
|
||||
.setView(view)
|
||||
.setTitle(R.string.change_value)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
picker.clearFocus()
|
||||
val v = picker.value + 0.05 * picker2.value
|
||||
callback.onNumberPicked(v)
|
||||
}
|
||||
.create()
|
||||
|
||||
dialog.window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE)
|
||||
|
||||
InterfaceUtils.setupEditorAction(picker) { _, actionId, _ ->
|
||||
if (actionId == EditorInfo.IME_ACTION_DONE)
|
||||
dialog.getButton(DialogInterface.BUTTON_POSITIVE).performClick()
|
||||
false
|
||||
}
|
||||
|
||||
return dialog
|
||||
}
|
||||
|
||||
private fun refreshInitialValue(picker: NumberPicker) {
|
||||
// Workaround for Android bug:
|
||||
// https://code.google.com/p/android/issues/detail?id=35482
|
||||
val f = NumberPicker::class.java.getDeclaredField("mInputText")
|
||||
f.isAccessible = true
|
||||
val inputText = f.get(picker) as EditText
|
||||
inputText.filters = arrayOfNulls<InputFilter>(0)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.common.dialogs;
|
||||
|
||||
import android.app.*;
|
||||
import android.content.*;
|
||||
import android.os.*;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.support.v7.app.*;
|
||||
|
||||
import org.isoron.uhabits.*;
|
||||
import org.isoron.uhabits.core.models.*;
|
||||
import org.isoron.uhabits.core.utils.*;
|
||||
|
||||
/**
|
||||
* Dialog that allows the user to pick one or more days of the week.
|
||||
*/
|
||||
public class WeekdayPickerDialog extends AppCompatDialogFragment implements
|
||||
DialogInterface.OnMultiChoiceClickListener,
|
||||
DialogInterface.OnClickListener
|
||||
{
|
||||
private boolean[] selectedDays;
|
||||
|
||||
private OnWeekdaysPickedListener listener;
|
||||
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which, boolean isChecked)
|
||||
{
|
||||
selectedDays[which] = isChecked;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which)
|
||||
{
|
||||
if (listener != null)
|
||||
listener.onWeekdaysSet(new WeekdayList(selectedDays));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState)
|
||||
{
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
|
||||
builder
|
||||
.setTitle(R.string.select_weekdays)
|
||||
.setMultiChoiceItems(DateUtils.getLongDayNames(), selectedDays,
|
||||
this)
|
||||
.setPositiveButton(android.R.string.yes, this)
|
||||
.setNegativeButton(android.R.string.cancel, (dialog, which) -> {
|
||||
dismiss();
|
||||
});
|
||||
|
||||
return builder.create();
|
||||
}
|
||||
|
||||
public void setListener(OnWeekdaysPickedListener listener)
|
||||
{
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
public void setSelectedDays(WeekdayList days)
|
||||
{
|
||||
this.selectedDays = days.toArray();
|
||||
}
|
||||
|
||||
public interface OnWeekdaysPickedListener
|
||||
{
|
||||
void onWeekdaysSet(WeekdayList days);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,479 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.common.views;
|
||||
|
||||
import android.content.*;
|
||||
import android.graphics.*;
|
||||
import android.support.annotation.*;
|
||||
import android.util.*;
|
||||
|
||||
import org.isoron.androidbase.utils.*;
|
||||
import org.isoron.uhabits.*;
|
||||
import org.isoron.uhabits.activities.habits.list.views.*;
|
||||
import org.isoron.uhabits.core.models.*;
|
||||
import org.isoron.uhabits.core.utils.*;
|
||||
import org.isoron.uhabits.utils.*;
|
||||
|
||||
import java.text.*;
|
||||
import java.util.*;
|
||||
|
||||
import static org.isoron.androidbase.utils.InterfaceUtils.*;
|
||||
|
||||
public class BarChart extends ScrollableChart
|
||||
{
|
||||
private static final PorterDuffXfermode XFERMODE_CLEAR =
|
||||
new PorterDuffXfermode(PorterDuff.Mode.CLEAR);
|
||||
|
||||
private static final PorterDuffXfermode XFERMODE_SRC =
|
||||
new PorterDuffXfermode(PorterDuff.Mode.SRC);
|
||||
|
||||
private Paint pGrid;
|
||||
|
||||
private float em;
|
||||
|
||||
private SimpleDateFormat dfMonth;
|
||||
|
||||
private SimpleDateFormat dfDay;
|
||||
|
||||
private SimpleDateFormat dfYear;
|
||||
|
||||
private Paint pText, pGraph;
|
||||
|
||||
private RectF rect, prevRect;
|
||||
|
||||
private int baseSize;
|
||||
|
||||
private int paddingTop;
|
||||
|
||||
private float columnWidth;
|
||||
|
||||
private int columnHeight;
|
||||
|
||||
private int nColumns;
|
||||
|
||||
private int textColor;
|
||||
|
||||
private int gridColor;
|
||||
|
||||
@Nullable
|
||||
private List<Checkmark> checkmarks;
|
||||
|
||||
private int primaryColor;
|
||||
|
||||
private int bucketSize = 7;
|
||||
|
||||
private int backgroundColor;
|
||||
|
||||
private Bitmap drawingCache;
|
||||
|
||||
private Canvas cacheCanvas;
|
||||
|
||||
private boolean isTransparencyEnabled;
|
||||
|
||||
private int skipYear = 0;
|
||||
|
||||
private String previousYearText;
|
||||
|
||||
private String previousMonthText;
|
||||
|
||||
private double maxValue;
|
||||
|
||||
private double target;
|
||||
|
||||
public BarChart(Context context)
|
||||
{
|
||||
super(context);
|
||||
init();
|
||||
}
|
||||
|
||||
public BarChart(Context context, AttributeSet attrs)
|
||||
{
|
||||
super(context, attrs);
|
||||
init();
|
||||
}
|
||||
|
||||
public void populateWithRandomData()
|
||||
{
|
||||
Random random = new Random();
|
||||
List<Checkmark> checkmarks = new LinkedList<>();
|
||||
|
||||
Timestamp today = DateUtils.getToday();
|
||||
|
||||
for (int i = 1; i < 100; i++)
|
||||
{
|
||||
int value = random.nextInt(1000);
|
||||
checkmarks.add(new Checkmark(today.minus(i), value));
|
||||
}
|
||||
|
||||
setCheckmarks(checkmarks);
|
||||
setTarget(0.5);
|
||||
}
|
||||
|
||||
public void setBucketSize(int bucketSize)
|
||||
{
|
||||
this.bucketSize = bucketSize;
|
||||
postInvalidate();
|
||||
}
|
||||
|
||||
public void setCheckmarks(@NonNull List<Checkmark> checkmarks)
|
||||
{
|
||||
this.checkmarks = checkmarks;
|
||||
|
||||
maxValue = 1.0;
|
||||
for (Checkmark c : checkmarks)
|
||||
maxValue = Math.max(maxValue, c.getValue());
|
||||
maxValue = Math.ceil(maxValue / 1000 * 1.05) * 1000;
|
||||
|
||||
postInvalidate();
|
||||
}
|
||||
|
||||
public void setColor(int primaryColor)
|
||||
{
|
||||
this.primaryColor = primaryColor;
|
||||
postInvalidate();
|
||||
}
|
||||
|
||||
public void setIsTransparencyEnabled(boolean enabled)
|
||||
{
|
||||
this.isTransparencyEnabled = enabled;
|
||||
postInvalidate();
|
||||
}
|
||||
|
||||
public void setTarget(double target)
|
||||
{
|
||||
this.target = target;
|
||||
postInvalidate();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas)
|
||||
{
|
||||
super.onDraw(canvas);
|
||||
Canvas activeCanvas;
|
||||
|
||||
if (isTransparencyEnabled)
|
||||
{
|
||||
if (drawingCache == null) initCache(getWidth(), getHeight());
|
||||
|
||||
activeCanvas = cacheCanvas;
|
||||
drawingCache.eraseColor(Color.TRANSPARENT);
|
||||
}
|
||||
else
|
||||
{
|
||||
activeCanvas = canvas;
|
||||
}
|
||||
|
||||
if (checkmarks == null) return;
|
||||
|
||||
rect.set(0, 0, nColumns * columnWidth, columnHeight);
|
||||
rect.offset(0, paddingTop);
|
||||
|
||||
drawGrid(activeCanvas, rect);
|
||||
|
||||
pText.setColor(textColor);
|
||||
pGraph.setColor(primaryColor);
|
||||
prevRect.setEmpty();
|
||||
|
||||
previousMonthText = "";
|
||||
previousYearText = "";
|
||||
skipYear = 0;
|
||||
|
||||
for (int k = 0; k < nColumns; k++)
|
||||
{
|
||||
int offset = nColumns - k - 1 + getDataOffset();
|
||||
if (offset >= checkmarks.size()) continue;
|
||||
|
||||
double value = checkmarks.get(offset).getValue();
|
||||
Timestamp timestamp = checkmarks.get(offset).getTimestamp();
|
||||
int height = (int) (columnHeight * value / maxValue);
|
||||
|
||||
rect.set(0, 0, baseSize, height);
|
||||
rect.offset(k * columnWidth + (columnWidth - baseSize) / 2,
|
||||
paddingTop + columnHeight - height);
|
||||
|
||||
drawValue(activeCanvas, rect, value);
|
||||
drawBar(activeCanvas, rect, value);
|
||||
|
||||
prevRect.set(rect);
|
||||
rect.set(0, 0, columnWidth, columnHeight);
|
||||
rect.offset(k * columnWidth, paddingTop);
|
||||
|
||||
drawFooter(activeCanvas, rect, timestamp);
|
||||
}
|
||||
|
||||
if (activeCanvas != canvas) canvas.drawBitmap(drawingCache, 0, 0, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
|
||||
{
|
||||
int width = MeasureSpec.getSize(widthMeasureSpec);
|
||||
int height = MeasureSpec.getSize(heightMeasureSpec);
|
||||
setMeasuredDimension(width, height);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSizeChanged(int width,
|
||||
int height,
|
||||
int oldWidth,
|
||||
int oldHeight)
|
||||
{
|
||||
if (height < 9) height = 200;
|
||||
|
||||
float maxTextSize = getDimension(getContext(), R.dimen.tinyTextSize);
|
||||
float textSize = height * 0.06f;
|
||||
pText.setTextSize(Math.min(textSize, maxTextSize));
|
||||
em = pText.getFontSpacing();
|
||||
|
||||
int footerHeight = (int) (3 * em);
|
||||
paddingTop = (int) (em);
|
||||
|
||||
baseSize = (height - footerHeight - paddingTop) / 12;
|
||||
columnWidth = baseSize;
|
||||
columnWidth = Math.max(columnWidth, getMaxDayWidth() * 1.5f);
|
||||
columnWidth = Math.max(columnWidth, getMaxMonthWidth() * 1.2f);
|
||||
|
||||
nColumns = (int) (width / columnWidth);
|
||||
columnWidth = (float) width / nColumns;
|
||||
setScrollerBucketSize((int) columnWidth);
|
||||
|
||||
columnHeight = 12 * baseSize;
|
||||
|
||||
float minStrokeWidth = dpToPixels(getContext(), 1);
|
||||
pGraph.setTextSize(baseSize * 0.5f);
|
||||
pGraph.setStrokeWidth(baseSize * 0.1f);
|
||||
pGrid.setStrokeWidth(Math.min(minStrokeWidth, baseSize * 0.05f));
|
||||
|
||||
if (isTransparencyEnabled) initCache(width, height);
|
||||
}
|
||||
|
||||
private void drawBar(Canvas canvas, RectF rect, double value)
|
||||
{
|
||||
float margin = baseSize * 0.225f;
|
||||
|
||||
int color = textColor;
|
||||
if (value / 1000 >= target) color = primaryColor;
|
||||
|
||||
rect.inset(-margin, 0);
|
||||
setModeOrColor(pGraph, XFERMODE_CLEAR, backgroundColor);
|
||||
canvas.drawRect(rect, pGraph);
|
||||
|
||||
rect.inset(margin, 0);
|
||||
setModeOrColor(pGraph, XFERMODE_SRC, color);
|
||||
canvas.drawRect(rect, pGraph);
|
||||
|
||||
if (isTransparencyEnabled) pGraph.setXfermode(XFERMODE_SRC);
|
||||
}
|
||||
|
||||
private void drawFooter(Canvas canvas, RectF rect, Timestamp currentDate)
|
||||
{
|
||||
String yearText = dfYear.format(currentDate.toJavaDate());
|
||||
String monthText = dfMonth.format(currentDate.toJavaDate());
|
||||
String dayText = dfDay.format(currentDate.toJavaDate());
|
||||
|
||||
GregorianCalendar calendar = currentDate.toCalendar();
|
||||
pText.setColor(textColor);
|
||||
|
||||
String text;
|
||||
int year = calendar.get(Calendar.YEAR);
|
||||
|
||||
boolean shouldPrintYear = true;
|
||||
if (yearText.equals(previousYearText)) shouldPrintYear = false;
|
||||
|
||||
if (skipYear > 0)
|
||||
{
|
||||
skipYear--;
|
||||
shouldPrintYear = false;
|
||||
}
|
||||
|
||||
if (bucketSize >= 365) shouldPrintYear = true;
|
||||
|
||||
if (shouldPrintYear)
|
||||
{
|
||||
previousYearText = yearText;
|
||||
previousMonthText = "";
|
||||
|
||||
pText.setTextAlign(Paint.Align.CENTER);
|
||||
canvas.drawText(yearText, rect.centerX(), rect.bottom + em * 2.2f, pText);
|
||||
skipYear = 1;
|
||||
|
||||
|
||||
}
|
||||
|
||||
if (bucketSize < 365)
|
||||
{
|
||||
if (!monthText.equals(previousMonthText))
|
||||
{
|
||||
previousMonthText = monthText;
|
||||
text = monthText;
|
||||
}
|
||||
else
|
||||
{
|
||||
text = dayText;
|
||||
}
|
||||
|
||||
canvas.drawText(text, rect.centerX(), rect.bottom + em * 1.2f,
|
||||
pText);
|
||||
}
|
||||
}
|
||||
|
||||
private void drawGrid(Canvas canvas, RectF rGrid)
|
||||
{
|
||||
int nRows = 5;
|
||||
float rowHeight = rGrid.height() / nRows;
|
||||
|
||||
pText.setColor(textColor);
|
||||
pGrid.setColor(gridColor);
|
||||
|
||||
for (int i = 0; i < nRows; i++)
|
||||
{
|
||||
canvas.drawLine(rGrid.left, rGrid.top, rGrid.right, rGrid.top,
|
||||
pGrid);
|
||||
rGrid.offset(0, rowHeight);
|
||||
}
|
||||
|
||||
canvas.drawLine(rGrid.left, rGrid.top, rGrid.right, rGrid.top, pGrid);
|
||||
}
|
||||
|
||||
private void drawValue(Canvas canvas, RectF rect, double value)
|
||||
{
|
||||
if (value == 0) return;
|
||||
|
||||
int activeColor = textColor;
|
||||
if (value / 1000 >= target)
|
||||
activeColor = primaryColor;
|
||||
|
||||
String label = NumberButtonViewKt.toShortString(value / 1000);
|
||||
Rect rText = new Rect();
|
||||
pText.getTextBounds(label, 0, label.length(), rText);
|
||||
|
||||
float offset = 0.5f * em;
|
||||
float x = rect.centerX();
|
||||
float y = rect.top - offset;
|
||||
int cap = (int) (-0.1f * em);
|
||||
|
||||
rText.offset((int) x, (int) y);
|
||||
rText.offset(-rText.width() / 2, 0);
|
||||
rText.inset(3 * cap, cap);
|
||||
|
||||
setModeOrColor(pText, XFERMODE_CLEAR, backgroundColor);
|
||||
canvas.drawRect(rText, pText);
|
||||
|
||||
setModeOrColor(pText, XFERMODE_SRC, activeColor);
|
||||
canvas.drawText(label, x, y, pText);
|
||||
}
|
||||
|
||||
private float getMaxDayWidth()
|
||||
{
|
||||
float maxDayWidth = 0;
|
||||
GregorianCalendar day = DateUtils.getStartOfTodayCalendar();
|
||||
|
||||
for (int i = 0; i < 28; i++)
|
||||
{
|
||||
day.set(Calendar.DAY_OF_MONTH, i);
|
||||
float monthWidth = pText.measureText(dfMonth.format(day.getTime()));
|
||||
maxDayWidth = Math.max(maxDayWidth, monthWidth);
|
||||
}
|
||||
|
||||
return maxDayWidth;
|
||||
}
|
||||
|
||||
private float getMaxMonthWidth()
|
||||
{
|
||||
float maxMonthWidth = 0;
|
||||
GregorianCalendar day = DateUtils.getStartOfTodayCalendar();
|
||||
|
||||
for (int i = 0; i < 12; i++)
|
||||
{
|
||||
day.set(Calendar.MONTH, i);
|
||||
float monthWidth = pText.measureText(dfMonth.format(day.getTime()));
|
||||
maxMonthWidth = Math.max(maxMonthWidth, monthWidth);
|
||||
}
|
||||
|
||||
return maxMonthWidth;
|
||||
}
|
||||
|
||||
private void init()
|
||||
{
|
||||
initPaints();
|
||||
initColors();
|
||||
initDateFormats();
|
||||
initRects();
|
||||
}
|
||||
|
||||
private void initCache(int width, int height)
|
||||
{
|
||||
if (drawingCache != null) drawingCache.recycle();
|
||||
drawingCache =
|
||||
Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
|
||||
cacheCanvas = new Canvas(drawingCache);
|
||||
}
|
||||
|
||||
private void initColors()
|
||||
{
|
||||
StyledResources res = new StyledResources(getContext());
|
||||
|
||||
primaryColor = Color.BLACK;
|
||||
textColor = res.getColor(R.attr.mediumContrastTextColor);
|
||||
gridColor = res.getColor(R.attr.lowContrastTextColor);
|
||||
backgroundColor = res.getColor(R.attr.cardBackgroundColor);
|
||||
}
|
||||
|
||||
private void initDateFormats()
|
||||
{
|
||||
if (isInEditMode())
|
||||
{
|
||||
dfYear = new SimpleDateFormat("yyyy", Locale.US);
|
||||
dfMonth = new SimpleDateFormat("MMM", Locale.US);
|
||||
dfDay = new SimpleDateFormat("d", Locale.US);
|
||||
return;
|
||||
}
|
||||
|
||||
dfYear = AndroidDateFormats.fromSkeleton("yyyy");
|
||||
dfMonth = AndroidDateFormats.fromSkeleton("MMM");
|
||||
dfDay = AndroidDateFormats.fromSkeleton("d");
|
||||
}
|
||||
|
||||
private void initPaints()
|
||||
{
|
||||
pText = new Paint();
|
||||
pText.setAntiAlias(true);
|
||||
pText.setTextAlign(Paint.Align.CENTER);
|
||||
|
||||
pGraph = new Paint();
|
||||
pGraph.setTextAlign(Paint.Align.CENTER);
|
||||
pGraph.setAntiAlias(true);
|
||||
|
||||
pGrid = new Paint();
|
||||
pGrid.setAntiAlias(true);
|
||||
}
|
||||
|
||||
private void initRects()
|
||||
{
|
||||
rect = new RectF();
|
||||
prevRect = new RectF();
|
||||
}
|
||||
|
||||
private void setModeOrColor(Paint p, PorterDuffXfermode mode, int color)
|
||||
{
|
||||
if (isTransparencyEnabled) p.setXfermode(mode);
|
||||
else p.setColor(color);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.common.views;
|
||||
|
||||
import android.os.*;
|
||||
import android.support.v4.os.*;
|
||||
|
||||
public class BundleSavedState extends android.support.v4.view.AbsSavedState
|
||||
{
|
||||
public static final Parcelable.Creator<BundleSavedState> CREATOR =
|
||||
ParcelableCompat.newCreator(
|
||||
new ParcelableCompatCreatorCallbacks<BundleSavedState>()
|
||||
{
|
||||
@Override
|
||||
public BundleSavedState createFromParcel(Parcel source,
|
||||
ClassLoader loader)
|
||||
{
|
||||
return new BundleSavedState(source, loader);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BundleSavedState[] newArray(int size)
|
||||
{
|
||||
return new BundleSavedState[size];
|
||||
}
|
||||
});
|
||||
|
||||
public final Bundle bundle;
|
||||
|
||||
public BundleSavedState(Parcelable superState, Bundle bundle)
|
||||
{
|
||||
super(superState);
|
||||
this.bundle = bundle;
|
||||
}
|
||||
|
||||
public BundleSavedState(Parcel source, ClassLoader loader)
|
||||
{
|
||||
super(source, loader);
|
||||
this.bundle = source.readBundle(loader);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel out, int flags)
|
||||
{
|
||||
super.writeToParcel(out, flags);
|
||||
out.writeBundle(bundle);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,339 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.common.views;
|
||||
|
||||
import android.content.*;
|
||||
import android.graphics.*;
|
||||
import android.support.annotation.*;
|
||||
import android.util.*;
|
||||
|
||||
import org.isoron.androidbase.utils.*;
|
||||
import org.isoron.uhabits.*;
|
||||
import org.isoron.uhabits.core.models.*;
|
||||
import org.isoron.uhabits.core.utils.*;
|
||||
import org.isoron.uhabits.utils.*;
|
||||
|
||||
import java.text.*;
|
||||
import java.util.*;
|
||||
|
||||
public class FrequencyChart extends ScrollableChart
|
||||
{
|
||||
private Paint pGrid;
|
||||
|
||||
private float em;
|
||||
|
||||
private SimpleDateFormat dfMonth;
|
||||
|
||||
private SimpleDateFormat dfYear;
|
||||
|
||||
private Paint pText, pGraph;
|
||||
|
||||
private RectF rect, prevRect;
|
||||
|
||||
private int baseSize;
|
||||
|
||||
private int paddingTop;
|
||||
|
||||
private float columnWidth;
|
||||
|
||||
private int columnHeight;
|
||||
|
||||
private int nColumns;
|
||||
|
||||
private int textColor;
|
||||
|
||||
private int gridColor;
|
||||
|
||||
private int[] colors;
|
||||
|
||||
private int primaryColor;
|
||||
|
||||
private boolean isBackgroundTransparent;
|
||||
|
||||
@NonNull
|
||||
private HashMap<Timestamp, Integer[]> frequency;
|
||||
private int maxFreq;
|
||||
|
||||
public FrequencyChart(Context context)
|
||||
{
|
||||
super(context);
|
||||
init();
|
||||
}
|
||||
|
||||
public FrequencyChart(Context context, AttributeSet attrs)
|
||||
{
|
||||
super(context, attrs);
|
||||
this.frequency = new HashMap<>();
|
||||
init();
|
||||
}
|
||||
|
||||
public void setColor(int color)
|
||||
{
|
||||
this.primaryColor = color;
|
||||
initColors();
|
||||
postInvalidate();
|
||||
}
|
||||
|
||||
public void setFrequency(HashMap<Timestamp, Integer[]> frequency)
|
||||
{
|
||||
this.frequency = frequency;
|
||||
maxFreq = getMaxFreq(frequency);
|
||||
postInvalidate();
|
||||
}
|
||||
|
||||
private int getMaxFreq(HashMap<Timestamp, Integer[]> frequency)
|
||||
{
|
||||
int maxValue = 1;
|
||||
|
||||
for (Integer[] values : frequency.values())
|
||||
for (Integer value : values)
|
||||
maxValue = Math.max(value, maxValue);
|
||||
|
||||
return maxValue;
|
||||
}
|
||||
|
||||
public void setIsBackgroundTransparent(boolean isBackgroundTransparent)
|
||||
{
|
||||
this.isBackgroundTransparent = isBackgroundTransparent;
|
||||
initColors();
|
||||
}
|
||||
|
||||
protected void initPaints()
|
||||
{
|
||||
pText = new Paint();
|
||||
pText.setAntiAlias(true);
|
||||
|
||||
pGraph = new Paint();
|
||||
pGraph.setTextAlign(Paint.Align.CENTER);
|
||||
pGraph.setAntiAlias(true);
|
||||
|
||||
pGrid = new Paint();
|
||||
pGrid.setAntiAlias(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas)
|
||||
{
|
||||
super.onDraw(canvas);
|
||||
|
||||
rect.set(0, 0, nColumns * columnWidth, columnHeight);
|
||||
rect.offset(0, paddingTop);
|
||||
|
||||
drawGrid(canvas, rect);
|
||||
|
||||
pText.setTextAlign(Paint.Align.CENTER);
|
||||
pText.setColor(textColor);
|
||||
pGraph.setColor(primaryColor);
|
||||
prevRect.setEmpty();
|
||||
|
||||
GregorianCalendar currentDate = DateUtils.getStartOfTodayCalendar();
|
||||
|
||||
currentDate.set(Calendar.DAY_OF_MONTH, 1);
|
||||
currentDate.add(Calendar.MONTH, -nColumns + 2 - getDataOffset());
|
||||
|
||||
for (int i = 0; i < nColumns - 1; i++)
|
||||
{
|
||||
rect.set(0, 0, columnWidth, columnHeight);
|
||||
rect.offset(i * columnWidth, 0);
|
||||
|
||||
drawColumn(canvas, rect, currentDate);
|
||||
currentDate.add(Calendar.MONTH, 1);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
|
||||
{
|
||||
int width = MeasureSpec.getSize(widthMeasureSpec);
|
||||
int height = MeasureSpec.getSize(heightMeasureSpec);
|
||||
setMeasuredDimension(width, height);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSizeChanged(int width,
|
||||
int height,
|
||||
int oldWidth,
|
||||
int oldHeight)
|
||||
{
|
||||
if (height < 9) height = 200;
|
||||
|
||||
baseSize = height / 8;
|
||||
setScrollerBucketSize(baseSize);
|
||||
|
||||
pText.setTextSize(baseSize * 0.4f);
|
||||
pGraph.setTextSize(baseSize * 0.4f);
|
||||
pGraph.setStrokeWidth(baseSize * 0.1f);
|
||||
pGrid.setStrokeWidth(baseSize * 0.05f);
|
||||
em = pText.getFontSpacing();
|
||||
|
||||
columnWidth = baseSize;
|
||||
columnWidth = Math.max(columnWidth, getMaxMonthWidth() * 1.2f);
|
||||
|
||||
columnHeight = 8 * baseSize;
|
||||
nColumns = (int) (width / columnWidth);
|
||||
paddingTop = 0;
|
||||
}
|
||||
|
||||
private void drawColumn(Canvas canvas, RectF rect, GregorianCalendar date)
|
||||
{
|
||||
Integer values[] = frequency.get(new Timestamp(date));
|
||||
float rowHeight = rect.height() / 8.0f;
|
||||
prevRect.set(rect);
|
||||
|
||||
Integer[] localeWeekdayList = DateUtils.getLocaleWeekdayList();
|
||||
for (int j = 0; j < localeWeekdayList.length; j++)
|
||||
{
|
||||
rect.set(0, 0, baseSize, baseSize);
|
||||
rect.offset(prevRect.left, prevRect.top + baseSize * j);
|
||||
|
||||
int i = localeWeekdayList[j] % 7;
|
||||
if (values != null) drawMarker(canvas, rect, values[i]);
|
||||
|
||||
rect.offset(0, rowHeight);
|
||||
}
|
||||
|
||||
drawFooter(canvas, rect, date);
|
||||
}
|
||||
|
||||
private void drawFooter(Canvas canvas, RectF rect, GregorianCalendar date)
|
||||
{
|
||||
Date time = date.getTime();
|
||||
|
||||
canvas.drawText(dfMonth.format(time), rect.centerX(),
|
||||
rect.centerY() - 0.1f * em, pText);
|
||||
|
||||
if (date.get(Calendar.MONTH) == 1)
|
||||
canvas.drawText(dfYear.format(time), rect.centerX(),
|
||||
rect.centerY() + 0.9f * em, pText);
|
||||
}
|
||||
|
||||
private void drawGrid(Canvas canvas, RectF rGrid)
|
||||
{
|
||||
int nRows = 7;
|
||||
float rowHeight = rGrid.height() / (nRows + 1);
|
||||
|
||||
pText.setTextAlign(Paint.Align.LEFT);
|
||||
pText.setColor(textColor);
|
||||
pGrid.setColor(gridColor);
|
||||
|
||||
for (String day : DateUtils.getLocaleDayNames(Calendar.SHORT))
|
||||
{
|
||||
canvas.drawText(day, rGrid.right - columnWidth,
|
||||
rGrid.top + rowHeight / 2 + 0.25f * em, pText);
|
||||
|
||||
pGrid.setStrokeWidth(1f);
|
||||
canvas.drawLine(rGrid.left, rGrid.top, rGrid.right, rGrid.top,
|
||||
pGrid);
|
||||
|
||||
rGrid.offset(0, rowHeight);
|
||||
}
|
||||
|
||||
canvas.drawLine(rGrid.left, rGrid.top, rGrid.right, rGrid.top, pGrid);
|
||||
}
|
||||
|
||||
private void drawMarker(Canvas canvas, RectF rect, Integer value)
|
||||
{
|
||||
float padding = rect.height() * 0.2f;
|
||||
// maximal allowed mark radius
|
||||
float maxRadius = (rect.height() - 2 * padding) / 2.0f;
|
||||
// the real mark radius is scaled down by a factor depending on the maximal frequency
|
||||
float scale = 1.0f/maxFreq * value;
|
||||
float radius = maxRadius * scale;
|
||||
|
||||
int colorIndex = Math.min(colors.length - 1, Math.round((colors.length - 1) * scale));
|
||||
pGraph.setColor(colors[colorIndex]);
|
||||
canvas.drawCircle(rect.centerX(), rect.centerY(), radius, pGraph);
|
||||
}
|
||||
|
||||
private float getMaxMonthWidth()
|
||||
{
|
||||
float maxMonthWidth = 0;
|
||||
GregorianCalendar day = DateUtils.getStartOfTodayCalendar();
|
||||
|
||||
for (int i = 0; i < 12; i++)
|
||||
{
|
||||
day.set(Calendar.MONTH, i);
|
||||
float monthWidth = pText.measureText(dfMonth.format(day.getTime()));
|
||||
maxMonthWidth = Math.max(maxMonthWidth, monthWidth);
|
||||
}
|
||||
|
||||
return maxMonthWidth;
|
||||
}
|
||||
|
||||
private void init()
|
||||
{
|
||||
initPaints();
|
||||
initColors();
|
||||
initDateFormats();
|
||||
initRects();
|
||||
}
|
||||
|
||||
private void initColors()
|
||||
{
|
||||
StyledResources res = new StyledResources(getContext());
|
||||
textColor = res.getColor(R.attr.mediumContrastTextColor);
|
||||
gridColor = res.getColor(R.attr.lowContrastTextColor);
|
||||
|
||||
colors = new int[4];
|
||||
colors[0] = gridColor;
|
||||
colors[3] = primaryColor;
|
||||
colors[1] = ColorUtils.mixColors(colors[0], colors[3], 0.66f);
|
||||
colors[2] = ColorUtils.mixColors(colors[0], colors[3], 0.33f);
|
||||
}
|
||||
|
||||
private void initDateFormats()
|
||||
{
|
||||
if (isInEditMode())
|
||||
{
|
||||
dfMonth = new SimpleDateFormat("MMM", Locale.getDefault());
|
||||
dfYear = new SimpleDateFormat("yyyy", Locale.getDefault());
|
||||
}
|
||||
else
|
||||
{
|
||||
dfMonth = AndroidDateFormats.fromSkeleton("MMM");
|
||||
dfYear = AndroidDateFormats.fromSkeleton("yyyy");
|
||||
}
|
||||
}
|
||||
|
||||
private void initRects()
|
||||
{
|
||||
rect = new RectF();
|
||||
prevRect = new RectF();
|
||||
}
|
||||
|
||||
public void populateWithRandomData()
|
||||
{
|
||||
GregorianCalendar date = DateUtils.getStartOfTodayCalendar();
|
||||
date.set(Calendar.DAY_OF_MONTH, 1);
|
||||
Random rand = new Random();
|
||||
frequency.clear();
|
||||
|
||||
for (int i = 0; i < 40; i++)
|
||||
{
|
||||
Integer values[] = new Integer[7];
|
||||
for (int j = 0; j < 7; j++)
|
||||
values[j] = rand.nextInt(5);
|
||||
|
||||
frequency.put(new Timestamp(date), values);
|
||||
date.add(Calendar.MONTH, -1);
|
||||
}
|
||||
maxFreq = getMaxFreq(frequency);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.common.views;
|
||||
|
||||
import org.isoron.uhabits.core.models.Habit;
|
||||
|
||||
public interface HabitChart
|
||||
{
|
||||
void setHabit(Habit habit);
|
||||
|
||||
void refreshData();
|
||||
}
|
||||
@@ -0,0 +1,486 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.common.views;
|
||||
|
||||
import android.content.*;
|
||||
import android.graphics.*;
|
||||
import android.graphics.Paint.*;
|
||||
import android.support.annotation.*;
|
||||
import android.util.*;
|
||||
import android.view.*;
|
||||
|
||||
import org.isoron.androidbase.utils.*;
|
||||
import org.isoron.uhabits.*;
|
||||
import org.isoron.uhabits.core.models.*;
|
||||
import org.isoron.uhabits.core.utils.*;
|
||||
import org.isoron.uhabits.utils.*;
|
||||
|
||||
import java.text.*;
|
||||
import java.util.*;
|
||||
|
||||
import static org.isoron.androidbase.utils.InterfaceUtils.*;
|
||||
import static org.isoron.uhabits.core.models.Checkmark.*;
|
||||
|
||||
public class HistoryChart extends ScrollableChart
|
||||
{
|
||||
private int[] checkmarks;
|
||||
|
||||
private int target;
|
||||
|
||||
private Paint pSquareBg, pSquareFg, pTextHeader;
|
||||
|
||||
private float squareSpacing;
|
||||
|
||||
private float squareTextOffset;
|
||||
|
||||
private float headerTextOffset;
|
||||
|
||||
private float columnWidth;
|
||||
|
||||
private float columnHeight;
|
||||
|
||||
private int nColumns;
|
||||
|
||||
private SimpleDateFormat dfMonth;
|
||||
|
||||
private SimpleDateFormat dfYear;
|
||||
|
||||
private Calendar baseDate;
|
||||
|
||||
private int nDays;
|
||||
|
||||
/**
|
||||
* 0-based-position of today in the column
|
||||
*/
|
||||
private int todayPositionInColumn;
|
||||
|
||||
private int colors[];
|
||||
|
||||
private RectF baseLocation;
|
||||
|
||||
private int primaryColor;
|
||||
|
||||
private boolean isBackgroundTransparent;
|
||||
|
||||
private int textColor;
|
||||
|
||||
private int reverseTextColor;
|
||||
|
||||
private boolean isEditable;
|
||||
|
||||
private String previousMonth;
|
||||
|
||||
private String previousYear;
|
||||
|
||||
private float headerOverflow = 0;
|
||||
|
||||
private boolean isNumerical = false;
|
||||
|
||||
@NonNull
|
||||
private Controller controller;
|
||||
|
||||
public HistoryChart(Context context)
|
||||
{
|
||||
super(context);
|
||||
init();
|
||||
}
|
||||
|
||||
public HistoryChart(Context context, AttributeSet attrs)
|
||||
{
|
||||
super(context, attrs);
|
||||
init();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLongPress(MotionEvent e)
|
||||
{
|
||||
onSingleTapUp(e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onSingleTapUp(MotionEvent e)
|
||||
{
|
||||
if (!isEditable) return false;
|
||||
|
||||
performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP);
|
||||
float x, y;
|
||||
|
||||
try
|
||||
{
|
||||
int pointerId = e.getPointerId(0);
|
||||
x = e.getX(pointerId);
|
||||
y = e.getY(pointerId);
|
||||
}
|
||||
catch (RuntimeException ex)
|
||||
{
|
||||
// Android often throws IllegalArgumentException here. Apparently,
|
||||
// the pointer id may become invalid shortly after calling
|
||||
// e.getPointerId.
|
||||
return false;
|
||||
}
|
||||
|
||||
final Timestamp timestamp = positionToTimestamp(x, y);
|
||||
if (timestamp == null) return false;
|
||||
|
||||
Timestamp today = DateUtils.getToday();
|
||||
int offset = timestamp.daysUntil(today);
|
||||
if (offset < checkmarks.length)
|
||||
{
|
||||
boolean isChecked = checkmarks[offset] == CHECKED_EXPLICITLY;
|
||||
checkmarks[offset] = (isChecked ? UNCHECKED : CHECKED_EXPLICITLY);
|
||||
}
|
||||
|
||||
controller.onToggleCheckmark(timestamp);
|
||||
postInvalidate();
|
||||
return true;
|
||||
}
|
||||
|
||||
public void populateWithRandomData()
|
||||
{
|
||||
Random random = new Random();
|
||||
checkmarks = new int[100];
|
||||
|
||||
for (int i = 0; i < 100; i++)
|
||||
if (random.nextFloat() < 0.3) checkmarks[i] = 2;
|
||||
|
||||
for (int i = 0; i < 100 - 7; i++)
|
||||
{
|
||||
int count = 0;
|
||||
for (int j = 0; j < 7; j++)
|
||||
if (checkmarks[i + j] != 0) count++;
|
||||
|
||||
if (count >= 3) checkmarks[i] = Math.max(checkmarks[i], 1);
|
||||
}
|
||||
}
|
||||
|
||||
public void setCheckmarks(int[] checkmarks)
|
||||
{
|
||||
this.checkmarks = checkmarks;
|
||||
postInvalidate();
|
||||
}
|
||||
|
||||
public void setColor(int color)
|
||||
{
|
||||
this.primaryColor = color;
|
||||
initColors();
|
||||
postInvalidate();
|
||||
}
|
||||
|
||||
public void setController(@NonNull Controller controller)
|
||||
{
|
||||
this.controller = controller;
|
||||
}
|
||||
|
||||
public void setNumerical(boolean numerical)
|
||||
{
|
||||
isNumerical = numerical;
|
||||
}
|
||||
|
||||
public void setIsBackgroundTransparent(boolean isBackgroundTransparent)
|
||||
{
|
||||
this.isBackgroundTransparent = isBackgroundTransparent;
|
||||
initColors();
|
||||
}
|
||||
|
||||
public void setIsEditable(boolean isEditable)
|
||||
{
|
||||
this.isEditable = isEditable;
|
||||
}
|
||||
|
||||
public void setTarget(int target)
|
||||
{
|
||||
this.target = target;
|
||||
postInvalidate();
|
||||
}
|
||||
|
||||
protected void initPaints()
|
||||
{
|
||||
pTextHeader = new Paint();
|
||||
pTextHeader.setTextAlign(Align.LEFT);
|
||||
pTextHeader.setAntiAlias(true);
|
||||
|
||||
pSquareBg = new Paint();
|
||||
|
||||
pSquareFg = new Paint();
|
||||
pSquareFg.setAntiAlias(true);
|
||||
pSquareFg.setTextAlign(Align.CENTER);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas)
|
||||
{
|
||||
super.onDraw(canvas);
|
||||
|
||||
baseLocation.set(0, 0, columnWidth - squareSpacing,
|
||||
columnWidth - squareSpacing);
|
||||
baseLocation.offset(getPaddingLeft(), getPaddingTop());
|
||||
|
||||
headerOverflow = 0;
|
||||
previousMonth = "";
|
||||
previousYear = "";
|
||||
pTextHeader.setColor(textColor);
|
||||
|
||||
updateDate();
|
||||
GregorianCalendar currentDate = (GregorianCalendar) baseDate.clone();
|
||||
|
||||
for (int column = 0; column < nColumns - 1; column++)
|
||||
{
|
||||
drawColumn(canvas, baseLocation, currentDate, column);
|
||||
baseLocation.offset(columnWidth, -columnHeight);
|
||||
}
|
||||
|
||||
drawAxis(canvas, baseLocation);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
|
||||
{
|
||||
int width = MeasureSpec.getSize(widthMeasureSpec);
|
||||
int height = MeasureSpec.getSize(heightMeasureSpec);
|
||||
setMeasuredDimension(width, height);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSizeChanged(int width,
|
||||
int height,
|
||||
int oldWidth,
|
||||
int oldHeight)
|
||||
{
|
||||
if (height < 8) height = 200;
|
||||
float baseSize = height / 8.0f;
|
||||
setScrollerBucketSize((int) baseSize);
|
||||
|
||||
squareSpacing = dpToPixels(getContext(), 1.0f);
|
||||
float maxTextSize = getDimension(getContext(), R.dimen.regularTextSize);
|
||||
float textSize = height * 0.06f;
|
||||
textSize = Math.min(textSize, maxTextSize);
|
||||
|
||||
pSquareFg.setTextSize(textSize);
|
||||
pTextHeader.setTextSize(textSize);
|
||||
squareTextOffset = pSquareFg.getFontSpacing() * 0.4f;
|
||||
headerTextOffset = pTextHeader.getFontSpacing() * 0.3f;
|
||||
|
||||
float rightLabelWidth = getWeekdayLabelWidth() + headerTextOffset;
|
||||
float horizontalPadding = getPaddingRight() + getPaddingLeft();
|
||||
|
||||
columnWidth = baseSize;
|
||||
columnHeight = 8 * baseSize;
|
||||
nColumns =
|
||||
(int) ((width - rightLabelWidth - horizontalPadding) / baseSize) +
|
||||
1;
|
||||
|
||||
updateDate();
|
||||
}
|
||||
|
||||
private void drawAxis(Canvas canvas, RectF location)
|
||||
{
|
||||
float verticalOffset = pTextHeader.getFontSpacing() * 0.4f;
|
||||
|
||||
for (String day : DateUtils.getLocaleDayNames(Calendar.SHORT))
|
||||
{
|
||||
location.offset(0, columnWidth);
|
||||
canvas.drawText(day, location.left + headerTextOffset,
|
||||
location.centerY() + verticalOffset, pTextHeader);
|
||||
}
|
||||
}
|
||||
|
||||
private void drawColumn(Canvas canvas,
|
||||
RectF location,
|
||||
GregorianCalendar date,
|
||||
int column)
|
||||
{
|
||||
drawColumnHeader(canvas, location, date);
|
||||
location.offset(0, columnWidth);
|
||||
|
||||
for (int j = 0; j < 7; j++)
|
||||
{
|
||||
if (!(column == nColumns - 2 && getDataOffset() == 0 &&
|
||||
j > todayPositionInColumn))
|
||||
{
|
||||
int checkmarkOffset =
|
||||
getDataOffset() * 7 + nDays - 7 * (column + 1) +
|
||||
todayPositionInColumn - j;
|
||||
drawSquare(canvas, location, date, checkmarkOffset);
|
||||
}
|
||||
|
||||
date.add(Calendar.DAY_OF_MONTH, 1);
|
||||
location.offset(0, columnWidth);
|
||||
}
|
||||
}
|
||||
|
||||
private void drawColumnHeader(Canvas canvas,
|
||||
RectF location,
|
||||
GregorianCalendar date)
|
||||
{
|
||||
String month = dfMonth.format(date.getTime());
|
||||
String year = dfYear.format(date.getTime());
|
||||
|
||||
String text = null;
|
||||
if (!month.equals(previousMonth)) text = previousMonth = month;
|
||||
else if (!year.equals(previousYear)) text = previousYear = year;
|
||||
|
||||
if (text != null)
|
||||
{
|
||||
canvas.drawText(text, location.left + headerOverflow,
|
||||
location.bottom - headerTextOffset, pTextHeader);
|
||||
headerOverflow +=
|
||||
pTextHeader.measureText(text) + columnWidth * 0.2f;
|
||||
}
|
||||
|
||||
headerOverflow = Math.max(0, headerOverflow - columnWidth);
|
||||
}
|
||||
|
||||
private void drawSquare(Canvas canvas,
|
||||
RectF location,
|
||||
GregorianCalendar date,
|
||||
int checkmarkOffset)
|
||||
{
|
||||
if (checkmarkOffset >= checkmarks.length) pSquareBg.setColor(colors[0]);
|
||||
else
|
||||
{
|
||||
int checkmark = checkmarks[checkmarkOffset];
|
||||
if(checkmark == 0) pSquareBg.setColor(colors[0]);
|
||||
else if(checkmark < target)
|
||||
{
|
||||
pSquareBg.setColor(isNumerical ? textColor : colors[1]);
|
||||
}
|
||||
else pSquareBg.setColor(colors[2]);
|
||||
}
|
||||
|
||||
pSquareFg.setColor(reverseTextColor);
|
||||
canvas.drawRect(location, pSquareBg);
|
||||
String text = Integer.toString(date.get(Calendar.DAY_OF_MONTH));
|
||||
canvas.drawText(text, location.centerX(),
|
||||
location.centerY() + squareTextOffset, pSquareFg);
|
||||
}
|
||||
|
||||
private float getWeekdayLabelWidth()
|
||||
{
|
||||
float width = 0;
|
||||
|
||||
for (String w : DateUtils.getLocaleDayNames(Calendar.SHORT))
|
||||
width = Math.max(width, pSquareFg.measureText(w));
|
||||
|
||||
return width;
|
||||
}
|
||||
|
||||
private void init()
|
||||
{
|
||||
isEditable = false;
|
||||
checkmarks = new int[0];
|
||||
controller = new Controller() {};
|
||||
target = 2;
|
||||
|
||||
initColors();
|
||||
initPaints();
|
||||
initDateFormats();
|
||||
initRects();
|
||||
}
|
||||
|
||||
private void initColors()
|
||||
{
|
||||
StyledResources res = new StyledResources(getContext());
|
||||
|
||||
if (isBackgroundTransparent)
|
||||
primaryColor = ColorUtils.setMinValue(primaryColor, 0.75f);
|
||||
|
||||
int red = Color.red(primaryColor);
|
||||
int green = Color.green(primaryColor);
|
||||
int blue = Color.blue(primaryColor);
|
||||
|
||||
if (isBackgroundTransparent)
|
||||
{
|
||||
colors = new int[3];
|
||||
colors[0] = Color.argb(16, 255, 255, 255);
|
||||
colors[1] = Color.argb(128, red, green, blue);
|
||||
colors[2] = primaryColor;
|
||||
textColor = Color.WHITE;
|
||||
reverseTextColor = Color.WHITE;
|
||||
}
|
||||
else
|
||||
{
|
||||
colors = new int[3];
|
||||
colors[0] = res.getColor(R.attr.lowContrastTextColor);
|
||||
colors[1] = Color.argb(127, red, green, blue);
|
||||
colors[2] = primaryColor;
|
||||
textColor = res.getColor(R.attr.mediumContrastTextColor);
|
||||
reverseTextColor =
|
||||
res.getColor(R.attr.highContrastReverseTextColor);
|
||||
}
|
||||
}
|
||||
|
||||
private void initDateFormats()
|
||||
{
|
||||
if (isInEditMode())
|
||||
{
|
||||
dfMonth = new SimpleDateFormat("MMM", Locale.getDefault());
|
||||
dfYear = new SimpleDateFormat("yyyy", Locale.getDefault());
|
||||
}
|
||||
else
|
||||
{
|
||||
dfMonth = AndroidDateFormats.fromSkeleton("MMM");
|
||||
dfYear = AndroidDateFormats.fromSkeleton("yyyy");
|
||||
}
|
||||
}
|
||||
|
||||
private void initRects()
|
||||
{
|
||||
baseLocation = new RectF();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Timestamp positionToTimestamp(float x, float y)
|
||||
{
|
||||
int col = (int) (x / columnWidth);
|
||||
int row = (int) (y / columnWidth);
|
||||
|
||||
if (row == 0) return null;
|
||||
if (col == nColumns - 1) return null;
|
||||
|
||||
int offset = col * 7 + (row - 1);
|
||||
Calendar date = (Calendar) baseDate.clone();
|
||||
date.add(Calendar.DAY_OF_YEAR, offset);
|
||||
|
||||
if (DateUtils.getStartOfDay(date.getTimeInMillis()) >
|
||||
DateUtils.getStartOfToday()) return null;
|
||||
|
||||
return new Timestamp(date.getTimeInMillis());
|
||||
}
|
||||
|
||||
private void updateDate()
|
||||
{
|
||||
baseDate = DateUtils.getStartOfTodayCalendar();
|
||||
baseDate.add(Calendar.DAY_OF_YEAR, -(getDataOffset() - 1) * 7);
|
||||
|
||||
nDays = (nColumns - 1) * 7;
|
||||
int realWeekday =
|
||||
DateUtils.getStartOfTodayCalendar().get(Calendar.DAY_OF_WEEK);
|
||||
todayPositionInColumn =
|
||||
(7 + realWeekday - baseDate.getFirstDayOfWeek()) % 7;
|
||||
|
||||
baseDate.add(Calendar.DAY_OF_YEAR, -nDays);
|
||||
baseDate.add(Calendar.DAY_OF_YEAR, -todayPositionInColumn);
|
||||
}
|
||||
|
||||
public interface Controller
|
||||
{
|
||||
default void onToggleCheckmark(Timestamp timestamp) {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.common.views;
|
||||
|
||||
import android.content.*;
|
||||
import android.graphics.*;
|
||||
import android.support.annotation.*;
|
||||
import android.text.*;
|
||||
import android.util.*;
|
||||
import android.view.*;
|
||||
|
||||
import org.isoron.androidbase.utils.*;
|
||||
import org.isoron.uhabits.*;
|
||||
import org.isoron.uhabits.utils.*;
|
||||
|
||||
import static org.isoron.uhabits.utils.AttributeSetUtils.*;
|
||||
import static org.isoron.androidbase.utils.InterfaceUtils.*;
|
||||
|
||||
public class RingView extends View
|
||||
{
|
||||
public static final PorterDuffXfermode XFERMODE_CLEAR =
|
||||
new PorterDuffXfermode(PorterDuff.Mode.CLEAR);
|
||||
|
||||
private int color;
|
||||
|
||||
private float precision;
|
||||
|
||||
private float percentage;
|
||||
|
||||
private int diameter;
|
||||
|
||||
private float thickness;
|
||||
|
||||
private RectF rect;
|
||||
|
||||
private TextPaint pRing;
|
||||
|
||||
private Integer backgroundColor;
|
||||
|
||||
private Integer inactiveColor;
|
||||
|
||||
private float em;
|
||||
|
||||
private String text;
|
||||
|
||||
private float textSize;
|
||||
|
||||
private boolean enableFontAwesome;
|
||||
|
||||
@Nullable
|
||||
private Bitmap drawingCache;
|
||||
|
||||
private Canvas cacheCanvas;
|
||||
|
||||
private boolean isTransparencyEnabled;
|
||||
|
||||
public RingView(Context context)
|
||||
{
|
||||
super(context);
|
||||
|
||||
percentage = 0.0f;
|
||||
precision = 0.01f;
|
||||
color = PaletteUtils.getAndroidTestColor(0);
|
||||
thickness = dpToPixels(getContext(), 2);
|
||||
text = "";
|
||||
textSize = getDimension(context, R.dimen.smallTextSize);
|
||||
|
||||
init();
|
||||
}
|
||||
|
||||
public RingView(Context ctx, AttributeSet attrs)
|
||||
{
|
||||
super(ctx, attrs);
|
||||
|
||||
percentage = getFloatAttribute(ctx, attrs, "percentage", 0);
|
||||
precision = getFloatAttribute(ctx, attrs, "precision", 0.01f);
|
||||
|
||||
color = getColorAttribute(ctx, attrs, "color", 0);
|
||||
backgroundColor = getColorAttribute(ctx, attrs, "backgroundColor", null);
|
||||
inactiveColor = getColorAttribute(ctx, attrs, "inactiveColor", null);
|
||||
|
||||
thickness = getFloatAttribute(ctx, attrs, "thickness", 0);
|
||||
thickness = dpToPixels(ctx, thickness);
|
||||
|
||||
float defaultTextSize = getDimension(ctx, R.dimen.smallTextSize);
|
||||
textSize = getFloatAttribute(ctx, attrs, "textSize", defaultTextSize);
|
||||
textSize = spToPixels(ctx, textSize);
|
||||
text = getAttribute(ctx, attrs, "text", "");
|
||||
|
||||
enableFontAwesome =
|
||||
getBooleanAttribute(ctx, attrs, "enableFontAwesome", false);
|
||||
|
||||
init();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBackgroundColor(int backgroundColor)
|
||||
{
|
||||
this.backgroundColor = backgroundColor;
|
||||
invalidate();
|
||||
}
|
||||
|
||||
public void setColor(int color)
|
||||
{
|
||||
this.color = color;
|
||||
invalidate();
|
||||
}
|
||||
|
||||
public int getColor()
|
||||
{
|
||||
return color;
|
||||
}
|
||||
|
||||
public void setIsTransparencyEnabled(boolean isTransparencyEnabled)
|
||||
{
|
||||
this.isTransparencyEnabled = isTransparencyEnabled;
|
||||
}
|
||||
|
||||
public void setPercentage(float percentage)
|
||||
{
|
||||
this.percentage = percentage;
|
||||
invalidate();
|
||||
}
|
||||
|
||||
public void setPrecision(float precision)
|
||||
{
|
||||
this.precision = precision;
|
||||
invalidate();
|
||||
}
|
||||
|
||||
public void setText(String text)
|
||||
{
|
||||
this.text = text;
|
||||
invalidate();
|
||||
}
|
||||
|
||||
public void setTextSize(float textSize)
|
||||
{
|
||||
this.textSize = textSize;
|
||||
}
|
||||
|
||||
public void setThickness(float thickness)
|
||||
{
|
||||
this.thickness = thickness;
|
||||
invalidate();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas)
|
||||
{
|
||||
super.onDraw(canvas);
|
||||
Canvas activeCanvas;
|
||||
|
||||
if (isTransparencyEnabled)
|
||||
{
|
||||
if (drawingCache == null) reallocateCache();
|
||||
activeCanvas = cacheCanvas;
|
||||
drawingCache.eraseColor(Color.TRANSPARENT);
|
||||
}
|
||||
else
|
||||
{
|
||||
activeCanvas = canvas;
|
||||
}
|
||||
|
||||
pRing.setColor(color);
|
||||
rect.set(0, 0, diameter, diameter);
|
||||
|
||||
float angle = 360 * Math.round(percentage / precision) * precision;
|
||||
|
||||
activeCanvas.drawArc(rect, -90, angle, true, pRing);
|
||||
|
||||
pRing.setColor(inactiveColor);
|
||||
activeCanvas.drawArc(rect, angle - 90, 360 - angle, true, pRing);
|
||||
|
||||
if (thickness > 0)
|
||||
{
|
||||
if (isTransparencyEnabled) pRing.setXfermode(XFERMODE_CLEAR);
|
||||
else pRing.setColor(backgroundColor);
|
||||
|
||||
rect.inset(thickness, thickness);
|
||||
activeCanvas.drawArc(rect, 0, 360, true, pRing);
|
||||
pRing.setXfermode(null);
|
||||
|
||||
pRing.setColor(color);
|
||||
pRing.setTextSize(textSize);
|
||||
if (enableFontAwesome)
|
||||
pRing.setTypeface(getFontAwesome(getContext()));
|
||||
activeCanvas.drawText(text, rect.centerX(),
|
||||
rect.centerY() + 0.4f * em, pRing);
|
||||
}
|
||||
|
||||
if (activeCanvas != canvas) canvas.drawBitmap(drawingCache, 0, 0, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
|
||||
{
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
|
||||
int width = MeasureSpec.getSize(widthMeasureSpec);
|
||||
int height = MeasureSpec.getSize(heightMeasureSpec);
|
||||
diameter = Math.min(height, width);
|
||||
|
||||
pRing.setTextSize(textSize);
|
||||
em = pRing.measureText("M");
|
||||
|
||||
setMeasuredDimension(diameter, diameter);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSizeChanged(int w, int h, int oldw, int oldh)
|
||||
{
|
||||
super.onSizeChanged(w, h, oldw, oldh);
|
||||
|
||||
if (isTransparencyEnabled) reallocateCache();
|
||||
}
|
||||
|
||||
private void init()
|
||||
{
|
||||
pRing = new TextPaint();
|
||||
pRing.setAntiAlias(true);
|
||||
pRing.setColor(color);
|
||||
pRing.setTextAlign(Paint.Align.CENTER);
|
||||
|
||||
StyledResources res = new StyledResources(getContext());
|
||||
|
||||
if (backgroundColor == null)
|
||||
backgroundColor = res.getColor(R.attr.cardBackgroundColor);
|
||||
|
||||
if (inactiveColor == null)
|
||||
inactiveColor = res.getColor(R.attr.highContrastTextColor);
|
||||
|
||||
inactiveColor = ColorUtils.setAlpha(inactiveColor, 0.1f);
|
||||
|
||||
rect = new RectF();
|
||||
}
|
||||
|
||||
private void reallocateCache()
|
||||
{
|
||||
if (drawingCache != null) drawingCache.recycle();
|
||||
drawingCache =
|
||||
Bitmap.createBitmap(diameter, diameter, Bitmap.Config.ARGB_8888);
|
||||
cacheCanvas = new Canvas(drawingCache);
|
||||
}
|
||||
|
||||
public float getPercentage()
|
||||
{
|
||||
return percentage;
|
||||
}
|
||||
|
||||
public float getPrecision()
|
||||
{
|
||||
return precision;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,452 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.common.views;
|
||||
|
||||
import android.content.*;
|
||||
import android.graphics.*;
|
||||
import android.support.annotation.*;
|
||||
import android.util.*;
|
||||
|
||||
import org.isoron.androidbase.utils.*;
|
||||
import org.isoron.uhabits.*;
|
||||
import org.isoron.uhabits.core.models.*;
|
||||
import org.isoron.uhabits.core.utils.*;
|
||||
import org.isoron.uhabits.utils.*;
|
||||
|
||||
import java.text.*;
|
||||
import java.util.*;
|
||||
|
||||
import static org.isoron.androidbase.utils.InterfaceUtils.*;
|
||||
|
||||
public class ScoreChart extends ScrollableChart
|
||||
{
|
||||
private static final PorterDuffXfermode XFERMODE_CLEAR =
|
||||
new PorterDuffXfermode(PorterDuff.Mode.CLEAR);
|
||||
|
||||
private static final PorterDuffXfermode XFERMODE_SRC =
|
||||
new PorterDuffXfermode(PorterDuff.Mode.SRC);
|
||||
|
||||
private Paint pGrid;
|
||||
|
||||
private float em;
|
||||
|
||||
private SimpleDateFormat dfMonth;
|
||||
|
||||
private SimpleDateFormat dfDay;
|
||||
|
||||
private SimpleDateFormat dfYear;
|
||||
|
||||
private Paint pText, pGraph;
|
||||
|
||||
private RectF rect, prevRect;
|
||||
|
||||
private int baseSize;
|
||||
|
||||
private int paddingTop;
|
||||
|
||||
private float columnWidth;
|
||||
|
||||
private int columnHeight;
|
||||
|
||||
private int nColumns;
|
||||
|
||||
private int textColor;
|
||||
|
||||
private int gridColor;
|
||||
|
||||
@Nullable
|
||||
private List<Score> scores;
|
||||
|
||||
private int primaryColor;
|
||||
|
||||
@Deprecated
|
||||
private int bucketSize = 7;
|
||||
|
||||
private int backgroundColor;
|
||||
|
||||
private Bitmap drawingCache;
|
||||
|
||||
private Canvas cacheCanvas;
|
||||
|
||||
private boolean isTransparencyEnabled;
|
||||
|
||||
private int skipYear = 0;
|
||||
|
||||
private String previousYearText;
|
||||
|
||||
private String previousMonthText;
|
||||
|
||||
public ScoreChart(Context context)
|
||||
{
|
||||
super(context);
|
||||
init();
|
||||
}
|
||||
|
||||
public ScoreChart(Context context, AttributeSet attrs)
|
||||
{
|
||||
super(context, attrs);
|
||||
init();
|
||||
}
|
||||
|
||||
public void populateWithRandomData()
|
||||
{
|
||||
Random random = new Random();
|
||||
scores = new LinkedList<>();
|
||||
|
||||
double previous = 0.5f;
|
||||
Timestamp timestamp = DateUtils.getToday();
|
||||
|
||||
for (int i = 1; i < 100; i++)
|
||||
{
|
||||
double step = 0.1f;
|
||||
double current = previous + random.nextDouble() * step * 2 - step;
|
||||
current = Math.max(0, Math.min(1.0f, current));
|
||||
scores.add(new Score(timestamp.minus(i), current));
|
||||
previous = current;
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public void setBucketSize(int bucketSize)
|
||||
{
|
||||
this.bucketSize = bucketSize;
|
||||
postInvalidate();
|
||||
}
|
||||
|
||||
public void setIsTransparencyEnabled(boolean enabled)
|
||||
{
|
||||
this.isTransparencyEnabled = enabled;
|
||||
postInvalidate();
|
||||
}
|
||||
|
||||
public void setColor(int primaryColor)
|
||||
{
|
||||
this.primaryColor = primaryColor;
|
||||
postInvalidate();
|
||||
}
|
||||
|
||||
public void setScores(@NonNull List<Score> scores)
|
||||
{
|
||||
this.scores = scores;
|
||||
postInvalidate();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas)
|
||||
{
|
||||
super.onDraw(canvas);
|
||||
Canvas activeCanvas;
|
||||
|
||||
if (isTransparencyEnabled)
|
||||
{
|
||||
if (drawingCache == null) initCache(getWidth(), getHeight());
|
||||
|
||||
activeCanvas = cacheCanvas;
|
||||
drawingCache.eraseColor(Color.TRANSPARENT);
|
||||
}
|
||||
else
|
||||
{
|
||||
activeCanvas = canvas;
|
||||
}
|
||||
|
||||
if (scores == null) return;
|
||||
|
||||
rect.set(0, 0, nColumns * columnWidth, columnHeight);
|
||||
rect.offset(0, paddingTop);
|
||||
|
||||
drawGrid(activeCanvas, rect);
|
||||
|
||||
pText.setColor(textColor);
|
||||
pGraph.setColor(primaryColor);
|
||||
prevRect.setEmpty();
|
||||
|
||||
previousMonthText = "";
|
||||
previousYearText = "";
|
||||
skipYear = 0;
|
||||
|
||||
for (int k = 0; k < nColumns; k++)
|
||||
{
|
||||
int offset = nColumns - k - 1 + getDataOffset();
|
||||
if (offset >= scores.size()) continue;
|
||||
|
||||
double score = scores.get(offset).getValue();
|
||||
Timestamp timestamp = scores.get(offset).getTimestamp();
|
||||
|
||||
int height = (int) (columnHeight * score);
|
||||
|
||||
rect.set(0, 0, baseSize, baseSize);
|
||||
rect.offset(k * columnWidth + (columnWidth - baseSize) / 2,
|
||||
paddingTop + columnHeight - height - baseSize / 2);
|
||||
|
||||
if (!prevRect.isEmpty())
|
||||
{
|
||||
drawLine(activeCanvas, prevRect, rect);
|
||||
drawMarker(activeCanvas, prevRect);
|
||||
}
|
||||
|
||||
if (k == nColumns - 1) drawMarker(activeCanvas, rect);
|
||||
|
||||
prevRect.set(rect);
|
||||
rect.set(0, 0, columnWidth, columnHeight);
|
||||
rect.offset(k * columnWidth, paddingTop);
|
||||
|
||||
drawFooter(activeCanvas, rect, timestamp);
|
||||
}
|
||||
|
||||
if (activeCanvas != canvas) canvas.drawBitmap(drawingCache, 0, 0, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
|
||||
{
|
||||
int width = MeasureSpec.getSize(widthMeasureSpec);
|
||||
int height = MeasureSpec.getSize(heightMeasureSpec);
|
||||
setMeasuredDimension(width, height);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSizeChanged(int width,
|
||||
int height,
|
||||
int oldWidth,
|
||||
int oldHeight)
|
||||
{
|
||||
if (height < 9) height = 200;
|
||||
|
||||
float maxTextSize = getDimension(getContext(), R.dimen.tinyTextSize);
|
||||
float textSize = height * 0.06f;
|
||||
pText.setTextSize(Math.min(textSize, maxTextSize));
|
||||
em = pText.getFontSpacing();
|
||||
|
||||
int footerHeight = (int) (3 * em);
|
||||
paddingTop = (int) (em);
|
||||
|
||||
baseSize = (height - footerHeight - paddingTop) / 8;
|
||||
columnWidth = baseSize;
|
||||
columnWidth = Math.max(columnWidth, getMaxDayWidth() * 1.5f);
|
||||
columnWidth = Math.max(columnWidth, getMaxMonthWidth() * 1.2f);
|
||||
|
||||
nColumns = (int) (width / columnWidth);
|
||||
columnWidth = (float) width / nColumns;
|
||||
setScrollerBucketSize((int) columnWidth);
|
||||
|
||||
columnHeight = 8 * baseSize;
|
||||
|
||||
float minStrokeWidth = dpToPixels(getContext(), 1);
|
||||
pGraph.setTextSize(baseSize * 0.5f);
|
||||
pGraph.setStrokeWidth(baseSize * 0.1f);
|
||||
pGrid.setStrokeWidth(Math.min(minStrokeWidth, baseSize * 0.05f));
|
||||
|
||||
if (isTransparencyEnabled) initCache(width, height);
|
||||
}
|
||||
|
||||
private void drawFooter(Canvas canvas, RectF rect, Timestamp currentDate)
|
||||
{
|
||||
String yearText = dfYear.format(currentDate.toJavaDate());
|
||||
String monthText = dfMonth.format(currentDate.toJavaDate());
|
||||
String dayText = dfDay.format(currentDate.toJavaDate());
|
||||
|
||||
GregorianCalendar calendar = currentDate.toCalendar();
|
||||
|
||||
String text;
|
||||
int year = calendar.get(Calendar.YEAR);
|
||||
|
||||
boolean shouldPrintYear = true;
|
||||
if (yearText.equals(previousYearText)) shouldPrintYear = false;
|
||||
if (bucketSize >= 365 && (year % 2) != 0) shouldPrintYear = false;
|
||||
|
||||
if (skipYear > 0)
|
||||
{
|
||||
skipYear--;
|
||||
shouldPrintYear = false;
|
||||
}
|
||||
|
||||
if (shouldPrintYear)
|
||||
{
|
||||
previousYearText = yearText;
|
||||
previousMonthText = "";
|
||||
|
||||
pText.setTextAlign(Paint.Align.CENTER);
|
||||
canvas.drawText(yearText, rect.centerX(), rect.bottom + em * 2.2f,
|
||||
pText);
|
||||
|
||||
skipYear = 1;
|
||||
}
|
||||
|
||||
if (bucketSize < 365)
|
||||
{
|
||||
if (!monthText.equals(previousMonthText))
|
||||
{
|
||||
previousMonthText = monthText;
|
||||
text = monthText;
|
||||
}
|
||||
else
|
||||
{
|
||||
text = dayText;
|
||||
}
|
||||
|
||||
pText.setTextAlign(Paint.Align.CENTER);
|
||||
canvas.drawText(text, rect.centerX(), rect.bottom + em * 1.2f,
|
||||
pText);
|
||||
}
|
||||
}
|
||||
|
||||
private void drawGrid(Canvas canvas, RectF rGrid)
|
||||
{
|
||||
int nRows = 5;
|
||||
float rowHeight = rGrid.height() / nRows;
|
||||
|
||||
pText.setTextAlign(Paint.Align.LEFT);
|
||||
pText.setColor(textColor);
|
||||
pGrid.setColor(gridColor);
|
||||
|
||||
for (int i = 0; i < nRows; i++)
|
||||
{
|
||||
canvas.drawText(String.format("%d%%", (100 - i * 100 / nRows)),
|
||||
rGrid.left + 0.5f * em, rGrid.top + 1f * em, pText);
|
||||
canvas.drawLine(rGrid.left, rGrid.top, rGrid.right, rGrid.top,
|
||||
pGrid);
|
||||
rGrid.offset(0, rowHeight);
|
||||
}
|
||||
|
||||
canvas.drawLine(rGrid.left, rGrid.top, rGrid.right, rGrid.top, pGrid);
|
||||
}
|
||||
|
||||
private void drawLine(Canvas canvas, RectF rectFrom, RectF rectTo)
|
||||
{
|
||||
pGraph.setColor(primaryColor);
|
||||
canvas.drawLine(rectFrom.centerX(), rectFrom.centerY(),
|
||||
rectTo.centerX(), rectTo.centerY(), pGraph);
|
||||
}
|
||||
|
||||
private void drawMarker(Canvas canvas, RectF rect)
|
||||
{
|
||||
rect.inset(baseSize * 0.225f, baseSize * 0.225f);
|
||||
setModeOrColor(pGraph, XFERMODE_CLEAR, backgroundColor);
|
||||
canvas.drawOval(rect, pGraph);
|
||||
|
||||
rect.inset(baseSize * 0.1f, baseSize * 0.1f);
|
||||
setModeOrColor(pGraph, XFERMODE_SRC, primaryColor);
|
||||
canvas.drawOval(rect, pGraph);
|
||||
|
||||
// rect.inset(baseSize * 0.1f, baseSize * 0.1f);
|
||||
// setModeOrColor(pGraph, XFERMODE_CLEAR, backgroundColor);
|
||||
// canvas.drawOval(rect, pGraph);
|
||||
|
||||
if (isTransparencyEnabled) pGraph.setXfermode(XFERMODE_SRC);
|
||||
}
|
||||
|
||||
private float getMaxDayWidth()
|
||||
{
|
||||
float maxDayWidth = 0;
|
||||
GregorianCalendar day = DateUtils.getStartOfTodayCalendar();
|
||||
|
||||
for (int i = 0; i < 28; i++)
|
||||
{
|
||||
day.set(Calendar.DAY_OF_MONTH, i);
|
||||
float monthWidth = pText.measureText(dfMonth.format(day.getTime()));
|
||||
maxDayWidth = Math.max(maxDayWidth, monthWidth);
|
||||
}
|
||||
|
||||
return maxDayWidth;
|
||||
}
|
||||
|
||||
private float getMaxMonthWidth()
|
||||
{
|
||||
float maxMonthWidth = 0;
|
||||
GregorianCalendar day = DateUtils.getStartOfTodayCalendar();
|
||||
|
||||
for (int i = 0; i < 12; i++)
|
||||
{
|
||||
day.set(Calendar.MONTH, i);
|
||||
float monthWidth = pText.measureText(dfMonth.format(day.getTime()));
|
||||
maxMonthWidth = Math.max(maxMonthWidth, monthWidth);
|
||||
}
|
||||
|
||||
return maxMonthWidth;
|
||||
}
|
||||
|
||||
private void init()
|
||||
{
|
||||
initPaints();
|
||||
initColors();
|
||||
initDateFormats();
|
||||
initRects();
|
||||
}
|
||||
|
||||
private void initCache(int width, int height)
|
||||
{
|
||||
if (drawingCache != null) drawingCache.recycle();
|
||||
drawingCache =
|
||||
Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
|
||||
cacheCanvas = new Canvas(drawingCache);
|
||||
}
|
||||
|
||||
private void initColors()
|
||||
{
|
||||
StyledResources res = new StyledResources(getContext());
|
||||
|
||||
primaryColor = Color.BLACK;
|
||||
textColor = res.getColor(R.attr.mediumContrastTextColor);
|
||||
gridColor = res.getColor(R.attr.lowContrastTextColor);
|
||||
backgroundColor = res.getColor(R.attr.cardBackgroundColor);
|
||||
}
|
||||
|
||||
private void initDateFormats()
|
||||
{
|
||||
if (isInEditMode())
|
||||
{
|
||||
dfMonth = new SimpleDateFormat("MMM", Locale.getDefault());
|
||||
dfYear = new SimpleDateFormat("yyyy", Locale.getDefault());
|
||||
dfDay = new SimpleDateFormat("d", Locale.getDefault());
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
dfMonth = AndroidDateFormats.fromSkeleton("MMM");
|
||||
dfYear = AndroidDateFormats.fromSkeleton("yyyy");
|
||||
dfDay = AndroidDateFormats.fromSkeleton("d");
|
||||
}
|
||||
}
|
||||
|
||||
private void initPaints()
|
||||
{
|
||||
pText = new Paint();
|
||||
pText.setAntiAlias(true);
|
||||
|
||||
pGraph = new Paint();
|
||||
pGraph.setTextAlign(Paint.Align.CENTER);
|
||||
pGraph.setAntiAlias(true);
|
||||
|
||||
pGrid = new Paint();
|
||||
pGrid.setAntiAlias(true);
|
||||
}
|
||||
|
||||
private void initRects()
|
||||
{
|
||||
rect = new RectF();
|
||||
prevRect = new RectF();
|
||||
}
|
||||
|
||||
private void setModeOrColor(Paint p, PorterDuffXfermode mode, int color)
|
||||
{
|
||||
if (isTransparencyEnabled) p.setXfermode(mode);
|
||||
else p.setColor(color);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.common.views;
|
||||
|
||||
import android.animation.*;
|
||||
import android.content.*;
|
||||
import android.os.*;
|
||||
import android.util.*;
|
||||
import android.view.*;
|
||||
import android.widget.*;
|
||||
|
||||
public abstract class ScrollableChart extends View
|
||||
implements GestureDetector.OnGestureListener,
|
||||
ValueAnimator.AnimatorUpdateListener
|
||||
{
|
||||
|
||||
private int dataOffset;
|
||||
|
||||
private int scrollerBucketSize = 1;
|
||||
|
||||
private int direction = 1;
|
||||
|
||||
private GestureDetector detector;
|
||||
|
||||
private Scroller scroller;
|
||||
|
||||
private ValueAnimator scrollAnimator;
|
||||
|
||||
private ScrollController scrollController;
|
||||
|
||||
private int maxDataOffset = 10000;
|
||||
|
||||
public ScrollableChart(Context context)
|
||||
{
|
||||
super(context);
|
||||
init(context);
|
||||
}
|
||||
|
||||
public ScrollableChart(Context context, AttributeSet attrs)
|
||||
{
|
||||
super(context, attrs);
|
||||
init(context);
|
||||
}
|
||||
|
||||
public int getDataOffset()
|
||||
{
|
||||
return dataOffset;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationUpdate(ValueAnimator animation)
|
||||
{
|
||||
if (!scroller.isFinished())
|
||||
{
|
||||
scroller.computeScrollOffset();
|
||||
updateDataOffset();
|
||||
}
|
||||
else
|
||||
{
|
||||
scrollAnimator.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onDown(MotionEvent e)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onFling(MotionEvent e1,
|
||||
MotionEvent e2,
|
||||
float velocityX,
|
||||
float velocityY)
|
||||
{
|
||||
scroller.fling(scroller.getCurrX(), scroller.getCurrY(),
|
||||
direction * ((int) velocityX) / 2, 0, 0, getMaxX(), 0, 0);
|
||||
invalidate();
|
||||
|
||||
scrollAnimator.setDuration(scroller.getDuration());
|
||||
scrollAnimator.start();
|
||||
return false;
|
||||
}
|
||||
|
||||
private int getMaxX()
|
||||
{
|
||||
return maxDataOffset * scrollerBucketSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRestoreInstanceState(Parcelable state)
|
||||
{
|
||||
if(!(state instanceof BundleSavedState))
|
||||
{
|
||||
super.onRestoreInstanceState(state);
|
||||
return;
|
||||
}
|
||||
|
||||
BundleSavedState bss = (BundleSavedState) state;
|
||||
int x = bss.bundle.getInt("x");
|
||||
int y = bss.bundle.getInt("y");
|
||||
direction = bss.bundle.getInt("direction");
|
||||
dataOffset = bss.bundle.getInt("dataOffset");
|
||||
maxDataOffset = bss.bundle.getInt("maxDataOffset");
|
||||
scroller.startScroll(0, 0, x, y, 0);
|
||||
scroller.computeScrollOffset();
|
||||
super.onRestoreInstanceState(bss.getSuperState());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Parcelable onSaveInstanceState()
|
||||
{
|
||||
Parcelable superState = super.onSaveInstanceState();
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putInt("x", scroller.getCurrX());
|
||||
bundle.putInt("y", scroller.getCurrY());
|
||||
bundle.putInt("dataOffset", dataOffset);
|
||||
bundle.putInt("direction", direction);
|
||||
bundle.putInt("maxDataOffset", maxDataOffset);
|
||||
return new BundleSavedState(superState, bundle);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onScroll(MotionEvent e1, MotionEvent e2, float dx, float dy)
|
||||
{
|
||||
if (scrollerBucketSize == 0) return false;
|
||||
|
||||
if (Math.abs(dx) > Math.abs(dy))
|
||||
{
|
||||
ViewParent parent = getParent();
|
||||
if (parent != null) parent.requestDisallowInterceptTouchEvent(true);
|
||||
}
|
||||
|
||||
|
||||
dx = - direction * dx;
|
||||
dx = Math.min(dx, getMaxX() - scroller.getCurrX());
|
||||
scroller.startScroll(scroller.getCurrX(), scroller.getCurrY(), (int) dx,
|
||||
(int) dy, 0);
|
||||
|
||||
scroller.computeScrollOffset();
|
||||
updateDataOffset();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onShowPress(MotionEvent e)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onSingleTapUp(MotionEvent e)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent event)
|
||||
{
|
||||
return detector.onTouchEvent(event);
|
||||
}
|
||||
|
||||
public void setScrollDirection(int direction)
|
||||
{
|
||||
if (direction != 1 && direction != -1)
|
||||
throw new IllegalArgumentException();
|
||||
this.direction = direction;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLongPress(MotionEvent e)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public void setMaxDataOffset(int maxDataOffset)
|
||||
{
|
||||
this.maxDataOffset = maxDataOffset;
|
||||
this.dataOffset = Math.min(dataOffset, maxDataOffset);
|
||||
scrollController.onDataOffsetChanged(this.dataOffset);
|
||||
postInvalidate();
|
||||
}
|
||||
|
||||
public void setScrollController(ScrollController scrollController)
|
||||
{
|
||||
this.scrollController = scrollController;
|
||||
}
|
||||
|
||||
public void setScrollerBucketSize(int scrollerBucketSize)
|
||||
{
|
||||
this.scrollerBucketSize = scrollerBucketSize;
|
||||
}
|
||||
|
||||
private void init(Context context)
|
||||
{
|
||||
detector = new GestureDetector(context, this);
|
||||
scroller = new Scroller(context, null, true);
|
||||
scrollAnimator = ValueAnimator.ofFloat(0, 1);
|
||||
scrollAnimator.addUpdateListener(this);
|
||||
scrollController = new ScrollController() {};
|
||||
}
|
||||
|
||||
private void updateDataOffset()
|
||||
{
|
||||
int newDataOffset = scroller.getCurrX() / scrollerBucketSize;
|
||||
newDataOffset = Math.max(0, newDataOffset);
|
||||
newDataOffset = Math.min(maxDataOffset, newDataOffset);
|
||||
|
||||
if (newDataOffset != dataOffset)
|
||||
{
|
||||
dataOffset = newDataOffset;
|
||||
scrollController.onDataOffsetChanged(dataOffset);
|
||||
postInvalidate();
|
||||
}
|
||||
}
|
||||
|
||||
public interface ScrollController
|
||||
{
|
||||
default void onDataOffsetChanged(int newDataOffset) {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,298 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.common.views;
|
||||
|
||||
import android.content.*;
|
||||
import android.graphics.*;
|
||||
import android.util.*;
|
||||
import android.view.*;
|
||||
import android.view.ViewGroup.*;
|
||||
|
||||
import org.isoron.androidbase.utils.*;
|
||||
import org.isoron.uhabits.*;
|
||||
import org.isoron.uhabits.core.models.*;
|
||||
import org.isoron.uhabits.core.utils.*;
|
||||
|
||||
import java.text.*;
|
||||
import java.util.*;
|
||||
|
||||
import static android.view.View.MeasureSpec.*;
|
||||
import static org.isoron.androidbase.utils.InterfaceUtils.getDimension;
|
||||
|
||||
public class StreakChart extends View
|
||||
{
|
||||
private Paint paint;
|
||||
|
||||
private long minLength;
|
||||
|
||||
private long maxLength;
|
||||
|
||||
private int[] colors;
|
||||
|
||||
private RectF rect;
|
||||
|
||||
private int baseSize;
|
||||
|
||||
private int primaryColor;
|
||||
|
||||
private List<Streak> streaks;
|
||||
|
||||
private boolean isBackgroundTransparent;
|
||||
|
||||
private DateFormat dateFormat;
|
||||
|
||||
private int width;
|
||||
|
||||
private float em;
|
||||
|
||||
private float maxLabelWidth;
|
||||
|
||||
private float textMargin;
|
||||
|
||||
private boolean shouldShowLabels;
|
||||
|
||||
private int textColor;
|
||||
|
||||
private int reverseTextColor;
|
||||
|
||||
public StreakChart(Context context)
|
||||
{
|
||||
super(context);
|
||||
init();
|
||||
}
|
||||
|
||||
public StreakChart(Context context, AttributeSet attrs)
|
||||
{
|
||||
super(context, attrs);
|
||||
init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the maximum number of streaks this view is able to show, given
|
||||
* its current size.
|
||||
*
|
||||
* @return max number of visible streaks
|
||||
*/
|
||||
public int getMaxStreakCount()
|
||||
{
|
||||
return (int) Math.floor(getMeasuredHeight() / baseSize);
|
||||
}
|
||||
|
||||
public void populateWithRandomData()
|
||||
{
|
||||
Timestamp start = DateUtils.getToday();
|
||||
LinkedList<Streak> streaks = new LinkedList<>();
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
int length = new Random().nextInt(100);
|
||||
Timestamp end = start.plus(length);
|
||||
streaks.add(new Streak(start, end));
|
||||
start = end.plus(1);
|
||||
}
|
||||
|
||||
setStreaks(streaks);
|
||||
}
|
||||
|
||||
public void setColor(int color)
|
||||
{
|
||||
this.primaryColor = color;
|
||||
postInvalidate();
|
||||
}
|
||||
|
||||
public void setIsBackgroundTransparent(boolean isBackgroundTransparent)
|
||||
{
|
||||
this.isBackgroundTransparent = isBackgroundTransparent;
|
||||
initColors();
|
||||
}
|
||||
|
||||
public void setStreaks(List<Streak> streaks)
|
||||
{
|
||||
this.streaks = streaks;
|
||||
initColors();
|
||||
updateMaxMinLengths();
|
||||
requestLayout();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas)
|
||||
{
|
||||
super.onDraw(canvas);
|
||||
if (streaks.size() == 0) return;
|
||||
|
||||
rect.set(0, 0, width, baseSize);
|
||||
|
||||
for (Streak s : streaks)
|
||||
{
|
||||
drawRow(canvas, s, rect);
|
||||
rect.offset(0, baseSize);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthSpec, int heightSpec)
|
||||
{
|
||||
LayoutParams params = getLayoutParams();
|
||||
|
||||
if (params != null && params.height == LayoutParams.WRAP_CONTENT)
|
||||
{
|
||||
int width = getSize(widthSpec);
|
||||
int height = streaks.size() * baseSize;
|
||||
|
||||
heightSpec = makeMeasureSpec(height, EXACTLY);
|
||||
widthSpec = makeMeasureSpec(width, EXACTLY);
|
||||
}
|
||||
|
||||
setMeasuredDimension(widthSpec, heightSpec);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSizeChanged(int width,
|
||||
int height,
|
||||
int oldWidth,
|
||||
int oldHeight)
|
||||
{
|
||||
this.width = width;
|
||||
|
||||
Context context = getContext();
|
||||
float minTextSize = getDimension(context, R.dimen.tinyTextSize);
|
||||
float maxTextSize = getDimension(context, R.dimen.regularTextSize);
|
||||
float textSize = baseSize * 0.5f;
|
||||
|
||||
paint.setTextSize(
|
||||
Math.max(Math.min(textSize, maxTextSize), minTextSize));
|
||||
em = paint.getFontSpacing();
|
||||
textMargin = 0.5f * em;
|
||||
|
||||
updateMaxMinLengths();
|
||||
}
|
||||
|
||||
private void drawRow(Canvas canvas, Streak streak, RectF rect)
|
||||
{
|
||||
if (maxLength == 0) return;
|
||||
|
||||
float percentage = (float) streak.getLength() / maxLength;
|
||||
float availableWidth = width - 2 * maxLabelWidth;
|
||||
if (shouldShowLabels) availableWidth -= 2 * textMargin;
|
||||
|
||||
float barWidth = percentage * availableWidth;
|
||||
float minBarWidth =
|
||||
paint.measureText(Long.toString(streak.getLength())) + em;
|
||||
barWidth = Math.max(barWidth, minBarWidth);
|
||||
|
||||
float gap = (width - barWidth) / 2;
|
||||
float paddingTopBottom = baseSize * 0.05f;
|
||||
|
||||
paint.setColor(percentageToColor(percentage));
|
||||
|
||||
canvas.drawRect(rect.left + gap, rect.top + paddingTopBottom,
|
||||
rect.right - gap, rect.bottom - paddingTopBottom, paint);
|
||||
|
||||
float yOffset = rect.centerY() + 0.3f * em;
|
||||
|
||||
paint.setColor(reverseTextColor);
|
||||
paint.setTextAlign(Paint.Align.CENTER);
|
||||
canvas.drawText(Long.toString(streak.getLength()), rect.centerX(),
|
||||
yOffset, paint);
|
||||
|
||||
if (shouldShowLabels)
|
||||
{
|
||||
String startLabel = dateFormat.format(streak.getStart().toJavaDate());
|
||||
String endLabel = dateFormat.format(streak.getEnd().toJavaDate());
|
||||
|
||||
paint.setColor(textColor);
|
||||
paint.setTextAlign(Paint.Align.RIGHT);
|
||||
canvas.drawText(startLabel, gap - textMargin, yOffset, paint);
|
||||
|
||||
paint.setTextAlign(Paint.Align.LEFT);
|
||||
canvas.drawText(endLabel, width - gap + textMargin, yOffset, paint);
|
||||
}
|
||||
}
|
||||
|
||||
private void init()
|
||||
{
|
||||
initPaints();
|
||||
initColors();
|
||||
|
||||
streaks = Collections.emptyList();
|
||||
|
||||
dateFormat = DateFormat.getDateInstance(DateFormat.MEDIUM);
|
||||
if (!isInEditMode()) dateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
|
||||
rect = new RectF();
|
||||
baseSize = getResources().getDimensionPixelSize(R.dimen.baseSize);
|
||||
}
|
||||
|
||||
private void initColors()
|
||||
{
|
||||
int red = Color.red(primaryColor);
|
||||
int green = Color.green(primaryColor);
|
||||
int blue = Color.blue(primaryColor);
|
||||
|
||||
StyledResources res = new StyledResources(getContext());
|
||||
|
||||
colors = new int[4];
|
||||
colors[3] = primaryColor;
|
||||
colors[2] = Color.argb(192, red, green, blue);
|
||||
colors[1] = Color.argb(96, red, green, blue);
|
||||
colors[0] = res.getColor(R.attr.lowContrastTextColor);
|
||||
textColor = res.getColor(R.attr.mediumContrastTextColor);
|
||||
reverseTextColor = res.getColor(R.attr.highContrastReverseTextColor);
|
||||
}
|
||||
|
||||
private void initPaints()
|
||||
{
|
||||
paint = new Paint();
|
||||
paint.setTextAlign(Paint.Align.CENTER);
|
||||
paint.setAntiAlias(true);
|
||||
}
|
||||
|
||||
private int percentageToColor(float percentage)
|
||||
{
|
||||
if (percentage >= 1.0f) return colors[3];
|
||||
if (percentage >= 0.8f) return colors[2];
|
||||
if (percentage >= 0.5f) return colors[1];
|
||||
return colors[0];
|
||||
}
|
||||
|
||||
private void updateMaxMinLengths()
|
||||
{
|
||||
maxLength = 0;
|
||||
minLength = Long.MAX_VALUE;
|
||||
shouldShowLabels = true;
|
||||
|
||||
for (Streak s : streaks)
|
||||
{
|
||||
maxLength = Math.max(maxLength, s.getLength());
|
||||
minLength = Math.min(minLength, s.getLength());
|
||||
|
||||
float lw1 =
|
||||
paint.measureText(dateFormat.format(s.getStart().toJavaDate()));
|
||||
float lw2 =
|
||||
paint.measureText(dateFormat.format(s.getEnd().toJavaDate()));
|
||||
maxLabelWidth = Math.max(maxLabelWidth, Math.max(lw1, lw2));
|
||||
}
|
||||
|
||||
if (width - 2 * maxLabelWidth < width * 0.25f)
|
||||
{
|
||||
maxLabelWidth = 0;
|
||||
shouldShowLabels = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.common.views
|
||||
|
||||
import android.content.*
|
||||
import android.widget.*
|
||||
import org.isoron.androidbase.activities.*
|
||||
import org.isoron.uhabits.core.tasks.*
|
||||
|
||||
class TaskProgressBar(
|
||||
context: Context,
|
||||
private val runner: TaskRunner
|
||||
) : ProgressBar(
|
||||
context,
|
||||
null,
|
||||
android.R.attr.progressBarStyleHorizontal
|
||||
), TaskRunner.Listener {
|
||||
|
||||
init {
|
||||
visibility = BaseRootView.GONE
|
||||
isIndeterminate = true
|
||||
}
|
||||
|
||||
override fun onTaskStarted(task: Task?) = update()
|
||||
override fun onTaskFinished(task: Task?) = update()
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
runner.addListener(this)
|
||||
update()
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
runner.removeListener(this)
|
||||
super.onDetachedFromWindow()
|
||||
}
|
||||
|
||||
fun update() {
|
||||
val callback = {
|
||||
val activeTaskCount = runner.activeTaskCount
|
||||
val newVisibility = when (activeTaskCount) {
|
||||
0 -> GONE
|
||||
else -> VISIBLE
|
||||
}
|
||||
if (visibility != newVisibility) visibility = newVisibility
|
||||
}
|
||||
postDelayed(callback, 500)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.habits.edit;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.content.*;
|
||||
import android.os.*;
|
||||
import android.support.annotation.*;
|
||||
import android.support.v7.app.*;
|
||||
import android.text.format.*;
|
||||
import android.view.*;
|
||||
|
||||
import com.android.datetimepicker.time.*;
|
||||
|
||||
import org.isoron.uhabits.*;
|
||||
import org.isoron.uhabits.R;
|
||||
import org.isoron.uhabits.activities.*;
|
||||
import org.isoron.uhabits.activities.common.dialogs.*;
|
||||
import org.isoron.uhabits.activities.habits.edit.views.*;
|
||||
import org.isoron.uhabits.core.commands.*;
|
||||
import org.isoron.uhabits.core.models.*;
|
||||
import org.isoron.uhabits.core.preferences.*;
|
||||
|
||||
import butterknife.*;
|
||||
|
||||
import static android.view.View.GONE;
|
||||
import static org.isoron.uhabits.core.ui.ThemeSwitcher.THEME_LIGHT;
|
||||
|
||||
public class EditHabitDialog extends AppCompatDialogFragment
|
||||
{
|
||||
public static final String BUNDLE_HABIT_ID = "habitId";
|
||||
|
||||
public static final String BUNDLE_HABIT_TYPE = "habitType";
|
||||
|
||||
protected Habit originalHabit;
|
||||
|
||||
protected Preferences prefs;
|
||||
|
||||
protected CommandRunner commandRunner;
|
||||
|
||||
protected HabitList habitList;
|
||||
|
||||
protected HabitsApplicationComponent component;
|
||||
|
||||
protected ModelFactory modelFactory;
|
||||
|
||||
@BindView(R.id.namePanel)
|
||||
NameDescriptionPanel namePanel;
|
||||
|
||||
@BindView(R.id.reminderPanel)
|
||||
ReminderPanel reminderPanel;
|
||||
|
||||
@BindView(R.id.frequencyPanel)
|
||||
FrequencyPanel frequencyPanel;
|
||||
|
||||
@BindView(R.id.targetPanel)
|
||||
TargetPanel targetPanel;
|
||||
|
||||
private ColorPickerDialogFactory colorPickerDialogFactory;
|
||||
|
||||
@Override
|
||||
public int getTheme()
|
||||
{
|
||||
HabitsApplicationComponent component =
|
||||
((HabitsApplication) getContext().getApplicationContext()).getComponent();
|
||||
|
||||
if(component.getPreferences().getTheme() == THEME_LIGHT)
|
||||
return R.style.DialogWithTitle;
|
||||
else
|
||||
return R.style.DarkDialogWithTitle;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityCreated(Bundle savedInstanceState)
|
||||
{
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
|
||||
HabitsActivity activity = (HabitsActivity) getActivity();
|
||||
colorPickerDialogFactory =
|
||||
activity.getComponent().getColorPickerDialogFactory();
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater,
|
||||
ViewGroup container,
|
||||
Bundle savedInstanceState)
|
||||
{
|
||||
View view;
|
||||
view = inflater.inflate(R.layout.edit_habit, container, false);
|
||||
|
||||
initDependencies();
|
||||
ButterKnife.bind(this, view);
|
||||
|
||||
originalHabit = parseHabitFromArguments();
|
||||
getDialog().setTitle(getTitle());
|
||||
|
||||
populateForm();
|
||||
setupReminderController();
|
||||
setupNameController();
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
final Dialog dialog = super.onCreateDialog(savedInstanceState);
|
||||
final Window window = dialog.getWindow();
|
||||
if (window != null) {
|
||||
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
|
||||
}
|
||||
return dialog;
|
||||
}
|
||||
|
||||
protected int getTitle()
|
||||
{
|
||||
if (originalHabit != null) return R.string.edit_habit;
|
||||
else return R.string.create_habit;
|
||||
}
|
||||
|
||||
protected void saveHabit(@NonNull Habit habit)
|
||||
{
|
||||
if (originalHabit == null)
|
||||
{
|
||||
commandRunner.execute(component
|
||||
.getCreateHabitCommandFactory()
|
||||
.create(habitList, habit), null);
|
||||
}
|
||||
else
|
||||
{
|
||||
commandRunner.execute(component.getEditHabitCommandFactory().
|
||||
create(habitList, originalHabit, habit), originalHabit.getId());
|
||||
}
|
||||
}
|
||||
|
||||
private int getTypeFromArguments()
|
||||
{
|
||||
return getArguments().getInt(BUNDLE_HABIT_TYPE);
|
||||
}
|
||||
|
||||
private void initDependencies()
|
||||
{
|
||||
Context appContext = getContext().getApplicationContext();
|
||||
HabitsApplication app = (HabitsApplication) appContext;
|
||||
|
||||
component = app.getComponent();
|
||||
prefs = component.getPreferences();
|
||||
habitList = component.getHabitList();
|
||||
commandRunner = component.getCommandRunner();
|
||||
modelFactory = component.getModelFactory();
|
||||
}
|
||||
|
||||
@OnClick(R.id.buttonDiscard)
|
||||
void onButtonDiscardClick()
|
||||
{
|
||||
dismiss();
|
||||
}
|
||||
|
||||
@OnClick(R.id.buttonSave)
|
||||
void onSaveButtonClick()
|
||||
{
|
||||
int type = getTypeFromArguments();
|
||||
|
||||
if (!namePanel.validate()) return;
|
||||
if (type == Habit.YES_NO_HABIT && !frequencyPanel.validate()) return;
|
||||
if (type == Habit.NUMBER_HABIT && !targetPanel.validate()) return;
|
||||
|
||||
Habit habit = modelFactory.buildHabit();
|
||||
if( originalHabit != null )
|
||||
habit.copyFrom(originalHabit);
|
||||
habit.setName(namePanel.getName());
|
||||
habit.setDescription(namePanel.getDescription());
|
||||
habit.setColor(namePanel.getColor());
|
||||
habit.setReminder(reminderPanel.getReminder());
|
||||
habit.setFrequency(frequencyPanel.getFrequency());
|
||||
habit.setUnit(targetPanel.getUnit());
|
||||
habit.setTargetValue(targetPanel.getTargetValue());
|
||||
habit.setType(type);
|
||||
|
||||
saveHabit(habit);
|
||||
dismiss();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Habit parseHabitFromArguments()
|
||||
{
|
||||
Bundle arguments = getArguments();
|
||||
if (arguments == null) return null;
|
||||
|
||||
Long id = (Long) arguments.get(BUNDLE_HABIT_ID);
|
||||
if (id == null) return null;
|
||||
|
||||
Habit habit = habitList.getById(id);
|
||||
if (habit == null) throw new IllegalStateException();
|
||||
|
||||
return habit;
|
||||
}
|
||||
|
||||
private void populateForm()
|
||||
{
|
||||
Habit habit = modelFactory.buildHabit();
|
||||
habit.setFrequency(Frequency.DAILY);
|
||||
habit.setColor(prefs.getDefaultHabitColor(habit.getColor()));
|
||||
habit.setType(getTypeFromArguments());
|
||||
|
||||
if (originalHabit != null) habit.copyFrom(originalHabit);
|
||||
|
||||
if (habit.isNumerical()) frequencyPanel.setVisibility(GONE);
|
||||
else targetPanel.setVisibility(GONE);
|
||||
|
||||
namePanel.populateFrom(habit);
|
||||
frequencyPanel.setFrequency(habit.getFrequency());
|
||||
targetPanel.setTargetValue(habit.getTargetValue());
|
||||
targetPanel.setUnit(habit.getUnit());
|
||||
if (habit.hasReminder()) reminderPanel.setReminder(habit.getReminder());
|
||||
}
|
||||
|
||||
private void setupNameController()
|
||||
{
|
||||
namePanel.setController(new NameDescriptionPanel.Controller()
|
||||
{
|
||||
@Override
|
||||
public void onColorPickerClicked(int previousColor)
|
||||
{
|
||||
ColorPickerDialog picker =
|
||||
colorPickerDialogFactory.create(previousColor);
|
||||
|
||||
picker.setListener(c ->
|
||||
{
|
||||
prefs.setDefaultHabitColor(c);
|
||||
namePanel.setColor(c);
|
||||
});
|
||||
|
||||
picker.show(getFragmentManager(), "picker");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void setupReminderController()
|
||||
{
|
||||
reminderPanel.setController(new ReminderPanel.Controller()
|
||||
{
|
||||
@Override
|
||||
public void onTimeClicked(int currentHour, int currentMin)
|
||||
{
|
||||
TimePickerDialog timePicker;
|
||||
boolean is24HourMode = DateFormat.is24HourFormat(getContext());
|
||||
timePicker =
|
||||
TimePickerDialog.newInstance(reminderPanel, currentHour,
|
||||
currentMin, is24HourMode);
|
||||
timePicker.show(getFragmentManager(), "timePicker");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWeekdayClicked(WeekdayList currentDays)
|
||||
{
|
||||
WeekdayPickerDialog dialog = new WeekdayPickerDialog();
|
||||
dialog.setListener(reminderPanel);
|
||||
dialog.setSelectedDays(currentDays);
|
||||
dialog.show(getFragmentManager(), "weekdayPicker");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.habits.edit;
|
||||
|
||||
import android.os.*;
|
||||
import android.support.annotation.*;
|
||||
|
||||
import org.isoron.uhabits.core.models.*;
|
||||
|
||||
import javax.inject.*;
|
||||
|
||||
import static org.isoron.uhabits.activities.habits.edit.EditHabitDialog.*;
|
||||
|
||||
public class EditHabitDialogFactory
|
||||
{
|
||||
@Inject
|
||||
public EditHabitDialogFactory()
|
||||
{
|
||||
}
|
||||
|
||||
public EditHabitDialog createBoolean()
|
||||
{
|
||||
EditHabitDialog dialog = new EditHabitDialog();
|
||||
Bundle args = new Bundle();
|
||||
args.putInt(BUNDLE_HABIT_TYPE, Habit.YES_NO_HABIT);
|
||||
dialog.setArguments(args);
|
||||
return dialog;
|
||||
}
|
||||
|
||||
public EditHabitDialog createNumerical()
|
||||
{
|
||||
EditHabitDialog dialog = new EditHabitDialog();
|
||||
Bundle args = new Bundle();
|
||||
args.putInt(BUNDLE_HABIT_TYPE, Habit.NUMBER_HABIT);
|
||||
dialog.setArguments(args);
|
||||
return dialog;
|
||||
}
|
||||
|
||||
public EditHabitDialog edit(@NonNull Habit habit)
|
||||
{
|
||||
if (habit.getId() == null)
|
||||
throw new IllegalArgumentException("habit not saved");
|
||||
|
||||
EditHabitDialog dialog = new EditHabitDialog();
|
||||
Bundle args = new Bundle();
|
||||
args.putLong(BUNDLE_HABIT_ID, habit.getId());
|
||||
args.putInt(BUNDLE_HABIT_TYPE, habit.getType());
|
||||
dialog.setArguments(args);
|
||||
return dialog;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.habits.edit.views;
|
||||
|
||||
import android.content.*;
|
||||
import android.support.annotation.*;
|
||||
import android.text.*;
|
||||
import android.util.*;
|
||||
import android.view.*;
|
||||
import android.widget.*;
|
||||
|
||||
import org.isoron.androidbase.utils.*;
|
||||
import org.isoron.uhabits.*;
|
||||
|
||||
import static org.isoron.uhabits.utils.AttributeSetUtils.*;
|
||||
|
||||
/**
|
||||
* An EditText that shows an example usage when there is no text
|
||||
* currently set. The example disappears when the widget gains focus.
|
||||
*/
|
||||
public class ExampleEditText extends EditText
|
||||
implements View.OnFocusChangeListener
|
||||
{
|
||||
|
||||
private String example;
|
||||
|
||||
private String realText;
|
||||
|
||||
private int color;
|
||||
|
||||
private int exampleColor;
|
||||
|
||||
private int inputType;
|
||||
|
||||
public ExampleEditText(Context context, @Nullable AttributeSet attrs)
|
||||
{
|
||||
super(context, attrs);
|
||||
|
||||
if (attrs != null)
|
||||
example = getAttribute(context, attrs, "example", "");
|
||||
|
||||
inputType = getInputType();
|
||||
realText = getText().toString();
|
||||
color = getCurrentTextColor();
|
||||
init();
|
||||
}
|
||||
|
||||
public String getRealText()
|
||||
{
|
||||
if(hasFocus()) return getText().toString();
|
||||
else return realText;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFocusChange(View v, boolean hasFocus)
|
||||
{
|
||||
if (!hasFocus) realText = getText().toString();
|
||||
updateText();
|
||||
}
|
||||
|
||||
public void setExample(String example)
|
||||
{
|
||||
this.example = example;
|
||||
updateText();
|
||||
}
|
||||
|
||||
public void setRealText(String realText)
|
||||
{
|
||||
this.realText = realText;
|
||||
updateText();
|
||||
}
|
||||
|
||||
private void init()
|
||||
{
|
||||
StyledResources sr = new StyledResources(getContext());
|
||||
exampleColor = sr.getColor(R.attr.mediumContrastTextColor);
|
||||
setOnFocusChangeListener(this);
|
||||
updateText();
|
||||
}
|
||||
|
||||
private void updateText()
|
||||
{
|
||||
if (realText.isEmpty() && !isFocused())
|
||||
{
|
||||
setTextColor(exampleColor);
|
||||
setText(example);
|
||||
setInputType(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
|
||||
}
|
||||
else
|
||||
{
|
||||
setText(realText);
|
||||
setTextColor(color);
|
||||
setInputType(inputType);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.habits.edit.views;
|
||||
|
||||
import android.annotation.*;
|
||||
import android.content.*;
|
||||
import android.content.res.*;
|
||||
import android.support.annotation.*;
|
||||
import android.util.*;
|
||||
import android.view.*;
|
||||
import android.widget.*;
|
||||
|
||||
import org.isoron.uhabits.R;
|
||||
import org.isoron.uhabits.core.models.*;
|
||||
|
||||
import butterknife.*;
|
||||
|
||||
import static org.isoron.uhabits.R.id.*;
|
||||
|
||||
|
||||
public class FrequencyPanel extends FrameLayout
|
||||
{
|
||||
@BindView(numerator)
|
||||
TextView tvNumerator;
|
||||
|
||||
@BindView(R.id.denominator)
|
||||
TextView tvDenominator;
|
||||
|
||||
@BindView(R.id.spinner)
|
||||
Spinner spinner;
|
||||
|
||||
@BindView(R.id.customFreqPanel)
|
||||
ViewGroup customFreqPanel;
|
||||
|
||||
public FrequencyPanel(@NonNull Context context,
|
||||
@Nullable AttributeSet attrs)
|
||||
{
|
||||
super(context, attrs);
|
||||
|
||||
View view = inflate(context, R.layout.edit_habit_frequency, null);
|
||||
ButterKnife.bind(this, view);
|
||||
addView(view);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Frequency getFrequency()
|
||||
{
|
||||
String freqNum = tvNumerator.getText().toString();
|
||||
String freqDen = tvDenominator.getText().toString();
|
||||
|
||||
if (!freqNum.isEmpty() && !freqDen.isEmpty())
|
||||
{
|
||||
int numerator = Integer.parseInt(freqNum);
|
||||
int denominator = Integer.parseInt(freqDen);
|
||||
return new Frequency(numerator, denominator);
|
||||
}
|
||||
|
||||
return Frequency.DAILY;
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
public void setFrequency(@NonNull Frequency freq)
|
||||
{
|
||||
int position = getQuickSelectPosition(freq);
|
||||
|
||||
if (position >= 0) showSimplifiedFrequency(position);
|
||||
else showCustomFrequency();
|
||||
|
||||
tvNumerator.setText(Integer.toString(freq.getNumerator()));
|
||||
tvDenominator.setText(Integer.toString(freq.getDenominator()));
|
||||
}
|
||||
|
||||
@OnItemSelected(R.id.spinner)
|
||||
public void onFrequencySelected(int position)
|
||||
{
|
||||
if (position < 0 || position > 4) throw new IllegalArgumentException();
|
||||
int freqNums[] = { 1, 1, 2, 5, 3 };
|
||||
int freqDens[] = { 1, 7, 7, 7, 7 };
|
||||
setFrequency(new Frequency(freqNums[position], freqDens[position]));
|
||||
}
|
||||
|
||||
public boolean validate()
|
||||
{
|
||||
boolean valid = true;
|
||||
Resources res = getResources();
|
||||
|
||||
String freqNum = tvNumerator.getText().toString();
|
||||
String freqDen = tvDenominator.getText().toString();
|
||||
|
||||
if (freqDen.isEmpty())
|
||||
{
|
||||
tvDenominator.setError(
|
||||
res.getString(R.string.validation_show_not_be_blank));
|
||||
valid = false;
|
||||
}
|
||||
|
||||
if (freqNum.isEmpty())
|
||||
{
|
||||
tvNumerator.setError(
|
||||
res.getString(R.string.validation_show_not_be_blank));
|
||||
valid = false;
|
||||
}
|
||||
|
||||
if (!valid) return false;
|
||||
|
||||
int numerator = Integer.parseInt(freqNum);
|
||||
int denominator = Integer.parseInt(freqDen);
|
||||
|
||||
if (numerator <= 0)
|
||||
{
|
||||
tvNumerator.setError(
|
||||
res.getString(R.string.validation_number_should_be_positive));
|
||||
valid = false;
|
||||
}
|
||||
|
||||
if (numerator > denominator)
|
||||
{
|
||||
tvNumerator.setError(
|
||||
res.getString(R.string.validation_at_most_one_rep_per_day));
|
||||
valid = false;
|
||||
}
|
||||
|
||||
return valid;
|
||||
}
|
||||
|
||||
private int getQuickSelectPosition(@NonNull Frequency freq)
|
||||
{
|
||||
if (freq.equals(Frequency.DAILY)) return 0;
|
||||
if (freq.equals(Frequency.WEEKLY)) return 1;
|
||||
if (freq.equals(Frequency.TWO_TIMES_PER_WEEK)) return 2;
|
||||
if (freq.equals(Frequency.FIVE_TIMES_PER_WEEK)) return 3;
|
||||
return -1;
|
||||
}
|
||||
|
||||
private void showCustomFrequency()
|
||||
{
|
||||
spinner.setVisibility(View.GONE);
|
||||
customFreqPanel.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
private void showSimplifiedFrequency(int quickSelectPosition)
|
||||
{
|
||||
spinner.setVisibility(View.VISIBLE);
|
||||
spinner.setSelection(quickSelectPosition);
|
||||
customFreqPanel.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.habits.edit.views;
|
||||
|
||||
import android.content.*;
|
||||
import android.content.res.*;
|
||||
import android.os.*;
|
||||
import android.support.annotation.*;
|
||||
import android.util.*;
|
||||
import android.view.*;
|
||||
import android.widget.*;
|
||||
|
||||
import org.isoron.uhabits.R;
|
||||
import org.isoron.uhabits.activities.common.views.*;
|
||||
import org.isoron.uhabits.core.models.*;
|
||||
import org.isoron.uhabits.utils.*;
|
||||
|
||||
import butterknife.*;
|
||||
|
||||
|
||||
public class NameDescriptionPanel extends FrameLayout
|
||||
{
|
||||
@BindView(R.id.tvName)
|
||||
EditText tvName;
|
||||
|
||||
@BindView(R.id.tvDescription)
|
||||
ExampleEditText tvDescription;
|
||||
|
||||
private int color;
|
||||
|
||||
@NonNull
|
||||
private Controller controller;
|
||||
|
||||
public NameDescriptionPanel(@NonNull Context context,
|
||||
@Nullable AttributeSet attrs)
|
||||
{
|
||||
super(context, attrs);
|
||||
|
||||
View view = inflate(context, R.layout.edit_habit_name, null);
|
||||
ButterKnife.bind(this, view);
|
||||
addView(view);
|
||||
|
||||
controller = new Controller() {};
|
||||
}
|
||||
|
||||
public int getColor()
|
||||
{
|
||||
return color;
|
||||
}
|
||||
|
||||
public void setColor(int color)
|
||||
{
|
||||
this.color = color;
|
||||
tvName.setTextColor(PaletteUtils.getColor(getContext(), color));
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public String getDescription()
|
||||
{
|
||||
return tvDescription.getRealText().trim();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public String getName()
|
||||
{
|
||||
return tvName.getText().toString().trim();
|
||||
}
|
||||
|
||||
public void populateFrom(@NonNull Habit habit)
|
||||
{
|
||||
Resources res = getResources();
|
||||
|
||||
if(habit.isNumerical())
|
||||
tvDescription.setExample(res.getString(R.string.example_question_numerical));
|
||||
else
|
||||
tvDescription.setExample(res.getString(R.string.example_question_boolean));
|
||||
|
||||
setColor(habit.getColor());
|
||||
tvName.setText(habit.getName());
|
||||
tvDescription.setRealText(habit.getDescription());
|
||||
}
|
||||
|
||||
public boolean validate()
|
||||
{
|
||||
Resources res = getResources();
|
||||
|
||||
if (getName().isEmpty())
|
||||
{
|
||||
tvName.setError(
|
||||
res.getString(R.string.validation_name_should_not_be_blank));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRestoreInstanceState(Parcelable state)
|
||||
{
|
||||
BundleSavedState bss = (BundleSavedState) state;
|
||||
setColor(bss.bundle.getInt("color"));
|
||||
super.onRestoreInstanceState(bss.getSuperState());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Parcelable onSaveInstanceState()
|
||||
{
|
||||
Parcelable superState = super.onSaveInstanceState();
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putInt("color", color);
|
||||
return new BundleSavedState(superState, bundle);
|
||||
}
|
||||
|
||||
@OnClick(R.id.buttonPickColor)
|
||||
void showColorPicker()
|
||||
{
|
||||
controller.onColorPickerClicked(color);
|
||||
}
|
||||
|
||||
public void setController(@NonNull Controller controller)
|
||||
{
|
||||
this.controller = controller;
|
||||
}
|
||||
|
||||
public interface Controller
|
||||
{
|
||||
/**
|
||||
* Called when the user has clicked the widget to select a new
|
||||
* color for the habit.
|
||||
*
|
||||
* @param previousColor the color previously selected
|
||||
*/
|
||||
default void onColorPickerClicked(int previousColor) {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.habits.edit.views;
|
||||
|
||||
import android.content.*;
|
||||
import android.os.*;
|
||||
import android.support.annotation.*;
|
||||
import android.util.*;
|
||||
import android.view.*;
|
||||
import android.widget.*;
|
||||
|
||||
import com.android.datetimepicker.time.*;
|
||||
|
||||
import org.isoron.uhabits.R;
|
||||
import org.isoron.uhabits.activities.common.dialogs.*;
|
||||
import org.isoron.uhabits.activities.common.views.*;
|
||||
import org.isoron.uhabits.core.models.*;
|
||||
import org.isoron.uhabits.utils.*;
|
||||
|
||||
import butterknife.*;
|
||||
|
||||
public class ReminderPanel extends FrameLayout
|
||||
implements TimePickerDialog.OnTimeSetListener,
|
||||
WeekdayPickerDialog.OnWeekdaysPickedListener
|
||||
{
|
||||
@BindView(R.id.tvReminderTime)
|
||||
TextView tvReminderTime;
|
||||
|
||||
@BindView(R.id.llReminderDays)
|
||||
ViewGroup llReminderDays;
|
||||
|
||||
@BindView(R.id.tvReminderDays)
|
||||
TextView tvReminderDays;
|
||||
|
||||
@Nullable
|
||||
private Reminder reminder;
|
||||
|
||||
@NonNull
|
||||
private Controller controller;
|
||||
|
||||
public ReminderPanel(@NonNull Context context, @Nullable AttributeSet attrs)
|
||||
{
|
||||
super(context, attrs);
|
||||
|
||||
View view = inflate(context, R.layout.edit_habit_reminder, null);
|
||||
ButterKnife.bind(this, view);
|
||||
addView(view);
|
||||
|
||||
controller = new Controller() {};
|
||||
setReminder(null);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Reminder getReminder()
|
||||
{
|
||||
return reminder;
|
||||
}
|
||||
|
||||
public void setReminder(@Nullable Reminder reminder)
|
||||
{
|
||||
this.reminder = reminder;
|
||||
|
||||
if (reminder == null)
|
||||
{
|
||||
tvReminderTime.setText(R.string.reminder_off);
|
||||
llReminderDays.setVisibility(View.GONE);
|
||||
return;
|
||||
}
|
||||
|
||||
Context ctx = getContext();
|
||||
String time = AndroidDateUtils.formatTime(ctx, reminder.getHour(), reminder.getMinute());
|
||||
tvReminderTime.setText(time);
|
||||
llReminderDays.setVisibility(View.VISIBLE);
|
||||
|
||||
boolean weekdays[] = reminder.getDays().toArray();
|
||||
tvReminderDays.setText(AndroidDateUtils.formatWeekdayList(ctx, weekdays));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTimeCleared(RadialPickerLayout view)
|
||||
{
|
||||
setReminder(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTimeSet(RadialPickerLayout view, int hour, int minute)
|
||||
{
|
||||
WeekdayList days = WeekdayList.EVERY_DAY;
|
||||
if (reminder != null) days = reminder.getDays();
|
||||
setReminder(new Reminder(hour, minute, days));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWeekdaysSet(WeekdayList selectedDays)
|
||||
{
|
||||
if (reminder == null) return;
|
||||
if (selectedDays.isEmpty()) selectedDays = WeekdayList.EVERY_DAY;
|
||||
|
||||
setReminder(new Reminder(reminder.getHour(), reminder.getMinute(),
|
||||
selectedDays));
|
||||
}
|
||||
|
||||
public void setController(@NonNull Controller controller)
|
||||
{
|
||||
this.controller = controller;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRestoreInstanceState(Parcelable state)
|
||||
{
|
||||
BundleSavedState bss = (BundleSavedState) state;
|
||||
if (!bss.bundle.isEmpty())
|
||||
{
|
||||
int days = bss.bundle.getInt("days");
|
||||
int hour = bss.bundle.getInt("hour");
|
||||
int minute = bss.bundle.getInt("minute");
|
||||
reminder = new Reminder(hour, minute, new WeekdayList(days));
|
||||
setReminder(reminder);
|
||||
}
|
||||
super.onRestoreInstanceState(bss.getSuperState());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Parcelable onSaveInstanceState()
|
||||
{
|
||||
Parcelable superState = super.onSaveInstanceState();
|
||||
Bundle bundle = new Bundle();
|
||||
if (reminder != null)
|
||||
{
|
||||
bundle.putInt("days", reminder.getDays().toInteger());
|
||||
bundle.putInt("hour", reminder.getHour());
|
||||
bundle.putInt("minute", reminder.getMinute());
|
||||
}
|
||||
return new BundleSavedState(superState, bundle);
|
||||
}
|
||||
|
||||
@OnClick(R.id.tvReminderTime)
|
||||
void onDateSpinnerClick()
|
||||
{
|
||||
int hour = 8;
|
||||
int min = 0;
|
||||
|
||||
if (reminder != null)
|
||||
{
|
||||
hour = reminder.getHour();
|
||||
min = reminder.getMinute();
|
||||
}
|
||||
|
||||
controller.onTimeClicked(hour, min);
|
||||
}
|
||||
|
||||
@OnClick(R.id.tvReminderDays)
|
||||
void onWeekdayClicked()
|
||||
{
|
||||
if (reminder == null) return;
|
||||
controller.onWeekdayClicked(reminder.getDays());
|
||||
}
|
||||
|
||||
public interface Controller
|
||||
{
|
||||
/**
|
||||
* Called when the user has clicked the widget to change the time of
|
||||
* the reminder.
|
||||
*
|
||||
* @param currentHour hour previously picked by the user
|
||||
* @param currentMin minute previously picked by the user
|
||||
*/
|
||||
default void onTimeClicked(int currentHour, int currentMin) {}
|
||||
|
||||
/**
|
||||
* Called when the used has clicked the widget to change the days
|
||||
* of the reminder.
|
||||
*
|
||||
* @param currentDays days previously selected by the user.
|
||||
*/
|
||||
default void onWeekdayClicked(WeekdayList currentDays) {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.habits.edit.views;
|
||||
|
||||
import android.content.*;
|
||||
import android.content.res.*;
|
||||
import android.support.annotation.*;
|
||||
import android.util.*;
|
||||
import android.view.*;
|
||||
import android.widget.*;
|
||||
|
||||
import org.isoron.uhabits.R;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
|
||||
import butterknife.*;
|
||||
|
||||
|
||||
public class TargetPanel extends FrameLayout
|
||||
{
|
||||
private DecimalFormat valueFormatter = new DecimalFormat("#.##");
|
||||
|
||||
@BindView(R.id.tvUnit)
|
||||
ExampleEditText tvUnit;
|
||||
|
||||
@BindView(R.id.tvTargetCount)
|
||||
TextView tvTargetValue;
|
||||
|
||||
public TargetPanel(@NonNull Context context, @Nullable AttributeSet attrs)
|
||||
{
|
||||
super(context, attrs);
|
||||
|
||||
View view = inflate(context, R.layout.edit_habit_target, null);
|
||||
ButterKnife.bind(this, view);
|
||||
addView(view);
|
||||
}
|
||||
|
||||
public double getTargetValue()
|
||||
{
|
||||
String sValue = tvTargetValue.getText().toString();
|
||||
return Double.parseDouble(sValue);
|
||||
}
|
||||
|
||||
public void setTargetValue(double targetValue)
|
||||
{
|
||||
tvTargetValue.setText(valueFormatter.format(targetValue));
|
||||
}
|
||||
|
||||
public String getUnit()
|
||||
{
|
||||
return tvUnit.getRealText();
|
||||
}
|
||||
|
||||
public void setUnit(String unit)
|
||||
{
|
||||
tvUnit.setRealText(unit);
|
||||
}
|
||||
|
||||
public boolean validate()
|
||||
{
|
||||
Resources res = getResources();
|
||||
String sValue = tvTargetValue.getText().toString();
|
||||
double value = Double.parseDouble(sValue);
|
||||
|
||||
if (value <= 0)
|
||||
{
|
||||
tvTargetValue.setError(
|
||||
res.getString(R.string.validation_number_should_be_positive));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.habits.list
|
||||
|
||||
import android.os.*
|
||||
import org.isoron.uhabits.activities.*
|
||||
import org.isoron.uhabits.activities.habits.list.views.*
|
||||
import org.isoron.uhabits.core.preferences.*
|
||||
import org.isoron.uhabits.core.ui.ThemeSwitcher.*
|
||||
import org.isoron.uhabits.core.utils.*
|
||||
|
||||
class ListHabitsActivity : HabitsActivity() {
|
||||
|
||||
var pureBlack: Boolean = false
|
||||
lateinit var adapter: HabitCardListAdapter
|
||||
lateinit var rootView: ListHabitsRootView
|
||||
lateinit var screen: ListHabitsScreen
|
||||
lateinit var prefs: Preferences
|
||||
lateinit var midnightTimer: MidnightTimer
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
prefs = appComponent.preferences
|
||||
pureBlack = prefs.isPureBlackEnabled
|
||||
midnightTimer = appComponent.midnightTimer
|
||||
rootView = component.listHabitsRootView
|
||||
screen = component.listHabitsScreen
|
||||
adapter = component.habitCardListAdapter
|
||||
|
||||
setScreen(screen)
|
||||
component.listHabitsBehavior.onStartup()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
midnightTimer.onPause()
|
||||
screen.onDettached()
|
||||
adapter.cancelRefresh()
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
adapter.refresh()
|
||||
screen.onAttached()
|
||||
rootView.postInvalidate()
|
||||
midnightTimer.onResume()
|
||||
|
||||
if (prefs.theme == THEME_DARK && prefs.isPureBlackEnabled != pureBlack) {
|
||||
restartWithFade(ListHabitsActivity::class.java)
|
||||
}
|
||||
|
||||
super.onResume()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.habits.list
|
||||
|
||||
import android.view.*
|
||||
import org.isoron.androidbase.activities.*
|
||||
import org.isoron.uhabits.*
|
||||
import org.isoron.uhabits.core.preferences.*
|
||||
import org.isoron.uhabits.core.ui.*
|
||||
import org.isoron.uhabits.core.ui.screens.habits.list.*
|
||||
import javax.inject.*
|
||||
|
||||
@ActivityScope
|
||||
class ListHabitsMenu @Inject constructor(
|
||||
activity: BaseActivity,
|
||||
private val preferences: Preferences,
|
||||
private val themeSwitcher: ThemeSwitcher,
|
||||
private val behavior: ListHabitsMenuBehavior
|
||||
) : BaseMenu(activity) {
|
||||
|
||||
override fun onCreate(menu: Menu) {
|
||||
val nightModeItem = menu.findItem(R.id.actionToggleNightMode)
|
||||
val hideArchivedItem = menu.findItem(R.id.actionHideArchived)
|
||||
val hideCompletedItem = menu.findItem(R.id.actionHideCompleted)
|
||||
nightModeItem.isChecked = themeSwitcher.isNightMode
|
||||
hideArchivedItem.isChecked = !preferences.showArchived
|
||||
hideCompletedItem.isChecked = !preferences.showCompleted
|
||||
}
|
||||
|
||||
override fun onItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.actionToggleNightMode -> {
|
||||
behavior.onToggleNightMode()
|
||||
return true
|
||||
}
|
||||
|
||||
R.id.actionAdd -> {
|
||||
behavior.onCreateHabit()
|
||||
return true
|
||||
}
|
||||
|
||||
R.id.actionFAQ -> {
|
||||
behavior.onViewFAQ()
|
||||
return true
|
||||
}
|
||||
|
||||
R.id.actionAbout -> {
|
||||
behavior.onViewAbout()
|
||||
return true
|
||||
}
|
||||
|
||||
R.id.actionSettings -> {
|
||||
behavior.onViewSettings()
|
||||
return true
|
||||
}
|
||||
|
||||
R.id.actionHideArchived -> {
|
||||
behavior.onToggleShowArchived()
|
||||
invalidate()
|
||||
return true
|
||||
}
|
||||
|
||||
R.id.actionHideCompleted -> {
|
||||
behavior.onToggleShowCompleted()
|
||||
invalidate()
|
||||
return true
|
||||
}
|
||||
|
||||
R.id.actionSortColor -> {
|
||||
behavior.onSortByColor()
|
||||
return true
|
||||
}
|
||||
|
||||
R.id.actionSortManual -> {
|
||||
behavior.onSortByManually()
|
||||
return true
|
||||
}
|
||||
|
||||
R.id.actionSortName -> {
|
||||
behavior.onSortByName()
|
||||
return true
|
||||
}
|
||||
|
||||
R.id.actionSortScore -> {
|
||||
behavior.onSortByScore()
|
||||
return true
|
||||
}
|
||||
|
||||
else -> return false
|
||||
}
|
||||
}
|
||||
|
||||
override fun getMenuResourceId() = R.menu.list_habits
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.habits.list
|
||||
|
||||
import android.content.*
|
||||
import dagger.*
|
||||
import org.isoron.androidbase.*
|
||||
import org.isoron.uhabits.activities.*
|
||||
import org.isoron.uhabits.activities.habits.list.views.*
|
||||
import org.isoron.uhabits.core.ui.screens.habits.list.*
|
||||
import javax.inject.*
|
||||
|
||||
class BugReporterProxy
|
||||
@Inject constructor(
|
||||
@AppContext context: Context
|
||||
) : AndroidBugReporter(context), ListHabitsBehavior.BugReporter
|
||||
|
||||
@Module
|
||||
abstract class ListHabitsModule {
|
||||
|
||||
@Binds
|
||||
abstract fun getAdapter(adapter: HabitCardListAdapter): ListHabitsMenuBehavior.Adapter
|
||||
|
||||
@Binds
|
||||
abstract fun getBugReporter(proxy: BugReporterProxy): ListHabitsBehavior.BugReporter
|
||||
|
||||
@Binds
|
||||
abstract fun getMenuScreen(screen: ListHabitsScreen): ListHabitsMenuBehavior.Screen
|
||||
|
||||
@Binds
|
||||
abstract fun getScreen(screen: ListHabitsScreen): ListHabitsBehavior.Screen
|
||||
|
||||
@Binds
|
||||
abstract fun getSelMenuAdapter(adapter: HabitCardListAdapter): ListHabitsSelectionMenuBehavior.Adapter
|
||||
|
||||
@Binds
|
||||
abstract fun getSelMenuScreen(screen: ListHabitsScreen): ListHabitsSelectionMenuBehavior.Screen
|
||||
|
||||
@Binds
|
||||
abstract fun getSystem(system: HabitsDirFinder): ListHabitsBehavior.DirFinder
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.habits.list
|
||||
|
||||
import android.content.*
|
||||
import android.os.Build.VERSION.*
|
||||
import android.os.Build.VERSION_CODES.*
|
||||
import android.support.v7.widget.Toolbar
|
||||
import android.view.ViewGroup.LayoutParams.*
|
||||
import android.widget.*
|
||||
import org.isoron.androidbase.activities.*
|
||||
import org.isoron.uhabits.*
|
||||
import org.isoron.uhabits.activities.common.views.*
|
||||
import org.isoron.uhabits.activities.habits.list.views.*
|
||||
import org.isoron.uhabits.core.models.*
|
||||
import org.isoron.uhabits.core.preferences.*
|
||||
import org.isoron.uhabits.core.tasks.*
|
||||
import org.isoron.uhabits.core.ui.screens.habits.list.*
|
||||
import org.isoron.uhabits.core.utils.*
|
||||
import org.isoron.uhabits.utils.*
|
||||
import java.lang.Math.*
|
||||
import javax.inject.*
|
||||
|
||||
const val MAX_CHECKMARK_COUNT = 60
|
||||
|
||||
@ActivityScope
|
||||
class ListHabitsRootView @Inject constructor(
|
||||
@ActivityContext context: Context,
|
||||
hintListFactory: HintListFactory,
|
||||
preferences: Preferences,
|
||||
midnightTimer: MidnightTimer,
|
||||
runner: TaskRunner,
|
||||
private val listAdapter: HabitCardListAdapter,
|
||||
habitCardListViewFactory: HabitCardListViewFactory
|
||||
) : BaseRootView(context), ModelObservable.Listener {
|
||||
|
||||
val listView: HabitCardListView = habitCardListViewFactory.create()
|
||||
val llEmpty = EmptyListView(context)
|
||||
val tbar = buildToolbar()
|
||||
val progressBar = TaskProgressBar(context, runner)
|
||||
val hintView: HintView
|
||||
val header = HeaderView(context, preferences, midnightTimer)
|
||||
|
||||
init {
|
||||
val hints = resources.getStringArray(R.array.hints)
|
||||
val hintList = hintListFactory.create(hints)
|
||||
hintView = HintView(context, hintList)
|
||||
|
||||
addView(RelativeLayout(context).apply {
|
||||
background = sres.getDrawable(R.attr.windowBackgroundColor)
|
||||
addAtTop(tbar)
|
||||
addBelow(header, tbar)
|
||||
addBelow(listView, header, height = MATCH_PARENT)
|
||||
addBelow(llEmpty, header, height = MATCH_PARENT)
|
||||
addBelow(progressBar, header) {
|
||||
it.topMargin = dp(-6.0f).toInt()
|
||||
}
|
||||
addAtBottom(hintView)
|
||||
if (SDK_INT < LOLLIPOP) {
|
||||
addBelow(ShadowView(context), tbar)
|
||||
addBelow(ShadowView(context), header)
|
||||
}
|
||||
}, MATCH_PARENT, MATCH_PARENT)
|
||||
|
||||
listAdapter.setListView(listView)
|
||||
initToolbar()
|
||||
}
|
||||
|
||||
override fun getToolbar(): Toolbar {
|
||||
return tbar
|
||||
}
|
||||
|
||||
override fun onModelChange() {
|
||||
updateEmptyView()
|
||||
}
|
||||
|
||||
private fun setupControllers() {
|
||||
header.setScrollController(object : ScrollableChart.ScrollController {
|
||||
override fun onDataOffsetChanged(newDataOffset: Int) {
|
||||
listView.dataOffset = newDataOffset
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
setupControllers()
|
||||
listAdapter.observable.addListener(this)
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
listAdapter.observable.removeListener(this)
|
||||
super.onDetachedFromWindow()
|
||||
}
|
||||
|
||||
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
||||
val count = getCheckmarkCount()
|
||||
header.buttonCount = count
|
||||
header.setMaxDataOffset(max(MAX_CHECKMARK_COUNT - count, 0))
|
||||
listView.checkmarkCount = count
|
||||
super.onSizeChanged(w, h, oldw, oldh)
|
||||
}
|
||||
|
||||
private fun getCheckmarkCount(): Int {
|
||||
val nameWidth = dim(R.dimen.habitNameWidth)
|
||||
val buttonWidth = dim(R.dimen.checkmarkWidth)
|
||||
val labelWidth = max((measuredWidth / 3).toFloat(), nameWidth)
|
||||
val buttonCount = ((measuredWidth - labelWidth) / buttonWidth).toInt()
|
||||
return min(MAX_CHECKMARK_COUNT, max(0, buttonCount))
|
||||
}
|
||||
|
||||
private fun updateEmptyView() {
|
||||
llEmpty.visibility = when (listAdapter.itemCount) {
|
||||
0 -> VISIBLE
|
||||
else -> GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.habits.list
|
||||
|
||||
import android.app.*
|
||||
import android.content.*
|
||||
import android.support.annotation.*
|
||||
import dagger.*
|
||||
import org.isoron.androidbase.activities.*
|
||||
import org.isoron.androidbase.utils.*
|
||||
import org.isoron.uhabits.*
|
||||
import org.isoron.uhabits.activities.common.dialogs.*
|
||||
import org.isoron.uhabits.activities.habits.edit.*
|
||||
import org.isoron.uhabits.activities.habits.list.views.*
|
||||
import org.isoron.uhabits.core.commands.*
|
||||
import org.isoron.uhabits.core.models.*
|
||||
import org.isoron.uhabits.core.preferences.*
|
||||
import org.isoron.uhabits.core.tasks.*
|
||||
import org.isoron.uhabits.core.ui.*
|
||||
import org.isoron.uhabits.core.ui.callbacks.*
|
||||
import org.isoron.uhabits.core.ui.screens.habits.list.*
|
||||
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior.Message.*
|
||||
import org.isoron.uhabits.intents.*
|
||||
import org.isoron.uhabits.tasks.*
|
||||
import java.io.*
|
||||
import javax.inject.*
|
||||
|
||||
const val RESULT_IMPORT_DATA = 1
|
||||
const val RESULT_EXPORT_CSV = 2
|
||||
const val RESULT_EXPORT_DB = 3
|
||||
const val RESULT_BUG_REPORT = 4
|
||||
const val RESULT_REPAIR_DB = 5
|
||||
const val REQUEST_OPEN_DOCUMENT = 6
|
||||
const val REQUEST_SETTINGS = 7
|
||||
|
||||
@ActivityScope
|
||||
class ListHabitsScreen
|
||||
@Inject constructor(
|
||||
activity: BaseActivity,
|
||||
rootView: ListHabitsRootView,
|
||||
private val commandRunner: CommandRunner,
|
||||
private val intentFactory: IntentFactory,
|
||||
private val themeSwitcher: ThemeSwitcher,
|
||||
private val preferences: Preferences,
|
||||
private val adapter: HabitCardListAdapter,
|
||||
private val taskRunner: TaskRunner,
|
||||
private val exportDBFactory: ExportDBTaskFactory,
|
||||
private val importTaskFactory: ImportDataTaskFactory,
|
||||
private val confirmDeleteDialogFactory: ConfirmDeleteDialogFactory,
|
||||
private val colorPickerFactory: ColorPickerDialogFactory,
|
||||
private val editHabitDialogFactory: EditHabitDialogFactory,
|
||||
private val numberPickerFactory: NumberPickerFactory,
|
||||
private val behavior: Lazy<ListHabitsBehavior>,
|
||||
private val menu: Lazy<ListHabitsMenu>,
|
||||
private val selectionMenu: Lazy<ListHabitsSelectionMenu>
|
||||
) : BaseScreen(activity),
|
||||
CommandRunner.Listener,
|
||||
ListHabitsBehavior.Screen,
|
||||
ListHabitsMenuBehavior.Screen,
|
||||
ListHabitsSelectionMenuBehavior.Screen {
|
||||
|
||||
init {
|
||||
setRootView(rootView)
|
||||
}
|
||||
|
||||
fun onAttached() {
|
||||
setMenu(menu.get())
|
||||
setSelectionMenu(selectionMenu.get())
|
||||
commandRunner.addListener(this)
|
||||
}
|
||||
|
||||
fun onDettached() {
|
||||
commandRunner.removeListener(this)
|
||||
}
|
||||
|
||||
override fun onCommandExecuted(command: Command, refreshKey: Long?) {
|
||||
if (command.isRemote) return
|
||||
showMessage(getExecuteString(command))
|
||||
}
|
||||
|
||||
override fun onResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
when (requestCode) {
|
||||
REQUEST_OPEN_DOCUMENT -> onOpenDocumentResult(resultCode, data)
|
||||
REQUEST_SETTINGS -> onSettingsResult(resultCode)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onOpenDocumentResult(resultCode: Int, data: Intent?) {
|
||||
if (data == null) return
|
||||
if (resultCode != Activity.RESULT_OK) return
|
||||
try {
|
||||
val inStream = activity.contentResolver.openInputStream(data.data)
|
||||
val cacheDir = activity.externalCacheDir
|
||||
val tempFile = File.createTempFile("import", "", cacheDir)
|
||||
FileUtils.copy(inStream, tempFile)
|
||||
onImportData(tempFile) { tempFile.delete() }
|
||||
} catch (e: IOException) {
|
||||
showMessage(R.string.could_not_import)
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onSettingsResult(resultCode: Int) {
|
||||
when (resultCode) {
|
||||
RESULT_IMPORT_DATA -> showImportScreen()
|
||||
RESULT_EXPORT_CSV -> behavior.get().onExportCSV()
|
||||
RESULT_EXPORT_DB -> onExportDB()
|
||||
RESULT_BUG_REPORT -> behavior.get().onSendBugReport()
|
||||
RESULT_REPAIR_DB -> behavior.get().onRepairDB()
|
||||
}
|
||||
}
|
||||
|
||||
override fun applyTheme() {
|
||||
themeSwitcher.apply()
|
||||
activity.restartWithFade(ListHabitsActivity::class.java)
|
||||
}
|
||||
|
||||
override fun showAboutScreen() {
|
||||
val intent = intentFactory.startAboutActivity(activity)
|
||||
activity.startActivity(intent)
|
||||
}
|
||||
|
||||
fun showCreateBooleanHabitScreen() {
|
||||
val dialog = editHabitDialogFactory.createBoolean()
|
||||
activity.showDialog(dialog, "editHabit")
|
||||
}
|
||||
|
||||
override fun showCreateHabitScreen() {
|
||||
if (!preferences.isNumericalHabitsFeatureEnabled) {
|
||||
showCreateBooleanHabitScreen()
|
||||
return
|
||||
}
|
||||
|
||||
val dialog = AlertDialog.Builder(activity)
|
||||
.setTitle("Type of habit")
|
||||
.setItems(R.array.habitTypes) { _, which ->
|
||||
if (which == 0) showCreateBooleanHabitScreen()
|
||||
else showCreateNumericalHabitScreen()
|
||||
}
|
||||
.create()
|
||||
|
||||
dialog.show()
|
||||
}
|
||||
|
||||
override fun showDeleteConfirmationScreen(callback: OnConfirmedCallback) {
|
||||
activity.showDialog(confirmDeleteDialogFactory.create(callback))
|
||||
}
|
||||
|
||||
override fun showEditHabitsScreen(habits: List<Habit>) {
|
||||
val dialog = editHabitDialogFactory.edit(habits[0])
|
||||
activity.showDialog(dialog, "editNumericalHabit")
|
||||
}
|
||||
|
||||
override fun showFAQScreen() {
|
||||
val intent = intentFactory.viewFAQ(activity)
|
||||
activity.startActivity(intent)
|
||||
}
|
||||
|
||||
override fun showHabitScreen(habit: Habit) {
|
||||
val intent = intentFactory.startShowHabitActivity(activity, habit)
|
||||
activity.startActivity(intent)
|
||||
}
|
||||
|
||||
fun showImportScreen() {
|
||||
val intent = intentFactory.openDocument()
|
||||
activity.startActivityForResult(intent, REQUEST_OPEN_DOCUMENT)
|
||||
}
|
||||
|
||||
override fun showIntroScreen() {
|
||||
val intent = intentFactory.startIntroActivity(activity)
|
||||
activity.startActivity(intent)
|
||||
}
|
||||
|
||||
override fun showMessage(m: ListHabitsBehavior.Message) {
|
||||
showMessage(when (m) {
|
||||
COULD_NOT_EXPORT -> R.string.could_not_export
|
||||
IMPORT_SUCCESSFUL -> R.string.habits_imported
|
||||
IMPORT_FAILED -> R.string.could_not_import
|
||||
DATABASE_REPAIRED -> R.string.database_repaired
|
||||
COULD_NOT_GENERATE_BUG_REPORT -> R.string.bug_report_failed
|
||||
FILE_NOT_RECOGNIZED -> R.string.file_not_recognized
|
||||
})
|
||||
}
|
||||
|
||||
override fun showSendBugReportToDeveloperScreen(log: String) {
|
||||
val to = R.string.bugReportTo
|
||||
val subject = R.string.bugReportSubject
|
||||
showSendEmailScreen(to, subject, log)
|
||||
}
|
||||
|
||||
override fun showSettingsScreen() {
|
||||
val intent = intentFactory.startSettingsActivity(activity)
|
||||
activity.startActivityForResult(intent, REQUEST_SETTINGS)
|
||||
}
|
||||
|
||||
override fun showColorPicker(defaultColor: Int,
|
||||
callback: OnColorPickedCallback) {
|
||||
val picker = colorPickerFactory.create(defaultColor)
|
||||
picker.setListener(callback)
|
||||
activity.showDialog(picker, "picker")
|
||||
}
|
||||
|
||||
override fun showNumberPicker(value: Double,
|
||||
unit: String,
|
||||
callback: ListHabitsBehavior.NumberPickerCallback) {
|
||||
numberPickerFactory.create(value, unit, callback).show()
|
||||
}
|
||||
|
||||
@StringRes
|
||||
private fun getExecuteString(command: Command): Int? {
|
||||
when (command) {
|
||||
is ArchiveHabitsCommand -> return R.string.toast_habit_archived
|
||||
is ChangeHabitColorCommand -> return R.string.toast_habit_changed
|
||||
is CreateHabitCommand -> return R.string.toast_habit_created
|
||||
is DeleteHabitsCommand -> return R.string.toast_habit_deleted
|
||||
is EditHabitCommand -> return R.string.toast_habit_changed
|
||||
is UnarchiveHabitsCommand -> return R.string.toast_habit_unarchived
|
||||
else -> return null
|
||||
}
|
||||
}
|
||||
|
||||
private fun showCreateNumericalHabitScreen() {
|
||||
val dialog = editHabitDialogFactory.createNumerical()
|
||||
activity.showDialog(dialog, "editHabit")
|
||||
}
|
||||
|
||||
private fun onImportData(file: File, onFinished: () -> Unit) {
|
||||
taskRunner.execute(importTaskFactory.create(file) { result ->
|
||||
if (result == ImportDataTask.SUCCESS) {
|
||||
adapter.refresh()
|
||||
showMessage(R.string.habits_imported)
|
||||
} else if (result == ImportDataTask.NOT_RECOGNIZED) {
|
||||
showMessage(R.string.file_not_recognized)
|
||||
} else {
|
||||
showMessage(R.string.could_not_import)
|
||||
}
|
||||
onFinished()
|
||||
})
|
||||
}
|
||||
|
||||
private fun onExportDB() {
|
||||
taskRunner.execute(exportDBFactory.create { filename ->
|
||||
if (filename != null) showSendFileScreen(filename)
|
||||
else showMessage(R.string.could_not_export)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.habits.list
|
||||
|
||||
import android.view.*
|
||||
import dagger.*
|
||||
import org.isoron.androidbase.activities.*
|
||||
import org.isoron.uhabits.*
|
||||
import org.isoron.uhabits.activities.habits.list.views.*
|
||||
import org.isoron.uhabits.core.commands.*
|
||||
import org.isoron.uhabits.core.preferences.*
|
||||
import org.isoron.uhabits.core.ui.*
|
||||
import org.isoron.uhabits.core.ui.screens.habits.list.*
|
||||
import org.isoron.uhabits.core.utils.*
|
||||
import javax.inject.*
|
||||
|
||||
@ActivityScope
|
||||
class ListHabitsSelectionMenu @Inject constructor(
|
||||
private val screen: ListHabitsScreen,
|
||||
private val listAdapter: HabitCardListAdapter,
|
||||
var commandRunner: CommandRunner,
|
||||
private val prefs: Preferences,
|
||||
private val behavior: ListHabitsSelectionMenuBehavior,
|
||||
private val listController: Lazy<HabitCardListController>,
|
||||
private val notificationTray: NotificationTray
|
||||
) : BaseSelectionMenu() {
|
||||
|
||||
override fun onFinish() {
|
||||
listController.get().onSelectionFinished()
|
||||
super.onFinish()
|
||||
}
|
||||
|
||||
override fun onItemClicked(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.action_edit_habit -> {
|
||||
behavior.onEditHabits()
|
||||
return true
|
||||
}
|
||||
|
||||
R.id.action_archive_habit -> {
|
||||
behavior.onArchiveHabits()
|
||||
return true
|
||||
}
|
||||
|
||||
R.id.action_unarchive_habit -> {
|
||||
behavior.onUnarchiveHabits()
|
||||
return true
|
||||
}
|
||||
|
||||
R.id.action_delete -> {
|
||||
behavior.onDeleteHabits()
|
||||
return true
|
||||
}
|
||||
|
||||
R.id.action_color -> {
|
||||
behavior.onChangeColor()
|
||||
return true
|
||||
}
|
||||
|
||||
R.id.action_notify -> {
|
||||
for(h in listAdapter.selected)
|
||||
notificationTray.show(h, DateUtils.getToday(), 0)
|
||||
return true
|
||||
}
|
||||
|
||||
else -> return false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPrepare(menu: Menu): Boolean {
|
||||
val itemEdit = menu.findItem(R.id.action_edit_habit)
|
||||
val itemColor = menu.findItem(R.id.action_color)
|
||||
val itemArchive = menu.findItem(R.id.action_archive_habit)
|
||||
val itemUnarchive = menu.findItem(R.id.action_unarchive_habit)
|
||||
val itemNotify = menu.findItem(R.id.action_notify)
|
||||
|
||||
itemColor.isVisible = true
|
||||
itemEdit.isVisible = behavior.canEdit()
|
||||
itemArchive.isVisible = behavior.canArchive()
|
||||
itemUnarchive.isVisible = behavior.canUnarchive()
|
||||
setTitle(Integer.toString(listAdapter.selected.size))
|
||||
itemNotify.isVisible = prefs.isDeveloper
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
fun onSelectionStart() = screen.startSelection()
|
||||
fun onSelectionChange() = invalidate()
|
||||
fun onSelectionFinish() = finish()
|
||||
override fun getResourceId() = R.menu.list_habits_selection
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.habits.list.views
|
||||
|
||||
import android.content.*
|
||||
import android.view.*
|
||||
import android.view.View.MeasureSpec.*
|
||||
import android.widget.*
|
||||
import org.isoron.uhabits.*
|
||||
import org.isoron.uhabits.core.preferences.*
|
||||
import org.isoron.uhabits.utils.*
|
||||
|
||||
abstract class ButtonPanelView<T : View>(
|
||||
context: Context,
|
||||
val preferences: Preferences
|
||||
) : LinearLayout(context),
|
||||
Preferences.Listener {
|
||||
|
||||
var buttonCount = 0
|
||||
set(value) {
|
||||
field = value
|
||||
inflateButtons()
|
||||
}
|
||||
|
||||
var dataOffset = 0
|
||||
set(value) {
|
||||
field = value
|
||||
setupButtons()
|
||||
}
|
||||
|
||||
var buttons = mutableListOf<T>()
|
||||
|
||||
override fun onCheckmarkSequenceChanged() {
|
||||
inflateButtons()
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
protected fun inflateButtons() {
|
||||
val reverse = preferences.isCheckmarkSequenceReversed
|
||||
|
||||
buttons.clear()
|
||||
repeat(buttonCount) { buttons.add(createButton()) }
|
||||
|
||||
removeAllViews()
|
||||
if (reverse) buttons.reversed().forEach { addView(it) }
|
||||
else buttons.forEach { addView(it) }
|
||||
setupButtons()
|
||||
requestLayout()
|
||||
}
|
||||
|
||||
public override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
preferences.addListener(this)
|
||||
}
|
||||
|
||||
public override fun onDetachedFromWindow() {
|
||||
preferences.removeListener(this)
|
||||
super.onDetachedFromWindow()
|
||||
}
|
||||
|
||||
override fun onMeasure(widthSpec: Int, heightSpec: Int) {
|
||||
val buttonWidth = dim(R.dimen.checkmarkWidth)
|
||||
val buttonHeight = dim(R.dimen.checkmarkHeight)
|
||||
val width = (buttonWidth * buttonCount)
|
||||
super.onMeasure(width.toMeasureSpec(EXACTLY),
|
||||
buttonHeight.toMeasureSpec(EXACTLY))
|
||||
}
|
||||
|
||||
protected abstract fun setupButtons()
|
||||
protected abstract fun createButton(): T
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.habits.list.views
|
||||
|
||||
import android.content.*
|
||||
import android.graphics.*
|
||||
import android.text.*
|
||||
import android.view.*
|
||||
import android.view.View.MeasureSpec.*
|
||||
import com.google.auto.factory.*
|
||||
import org.isoron.androidbase.activities.*
|
||||
import org.isoron.uhabits.*
|
||||
import org.isoron.uhabits.core.models.Checkmark.*
|
||||
import org.isoron.uhabits.core.preferences.*
|
||||
import org.isoron.uhabits.utils.*
|
||||
|
||||
@AutoFactory
|
||||
class CheckmarkButtonView(
|
||||
@Provided @ActivityContext context: Context,
|
||||
@Provided val preferences: Preferences
|
||||
) : View(context),
|
||||
View.OnClickListener,
|
||||
View.OnLongClickListener {
|
||||
|
||||
var color: Int = Color.BLACK
|
||||
set(value) {
|
||||
field = value
|
||||
invalidate()
|
||||
}
|
||||
|
||||
var value: Int = 0
|
||||
set(value) {
|
||||
field = value
|
||||
invalidate()
|
||||
}
|
||||
|
||||
var onToggle: () -> Unit = {}
|
||||
private var drawer = Drawer()
|
||||
|
||||
init {
|
||||
isFocusable = false
|
||||
setOnClickListener(this)
|
||||
setOnLongClickListener(this)
|
||||
}
|
||||
|
||||
fun performToggle() {
|
||||
onToggle()
|
||||
value = when (value) {
|
||||
CHECKED_EXPLICITLY -> UNCHECKED
|
||||
else -> CHECKED_EXPLICITLY
|
||||
}
|
||||
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
||||
invalidate()
|
||||
}
|
||||
|
||||
override fun onClick(v: View) {
|
||||
if (preferences.isShortToggleEnabled) performToggle()
|
||||
else showMessage(R.string.long_press_to_toggle)
|
||||
}
|
||||
|
||||
override fun onLongClick(v: View): Boolean {
|
||||
performToggle()
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
super.onDraw(canvas)
|
||||
drawer.draw(canvas)
|
||||
}
|
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
val height = resources.getDimensionPixelSize(R.dimen.checkmarkHeight)
|
||||
val width = resources.getDimensionPixelSize(R.dimen.checkmarkWidth)
|
||||
super.onMeasure(width.toMeasureSpec(EXACTLY),
|
||||
height.toMeasureSpec(EXACTLY))
|
||||
}
|
||||
|
||||
private inner class Drawer {
|
||||
private val rect = RectF()
|
||||
private val lowContrastColor = sres.getColor(R.attr.lowContrastTextColor)
|
||||
|
||||
private val paint = TextPaint().apply {
|
||||
typeface = getFontAwesome()
|
||||
isAntiAlias = true
|
||||
textAlign = Paint.Align.CENTER
|
||||
textSize = dim(R.dimen.smallTextSize)
|
||||
}
|
||||
|
||||
fun draw(canvas: Canvas) {
|
||||
paint.color = when (value) {
|
||||
CHECKED_EXPLICITLY -> color
|
||||
else -> lowContrastColor
|
||||
}
|
||||
val id = when (value) {
|
||||
UNCHECKED -> R.string.fa_times
|
||||
else -> R.string.fa_check
|
||||
}
|
||||
val label = resources.getString(id)
|
||||
val em = paint.measureText("m")
|
||||
|
||||
rect.set(0f, 0f, width.toFloat(), height.toFloat())
|
||||
rect.offset(0f, 0.4f * em)
|
||||
canvas.drawText(label, rect.centerX(), rect.centerY(), paint)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.habits.list.views
|
||||
|
||||
import android.content.*
|
||||
import com.google.auto.factory.*
|
||||
import org.isoron.androidbase.activities.*
|
||||
import org.isoron.uhabits.core.models.*
|
||||
import org.isoron.uhabits.core.models.Checkmark.*
|
||||
import org.isoron.uhabits.core.preferences.*
|
||||
import org.isoron.uhabits.core.utils.*
|
||||
|
||||
@AutoFactory
|
||||
class CheckmarkPanelView(
|
||||
@Provided @ActivityContext context: Context,
|
||||
@Provided preferences: Preferences,
|
||||
@Provided private val buttonFactory: CheckmarkButtonViewFactory
|
||||
) : ButtonPanelView<CheckmarkButtonView>(context, preferences) {
|
||||
|
||||
var values = IntArray(0)
|
||||
set(values) {
|
||||
field = values
|
||||
setupButtons()
|
||||
}
|
||||
|
||||
var color = 0
|
||||
set(value) {
|
||||
field = value
|
||||
setupButtons()
|
||||
}
|
||||
|
||||
var onToggle: (Timestamp) -> Unit = {}
|
||||
set(value) {
|
||||
field = value
|
||||
setupButtons()
|
||||
}
|
||||
|
||||
override fun createButton(): CheckmarkButtonView = buttonFactory.create()
|
||||
|
||||
@Synchronized
|
||||
override fun setupButtons() {
|
||||
val today = DateUtils.getToday()
|
||||
|
||||
buttons.forEachIndexed { index, button ->
|
||||
val timestamp = today.minus(index + dataOffset)
|
||||
button.value = when {
|
||||
index + dataOffset < values.size -> values[index + dataOffset]
|
||||
else -> UNCHECKED
|
||||
}
|
||||
button.color = color
|
||||
button.onToggle = { onToggle(timestamp) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.habits.list.views
|
||||
|
||||
import android.content.*
|
||||
import android.view.*
|
||||
import android.view.Gravity.*
|
||||
import android.view.ViewGroup.LayoutParams.*
|
||||
import android.widget.*
|
||||
import org.isoron.androidbase.activities.*
|
||||
import org.isoron.uhabits.*
|
||||
import org.isoron.uhabits.utils.*
|
||||
|
||||
class EmptyListView(context: Context) : LinearLayout(context) {
|
||||
init {
|
||||
orientation = VERTICAL
|
||||
gravity = Gravity.CENTER
|
||||
visibility = BaseRootView.GONE
|
||||
|
||||
addView(TextView(context).apply {
|
||||
text = str(R.string.fa_star_half_o)
|
||||
typeface = getFontAwesome()
|
||||
textSize = sp(40.0f)
|
||||
gravity = CENTER
|
||||
setTextColor(sres.getColor(R.attr.mediumContrastTextColor))
|
||||
}, MATCH_PARENT, WRAP_CONTENT)
|
||||
|
||||
addView(TextView(context).apply {
|
||||
text = str(R.string.no_habits_found)
|
||||
gravity = CENTER
|
||||
setPadding(0, dp(20.0f).toInt(), 0, 0)
|
||||
setTextColor(sres.getColor(R.attr.mediumContrastTextColor))
|
||||
}, MATCH_PARENT, WRAP_CONTENT)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,334 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.habits.list.views;
|
||||
|
||||
import android.support.annotation.*;
|
||||
import android.support.v7.widget.*;
|
||||
import android.view.*;
|
||||
|
||||
import org.isoron.androidbase.activities.*;
|
||||
import org.isoron.uhabits.activities.habits.list.*;
|
||||
import org.isoron.uhabits.core.models.*;
|
||||
import org.isoron.uhabits.core.preferences.*;
|
||||
import org.isoron.uhabits.core.ui.screens.habits.list.*;
|
||||
import org.isoron.uhabits.core.utils.*;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import javax.inject.*;
|
||||
|
||||
/**
|
||||
* Provides data that backs a {@link HabitCardListView}.
|
||||
* <p>
|
||||
* The data if fetched and cached by a {@link HabitCardListCache}. This adapter
|
||||
* also holds a list of items that have been selected.
|
||||
*/
|
||||
@ActivityScope
|
||||
public class HabitCardListAdapter
|
||||
extends RecyclerView.Adapter<HabitCardViewHolder> implements
|
||||
HabitCardListCache.Listener,
|
||||
MidnightTimer.MidnightListener,
|
||||
ListHabitsMenuBehavior.Adapter,
|
||||
ListHabitsSelectionMenuBehavior.Adapter
|
||||
{
|
||||
@NonNull
|
||||
private ModelObservable observable;
|
||||
|
||||
@Nullable
|
||||
private HabitCardListView listView;
|
||||
|
||||
@NonNull
|
||||
private final LinkedList<Habit> selected;
|
||||
|
||||
@NonNull
|
||||
private final HabitCardListCache cache;
|
||||
|
||||
@NonNull
|
||||
private Preferences preferences;
|
||||
|
||||
private final MidnightTimer midnightTimer;
|
||||
|
||||
@Inject
|
||||
public HabitCardListAdapter(@NonNull HabitCardListCache cache,
|
||||
@NonNull Preferences preferences,
|
||||
@NonNull MidnightTimer midnightTimer)
|
||||
{
|
||||
this.preferences = preferences;
|
||||
this.selected = new LinkedList<>();
|
||||
this.observable = new ModelObservable();
|
||||
this.cache = cache;
|
||||
|
||||
this.midnightTimer = midnightTimer;
|
||||
|
||||
cache.setListener(this);
|
||||
cache.setCheckmarkCount(
|
||||
ListHabitsRootViewKt.MAX_CHECKMARK_COUNT);
|
||||
cache.setOrder(preferences.getDefaultOrder());
|
||||
|
||||
setHasStableIds(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void atMidnight()
|
||||
{
|
||||
cache.refreshAllHabits();
|
||||
}
|
||||
|
||||
public void cancelRefresh()
|
||||
{
|
||||
cache.cancelTasks();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets all items as not selected.
|
||||
*/
|
||||
@Override
|
||||
public void clearSelection()
|
||||
{
|
||||
selected.clear();
|
||||
notifyDataSetChanged();
|
||||
observable.notifyListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the item that occupies a certain position on the list
|
||||
*
|
||||
* @param position position of the item
|
||||
* @return the item at given position or null if position is invalid
|
||||
*/
|
||||
@Deprecated
|
||||
@Nullable
|
||||
public Habit getItem(int position)
|
||||
{
|
||||
return cache.getHabitByPosition(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount()
|
||||
{
|
||||
return cache.getHabitCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position)
|
||||
{
|
||||
return getItem(position).getId();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public ModelObservable getObservable()
|
||||
{
|
||||
return observable;
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public List<Habit> getSelected()
|
||||
{
|
||||
return new LinkedList<>(selected);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether list of selected items is empty.
|
||||
*
|
||||
* @return true if selection is empty, false otherwise
|
||||
*/
|
||||
public boolean isSelectionEmpty()
|
||||
{
|
||||
return selected.isEmpty();
|
||||
}
|
||||
|
||||
public boolean isSortable()
|
||||
{
|
||||
return cache.getOrder() == HabitList.Order.BY_POSITION;
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify the adapter that it has been attached to a ListView.
|
||||
*/
|
||||
public void onAttached()
|
||||
{
|
||||
cache.onAttached();
|
||||
midnightTimer.addListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@Nullable HabitCardViewHolder holder,
|
||||
int position)
|
||||
{
|
||||
if (holder == null) return;
|
||||
if (listView == null) return;
|
||||
|
||||
Habit habit = cache.getHabitByPosition(position);
|
||||
double score = cache.getScore(habit.getId());
|
||||
int checkmarks[] = cache.getCheckmarks(habit.getId());
|
||||
boolean selected = this.selected.contains(habit);
|
||||
|
||||
listView.bindCardView(holder, habit, score, checkmarks, selected);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewAttachedToWindow(@Nullable HabitCardViewHolder holder)
|
||||
{
|
||||
if (listView == null) return;
|
||||
listView.attachCardView(holder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewDetachedFromWindow(@Nullable HabitCardViewHolder holder)
|
||||
{
|
||||
if (listView == null) return;
|
||||
listView.detachCardView(holder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HabitCardViewHolder onCreateViewHolder(ViewGroup parent,
|
||||
int viewType)
|
||||
{
|
||||
if (listView == null) return null;
|
||||
View view = listView.createHabitCardView();
|
||||
return new HabitCardViewHolder(view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify the adapter that it has been detached from a ListView.
|
||||
*/
|
||||
public void onDetached()
|
||||
{
|
||||
cache.onDetached();
|
||||
midnightTimer.removeListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemChanged(int position)
|
||||
{
|
||||
notifyItemChanged(position);
|
||||
observable.notifyListeners();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemInserted(int position)
|
||||
{
|
||||
notifyItemInserted(position);
|
||||
observable.notifyListeners();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemMoved(int fromPosition, int toPosition)
|
||||
{
|
||||
notifyItemMoved(fromPosition, toPosition);
|
||||
observable.notifyListeners();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemRemoved(int position)
|
||||
{
|
||||
notifyItemRemoved(position);
|
||||
observable.notifyListeners();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRefreshFinished()
|
||||
{
|
||||
observable.notifyListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a list of habits from the adapter.
|
||||
* <p>
|
||||
* Note that this only has effect on the adapter cache. The database is not
|
||||
* modified, and the change is lost when the cache is refreshed. This method
|
||||
* is useful for making the ListView more responsive: while we wait for the
|
||||
* database operation to finish, the cache can be modified to reflect the
|
||||
* changes immediately.
|
||||
*
|
||||
* @param habits list of habits to be removed
|
||||
*/
|
||||
@Override
|
||||
public void performRemove(List<Habit> habits)
|
||||
{
|
||||
for (Habit h : habits)
|
||||
cache.remove(h.getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the order of habits on the adapter.
|
||||
* <p>
|
||||
* Note that this only has effect on the adapter cache. The database is not
|
||||
* modified, and the change is lost when the cache is refreshed. This method
|
||||
* is useful for making the ListView more responsive: while we wait for the
|
||||
* database operation to finish, the cache can be modified to reflect the
|
||||
* changes immediately.
|
||||
*
|
||||
* @param from the habit that should be moved
|
||||
* @param to the habit that currently occupies the desired position
|
||||
*/
|
||||
public void performReorder(int from, int to)
|
||||
{
|
||||
cache.reorder(from, to);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void refresh()
|
||||
{
|
||||
cache.refreshAllHabits();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFilter(HabitMatcher matcher)
|
||||
{
|
||||
cache.setFilter(matcher);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the HabitCardListView that this adapter will provide data for.
|
||||
* <p>
|
||||
* This object will be used to generated new HabitCardViews, upon demand.
|
||||
*
|
||||
* @param listView the HabitCardListView associated with this adapter
|
||||
*/
|
||||
public void setListView(@Nullable HabitCardListView listView)
|
||||
{
|
||||
this.listView = listView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOrder(HabitList.Order order)
|
||||
{
|
||||
cache.setOrder(order);
|
||||
preferences.setDefaultOrder(order);
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects or deselects the item at a given position.
|
||||
*
|
||||
* @param position position of the item to be toggled
|
||||
*/
|
||||
public void toggleSelection(int position)
|
||||
{
|
||||
Habit h = getItem(position);
|
||||
if (h == null) return;
|
||||
|
||||
int k = selected.indexOf(h);
|
||||
if (k < 0) selected.add(h);
|
||||
else selected.remove(h);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.habits.list.views
|
||||
|
||||
import dagger.*
|
||||
import org.isoron.androidbase.activities.*
|
||||
import org.isoron.uhabits.activities.habits.list.*
|
||||
import org.isoron.uhabits.core.models.*
|
||||
import org.isoron.uhabits.core.ui.screens.habits.list.*
|
||||
import javax.inject.*
|
||||
|
||||
/**
|
||||
* Controller responsible for receiving and processing the events generated by a
|
||||
* HabitListView. These include selecting and reordering items, toggling
|
||||
* checkmarks and clicking habits.
|
||||
*/
|
||||
@ActivityScope
|
||||
class HabitCardListController @Inject constructor(
|
||||
private val adapter: HabitCardListAdapter,
|
||||
private val behavior: ListHabitsBehavior,
|
||||
private val selectionMenu: Lazy<ListHabitsSelectionMenu>
|
||||
) : HabitCardListView.Controller, ModelObservable.Listener {
|
||||
|
||||
private val NORMAL_MODE = NormalMode()
|
||||
private val SELECTION_MODE = SelectionMode()
|
||||
private var activeMode: Mode
|
||||
|
||||
init {
|
||||
this.activeMode = NORMAL_MODE
|
||||
adapter.observable.addListener(this)
|
||||
}
|
||||
|
||||
override fun drop(from: Int, to: Int) {
|
||||
if (from == to) return
|
||||
cancelSelection()
|
||||
|
||||
val habitFrom = adapter.getItem(from)
|
||||
val habitTo = adapter.getItem(to)
|
||||
if(habitFrom == null || habitTo == null) return
|
||||
|
||||
adapter.performReorder(from, to)
|
||||
behavior.onReorderHabit(habitFrom, habitTo)
|
||||
}
|
||||
|
||||
override fun onItemClick(position: Int) {
|
||||
activeMode.onItemClick(position)
|
||||
}
|
||||
|
||||
override fun onItemLongClick(position: Int) {
|
||||
activeMode.onItemLongClick(position)
|
||||
}
|
||||
|
||||
override fun onModelChange() {
|
||||
if (adapter.isSelectionEmpty) {
|
||||
activeMode = NormalMode()
|
||||
selectionMenu.get().onSelectionFinish()
|
||||
}
|
||||
}
|
||||
|
||||
fun onSelectionFinished() {
|
||||
cancelSelection()
|
||||
}
|
||||
|
||||
override fun startDrag(position: Int) {
|
||||
activeMode.startDrag(position)
|
||||
}
|
||||
|
||||
protected fun toggleSelection(position: Int) {
|
||||
adapter.toggleSelection(position)
|
||||
activeMode = if (adapter.isSelectionEmpty) NORMAL_MODE else SELECTION_MODE
|
||||
}
|
||||
|
||||
private fun cancelSelection() {
|
||||
adapter.clearSelection()
|
||||
activeMode = NormalMode()
|
||||
selectionMenu.get().onSelectionFinish()
|
||||
}
|
||||
|
||||
interface HabitListener {
|
||||
fun onHabitClick(habit: Habit)
|
||||
fun onHabitReorder(from: Habit, to: Habit)
|
||||
}
|
||||
|
||||
/**
|
||||
* A Mode describes the behavior of the list upon clicking, long clicking
|
||||
* and dragging an item. This depends on whether some items are already
|
||||
* selected or not.
|
||||
*/
|
||||
private interface Mode {
|
||||
fun onItemClick(position: Int)
|
||||
fun onItemLongClick(position: Int): Boolean
|
||||
fun startDrag(position: Int)
|
||||
}
|
||||
|
||||
/**
|
||||
* Mode activated when there are no items selected. Clicks trigger habit
|
||||
* click. Long clicks start selection.
|
||||
*/
|
||||
internal inner class NormalMode : Mode {
|
||||
override fun onItemClick(position: Int) {
|
||||
val habit = adapter.getItem(position)
|
||||
if (habit == null) return
|
||||
behavior.onClickHabit(habit)
|
||||
}
|
||||
|
||||
override fun onItemLongClick(position: Int): Boolean {
|
||||
startSelection(position)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun startDrag(position: Int) {
|
||||
startSelection(position)
|
||||
}
|
||||
|
||||
protected fun startSelection(position: Int) {
|
||||
toggleSelection(position)
|
||||
activeMode = SELECTION_MODE
|
||||
selectionMenu.get().onSelectionStart()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mode activated when some items are already selected. Clicks toggle
|
||||
* item selection. Long clicks select more items.
|
||||
*/
|
||||
internal inner class SelectionMode : Mode {
|
||||
override fun onItemClick(position: Int) {
|
||||
toggleSelection(position)
|
||||
notifyListener()
|
||||
}
|
||||
|
||||
override fun onItemLongClick(position: Int): Boolean {
|
||||
toggleSelection(position)
|
||||
notifyListener()
|
||||
return true
|
||||
}
|
||||
|
||||
override fun startDrag(position: Int) {
|
||||
toggleSelection(position)
|
||||
notifyListener()
|
||||
}
|
||||
|
||||
protected fun notifyListener() {
|
||||
if (activeMode === SELECTION_MODE)
|
||||
selectionMenu.get().onSelectionChange()
|
||||
else
|
||||
selectionMenu.get().onSelectionFinish()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.habits.list.views
|
||||
|
||||
import android.content.*
|
||||
import android.os.*
|
||||
import android.support.v7.widget.*
|
||||
import android.support.v7.widget.helper.*
|
||||
import android.support.v7.widget.helper.ItemTouchHelper.*
|
||||
import android.view.*
|
||||
import com.google.auto.factory.*
|
||||
import dagger.*
|
||||
import org.isoron.androidbase.activities.*
|
||||
import org.isoron.uhabits.R
|
||||
import org.isoron.uhabits.activities.common.views.*
|
||||
import org.isoron.uhabits.core.models.*
|
||||
|
||||
@AutoFactory
|
||||
class HabitCardListView(
|
||||
@Provided @ActivityContext context: Context,
|
||||
@Provided private val adapter: HabitCardListAdapter,
|
||||
@Provided private val cardViewFactory: HabitCardViewFactory,
|
||||
@Provided private val controller: Lazy<HabitCardListController>
|
||||
) : RecyclerView(context, null, R.attr.scrollableRecyclerViewStyle) {
|
||||
|
||||
var checkmarkCount: Int = 0
|
||||
|
||||
var dataOffset: Int = 0
|
||||
set(value) {
|
||||
field = value
|
||||
attachedHolders
|
||||
.map { it.itemView as HabitCardView }
|
||||
.forEach { it.dataOffset = value }
|
||||
}
|
||||
|
||||
private val attachedHolders = mutableListOf<HabitCardViewHolder>()
|
||||
private val touchHelper = ItemTouchHelper(TouchHelperCallback()).apply {
|
||||
attachToRecyclerView(this@HabitCardListView)
|
||||
}
|
||||
|
||||
init {
|
||||
setHasFixedSize(true)
|
||||
isLongClickable = true
|
||||
layoutManager = LinearLayoutManager(context)
|
||||
super.setAdapter(adapter)
|
||||
}
|
||||
|
||||
fun createHabitCardView(): View {
|
||||
return cardViewFactory.create()
|
||||
}
|
||||
|
||||
fun bindCardView(holder: HabitCardViewHolder,
|
||||
habit: Habit,
|
||||
score: Double,
|
||||
checkmarks: IntArray,
|
||||
selected: Boolean): View {
|
||||
val cardView = holder.itemView as HabitCardView
|
||||
cardView.habit = habit
|
||||
cardView.isSelected = selected
|
||||
cardView.values = checkmarks
|
||||
cardView.buttonCount = checkmarkCount
|
||||
cardView.dataOffset = dataOffset
|
||||
cardView.score = score
|
||||
cardView.unit = habit.unit
|
||||
cardView.threshold = habit.targetValue
|
||||
|
||||
val detector = GestureDetector(context, CardViewGestureDetector(holder))
|
||||
cardView.setOnTouchListener { _, ev ->
|
||||
detector.onTouchEvent(ev)
|
||||
true
|
||||
}
|
||||
|
||||
return cardView
|
||||
}
|
||||
|
||||
fun attachCardView(holder: HabitCardViewHolder) {
|
||||
attachedHolders.add(holder)
|
||||
}
|
||||
|
||||
fun detachCardView(holder: HabitCardViewHolder) {
|
||||
attachedHolders.remove(holder)
|
||||
}
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
adapter.onAttached()
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
adapter.onDetached()
|
||||
super.onDetachedFromWindow()
|
||||
}
|
||||
|
||||
override fun onRestoreInstanceState(state: Parcelable) {
|
||||
if (state !is BundleSavedState) {
|
||||
super.onRestoreInstanceState(state)
|
||||
return
|
||||
}
|
||||
dataOffset = state.bundle.getInt("dataOffset")
|
||||
super.onRestoreInstanceState(state.superState)
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(): Parcelable {
|
||||
val superState = super.onSaveInstanceState()
|
||||
val bundle = Bundle().apply {
|
||||
putInt("dataOffset", dataOffset)
|
||||
}
|
||||
return BundleSavedState(superState, bundle)
|
||||
}
|
||||
|
||||
interface Controller {
|
||||
fun drop(from: Int, to: Int) {}
|
||||
fun onItemClick(pos: Int) {}
|
||||
fun onItemLongClick(pos: Int) {}
|
||||
fun startDrag(position: Int) {}
|
||||
}
|
||||
|
||||
private inner class CardViewGestureDetector(
|
||||
private val holder: HabitCardViewHolder
|
||||
) : GestureDetector.SimpleOnGestureListener() {
|
||||
|
||||
override fun onLongPress(e: MotionEvent) {
|
||||
val position = holder.adapterPosition
|
||||
controller.get().onItemLongClick(position)
|
||||
if (adapter.isSortable) touchHelper.startDrag(holder)
|
||||
}
|
||||
|
||||
override fun onSingleTapUp(e: MotionEvent): Boolean {
|
||||
val position = holder.adapterPosition
|
||||
controller.get().onItemClick(position)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
inner class TouchHelperCallback : ItemTouchHelper.Callback() {
|
||||
override fun getMovementFlags(recyclerView: RecyclerView,
|
||||
viewHolder: RecyclerView.ViewHolder): Int {
|
||||
return makeMovementFlags(UP or DOWN, START or END)
|
||||
}
|
||||
|
||||
override fun onMove(recyclerView: RecyclerView,
|
||||
from: RecyclerView.ViewHolder,
|
||||
to: RecyclerView.ViewHolder): Boolean {
|
||||
controller.get().drop(from.adapterPosition, to.adapterPosition)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onSwiped(viewHolder: RecyclerView.ViewHolder,
|
||||
direction: Int) {
|
||||
}
|
||||
|
||||
override fun isItemViewSwipeEnabled() = false
|
||||
override fun isLongPressDragEnabled() = false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.habits.list.views
|
||||
|
||||
import android.content.*
|
||||
import android.os.*
|
||||
import android.os.Build.VERSION.*
|
||||
import android.os.Build.VERSION_CODES.*
|
||||
import android.text.*
|
||||
import android.view.*
|
||||
import android.view.ViewGroup.LayoutParams.*
|
||||
import android.widget.*
|
||||
import com.google.auto.factory.*
|
||||
import org.isoron.androidbase.activities.*
|
||||
import org.isoron.uhabits.*
|
||||
import org.isoron.uhabits.activities.common.views.*
|
||||
import org.isoron.uhabits.core.models.*
|
||||
import org.isoron.uhabits.core.ui.screens.habits.list.*
|
||||
import org.isoron.uhabits.core.utils.*
|
||||
import org.isoron.uhabits.utils.*
|
||||
|
||||
@AutoFactory
|
||||
class HabitCardView(
|
||||
@Provided @ActivityContext context: Context,
|
||||
@Provided private val checkmarkPanelFactory: CheckmarkPanelViewFactory,
|
||||
@Provided private val numberPanelFactory: NumberPanelViewFactory,
|
||||
@Provided private val behavior: ListHabitsBehavior
|
||||
) : FrameLayout(context),
|
||||
ModelObservable.Listener {
|
||||
|
||||
var buttonCount
|
||||
get() = checkmarkPanel.buttonCount
|
||||
set(value) {
|
||||
checkmarkPanel.buttonCount = value
|
||||
numberPanel.buttonCount = value
|
||||
}
|
||||
|
||||
var dataOffset = 0
|
||||
set(value) {
|
||||
field = value
|
||||
checkmarkPanel.dataOffset = value
|
||||
numberPanel.dataOffset = value
|
||||
}
|
||||
|
||||
var habit: Habit? = null
|
||||
set(newHabit) {
|
||||
if (isAttachedToWindow) {
|
||||
field?.observable?.removeListener(this)
|
||||
newHabit?.observable?.addListener(this)
|
||||
}
|
||||
field = newHabit
|
||||
if (newHabit != null) copyAttributesFrom(newHabit)
|
||||
}
|
||||
|
||||
var score
|
||||
get() = scoreRing.percentage.toDouble()
|
||||
set(value) {
|
||||
scoreRing.percentage = value.toFloat()
|
||||
scoreRing.precision = 1.0f / 16
|
||||
}
|
||||
|
||||
var unit
|
||||
get() = numberPanel.units
|
||||
set(value) {
|
||||
numberPanel.units = value
|
||||
}
|
||||
|
||||
var values
|
||||
get() = checkmarkPanel.values
|
||||
set(values) {
|
||||
checkmarkPanel.values = values
|
||||
numberPanel.values = values.map { it / 1000.0 }.toDoubleArray()
|
||||
}
|
||||
|
||||
var threshold: Double
|
||||
get() = numberPanel.threshold
|
||||
set(value) {
|
||||
numberPanel.threshold = value
|
||||
}
|
||||
|
||||
private var checkmarkPanel: CheckmarkPanelView
|
||||
private var numberPanel: NumberPanelView
|
||||
private var innerFrame: LinearLayout
|
||||
private var label: TextView
|
||||
private var scoreRing: RingView
|
||||
|
||||
init {
|
||||
scoreRing = RingView(context).apply {
|
||||
val thickness = dp(3f)
|
||||
val margin = dp(8f).toInt()
|
||||
val ringSize = dp(15f).toInt()
|
||||
layoutParams = LinearLayout.LayoutParams(ringSize, ringSize).apply {
|
||||
setMargins(margin, 0, margin, 0)
|
||||
gravity = Gravity.CENTER
|
||||
}
|
||||
setThickness(thickness)
|
||||
}
|
||||
|
||||
label = TextView(context).apply {
|
||||
maxLines = 2
|
||||
ellipsize = TextUtils.TruncateAt.END
|
||||
layoutParams = LinearLayout.LayoutParams(0, WRAP_CONTENT, 1f)
|
||||
if (SDK_INT >= M) breakStrategy = Layout.BREAK_STRATEGY_BALANCED
|
||||
}
|
||||
|
||||
checkmarkPanel = checkmarkPanelFactory.create().apply {
|
||||
onToggle = { timestamp ->
|
||||
triggerRipple(timestamp)
|
||||
habit?.let { behavior.onToggle(it, timestamp) }
|
||||
}
|
||||
}
|
||||
|
||||
numberPanel = numberPanelFactory.create().apply {
|
||||
visibility = GONE
|
||||
onEdit = { timestamp ->
|
||||
triggerRipple(timestamp)
|
||||
habit?.let { behavior.onEdit(it, timestamp) }
|
||||
}
|
||||
}
|
||||
|
||||
innerFrame = LinearLayout(context).apply {
|
||||
gravity = Gravity.CENTER_VERTICAL
|
||||
orientation = LinearLayout.HORIZONTAL
|
||||
layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
|
||||
if (SDK_INT >= LOLLIPOP) elevation = dp(1f)
|
||||
|
||||
addView(scoreRing)
|
||||
addView(label)
|
||||
addView(checkmarkPanel)
|
||||
addView(numberPanel)
|
||||
|
||||
setOnTouchListener { v, event ->
|
||||
if (SDK_INT >= LOLLIPOP)
|
||||
v.background.setHotspot(event.x, event.y)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
clipToPadding = false
|
||||
layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
|
||||
val margin = dp(3f).toInt()
|
||||
setPadding(margin, 0, margin, margin)
|
||||
addView(innerFrame)
|
||||
}
|
||||
|
||||
override fun onModelChange() {
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
habit?.let { copyAttributesFrom(it) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun setSelected(isSelected: Boolean) {
|
||||
super.setSelected(isSelected)
|
||||
updateBackground(isSelected)
|
||||
}
|
||||
|
||||
fun triggerRipple(timestamp: Timestamp) {
|
||||
val today = DateUtils.getToday()
|
||||
val offset = timestamp.daysUntil(today) - dataOffset
|
||||
val button = checkmarkPanel.buttons[offset]
|
||||
val y = button.height / 2.0f
|
||||
val x = checkmarkPanel.x + button.x + (button.width / 2).toFloat()
|
||||
triggerRipple(x, y)
|
||||
}
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
habit?.observable?.addListener(this)
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
habit?.observable?.removeListener(this)
|
||||
super.onDetachedFromWindow()
|
||||
}
|
||||
|
||||
private fun copyAttributesFrom(h: Habit) {
|
||||
|
||||
fun getActiveColor(habit: Habit): Int {
|
||||
return when (habit.isArchived) {
|
||||
true -> sres.getColor(R.attr.mediumContrastTextColor)
|
||||
false -> PaletteUtils.getColor(context, habit.color)
|
||||
}
|
||||
}
|
||||
|
||||
val c = getActiveColor(h)
|
||||
label.apply {
|
||||
text = h.name
|
||||
setTextColor(c)
|
||||
}
|
||||
scoreRing.apply {
|
||||
color = c
|
||||
}
|
||||
checkmarkPanel.apply {
|
||||
color = c
|
||||
visibility = when (h.isNumerical) {
|
||||
true -> View.GONE
|
||||
false -> View.VISIBLE
|
||||
}
|
||||
}
|
||||
numberPanel.apply {
|
||||
color = c
|
||||
units = h.unit
|
||||
threshold = h.targetValue
|
||||
visibility = when (h.isNumerical) {
|
||||
true -> View.VISIBLE
|
||||
false -> View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun triggerRipple(x: Float, y: Float) {
|
||||
val background = innerFrame.background
|
||||
if (SDK_INT >= LOLLIPOP) background.setHotspot(x, y)
|
||||
background.state = intArrayOf(android.R.attr.state_pressed,
|
||||
android.R.attr.state_enabled)
|
||||
Handler().postDelayed({ background.state = intArrayOf() }, 25)
|
||||
}
|
||||
|
||||
private fun updateBackground(isSelected: Boolean) {
|
||||
if (SDK_INT < LOLLIPOP) {
|
||||
val background = when (isSelected) {
|
||||
true -> sres.getDrawable(R.attr.selectedBackground)
|
||||
false -> sres.getDrawable(R.attr.cardBackground)
|
||||
}
|
||||
innerFrame.setBackgroundDrawable(background)
|
||||
return
|
||||
}
|
||||
|
||||
val background = when (isSelected) {
|
||||
true -> R.drawable.selected_box
|
||||
false -> R.drawable.ripple
|
||||
}
|
||||
innerFrame.setBackgroundResource(background)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.habits.list.views
|
||||
|
||||
import android.support.v7.widget.*
|
||||
import android.view.*
|
||||
|
||||
class HabitCardViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
|
||||
@@ -0,0 +1,136 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.habits.list.views
|
||||
|
||||
import android.content.*
|
||||
import android.graphics.*
|
||||
import android.os.Build.VERSION.*
|
||||
import android.os.Build.VERSION_CODES.*
|
||||
import android.text.*
|
||||
import android.view.View.MeasureSpec.*
|
||||
import org.isoron.uhabits.*
|
||||
import org.isoron.uhabits.activities.common.views.*
|
||||
import org.isoron.uhabits.core.preferences.*
|
||||
import org.isoron.uhabits.core.utils.*
|
||||
import org.isoron.uhabits.core.utils.DateUtils.*
|
||||
import org.isoron.uhabits.utils.*
|
||||
import java.util.*
|
||||
|
||||
class HeaderView(
|
||||
context: Context,
|
||||
val prefs: Preferences,
|
||||
val midnightTimer: MidnightTimer
|
||||
) : ScrollableChart(context),
|
||||
Preferences.Listener,
|
||||
MidnightTimer.MidnightListener {
|
||||
|
||||
private var drawer = Drawer()
|
||||
|
||||
var buttonCount: Int = 0
|
||||
set(value) {
|
||||
field = value
|
||||
requestLayout()
|
||||
}
|
||||
|
||||
init {
|
||||
setScrollerBucketSize(dim(R.dimen.checkmarkWidth).toInt())
|
||||
setBackgroundColor(sres.getColor(R.attr.headerBackgroundColor))
|
||||
if (SDK_INT >= LOLLIPOP) elevation = dp(2.0f)
|
||||
}
|
||||
|
||||
override fun atMidnight() {
|
||||
post { invalidate() }
|
||||
}
|
||||
|
||||
override fun onCheckmarkSequenceChanged() {
|
||||
updateScrollDirection()
|
||||
postInvalidate()
|
||||
}
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
updateScrollDirection()
|
||||
prefs.addListener(this)
|
||||
midnightTimer.addListener(this)
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
midnightTimer.removeListener(this)
|
||||
prefs.removeListener(this)
|
||||
super.onDetachedFromWindow()
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
super.onDraw(canvas)
|
||||
drawer.draw(canvas)
|
||||
}
|
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
val height = dim(R.dimen.checkmarkHeight)
|
||||
setMeasuredDimension(widthMeasureSpec, height.toMeasureSpec(EXACTLY))
|
||||
}
|
||||
|
||||
private fun updateScrollDirection() {
|
||||
var direction = -1
|
||||
if (prefs.isCheckmarkSequenceReversed) direction *= -1
|
||||
if (isRTL()) direction *= -1
|
||||
setScrollDirection(direction)
|
||||
}
|
||||
|
||||
private inner class Drawer {
|
||||
private val rect = RectF()
|
||||
private val paint = TextPaint().apply {
|
||||
color = Color.BLACK
|
||||
isAntiAlias = true
|
||||
textSize = dim(R.dimen.tinyTextSize)
|
||||
textAlign = Paint.Align.CENTER
|
||||
typeface = Typeface.DEFAULT_BOLD
|
||||
color = sres.getColor(R.attr.mediumContrastTextColor)
|
||||
}
|
||||
|
||||
fun draw(canvas: Canvas) {
|
||||
val day = getStartOfTodayCalendar()
|
||||
val width = dim(R.dimen.checkmarkWidth)
|
||||
val height = dim(R.dimen.checkmarkHeight)
|
||||
val isReversed = prefs.isCheckmarkSequenceReversed
|
||||
|
||||
day.add(GregorianCalendar.DAY_OF_MONTH, -dataOffset)
|
||||
val em = paint.measureText("m")
|
||||
|
||||
repeat(buttonCount) { index ->
|
||||
rect.set(0f, 0f, width, height)
|
||||
rect.offset(canvas.width.toFloat(), 0f)
|
||||
|
||||
if (isReversed) rect.offset(-(index + 1) * width, 0f)
|
||||
else rect.offset((index - buttonCount) * width, 0f)
|
||||
|
||||
if (isRTL()) rect.set(canvas.width - rect.right, rect.top,
|
||||
canvas.width - rect.left, rect.bottom)
|
||||
|
||||
val y1 = rect.centerY() - 0.25 * em
|
||||
val y2 = rect.centerY() + 1.25 * em
|
||||
val lines = formatHeaderDate(day).toUpperCase().split("\n")
|
||||
canvas.drawText(lines[0], rect.centerX(), y1.toFloat(), paint)
|
||||
canvas.drawText(lines[1], rect.centerX(), y2.toFloat(), paint)
|
||||
day.add(GregorianCalendar.DAY_OF_MONTH, -1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.habits.list.views
|
||||
|
||||
import android.animation.*
|
||||
import android.content.*
|
||||
import android.graphics.*
|
||||
import android.graphics.Color.*
|
||||
import android.view.*
|
||||
import android.view.ViewGroup.LayoutParams.*
|
||||
import android.widget.*
|
||||
import org.isoron.uhabits.*
|
||||
import org.isoron.uhabits.core.ui.screens.habits.list.*
|
||||
import org.isoron.uhabits.utils.*
|
||||
|
||||
class HintView(
|
||||
context: Context,
|
||||
private val hintList: HintList
|
||||
) : LinearLayout(context) {
|
||||
|
||||
val hintContent: TextView
|
||||
|
||||
init {
|
||||
isClickable = true
|
||||
visibility = GONE
|
||||
orientation = VERTICAL
|
||||
val p1 = dp(16.0f).toInt()
|
||||
val p2 = dp(4.0f).toInt()
|
||||
setPadding(p1, p1, p2, p1)
|
||||
setBackgroundColor(resources.getColor(R.color.indigo_500))
|
||||
|
||||
val hintTitle = TextView(context).apply {
|
||||
setTextColor(WHITE)
|
||||
setTypeface(null, Typeface.BOLD)
|
||||
text = resources.getString(R.string.hint_title)
|
||||
}
|
||||
|
||||
hintContent = TextView(context).apply {
|
||||
setTextColor(WHITE)
|
||||
setPadding(0, dp(5.0f).toInt(), 0, 0)
|
||||
}
|
||||
|
||||
addView(hintTitle, WRAP_CONTENT, WRAP_CONTENT)
|
||||
addView(hintContent, WRAP_CONTENT, WRAP_CONTENT)
|
||||
setOnClickListener { dismiss() }
|
||||
}
|
||||
|
||||
public override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
showNext()
|
||||
}
|
||||
|
||||
fun showNext() {
|
||||
if (!hintList.shouldShow()) return
|
||||
val hint = hintList.pop() ?: return
|
||||
|
||||
hintContent.text = hint
|
||||
requestLayout()
|
||||
|
||||
alpha = 0.0f
|
||||
visibility = View.VISIBLE
|
||||
animate().alpha(1f).duration = 500
|
||||
}
|
||||
|
||||
private fun dismiss() {
|
||||
animate().alpha(0f).setDuration(500).setListener(DismissAnimator())
|
||||
}
|
||||
|
||||
private inner class DismissAnimator : AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animation: android.animation.Animator) {
|
||||
visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.habits.list.views
|
||||
|
||||
import android.content.*
|
||||
import android.graphics.*
|
||||
import android.text.*
|
||||
import android.view.*
|
||||
import android.view.View.*
|
||||
import com.google.auto.factory.*
|
||||
import org.isoron.androidbase.activities.*
|
||||
import org.isoron.androidbase.utils.*
|
||||
import org.isoron.androidbase.utils.InterfaceUtils.*
|
||||
import org.isoron.uhabits.*
|
||||
import org.isoron.uhabits.core.preferences.*
|
||||
import org.isoron.uhabits.utils.*
|
||||
import java.text.*
|
||||
|
||||
private val BOLD_TYPEFACE = Typeface.create("sans-serif-condensed", Typeface.BOLD)
|
||||
private val NORMAL_TYPEFACE = Typeface.create("sans-serif-condensed", Typeface.NORMAL)
|
||||
|
||||
fun Double.toShortString(): String = when {
|
||||
this >= 1e9 -> String.format("%.1fG", this / 1e9)
|
||||
this >= 1e8 -> String.format("%.0fM", this / 1e6)
|
||||
this >= 1e7 -> String.format("%.1fM", this / 1e6)
|
||||
this >= 1e6 -> String.format("%.1fM", this / 1e6)
|
||||
this >= 1e5 -> String.format("%.0fk", this / 1e3)
|
||||
this >= 1e4 -> String.format("%.1fk", this / 1e3)
|
||||
this >= 1e3 -> String.format("%.1fk", this / 1e3)
|
||||
this >= 1e2 -> DecimalFormat("#").format(this)
|
||||
this >= 1e1 -> DecimalFormat("#.#").format(this)
|
||||
else -> DecimalFormat("#.##").format(this)
|
||||
}
|
||||
|
||||
@AutoFactory
|
||||
class NumberButtonView(
|
||||
@Provided @ActivityContext context: Context,
|
||||
@Provided val preferences: Preferences
|
||||
) : View(context),
|
||||
OnClickListener,
|
||||
OnLongClickListener {
|
||||
|
||||
var color = 0
|
||||
set(value) {
|
||||
field = value
|
||||
invalidate()
|
||||
}
|
||||
|
||||
var value = 0.0
|
||||
set(value) {
|
||||
field = value
|
||||
invalidate()
|
||||
}
|
||||
|
||||
var threshold = 0.0
|
||||
set(value) {
|
||||
field = value
|
||||
invalidate()
|
||||
}
|
||||
|
||||
var units = ""
|
||||
set(value) {
|
||||
field = value
|
||||
invalidate()
|
||||
}
|
||||
|
||||
var onEdit: () -> Unit = {}
|
||||
private var drawer: Drawer = Drawer(context)
|
||||
|
||||
init {
|
||||
setOnClickListener(this)
|
||||
setOnLongClickListener(this)
|
||||
}
|
||||
|
||||
override fun onClick(v: View) {
|
||||
if (preferences.isShortToggleEnabled) onEdit()
|
||||
else showMessage(R.string.long_press_to_edit)
|
||||
}
|
||||
|
||||
override fun onLongClick(v: View): Boolean {
|
||||
onEdit()
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
super.onDraw(canvas)
|
||||
drawer.draw(canvas)
|
||||
}
|
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
val width = getDimension(context, R.dimen.checkmarkWidth).toInt()
|
||||
val height = getDimension(context, R.dimen.checkmarkHeight).toInt()
|
||||
setMeasuredDimension(width, height)
|
||||
}
|
||||
|
||||
private inner class Drawer(context: Context) {
|
||||
|
||||
private val em: Float
|
||||
private val rect: RectF = RectF()
|
||||
private val sr = StyledResources(context)
|
||||
|
||||
private val lightGrey: Int
|
||||
private val darkGrey: Int
|
||||
|
||||
private val pRegular: TextPaint = TextPaint().apply {
|
||||
textSize = getDimension(context, R.dimen.smallerTextSize)
|
||||
typeface = NORMAL_TYPEFACE
|
||||
isAntiAlias = true
|
||||
textAlign = Paint.Align.CENTER
|
||||
}
|
||||
|
||||
private val pBold: TextPaint = TextPaint().apply {
|
||||
textSize = getDimension(context, R.dimen.smallTextSize)
|
||||
typeface = BOLD_TYPEFACE
|
||||
isAntiAlias = true
|
||||
textAlign = Paint.Align.CENTER
|
||||
}
|
||||
|
||||
init {
|
||||
em = pBold.measureText("m")
|
||||
lightGrey = sr.getColor(R.attr.lowContrastTextColor)
|
||||
darkGrey = sr.getColor(R.attr.mediumContrastTextColor)
|
||||
}
|
||||
|
||||
fun draw(canvas: Canvas) {
|
||||
val activeColor = when {
|
||||
value == 0.0 -> lightGrey
|
||||
value < threshold -> darkGrey
|
||||
else -> color
|
||||
}
|
||||
|
||||
val label = value.toShortString()
|
||||
pBold.color = activeColor
|
||||
pRegular.color = activeColor
|
||||
|
||||
rect.set(0f, 0f, width.toFloat(), height.toFloat())
|
||||
canvas.drawText(label, rect.centerX(), rect.centerY(), pBold)
|
||||
|
||||
rect.offset(0f, 1.2f * em)
|
||||
canvas.drawText(units, rect.centerX(), rect.centerY(), pRegular)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.habits.list.views
|
||||
|
||||
import android.content.*
|
||||
import com.google.auto.factory.*
|
||||
import org.isoron.androidbase.activities.*
|
||||
import org.isoron.uhabits.core.models.*
|
||||
import org.isoron.uhabits.core.preferences.*
|
||||
import org.isoron.uhabits.core.utils.*
|
||||
|
||||
@AutoFactory
|
||||
class NumberPanelView(
|
||||
@Provided @ActivityContext context: Context,
|
||||
@Provided preferences: Preferences,
|
||||
@Provided private val buttonFactory: NumberButtonViewFactory
|
||||
) : ButtonPanelView<NumberButtonView>(context, preferences) {
|
||||
|
||||
var values = DoubleArray(0)
|
||||
set(values) {
|
||||
field = values
|
||||
setupButtons()
|
||||
}
|
||||
|
||||
var threshold = 0.0
|
||||
set(value) {
|
||||
field = value
|
||||
setupButtons()
|
||||
}
|
||||
|
||||
var color = 0
|
||||
set(value) {
|
||||
field = value
|
||||
setupButtons()
|
||||
}
|
||||
|
||||
var units = ""
|
||||
set(value) {
|
||||
field = value
|
||||
setupButtons()
|
||||
}
|
||||
|
||||
var onEdit: (Timestamp) -> Unit = {}
|
||||
set(value) {
|
||||
field = value
|
||||
setupButtons()
|
||||
}
|
||||
|
||||
override fun createButton() = buttonFactory.create()!!
|
||||
|
||||
@Synchronized
|
||||
override fun setupButtons() {
|
||||
val today = DateUtils.getToday()
|
||||
|
||||
buttons.forEachIndexed { index, button ->
|
||||
val timestamp = today.minus(index + dataOffset)
|
||||
button.value = when {
|
||||
index + dataOffset < values.size -> values[index + dataOffset]
|
||||
else -> 0.0
|
||||
}
|
||||
button.color = color
|
||||
button.threshold = threshold
|
||||
button.units = units
|
||||
button.onEdit = { onEdit(timestamp) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.habits.list.views
|
||||
|
||||
import android.content.*
|
||||
import android.view.*
|
||||
import android.view.View.MeasureSpec.*
|
||||
import org.isoron.uhabits.*
|
||||
import org.isoron.uhabits.utils.*
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
class ShadowView(context: Context) : View(context) {
|
||||
init {
|
||||
alpha = 0.2f
|
||||
background = resources.getDrawable(R.drawable.shadow)
|
||||
}
|
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
super.onMeasure(widthMeasureSpec,
|
||||
dp(2.0f).toInt().toMeasureSpec(EXACTLY))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.habits.show;
|
||||
|
||||
import android.os.*;
|
||||
|
||||
import org.isoron.uhabits.activities.*;
|
||||
|
||||
/**
|
||||
* Activity that allows the user to see more information about a single habit.
|
||||
* <p>
|
||||
* Shows all the metadata for the habit, in addition to several charts.
|
||||
*/
|
||||
public class ShowHabitActivity extends HabitsActivity
|
||||
{
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState)
|
||||
{
|
||||
super.onCreate(savedInstanceState);
|
||||
setScreen(getComponent().getShowHabitScreen());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.habits.show;
|
||||
|
||||
import org.isoron.uhabits.activities.*;
|
||||
import org.isoron.uhabits.core.ui.screens.habits.show.*;
|
||||
|
||||
import dagger.*;
|
||||
|
||||
@Module
|
||||
public abstract class ShowHabitModule
|
||||
{
|
||||
@Binds
|
||||
abstract ShowHabitBehavior.Screen getScreen(ShowHabitScreen screen);
|
||||
|
||||
@Binds
|
||||
abstract ShowHabitMenuBehavior.Screen getMenuScreen(ShowHabitScreen screen);
|
||||
|
||||
@Binds
|
||||
abstract ShowHabitMenuBehavior.System getSystem(HabitsDirFinder system);
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.habits.show;
|
||||
|
||||
import android.content.*;
|
||||
import android.os.*;
|
||||
import android.support.annotation.*;
|
||||
import android.support.v7.widget.*;
|
||||
|
||||
import org.isoron.androidbase.activities.*;
|
||||
import org.isoron.androidbase.utils.*;
|
||||
import org.isoron.uhabits.R;
|
||||
import org.isoron.uhabits.activities.habits.show.views.*;
|
||||
import org.isoron.uhabits.core.models.*;
|
||||
import org.isoron.uhabits.utils.*;
|
||||
|
||||
import javax.inject.*;
|
||||
|
||||
import butterknife.*;
|
||||
|
||||
@ActivityScope
|
||||
public class ShowHabitRootView extends BaseRootView
|
||||
implements ModelObservable.Listener
|
||||
{
|
||||
@NonNull
|
||||
private Habit habit;
|
||||
|
||||
@BindView(R.id.frequencyCard)
|
||||
FrequencyCard frequencyCard;
|
||||
|
||||
@BindView(R.id.streakCard)
|
||||
StreakCard streakCard;
|
||||
|
||||
@BindView(R.id.subtitleCard)
|
||||
SubtitleCard subtitleCard;
|
||||
|
||||
@BindView(R.id.overviewCard)
|
||||
OverviewCard overviewCard;
|
||||
|
||||
@BindView(R.id.scoreCard)
|
||||
ScoreCard scoreCard;
|
||||
|
||||
@BindView(R.id.historyCard)
|
||||
HistoryCard historyCard;
|
||||
|
||||
@BindView(R.id.barCard)
|
||||
BarCard barCard;
|
||||
|
||||
@BindView(R.id.toolbar)
|
||||
Toolbar toolbar;
|
||||
|
||||
@NonNull
|
||||
private Controller controller;
|
||||
|
||||
@Inject
|
||||
public ShowHabitRootView(@NonNull @ActivityContext Context context,
|
||||
@NonNull Habit habit)
|
||||
{
|
||||
super(context);
|
||||
this.habit = habit;
|
||||
|
||||
addView(inflate(getContext(), R.layout.show_habit, null));
|
||||
ButterKnife.bind(this);
|
||||
|
||||
controller = new Controller() {};
|
||||
setDisplayHomeAsUp(true);
|
||||
initCards();
|
||||
initToolbar();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getToolbarColor()
|
||||
{
|
||||
StyledResources res = new StyledResources(getContext());
|
||||
if (!res.getBoolean(R.attr.useHabitColorAsPrimary))
|
||||
return super.getToolbarColor();
|
||||
|
||||
return PaletteUtils.getColor(getContext(), habit.getColor());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onModelChange()
|
||||
{
|
||||
new Handler(Looper.getMainLooper()).post(() -> {
|
||||
toolbar.setTitle(habit.getName());
|
||||
});
|
||||
|
||||
controller.onToolbarChanged();
|
||||
}
|
||||
|
||||
public void setController(@NonNull Controller controller)
|
||||
{
|
||||
this.controller = controller;
|
||||
historyCard.setController(controller);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initToolbar()
|
||||
{
|
||||
super.initToolbar();
|
||||
toolbar.setTitle(habit.getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onAttachedToWindow()
|
||||
{
|
||||
super.onAttachedToWindow();
|
||||
habit.getObservable().addListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDetachedFromWindow()
|
||||
{
|
||||
habit.getObservable().removeListener(this);
|
||||
super.onDetachedFromWindow();
|
||||
}
|
||||
|
||||
private void initCards()
|
||||
{
|
||||
subtitleCard.setHabit(habit);
|
||||
overviewCard.setHabit(habit);
|
||||
scoreCard.setHabit(habit);
|
||||
historyCard.setHabit(habit);
|
||||
streakCard.setHabit(habit);
|
||||
frequencyCard.setHabit(habit);
|
||||
barCard.setHabit(habit);
|
||||
}
|
||||
|
||||
public interface Controller extends HistoryCard.Controller
|
||||
{
|
||||
default void onToolbarChanged() {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.habits.show;
|
||||
|
||||
import android.support.annotation.*;
|
||||
|
||||
import org.isoron.androidbase.activities.*;
|
||||
import org.isoron.uhabits.*;
|
||||
import org.isoron.uhabits.activities.common.dialogs.*;
|
||||
import org.isoron.uhabits.activities.habits.edit.*;
|
||||
import org.isoron.uhabits.core.models.*;
|
||||
import org.isoron.uhabits.core.ui.callbacks.*;
|
||||
import org.isoron.uhabits.core.ui.screens.habits.show.*;
|
||||
|
||||
import javax.inject.*;
|
||||
|
||||
import dagger.*;
|
||||
|
||||
@ActivityScope
|
||||
public class ShowHabitScreen extends BaseScreen
|
||||
implements ShowHabitMenuBehavior.Screen,
|
||||
ShowHabitBehavior.Screen,
|
||||
HistoryEditorDialog.Controller,
|
||||
ShowHabitRootView.Controller
|
||||
{
|
||||
@NonNull
|
||||
private final Habit habit;
|
||||
|
||||
@NonNull
|
||||
private final EditHabitDialogFactory editHabitDialogFactory;
|
||||
|
||||
@NonNull
|
||||
private final ConfirmDeleteDialogFactory confirmDeleteDialogFactory;
|
||||
|
||||
private final Lazy<ShowHabitBehavior> behavior;
|
||||
|
||||
@Inject
|
||||
public ShowHabitScreen(@NonNull BaseActivity activity,
|
||||
@NonNull Habit habit,
|
||||
@NonNull ShowHabitRootView view,
|
||||
@NonNull ShowHabitsMenu menu,
|
||||
@NonNull EditHabitDialogFactory editHabitDialogFactory,
|
||||
@NonNull ConfirmDeleteDialogFactory confirmDeleteDialogFactory,
|
||||
@NonNull Lazy<ShowHabitBehavior> behavior)
|
||||
{
|
||||
super(activity);
|
||||
setMenu(menu);
|
||||
setRootView(view);
|
||||
|
||||
this.habit = habit;
|
||||
this.behavior = behavior;
|
||||
this.editHabitDialogFactory = editHabitDialogFactory;
|
||||
this.confirmDeleteDialogFactory = confirmDeleteDialogFactory;
|
||||
view.setController(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEditHistoryButtonClick()
|
||||
{
|
||||
behavior.get().onEditHistory();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onToggleCheckmark(Timestamp timestamp)
|
||||
{
|
||||
behavior.get().onToggleCheckmark(timestamp);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onToolbarChanged()
|
||||
{
|
||||
invalidateToolbar();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reattachDialogs()
|
||||
{
|
||||
super.reattachDialogs();
|
||||
HistoryEditorDialog historyEditor = (HistoryEditorDialog) activity
|
||||
.getSupportFragmentManager()
|
||||
.findFragmentByTag("historyEditor");
|
||||
if (historyEditor != null) historyEditor.setController(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showEditHabitScreen(@NonNull Habit habit)
|
||||
{
|
||||
activity.showDialog(editHabitDialogFactory.edit(habit), "editHabit");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showEditHistoryScreen()
|
||||
{
|
||||
HistoryEditorDialog dialog = new HistoryEditorDialog();
|
||||
dialog.setHabit(habit);
|
||||
dialog.setController(this);
|
||||
dialog.show(activity.getSupportFragmentManager(), "historyEditor");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showMessage(ShowHabitMenuBehavior.Message m)
|
||||
{
|
||||
switch (m)
|
||||
{
|
||||
case COULD_NOT_EXPORT:
|
||||
showMessage(R.string.could_not_export);
|
||||
|
||||
case HABIT_DELETED:
|
||||
showMessage(R.string.delete_habits_message);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showDeleteConfirmationScreen(@NonNull OnConfirmedCallback callback) {
|
||||
activity.showDialog(confirmDeleteDialogFactory.create(callback));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
activity.finish();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.habits.show;
|
||||
|
||||
import android.support.annotation.*;
|
||||
import android.view.*;
|
||||
|
||||
import org.isoron.androidbase.activities.*;
|
||||
import org.isoron.uhabits.*;
|
||||
import org.isoron.uhabits.core.preferences.Preferences;
|
||||
import org.isoron.uhabits.core.ui.screens.habits.show.*;
|
||||
|
||||
import javax.inject.*;
|
||||
|
||||
import dagger.*;
|
||||
|
||||
@ActivityScope
|
||||
public class ShowHabitsMenu extends BaseMenu
|
||||
{
|
||||
@NonNull
|
||||
private Lazy<ShowHabitMenuBehavior> behavior;
|
||||
@NonNull
|
||||
private final Preferences prefs;
|
||||
|
||||
@Inject
|
||||
public ShowHabitsMenu(@NonNull BaseActivity activity,
|
||||
@NonNull Lazy<ShowHabitMenuBehavior> behavior,
|
||||
@NonNull Preferences prefs)
|
||||
{
|
||||
super(activity);
|
||||
this.behavior = behavior;
|
||||
this.prefs = prefs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@NonNull Menu menu)
|
||||
{
|
||||
super.onCreate(menu);
|
||||
|
||||
if (prefs.isDeveloper())
|
||||
menu.findItem(R.id.action_randomize).setVisible(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onItemSelected(@NonNull MenuItem item)
|
||||
{
|
||||
switch (item.getItemId())
|
||||
{
|
||||
case R.id.action_edit_habit:
|
||||
behavior.get().onEditHabit();
|
||||
return true;
|
||||
|
||||
case R.id.export:
|
||||
behavior.get().onExportCSV();
|
||||
return true;
|
||||
|
||||
case R.id.action_delete:
|
||||
behavior.get().onDeleteHabit();
|
||||
return true;
|
||||
|
||||
case R.id.action_randomize:
|
||||
behavior.get().onRandomize();
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getMenuResourceId()
|
||||
{
|
||||
return R.menu.show_habit;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.habits.show.views;
|
||||
|
||||
import android.content.*;
|
||||
import android.support.annotation.*;
|
||||
import android.util.*;
|
||||
import android.widget.*;
|
||||
|
||||
import org.isoron.uhabits.*;
|
||||
import org.isoron.uhabits.R;
|
||||
import org.isoron.uhabits.activities.common.views.*;
|
||||
import org.isoron.uhabits.core.models.*;
|
||||
import org.isoron.uhabits.core.tasks.*;
|
||||
import org.isoron.uhabits.utils.*;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import butterknife.*;
|
||||
|
||||
import static org.isoron.uhabits.activities.habits.show.views.ScoreCard.getTruncateField;
|
||||
|
||||
public class BarCard extends HabitCard
|
||||
{
|
||||
public static final int[] NUMERICAL_BUCKET_SIZES = {1, 7, 31, 92, 365};
|
||||
public static final int[] BOOLEAN_BUCKET_SIZES = {7, 31, 92, 365};
|
||||
|
||||
@BindView(R.id.numericalSpinner)
|
||||
Spinner numericalSpinner;
|
||||
|
||||
@BindView(R.id.boolSpinner)
|
||||
Spinner boolSpinner;
|
||||
|
||||
@BindView(R.id.barChart)
|
||||
BarChart chart;
|
||||
|
||||
@BindView(R.id.title)
|
||||
TextView title;
|
||||
|
||||
@Nullable
|
||||
private TaskRunner taskRunner;
|
||||
|
||||
private int bucketSize;
|
||||
|
||||
public BarCard(Context context)
|
||||
{
|
||||
super(context);
|
||||
init();
|
||||
}
|
||||
|
||||
public BarCard(Context context, AttributeSet attrs)
|
||||
{
|
||||
super(context, attrs);
|
||||
init();
|
||||
}
|
||||
|
||||
@OnItemSelected(R.id.numericalSpinner)
|
||||
public void onNumericalItemSelected(int position)
|
||||
{
|
||||
bucketSize = NUMERICAL_BUCKET_SIZES[position];
|
||||
refreshData();
|
||||
}
|
||||
|
||||
@OnItemSelected(R.id.boolSpinner)
|
||||
public void onBoolItemSelected(int position)
|
||||
{
|
||||
bucketSize = BOOLEAN_BUCKET_SIZES[position];
|
||||
refreshData();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void refreshData()
|
||||
{
|
||||
if (taskRunner == null) return;
|
||||
taskRunner.execute(new RefreshTask(getHabit()));
|
||||
}
|
||||
|
||||
private void init()
|
||||
{
|
||||
inflate(getContext(), R.layout.show_habit_bar, this);
|
||||
ButterKnife.bind(this);
|
||||
|
||||
boolSpinner.setSelection(1);
|
||||
numericalSpinner.setSelection(2);
|
||||
bucketSize = 7;
|
||||
|
||||
Context appContext = getContext().getApplicationContext();
|
||||
if (appContext instanceof HabitsApplication)
|
||||
{
|
||||
HabitsApplication app = (HabitsApplication) appContext;
|
||||
taskRunner = app.getComponent().getTaskRunner();
|
||||
}
|
||||
|
||||
if (isInEditMode()) initEditMode();
|
||||
}
|
||||
|
||||
private void initEditMode()
|
||||
{
|
||||
int color = PaletteUtils.getAndroidTestColor(1);
|
||||
title.setTextColor(color);
|
||||
chart.setColor(color);
|
||||
chart.populateWithRandomData();
|
||||
}
|
||||
|
||||
private class RefreshTask implements Task
|
||||
{
|
||||
private final Habit habit;
|
||||
|
||||
public RefreshTask(Habit habit)
|
||||
{
|
||||
this.habit = habit;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doInBackground()
|
||||
{
|
||||
List<Checkmark> checkmarks;
|
||||
if (bucketSize == 1) checkmarks = habit.getCheckmarks().getAll();
|
||||
else checkmarks = habit.getCheckmarks().groupBy(getTruncateField(bucketSize));
|
||||
chart.setCheckmarks(checkmarks);
|
||||
chart.setBucketSize(bucketSize);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPreExecute()
|
||||
{
|
||||
int color = PaletteUtils.getColor(getContext(), habit.getColor());
|
||||
title.setTextColor(color);
|
||||
chart.setColor(color);
|
||||
if (habit.isNumerical())
|
||||
{
|
||||
boolSpinner.setVisibility(GONE);
|
||||
chart.setTarget(habit.getTargetValue() * bucketSize);
|
||||
}
|
||||
else
|
||||
{
|
||||
numericalSpinner.setVisibility(GONE);
|
||||
chart.setTarget(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.habits.show.views;
|
||||
|
||||
import android.content.*;
|
||||
import android.support.annotation.*;
|
||||
import android.util.*;
|
||||
import android.widget.*;
|
||||
|
||||
import org.isoron.uhabits.*;
|
||||
import org.isoron.uhabits.R;
|
||||
import org.isoron.uhabits.activities.common.views.*;
|
||||
import org.isoron.uhabits.core.models.*;
|
||||
import org.isoron.uhabits.core.tasks.*;
|
||||
import org.isoron.uhabits.utils.*;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import butterknife.*;
|
||||
|
||||
public class FrequencyCard extends HabitCard
|
||||
{
|
||||
@BindView(R.id.title)
|
||||
TextView title;
|
||||
|
||||
@BindView(R.id.frequencyChart)
|
||||
FrequencyChart chart;
|
||||
|
||||
@Nullable
|
||||
private TaskRunner taskRunner;
|
||||
|
||||
public FrequencyCard(Context context)
|
||||
{
|
||||
super(context);
|
||||
init();
|
||||
}
|
||||
|
||||
public FrequencyCard(Context context, AttributeSet attrs)
|
||||
{
|
||||
super(context, attrs);
|
||||
init();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void refreshData()
|
||||
{
|
||||
if(taskRunner == null) return;
|
||||
taskRunner.execute(new RefreshTask());
|
||||
}
|
||||
|
||||
private void init()
|
||||
{
|
||||
inflate(getContext(), R.layout.show_habit_frequency, this);
|
||||
ButterKnife.bind(this);
|
||||
|
||||
Context appContext = getContext().getApplicationContext();
|
||||
if(appContext instanceof HabitsApplication)
|
||||
{
|
||||
HabitsApplication app = (HabitsApplication) appContext;
|
||||
taskRunner = app.getComponent().getTaskRunner();
|
||||
}
|
||||
|
||||
if (isInEditMode()) initEditMode();
|
||||
}
|
||||
|
||||
private void initEditMode()
|
||||
{
|
||||
int color = PaletteUtils.getAndroidTestColor(1);
|
||||
title.setTextColor(color);
|
||||
chart.setColor(color);
|
||||
chart.populateWithRandomData();
|
||||
}
|
||||
|
||||
private class RefreshTask implements Task
|
||||
{
|
||||
@Override
|
||||
public void doInBackground()
|
||||
{
|
||||
RepetitionList reps = getHabit().getRepetitions();
|
||||
HashMap<Timestamp, Integer[]> frequency = reps.getWeekdayFrequency();
|
||||
chart.setFrequency(frequency);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPreExecute()
|
||||
{
|
||||
int paletteColor = getHabit().getColor();
|
||||
int color = PaletteUtils.getColor(getContext(), paletteColor);
|
||||
title.setTextColor(color);
|
||||
chart.setColor(color);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.habits.show.views;
|
||||
|
||||
import android.content.*;
|
||||
import android.support.annotation.*;
|
||||
import android.util.*;
|
||||
import android.widget.*;
|
||||
|
||||
import org.isoron.uhabits.core.models.*;
|
||||
import org.isoron.uhabits.core.models.memory.*;
|
||||
|
||||
public abstract class HabitCard extends LinearLayout
|
||||
implements ModelObservable.Listener
|
||||
{
|
||||
@NonNull
|
||||
private Habit habit;
|
||||
|
||||
public HabitCard(Context context)
|
||||
{
|
||||
super(context);
|
||||
init();
|
||||
}
|
||||
|
||||
public HabitCard(Context context, AttributeSet attrs)
|
||||
{
|
||||
super(context, attrs);
|
||||
init();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Habit getHabit()
|
||||
{
|
||||
return habit;
|
||||
}
|
||||
|
||||
public void setHabit(@NonNull Habit habit)
|
||||
{
|
||||
detachFrom(this.habit);
|
||||
attachTo(habit);
|
||||
|
||||
this.habit = habit;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onModelChange()
|
||||
{
|
||||
post(() -> refreshData());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onAttachedToWindow()
|
||||
{
|
||||
if(isInEditMode()) return;
|
||||
|
||||
super.onAttachedToWindow();
|
||||
refreshData();
|
||||
attachTo(habit);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDetachedFromWindow()
|
||||
{
|
||||
detachFrom(habit);
|
||||
super.onDetachedFromWindow();
|
||||
}
|
||||
|
||||
protected abstract void refreshData();
|
||||
|
||||
private void attachTo(Habit habit)
|
||||
{
|
||||
habit.getObservable().addListener(this);
|
||||
habit.getRepetitions().getObservable().addListener(this);
|
||||
}
|
||||
|
||||
private void detachFrom(Habit habit)
|
||||
{
|
||||
habit.getRepetitions().getObservable().removeListener(this);
|
||||
habit.getObservable().removeListener(this);
|
||||
}
|
||||
|
||||
private void init()
|
||||
{
|
||||
if(!isInEditMode()) habit = new MemoryModelFactory().buildHabit();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.habits.show.views;
|
||||
|
||||
import android.content.*;
|
||||
import android.support.annotation.*;
|
||||
import android.util.*;
|
||||
import android.widget.*;
|
||||
|
||||
import org.isoron.uhabits.*;
|
||||
import org.isoron.uhabits.R;
|
||||
import org.isoron.uhabits.activities.common.views.*;
|
||||
import org.isoron.uhabits.core.models.*;
|
||||
import org.isoron.uhabits.core.tasks.*;
|
||||
import org.isoron.uhabits.utils.*;
|
||||
|
||||
import butterknife.*;
|
||||
|
||||
public class HistoryCard extends HabitCard
|
||||
{
|
||||
@BindView(R.id.historyChart)
|
||||
HistoryChart chart;
|
||||
|
||||
@BindView(R.id.title)
|
||||
TextView title;
|
||||
|
||||
@NonNull
|
||||
private Controller controller;
|
||||
|
||||
@Nullable
|
||||
private TaskRunner taskRunner;
|
||||
|
||||
public HistoryCard(Context context)
|
||||
{
|
||||
super(context);
|
||||
init();
|
||||
}
|
||||
|
||||
public HistoryCard(Context context, AttributeSet attrs)
|
||||
{
|
||||
super(context, attrs);
|
||||
init();
|
||||
}
|
||||
|
||||
@OnClick(R.id.edit)
|
||||
public void onClickEditButton()
|
||||
{
|
||||
controller.onEditHistoryButtonClick();
|
||||
}
|
||||
|
||||
public void setController(@NonNull Controller controller)
|
||||
{
|
||||
this.controller = controller;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void refreshData()
|
||||
{
|
||||
if(taskRunner == null) return;
|
||||
taskRunner.execute(new RefreshTask(getHabit()));
|
||||
}
|
||||
|
||||
private void init()
|
||||
{
|
||||
inflate(getContext(), R.layout.show_habit_history, this);
|
||||
ButterKnife.bind(this);
|
||||
|
||||
Context appContext = getContext().getApplicationContext();
|
||||
if (appContext instanceof HabitsApplication)
|
||||
{
|
||||
HabitsApplication app = (HabitsApplication) appContext;
|
||||
taskRunner = app.getComponent().getTaskRunner();
|
||||
}
|
||||
|
||||
controller = new Controller() {};
|
||||
if (isInEditMode()) initEditMode();
|
||||
}
|
||||
|
||||
private void initEditMode()
|
||||
{
|
||||
int color = PaletteUtils.getAndroidTestColor(1);
|
||||
title.setTextColor(color);
|
||||
chart.setColor(color);
|
||||
chart.populateWithRandomData();
|
||||
}
|
||||
|
||||
public interface Controller
|
||||
{
|
||||
default void onEditHistoryButtonClick() {}
|
||||
}
|
||||
|
||||
private class RefreshTask implements Task
|
||||
{
|
||||
private final Habit habit;
|
||||
|
||||
public RefreshTask(Habit habit) {this.habit = habit;}
|
||||
|
||||
@Override
|
||||
public void doInBackground()
|
||||
{
|
||||
int checkmarks[] = habit.getCheckmarks().getAllValues();
|
||||
chart.setCheckmarks(checkmarks);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPreExecute()
|
||||
{
|
||||
int color = PaletteUtils.getColor(getContext(), habit.getColor());
|
||||
title.setTextColor(color);
|
||||
chart.setColor(color);
|
||||
if(habit.isNumerical())
|
||||
{
|
||||
chart.setTarget((int) (habit.getTargetValue() * 1000));
|
||||
chart.setNumerical(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.habits.show.views;
|
||||
|
||||
import android.content.*;
|
||||
import android.support.annotation.*;
|
||||
import android.util.*;
|
||||
import android.widget.*;
|
||||
|
||||
import org.isoron.androidbase.utils.*;
|
||||
import org.isoron.uhabits.*;
|
||||
import org.isoron.uhabits.R;
|
||||
import org.isoron.uhabits.activities.common.views.*;
|
||||
import org.isoron.uhabits.core.models.*;
|
||||
import org.isoron.uhabits.core.tasks.*;
|
||||
import org.isoron.uhabits.core.utils.*;
|
||||
import org.isoron.uhabits.utils.*;
|
||||
|
||||
import butterknife.*;
|
||||
|
||||
public class OverviewCard extends HabitCard
|
||||
{
|
||||
@NonNull
|
||||
private Cache cache;
|
||||
|
||||
@BindView(R.id.scoreRing)
|
||||
RingView scoreRing;
|
||||
|
||||
@BindView(R.id.scoreLabel)
|
||||
TextView scoreLabel;
|
||||
|
||||
@BindView(R.id.monthDiffLabel)
|
||||
TextView monthDiffLabel;
|
||||
|
||||
@BindView(R.id.yearDiffLabel)
|
||||
TextView yearDiffLabel;
|
||||
|
||||
@BindView(R.id.totalCountLabel)
|
||||
TextView totalCountLabel;
|
||||
|
||||
@BindView(R.id.title)
|
||||
TextView title;
|
||||
|
||||
private int color;
|
||||
|
||||
@Nullable
|
||||
private TaskRunner taskRunner;
|
||||
|
||||
public OverviewCard(Context context)
|
||||
{
|
||||
super(context);
|
||||
init();
|
||||
}
|
||||
|
||||
public OverviewCard(Context context, AttributeSet attrs)
|
||||
{
|
||||
super(context, attrs);
|
||||
init();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void refreshData()
|
||||
{
|
||||
if(taskRunner == null) return;
|
||||
taskRunner.execute(new RefreshTask());
|
||||
}
|
||||
|
||||
private String formatPercentageDiff(float percentageDiff)
|
||||
{
|
||||
return String.format("%s%.0f%%", (percentageDiff >= 0 ? "+" : "\u2212"),
|
||||
Math.abs(percentageDiff) * 100);
|
||||
}
|
||||
|
||||
private void init()
|
||||
{
|
||||
Context appContext = getContext().getApplicationContext();
|
||||
if (appContext instanceof HabitsApplication)
|
||||
{
|
||||
HabitsApplication app = (HabitsApplication) appContext;
|
||||
taskRunner = app.getComponent().getTaskRunner();
|
||||
}
|
||||
|
||||
inflate(getContext(), R.layout.show_habit_overview, this);
|
||||
ButterKnife.bind(this);
|
||||
cache = new Cache();
|
||||
|
||||
if (isInEditMode()) initEditMode();
|
||||
}
|
||||
|
||||
private void initEditMode()
|
||||
{
|
||||
color = PaletteUtils.getAndroidTestColor(1);
|
||||
cache.todayScore = 0.6f;
|
||||
cache.lastMonthScore = 0.42f;
|
||||
cache.lastYearScore = 0.75f;
|
||||
refreshColors();
|
||||
refreshScore();
|
||||
}
|
||||
|
||||
private void refreshColors()
|
||||
{
|
||||
scoreRing.setColor(color);
|
||||
scoreLabel.setTextColor(color);
|
||||
title.setTextColor(color);
|
||||
}
|
||||
|
||||
private void refreshScore()
|
||||
{
|
||||
float todayPercentage = cache.todayScore;
|
||||
float monthDiff = todayPercentage - cache.lastMonthScore;
|
||||
float yearDiff = todayPercentage - cache.lastYearScore;
|
||||
|
||||
scoreRing.setPercentage(todayPercentage);
|
||||
scoreLabel.setText(String.format("%.0f%%", todayPercentage * 100));
|
||||
|
||||
monthDiffLabel.setText(formatPercentageDiff(monthDiff));
|
||||
yearDiffLabel.setText(formatPercentageDiff(yearDiff));
|
||||
totalCountLabel.setText(String.valueOf(cache.totalCount));
|
||||
|
||||
StyledResources res = new StyledResources(getContext());
|
||||
int inactiveColor = res.getColor(R.attr.mediumContrastTextColor);
|
||||
|
||||
monthDiffLabel.setTextColor(monthDiff >= 0 ? color : inactiveColor);
|
||||
yearDiffLabel.setTextColor(yearDiff >= 0 ? color : inactiveColor);
|
||||
totalCountLabel.setTextColor(yearDiff >= 0 ? color : inactiveColor);
|
||||
|
||||
postInvalidate();
|
||||
}
|
||||
|
||||
private class Cache
|
||||
{
|
||||
public float todayScore;
|
||||
|
||||
public float lastMonthScore;
|
||||
|
||||
public float lastYearScore;
|
||||
|
||||
public long totalCount;
|
||||
}
|
||||
|
||||
private class RefreshTask implements Task
|
||||
{
|
||||
@Override
|
||||
public void doInBackground()
|
||||
{
|
||||
Habit habit = getHabit();
|
||||
|
||||
ScoreList scores = habit.getScores();
|
||||
|
||||
Timestamp today = DateUtils.getToday();
|
||||
Timestamp lastMonth = today.minus(30);
|
||||
Timestamp lastYear = today.minus(365);
|
||||
|
||||
cache.todayScore = (float) scores.getTodayValue();
|
||||
cache.lastMonthScore = (float) scores.getValue(lastMonth);
|
||||
cache.lastYearScore = (float) scores.getValue(lastYear);
|
||||
cache.totalCount = habit.getRepetitions().getTotalCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPostExecute()
|
||||
{
|
||||
refreshScore();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPreExecute()
|
||||
{
|
||||
color = PaletteUtils.getColor(getContext(), getHabit().getColor());
|
||||
refreshColors();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.habits.show.views;
|
||||
|
||||
import android.content.*;
|
||||
import android.support.annotation.*;
|
||||
import android.util.*;
|
||||
import android.widget.*;
|
||||
|
||||
import org.isoron.uhabits.*;
|
||||
import org.isoron.uhabits.R;
|
||||
import org.isoron.uhabits.activities.common.views.*;
|
||||
import org.isoron.uhabits.core.models.*;
|
||||
import org.isoron.uhabits.core.preferences.*;
|
||||
import org.isoron.uhabits.core.tasks.*;
|
||||
import org.isoron.uhabits.core.utils.*;
|
||||
import org.isoron.uhabits.utils.*;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import butterknife.*;
|
||||
|
||||
public class ScoreCard extends HabitCard
|
||||
{
|
||||
public static final int[] BUCKET_SIZES = { 1, 7, 31, 92, 365 };
|
||||
|
||||
@BindView(R.id.spinner)
|
||||
Spinner spinner;
|
||||
|
||||
@BindView(R.id.scoreView)
|
||||
ScoreChart chart;
|
||||
|
||||
@BindView(R.id.title)
|
||||
TextView title;
|
||||
|
||||
private int bucketSize;
|
||||
|
||||
@Nullable
|
||||
private TaskRunner taskRunner;
|
||||
|
||||
@Nullable
|
||||
private Preferences prefs;
|
||||
|
||||
public ScoreCard(Context context)
|
||||
{
|
||||
super(context);
|
||||
init();
|
||||
}
|
||||
|
||||
public ScoreCard(Context context, AttributeSet attrs)
|
||||
{
|
||||
super(context, attrs);
|
||||
init();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static DateUtils.TruncateField getTruncateField(int bucketSize)
|
||||
{
|
||||
if (bucketSize == 7) return DateUtils.TruncateField.WEEK_NUMBER;
|
||||
if (bucketSize == 31) return DateUtils.TruncateField.MONTH;
|
||||
if (bucketSize == 92) return DateUtils.TruncateField.QUARTER;
|
||||
if (bucketSize == 365) return DateUtils.TruncateField.YEAR;
|
||||
|
||||
Log.e("ScoreCard",
|
||||
String.format("Unknown bucket size: %d", bucketSize));
|
||||
|
||||
return DateUtils.TruncateField.MONTH;
|
||||
}
|
||||
|
||||
@OnItemSelected(R.id.spinner)
|
||||
public void onItemSelected(int position)
|
||||
{
|
||||
setBucketSizeFromPosition(position);
|
||||
HabitsApplication app =
|
||||
(HabitsApplication) getContext().getApplicationContext();
|
||||
app.getComponent().getWidgetUpdater().updateWidgets();
|
||||
refreshData();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void refreshData()
|
||||
{
|
||||
if(taskRunner == null) return;
|
||||
taskRunner.execute(new RefreshTask());
|
||||
}
|
||||
|
||||
private int getDefaultSpinnerPosition()
|
||||
{
|
||||
if(prefs == null) return 0;
|
||||
return prefs.getDefaultScoreSpinnerPosition();
|
||||
}
|
||||
|
||||
private void init()
|
||||
{
|
||||
Context appContext = getContext().getApplicationContext();
|
||||
if (appContext instanceof HabitsApplication)
|
||||
{
|
||||
HabitsApplication app = (HabitsApplication) appContext;
|
||||
taskRunner = app.getComponent().getTaskRunner();
|
||||
prefs = app.getComponent().getPreferences();
|
||||
}
|
||||
|
||||
inflate(getContext(), R.layout.show_habit_score, this);
|
||||
ButterKnife.bind(this);
|
||||
|
||||
int defaultPosition = getDefaultSpinnerPosition();
|
||||
setBucketSizeFromPosition(defaultPosition);
|
||||
spinner.setSelection(defaultPosition);
|
||||
|
||||
if (isInEditMode())
|
||||
{
|
||||
spinner.setVisibility(GONE);
|
||||
title.setTextColor(PaletteUtils.getAndroidTestColor(1));
|
||||
chart.setColor(PaletteUtils.getAndroidTestColor(1));
|
||||
chart.populateWithRandomData();
|
||||
}
|
||||
}
|
||||
|
||||
private void setBucketSizeFromPosition(int position)
|
||||
{
|
||||
if(prefs == null) return;
|
||||
prefs.setDefaultScoreSpinnerPosition(position);
|
||||
bucketSize = BUCKET_SIZES[position];
|
||||
}
|
||||
|
||||
private class RefreshTask implements Task
|
||||
{
|
||||
@Override
|
||||
public void doInBackground()
|
||||
{
|
||||
List<Score> scores;
|
||||
ScoreList scoreList = getHabit().getScores();
|
||||
|
||||
if (bucketSize == 1) scores = scoreList.toList();
|
||||
else scores = scoreList.groupBy(getTruncateField(bucketSize));
|
||||
|
||||
chart.setScores(scores);
|
||||
chart.setBucketSize(bucketSize);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPreExecute()
|
||||
{
|
||||
int color =
|
||||
PaletteUtils.getColor(getContext(), getHabit().getColor());
|
||||
title.setTextColor(color);
|
||||
chart.setColor(color);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.habits.show.views;
|
||||
|
||||
import android.content.*;
|
||||
import android.support.annotation.*;
|
||||
import android.util.*;
|
||||
import android.widget.*;
|
||||
|
||||
import org.isoron.uhabits.*;
|
||||
import org.isoron.uhabits.R;
|
||||
import org.isoron.uhabits.activities.common.views.*;
|
||||
import org.isoron.uhabits.core.models.*;
|
||||
import org.isoron.uhabits.core.tasks.*;
|
||||
import org.isoron.uhabits.utils.*;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import butterknife.*;
|
||||
|
||||
public class StreakCard extends HabitCard
|
||||
{
|
||||
public static final int NUM_STREAKS = 10;
|
||||
|
||||
@BindView(R.id.title)
|
||||
TextView title;
|
||||
|
||||
@BindView(R.id.streakChart)
|
||||
StreakChart streakChart;
|
||||
|
||||
@Nullable
|
||||
private TaskRunner taskRunner;
|
||||
|
||||
public StreakCard(Context context)
|
||||
{
|
||||
super(context);
|
||||
init();
|
||||
}
|
||||
|
||||
public StreakCard(Context context, AttributeSet attrs)
|
||||
{
|
||||
super(context, attrs);
|
||||
init();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void refreshData()
|
||||
{
|
||||
if(taskRunner == null) return;
|
||||
taskRunner.execute(new RefreshTask());
|
||||
}
|
||||
|
||||
private void init()
|
||||
{
|
||||
Context appContext = getContext().getApplicationContext();
|
||||
if (appContext instanceof HabitsApplication)
|
||||
{
|
||||
HabitsApplication app = (HabitsApplication) appContext;
|
||||
taskRunner = app.getComponent().getTaskRunner();
|
||||
}
|
||||
|
||||
inflate(getContext(), R.layout.show_habit_streak, this);
|
||||
ButterKnife.bind(this);
|
||||
setOrientation(VERTICAL);
|
||||
if (isInEditMode()) initEditMode();
|
||||
}
|
||||
|
||||
private void initEditMode()
|
||||
{
|
||||
int color = PaletteUtils.getAndroidTestColor(1);
|
||||
title.setTextColor(color);
|
||||
streakChart.setColor(color);
|
||||
streakChart.populateWithRandomData();
|
||||
}
|
||||
|
||||
private class RefreshTask implements Task
|
||||
{
|
||||
public List<Streak> bestStreaks;
|
||||
|
||||
@Override
|
||||
public void doInBackground()
|
||||
{
|
||||
StreakList streaks = getHabit().getStreaks();
|
||||
bestStreaks = streaks.getBest(NUM_STREAKS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPostExecute()
|
||||
{
|
||||
streakChart.setStreaks(bestStreaks);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPreExecute()
|
||||
{
|
||||
int color =
|
||||
PaletteUtils.getColor(getContext(), getHabit().getColor());
|
||||
title.setTextColor(color);
|
||||
streakChart.setColor(color);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.habits.show.views;
|
||||
|
||||
import android.annotation.*;
|
||||
import android.content.*;
|
||||
import android.content.res.*;
|
||||
import android.util.*;
|
||||
import android.widget.*;
|
||||
|
||||
import org.isoron.uhabits.R;
|
||||
import org.isoron.uhabits.core.models.*;
|
||||
import org.isoron.uhabits.utils.*;
|
||||
|
||||
import butterknife.*;
|
||||
|
||||
public class SubtitleCard extends HabitCard
|
||||
{
|
||||
@BindView(R.id.questionLabel)
|
||||
TextView questionLabel;
|
||||
|
||||
@BindView(R.id.frequencyLabel)
|
||||
TextView frequencyLabel;
|
||||
|
||||
@BindView(R.id.reminderLabel)
|
||||
TextView reminderLabel;
|
||||
|
||||
public SubtitleCard(Context context, AttributeSet attrs)
|
||||
{
|
||||
super(context, attrs);
|
||||
init();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void refreshData()
|
||||
{
|
||||
Habit habit = getHabit();
|
||||
int color = PaletteUtils.getColor(getContext(), habit.getColor());
|
||||
|
||||
reminderLabel.setText(getResources().getString(R.string.reminder_off));
|
||||
questionLabel.setVisibility(VISIBLE);
|
||||
|
||||
questionLabel.setTextColor(color);
|
||||
questionLabel.setText(habit.getDescription());
|
||||
frequencyLabel.setText(toText(habit.getFrequency()));
|
||||
|
||||
if (habit.hasReminder()) updateReminderText(habit.getReminder());
|
||||
|
||||
if (habit.getDescription().isEmpty()) questionLabel.setVisibility(GONE);
|
||||
|
||||
invalidate();
|
||||
}
|
||||
|
||||
private void init()
|
||||
{
|
||||
inflate(getContext(), R.layout.show_habit_subtitle, this);
|
||||
ButterKnife.bind(this);
|
||||
|
||||
if (isInEditMode()) initEditMode();
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
private void initEditMode()
|
||||
{
|
||||
questionLabel.setTextColor(PaletteUtils.getAndroidTestColor(1));
|
||||
questionLabel.setText("Have you meditated today?");
|
||||
reminderLabel.setText("08:00");
|
||||
}
|
||||
|
||||
private String toText(Frequency freq)
|
||||
{
|
||||
Resources resources = getResources();
|
||||
Integer num = freq.getNumerator();
|
||||
Integer den = freq.getDenominator();
|
||||
|
||||
if (num.equals(den)) return resources.getString(R.string.every_day);
|
||||
|
||||
if (num == 1)
|
||||
{
|
||||
if (den == 7) return resources.getString(R.string.every_week);
|
||||
if (den % 7 == 0)
|
||||
return resources.getString(R.string.every_x_weeks, den / 7);
|
||||
return resources.getString(R.string.every_x_days, den);
|
||||
}
|
||||
|
||||
String times_every = resources.getString(R.string.times_every);
|
||||
return String.format("%d %s %d %s", num, times_every, den,
|
||||
resources.getString(R.string.days));
|
||||
}
|
||||
|
||||
private void updateReminderText(Reminder reminder)
|
||||
{
|
||||
reminderLabel.setText(
|
||||
AndroidDateUtils.formatTime(getContext(), reminder.getHour(),
|
||||
reminder.getMinute()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.intro;
|
||||
|
||||
import android.graphics.*;
|
||||
import android.os.*;
|
||||
|
||||
import com.github.paolorotolo.appintro.*;
|
||||
|
||||
import org.isoron.uhabits.R;
|
||||
|
||||
/**
|
||||
* Activity that introduces the app to the user, shown only after the app is
|
||||
* launched for the first time.
|
||||
*/
|
||||
public class IntroActivity extends AppIntro2
|
||||
{
|
||||
@Override
|
||||
public void init(Bundle savedInstanceState)
|
||||
{
|
||||
showStatusBar(false);
|
||||
|
||||
addSlide(AppIntroFragment.newInstance(getString(R.string.intro_title_1),
|
||||
getString(R.string.intro_description_1), R.drawable.intro_icon_1,
|
||||
Color.parseColor("#194673")));
|
||||
|
||||
addSlide(AppIntroFragment.newInstance(getString(R.string.intro_title_2),
|
||||
getString(R.string.intro_description_2), R.drawable.intro_icon_2,
|
||||
Color.parseColor("#ffa726")));
|
||||
|
||||
addSlide(AppIntroFragment.newInstance(getString(R.string.intro_title_4),
|
||||
getString(R.string.intro_description_4), R.drawable.intro_icon_4,
|
||||
Color.parseColor("#9575cd")));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNextPressed()
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDonePressed()
|
||||
{
|
||||
finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSlideChanged()
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.settings;
|
||||
|
||||
import android.os.*;
|
||||
|
||||
import org.isoron.androidbase.activities.*;
|
||||
import org.isoron.androidbase.utils.*;
|
||||
import org.isoron.uhabits.R;
|
||||
|
||||
/**
|
||||
* Activity that allows the user to view and modify the app settings.
|
||||
*/
|
||||
public class SettingsActivity extends BaseActivity
|
||||
{
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState)
|
||||
{
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.settings_activity);
|
||||
setupActionBarColor();
|
||||
}
|
||||
|
||||
private void setupActionBarColor()
|
||||
{
|
||||
StyledResources res = new StyledResources(this);
|
||||
int color = BaseScreen.getDefaultActionBarColor(this);
|
||||
|
||||
if (res.getBoolean(R.attr.useHabitColorAsPrimary))
|
||||
color = res.getColor(R.attr.aboutScreenColor);
|
||||
|
||||
BaseScreen.setupActionBarColor(this, color);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.activities.settings;
|
||||
|
||||
import android.app.backup.*;
|
||||
import android.content.*;
|
||||
import android.net.*;
|
||||
import android.os.*;
|
||||
import android.provider.*;
|
||||
import android.support.annotation.*;
|
||||
import android.support.v7.preference.*;
|
||||
|
||||
import org.isoron.uhabits.*;
|
||||
import org.isoron.uhabits.R;
|
||||
import org.isoron.uhabits.core.preferences.*;
|
||||
import org.isoron.uhabits.core.ui.*;
|
||||
import org.isoron.uhabits.notifications.*;
|
||||
|
||||
import static android.media.RingtoneManager.*;
|
||||
import static android.os.Build.VERSION.SDK_INT;
|
||||
import static org.isoron.uhabits.activities.habits.list.ListHabitsScreenKt.*;
|
||||
|
||||
public class SettingsFragment extends PreferenceFragmentCompat
|
||||
implements SharedPreferences.OnSharedPreferenceChangeListener
|
||||
{
|
||||
private static int RINGTONE_REQUEST_CODE = 1;
|
||||
|
||||
private SharedPreferences sharedPrefs;
|
||||
|
||||
private RingtoneManager ringtoneManager;
|
||||
|
||||
@Nullable
|
||||
private Preferences prefs;
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data)
|
||||
{
|
||||
if (requestCode == RINGTONE_REQUEST_CODE)
|
||||
{
|
||||
ringtoneManager.update(data);
|
||||
updateRingtoneDescription();
|
||||
return;
|
||||
}
|
||||
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState)
|
||||
{
|
||||
super.onCreate(savedInstanceState);
|
||||
addPreferencesFromResource(R.xml.preferences);
|
||||
|
||||
Context appContext = getContext().getApplicationContext();
|
||||
if (appContext instanceof HabitsApplication)
|
||||
{
|
||||
HabitsApplication app = (HabitsApplication) appContext;
|
||||
prefs = app.getComponent().getPreferences();
|
||||
}
|
||||
|
||||
setResultOnPreferenceClick("importData", RESULT_IMPORT_DATA);
|
||||
setResultOnPreferenceClick("exportCSV", RESULT_EXPORT_CSV);
|
||||
setResultOnPreferenceClick("exportDB", RESULT_EXPORT_DB);
|
||||
setResultOnPreferenceClick("repairDB", RESULT_REPAIR_DB);
|
||||
setResultOnPreferenceClick("bugReport", RESULT_BUG_REPORT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(Bundle bundle, String s)
|
||||
{
|
||||
// NOP
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause()
|
||||
{
|
||||
sharedPrefs.unregisterOnSharedPreferenceChangeListener(this);
|
||||
super.onPause();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPreferenceTreeClick(Preference preference)
|
||||
{
|
||||
String key = preference.getKey();
|
||||
if (key == null) return false;
|
||||
|
||||
if (key.equals("reminderSound"))
|
||||
{
|
||||
showRingtonePicker();
|
||||
return true;
|
||||
}
|
||||
else if (key.equals("reminderCustomize"))
|
||||
{
|
||||
if (SDK_INT < Build.VERSION_CODES.O) return true;
|
||||
AndroidNotificationTray.Companion.createAndroidNotificationChannel(getContext());
|
||||
Intent intent = new Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS);
|
||||
intent.putExtra(Settings.EXTRA_APP_PACKAGE, getContext().getPackageName());
|
||||
intent.putExtra(Settings.EXTRA_CHANNEL_ID, NotificationTray.REMINDERS_CHANNEL_ID);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
}
|
||||
|
||||
return super.onPreferenceTreeClick(preference);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume()
|
||||
{
|
||||
super.onResume();
|
||||
this.ringtoneManager = new RingtoneManager(getActivity());
|
||||
|
||||
sharedPrefs = getPreferenceManager().getSharedPreferences();
|
||||
sharedPrefs.registerOnSharedPreferenceChangeListener(this);
|
||||
|
||||
if (prefs != null && !prefs.isDeveloper())
|
||||
{
|
||||
PreferenceCategory devCategory =
|
||||
(PreferenceCategory) findPreference("devCategory");
|
||||
devCategory.removeAll();
|
||||
devCategory.setVisible(false);
|
||||
}
|
||||
|
||||
if (SDK_INT < Build.VERSION_CODES.O)
|
||||
findPreference("reminderCustomize").setVisible(false);
|
||||
else
|
||||
{
|
||||
findPreference("reminderSound").setVisible(false);
|
||||
findPreference("pref_snooze_interval").setVisible(false);
|
||||
}
|
||||
|
||||
updateSync();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,
|
||||
String key)
|
||||
{
|
||||
BackupManager.dataChanged("org.isoron.uhabits");
|
||||
updateSync();
|
||||
}
|
||||
|
||||
private void setResultOnPreferenceClick(String key, final int result)
|
||||
{
|
||||
Preference pref = findPreference(key);
|
||||
pref.setOnPreferenceClickListener(preference ->
|
||||
{
|
||||
getActivity().setResult(result);
|
||||
getActivity().finish();
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
private void showRingtonePicker()
|
||||
{
|
||||
Uri existingRingtoneUri = ringtoneManager.getURI();
|
||||
Uri defaultRingtoneUri = Settings.System.DEFAULT_NOTIFICATION_URI;
|
||||
|
||||
Intent intent = new Intent(ACTION_RINGTONE_PICKER);
|
||||
intent.putExtra(EXTRA_RINGTONE_TYPE, TYPE_NOTIFICATION);
|
||||
intent.putExtra(EXTRA_RINGTONE_SHOW_DEFAULT, true);
|
||||
intent.putExtra(EXTRA_RINGTONE_SHOW_SILENT, true);
|
||||
intent.putExtra(EXTRA_RINGTONE_DEFAULT_URI, defaultRingtoneUri);
|
||||
intent.putExtra(EXTRA_RINGTONE_EXISTING_URI, existingRingtoneUri);
|
||||
startActivityForResult(intent, RINGTONE_REQUEST_CODE);
|
||||
}
|
||||
|
||||
private void updateRingtoneDescription()
|
||||
{
|
||||
String ringtoneName = ringtoneManager.getName();
|
||||
if (ringtoneName == null) return;
|
||||
Preference ringtonePreference = findPreference("reminderSound");
|
||||
ringtonePreference.setSummary(ringtoneName);
|
||||
}
|
||||
|
||||
private void updateSync()
|
||||
{
|
||||
if (prefs == null) return;
|
||||
boolean enabled = prefs.isSyncEnabled();
|
||||
|
||||
Preference syncKey = findPreference("pref_sync_key");
|
||||
if (syncKey != null)
|
||||
{
|
||||
syncKey.setSummary(prefs.getSyncKey());
|
||||
syncKey.setVisible(enabled);
|
||||
}
|
||||
|
||||
Preference syncAddress = findPreference("pref_sync_address");
|
||||
if (syncAddress != null)
|
||||
{
|
||||
syncAddress.setSummary(prefs.getSyncAddress());
|
||||
syncAddress.setVisible(enabled);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright (C) 2015-2017 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.automation
|
||||
|
||||
import android.os.*
|
||||
|
||||
import org.isoron.androidbase.activities.*
|
||||
import org.isoron.uhabits.*
|
||||
import org.isoron.uhabits.core.models.*
|
||||
|
||||
class EditSettingActivity : BaseActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val app = applicationContext as HabitsApplication
|
||||
val habits = app.component.habitList.getFiltered(
|
||||
HabitMatcherBuilder()
|
||||
.setArchivedAllowed(false)
|
||||
.setCompletedAllowed(true)
|
||||
.build())
|
||||
|
||||
val controller = EditSettingController(this)
|
||||
val rootView = EditSettingRootView(this, habits, controller)
|
||||
val screen = BaseScreen(this)
|
||||
screen.setRootView(rootView)
|
||||
setScreen(screen)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright (C) 2015-2017 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.automation
|
||||
|
||||
import android.app.*
|
||||
import android.content.*
|
||||
import android.os.*
|
||||
import org.isoron.uhabits.*
|
||||
import org.isoron.uhabits.automation.FireSettingReceiver.*
|
||||
import org.isoron.uhabits.core.models.*
|
||||
|
||||
class EditSettingController(private val activity: Activity) {
|
||||
|
||||
fun onSave(habit: Habit, action: Int) {
|
||||
if (habit.getId() == null) return
|
||||
val actionName = getActionName(action)
|
||||
val blurb = String.format("%s: %s", actionName, habit.name)
|
||||
|
||||
val bundle = Bundle()
|
||||
bundle.putInt("action", action)
|
||||
bundle.putLong("habit", habit.getId()!!)
|
||||
|
||||
activity.setResult(Activity.RESULT_OK, Intent().apply {
|
||||
putExtra(EXTRA_STRING_BLURB, blurb)
|
||||
putExtra(EXTRA_BUNDLE, bundle)
|
||||
})
|
||||
activity.finish()
|
||||
}
|
||||
|
||||
private fun getActionName(action: Int): String {
|
||||
when (action) {
|
||||
ACTION_CHECK -> return activity.getString(R.string.check)
|
||||
ACTION_UNCHECK -> return activity.getString(R.string.uncheck)
|
||||
ACTION_TOGGLE -> return activity.getString(R.string.toggle)
|
||||
else -> return "???"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.automation
|
||||
|
||||
import android.R.layout.*
|
||||
import android.content.*
|
||||
import android.support.v7.widget.*
|
||||
import android.support.v7.widget.Toolbar
|
||||
import android.widget.*
|
||||
import butterknife.*
|
||||
import org.isoron.androidbase.activities.*
|
||||
import org.isoron.androidbase.utils.*
|
||||
import org.isoron.uhabits.R
|
||||
import org.isoron.uhabits.core.models.*
|
||||
import java.util.*
|
||||
|
||||
class EditSettingRootView(
|
||||
context: Context,
|
||||
private val habitList: HabitList,
|
||||
private val controller: EditSettingController
|
||||
) : BaseRootView(context) {
|
||||
|
||||
@BindView(R.id.toolbar)
|
||||
lateinit var tbar: Toolbar
|
||||
|
||||
@BindView(R.id.habitSpinner)
|
||||
lateinit var habitSpinner: AppCompatSpinner
|
||||
|
||||
@BindView(R.id.actionSpinner)
|
||||
lateinit var actionSpinner: AppCompatSpinner
|
||||
|
||||
init {
|
||||
addView(inflate(getContext(), R.layout.automation, null))
|
||||
ButterKnife.bind(this)
|
||||
populateHabitSpinner()
|
||||
}
|
||||
|
||||
override fun getToolbar(): Toolbar {
|
||||
return tbar
|
||||
}
|
||||
|
||||
override fun getToolbarColor(): Int {
|
||||
val res = StyledResources(context)
|
||||
if (!res.getBoolean(R.attr.useHabitColorAsPrimary))
|
||||
return super.getToolbarColor()
|
||||
|
||||
return res.getColor(R.attr.aboutScreenColor)
|
||||
}
|
||||
|
||||
@OnClick(R.id.buttonSave)
|
||||
fun onClickSave() {
|
||||
val action = actionSpinner.selectedItemPosition
|
||||
val habitPosition = habitSpinner.selectedItemPosition
|
||||
val habit = habitList.getByPosition(habitPosition)
|
||||
controller.onSave(habit, action)
|
||||
}
|
||||
|
||||
private fun populateHabitSpinner() {
|
||||
val names = habitList.mapTo(LinkedList<String>()) { it.name }
|
||||
val adapter = ArrayAdapter(context, simple_spinner_item, names)
|
||||
adapter.setDropDownViewResource(simple_spinner_dropdown_item)
|
||||
habitSpinner.adapter = adapter
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright (C) 2015-2017 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.automation
|
||||
|
||||
import android.content.*
|
||||
import dagger.*
|
||||
import org.isoron.uhabits.*
|
||||
import org.isoron.uhabits.core.models.*
|
||||
import org.isoron.uhabits.core.ui.widgets.*
|
||||
import org.isoron.uhabits.core.utils.*
|
||||
import org.isoron.uhabits.receivers.*
|
||||
|
||||
const val ACTION_CHECK = 0
|
||||
const val ACTION_UNCHECK = 1
|
||||
const val ACTION_TOGGLE = 2
|
||||
const val EXTRA_BUNDLE = "com.twofortyfouram.locale.intent.extra.BUNDLE"
|
||||
const val EXTRA_STRING_BLURB = "com.twofortyfouram.locale.intent.extra.BLURB"
|
||||
|
||||
class FireSettingReceiver : BroadcastReceiver() {
|
||||
|
||||
private lateinit var allHabits: HabitList
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
val app = context.applicationContext as HabitsApplication
|
||||
val component = DaggerFireSettingReceiver_ReceiverComponent
|
||||
.builder()
|
||||
.habitsApplicationComponent(app.component)
|
||||
.build()
|
||||
allHabits = app.component.habitList
|
||||
val args = parseIntent(intent) ?: return
|
||||
val timestamp = DateUtils.getToday()
|
||||
val controller = component.widgetController
|
||||
|
||||
when (args.action) {
|
||||
ACTION_CHECK -> controller.onAddRepetition(args.habit, timestamp)
|
||||
ACTION_UNCHECK -> controller.onRemoveRepetition(args.habit, timestamp)
|
||||
ACTION_TOGGLE -> controller.onToggleRepetition(args.habit, timestamp)
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseIntent(intent: Intent): Arguments? {
|
||||
val bundle = intent.getBundleExtra(EXTRA_BUNDLE) ?: return null
|
||||
val action = bundle.getInt("action")
|
||||
if (action < 0 || action > 2) return null
|
||||
val habit = allHabits.getById(bundle.getLong("habit")) ?: return null
|
||||
return Arguments(action, habit)
|
||||
}
|
||||
|
||||
@ReceiverScope
|
||||
@Component(dependencies = arrayOf(HabitsApplicationComponent::class))
|
||||
internal interface ReceiverComponent {
|
||||
val widgetController: WidgetBehavior
|
||||
}
|
||||
|
||||
private class Arguments(var action: Int, var habit: Habit)
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright (C) 2015-2017 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.database
|
||||
|
||||
import org.isoron.uhabits.core.database.*
|
||||
|
||||
class AndroidCursor(private val cursor: android.database.Cursor) : Cursor {
|
||||
|
||||
override fun close() = cursor.close()
|
||||
override fun moveToNext() = cursor.moveToNext()
|
||||
|
||||
override fun getInt(index: Int): Int? {
|
||||
if (cursor.isNull(index)) return null
|
||||
else return cursor.getInt(index)
|
||||
}
|
||||
|
||||
override fun getLong(index: Int): Long? {
|
||||
if (cursor.isNull(index)) return null
|
||||
else return cursor.getLong(index)
|
||||
}
|
||||
|
||||
override fun getDouble(index: Int): Double? {
|
||||
if (cursor.isNull(index)) return null
|
||||
else return cursor.getDouble(index)
|
||||
}
|
||||
|
||||
override fun getString(index: Int): String? {
|
||||
if (cursor.isNull(index)) return null
|
||||
else return cursor.getString(index)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright (C) 2015-2017 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.database
|
||||
|
||||
import android.content.*
|
||||
import android.database.sqlite.*
|
||||
import org.isoron.uhabits.core.database.*
|
||||
|
||||
class AndroidDatabase(private val db: SQLiteDatabase) : Database {
|
||||
|
||||
override fun beginTransaction() = db.beginTransaction()
|
||||
override fun setTransactionSuccessful() = db.setTransactionSuccessful()
|
||||
override fun endTransaction() = db.endTransaction()
|
||||
override fun close() = db.close()
|
||||
override fun getVersion() = db.version
|
||||
|
||||
override fun query(query: String, vararg params: String)
|
||||
= AndroidCursor(db.rawQuery(query, params))
|
||||
|
||||
override fun execute(query: String, vararg params: Any)
|
||||
= db.execSQL(query, params)
|
||||
|
||||
override fun update(tableName: String,
|
||||
map: Map<String, Any?>,
|
||||
where: String,
|
||||
vararg params: String): Int {
|
||||
val values = mapToContentValues(map)
|
||||
return db.update(tableName, values, where, params)
|
||||
}
|
||||
|
||||
override fun insert(tableName: String, map: Map<String, Any?>): Long? {
|
||||
val values = mapToContentValues(map)
|
||||
return db.insert(tableName, null, values)
|
||||
}
|
||||
|
||||
override fun delete(tableName: String,
|
||||
where: String,
|
||||
vararg params: String) {
|
||||
db.delete(tableName, where, params)
|
||||
}
|
||||
|
||||
private fun mapToContentValues(map: Map<String, Any?>): ContentValues {
|
||||
val values = ContentValues()
|
||||
for ((key, value) in map) {
|
||||
when (value) {
|
||||
null -> values.putNull(key)
|
||||
is Int -> values.put(key, value)
|
||||
is Long -> values.put(key, value)
|
||||
is Double -> values.put(key, value)
|
||||
is String -> values.put(key, value)
|
||||
else -> throw IllegalStateException(
|
||||
"unsupported type: " + value)
|
||||
}
|
||||
}
|
||||
return values
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright (C) 2015-2017 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.database
|
||||
|
||||
import android.database.sqlite.*
|
||||
import org.isoron.uhabits.core.database.*
|
||||
import java.io.*
|
||||
import javax.inject.*
|
||||
|
||||
class AndroidDatabaseOpener @Inject constructor() : DatabaseOpener {
|
||||
override fun open(file: File): AndroidDatabase {
|
||||
return AndroidDatabase(SQLiteDatabase.openDatabase(
|
||||
file.absolutePath, null, SQLiteDatabase.OPEN_READWRITE))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.intents
|
||||
|
||||
import android.content.*
|
||||
import android.net.*
|
||||
import org.isoron.uhabits.*
|
||||
import org.isoron.uhabits.activities.about.*
|
||||
import org.isoron.uhabits.activities.habits.show.*
|
||||
import org.isoron.uhabits.activities.intro.*
|
||||
import org.isoron.uhabits.activities.settings.*
|
||||
import org.isoron.uhabits.core.models.*
|
||||
import javax.inject.*
|
||||
|
||||
class IntentFactory
|
||||
@Inject constructor() {
|
||||
|
||||
fun helpTranslate(context: Context) =
|
||||
buildViewIntent(context.getString(R.string.translateURL))
|
||||
|
||||
fun openDocument() = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = "*/*"
|
||||
}
|
||||
|
||||
fun rateApp(context: Context) =
|
||||
buildViewIntent(context.getString(R.string.playStoreURL))
|
||||
|
||||
fun sendFeedback(context: Context) =
|
||||
buildSendToIntent(context.getString(R.string.feedbackURL))
|
||||
|
||||
fun startAboutActivity(context: Context) =
|
||||
Intent(context, AboutActivity::class.java)
|
||||
|
||||
fun startIntroActivity(context: Context) =
|
||||
Intent(context, IntroActivity::class.java)
|
||||
|
||||
fun startSettingsActivity(context: Context) =
|
||||
Intent(context, SettingsActivity::class.java)
|
||||
|
||||
fun startShowHabitActivity(context: Context, habit: Habit) =
|
||||
Intent(context, ShowHabitActivity::class.java).apply {
|
||||
data = Uri.parse(habit.uriString)
|
||||
}
|
||||
|
||||
fun viewFAQ(context: Context) =
|
||||
buildViewIntent(context.getString(R.string.helpURL))
|
||||
|
||||
fun viewSourceCode(context: Context) =
|
||||
buildViewIntent(context.getString(R.string.sourceCodeURL))
|
||||
|
||||
private fun buildSendToIntent(url: String) = Intent().apply {
|
||||
action = Intent.ACTION_SENDTO
|
||||
data = Uri.parse(url)
|
||||
}
|
||||
|
||||
private fun buildViewIntent(url: String) = Intent().apply {
|
||||
action = Intent.ACTION_VIEW
|
||||
data = Uri.parse(url)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.intents
|
||||
|
||||
import android.content.*
|
||||
import android.content.ContentUris.*
|
||||
import android.net.*
|
||||
import org.isoron.uhabits.core.*
|
||||
import org.isoron.uhabits.core.models.*
|
||||
import org.isoron.uhabits.core.utils.*
|
||||
import javax.inject.*
|
||||
|
||||
@AppScope
|
||||
class IntentParser
|
||||
@Inject constructor(private val habits: HabitList) {
|
||||
|
||||
fun parseCheckmarkIntent(intent: Intent): CheckmarkIntentData {
|
||||
val uri = intent.data ?: throw IllegalArgumentException("uri is null")
|
||||
return CheckmarkIntentData(parseHabit(uri), parseTimestamp(intent))
|
||||
}
|
||||
|
||||
private fun parseHabit(uri: Uri): Habit {
|
||||
val habit = habits.getById(parseId(uri)) ?:
|
||||
throw IllegalArgumentException("habit not found")
|
||||
return habit
|
||||
}
|
||||
|
||||
private fun parseTimestamp(intent: Intent): Timestamp {
|
||||
val today = DateUtils.getToday().unixTime
|
||||
var timestamp = intent.getLongExtra("timestamp", today)
|
||||
timestamp = DateUtils.getStartOfDay(timestamp)
|
||||
|
||||
if (timestamp < 0 || timestamp > today)
|
||||
throw IllegalArgumentException("timestamp is not valid")
|
||||
|
||||
return Timestamp(timestamp)
|
||||
}
|
||||
|
||||
class CheckmarkIntentData(var habit: Habit, var timestamp: Timestamp)
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.intents
|
||||
|
||||
import android.app.*
|
||||
import android.app.AlarmManager.*
|
||||
import android.content.*
|
||||
import android.content.Context.*
|
||||
import android.os.Build.VERSION.*
|
||||
import android.os.Build.VERSION_CODES.*
|
||||
import org.isoron.androidbase.*
|
||||
import org.isoron.uhabits.*
|
||||
import org.isoron.uhabits.core.*
|
||||
import org.isoron.uhabits.core.models.*
|
||||
import org.isoron.uhabits.core.reminders.*
|
||||
import javax.inject.*
|
||||
|
||||
@AppScope
|
||||
class IntentScheduler
|
||||
@Inject constructor(
|
||||
@AppContext context: Context,
|
||||
private val pendingIntents: PendingIntentFactory,
|
||||
private val logger: HabitLogger
|
||||
) : ReminderScheduler.SystemScheduler {
|
||||
|
||||
private val manager =
|
||||
context.getSystemService(ALARM_SERVICE) as AlarmManager
|
||||
|
||||
fun schedule(timestamp: Long, intent: PendingIntent) {
|
||||
if (SDK_INT >= M)
|
||||
manager.setExactAndAllowWhileIdle(RTC_WAKEUP, timestamp, intent)
|
||||
else
|
||||
manager.setExact(RTC_WAKEUP, timestamp, intent)
|
||||
}
|
||||
|
||||
override fun scheduleShowReminder(reminderTime: Long,
|
||||
habit: Habit,
|
||||
timestamp: Long) {
|
||||
val intent = pendingIntents.showReminder(habit, reminderTime, timestamp)
|
||||
schedule(reminderTime, intent)
|
||||
logger.logReminderScheduled(habit, reminderTime)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.intents
|
||||
|
||||
import android.app.*
|
||||
import android.app.PendingIntent.*
|
||||
import android.content.*
|
||||
import android.net.*
|
||||
import org.isoron.androidbase.*
|
||||
import org.isoron.uhabits.core.*
|
||||
import org.isoron.uhabits.core.models.*
|
||||
import org.isoron.uhabits.receivers.*
|
||||
import javax.inject.*
|
||||
|
||||
@AppScope
|
||||
class PendingIntentFactory
|
||||
@Inject constructor(
|
||||
@AppContext private val context: Context,
|
||||
private val intentFactory: IntentFactory) {
|
||||
|
||||
fun addCheckmark(habit: Habit, timestamp: Timestamp?): PendingIntent =
|
||||
PendingIntent.getBroadcast(
|
||||
context, 1,
|
||||
Intent(context, WidgetReceiver::class.java).apply {
|
||||
data = Uri.parse(habit.uriString)
|
||||
action = WidgetReceiver.ACTION_ADD_REPETITION
|
||||
if (timestamp != null) putExtra("timestamp", timestamp.unixTime)
|
||||
},
|
||||
FLAG_UPDATE_CURRENT)
|
||||
|
||||
fun dismissNotification(habit: Habit): PendingIntent =
|
||||
PendingIntent.getBroadcast(
|
||||
context, 0,
|
||||
Intent(context, ReminderReceiver::class.java).apply {
|
||||
action = WidgetReceiver.ACTION_DISMISS_REMINDER
|
||||
data = Uri.parse(habit.uriString)
|
||||
},
|
||||
FLAG_UPDATE_CURRENT)
|
||||
|
||||
fun removeRepetition(habit: Habit): PendingIntent =
|
||||
PendingIntent.getBroadcast(
|
||||
context, 3,
|
||||
Intent(context, WidgetReceiver::class.java).apply {
|
||||
action = WidgetReceiver.ACTION_REMOVE_REPETITION
|
||||
data = Uri.parse(habit.uriString)
|
||||
},
|
||||
FLAG_UPDATE_CURRENT)
|
||||
|
||||
fun showHabit(habit: Habit): PendingIntent =
|
||||
android.support.v4.app.TaskStackBuilder
|
||||
.create(context)
|
||||
.addNextIntentWithParentStack(
|
||||
intentFactory.startShowHabitActivity(
|
||||
context, habit))
|
||||
.getPendingIntent(0, FLAG_UPDATE_CURRENT)!!
|
||||
|
||||
fun showReminder(habit: Habit,
|
||||
reminderTime: Long?,
|
||||
timestamp: Long): PendingIntent =
|
||||
PendingIntent.getBroadcast(
|
||||
context,
|
||||
(habit.getId()!! % Integer.MAX_VALUE).toInt() + 1,
|
||||
Intent(context, ReminderReceiver::class.java).apply {
|
||||
action = ReminderReceiver.ACTION_SHOW_REMINDER
|
||||
data = Uri.parse(habit.uriString)
|
||||
putExtra("timestamp", timestamp)
|
||||
putExtra("reminderTime", reminderTime)
|
||||
},
|
||||
FLAG_UPDATE_CURRENT)
|
||||
|
||||
fun snoozeNotification(habit: Habit): PendingIntent =
|
||||
PendingIntent.getBroadcast(
|
||||
context, 0,
|
||||
Intent(context, ReminderReceiver::class.java).apply {
|
||||
data = Uri.parse(habit.uriString)
|
||||
action = ReminderReceiver.ACTION_SNOOZE_REMINDER
|
||||
},
|
||||
FLAG_UPDATE_CURRENT)
|
||||
|
||||
fun toggleCheckmark(habit: Habit, timestamp: Long?): PendingIntent =
|
||||
PendingIntent.getBroadcast(
|
||||
context, 2,
|
||||
Intent(context, WidgetReceiver::class.java).apply {
|
||||
data = Uri.parse(habit.uriString)
|
||||
action = WidgetReceiver.ACTION_TOGGLE_REPETITION
|
||||
if (timestamp != null) putExtra("timestamp", timestamp)
|
||||
},
|
||||
FLAG_UPDATE_CURRENT)
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.notifications
|
||||
|
||||
import android.app.*
|
||||
import android.content.*
|
||||
import android.graphics.*
|
||||
import android.graphics.BitmapFactory.*
|
||||
import android.os.*
|
||||
import android.os.Build.VERSION.*
|
||||
import android.support.annotation.*
|
||||
import android.support.v4.app.*
|
||||
import android.support.v4.app.NotificationCompat.*
|
||||
import org.isoron.androidbase.*
|
||||
import org.isoron.uhabits.R
|
||||
import org.isoron.uhabits.core.*
|
||||
import org.isoron.uhabits.core.models.*
|
||||
import org.isoron.uhabits.core.preferences.*
|
||||
import org.isoron.uhabits.core.ui.*
|
||||
import org.isoron.uhabits.intents.*
|
||||
import javax.inject.*
|
||||
|
||||
|
||||
|
||||
|
||||
@AppScope
|
||||
class AndroidNotificationTray
|
||||
@Inject constructor(
|
||||
@AppContext private val context: Context,
|
||||
private val pendingIntents: PendingIntentFactory,
|
||||
private val preferences: Preferences,
|
||||
private val ringtoneManager: RingtoneManager
|
||||
) : NotificationTray.SystemTray {
|
||||
|
||||
private var active = HashSet<Int>()
|
||||
|
||||
|
||||
override fun removeNotification(id: Int) {
|
||||
val manager = NotificationManagerCompat.from(context)
|
||||
manager.cancel(id)
|
||||
active.remove(id)
|
||||
|
||||
// Clear the group summary notification
|
||||
if(active.isEmpty()) manager.cancelAll()
|
||||
}
|
||||
|
||||
override fun showNotification(habit: Habit,
|
||||
notificationId: Int,
|
||||
timestamp: Timestamp,
|
||||
reminderTime: Long)
|
||||
{
|
||||
val notificationManager = NotificationManagerCompat.from(context)
|
||||
val summary = buildSummary(reminderTime)
|
||||
notificationManager.notify(Int.MAX_VALUE, summary)
|
||||
val notification = buildNotification(habit, reminderTime, timestamp)
|
||||
createAndroidNotificationChannel(context)
|
||||
notificationManager.notify(notificationId, notification)
|
||||
active.add(notificationId)
|
||||
}
|
||||
|
||||
@NonNull
|
||||
fun buildNotification(@NonNull habit: Habit,
|
||||
@NonNull reminderTime: Long,
|
||||
@NonNull timestamp: Timestamp) : Notification
|
||||
{
|
||||
|
||||
val addRepetitionAction = Action(
|
||||
R.drawable.ic_action_check,
|
||||
context.getString(R.string.yes),
|
||||
pendingIntents.addCheckmark(habit, timestamp))
|
||||
|
||||
val removeRepetitionAction = Action(
|
||||
R.drawable.ic_action_cancel,
|
||||
context.getString(R.string.no),
|
||||
pendingIntents.removeRepetition(habit))
|
||||
|
||||
val wearableBg = decodeResource(context.resources, R.drawable.stripe)
|
||||
|
||||
// Even though the set of actions is the same on the phone and
|
||||
// on the watch, Pebble requires us to add them to the
|
||||
// WearableExtender.
|
||||
val wearableExtender = WearableExtender()
|
||||
.setBackground(wearableBg)
|
||||
.addAction(addRepetitionAction)
|
||||
.addAction(removeRepetitionAction)
|
||||
|
||||
val builder = NotificationCompat.Builder(context, REMINDERS_CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.ic_notification)
|
||||
.setContentTitle(habit.name)
|
||||
.setContentText(habit.description)
|
||||
.setContentIntent(pendingIntents.showHabit(habit))
|
||||
.setDeleteIntent(pendingIntents.dismissNotification(habit))
|
||||
.addAction(addRepetitionAction)
|
||||
.addAction(removeRepetitionAction)
|
||||
.setSound(ringtoneManager.getURI())
|
||||
.setWhen(reminderTime)
|
||||
.setShowWhen(true)
|
||||
.setOngoing(preferences.shouldMakeNotificationsSticky())
|
||||
.setGroup("default")
|
||||
|
||||
if (preferences.shouldMakeNotificationsLed())
|
||||
builder.setLights(Color.RED, 1000, 1000)
|
||||
|
||||
if(SDK_INT < Build.VERSION_CODES.O) {
|
||||
val snoozeAction = Action(R.drawable.ic_action_snooze,
|
||||
context.getString(R.string.snooze),
|
||||
pendingIntents.snoozeNotification(habit))
|
||||
wearableExtender.addAction(snoozeAction)
|
||||
builder.addAction(snoozeAction)
|
||||
}
|
||||
|
||||
builder.extend(wearableExtender)
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private fun buildSummary(@NonNull reminderTime: Long) : Notification
|
||||
{
|
||||
return NotificationCompat.Builder(context, REMINDERS_CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.ic_notification)
|
||||
.setContentTitle(context.getString(R.string.app_name))
|
||||
.setWhen(reminderTime)
|
||||
.setShowWhen(true)
|
||||
.setGroup("default")
|
||||
.setGroupSummary(true)
|
||||
.build()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val REMINDERS_CHANNEL_ID = "REMINDERS"
|
||||
fun createAndroidNotificationChannel(context: Context) {
|
||||
val notificationManager = context.getSystemService(Activity.NOTIFICATION_SERVICE)
|
||||
as NotificationManager
|
||||
if (SDK_INT >= Build.VERSION_CODES.O)
|
||||
{
|
||||
val channel = NotificationChannel(REMINDERS_CHANNEL_ID,
|
||||
context.resources.getString(R.string.reminder),
|
||||
NotificationManager.IMPORTANCE_DEFAULT)
|
||||
notificationManager.createNotificationChannel(channel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.notifications
|
||||
|
||||
import android.content.*
|
||||
import android.media.RingtoneManager.*
|
||||
import android.net.*
|
||||
import android.preference.*
|
||||
import android.provider.*
|
||||
import org.isoron.androidbase.*
|
||||
import org.isoron.uhabits.R
|
||||
import org.isoron.uhabits.core.*
|
||||
import javax.inject.*
|
||||
|
||||
@AppScope
|
||||
class RingtoneManager
|
||||
@Inject constructor(@AppContext private val context: Context) {
|
||||
|
||||
val prefs: SharedPreferences =
|
||||
PreferenceManager.getDefaultSharedPreferences(context)
|
||||
|
||||
fun getName(): String? {
|
||||
try {
|
||||
var ringtoneName = context.resources.getString(R.string.none)
|
||||
val ringtoneUri = getURI()
|
||||
if (ringtoneUri != null) {
|
||||
val ringtone = getRingtone(context, ringtoneUri)
|
||||
if (ringtone != null) ringtoneName = ringtone.getTitle(context)
|
||||
}
|
||||
return ringtoneName
|
||||
} catch (e: RuntimeException) {
|
||||
e.printStackTrace()
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
fun getURI(): Uri? {
|
||||
var ringtoneUri: Uri? = null
|
||||
val defaultRingtoneUri = Settings.System.DEFAULT_NOTIFICATION_URI
|
||||
val prefRingtoneUri = prefs.getString("pref_ringtone_uri",
|
||||
defaultRingtoneUri.toString())
|
||||
if (prefRingtoneUri.isNotEmpty())
|
||||
ringtoneUri = Uri.parse(prefRingtoneUri)
|
||||
|
||||
return ringtoneUri
|
||||
}
|
||||
|
||||
fun update(data: Intent?) {
|
||||
if (data == null) return
|
||||
val ringtoneUri = data.getParcelableExtra<Uri>(EXTRA_RINGTONE_PICKED_URI)
|
||||
if (ringtoneUri != null) {
|
||||
prefs.edit().putString("pref_ringtone_uri", ringtoneUri.toString()).apply()
|
||||
} else {
|
||||
prefs.edit().putString("pref_ringtone_uri", "").apply()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package org.isoron.uhabits.notifications;
|
||||
|
||||
|
||||
import android.app.*;
|
||||
import android.os.*;
|
||||
import android.support.annotation.*;
|
||||
import android.support.v4.app.*;
|
||||
import android.text.format.*;
|
||||
import android.view.*;
|
||||
import android.widget.*;
|
||||
|
||||
import com.android.datetimepicker.time.TimePickerDialog;
|
||||
|
||||
import org.isoron.uhabits.*;
|
||||
import org.isoron.uhabits.core.models.*;
|
||||
import org.isoron.uhabits.receivers.*;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import static android.content.ContentUris.parseId;
|
||||
|
||||
public class SnoozeDelayPickerActivity extends FragmentActivity
|
||||
implements AdapterView.OnItemClickListener
|
||||
{
|
||||
private Habit habit;
|
||||
|
||||
private ReminderController reminderController;
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle bundle)
|
||||
{
|
||||
super.onCreate(bundle);
|
||||
if (getIntent() == null) finish();
|
||||
if (getIntent().getData() == null) finish();
|
||||
|
||||
HabitsApplication app = (HabitsApplication) getApplicationContext();
|
||||
HabitsApplicationComponent appComponent = app.getComponent();
|
||||
reminderController = appComponent.getReminderController();
|
||||
habit = appComponent.getHabitList().getById(parseId(getIntent().getData()));
|
||||
if (habit == null) finish();
|
||||
|
||||
int theme = R.style.Theme_AppCompat_Light_Dialog_Alert;
|
||||
AlertDialog dialog = new AlertDialog.Builder(new ContextThemeWrapper(this, theme))
|
||||
.setTitle(R.string.select_snooze_delay)
|
||||
.setItems(R.array.snooze_picker_names, null)
|
||||
.create();
|
||||
|
||||
dialog.getListView().setOnItemClickListener(this);
|
||||
dialog.setOnDismissListener(d -> finish());
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
private void showTimePicker()
|
||||
{
|
||||
final Calendar calendar = Calendar.getInstance();
|
||||
TimePickerDialog dialog = TimePickerDialog.newInstance(
|
||||
(view, hour, minute) -> {
|
||||
reminderController.onSnoozeTimePicked(habit, hour, minute);
|
||||
finish();
|
||||
},
|
||||
calendar.get(Calendar.HOUR_OF_DAY),
|
||||
calendar.get(Calendar.MINUTE),
|
||||
DateFormat.is24HourFormat(this));
|
||||
dialog.show(getSupportFragmentManager(), "timePicker");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> parent, View view, int position, long id)
|
||||
{
|
||||
int[] snoozeValues = getResources().getIntArray(R.array.snooze_picker_values);
|
||||
if (snoozeValues[position] >= 0)
|
||||
{
|
||||
reminderController.onSnoozeDelayPicked(habit, snoozeValues[position]);
|
||||
finish();
|
||||
}
|
||||
else showTimePicker();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void finish()
|
||||
{
|
||||
super.finish();
|
||||
overridePendingTransition(0, 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.preferences
|
||||
|
||||
import android.content.*
|
||||
import android.preference.*
|
||||
import org.isoron.androidbase.*
|
||||
import org.isoron.uhabits.R
|
||||
import org.isoron.uhabits.core.*
|
||||
import org.isoron.uhabits.core.preferences.*
|
||||
import javax.inject.*
|
||||
|
||||
@AppScope
|
||||
class SharedPreferencesStorage
|
||||
@Inject constructor(
|
||||
@AppContext context: Context
|
||||
) : SharedPreferences.OnSharedPreferenceChangeListener, Preferences.Storage {
|
||||
|
||||
private val sharedPrefs: SharedPreferences =
|
||||
PreferenceManager.getDefaultSharedPreferences(context)
|
||||
|
||||
private var preferences: Preferences? = null
|
||||
|
||||
init {
|
||||
sharedPrefs.registerOnSharedPreferenceChangeListener(this)
|
||||
PreferenceManager.setDefaultValues(context, R.xml.preferences, false)
|
||||
}
|
||||
|
||||
override fun clear() = sharedPrefs.edit().clear().apply()
|
||||
|
||||
override fun getBoolean(key: String, defValue: Boolean) =
|
||||
sharedPrefs.getBoolean(key, defValue)
|
||||
|
||||
override fun getInt(key: String, defValue: Int) =
|
||||
sharedPrefs.getInt(key, defValue)
|
||||
|
||||
override fun getLong(key: String, defValue: Long) =
|
||||
sharedPrefs.getLong(key, defValue)
|
||||
|
||||
override fun getString(key: String, defValue: String): String =
|
||||
sharedPrefs.getString(key, defValue)
|
||||
|
||||
override fun onAttached(preferences: Preferences) {
|
||||
this.preferences = preferences
|
||||
}
|
||||
|
||||
override fun putBoolean(key: String, value: Boolean) =
|
||||
sharedPrefs.edit().putBoolean(key, value).apply()
|
||||
|
||||
override fun putInt(key: String, value: Int) =
|
||||
sharedPrefs.edit().putInt(key, value).apply()
|
||||
|
||||
override fun putLong(key: String, value: Long) =
|
||||
sharedPrefs.edit().putLong(key, value).apply()
|
||||
|
||||
override fun putString(key: String, value: String) =
|
||||
sharedPrefs.edit().putString(key, value).apply()
|
||||
|
||||
override fun remove(key: String) =
|
||||
sharedPrefs.edit().remove(key).apply()
|
||||
|
||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences,
|
||||
key: String) {
|
||||
val preferences = this.preferences ?: return
|
||||
sharedPreferences.unregisterOnSharedPreferenceChangeListener(this)
|
||||
when (key) {
|
||||
"pref_checkmark_reverse_order" ->
|
||||
preferences.isCheckmarkSequenceReversed = getBoolean(key, false)
|
||||
"pref_sticky_notifications" ->
|
||||
preferences.setNotificationsSticky(getBoolean(key, false))
|
||||
"pref_led_notifications" ->
|
||||
preferences.setNotificationsLed(getBoolean(key, false))
|
||||
"pref_feature_sync" ->
|
||||
preferences.isSyncEnabled = getBoolean(key, false)
|
||||
}
|
||||
sharedPreferences.registerOnSharedPreferenceChangeListener(this)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.receivers
|
||||
|
||||
import android.content.*
|
||||
import android.content.Context.*
|
||||
import android.net.*
|
||||
import org.isoron.uhabits.*
|
||||
|
||||
class ConnectivityReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
if (context == null || intent == null) return
|
||||
val app = context.applicationContext as HabitsApplication
|
||||
val networkInfo = (context.getSystemService(CONNECTIVITY_SERVICE)
|
||||
as ConnectivityManager).activeNetworkInfo
|
||||
val isConnected = (networkInfo != null) &&
|
||||
networkInfo.isConnectedOrConnecting
|
||||
val syncManager = app.component.syncManager
|
||||
syncManager.onNetworkStatusChanged(isConnected)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.receivers;
|
||||
|
||||
import javax.inject.*;
|
||||
|
||||
@Scope
|
||||
public @interface ReceiverScope { }
|
||||
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.receivers;
|
||||
|
||||
import android.content.*;
|
||||
import android.net.*;
|
||||
import android.support.annotation.*;
|
||||
|
||||
import org.isoron.uhabits.core.*;
|
||||
import org.isoron.uhabits.core.models.*;
|
||||
import org.isoron.uhabits.core.preferences.*;
|
||||
import org.isoron.uhabits.core.reminders.*;
|
||||
import org.isoron.uhabits.core.ui.*;
|
||||
import org.isoron.uhabits.core.utils.*;
|
||||
import org.isoron.uhabits.notifications.*;
|
||||
|
||||
import javax.inject.*;
|
||||
|
||||
@AppScope
|
||||
public class ReminderController
|
||||
{
|
||||
@NonNull
|
||||
private final ReminderScheduler reminderScheduler;
|
||||
|
||||
@NonNull
|
||||
private final NotificationTray notificationTray;
|
||||
|
||||
@NonNull
|
||||
private Preferences preferences;
|
||||
|
||||
@Inject
|
||||
public ReminderController(@NonNull ReminderScheduler reminderScheduler,
|
||||
@NonNull NotificationTray notificationTray,
|
||||
@NonNull Preferences preferences)
|
||||
{
|
||||
this.reminderScheduler = reminderScheduler;
|
||||
this.notificationTray = notificationTray;
|
||||
this.preferences = preferences;
|
||||
}
|
||||
|
||||
public void onBootCompleted()
|
||||
{
|
||||
reminderScheduler.scheduleAll();
|
||||
}
|
||||
|
||||
public void onShowReminder(@NonNull Habit habit,
|
||||
Timestamp timestamp,
|
||||
long reminderTime)
|
||||
{
|
||||
notificationTray.show(habit, timestamp, reminderTime);
|
||||
reminderScheduler.scheduleAll();
|
||||
}
|
||||
|
||||
public void onSnoozePressed(@NonNull Habit habit, final Context context)
|
||||
{
|
||||
long delay = preferences.getSnoozeInterval();
|
||||
|
||||
if (delay < 0)
|
||||
showSnoozeDelayPicker(habit, context);
|
||||
else
|
||||
scheduleReminderMinutesFromNow(habit, delay);
|
||||
}
|
||||
|
||||
public void onSnoozeDelayPicked(Habit habit, int delay)
|
||||
{
|
||||
scheduleReminderMinutesFromNow(habit, delay);
|
||||
}
|
||||
|
||||
public void onSnoozeTimePicked(Habit habit, int hour, int minute)
|
||||
{
|
||||
Long time = DateUtils.getUpcomingTimeInMillis(hour, minute);
|
||||
reminderScheduler.scheduleAtTime(habit, time);
|
||||
notificationTray.cancel(habit);
|
||||
}
|
||||
|
||||
public void onDismiss(@NonNull Habit habit)
|
||||
{
|
||||
notificationTray.cancel(habit);
|
||||
}
|
||||
|
||||
private void scheduleReminderMinutesFromNow(Habit habit, long minutes)
|
||||
{
|
||||
reminderScheduler.scheduleMinutesFromNow(habit, minutes);
|
||||
notificationTray.cancel(habit);
|
||||
}
|
||||
|
||||
private void showSnoozeDelayPicker(@NonNull Habit habit, Context context)
|
||||
{
|
||||
context.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
|
||||
Intent intent = new Intent(context, SnoozeDelayPickerActivity.class);
|
||||
intent.setData(Uri.parse(habit.getUriString()));
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
context.startActivity(intent);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.receivers;
|
||||
|
||||
import android.content.*;
|
||||
import android.support.annotation.*;
|
||||
import android.util.*;
|
||||
|
||||
import org.isoron.uhabits.*;
|
||||
import org.isoron.uhabits.core.models.*;
|
||||
import org.isoron.uhabits.core.utils.*;
|
||||
|
||||
import static android.content.ContentUris.*;
|
||||
|
||||
/**
|
||||
* The Android BroadcastReceiver for Loop Habit Tracker.
|
||||
* <p>
|
||||
* All broadcast messages are received and processed by this class.
|
||||
*/
|
||||
public class ReminderReceiver extends BroadcastReceiver
|
||||
{
|
||||
public static final String ACTION_DISMISS_REMINDER =
|
||||
"org.isoron.uhabits.ACTION_DISMISS_REMINDER";
|
||||
|
||||
public static final String ACTION_SHOW_REMINDER =
|
||||
"org.isoron.uhabits.ACTION_SHOW_REMINDER";
|
||||
|
||||
public static final String ACTION_SNOOZE_REMINDER =
|
||||
"org.isoron.uhabits.ACTION_SNOOZE_REMINDER";
|
||||
|
||||
private static final String TAG = "ReminderReceiver";
|
||||
|
||||
@Override
|
||||
public void onReceive(@Nullable final Context context, @Nullable Intent intent)
|
||||
{
|
||||
if (context == null || intent == null) return;
|
||||
if (intent.getAction() == null) return;
|
||||
|
||||
HabitsApplication app = (HabitsApplication) context.getApplicationContext();
|
||||
HabitsApplicationComponent appComponent = app.getComponent();
|
||||
HabitList habits = appComponent.getHabitList();
|
||||
ReminderController reminderController = appComponent.getReminderController();
|
||||
|
||||
Log.i(TAG, String.format("Received intent: %s", intent.toString()));
|
||||
|
||||
Habit habit = null;
|
||||
long today = DateUtils.getStartOfToday();
|
||||
|
||||
if (intent.getData() != null)
|
||||
habit = habits.getById(parseId(intent.getData()));
|
||||
final Long timestamp = intent.getLongExtra("timestamp", today);
|
||||
final Long reminderTime = intent.getLongExtra("reminderTime", today);
|
||||
|
||||
try
|
||||
{
|
||||
switch (intent.getAction())
|
||||
{
|
||||
case ACTION_SHOW_REMINDER:
|
||||
if (habit == null) return;
|
||||
reminderController.onShowReminder(habit,
|
||||
new Timestamp(timestamp), reminderTime);
|
||||
break;
|
||||
|
||||
case ACTION_DISMISS_REMINDER:
|
||||
if (habit == null) return;
|
||||
reminderController.onDismiss(habit);
|
||||
break;
|
||||
|
||||
case ACTION_SNOOZE_REMINDER:
|
||||
if (habit == null) return;
|
||||
reminderController.onSnoozePressed(habit, context);
|
||||
break;
|
||||
|
||||
case Intent.ACTION_BOOT_COMPLETED:
|
||||
reminderController.onBootCompleted();
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (RuntimeException e)
|
||||
{
|
||||
Log.e(TAG, "could not process intent", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits.receivers;
|
||||
|
||||
import android.content.*;
|
||||
import android.util.*;
|
||||
|
||||
import org.isoron.uhabits.*;
|
||||
import org.isoron.uhabits.core.preferences.*;
|
||||
import org.isoron.uhabits.core.ui.widgets.*;
|
||||
import org.isoron.uhabits.intents.*;
|
||||
import org.isoron.uhabits.sync.*;
|
||||
|
||||
import dagger.*;
|
||||
|
||||
/**
|
||||
* The Android BroadcastReceiver for Loop Habit Tracker.
|
||||
* <p>
|
||||
* All broadcast messages are received and processed by this class.
|
||||
*/
|
||||
public class WidgetReceiver extends BroadcastReceiver
|
||||
{
|
||||
public static final String ACTION_ADD_REPETITION =
|
||||
"org.isoron.uhabits.ACTION_ADD_REPETITION";
|
||||
|
||||
public static final String ACTION_DISMISS_REMINDER =
|
||||
"org.isoron.uhabits.ACTION_DISMISS_REMINDER";
|
||||
|
||||
public static final String ACTION_REMOVE_REPETITION =
|
||||
"org.isoron.uhabits.ACTION_REMOVE_REPETITION";
|
||||
|
||||
public static final String ACTION_TOGGLE_REPETITION =
|
||||
"org.isoron.uhabits.ACTION_TOGGLE_REPETITION";
|
||||
|
||||
@Override
|
||||
public void onReceive(final Context context, Intent intent)
|
||||
{
|
||||
HabitsApplication app =
|
||||
(HabitsApplication) context.getApplicationContext();
|
||||
|
||||
WidgetComponent component = DaggerWidgetReceiver_WidgetComponent
|
||||
.builder()
|
||||
.habitsApplicationComponent(app.getComponent())
|
||||
.build();
|
||||
|
||||
IntentParser parser = app.getComponent().getIntentParser();
|
||||
WidgetBehavior controller = component.getWidgetController();
|
||||
Preferences prefs = app.getComponent().getPreferences();
|
||||
|
||||
if(prefs.isSyncEnabled())
|
||||
context.startService(new Intent(context, SyncService.class));
|
||||
|
||||
try
|
||||
{
|
||||
IntentParser.CheckmarkIntentData data;
|
||||
data = parser.parseCheckmarkIntent(intent);
|
||||
|
||||
switch (intent.getAction())
|
||||
{
|
||||
case ACTION_ADD_REPETITION:
|
||||
controller.onAddRepetition(data.getHabit(),
|
||||
data.getTimestamp());
|
||||
break;
|
||||
|
||||
case ACTION_TOGGLE_REPETITION:
|
||||
controller.onToggleRepetition(data.getHabit(),
|
||||
data.getTimestamp());
|
||||
break;
|
||||
|
||||
case ACTION_REMOVE_REPETITION:
|
||||
controller.onRemoveRepetition(data.getHabit(),
|
||||
data.getTimestamp());
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (RuntimeException e)
|
||||
{
|
||||
Log.e("WidgetReceiver", "could not process intent", e);
|
||||
}
|
||||
}
|
||||
|
||||
@ReceiverScope
|
||||
@Component(dependencies = HabitsApplicationComponent.class)
|
||||
interface WidgetComponent
|
||||
{
|
||||
WidgetBehavior getWidgetController();
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user