mirror of
https://github.com/iSoron/uhabits.git
synced 2025-12-06 09:08:52 -06:00
Merge pull request #716 from hiqua/dev
This commit is contained in:
@@ -85,6 +85,7 @@ dependencies {
|
|||||||
androidTestImplementation("androidx.test.ext:junit:1.1.2")
|
androidTestImplementation("androidx.test.ext:junit:1.1.2")
|
||||||
androidTestImplementation("androidx.test.uiautomator:uiautomator:2.2.0")
|
androidTestImplementation("androidx.test.uiautomator:uiautomator:2.2.0")
|
||||||
androidTestImplementation("androidx.test:rules:1.3.0")
|
androidTestImplementation("androidx.test:rules:1.3.0")
|
||||||
|
androidTestImplementation("com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0")
|
||||||
compileOnly("javax.annotation:jsr250-api:1.0")
|
compileOnly("javax.annotation:jsr250-api:1.0")
|
||||||
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.1")
|
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.1")
|
||||||
implementation("com.github.paolorotolo:appintro:3.4.0")
|
implementation("com.github.paolorotolo:appintro:3.4.0")
|
||||||
|
|||||||
@@ -1,289 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
|
||||||
*
|
|
||||||
* This file is part of Loop Habit Tracker.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by the
|
|
||||||
* Free Software Foundation, either version 3 of the License, or (at your
|
|
||||||
* option) any later version.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
|
||||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
|
||||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
||||||
* more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License along
|
|
||||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.isoron.uhabits;
|
|
||||||
|
|
||||||
import android.appwidget.*;
|
|
||||||
import android.content.*;
|
|
||||||
import android.content.res.*;
|
|
||||||
import android.os.*;
|
|
||||||
import android.util.*;
|
|
||||||
|
|
||||||
import androidx.annotation.*;
|
|
||||||
import androidx.test.filters.*;
|
|
||||||
import androidx.test.platform.app.*;
|
|
||||||
import androidx.test.uiautomator.*;
|
|
||||||
|
|
||||||
import junit.framework.*;
|
|
||||||
|
|
||||||
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.inject.*;
|
|
||||||
import org.isoron.uhabits.utils.*;
|
|
||||||
import org.junit.*;
|
|
||||||
|
|
||||||
import java.io.*;
|
|
||||||
import java.time.*;
|
|
||||||
import java.util.*;
|
|
||||||
import java.util.concurrent.*;
|
|
||||||
|
|
||||||
import static androidx.test.platform.app.InstrumentationRegistry.*;
|
|
||||||
import static androidx.test.uiautomator.UiDevice.*;
|
|
||||||
import static org.hamcrest.CoreMatchers.*;
|
|
||||||
import static org.hamcrest.MatcherAssert.*;
|
|
||||||
|
|
||||||
@MediumTest
|
|
||||||
public class BaseAndroidTest extends TestCase
|
|
||||||
{
|
|
||||||
// 8:00am, January 25th, 2015 (UTC)
|
|
||||||
public static final long FIXED_LOCAL_TIME = 1422172800000L;
|
|
||||||
|
|
||||||
protected Context testContext;
|
|
||||||
|
|
||||||
protected Context targetContext;
|
|
||||||
|
|
||||||
protected Preferences prefs;
|
|
||||||
|
|
||||||
protected HabitList habitList;
|
|
||||||
|
|
||||||
protected TaskRunner taskRunner;
|
|
||||||
|
|
||||||
protected HabitFixtures fixtures;
|
|
||||||
|
|
||||||
protected CountDownLatch latch;
|
|
||||||
|
|
||||||
protected HabitsApplicationTestComponent appComponent;
|
|
||||||
|
|
||||||
protected ModelFactory modelFactory;
|
|
||||||
|
|
||||||
protected HabitsActivityTestComponent component;
|
|
||||||
|
|
||||||
private boolean isDone = false;
|
|
||||||
|
|
||||||
private UiDevice device;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Before
|
|
||||||
public void setUp()
|
|
||||||
{
|
|
||||||
if (Looper.myLooper() == null) Looper.prepare();
|
|
||||||
device = getInstance(getInstrumentation());
|
|
||||||
|
|
||||||
targetContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
|
|
||||||
testContext = InstrumentationRegistry.getInstrumentation().getContext();
|
|
||||||
|
|
||||||
DateUtils.setFixedLocalTime(FIXED_LOCAL_TIME);
|
|
||||||
DateUtils.setStartDayOffset(0, 0);
|
|
||||||
setResolution(2.0f);
|
|
||||||
setTheme(R.style.AppBaseTheme);
|
|
||||||
setLocale("en", "US");
|
|
||||||
|
|
||||||
latch = new CountDownLatch(1);
|
|
||||||
|
|
||||||
Context context = targetContext.getApplicationContext();
|
|
||||||
File dbFile = DatabaseUtils.getDatabaseFile(context);
|
|
||||||
appComponent = DaggerHabitsApplicationTestComponent
|
|
||||||
.builder()
|
|
||||||
.appContextModule(new AppContextModule(context))
|
|
||||||
.habitsModule(new HabitsModule(dbFile))
|
|
||||||
.build();
|
|
||||||
|
|
||||||
HabitsApplication.Companion.setComponent(appComponent);
|
|
||||||
prefs = appComponent.getPreferences();
|
|
||||||
habitList = appComponent.getHabitList();
|
|
||||||
taskRunner = appComponent.getTaskRunner();
|
|
||||||
modelFactory = appComponent.getModelFactory();
|
|
||||||
|
|
||||||
prefs.clear();
|
|
||||||
|
|
||||||
fixtures = new HabitFixtures(modelFactory, habitList);
|
|
||||||
fixtures.purgeHabits(appComponent.getHabitList());
|
|
||||||
Habit habit = fixtures.createEmptyHabit();
|
|
||||||
|
|
||||||
component = DaggerHabitsActivityTestComponent
|
|
||||||
.builder()
|
|
||||||
.activityContextModule(new ActivityContextModule(targetContext))
|
|
||||||
.habitsApplicationComponent(appComponent)
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void assertWidgetProviderIsInstalled(Class componentClass)
|
|
||||||
{
|
|
||||||
ComponentName provider =
|
|
||||||
new ComponentName(targetContext, componentClass);
|
|
||||||
AppWidgetManager manager = AppWidgetManager.getInstance(targetContext);
|
|
||||||
|
|
||||||
List<ComponentName> installedProviders = new LinkedList<>();
|
|
||||||
for (AppWidgetProviderInfo info : manager.getInstalledProviders())
|
|
||||||
installedProviders.add(info.provider);
|
|
||||||
|
|
||||||
assertThat(installedProviders, hasItems(provider));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void awaitLatch() throws InterruptedException
|
|
||||||
{
|
|
||||||
assertTrue(latch.await(1, TimeUnit.SECONDS));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void setLocale(@NonNull String language, @NonNull String country)
|
|
||||||
{
|
|
||||||
Locale locale = new Locale(language, country);
|
|
||||||
Locale.setDefault(locale);
|
|
||||||
Resources res = targetContext.getResources();
|
|
||||||
Configuration config = res.getConfiguration();
|
|
||||||
config.setLocale(locale);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void setResolution(float r)
|
|
||||||
{
|
|
||||||
DisplayMetrics dm = targetContext.getResources().getDisplayMetrics();
|
|
||||||
dm.density = r;
|
|
||||||
dm.scaledDensity = r;
|
|
||||||
InterfaceUtils.setFixedResolution(r);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void runConcurrently(Runnable... runnableList) throws Exception
|
|
||||||
{
|
|
||||||
isDone = false;
|
|
||||||
ExecutorService executor = Executors.newFixedThreadPool(100);
|
|
||||||
List<Future> futures = new LinkedList<>();
|
|
||||||
for (Runnable r : runnableList)
|
|
||||||
futures.add(executor.submit(() ->
|
|
||||||
{
|
|
||||||
while (!isDone) r.run();
|
|
||||||
return null;
|
|
||||||
}));
|
|
||||||
|
|
||||||
Thread.sleep(3000);
|
|
||||||
isDone = true;
|
|
||||||
executor.shutdown();
|
|
||||||
for(Future f : futures) f.get();
|
|
||||||
while (!executor.isTerminated()) Thread.sleep(50);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void setTheme(@StyleRes int themeId)
|
|
||||||
{
|
|
||||||
targetContext.setTheme(themeId);
|
|
||||||
StyledResources.setFixedTheme(themeId);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void sleep(int time)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Thread.sleep(time);
|
|
||||||
}
|
|
||||||
catch (InterruptedException e)
|
|
||||||
{
|
|
||||||
fail();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public long timestamp(int year, int month, int day)
|
|
||||||
{
|
|
||||||
GregorianCalendar cal = DateUtils.getStartOfTodayCalendar();
|
|
||||||
cal.set(year, month, day);
|
|
||||||
return cal.getTimeInMillis();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void startTracing()
|
|
||||||
{
|
|
||||||
File dir = new AndroidDirFinder(targetContext).getFilesDir("Profile");
|
|
||||||
assertNotNull(dir);
|
|
||||||
String tracePath = dir.getAbsolutePath() + "/performance.trace";
|
|
||||||
Log.d("PerformanceTest", String.format("Saving trace file to %s", tracePath));
|
|
||||||
Debug.startMethodTracingSampling(tracePath, 0, 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void stopTracing()
|
|
||||||
{
|
|
||||||
Debug.stopMethodTracing();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected Timestamp day(int offset)
|
|
||||||
{
|
|
||||||
return DateUtils.getToday().minus(offset);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public void setSystemTime(String tz,
|
|
||||||
int year,
|
|
||||||
int javaMonth,
|
|
||||||
int day,
|
|
||||||
int hourOfDay,
|
|
||||||
int minute) throws Exception
|
|
||||||
{
|
|
||||||
GregorianCalendar cal = new GregorianCalendar();
|
|
||||||
cal.set(Calendar.SECOND, 0);
|
|
||||||
cal.set(year, javaMonth, day, hourOfDay, minute);
|
|
||||||
cal.setTimeZone(TimeZone.getTimeZone(tz));
|
|
||||||
setSystemTime(cal);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setSystemTime(GregorianCalendar cal) throws Exception
|
|
||||||
{
|
|
||||||
ZoneId tz = cal.getTimeZone().toZoneId();
|
|
||||||
|
|
||||||
// Set time zone (temporary)
|
|
||||||
String command = String.format("service call alarm 3 s16 %s", tz);
|
|
||||||
device.executeShellCommand(command);
|
|
||||||
|
|
||||||
// Set time zone (permanent)
|
|
||||||
command = String.format("setprop persist.sys.timezone %s", tz);
|
|
||||||
device.executeShellCommand(command);
|
|
||||||
|
|
||||||
// Set time
|
|
||||||
String date = String.format("%02d%02d%02d%02d%02d.%02d",
|
|
||||||
cal.get(Calendar.MONTH) + 1,
|
|
||||||
cal.get(Calendar.DAY_OF_MONTH),
|
|
||||||
cal.get(Calendar.HOUR_OF_DAY),
|
|
||||||
cal.get(Calendar.MINUTE),
|
|
||||||
cal.get(Calendar.YEAR),
|
|
||||||
cal.get(Calendar.SECOND));
|
|
||||||
|
|
||||||
// Set time (method 1)
|
|
||||||
// Run twice to override daylight saving time
|
|
||||||
device.executeShellCommand("date " + date);
|
|
||||||
device.executeShellCommand("date " + date);
|
|
||||||
|
|
||||||
// Set time (method 2)
|
|
||||||
// Run in addition to the method above because one of these mail fail, depending
|
|
||||||
// on the Android API version.
|
|
||||||
command = String.format("date -u @%d", cal.getTimeInMillis() / 1000);
|
|
||||||
device.executeShellCommand(command);
|
|
||||||
|
|
||||||
// Wait for system events to settle
|
|
||||||
Thread.sleep(1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
private GregorianCalendar savedCalendar = null;
|
|
||||||
|
|
||||||
public void saveSystemTime()
|
|
||||||
{
|
|
||||||
savedCalendar = new GregorianCalendar();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void restoreSystemTime() throws Exception
|
|
||||||
{
|
|
||||||
if (savedCalendar == null) throw new NullPointerException();
|
|
||||||
setSystemTime(savedCalendar);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,218 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
||||||
|
*
|
||||||
|
* This file is part of Loop Habit Tracker.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by the
|
||||||
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along
|
||||||
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.isoron.uhabits
|
||||||
|
|
||||||
|
import android.appwidget.AppWidgetManager
|
||||||
|
import android.content.ComponentName
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Looper
|
||||||
|
import androidx.annotation.StyleRes
|
||||||
|
import androidx.test.filters.MediumTest
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import androidx.test.uiautomator.UiDevice
|
||||||
|
import junit.framework.TestCase
|
||||||
|
import org.hamcrest.CoreMatchers.hasItems
|
||||||
|
import org.hamcrest.MatcherAssert.assertThat
|
||||||
|
import org.isoron.uhabits.core.models.HabitList
|
||||||
|
import org.isoron.uhabits.core.models.ModelFactory
|
||||||
|
import org.isoron.uhabits.core.models.Timestamp
|
||||||
|
import org.isoron.uhabits.core.preferences.Preferences
|
||||||
|
import org.isoron.uhabits.core.tasks.TaskRunner
|
||||||
|
import org.isoron.uhabits.core.utils.DateUtils.Companion.getToday
|
||||||
|
import org.isoron.uhabits.core.utils.DateUtils.Companion.setFixedLocalTime
|
||||||
|
import org.isoron.uhabits.core.utils.DateUtils.Companion.setStartDayOffset
|
||||||
|
import org.isoron.uhabits.inject.ActivityContextModule
|
||||||
|
import org.isoron.uhabits.inject.AppContextModule
|
||||||
|
import org.isoron.uhabits.inject.HabitsModule
|
||||||
|
import org.isoron.uhabits.utils.DatabaseUtils.getDatabaseFile
|
||||||
|
import org.isoron.uhabits.utils.InterfaceUtils.setFixedResolution
|
||||||
|
import org.isoron.uhabits.utils.StyledResources.Companion.setFixedTheme
|
||||||
|
import org.isoron.uhabits.widgets.BaseWidgetProvider
|
||||||
|
import org.junit.Before
|
||||||
|
import java.util.Calendar
|
||||||
|
import java.util.GregorianCalendar
|
||||||
|
import java.util.LinkedList
|
||||||
|
import java.util.Locale
|
||||||
|
import java.util.TimeZone
|
||||||
|
import java.util.concurrent.CountDownLatch
|
||||||
|
|
||||||
|
@MediumTest
|
||||||
|
abstract class BaseAndroidTest : TestCase() {
|
||||||
|
@JvmField
|
||||||
|
protected var testContext: Context = InstrumentationRegistry.getInstrumentation().context
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
protected var targetContext: Context =
|
||||||
|
InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
|
protected lateinit var prefs: Preferences
|
||||||
|
|
||||||
|
protected lateinit var habitList: HabitList
|
||||||
|
protected lateinit var taskRunner: TaskRunner
|
||||||
|
protected lateinit var fixtures: HabitFixtures
|
||||||
|
protected lateinit var latch: CountDownLatch
|
||||||
|
protected lateinit var appComponent: HabitsApplicationTestComponent
|
||||||
|
protected lateinit var modelFactory: ModelFactory
|
||||||
|
protected lateinit var component: HabitsActivityTestComponent
|
||||||
|
private lateinit var device: UiDevice
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public override fun setUp() {
|
||||||
|
if (Looper.myLooper() == null) Looper.prepare()
|
||||||
|
device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
|
||||||
|
setFixedLocalTime(FIXED_LOCAL_TIME)
|
||||||
|
setStartDayOffset(0, 0)
|
||||||
|
setResolution(2.0f)
|
||||||
|
setTheme(R.style.AppBaseTheme)
|
||||||
|
setLocale("en", "US")
|
||||||
|
latch = CountDownLatch(1)
|
||||||
|
val context = targetContext.applicationContext
|
||||||
|
val dbFile = getDatabaseFile(context)
|
||||||
|
appComponent = DaggerHabitsApplicationTestComponent
|
||||||
|
.builder()
|
||||||
|
.appContextModule(AppContextModule(context))
|
||||||
|
.habitsModule(HabitsModule(dbFile))
|
||||||
|
.build()
|
||||||
|
HabitsApplication.component = appComponent
|
||||||
|
prefs = appComponent.preferences
|
||||||
|
habitList = appComponent.habitList
|
||||||
|
taskRunner = appComponent.taskRunner
|
||||||
|
modelFactory = appComponent.modelFactory
|
||||||
|
prefs.clear()
|
||||||
|
fixtures = HabitFixtures(modelFactory, habitList)
|
||||||
|
fixtures.purgeHabits(appComponent.habitList)
|
||||||
|
fixtures.createEmptyHabit()
|
||||||
|
component = DaggerHabitsActivityTestComponent
|
||||||
|
.builder()
|
||||||
|
.activityContextModule(ActivityContextModule(targetContext))
|
||||||
|
.habitsApplicationComponent(appComponent)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun assertWidgetProviderIsInstalled(componentClass: Class<out BaseWidgetProvider?>?) {
|
||||||
|
val provider = ComponentName(targetContext, componentClass!!)
|
||||||
|
val manager = AppWidgetManager.getInstance(targetContext)
|
||||||
|
val installedProviders: MutableList<ComponentName> = LinkedList()
|
||||||
|
for (info in manager.installedProviders) installedProviders.add(info.provider)
|
||||||
|
assertThat<List<ComponentName>>(
|
||||||
|
installedProviders,
|
||||||
|
hasItems(provider)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun setLocale(language: String, country: String) {
|
||||||
|
val locale = Locale(language, country)
|
||||||
|
Locale.setDefault(locale)
|
||||||
|
val res = targetContext.resources
|
||||||
|
val config = res.configuration
|
||||||
|
config.setLocale(locale)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun setResolution(r: Float) {
|
||||||
|
val dm = targetContext.resources.displayMetrics
|
||||||
|
dm.density = r
|
||||||
|
dm.scaledDensity = r
|
||||||
|
setFixedResolution(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun setTheme(@StyleRes themeId: Int) {
|
||||||
|
targetContext.setTheme(themeId)
|
||||||
|
setFixedTheme(themeId)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun sleep(time: Int) {
|
||||||
|
try {
|
||||||
|
Thread.sleep(time.toLong())
|
||||||
|
} catch (e: InterruptedException) {
|
||||||
|
fail()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun day(offset: Int): Timestamp {
|
||||||
|
return getToday().minus(offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun setSystemTime(
|
||||||
|
tz: String?,
|
||||||
|
year: Int,
|
||||||
|
javaMonth: Int,
|
||||||
|
day: Int,
|
||||||
|
hourOfDay: Int,
|
||||||
|
minute: Int
|
||||||
|
) {
|
||||||
|
val cal = GregorianCalendar()
|
||||||
|
cal[Calendar.SECOND] = 0
|
||||||
|
cal[year, javaMonth, day, hourOfDay] = minute
|
||||||
|
cal.timeZone = TimeZone.getTimeZone(tz)
|
||||||
|
setSystemTime(cal)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
private fun setSystemTime(cal: GregorianCalendar) {
|
||||||
|
val tz = cal.timeZone.toZoneId()
|
||||||
|
|
||||||
|
// Set time zone (temporary)
|
||||||
|
var command = String.format("service call alarm 3 s16 %s", tz)
|
||||||
|
device.executeShellCommand(command)
|
||||||
|
|
||||||
|
// Set time zone (permanent)
|
||||||
|
command = String.format("setprop persist.sys.timezone %s", tz)
|
||||||
|
device.executeShellCommand(command)
|
||||||
|
|
||||||
|
// Set time
|
||||||
|
val date = String.format(
|
||||||
|
"%02d%02d%02d%02d%02d.%02d",
|
||||||
|
cal[Calendar.MONTH] + 1,
|
||||||
|
cal[Calendar.DAY_OF_MONTH],
|
||||||
|
cal[Calendar.HOUR_OF_DAY],
|
||||||
|
cal[Calendar.MINUTE],
|
||||||
|
cal[Calendar.YEAR],
|
||||||
|
cal[Calendar.SECOND]
|
||||||
|
)
|
||||||
|
|
||||||
|
// Set time (method 1)
|
||||||
|
// Run twice to override daylight saving time
|
||||||
|
device.executeShellCommand("date $date")
|
||||||
|
device.executeShellCommand("date $date")
|
||||||
|
|
||||||
|
// Set time (method 2)
|
||||||
|
// Run in addition to the method above because one of these mail fail, depending
|
||||||
|
// on the Android API version.
|
||||||
|
command = String.format("date -u @%d", cal.timeInMillis / 1000)
|
||||||
|
device.executeShellCommand(command)
|
||||||
|
|
||||||
|
// Wait for system events to settle
|
||||||
|
Thread.sleep(1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
private lateinit var savedCalendar: GregorianCalendar
|
||||||
|
fun saveSystemTime() {
|
||||||
|
savedCalendar = GregorianCalendar()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun restoreSystemTime() {
|
||||||
|
setSystemTime(savedCalendar)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
// 8:00am, January 25th, 2015 (UTC)
|
||||||
|
const val FIXED_LOCAL_TIME = 1422172800000L
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,133 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
|
||||||
*
|
|
||||||
* This file is part of Loop Habit Tracker.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by the
|
|
||||||
* Free Software Foundation, either version 3 of the License, or (at your
|
|
||||||
* option) any later version.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
|
||||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
|
||||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
||||||
* more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License along
|
|
||||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.isoron.uhabits;
|
|
||||||
|
|
||||||
import android.content.*;
|
|
||||||
|
|
||||||
import androidx.test.uiautomator.*;
|
|
||||||
|
|
||||||
import com.linkedin.android.testbutler.*;
|
|
||||||
|
|
||||||
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 org.isoron.uhabits.inject.*;
|
|
||||||
import org.junit.*;
|
|
||||||
|
|
||||||
import static androidx.test.core.app.ApplicationProvider.*;
|
|
||||||
import static androidx.test.platform.app.InstrumentationRegistry.*;
|
|
||||||
import static androidx.test.uiautomator.UiDevice.*;
|
|
||||||
|
|
||||||
public class BaseUserInterfaceTest
|
|
||||||
{
|
|
||||||
private static final String PKG = "org.isoron.uhabits";
|
|
||||||
public static final String EMPTY_DESCRIPTION_HABIT_NAME = "Read books";
|
|
||||||
|
|
||||||
public static UiDevice device;
|
|
||||||
|
|
||||||
private HabitsApplicationComponent component;
|
|
||||||
|
|
||||||
private HabitList habitList;
|
|
||||||
|
|
||||||
private Preferences prefs;
|
|
||||||
|
|
||||||
private HabitFixtures fixtures;
|
|
||||||
|
|
||||||
private HabitCardListCache cache;
|
|
||||||
|
|
||||||
public static void startActivity(Class cls)
|
|
||||||
{
|
|
||||||
Intent intent = new Intent();
|
|
||||||
intent.setComponent(new ComponentName(PKG, cls.getCanonicalName()));
|
|
||||||
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
|
||||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
|
||||||
getApplicationContext().startActivity(intent);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Before
|
|
||||||
public void setUp() throws Exception
|
|
||||||
{
|
|
||||||
device = getInstance(getInstrumentation());
|
|
||||||
TestButler.setup(getApplicationContext());
|
|
||||||
TestButler.verifyAnimationsDisabled(getApplicationContext());
|
|
||||||
|
|
||||||
HabitsApplication app =
|
|
||||||
(HabitsApplication) getApplicationContext().getApplicationContext();
|
|
||||||
component = app.getComponent();
|
|
||||||
habitList = component.getHabitList();
|
|
||||||
prefs = component.getPreferences();
|
|
||||||
cache = component.getHabitCardListCache();
|
|
||||||
fixtures = new HabitFixtures(component.getModelFactory(), habitList);
|
|
||||||
resetState();
|
|
||||||
}
|
|
||||||
|
|
||||||
@After
|
|
||||||
public void tearDown() throws Exception
|
|
||||||
{
|
|
||||||
for (int i = 0; i < 10; i++) device.pressBack();
|
|
||||||
TestButler.teardown(getApplicationContext());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void resetState() throws Exception
|
|
||||||
{
|
|
||||||
prefs.clear();
|
|
||||||
prefs.setFirstRun(false);
|
|
||||||
prefs.updateLastHint(100, DateUtils.getToday());
|
|
||||||
habitList.removeAll();
|
|
||||||
cache.refreshAllHabits();
|
|
||||||
Thread.sleep(1000);
|
|
||||||
|
|
||||||
Habit h1 = fixtures.createEmptyHabit();
|
|
||||||
h1.setName("Wake up early");
|
|
||||||
h1.setQuestion("Did you wake up early today?");
|
|
||||||
h1.setDescription("test description 1");
|
|
||||||
h1.setColor(new PaletteColor(5));
|
|
||||||
habitList.update(h1);
|
|
||||||
|
|
||||||
Habit h2 = fixtures.createShortHabit();
|
|
||||||
h2.setName("Track time");
|
|
||||||
h2.setQuestion("Did you track your time?");
|
|
||||||
h2.setDescription("test description 2");
|
|
||||||
h2.setColor(new PaletteColor(5));
|
|
||||||
habitList.update(h2);
|
|
||||||
|
|
||||||
Habit h3 = fixtures.createLongHabit();
|
|
||||||
h3.setName("Meditate");
|
|
||||||
h3.setQuestion("Did meditate today?");
|
|
||||||
h3.setDescription("test description 3");
|
|
||||||
h3.setColor(new PaletteColor(10));
|
|
||||||
habitList.update(h3);
|
|
||||||
|
|
||||||
Habit h4 = fixtures.createEmptyHabit();
|
|
||||||
h4.setName(EMPTY_DESCRIPTION_HABIT_NAME);
|
|
||||||
h4.setQuestion("Did you read books today?");
|
|
||||||
h4.setDescription("");
|
|
||||||
h4.setColor(new PaletteColor(2));
|
|
||||||
habitList.update(h4);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void rotateDevice() throws Exception
|
|
||||||
{
|
|
||||||
device.setOrientationLeft();
|
|
||||||
device.setOrientationNatural();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
||||||
|
*
|
||||||
|
* This file is part of Loop Habit Tracker.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by the
|
||||||
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along
|
||||||
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.isoron.uhabits
|
||||||
|
|
||||||
|
import android.content.ComponentName
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import androidx.test.core.app.ApplicationProvider
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import androidx.test.uiautomator.UiDevice
|
||||||
|
import com.linkedin.android.testbutler.TestButler
|
||||||
|
import org.isoron.uhabits.core.models.HabitList
|
||||||
|
import org.isoron.uhabits.core.models.PaletteColor
|
||||||
|
import org.isoron.uhabits.core.preferences.Preferences
|
||||||
|
import org.isoron.uhabits.core.ui.screens.habits.list.HabitCardListCache
|
||||||
|
import org.isoron.uhabits.core.utils.DateUtils.Companion.getToday
|
||||||
|
import org.isoron.uhabits.inject.HabitsApplicationComponent
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Before
|
||||||
|
|
||||||
|
open class BaseUserInterfaceTest {
|
||||||
|
private lateinit var component: HabitsApplicationComponent
|
||||||
|
private lateinit var habitList: HabitList
|
||||||
|
private lateinit var prefs: Preferences
|
||||||
|
private lateinit var fixtures: HabitFixtures
|
||||||
|
private lateinit var cache: HabitCardListCache
|
||||||
|
@Before
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun setUp() {
|
||||||
|
device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
|
||||||
|
TestButler.setup(ApplicationProvider.getApplicationContext())
|
||||||
|
TestButler.verifyAnimationsDisabled(ApplicationProvider.getApplicationContext())
|
||||||
|
val app =
|
||||||
|
ApplicationProvider.getApplicationContext<Context>().applicationContext as HabitsApplication
|
||||||
|
component = app.component
|
||||||
|
habitList = component.habitList
|
||||||
|
prefs = component.preferences
|
||||||
|
cache = component.habitCardListCache
|
||||||
|
fixtures = HabitFixtures(component.modelFactory, habitList)
|
||||||
|
resetState()
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun tearDown() {
|
||||||
|
for (i in 0..9) device.pressBack()
|
||||||
|
TestButler.teardown(ApplicationProvider.getApplicationContext())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
private fun resetState() {
|
||||||
|
prefs.clear()
|
||||||
|
prefs.isFirstRun = false
|
||||||
|
prefs.updateLastHint(100, getToday())
|
||||||
|
habitList.removeAll()
|
||||||
|
cache.refreshAllHabits()
|
||||||
|
Thread.sleep(1000)
|
||||||
|
val h1 = fixtures.createEmptyHabit()
|
||||||
|
h1.name = "Wake up early"
|
||||||
|
h1.question = "Did you wake up early today?"
|
||||||
|
h1.description = "test description 1"
|
||||||
|
h1.color = PaletteColor(5)
|
||||||
|
habitList.update(h1)
|
||||||
|
val h2 = fixtures.createShortHabit()
|
||||||
|
h2.name = "Track time"
|
||||||
|
h2.question = "Did you track your time?"
|
||||||
|
h2.description = "test description 2"
|
||||||
|
h2.color = PaletteColor(5)
|
||||||
|
habitList.update(h2)
|
||||||
|
val h3 = fixtures.createLongHabit()
|
||||||
|
h3.name = "Meditate"
|
||||||
|
h3.question = "Did meditate today?"
|
||||||
|
h3.description = "test description 3"
|
||||||
|
h3.color = PaletteColor(10)
|
||||||
|
habitList.update(h3)
|
||||||
|
val h4 = fixtures.createEmptyHabit()
|
||||||
|
h4.name = EMPTY_DESCRIPTION_HABIT_NAME
|
||||||
|
h4.question = "Did you read books today?"
|
||||||
|
h4.description = ""
|
||||||
|
h4.color = PaletteColor(2)
|
||||||
|
habitList.update(h4)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
protected fun rotateDevice() {
|
||||||
|
device.setOrientationLeft()
|
||||||
|
device.setOrientationNatural()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val PKG = "org.isoron.uhabits"
|
||||||
|
const val EMPTY_DESCRIPTION_HABIT_NAME = "Read books"
|
||||||
|
lateinit var device: UiDevice
|
||||||
|
fun startActivity(cls: Class<*>) {
|
||||||
|
val intent = Intent()
|
||||||
|
intent.component = ComponentName(PKG, cls.canonicalName!!)
|
||||||
|
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||||
|
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
|
ApplicationProvider.getApplicationContext<Context>().startActivity(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,198 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
|
||||||
*
|
|
||||||
* This file is part of Loop Habit Tracker.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by the
|
|
||||||
* Free Software Foundation, either version 3 of the License, or (at your
|
|
||||||
* option) any later version.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
|
||||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
|
||||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
||||||
* more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License along
|
|
||||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.isoron.uhabits;
|
|
||||||
|
|
||||||
import android.graphics.*;
|
|
||||||
import android.view.*;
|
|
||||||
import android.widget.*;
|
|
||||||
|
|
||||||
import androidx.annotation.*;
|
|
||||||
import androidx.test.platform.app.*;
|
|
||||||
|
|
||||||
import org.isoron.uhabits.utils.*;
|
|
||||||
import org.isoron.uhabits.widgets.*;
|
|
||||||
|
|
||||||
import java.io.*;
|
|
||||||
import java.util.*;
|
|
||||||
|
|
||||||
import static android.view.View.MeasureSpec.*;
|
|
||||||
|
|
||||||
public class BaseViewTest extends BaseAndroidTest
|
|
||||||
{
|
|
||||||
public double similarityCutoff = 0.00018;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setUp()
|
|
||||||
{
|
|
||||||
super.setUp();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void assertRenders(View view, String expectedImagePath)
|
|
||||||
throws IOException
|
|
||||||
{
|
|
||||||
Bitmap actual = renderView(view);
|
|
||||||
if(actual == null) throw new IllegalStateException("actual is null");
|
|
||||||
assertRenders(actual, expectedImagePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void assertRenders(Bitmap actual, String expectedImagePath) throws IOException {
|
|
||||||
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
|
|
||||||
expectedImagePath = "views/" + expectedImagePath;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Bitmap expected = getBitmapFromAssets(expectedImagePath);
|
|
||||||
double distance = distance(actual, expected);
|
|
||||||
if (distance > similarityCutoff)
|
|
||||||
{
|
|
||||||
saveBitmap(expectedImagePath, ".expected", expected);
|
|
||||||
String path = saveBitmap(expectedImagePath, "", actual);
|
|
||||||
fail(String.format("Image differs from expected " +
|
|
||||||
"(distance=%f). Actual rendered " +
|
|
||||||
"image saved to %s", distance, path));
|
|
||||||
}
|
|
||||||
|
|
||||||
expected.recycle();
|
|
||||||
}
|
|
||||||
catch (IOException e)
|
|
||||||
{
|
|
||||||
String path = saveBitmap(expectedImagePath, "", actual);
|
|
||||||
fail(String.format("Could not open expected image. Actual " +
|
|
||||||
"rendered image saved to %s", path));
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
protected FrameLayout convertToView(BaseWidget widget,
|
|
||||||
int width,
|
|
||||||
int height)
|
|
||||||
{
|
|
||||||
widget.setDimensions(
|
|
||||||
new WidgetDimensions(width, height, width, height));
|
|
||||||
FrameLayout view = new FrameLayout(targetContext);
|
|
||||||
RemoteViews remoteViews = widget.getPortraitRemoteViews();
|
|
||||||
view.addView(remoteViews.apply(targetContext, view));
|
|
||||||
measureView(view, width, height);
|
|
||||||
return view;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected float dpToPixels(int dp)
|
|
||||||
{
|
|
||||||
return InterfaceUtils.dpToPixels(targetContext, dp);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void measureView(View view, float width, float height)
|
|
||||||
{
|
|
||||||
int specWidth = makeMeasureSpec((int) width, View.MeasureSpec.EXACTLY);
|
|
||||||
int specHeight = makeMeasureSpec((int) height, View.MeasureSpec.EXACTLY);
|
|
||||||
|
|
||||||
view.setLayoutParams(new ViewGroup.LayoutParams((int) width, (int) height));
|
|
||||||
view.measure(specWidth, specHeight);
|
|
||||||
view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight());
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void skipAnimation(View view)
|
|
||||||
{
|
|
||||||
ViewPropertyAnimator animator = view.animate();
|
|
||||||
animator.setDuration(0);
|
|
||||||
animator.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
private int[] colorToArgb(int c1)
|
|
||||||
{
|
|
||||||
return new int[]{
|
|
||||||
(c1 >> 24) & 0xff, //alpha
|
|
||||||
(c1 >> 16) & 0xff, //red
|
|
||||||
(c1 >> 8) & 0xff, //green
|
|
||||||
(c1) & 0xff //blue
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private double distance(Bitmap b1, Bitmap b2)
|
|
||||||
{
|
|
||||||
if (b1.getWidth() != b2.getWidth()) return 1.0;
|
|
||||||
if (b1.getHeight() != b2.getHeight()) return 1.0;
|
|
||||||
|
|
||||||
Random random = new Random();
|
|
||||||
|
|
||||||
double distance = 0.0;
|
|
||||||
for (int x = 0; x < b1.getWidth(); x++)
|
|
||||||
{
|
|
||||||
for (int y = 0; y < b1.getHeight(); y++)
|
|
||||||
{
|
|
||||||
if (random.nextInt(4) != 0) continue;
|
|
||||||
|
|
||||||
int[] argb1 = colorToArgb(b1.getPixel(x, y));
|
|
||||||
int[] argb2 = colorToArgb(b2.getPixel(x, y));
|
|
||||||
distance += Math.abs(argb1[0] - argb2[0]);
|
|
||||||
distance += Math.abs(argb1[1] - argb2[1]);
|
|
||||||
distance += Math.abs(argb1[2] - argb2[2]);
|
|
||||||
distance += Math.abs(argb1[3] - argb2[3]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
distance /= 255.0 * 16 * b1.getWidth() * b1.getHeight();
|
|
||||||
return distance;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Bitmap getBitmapFromAssets(String path) throws IOException
|
|
||||||
{
|
|
||||||
InputStream stream = testContext.getAssets().open(path);
|
|
||||||
return BitmapFactory.decodeStream(stream);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String saveBitmap(String filename, String suffix, Bitmap bitmap)
|
|
||||||
throws IOException
|
|
||||||
{
|
|
||||||
File dir = FileUtils.getSDCardDir("test-screenshots");
|
|
||||||
if (dir == null)
|
|
||||||
dir = new AndroidDirFinder(targetContext).getFilesDir("test-screenshots");
|
|
||||||
if (dir == null) throw new RuntimeException(
|
|
||||||
"Could not find suitable dir for screenshots");
|
|
||||||
|
|
||||||
filename = filename.replaceAll("\\.png$", suffix + ".png");
|
|
||||||
String absolutePath =
|
|
||||||
String.format("%s/%s", dir.getAbsolutePath(), filename);
|
|
||||||
|
|
||||||
File parent = new File(absolutePath).getParentFile();
|
|
||||||
if (!parent.exists() && !parent.mkdirs()) throw new RuntimeException(
|
|
||||||
String.format("Could not create dir: %s",
|
|
||||||
parent.getAbsolutePath()));
|
|
||||||
|
|
||||||
FileOutputStream out = new FileOutputStream(absolutePath);
|
|
||||||
bitmap.compress(Bitmap.CompressFormat.PNG, 100, out);
|
|
||||||
|
|
||||||
return absolutePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Bitmap renderView(View view)
|
|
||||||
{
|
|
||||||
int width = view.getMeasuredWidth();
|
|
||||||
int height = view.getMeasuredHeight();
|
|
||||||
if(view.isLayoutRequested())
|
|
||||||
measureView(view, width, height);
|
|
||||||
|
|
||||||
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
|
|
||||||
Canvas canvas = new Canvas(bitmap);
|
|
||||||
view.invalidate();
|
|
||||||
view.draw(canvas);
|
|
||||||
return bitmap;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
||||||
|
*
|
||||||
|
* This file is part of Loop Habit Tracker.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by the
|
||||||
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along
|
||||||
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.isoron.uhabits
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.view.View
|
||||||
|
import android.view.View.MeasureSpec
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import org.isoron.uhabits.utils.FileUtils.getSDCardDir
|
||||||
|
import org.isoron.uhabits.utils.InterfaceUtils.dpToPixels
|
||||||
|
import org.isoron.uhabits.widgets.BaseWidget
|
||||||
|
import org.isoron.uhabits.widgets.WidgetDimensions
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.io.IOException
|
||||||
|
import java.util.Random
|
||||||
|
import kotlin.math.abs
|
||||||
|
|
||||||
|
open class BaseViewTest : BaseAndroidTest() {
|
||||||
|
var similarityCutoff = 0.00018
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
protected fun assertRenders(view: View, expectedImagePath: String) {
|
||||||
|
val actual = renderView(view)
|
||||||
|
assertRenders(actual, expectedImagePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
protected fun assertRenders(actual: Bitmap, expectedImagePath: String) {
|
||||||
|
var expectedImagePath = expectedImagePath
|
||||||
|
InstrumentationRegistry.getInstrumentation().waitForIdleSync()
|
||||||
|
expectedImagePath = "views/$expectedImagePath"
|
||||||
|
try {
|
||||||
|
val expected = getBitmapFromAssets(expectedImagePath)
|
||||||
|
val distance = distance(actual, expected)
|
||||||
|
if (distance > similarityCutoff) {
|
||||||
|
saveBitmap(expectedImagePath, ".expected", expected)
|
||||||
|
val path = saveBitmap(expectedImagePath, "", actual)
|
||||||
|
fail(
|
||||||
|
String.format(
|
||||||
|
"Image differs from expected " +
|
||||||
|
"(distance=%f). Actual rendered " +
|
||||||
|
"image saved to %s",
|
||||||
|
distance,
|
||||||
|
path
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
expected.recycle()
|
||||||
|
} catch (e: IOException) {
|
||||||
|
val path = saveBitmap(expectedImagePath, "", actual)
|
||||||
|
fail(
|
||||||
|
String.format(
|
||||||
|
"Could not open expected image. Actual " +
|
||||||
|
"rendered image saved to %s",
|
||||||
|
path
|
||||||
|
)
|
||||||
|
)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun convertToView(
|
||||||
|
widget: BaseWidget,
|
||||||
|
width: Int,
|
||||||
|
height: Int
|
||||||
|
): FrameLayout {
|
||||||
|
widget.setDimensions(
|
||||||
|
WidgetDimensions(width, height, width, height)
|
||||||
|
)
|
||||||
|
val view = FrameLayout(targetContext)
|
||||||
|
val remoteViews = widget.portraitRemoteViews
|
||||||
|
view.addView(remoteViews.apply(targetContext, view))
|
||||||
|
measureView(view, width.toFloat(), height.toFloat())
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun dpToPixels(dp: Int): Float {
|
||||||
|
return dpToPixels(targetContext, dp.toFloat())
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun measureView(view: View, width: Float, height: Float) {
|
||||||
|
val specWidth = MeasureSpec.makeMeasureSpec(width.toInt(), MeasureSpec.EXACTLY)
|
||||||
|
val specHeight = MeasureSpec.makeMeasureSpec(height.toInt(), MeasureSpec.EXACTLY)
|
||||||
|
view.layoutParams = ViewGroup.LayoutParams(width.toInt(), height.toInt())
|
||||||
|
view.measure(specWidth, specHeight)
|
||||||
|
view.layout(0, 0, view.measuredWidth, view.measuredHeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun skipAnimation(view: View) {
|
||||||
|
val animator = view.animate()
|
||||||
|
animator.duration = 0
|
||||||
|
animator.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun colorToArgb(c1: Int): IntArray {
|
||||||
|
return intArrayOf(
|
||||||
|
c1 shr 24 and 0xff, // alpha
|
||||||
|
c1 shr 16 and 0xff, // red
|
||||||
|
c1 shr 8 and 0xff, // green
|
||||||
|
c1 and 0xff // blue
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun distance(b1: Bitmap, b2: Bitmap): Double {
|
||||||
|
if (b1.width != b2.width) return 1.0
|
||||||
|
if (b1.height != b2.height) return 1.0
|
||||||
|
val random = Random()
|
||||||
|
var distance = 0.0
|
||||||
|
for (x in 0 until b1.width) {
|
||||||
|
for (y in 0 until b1.height) {
|
||||||
|
if (random.nextInt(4) != 0) continue
|
||||||
|
val argb1 = colorToArgb(b1.getPixel(x, y))
|
||||||
|
val argb2 = colorToArgb(b2.getPixel(x, y))
|
||||||
|
distance += abs(argb1[0] - argb2[0]).toDouble()
|
||||||
|
distance += abs(argb1[1] - argb2[1]).toDouble()
|
||||||
|
distance += abs(argb1[2] - argb2[2]).toDouble()
|
||||||
|
distance += abs(argb1[3] - argb2[3]).toDouble()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
distance /= 255.0 * 16 * b1.width * b1.height
|
||||||
|
return distance
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
private fun getBitmapFromAssets(path: String): Bitmap {
|
||||||
|
val stream = testContext.assets.open(path)
|
||||||
|
return BitmapFactory.decodeStream(stream)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
private fun saveBitmap(filename: String, suffix: String, bitmap: Bitmap): String {
|
||||||
|
var filename = filename
|
||||||
|
var dir = getSDCardDir("test-screenshots")
|
||||||
|
if (dir == null) dir = AndroidDirFinder(targetContext).getFilesDir("test-screenshots")
|
||||||
|
if (dir == null) throw RuntimeException(
|
||||||
|
"Could not find suitable dir for screenshots"
|
||||||
|
)
|
||||||
|
filename = filename.replace("\\.png$".toRegex(), "$suffix.png")
|
||||||
|
val absolutePath = String.format("%s/%s", dir.absolutePath, filename)
|
||||||
|
val parent = File(absolutePath).parentFile
|
||||||
|
if (!parent.exists() && !parent.mkdirs()) throw RuntimeException(
|
||||||
|
String.format(
|
||||||
|
"Could not create dir: %s",
|
||||||
|
parent.absolutePath
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val out = FileOutputStream(absolutePath)
|
||||||
|
bitmap.compress(Bitmap.CompressFormat.PNG, 100, out)
|
||||||
|
return absolutePath
|
||||||
|
}
|
||||||
|
|
||||||
|
fun renderView(view: View): Bitmap {
|
||||||
|
val width = view.measuredWidth
|
||||||
|
val height = view.measuredHeight
|
||||||
|
if (view.isLayoutRequested) measureView(view, width.toFloat(), height.toFloat())
|
||||||
|
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
||||||
|
val canvas = Canvas(bitmap)
|
||||||
|
view.invalidate()
|
||||||
|
view.draw(canvas)
|
||||||
|
return bitmap
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,164 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
|
||||||
*
|
|
||||||
* This file is part of Loop Habit Tracker.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by the
|
|
||||||
* Free Software Foundation, either version 3 of the License, or (at your
|
|
||||||
* option) any later version.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
|
||||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
|
||||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
||||||
* more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License along
|
|
||||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.isoron.uhabits;
|
|
||||||
|
|
||||||
import org.isoron.uhabits.core.models.*;
|
|
||||||
import org.isoron.uhabits.core.utils.DateUtils;
|
|
||||||
|
|
||||||
import static org.isoron.uhabits.core.models.Entry.*;
|
|
||||||
|
|
||||||
public class HabitFixtures
|
|
||||||
{
|
|
||||||
public boolean LONG_HABIT_ENTRIES[] = {
|
|
||||||
true, false, false, true, true, true, false, false, true, true
|
|
||||||
};
|
|
||||||
|
|
||||||
public int LONG_NUMERICAL_HABIT_ENTRIES[] = {
|
|
||||||
200000, 0, 150000, 137000, 0, 0, 500000, 30000, 100000, 0, 300000,
|
|
||||||
100000, 0, 100000
|
|
||||||
};
|
|
||||||
|
|
||||||
private ModelFactory modelFactory;
|
|
||||||
|
|
||||||
private final HabitList habitList;
|
|
||||||
|
|
||||||
public HabitFixtures(ModelFactory modelFactory, HabitList habitList)
|
|
||||||
{
|
|
||||||
this.modelFactory = modelFactory;
|
|
||||||
this.habitList = habitList;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Habit createEmptyHabit()
|
|
||||||
{
|
|
||||||
return createEmptyHabit(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Habit createEmptyHabit(Long id)
|
|
||||||
{
|
|
||||||
Habit habit = modelFactory.buildHabit();
|
|
||||||
habit.setName("Meditate");
|
|
||||||
habit.setQuestion("Did you meditate this morning?");
|
|
||||||
habit.setDescription("This is a test description");
|
|
||||||
habit.setColor(new PaletteColor(5));
|
|
||||||
habit.setFrequency(Frequency.DAILY);
|
|
||||||
habit.setId(id);
|
|
||||||
habitList.add(habit);
|
|
||||||
return habit;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Habit createLongHabit()
|
|
||||||
{
|
|
||||||
Habit habit = createEmptyHabit();
|
|
||||||
habit.setFrequency(new Frequency(3, 7));
|
|
||||||
habit.setColor(new PaletteColor(7));
|
|
||||||
|
|
||||||
Timestamp today = DateUtils.getToday();
|
|
||||||
int marks[] = { 0, 1, 3, 5, 7, 8, 9, 10, 12, 14, 15, 17, 19, 20, 26, 27,
|
|
||||||
28, 50, 51, 52, 53, 54, 58, 60, 63, 65, 70, 71, 72, 73, 74, 75, 80,
|
|
||||||
81, 83, 89, 90, 91, 95, 102, 103, 108, 109, 120};
|
|
||||||
|
|
||||||
for (int mark : marks)
|
|
||||||
habit.getOriginalEntries().add(new Entry(today.minus(mark), YES_MANUAL));
|
|
||||||
|
|
||||||
habit.recompute();
|
|
||||||
return habit;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Habit createVeryLongHabit()
|
|
||||||
{
|
|
||||||
Habit habit = createEmptyHabit();
|
|
||||||
habit.setFrequency(new Frequency(1, 2));
|
|
||||||
habit.setColor(new PaletteColor(11));
|
|
||||||
|
|
||||||
Timestamp today = DateUtils.getToday();
|
|
||||||
int marks[] = {0, 3, 5, 6, 7, 10, 13, 14, 15, 18, 21, 22, 23, 24, 27, 28, 30, 31, 34, 37,
|
|
||||||
39, 42, 43, 46, 47, 48, 51, 52, 54, 55, 57, 59, 62, 65, 68, 71, 73, 76, 79,
|
|
||||||
80, 81, 83, 85, 86, 89, 90, 91, 94, 96, 98, 100, 103, 104, 106, 109, 111,
|
|
||||||
112, 113, 115, 117, 120, 123, 126, 129, 132, 134, 136, 139, 141, 142, 145,
|
|
||||||
148, 149, 151, 152, 154, 156, 157, 159, 161, 162, 163, 164, 166, 168, 170,
|
|
||||||
172, 173, 174, 175, 176, 178, 180, 181, 184, 185, 188, 189, 190, 191, 194,
|
|
||||||
195, 197, 198, 199, 200, 202, 205, 208, 211, 213, 215, 216, 218, 220, 222,
|
|
||||||
223, 225, 227, 228, 230, 231, 232, 234, 235, 238, 241, 242, 244, 247, 250,
|
|
||||||
251, 253, 254, 257, 260, 261, 263, 264, 266, 269, 272, 273, 276, 279, 281,
|
|
||||||
284, 285, 288, 291, 292, 294, 296, 297, 299, 300, 301, 303, 306, 307, 308,
|
|
||||||
309, 310, 313, 316, 319, 322, 324, 326, 329, 330, 332, 334, 335, 337, 338,
|
|
||||||
341, 344, 345, 346, 347, 350, 352, 355, 358, 360, 361, 362, 363, 365, 368,
|
|
||||||
371, 373, 374, 376, 379, 380, 382, 384, 385, 387, 389, 390, 392, 393, 395,
|
|
||||||
396, 399, 401, 404, 407, 410, 411, 413, 414, 416, 417, 419, 420, 423, 424,
|
|
||||||
427, 429, 431, 433, 436, 439, 440, 442, 445, 447, 450, 453, 454, 456, 459,
|
|
||||||
460, 461, 464, 466, 468, 470, 473, 474, 475, 477, 479, 481, 482, 483, 486,
|
|
||||||
489, 491, 493, 495, 497, 498, 500, 503, 504, 507, 510, 511, 512, 515, 518,
|
|
||||||
519, 521, 522, 525, 528, 531, 532, 534, 537, 539, 541, 543, 544, 547, 550,
|
|
||||||
551, 554, 556, 557, 560, 561, 564, 567, 568, 569, 570, 572, 575, 576, 579,
|
|
||||||
582, 583, 584, 586, 589};
|
|
||||||
|
|
||||||
for (int mark : marks)
|
|
||||||
habit.getOriginalEntries().add(new Entry(today.minus(mark), YES_MANUAL));
|
|
||||||
|
|
||||||
habit.recompute();
|
|
||||||
return habit;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Habit createLongNumericalHabit()
|
|
||||||
{
|
|
||||||
Habit habit = modelFactory.buildHabit();
|
|
||||||
habit.setName("Read");
|
|
||||||
habit.setQuestion("How many pages did you walk today?");
|
|
||||||
habit.setType(Habit.NUMBER_HABIT);
|
|
||||||
habit.setTargetType(Habit.AT_LEAST);
|
|
||||||
habit.setTargetValue(200.0);
|
|
||||||
habit.setUnit("pages");
|
|
||||||
habitList.add(habit);
|
|
||||||
|
|
||||||
Timestamp timestamp = DateUtils.getToday();
|
|
||||||
for (int value : LONG_NUMERICAL_HABIT_ENTRIES)
|
|
||||||
{
|
|
||||||
habit.getOriginalEntries().add(new Entry(timestamp, value));
|
|
||||||
timestamp = timestamp.minus(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
habit.recompute();
|
|
||||||
return habit;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Habit createShortHabit()
|
|
||||||
{
|
|
||||||
Habit habit = modelFactory.buildHabit();
|
|
||||||
habit.setName("Wake up early");
|
|
||||||
habit.setQuestion("Did you wake up before 6am?");
|
|
||||||
habit.setFrequency(new Frequency(2, 3));
|
|
||||||
habitList.add(habit);
|
|
||||||
|
|
||||||
Timestamp timestamp = DateUtils.getToday();
|
|
||||||
for (boolean c : LONG_HABIT_ENTRIES)
|
|
||||||
{
|
|
||||||
if (c) habit.getOriginalEntries().add(new Entry(timestamp, YES_MANUAL));
|
|
||||||
timestamp = timestamp.minus(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
habit.recompute();
|
|
||||||
return habit;
|
|
||||||
}
|
|
||||||
|
|
||||||
public synchronized void purgeHabits(HabitList habitList)
|
|
||||||
{
|
|
||||||
habitList.removeAll();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
||||||
|
*
|
||||||
|
* This file is part of Loop Habit Tracker.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by the
|
||||||
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along
|
||||||
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.isoron.uhabits
|
||||||
|
|
||||||
|
import org.isoron.uhabits.core.models.Entry
|
||||||
|
import org.isoron.uhabits.core.models.Entry.Companion.YES_MANUAL
|
||||||
|
import org.isoron.uhabits.core.models.Frequency
|
||||||
|
import org.isoron.uhabits.core.models.Frequency.Companion.DAILY
|
||||||
|
import org.isoron.uhabits.core.models.Habit
|
||||||
|
import org.isoron.uhabits.core.models.Habit.Companion.AT_LEAST
|
||||||
|
import org.isoron.uhabits.core.models.Habit.Companion.NUMBER_HABIT
|
||||||
|
import org.isoron.uhabits.core.models.HabitList
|
||||||
|
import org.isoron.uhabits.core.models.ModelFactory
|
||||||
|
import org.isoron.uhabits.core.models.PaletteColor
|
||||||
|
import org.isoron.uhabits.core.models.Timestamp
|
||||||
|
import org.isoron.uhabits.core.utils.DateUtils.Companion.getToday
|
||||||
|
|
||||||
|
class HabitFixtures(private val modelFactory: ModelFactory, private val habitList: HabitList) {
|
||||||
|
var LONG_HABIT_ENTRIES = booleanArrayOf(
|
||||||
|
true, false, false, true, true, true, false, false, true, true
|
||||||
|
)
|
||||||
|
var LONG_NUMERICAL_HABIT_ENTRIES = intArrayOf(
|
||||||
|
200000, 0, 150000, 137000, 0, 0, 500000, 30000, 100000, 0, 300000,
|
||||||
|
100000, 0, 100000
|
||||||
|
)
|
||||||
|
|
||||||
|
fun createEmptyHabit(): Habit {
|
||||||
|
val habit = modelFactory.buildHabit()
|
||||||
|
habit.name = "Meditate"
|
||||||
|
habit.question = "Did you meditate this morning?"
|
||||||
|
habit.description = "This is a test description"
|
||||||
|
habit.color = PaletteColor(5)
|
||||||
|
habit.frequency = DAILY
|
||||||
|
habitList.add(habit)
|
||||||
|
return habit
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createLongHabit(): Habit {
|
||||||
|
val habit = createEmptyHabit()
|
||||||
|
habit.frequency = Frequency(3, 7)
|
||||||
|
habit.color = PaletteColor(7)
|
||||||
|
val today: Timestamp = getToday()
|
||||||
|
val marks = intArrayOf(
|
||||||
|
0, 1, 3, 5, 7, 8, 9, 10, 12, 14, 15, 17, 19, 20, 26, 27,
|
||||||
|
28, 50, 51, 52, 53, 54, 58, 60, 63, 65, 70, 71, 72, 73, 74, 75, 80,
|
||||||
|
81, 83, 89, 90, 91, 95, 102, 103, 108, 109, 120
|
||||||
|
)
|
||||||
|
for (mark in marks) habit.originalEntries.add(Entry(today.minus(mark), YES_MANUAL))
|
||||||
|
habit.recompute()
|
||||||
|
return habit
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createVeryLongHabit(): Habit {
|
||||||
|
val habit = createEmptyHabit()
|
||||||
|
habit.frequency = Frequency(1, 2)
|
||||||
|
habit.color = PaletteColor(11)
|
||||||
|
val today: Timestamp = getToday()
|
||||||
|
val marks = intArrayOf(
|
||||||
|
0, 3, 5, 6, 7, 10, 13, 14, 15, 18, 21, 22, 23, 24, 27, 28, 30, 31, 34, 37,
|
||||||
|
39, 42, 43, 46, 47, 48, 51, 52, 54, 55, 57, 59, 62, 65, 68, 71, 73, 76, 79,
|
||||||
|
80, 81, 83, 85, 86, 89, 90, 91, 94, 96, 98, 100, 103, 104, 106, 109, 111,
|
||||||
|
112, 113, 115, 117, 120, 123, 126, 129, 132, 134, 136, 139, 141, 142, 145,
|
||||||
|
148, 149, 151, 152, 154, 156, 157, 159, 161, 162, 163, 164, 166, 168, 170,
|
||||||
|
172, 173, 174, 175, 176, 178, 180, 181, 184, 185, 188, 189, 190, 191, 194,
|
||||||
|
195, 197, 198, 199, 200, 202, 205, 208, 211, 213, 215, 216, 218, 220, 222,
|
||||||
|
223, 225, 227, 228, 230, 231, 232, 234, 235, 238, 241, 242, 244, 247, 250,
|
||||||
|
251, 253, 254, 257, 260, 261, 263, 264, 266, 269, 272, 273, 276, 279, 281,
|
||||||
|
284, 285, 288, 291, 292, 294, 296, 297, 299, 300, 301, 303, 306, 307, 308,
|
||||||
|
309, 310, 313, 316, 319, 322, 324, 326, 329, 330, 332, 334, 335, 337, 338,
|
||||||
|
341, 344, 345, 346, 347, 350, 352, 355, 358, 360, 361, 362, 363, 365, 368,
|
||||||
|
371, 373, 374, 376, 379, 380, 382, 384, 385, 387, 389, 390, 392, 393, 395,
|
||||||
|
396, 399, 401, 404, 407, 410, 411, 413, 414, 416, 417, 419, 420, 423, 424,
|
||||||
|
427, 429, 431, 433, 436, 439, 440, 442, 445, 447, 450, 453, 454, 456, 459,
|
||||||
|
460, 461, 464, 466, 468, 470, 473, 474, 475, 477, 479, 481, 482, 483, 486,
|
||||||
|
489, 491, 493, 495, 497, 498, 500, 503, 504, 507, 510, 511, 512, 515, 518,
|
||||||
|
519, 521, 522, 525, 528, 531, 532, 534, 537, 539, 541, 543, 544, 547, 550,
|
||||||
|
551, 554, 556, 557, 560, 561, 564, 567, 568, 569, 570, 572, 575, 576, 579,
|
||||||
|
582, 583, 584, 586, 589
|
||||||
|
)
|
||||||
|
for (mark in marks) habit.originalEntries.add(Entry(today.minus(mark), YES_MANUAL))
|
||||||
|
habit.recompute()
|
||||||
|
return habit
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createLongNumericalHabit(): Habit {
|
||||||
|
val habit = modelFactory.buildHabit().apply {
|
||||||
|
name = "Read"
|
||||||
|
question = "How many pages did you walk today?"
|
||||||
|
type = NUMBER_HABIT
|
||||||
|
targetType = AT_LEAST
|
||||||
|
targetValue = 200.0
|
||||||
|
unit = "pages"
|
||||||
|
}
|
||||||
|
habitList.add(habit)
|
||||||
|
var timestamp: Timestamp = getToday()
|
||||||
|
for (value in LONG_NUMERICAL_HABIT_ENTRIES) {
|
||||||
|
habit.originalEntries.add(Entry(timestamp, value))
|
||||||
|
timestamp = timestamp.minus(1)
|
||||||
|
}
|
||||||
|
habit.recompute()
|
||||||
|
return habit
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createShortHabit(): Habit {
|
||||||
|
val habit = modelFactory.buildHabit().apply {
|
||||||
|
name = "Wake up early"
|
||||||
|
question = "Did you wake up before 6am?"
|
||||||
|
frequency = Frequency(2, 3)
|
||||||
|
}
|
||||||
|
habitList.add(habit)
|
||||||
|
var timestamp: Timestamp = getToday()
|
||||||
|
for (c in LONG_HABIT_ENTRIES) {
|
||||||
|
if (c) habit.originalEntries.add(Entry(timestamp, YES_MANUAL))
|
||||||
|
timestamp = timestamp.minus(1)
|
||||||
|
}
|
||||||
|
habit.recompute()
|
||||||
|
return habit
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun purgeHabits(habitList: HabitList) {
|
||||||
|
habitList.removeAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@
|
|||||||
|
|
||||||
package org.isoron.uhabits
|
package org.isoron.uhabits
|
||||||
|
|
||||||
|
import com.nhaarman.mockitokotlin2.mock
|
||||||
import dagger.Component
|
import dagger.Component
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
@@ -34,11 +35,11 @@ import org.isoron.uhabits.inject.ActivityScope
|
|||||||
import org.isoron.uhabits.inject.HabitModule
|
import org.isoron.uhabits.inject.HabitModule
|
||||||
import org.isoron.uhabits.inject.HabitsActivityModule
|
import org.isoron.uhabits.inject.HabitsActivityModule
|
||||||
import org.isoron.uhabits.inject.HabitsApplicationComponent
|
import org.isoron.uhabits.inject.HabitsApplicationComponent
|
||||||
import org.mockito.Mockito.mock
|
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
class TestModule {
|
class TestModule {
|
||||||
@Provides fun listHabitsBehavior(): ListHabitsBehavior = mock(ListHabitsBehavior::class.java)
|
@Provides
|
||||||
|
fun listHabitsBehavior(): ListHabitsBehavior = mock()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ActivityScope
|
@ActivityScope
|
||||||
|
|||||||
@@ -16,32 +16,25 @@
|
|||||||
* You should have received a copy of the GNU General Public License along
|
* You should have received a copy of the GNU General Public License along
|
||||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
package org.isoron.uhabits
|
||||||
|
|
||||||
package org.isoron.uhabits;
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import androidx.test.filters.MediumTest
|
||||||
|
import org.hamcrest.CoreMatchers.containsString
|
||||||
|
import org.hamcrest.MatcherAssert.assertThat
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
import androidx.test.filters.*;
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
|
||||||
|
|
||||||
import org.junit.*;
|
|
||||||
import org.junit.runner.*;
|
|
||||||
|
|
||||||
import java.io.*;
|
|
||||||
|
|
||||||
import static org.hamcrest.CoreMatchers.*;
|
|
||||||
import static org.hamcrest.MatcherAssert.*;
|
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4.class)
|
|
||||||
@MediumTest
|
@MediumTest
|
||||||
public class HabitsApplicationTest extends BaseAndroidTest
|
class HabitsApplicationTest : BaseAndroidTest() {
|
||||||
{
|
|
||||||
@Test
|
@Test
|
||||||
public void test_getLogcat() throws IOException
|
@Throws(IOException::class)
|
||||||
{
|
fun test_getLogcat() {
|
||||||
String msg = "LOGCAT TEST";
|
val msg = "LOGCAT TEST"
|
||||||
new RuntimeException(msg).printStackTrace();
|
RuntimeException(msg).printStackTrace()
|
||||||
|
val log = AndroidBugReporter(targetContext).getLogcat()
|
||||||
String log = new AndroidBugReporter(targetContext).getLogcat();
|
assertThat(log, containsString(msg))
|
||||||
assertThat(log, containsString(msg));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
|
||||||
*
|
|
||||||
* This file is part of Loop Habit Tracker.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by the
|
|
||||||
* Free Software Foundation, either version 3 of the License, or (at your
|
|
||||||
* option) any later version.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
|
||||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
|
||||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
||||||
* more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License along
|
|
||||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.isoron.uhabits;
|
|
||||||
|
|
||||||
import org.isoron.uhabits.core.*;
|
|
||||||
import org.isoron.uhabits.core.tasks.*;
|
|
||||||
import org.isoron.uhabits.inject.*;
|
|
||||||
import org.isoron.uhabits.intents.*;
|
|
||||||
|
|
||||||
import dagger.*;
|
|
||||||
|
|
||||||
@AppScope
|
|
||||||
@Component(modules = {
|
|
||||||
AppContextModule.class,
|
|
||||||
HabitsModule.class,
|
|
||||||
SingleThreadModule.class,
|
|
||||||
})
|
|
||||||
public interface HabitsApplicationTestComponent
|
|
||||||
extends HabitsApplicationComponent
|
|
||||||
{
|
|
||||||
IntentScheduler getIntentScheduler();
|
|
||||||
}
|
|
||||||
|
|
||||||
@dagger.Module
|
|
||||||
class SingleThreadModule
|
|
||||||
{
|
|
||||||
@Provides
|
|
||||||
@AppScope
|
|
||||||
static TaskRunner provideTaskRunner()
|
|
||||||
{
|
|
||||||
return new SingleThreadTaskRunner();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -16,25 +16,17 @@
|
|||||||
* You should have received a copy of the GNU General Public License along
|
* You should have received a copy of the GNU General Public License along
|
||||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
package org.isoron.uhabits
|
||||||
|
|
||||||
package org.isoron.uhabits.core.tasks;
|
import dagger.Component
|
||||||
|
import org.isoron.uhabits.core.AppScope
|
||||||
|
import org.isoron.uhabits.inject.AppContextModule
|
||||||
|
import org.isoron.uhabits.inject.HabitsApplicationComponent
|
||||||
|
import org.isoron.uhabits.inject.HabitsModule
|
||||||
|
import org.isoron.uhabits.intents.IntentScheduler
|
||||||
|
|
||||||
public interface TaskRunner
|
@AppScope
|
||||||
{
|
@Component(modules = [AppContextModule::class, HabitsModule::class, SingleThreadModule::class])
|
||||||
void addListener(Listener listener);
|
interface HabitsApplicationTestComponent : HabitsApplicationComponent {
|
||||||
|
val intentScheduler: IntentScheduler?
|
||||||
void removeListener(Listener listener);
|
|
||||||
|
|
||||||
void execute(Task task);
|
|
||||||
|
|
||||||
void publishProgress(Task task, int progress);
|
|
||||||
|
|
||||||
int getActiveTaskCount();
|
|
||||||
|
|
||||||
interface Listener
|
|
||||||
{
|
|
||||||
void onTaskStarted(Task task);
|
|
||||||
|
|
||||||
void onTaskFinished(Task task);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package org.isoron.uhabits
|
||||||
|
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.Provides
|
||||||
|
import org.isoron.uhabits.core.AppScope
|
||||||
|
import org.isoron.uhabits.core.tasks.SingleThreadTaskRunner
|
||||||
|
import org.isoron.uhabits.core.tasks.TaskRunner
|
||||||
|
|
||||||
|
@Module
|
||||||
|
internal object SingleThreadModule {
|
||||||
|
@JvmStatic
|
||||||
|
@Provides
|
||||||
|
@AppScope
|
||||||
|
fun provideTaskRunner(): TaskRunner {
|
||||||
|
return SingleThreadTaskRunner()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
package org.isoron.uhabits.acceptance.steps
|
package org.isoron.uhabits.acceptance.steps
|
||||||
|
|
||||||
import androidx.test.uiautomator.UiSelector
|
import androidx.test.uiautomator.UiSelector
|
||||||
import org.isoron.uhabits.BaseUserInterfaceTest.device
|
import org.isoron.uhabits.BaseUserInterfaceTest.Companion.device
|
||||||
import org.isoron.uhabits.acceptance.steps.CommonSteps.clickText
|
import org.isoron.uhabits.acceptance.steps.CommonSteps.clickText
|
||||||
import org.isoron.uhabits.acceptance.steps.ListHabitsSteps.MenuItem.SETTINGS
|
import org.isoron.uhabits.acceptance.steps.ListHabitsSteps.MenuItem.SETTINGS
|
||||||
import org.isoron.uhabits.acceptance.steps.ListHabitsSteps.clickMenu
|
import org.isoron.uhabits.acceptance.steps.ListHabitsSteps.clickMenu
|
||||||
|
|||||||
@@ -1,84 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
|
||||||
*
|
|
||||||
* This file is part of Loop Habit Tracker.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by the
|
|
||||||
* Free Software Foundation, either version 3 of the License, or (at your
|
|
||||||
* option) any later version.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
|
||||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
|
||||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
||||||
* more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License along
|
|
||||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.isoron.uhabits.activities.common.views;
|
|
||||||
|
|
||||||
import androidx.test.ext.junit.runners.*;
|
|
||||||
import androidx.test.filters.*;
|
|
||||||
|
|
||||||
import org.isoron.uhabits.*;
|
|
||||||
import org.isoron.uhabits.core.models.*;
|
|
||||||
import org.isoron.uhabits.utils.*;
|
|
||||||
import org.junit.*;
|
|
||||||
import org.junit.runner.*;
|
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4.class)
|
|
||||||
@MediumTest
|
|
||||||
public class FrequencyChartTest extends BaseViewTest
|
|
||||||
{
|
|
||||||
public static final String BASE_PATH = "common/FrequencyChart/";
|
|
||||||
|
|
||||||
private FrequencyChart view;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Before
|
|
||||||
public void setUp()
|
|
||||||
{
|
|
||||||
super.setUp();
|
|
||||||
|
|
||||||
fixtures.purgeHabits(habitList);
|
|
||||||
Habit habit = fixtures.createLongHabit();
|
|
||||||
|
|
||||||
view = new FrequencyChart(targetContext);
|
|
||||||
view.setFrequency(habit.getOriginalEntries().computeWeekdayFrequency(
|
|
||||||
habit.isNumerical()
|
|
||||||
));
|
|
||||||
view.setColor(PaletteUtilsKt.toFixedAndroidColor(habit.getColor()));
|
|
||||||
measureView(view, dpToPixels(300), dpToPixels(100));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testRender() throws Throwable
|
|
||||||
{
|
|
||||||
assertRenders(view, BASE_PATH + "render.png");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testRender_withDataOffset() throws Throwable
|
|
||||||
{
|
|
||||||
view.onScroll(null, null, -dpToPixels(150), 0);
|
|
||||||
view.invalidate();
|
|
||||||
|
|
||||||
assertRenders(view, BASE_PATH + "renderDataOffset.png");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testRender_withDifferentSize() throws Throwable
|
|
||||||
{
|
|
||||||
measureView(view, dpToPixels(200), dpToPixels(200));
|
|
||||||
assertRenders(view, BASE_PATH + "renderDifferentSize.png");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testRender_withTransparentBackground() throws Throwable
|
|
||||||
{
|
|
||||||
view.setIsBackgroundTransparent(true);
|
|
||||||
assertRenders(view, BASE_PATH + "renderTransparent.png");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
||||||
|
*
|
||||||
|
* This file is part of Loop Habit Tracker.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by the
|
||||||
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along
|
||||||
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.isoron.uhabits.activities.common.views
|
||||||
|
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import androidx.test.filters.MediumTest
|
||||||
|
import org.isoron.uhabits.BaseViewTest
|
||||||
|
import org.isoron.uhabits.utils.toFixedAndroidColor
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
@MediumTest
|
||||||
|
class FrequencyChartTest : BaseViewTest() {
|
||||||
|
private lateinit var view: FrequencyChart
|
||||||
|
|
||||||
|
@Before
|
||||||
|
override fun setUp() {
|
||||||
|
super.setUp()
|
||||||
|
fixtures.purgeHabits(habitList)
|
||||||
|
val habit = fixtures.createLongHabit()
|
||||||
|
view = FrequencyChart(targetContext).apply {
|
||||||
|
setFrequency(habit.originalEntries.computeWeekdayFrequency(habit.isNumerical))
|
||||||
|
setColor(habit.color.toFixedAndroidColor())
|
||||||
|
}
|
||||||
|
measureView(view, dpToPixels(300), dpToPixels(100))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Throws(Throwable::class)
|
||||||
|
fun testRender() {
|
||||||
|
assertRenders(view, BASE_PATH + "render.png")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Throws(Throwable::class)
|
||||||
|
fun testRender_withDataOffset() {
|
||||||
|
view.onScroll(null, null, -dpToPixels(150), 0f)
|
||||||
|
view.invalidate()
|
||||||
|
assertRenders(view, BASE_PATH + "renderDataOffset.png")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Throws(Throwable::class)
|
||||||
|
fun testRender_withDifferentSize() {
|
||||||
|
measureView(view, dpToPixels(200), dpToPixels(200))
|
||||||
|
assertRenders(view, BASE_PATH + "renderDifferentSize.png")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Throws(Throwable::class)
|
||||||
|
fun testRender_withTransparentBackground() {
|
||||||
|
view.setIsBackgroundTransparent(true)
|
||||||
|
assertRenders(view, BASE_PATH + "renderTransparent.png")
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val BASE_PATH = "common/FrequencyChart/"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
|
||||||
*
|
|
||||||
* This file is part of Loop Habit Tracker.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by the
|
|
||||||
* Free Software Foundation, either version 3 of the License, or (at your
|
|
||||||
* option) any later version.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
|
||||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
|
||||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
||||||
* more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License along
|
|
||||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.isoron.uhabits.activities.common.views;
|
|
||||||
|
|
||||||
import android.graphics.*;
|
|
||||||
import androidx.test.filters.*;
|
|
||||||
import androidx.test.runner.*;
|
|
||||||
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
|
||||||
|
|
||||||
import org.isoron.uhabits.*;
|
|
||||||
import org.isoron.uhabits.utils.*;
|
|
||||||
import org.junit.*;
|
|
||||||
import org.junit.runner.*;
|
|
||||||
|
|
||||||
import java.io.*;
|
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4.class)
|
|
||||||
@MediumTest
|
|
||||||
public class RingViewTest extends BaseViewTest
|
|
||||||
{
|
|
||||||
private static final String BASE_PATH = "common/RingView/";
|
|
||||||
|
|
||||||
private RingView view;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Before
|
|
||||||
public void setUp()
|
|
||||||
{
|
|
||||||
super.setUp();
|
|
||||||
|
|
||||||
view = new RingView(targetContext);
|
|
||||||
view.setPercentage(0.6f);
|
|
||||||
view.setText("60%");
|
|
||||||
view.setColor(PaletteUtils.getAndroidTestColor(0));
|
|
||||||
view.setBackgroundColor(Color.WHITE);
|
|
||||||
view.setThickness(dpToPixels(3));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testRender_base() throws IOException
|
|
||||||
{
|
|
||||||
measureView(view, dpToPixels(100), dpToPixels(100));
|
|
||||||
assertRenders(view, BASE_PATH + "render.png");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testRender_withDifferentParams() throws IOException
|
|
||||||
{
|
|
||||||
view.setPercentage(0.25f);
|
|
||||||
view.setColor(PaletteUtils.getAndroidTestColor(5));
|
|
||||||
|
|
||||||
measureView(view, dpToPixels(200), dpToPixels(200));
|
|
||||||
assertRenders(view, BASE_PATH + "renderDifferentParams.png");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
||||||
|
*
|
||||||
|
* This file is part of Loop Habit Tracker.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by the
|
||||||
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along
|
||||||
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.isoron.uhabits.activities.common.views
|
||||||
|
|
||||||
|
import android.graphics.Color
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import androidx.test.filters.MediumTest
|
||||||
|
import org.isoron.uhabits.BaseViewTest
|
||||||
|
import org.isoron.uhabits.utils.PaletteUtils.getAndroidTestColor
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
@MediumTest
|
||||||
|
class RingViewTest : BaseViewTest() {
|
||||||
|
private lateinit var view: RingView
|
||||||
|
|
||||||
|
@Before
|
||||||
|
override fun setUp() {
|
||||||
|
super.setUp()
|
||||||
|
view = RingView(targetContext).apply {
|
||||||
|
setPercentage(0.6f)
|
||||||
|
setText("60%")
|
||||||
|
setColor(getAndroidTestColor(0))
|
||||||
|
setBackgroundColor(Color.WHITE)
|
||||||
|
setThickness(dpToPixels(3))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun testRender_base() {
|
||||||
|
measureView(view, dpToPixels(100), dpToPixels(100))
|
||||||
|
assertRenders(view, BASE_PATH + "render.png")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun testRender_withDifferentParams() {
|
||||||
|
view.setPercentage(0.25f)
|
||||||
|
view.setColor(getAndroidTestColor(5))
|
||||||
|
measureView(view, dpToPixels(200), dpToPixels(200))
|
||||||
|
assertRenders(view, BASE_PATH + "renderDifferentParams.png")
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val BASE_PATH = "common/RingView/"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
|
||||||
*
|
|
||||||
* This file is part of Loop Habit Tracker.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by the
|
|
||||||
* Free Software Foundation, either version 3 of the License, or (at your
|
|
||||||
* option) any later version.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
|
||||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
|
||||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
||||||
* more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License along
|
|
||||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.isoron.uhabits.activities.common.views;
|
|
||||||
|
|
||||||
import androidx.test.ext.junit.runners.*;
|
|
||||||
import androidx.test.filters.*;
|
|
||||||
|
|
||||||
import org.isoron.uhabits.*;
|
|
||||||
import org.isoron.uhabits.core.models.*;
|
|
||||||
import org.isoron.uhabits.core.ui.screens.habits.show.views.*;
|
|
||||||
import org.isoron.uhabits.utils.*;
|
|
||||||
import org.junit.*;
|
|
||||||
import org.junit.runner.*;
|
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4.class)
|
|
||||||
@MediumTest
|
|
||||||
public class ScoreChartTest extends BaseViewTest
|
|
||||||
{
|
|
||||||
private static final String BASE_PATH = "common/ScoreChart/";
|
|
||||||
|
|
||||||
private Habit habit;
|
|
||||||
|
|
||||||
private ScoreChart view;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Before
|
|
||||||
public void setUp()
|
|
||||||
{
|
|
||||||
super.setUp();
|
|
||||||
|
|
||||||
fixtures.purgeHabits(habitList);
|
|
||||||
habit = fixtures.createLongHabit();
|
|
||||||
ScoreCardState state = ScoreCardPresenter.Companion.buildState(habit, prefs.getFirstWeekdayInt(), 0);
|
|
||||||
|
|
||||||
view = new ScoreChart(targetContext);
|
|
||||||
view.setScores(state.getScores());
|
|
||||||
view.setColor(PaletteUtilsKt.toFixedAndroidColor(state.getColor()));
|
|
||||||
view.setBucketSize(state.getBucketSize());
|
|
||||||
measureView(view, dpToPixels(300), dpToPixels(200));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testRender() throws Throwable
|
|
||||||
{
|
|
||||||
assertRenders(view, BASE_PATH + "render.png");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testRender_withDataOffset() throws Throwable
|
|
||||||
{
|
|
||||||
view.onScroll(null, null, -dpToPixels(150), 0);
|
|
||||||
view.invalidate();
|
|
||||||
|
|
||||||
assertRenders(view, BASE_PATH + "renderDataOffset.png");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testRender_withDifferentSize() throws Throwable
|
|
||||||
{
|
|
||||||
measureView(view, dpToPixels(200), dpToPixels(200));
|
|
||||||
assertRenders(view, BASE_PATH + "renderDifferentSize.png");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testRender_withMonthlyBucket() throws Throwable
|
|
||||||
{
|
|
||||||
ScoreCardState model = ScoreCardPresenter.Companion.buildState(habit, prefs.getFirstWeekdayInt(), 2);
|
|
||||||
view.setScores(model.getScores());
|
|
||||||
view.setBucketSize(model.getBucketSize());
|
|
||||||
view.invalidate();
|
|
||||||
|
|
||||||
assertRenders(view, BASE_PATH + "renderMonthly.png");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testRender_withTransparentBackground() throws Throwable
|
|
||||||
{
|
|
||||||
view.setIsTransparencyEnabled(true);
|
|
||||||
assertRenders(view, BASE_PATH + "renderTransparent.png");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testRender_withYearlyBucket() throws Throwable
|
|
||||||
{
|
|
||||||
ScoreCardState model = ScoreCardPresenter.Companion.buildState(habit, prefs.getFirstWeekdayInt(), 4);
|
|
||||||
view.setScores(model.getScores());
|
|
||||||
view.setBucketSize(model.getBucketSize());
|
|
||||||
view.invalidate();
|
|
||||||
|
|
||||||
assertRenders(view, BASE_PATH + "renderYearly.png");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
||||||
|
*
|
||||||
|
* This file is part of Loop Habit Tracker.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by the
|
||||||
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along
|
||||||
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.isoron.uhabits.activities.common.views
|
||||||
|
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import androidx.test.filters.MediumTest
|
||||||
|
import org.isoron.uhabits.BaseViewTest
|
||||||
|
import org.isoron.uhabits.core.models.Habit
|
||||||
|
import org.isoron.uhabits.core.ui.screens.habits.show.views.ScoreCardPresenter.Companion.buildState
|
||||||
|
import org.isoron.uhabits.utils.toFixedAndroidColor
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
@MediumTest
|
||||||
|
class ScoreChartTest : BaseViewTest() {
|
||||||
|
private lateinit var habit: Habit
|
||||||
|
private lateinit var view: ScoreChart
|
||||||
|
|
||||||
|
@Before
|
||||||
|
override fun setUp() {
|
||||||
|
super.setUp()
|
||||||
|
fixtures.purgeHabits(habitList)
|
||||||
|
habit = fixtures.createLongHabit()
|
||||||
|
val state = buildState(habit, prefs.firstWeekdayInt, 0)
|
||||||
|
view = ScoreChart(targetContext).apply {
|
||||||
|
setScores(state.scores)
|
||||||
|
setColor(state.color.toFixedAndroidColor())
|
||||||
|
setBucketSize(state.bucketSize)
|
||||||
|
}
|
||||||
|
measureView(view, dpToPixels(300), dpToPixels(200))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Throws(Throwable::class)
|
||||||
|
fun testRender() {
|
||||||
|
assertRenders(view, BASE_PATH + "render.png")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Throws(Throwable::class)
|
||||||
|
fun testRender_withDataOffset() {
|
||||||
|
view.onScroll(null, null, -dpToPixels(150), 0f)
|
||||||
|
view.invalidate()
|
||||||
|
assertRenders(view, BASE_PATH + "renderDataOffset.png")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Throws(Throwable::class)
|
||||||
|
fun testRender_withDifferentSize() {
|
||||||
|
measureView(view, dpToPixels(200), dpToPixels(200))
|
||||||
|
assertRenders(view, BASE_PATH + "renderDifferentSize.png")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Throws(Throwable::class)
|
||||||
|
fun testRender_withMonthlyBucket() {
|
||||||
|
val (scores, bucketSize) = buildState(habit, prefs.firstWeekdayInt, 2)
|
||||||
|
view.setScores(scores)
|
||||||
|
view.setBucketSize(bucketSize)
|
||||||
|
view.invalidate()
|
||||||
|
assertRenders(view, BASE_PATH + "renderMonthly.png")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Throws(Throwable::class)
|
||||||
|
fun testRender_withTransparentBackground() {
|
||||||
|
view.setIsTransparencyEnabled(true)
|
||||||
|
assertRenders(view, BASE_PATH + "renderTransparent.png")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Throws(Throwable::class)
|
||||||
|
fun testRender_withYearlyBucket() {
|
||||||
|
val state = buildState(habit, prefs.firstWeekdayInt, 4)
|
||||||
|
view.setScores(state.scores)
|
||||||
|
view.setBucketSize(state.bucketSize)
|
||||||
|
view.invalidate()
|
||||||
|
assertRenders(view, BASE_PATH + "renderYearly.png")
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val BASE_PATH = "common/ScoreChart/"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
|
||||||
*
|
|
||||||
* This file is part of Loop Habit Tracker.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by the
|
|
||||||
* Free Software Foundation, either version 3 of the License, or (at your
|
|
||||||
* option) any later version.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
|
||||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
|
||||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
||||||
* more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License along
|
|
||||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.isoron.uhabits.activities.common.views;
|
|
||||||
|
|
||||||
import androidx.test.filters.*;
|
|
||||||
import androidx.test.runner.*;
|
|
||||||
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
|
||||||
|
|
||||||
import org.isoron.uhabits.*;
|
|
||||||
import org.isoron.uhabits.core.models.*;
|
|
||||||
import org.isoron.uhabits.utils.*;
|
|
||||||
import org.junit.*;
|
|
||||||
import org.junit.runner.*;
|
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4.class)
|
|
||||||
@MediumTest
|
|
||||||
public class StreakChartTest extends BaseViewTest
|
|
||||||
{
|
|
||||||
private static final String BASE_PATH = "common/StreakChart/";
|
|
||||||
|
|
||||||
private StreakChart view;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Before
|
|
||||||
public void setUp()
|
|
||||||
{
|
|
||||||
super.setUp();
|
|
||||||
|
|
||||||
fixtures.purgeHabits(habitList);
|
|
||||||
Habit habit = fixtures.createLongHabit();
|
|
||||||
|
|
||||||
view = new StreakChart(targetContext);
|
|
||||||
view.setColor(PaletteUtilsKt.toFixedAndroidColor(habit.getColor()));
|
|
||||||
view.setStreaks(habit.getStreaks().getBest(5));
|
|
||||||
measureView(view, dpToPixels(300), dpToPixels(100));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testRender() throws Throwable
|
|
||||||
{
|
|
||||||
assertRenders(view, BASE_PATH + "render.png");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testRender_withSmallSize() throws Throwable
|
|
||||||
{
|
|
||||||
measureView(view, dpToPixels(100), dpToPixels(100));
|
|
||||||
assertRenders(view, BASE_PATH + "renderSmallSize.png");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testRender_withTransparentBackground() throws Throwable
|
|
||||||
{
|
|
||||||
view.setIsBackgroundTransparent(true);
|
|
||||||
assertRenders(view, BASE_PATH + "renderTransparent.png");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
||||||
|
*
|
||||||
|
* This file is part of Loop Habit Tracker.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by the
|
||||||
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along
|
||||||
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.isoron.uhabits.activities.common.views
|
||||||
|
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import androidx.test.filters.MediumTest
|
||||||
|
import org.isoron.uhabits.BaseViewTest
|
||||||
|
import org.isoron.uhabits.utils.toFixedAndroidColor
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
@MediumTest
|
||||||
|
class StreakChartTest : BaseViewTest() {
|
||||||
|
private lateinit var view: StreakChart
|
||||||
|
@Before
|
||||||
|
override fun setUp() {
|
||||||
|
super.setUp()
|
||||||
|
fixtures.purgeHabits(habitList)
|
||||||
|
val habit = fixtures.createLongHabit()
|
||||||
|
view = StreakChart(targetContext).apply {
|
||||||
|
setColor(habit.color.toFixedAndroidColor())
|
||||||
|
setStreaks(habit.streaks.getBest(5))
|
||||||
|
}
|
||||||
|
measureView(view, dpToPixels(300), dpToPixels(100))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Throws(Throwable::class)
|
||||||
|
fun testRender() {
|
||||||
|
assertRenders(view, BASE_PATH + "render.png")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Throws(Throwable::class)
|
||||||
|
fun testRender_withSmallSize() {
|
||||||
|
measureView(view, dpToPixels(100), dpToPixels(100))
|
||||||
|
assertRenders(view, BASE_PATH + "renderSmallSize.png")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Throws(Throwable::class)
|
||||||
|
fun testRender_withTransparentBackground() {
|
||||||
|
view.setIsBackgroundTransparent(true)
|
||||||
|
assertRenders(view, BASE_PATH + "renderTransparent.png")
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val BASE_PATH = "common/StreakChart/"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
|
||||||
*
|
|
||||||
* This file is part of Loop Habit Tracker.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by the
|
|
||||||
* Free Software Foundation, either version 3 of the License, or (at your
|
|
||||||
* option) any later version.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
|
||||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
|
||||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
||||||
* more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License along
|
|
||||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.isoron.uhabits.activities.habits.list.views;
|
|
||||||
|
|
||||||
import androidx.test.filters.*;
|
|
||||||
import androidx.test.runner.*;
|
|
||||||
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
|
||||||
|
|
||||||
import org.isoron.uhabits.*;
|
|
||||||
import org.isoron.uhabits.core.preferences.*;
|
|
||||||
import org.isoron.uhabits.core.utils.*;
|
|
||||||
import org.junit.*;
|
|
||||||
import org.junit.runner.*;
|
|
||||||
|
|
||||||
import static org.mockito.Mockito.*;
|
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4.class)
|
|
||||||
@MediumTest
|
|
||||||
public class HeaderViewTest extends BaseViewTest
|
|
||||||
{
|
|
||||||
public static final String PATH = "habits/list/HeaderView/";
|
|
||||||
|
|
||||||
private HeaderView view;
|
|
||||||
|
|
||||||
private Preferences prefs;
|
|
||||||
|
|
||||||
private MidnightTimer midnightTimer;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Before
|
|
||||||
public void setUp()
|
|
||||||
{
|
|
||||||
super.setUp();
|
|
||||||
prefs = mock(Preferences.class);
|
|
||||||
midnightTimer = mock(MidnightTimer.class);
|
|
||||||
view = new HeaderView(targetContext, prefs, midnightTimer);
|
|
||||||
view.setButtonCount(5);
|
|
||||||
measureView(view, dpToPixels(600), dpToPixels(48));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testRender() throws Exception
|
|
||||||
{
|
|
||||||
when(prefs.isCheckmarkSequenceReversed()).thenReturn(false);
|
|
||||||
|
|
||||||
assertRenders(view, PATH + "render.png");
|
|
||||||
|
|
||||||
verify(prefs).isCheckmarkSequenceReversed();
|
|
||||||
verifyNoMoreInteractions(prefs);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testRender_reverse() throws Exception
|
|
||||||
{
|
|
||||||
when(prefs.isCheckmarkSequenceReversed()).thenReturn(true);
|
|
||||||
|
|
||||||
assertRenders(view, PATH + "render_reverse.png");
|
|
||||||
|
|
||||||
verify(prefs).isCheckmarkSequenceReversed();
|
|
||||||
verifyNoMoreInteractions(prefs);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
||||||
|
*
|
||||||
|
* This file is part of Loop Habit Tracker.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by the
|
||||||
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along
|
||||||
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.isoron.uhabits.activities.habits.list.views
|
||||||
|
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import androidx.test.filters.MediumTest
|
||||||
|
import com.nhaarman.mockitokotlin2.doReturn
|
||||||
|
import com.nhaarman.mockitokotlin2.mock
|
||||||
|
import com.nhaarman.mockitokotlin2.verify
|
||||||
|
import com.nhaarman.mockitokotlin2.verifyNoMoreInteractions
|
||||||
|
import com.nhaarman.mockitokotlin2.whenever
|
||||||
|
import org.isoron.uhabits.BaseViewTest
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
@MediumTest
|
||||||
|
class HeaderViewTest : BaseViewTest() {
|
||||||
|
private lateinit var view: HeaderView
|
||||||
|
|
||||||
|
@Before
|
||||||
|
override fun setUp() {
|
||||||
|
super.setUp()
|
||||||
|
prefs = mock()
|
||||||
|
view = HeaderView(targetContext, prefs, mock())
|
||||||
|
view.buttonCount = 5
|
||||||
|
measureView(view, dpToPixels(600), dpToPixels(48))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun testRender() {
|
||||||
|
whenever(prefs.isCheckmarkSequenceReversed).thenReturn(false)
|
||||||
|
assertRenders(view, PATH + "render.png")
|
||||||
|
verify(prefs).isCheckmarkSequenceReversed
|
||||||
|
verifyNoMoreInteractions(prefs)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun testRender_reverse() {
|
||||||
|
doReturn(true).whenever(prefs).isCheckmarkSequenceReversed
|
||||||
|
assertRenders(view, PATH + "render_reverse.png")
|
||||||
|
verify(prefs).isCheckmarkSequenceReversed
|
||||||
|
verifyNoMoreInteractions(prefs)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val PATH = "habits/list/HeaderView/"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
|
||||||
*
|
|
||||||
* This file is part of Loop Habit Tracker.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by the
|
|
||||||
* Free Software Foundation, either version 3 of the License, or (at your
|
|
||||||
* option) any later version.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
|
||||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
|
||||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
||||||
* more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License along
|
|
||||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.isoron.uhabits.activities.habits.list.views;
|
|
||||||
|
|
||||||
import androidx.test.filters.*;
|
|
||||||
import androidx.test.runner.*;
|
|
||||||
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
|
||||||
|
|
||||||
import org.isoron.uhabits.*;
|
|
||||||
import org.isoron.uhabits.core.ui.screens.habits.list.*;
|
|
||||||
import org.junit.*;
|
|
||||||
import org.junit.runner.*;
|
|
||||||
|
|
||||||
import static org.hamcrest.CoreMatchers.*;
|
|
||||||
import static org.hamcrest.MatcherAssert.*;
|
|
||||||
import static org.mockito.Mockito.*;
|
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4.class)
|
|
||||||
@MediumTest
|
|
||||||
public class HintViewTest extends BaseViewTest
|
|
||||||
{
|
|
||||||
public static final String PATH = "habits/list/HintView/";
|
|
||||||
|
|
||||||
private HintView view;
|
|
||||||
|
|
||||||
private HintList list;
|
|
||||||
|
|
||||||
@Before
|
|
||||||
@Override
|
|
||||||
public void setUp()
|
|
||||||
{
|
|
||||||
super.setUp();
|
|
||||||
|
|
||||||
list = mock(HintList.class);
|
|
||||||
view = new HintView(targetContext, list);
|
|
||||||
measureView(view, 400, 200);
|
|
||||||
|
|
||||||
String text =
|
|
||||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit.";
|
|
||||||
|
|
||||||
when(list.shouldShow()).thenReturn(true);
|
|
||||||
when(list.pop()).thenReturn(text);
|
|
||||||
|
|
||||||
view.showNext();
|
|
||||||
skipAnimation(view);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testRender() throws Exception
|
|
||||||
{
|
|
||||||
assertRenders(view, PATH + "render.png");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testClick() throws Exception
|
|
||||||
{
|
|
||||||
assertThat(view.getAlpha(), equalTo(1f));
|
|
||||||
view.performClick();
|
|
||||||
skipAnimation(view);
|
|
||||||
assertThat(view.getAlpha(), equalTo(0f));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
||||||
|
*
|
||||||
|
* This file is part of Loop Habit Tracker.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by the
|
||||||
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along
|
||||||
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.isoron.uhabits.activities.habits.list.views
|
||||||
|
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import androidx.test.filters.MediumTest
|
||||||
|
import com.nhaarman.mockitokotlin2.doReturn
|
||||||
|
import com.nhaarman.mockitokotlin2.mock
|
||||||
|
import com.nhaarman.mockitokotlin2.whenever
|
||||||
|
import org.hamcrest.CoreMatchers.equalTo
|
||||||
|
import org.hamcrest.MatcherAssert.assertThat
|
||||||
|
import org.isoron.uhabits.BaseViewTest
|
||||||
|
import org.isoron.uhabits.core.ui.screens.habits.list.HintList
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
@MediumTest
|
||||||
|
class HintViewTest : BaseViewTest() {
|
||||||
|
private lateinit var view: HintView
|
||||||
|
private lateinit var list: HintList
|
||||||
|
|
||||||
|
@Before
|
||||||
|
override fun setUp() {
|
||||||
|
super.setUp()
|
||||||
|
list = mock()
|
||||||
|
view = HintView(targetContext, list)
|
||||||
|
measureView(view, 400f, 200f)
|
||||||
|
val text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit."
|
||||||
|
doReturn(true).whenever(list).shouldShow()
|
||||||
|
doReturn(text).whenever(list).pop()
|
||||||
|
view.showNext()
|
||||||
|
skipAnimation(view)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun testRender() {
|
||||||
|
assertRenders(view, PATH + "render.png")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun testClick() {
|
||||||
|
assertThat(view.alpha, equalTo(1f))
|
||||||
|
view.performClick()
|
||||||
|
skipAnimation(view)
|
||||||
|
assertThat(view.alpha, equalTo(0f))
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val PATH = "habits/list/HintView/"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,7 +24,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
|
|||||||
import androidx.test.filters.MediumTest
|
import androidx.test.filters.MediumTest
|
||||||
import org.isoron.uhabits.BaseViewTest
|
import org.isoron.uhabits.BaseViewTest
|
||||||
import org.isoron.uhabits.R
|
import org.isoron.uhabits.R
|
||||||
import org.isoron.uhabits.core.ui.screens.habits.show.views.ScoreCardPresenter
|
import org.isoron.uhabits.core.ui.screens.habits.show.views.ScoreCardPresenter.Companion.buildState
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
@@ -43,13 +43,7 @@ class ScoreCardViewTest : BaseViewTest() {
|
|||||||
.from(targetContext)
|
.from(targetContext)
|
||||||
.inflate(R.layout.show_habit, null)
|
.inflate(R.layout.show_habit, null)
|
||||||
.findViewById<View>(R.id.scoreCard) as ScoreCardView
|
.findViewById<View>(R.id.scoreCard) as ScoreCardView
|
||||||
view.setState(
|
view.setState(buildState(habit = habit, firstWeekday = 0, spinnerPosition = 0))
|
||||||
ScoreCardPresenter.buildState(
|
|
||||||
habit = habit,
|
|
||||||
firstWeekday = 0,
|
|
||||||
spinnerPosition = 0,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
measureView(view, 800f, 600f)
|
measureView(view, 800f, 600f)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,59 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
|
||||||
*
|
|
||||||
* This file is part of Loop Habit Tracker.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by the
|
|
||||||
* Free Software Foundation, either version 3 of the License, or (at your
|
|
||||||
* option) any later version.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
|
||||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
|
||||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
||||||
* more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License along
|
|
||||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.isoron.uhabits.database;
|
|
||||||
|
|
||||||
import android.database.sqlite.*;
|
|
||||||
|
|
||||||
import org.isoron.uhabits.*;
|
|
||||||
import org.isoron.uhabits.core.database.*;
|
|
||||||
import org.junit.*;
|
|
||||||
|
|
||||||
import java.util.*;
|
|
||||||
|
|
||||||
import static org.hamcrest.MatcherAssert.*;
|
|
||||||
import static org.hamcrest.core.IsEqual.*;
|
|
||||||
|
|
||||||
|
|
||||||
public class AndroidDatabaseTest extends BaseAndroidTest
|
|
||||||
{
|
|
||||||
private AndroidDatabase db;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setUp()
|
|
||||||
{
|
|
||||||
super.setUp();
|
|
||||||
db = new AndroidDatabase(SQLiteDatabase.create(null), null);
|
|
||||||
db.execute("create table test(color int, name string)");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testInsert() throws Exception
|
|
||||||
{
|
|
||||||
HashMap<String, Object> map = new HashMap<>();
|
|
||||||
map.put("name", "asd");
|
|
||||||
map.put("color", null);
|
|
||||||
db.insert("test", map);
|
|
||||||
|
|
||||||
Cursor c = db.query("select * from test");
|
|
||||||
c.moveToNext();
|
|
||||||
assertNull(c.getInt(0));
|
|
||||||
assertThat(c.getString(1), equalTo("asd"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
||||||
|
*
|
||||||
|
* This file is part of Loop Habit Tracker.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by the
|
||||||
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along
|
||||||
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.isoron.uhabits.database
|
||||||
|
|
||||||
|
import android.database.sqlite.SQLiteDatabase
|
||||||
|
import org.hamcrest.MatcherAssert.assertThat
|
||||||
|
import org.hamcrest.core.IsEqual.equalTo
|
||||||
|
import org.isoron.uhabits.BaseAndroidTest
|
||||||
|
import org.isoron.uhabits.core.database.Cursor
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class AndroidDatabaseTest : BaseAndroidTest() {
|
||||||
|
private lateinit var db: AndroidDatabase
|
||||||
|
override fun setUp() {
|
||||||
|
super.setUp()
|
||||||
|
db = AndroidDatabase(SQLiteDatabase.create(null), null)
|
||||||
|
db.execute("create table test(color int, name string)")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun testInsert() {
|
||||||
|
val map = mapOf(Pair("name", "asd"), Pair("color", null))
|
||||||
|
db.insert("test", map)
|
||||||
|
val c: Cursor = db.query("select * from test")
|
||||||
|
c.moveToNext()
|
||||||
|
c.getInt(0)!!
|
||||||
|
assertThat(c.getString(1), equalTo("asd"))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -97,7 +97,7 @@ class IntentSchedulerTest : BaseAndroidTest() {
|
|||||||
|
|
||||||
val habit = habitList.getByPosition(0)
|
val habit = habitList.getByPosition(0)
|
||||||
val scheduler = appComponent.intentScheduler
|
val scheduler = appComponent.intentScheduler
|
||||||
assertThat(scheduler.scheduleShowReminder(reminderTime, habit, 0), equalTo(OK))
|
assertThat(scheduler!!.scheduleShowReminder(reminderTime, habit, 0), equalTo(OK))
|
||||||
|
|
||||||
setSystemTime("America/Chicago", 2020, JUNE, 2, 22, 44)
|
setSystemTime("America/Chicago", 2020, JUNE, 2, 22, 44)
|
||||||
assertNull(ReminderReceiver.lastReceivedIntent)
|
assertNull(ReminderReceiver.lastReceivedIntent)
|
||||||
@@ -116,7 +116,7 @@ class IntentSchedulerTest : BaseAndroidTest() {
|
|||||||
val updateTime = 1591155900000 // 2020-06-02 22:45:00 (America/Chicago)
|
val updateTime = 1591155900000 // 2020-06-02 22:45:00 (America/Chicago)
|
||||||
|
|
||||||
val scheduler = appComponent.intentScheduler
|
val scheduler = appComponent.intentScheduler
|
||||||
assertThat(scheduler.scheduleWidgetUpdate(updateTime), equalTo(OK))
|
assertThat(scheduler!!.scheduleWidgetUpdate(updateTime), equalTo(OK))
|
||||||
|
|
||||||
setSystemTime("America/Chicago", 2020, JUNE, 2, 22, 44)
|
setSystemTime("America/Chicago", 2020, JUNE, 2, 22, 44)
|
||||||
assertNull(WidgetReceiver.lastReceivedIntent)
|
assertNull(WidgetReceiver.lastReceivedIntent)
|
||||||
|
|||||||
@@ -1,78 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
|
||||||
*
|
|
||||||
* This file is part of Loop Habit Tracker.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by the
|
|
||||||
* Free Software Foundation, either version 3 of the License, or (at your
|
|
||||||
* option) any later version.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
|
||||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
|
||||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
||||||
* more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License along
|
|
||||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.isoron.uhabits.performance;
|
|
||||||
|
|
||||||
import androidx.test.ext.junit.runners.*;
|
|
||||||
import androidx.test.filters.*;
|
|
||||||
|
|
||||||
import org.isoron.uhabits.*;
|
|
||||||
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.junit.*;
|
|
||||||
import org.junit.runner.*;
|
|
||||||
|
|
||||||
import static org.isoron.uhabits.core.models.Timestamp.*;
|
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4.class)
|
|
||||||
@MediumTest
|
|
||||||
public class PerformanceTest extends BaseAndroidTest
|
|
||||||
{
|
|
||||||
private Habit habit;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setUp()
|
|
||||||
{
|
|
||||||
super.setUp();
|
|
||||||
habit = fixtures.createLongHabit();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Ignore
|
|
||||||
@Test(timeout = 5000)
|
|
||||||
public void benchmarkCreateHabitCommand()
|
|
||||||
{
|
|
||||||
Database db = ((SQLModelFactory) modelFactory).getDatabase();
|
|
||||||
db.beginTransaction();
|
|
||||||
for (int i = 0; i < 1_000; i++)
|
|
||||||
{
|
|
||||||
Habit model = modelFactory.buildHabit();
|
|
||||||
new CreateHabitCommand(modelFactory, habitList, model).run();
|
|
||||||
}
|
|
||||||
db.setTransactionSuccessful();
|
|
||||||
db.endTransaction();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Ignore
|
|
||||||
@Test(timeout = 5000)
|
|
||||||
public void benchmarkCreateRepetitionCommand()
|
|
||||||
{
|
|
||||||
Database db = ((SQLModelFactory) modelFactory).getDatabase();
|
|
||||||
db.beginTransaction();
|
|
||||||
Habit habit = fixtures.createEmptyHabit();
|
|
||||||
for (int i = 0; i < 5_000; i++)
|
|
||||||
{
|
|
||||||
Timestamp timestamp = new Timestamp(i * DAY_LENGTH);
|
|
||||||
new CreateRepetitionCommand(habitList, habit, timestamp, 1).run();
|
|
||||||
}
|
|
||||||
db.setTransactionSuccessful();
|
|
||||||
db.endTransaction();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
||||||
|
*
|
||||||
|
* This file is part of Loop Habit Tracker.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by the
|
||||||
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along
|
||||||
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.isoron.uhabits.performance
|
||||||
|
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import androidx.test.filters.MediumTest
|
||||||
|
import org.isoron.uhabits.BaseAndroidTest
|
||||||
|
import org.isoron.uhabits.core.commands.CreateHabitCommand
|
||||||
|
import org.isoron.uhabits.core.commands.CreateRepetitionCommand
|
||||||
|
import org.isoron.uhabits.core.models.Habit
|
||||||
|
import org.isoron.uhabits.core.models.Timestamp
|
||||||
|
import org.isoron.uhabits.core.models.Timestamp.Companion.DAY_LENGTH
|
||||||
|
import org.isoron.uhabits.core.models.sqlite.SQLModelFactory
|
||||||
|
import org.junit.Ignore
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
@MediumTest
|
||||||
|
class PerformanceTest : BaseAndroidTest() {
|
||||||
|
private var habit: Habit? = null
|
||||||
|
override fun setUp() {
|
||||||
|
super.setUp()
|
||||||
|
habit = fixtures.createLongHabit()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
@Test(timeout = 5000)
|
||||||
|
fun benchmarkCreateHabitCommand() {
|
||||||
|
val db = (modelFactory as SQLModelFactory).database
|
||||||
|
db.beginTransaction()
|
||||||
|
for (i in 0..999) {
|
||||||
|
val model = modelFactory.buildHabit()
|
||||||
|
CreateHabitCommand(modelFactory, habitList, model).run()
|
||||||
|
}
|
||||||
|
db.setTransactionSuccessful()
|
||||||
|
db.endTransaction()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
@Test(timeout = 5000)
|
||||||
|
fun benchmarkCreateRepetitionCommand() {
|
||||||
|
val db = (modelFactory as SQLModelFactory).database
|
||||||
|
db.beginTransaction()
|
||||||
|
val habit = fixtures.createEmptyHabit()
|
||||||
|
for (i in 0..4999) {
|
||||||
|
val timestamp: Timestamp = Timestamp(i * DAY_LENGTH)
|
||||||
|
CreateRepetitionCommand(habitList, habit, timestamp, 1).run()
|
||||||
|
}
|
||||||
|
db.setTransactionSuccessful()
|
||||||
|
db.endTransaction()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
|
||||||
*
|
|
||||||
* This file is part of Loop Habit Tracker.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by the
|
|
||||||
* Free Software Foundation, either version 3 of the License, or (at your
|
|
||||||
* option) any later version.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
|
||||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
|
||||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
||||||
* more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License along
|
|
||||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.isoron.uhabits.widgets;
|
|
||||||
|
|
||||||
import android.widget.*;
|
|
||||||
|
|
||||||
import androidx.test.ext.junit.runners.*;
|
|
||||||
import androidx.test.filters.*;
|
|
||||||
|
|
||||||
import org.isoron.uhabits.*;
|
|
||||||
import org.isoron.uhabits.core.models.*;
|
|
||||||
import org.isoron.uhabits.core.utils.*;
|
|
||||||
import org.junit.*;
|
|
||||||
import org.junit.runner.*;
|
|
||||||
|
|
||||||
import static org.hamcrest.CoreMatchers.*;
|
|
||||||
import static org.hamcrest.MatcherAssert.*;
|
|
||||||
import static org.isoron.uhabits.core.models.Entry.*;
|
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4.class)
|
|
||||||
@MediumTest
|
|
||||||
public class CheckmarkWidgetTest extends BaseViewTest
|
|
||||||
{
|
|
||||||
private static final String PATH = "widgets/CheckmarkWidget/";
|
|
||||||
|
|
||||||
private Habit habit;
|
|
||||||
|
|
||||||
private EntryList entries;
|
|
||||||
|
|
||||||
private FrameLayout view;
|
|
||||||
|
|
||||||
private Timestamp today = DateUtils.getTodayWithOffset();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setUp()
|
|
||||||
{
|
|
||||||
super.setUp();
|
|
||||||
setTheme(R.style.WidgetTheme);
|
|
||||||
prefs.setWidgetOpacity(255);
|
|
||||||
prefs.setSkipEnabled(true);
|
|
||||||
|
|
||||||
habit = fixtures.createVeryLongHabit();
|
|
||||||
entries = habit.getComputedEntries();
|
|
||||||
CheckmarkWidget widget = new CheckmarkWidget(targetContext, 0, habit);
|
|
||||||
view = convertToView(widget, 150, 200);
|
|
||||||
|
|
||||||
assertThat(entries.get(today).getValue(), equalTo(YES_MANUAL));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testClick() throws Exception
|
|
||||||
{
|
|
||||||
Button button = (Button) view.findViewById(R.id.button);
|
|
||||||
assertThat(button, is(not(nullValue())));
|
|
||||||
|
|
||||||
// A better test would be to capture the intent, but it doesn't seem
|
|
||||||
// possible to capture intents sent to BroadcastReceivers.
|
|
||||||
button.performClick();
|
|
||||||
sleep(1000);
|
|
||||||
assertThat(entries.get(today).getValue(), equalTo(SKIP));
|
|
||||||
|
|
||||||
button.performClick();
|
|
||||||
sleep(1000);
|
|
||||||
assertThat(entries.get(today).getValue(), equalTo(NO));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testIsInstalled()
|
|
||||||
{
|
|
||||||
assertWidgetProviderIsInstalled(CheckmarkWidgetProvider.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testRender() throws Exception
|
|
||||||
{
|
|
||||||
assertRenders(view, PATH + "render.png");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
||||||
|
*
|
||||||
|
* This file is part of Loop Habit Tracker.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by the
|
||||||
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along
|
||||||
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.isoron.uhabits.widgets
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.Button
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import androidx.test.filters.MediumTest
|
||||||
|
import org.hamcrest.CoreMatchers
|
||||||
|
import org.hamcrest.CoreMatchers.`is`
|
||||||
|
import org.hamcrest.CoreMatchers.equalTo
|
||||||
|
import org.hamcrest.MatcherAssert.assertThat
|
||||||
|
import org.isoron.uhabits.BaseViewTest
|
||||||
|
import org.isoron.uhabits.R
|
||||||
|
import org.isoron.uhabits.core.models.Entry
|
||||||
|
import org.isoron.uhabits.core.models.EntryList
|
||||||
|
import org.isoron.uhabits.core.models.Habit
|
||||||
|
import org.isoron.uhabits.core.utils.DateUtils.Companion.getTodayWithOffset
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
@MediumTest
|
||||||
|
class CheckmarkWidgetTest : BaseViewTest() {
|
||||||
|
private lateinit var habit: Habit
|
||||||
|
private lateinit var entries: EntryList
|
||||||
|
private lateinit var view: FrameLayout
|
||||||
|
private val today = getTodayWithOffset()
|
||||||
|
override fun setUp() {
|
||||||
|
super.setUp()
|
||||||
|
setTheme(R.style.WidgetTheme)
|
||||||
|
prefs.widgetOpacity = 255
|
||||||
|
prefs.isSkipEnabled = true
|
||||||
|
habit = fixtures.createVeryLongHabit()
|
||||||
|
entries = habit.computedEntries
|
||||||
|
val widget = CheckmarkWidget(targetContext, 0, habit)
|
||||||
|
view = convertToView(widget, 150, 200)
|
||||||
|
assertThat(entries.get(today).value, equalTo(Entry.YES_MANUAL))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun testClick() {
|
||||||
|
val button = view.findViewById<View>(R.id.button) as Button
|
||||||
|
assertThat(
|
||||||
|
button,
|
||||||
|
`is`(CoreMatchers.not(CoreMatchers.nullValue()))
|
||||||
|
)
|
||||||
|
|
||||||
|
// A better test would be to capture the intent, but it doesn't seem
|
||||||
|
// possible to capture intents sent to BroadcastReceivers.
|
||||||
|
button.performClick()
|
||||||
|
sleep(1000)
|
||||||
|
assertThat(entries.get(today).value, equalTo(Entry.SKIP))
|
||||||
|
button.performClick()
|
||||||
|
sleep(1000)
|
||||||
|
assertThat(entries.get(today).value, equalTo(Entry.NO))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testIsInstalled() {
|
||||||
|
assertWidgetProviderIsInstalled(CheckmarkWidgetProvider::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun testRender() {
|
||||||
|
assertRenders(view, PATH + "render.png")
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val PATH = "widgets/CheckmarkWidget/"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
|
||||||
*
|
|
||||||
* This file is part of Loop Habit Tracker.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by the
|
|
||||||
* Free Software Foundation, either version 3 of the License, or (at your
|
|
||||||
* option) any later version.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
|
||||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
|
||||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
||||||
* more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License along
|
|
||||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.isoron.uhabits.widgets;
|
|
||||||
|
|
||||||
import android.widget.*;
|
|
||||||
|
|
||||||
import androidx.test.ext.junit.runners.*;
|
|
||||||
import androidx.test.filters.*;
|
|
||||||
|
|
||||||
import org.isoron.uhabits.*;
|
|
||||||
import org.isoron.uhabits.core.models.*;
|
|
||||||
import org.junit.*;
|
|
||||||
import org.junit.runner.*;
|
|
||||||
|
|
||||||
import java.util.*;
|
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4.class)
|
|
||||||
@MediumTest
|
|
||||||
public class FrequencyWidgetTest extends BaseViewTest
|
|
||||||
{
|
|
||||||
private static final String PATH = "widgets/FrequencyWidget/";
|
|
||||||
|
|
||||||
private Habit habit;
|
|
||||||
|
|
||||||
private FrameLayout view;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setUp()
|
|
||||||
{
|
|
||||||
super.setUp();
|
|
||||||
setTheme(R.style.WidgetTheme);
|
|
||||||
prefs.setWidgetOpacity(255);
|
|
||||||
|
|
||||||
habit = fixtures.createVeryLongHabit();
|
|
||||||
FrequencyWidget widget = new FrequencyWidget(targetContext, 0, habit, Calendar.SUNDAY);
|
|
||||||
view = convertToView(widget, 400, 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testIsInstalled()
|
|
||||||
{
|
|
||||||
assertWidgetProviderIsInstalled(FrequencyWidgetProvider.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testRender() throws Exception
|
|
||||||
{
|
|
||||||
assertRenders(view, PATH + "render.png");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
||||||
|
*
|
||||||
|
* This file is part of Loop Habit Tracker.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by the
|
||||||
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along
|
||||||
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.isoron.uhabits.widgets
|
||||||
|
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import androidx.test.filters.MediumTest
|
||||||
|
import org.isoron.uhabits.BaseViewTest
|
||||||
|
import org.isoron.uhabits.R
|
||||||
|
import org.isoron.uhabits.core.models.Habit
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import java.util.Calendar
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
@MediumTest
|
||||||
|
class FrequencyWidgetTest : BaseViewTest() {
|
||||||
|
private lateinit var habit: Habit
|
||||||
|
private lateinit var view: FrameLayout
|
||||||
|
override fun setUp() {
|
||||||
|
super.setUp()
|
||||||
|
setTheme(R.style.WidgetTheme)
|
||||||
|
prefs.widgetOpacity = 255
|
||||||
|
habit = fixtures.createVeryLongHabit()
|
||||||
|
val widget = FrequencyWidget(targetContext, 0, habit, Calendar.SUNDAY)
|
||||||
|
view = convertToView(widget, 400, 400)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testIsInstalled() {
|
||||||
|
assertWidgetProviderIsInstalled(FrequencyWidgetProvider::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun testRender() {
|
||||||
|
assertRenders(view, PATH + "render.png")
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val PATH = "widgets/FrequencyWidget/"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
|
||||||
*
|
|
||||||
* This file is part of Loop Habit Tracker.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by the
|
|
||||||
* Free Software Foundation, either version 3 of the License, or (at your
|
|
||||||
* option) any later version.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
|
||||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
|
||||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
||||||
* more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License along
|
|
||||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.isoron.uhabits.widgets;
|
|
||||||
|
|
||||||
import android.widget.*;
|
|
||||||
|
|
||||||
import androidx.test.ext.junit.runners.*;
|
|
||||||
import androidx.test.filters.*;
|
|
||||||
|
|
||||||
import org.isoron.uhabits.*;
|
|
||||||
import org.isoron.uhabits.core.models.*;
|
|
||||||
import org.junit.*;
|
|
||||||
import org.junit.runner.*;
|
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4.class)
|
|
||||||
@MediumTest
|
|
||||||
public class HistoryWidgetTest extends BaseViewTest
|
|
||||||
{
|
|
||||||
private static final String PATH = "widgets/HistoryWidget/";
|
|
||||||
|
|
||||||
private Habit habit;
|
|
||||||
|
|
||||||
private FrameLayout view;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setUp()
|
|
||||||
{
|
|
||||||
super.setUp();
|
|
||||||
setTheme(R.style.WidgetTheme);
|
|
||||||
prefs.setWidgetOpacity(255);
|
|
||||||
|
|
||||||
habit = fixtures.createVeryLongHabit();
|
|
||||||
HistoryWidget widget = new HistoryWidget(targetContext, 0, habit);
|
|
||||||
view = convertToView(widget, 400, 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testIsInstalled()
|
|
||||||
{
|
|
||||||
assertWidgetProviderIsInstalled(HistoryWidgetProvider.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testRender() throws Exception
|
|
||||||
{
|
|
||||||
assertRenders(view, PATH + "render.png");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
||||||
|
*
|
||||||
|
* This file is part of Loop Habit Tracker.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by the
|
||||||
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along
|
||||||
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.isoron.uhabits.widgets
|
||||||
|
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import androidx.test.filters.MediumTest
|
||||||
|
import org.isoron.uhabits.BaseViewTest
|
||||||
|
import org.isoron.uhabits.R
|
||||||
|
import org.isoron.uhabits.core.models.Habit
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
@MediumTest
|
||||||
|
class HistoryWidgetTest : BaseViewTest() {
|
||||||
|
private lateinit var habit: Habit
|
||||||
|
private lateinit var view: FrameLayout
|
||||||
|
override fun setUp() {
|
||||||
|
super.setUp()
|
||||||
|
setTheme(R.style.WidgetTheme)
|
||||||
|
prefs.widgetOpacity = 255
|
||||||
|
habit = fixtures.createVeryLongHabit()
|
||||||
|
val widget = HistoryWidget(targetContext, 0, habit)
|
||||||
|
view = convertToView(widget, 400, 400)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testIsInstalled() {
|
||||||
|
assertWidgetProviderIsInstalled(HistoryWidgetProvider::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun testRender() {
|
||||||
|
assertRenders(view, PATH + "render.png")
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val PATH = "widgets/HistoryWidget/"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
|
||||||
*
|
|
||||||
* This file is part of Loop Habit Tracker.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by the
|
|
||||||
* Free Software Foundation, either version 3 of the License, or (at your
|
|
||||||
* option) any later version.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
|
||||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
|
||||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
||||||
* more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License along
|
|
||||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.isoron.uhabits.widgets;
|
|
||||||
|
|
||||||
import android.widget.*;
|
|
||||||
|
|
||||||
import androidx.test.ext.junit.runners.*;
|
|
||||||
import androidx.test.filters.*;
|
|
||||||
|
|
||||||
import org.isoron.uhabits.*;
|
|
||||||
import org.isoron.uhabits.core.models.*;
|
|
||||||
import org.junit.*;
|
|
||||||
import org.junit.runner.*;
|
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4.class)
|
|
||||||
@MediumTest
|
|
||||||
public class ScoreWidgetTest extends BaseViewTest
|
|
||||||
{
|
|
||||||
private static final String PATH = "widgets/ScoreWidget/";
|
|
||||||
|
|
||||||
private Habit habit;
|
|
||||||
|
|
||||||
private FrameLayout view;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setUp()
|
|
||||||
{
|
|
||||||
super.setUp();
|
|
||||||
setTheme(R.style.WidgetTheme);
|
|
||||||
prefs.setWidgetOpacity(255);
|
|
||||||
|
|
||||||
habit = fixtures.createVeryLongHabit();
|
|
||||||
ScoreWidget widget = new ScoreWidget(targetContext, 0, habit);
|
|
||||||
view = convertToView(widget, 400, 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testIsInstalled()
|
|
||||||
{
|
|
||||||
assertWidgetProviderIsInstalled(ScoreWidgetProvider.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testRender() throws Exception
|
|
||||||
{
|
|
||||||
assertRenders(view, PATH + "render.png");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
||||||
|
*
|
||||||
|
* This file is part of Loop Habit Tracker.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by the
|
||||||
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along
|
||||||
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.isoron.uhabits.widgets
|
||||||
|
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import androidx.test.filters.MediumTest
|
||||||
|
import org.isoron.uhabits.BaseViewTest
|
||||||
|
import org.isoron.uhabits.R
|
||||||
|
import org.isoron.uhabits.core.models.Habit
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
@MediumTest
|
||||||
|
class ScoreWidgetTest : BaseViewTest() {
|
||||||
|
private lateinit var habit: Habit
|
||||||
|
private lateinit var view: FrameLayout
|
||||||
|
override fun setUp() {
|
||||||
|
super.setUp()
|
||||||
|
setTheme(R.style.WidgetTheme)
|
||||||
|
prefs.widgetOpacity = 255
|
||||||
|
habit = fixtures.createVeryLongHabit()
|
||||||
|
val widget = ScoreWidget(targetContext, 0, habit)
|
||||||
|
view = convertToView(widget, 400, 400)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testIsInstalled() {
|
||||||
|
assertWidgetProviderIsInstalled(ScoreWidgetProvider::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun testRender() {
|
||||||
|
assertRenders(view, PATH + "render.png")
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val PATH = "widgets/ScoreWidget/"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
|
||||||
*
|
|
||||||
* This file is part of Loop Habit Tracker.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by the
|
|
||||||
* Free Software Foundation, either version 3 of the License, or (at your
|
|
||||||
* option) any later version.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
|
||||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
|
||||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
||||||
* more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License along
|
|
||||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.isoron.uhabits.widgets;
|
|
||||||
|
|
||||||
import android.widget.*;
|
|
||||||
|
|
||||||
import androidx.test.ext.junit.runners.*;
|
|
||||||
import androidx.test.filters.*;
|
|
||||||
|
|
||||||
import org.isoron.uhabits.*;
|
|
||||||
import org.isoron.uhabits.core.models.*;
|
|
||||||
import org.junit.*;
|
|
||||||
import org.junit.runner.*;
|
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4.class)
|
|
||||||
@MediumTest
|
|
||||||
public class StreakWidgetTest extends BaseViewTest
|
|
||||||
{
|
|
||||||
private static final String PATH = "widgets/StreakWidget/";
|
|
||||||
|
|
||||||
private Habit habit;
|
|
||||||
|
|
||||||
private FrameLayout view;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setUp()
|
|
||||||
{
|
|
||||||
super.setUp();
|
|
||||||
setTheme(R.style.WidgetTheme);
|
|
||||||
prefs.setWidgetOpacity(255);
|
|
||||||
|
|
||||||
habit = fixtures.createVeryLongHabit();
|
|
||||||
StreakWidget widget = new StreakWidget(targetContext, 0, habit);
|
|
||||||
view = convertToView(widget, 400, 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testIsInstalled()
|
|
||||||
{
|
|
||||||
assertWidgetProviderIsInstalled(StreakWidgetProvider.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testRender() throws Exception
|
|
||||||
{
|
|
||||||
assertRenders(view, PATH + "render.png");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
||||||
|
*
|
||||||
|
* This file is part of Loop Habit Tracker.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by the
|
||||||
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along
|
||||||
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.isoron.uhabits.widgets
|
||||||
|
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import androidx.test.filters.MediumTest
|
||||||
|
import org.isoron.uhabits.BaseViewTest
|
||||||
|
import org.isoron.uhabits.R
|
||||||
|
import org.isoron.uhabits.core.models.Habit
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
@MediumTest
|
||||||
|
class StreakWidgetTest : BaseViewTest() {
|
||||||
|
private lateinit var habit: Habit
|
||||||
|
private lateinit var view: FrameLayout
|
||||||
|
override fun setUp() {
|
||||||
|
super.setUp()
|
||||||
|
setTheme(R.style.WidgetTheme)
|
||||||
|
prefs.widgetOpacity = 255
|
||||||
|
habit = fixtures.createVeryLongHabit()
|
||||||
|
val widget = StreakWidget(targetContext, 0, habit)
|
||||||
|
view = convertToView(widget, 400, 400)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testIsInstalled() {
|
||||||
|
assertWidgetProviderIsInstalled(StreakWidgetProvider::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun testRender() {
|
||||||
|
assertRenders(view, PATH + "render.png")
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val PATH = "widgets/StreakWidget/"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
|
||||||
*
|
|
||||||
* This file is part of Loop Habit Tracker.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by the
|
|
||||||
* Free Software Foundation, either version 3 of the License, or (at your
|
|
||||||
* option) any later version.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
|
||||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
|
||||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
||||||
* more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License along
|
|
||||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.isoron.uhabits.widgets;
|
|
||||||
|
|
||||||
import android.widget.*;
|
|
||||||
|
|
||||||
import androidx.test.ext.junit.runners.*;
|
|
||||||
import androidx.test.filters.*;
|
|
||||||
|
|
||||||
import org.isoron.uhabits.*;
|
|
||||||
import org.isoron.uhabits.core.models.*;
|
|
||||||
import org.junit.*;
|
|
||||||
import org.junit.runner.*;
|
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4.class)
|
|
||||||
@MediumTest
|
|
||||||
public class TargetWidgetTest extends BaseViewTest
|
|
||||||
{
|
|
||||||
private static final String PATH = "widgets/TargetWidget/";
|
|
||||||
|
|
||||||
private Habit habit;
|
|
||||||
|
|
||||||
private FrameLayout view;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setUp()
|
|
||||||
{
|
|
||||||
super.setUp();
|
|
||||||
setTheme(R.style.WidgetTheme);
|
|
||||||
prefs.setWidgetOpacity(255);
|
|
||||||
|
|
||||||
habit = fixtures.createLongNumericalHabit();
|
|
||||||
habit.setColor(new PaletteColor(11));
|
|
||||||
habit.setFrequency(Frequency.WEEKLY);
|
|
||||||
habit.recompute();
|
|
||||||
TargetWidget widget = new TargetWidget(targetContext, 0, habit);
|
|
||||||
view = convertToView(widget, 400, 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testIsInstalled()
|
|
||||||
{
|
|
||||||
assertWidgetProviderIsInstalled(TargetWidgetProvider.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testRender() throws Exception
|
|
||||||
{
|
|
||||||
assertRenders(view, PATH + "render.png");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
||||||
|
*
|
||||||
|
* This file is part of Loop Habit Tracker.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by the
|
||||||
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along
|
||||||
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.isoron.uhabits.widgets
|
||||||
|
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import androidx.test.filters.MediumTest
|
||||||
|
import org.isoron.uhabits.BaseViewTest
|
||||||
|
import org.isoron.uhabits.R
|
||||||
|
import org.isoron.uhabits.core.models.Frequency
|
||||||
|
import org.isoron.uhabits.core.models.Habit
|
||||||
|
import org.isoron.uhabits.core.models.PaletteColor
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
@MediumTest
|
||||||
|
class TargetWidgetTest : BaseViewTest() {
|
||||||
|
private lateinit var habit: Habit
|
||||||
|
private lateinit var view: FrameLayout
|
||||||
|
override fun setUp() {
|
||||||
|
super.setUp()
|
||||||
|
setTheme(R.style.WidgetTheme)
|
||||||
|
prefs.widgetOpacity = 255
|
||||||
|
habit = fixtures.createLongNumericalHabit().apply {
|
||||||
|
color = PaletteColor(11)
|
||||||
|
frequency = Frequency.WEEKLY
|
||||||
|
recompute()
|
||||||
|
}
|
||||||
|
val widget = TargetWidget(targetContext, 0, habit)
|
||||||
|
view = convertToView(widget, 400, 400)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testIsInstalled() {
|
||||||
|
assertWidgetProviderIsInstalled(TargetWidgetProvider::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun testRender() {
|
||||||
|
assertRenders(view, PATH + "render.png")
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val PATH = "widgets/TargetWidget/"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
|
||||||
*
|
|
||||||
* This file is part of Loop Habit Tracker.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by the
|
|
||||||
* Free Software Foundation, either version 3 of the License, or (at your
|
|
||||||
* option) any later version.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
|
||||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
|
||||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
||||||
* more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License along
|
|
||||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.isoron.uhabits.widgets.views;
|
|
||||||
|
|
||||||
import androidx.test.ext.junit.runners.*;
|
|
||||||
import androidx.test.filters.*;
|
|
||||||
|
|
||||||
import org.isoron.uhabits.*;
|
|
||||||
import org.isoron.uhabits.core.models.*;
|
|
||||||
import org.isoron.uhabits.core.utils.*;
|
|
||||||
import org.isoron.uhabits.utils.*;
|
|
||||||
import org.junit.*;
|
|
||||||
import org.junit.runner.*;
|
|
||||||
|
|
||||||
import java.io.*;
|
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4.class)
|
|
||||||
@MediumTest
|
|
||||||
public class CheckmarkWidgetViewTest extends BaseViewTest
|
|
||||||
{
|
|
||||||
private static final String PATH = "widgets/CheckmarkWidgetView/";
|
|
||||||
|
|
||||||
private CheckmarkWidgetView view;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Before
|
|
||||||
public void setUp()
|
|
||||||
{
|
|
||||||
super.setUp();
|
|
||||||
setTheme(R.style.WidgetTheme);
|
|
||||||
|
|
||||||
Habit habit = fixtures.createShortHabit();
|
|
||||||
Timestamp today = DateUtils.getTodayWithOffset();
|
|
||||||
|
|
||||||
view = new CheckmarkWidgetView(targetContext);
|
|
||||||
double score = habit.getScores().get(today).getValue();
|
|
||||||
float percentage = (float) score;
|
|
||||||
|
|
||||||
view.setActiveColor(PaletteUtils.getAndroidTestColor(0));
|
|
||||||
view.setEntryState(habit.getComputedEntries().get(today).getValue());
|
|
||||||
view.setEntryValue(habit.getComputedEntries().get(today).getValue());
|
|
||||||
view.setPercentage(percentage);
|
|
||||||
view.setName(habit.getName());
|
|
||||||
view.refresh();
|
|
||||||
measureView(view, dpToPixels(100), dpToPixels(200));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testRender_checked() throws IOException
|
|
||||||
{
|
|
||||||
assertRenders(view, PATH + "checked.png");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testRender_largeSize() throws IOException
|
|
||||||
{
|
|
||||||
measureView(view, dpToPixels(300), dpToPixels(300));
|
|
||||||
assertRenders(view, PATH + "large_size.png");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
||||||
|
*
|
||||||
|
* This file is part of Loop Habit Tracker.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by the
|
||||||
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along
|
||||||
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.isoron.uhabits.widgets.views
|
||||||
|
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import androidx.test.filters.MediumTest
|
||||||
|
import org.isoron.uhabits.BaseViewTest
|
||||||
|
import org.isoron.uhabits.R
|
||||||
|
import org.isoron.uhabits.core.utils.DateUtils.Companion.getTodayWithOffset
|
||||||
|
import org.isoron.uhabits.utils.PaletteUtils.getAndroidTestColor
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
@MediumTest
|
||||||
|
class CheckmarkWidgetViewTest : BaseViewTest() {
|
||||||
|
private lateinit var view: CheckmarkWidgetView
|
||||||
|
@Before
|
||||||
|
override fun setUp() {
|
||||||
|
super.setUp()
|
||||||
|
setTheme(R.style.WidgetTheme)
|
||||||
|
val habit = fixtures.createShortHabit()
|
||||||
|
val computedEntries = habit.computedEntries
|
||||||
|
val scores = habit.scores
|
||||||
|
val today = getTodayWithOffset()
|
||||||
|
val score = scores[today].value
|
||||||
|
view = CheckmarkWidgetView(targetContext).apply {
|
||||||
|
activeColor = getAndroidTestColor(0)
|
||||||
|
entryState = computedEntries.get(today).value
|
||||||
|
entryValue = computedEntries.get(today).value
|
||||||
|
percentage = score.toFloat()
|
||||||
|
name = habit.name
|
||||||
|
}
|
||||||
|
view.refresh()
|
||||||
|
measureView(view, dpToPixels(100), dpToPixels(200))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun testRender_checked() {
|
||||||
|
assertRenders(view, PATH + "checked.png")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun testRender_largeSize() {
|
||||||
|
measureView(view, dpToPixels(300), dpToPixels(300))
|
||||||
|
assertRenders(view, PATH + "large_size.png")
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val PATH = "widgets/CheckmarkWidgetView/"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -41,7 +41,7 @@ constructor(
|
|||||||
preferences: Preferences,
|
preferences: Preferences,
|
||||||
) : ThemeSwitcher(preferences) {
|
) : ThemeSwitcher(preferences) {
|
||||||
|
|
||||||
private var currentTheme: Theme = LightTheme()
|
override var currentTheme: Theme = LightTheme()
|
||||||
|
|
||||||
override fun getSystemTheme(): Int {
|
override fun getSystemTheme(): Int {
|
||||||
if (SDK_INT < 29) return THEME_LIGHT
|
if (SDK_INT < 29) return THEME_LIGHT
|
||||||
@@ -53,10 +53,6 @@ constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getCurrentTheme(): Theme {
|
|
||||||
return currentTheme
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun applyDarkTheme() {
|
override fun applyDarkTheme() {
|
||||||
currentTheme = DarkTheme()
|
currentTheme = DarkTheme()
|
||||||
context.setTheme(R.style.AppBaseThemeDark)
|
context.setTheme(R.style.AppBaseThemeDark)
|
||||||
|
|||||||
@@ -1,71 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
|
||||||
*
|
|
||||||
* This file is part of Loop Habit Tracker.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by the
|
|
||||||
* Free Software Foundation, either version 3 of the License, or (at your
|
|
||||||
* option) any later version.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
|
||||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
|
||||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
||||||
* more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License along
|
|
||||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.isoron.uhabits.activities.common.views;
|
|
||||||
|
|
||||||
import android.os.*;
|
|
||||||
|
|
||||||
import androidx.customview.view.*;
|
|
||||||
|
|
||||||
public class BundleSavedState extends AbsSavedState
|
|
||||||
{
|
|
||||||
public static final Parcelable.Creator<BundleSavedState> CREATOR =
|
|
||||||
new ClassLoaderCreator<BundleSavedState>()
|
|
||||||
{
|
|
||||||
@Override
|
|
||||||
public BundleSavedState createFromParcel(Parcel source,
|
|
||||||
ClassLoader loader)
|
|
||||||
{
|
|
||||||
return new BundleSavedState(source, loader);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public BundleSavedState createFromParcel(Parcel source)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@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,62 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
||||||
|
*
|
||||||
|
* This file is part of Loop Habit Tracker.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by the
|
||||||
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along
|
||||||
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.isoron.uhabits.activities.common.views
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Parcel
|
||||||
|
import android.os.Parcelable
|
||||||
|
import android.os.Parcelable.ClassLoaderCreator
|
||||||
|
import androidx.customview.view.AbsSavedState
|
||||||
|
|
||||||
|
class BundleSavedState : AbsSavedState {
|
||||||
|
@JvmField val bundle: Bundle?
|
||||||
|
|
||||||
|
constructor(superState: Parcelable?, bundle: Bundle?) : super(superState!!) {
|
||||||
|
this.bundle = bundle
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(source: Parcel, loader: ClassLoader?) : super(source, loader) {
|
||||||
|
bundle = source.readBundle(loader)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun writeToParcel(out: Parcel, flags: Int) {
|
||||||
|
super.writeToParcel(out, flags)
|
||||||
|
out.writeBundle(bundle)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val CREATOR: Parcelable.Creator<BundleSavedState> =
|
||||||
|
object : ClassLoaderCreator<BundleSavedState> {
|
||||||
|
override fun createFromParcel(
|
||||||
|
source: Parcel,
|
||||||
|
loader: ClassLoader
|
||||||
|
): BundleSavedState {
|
||||||
|
return BundleSavedState(source, loader)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createFromParcel(source: Parcel): BundleSavedState? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun newArray(size: Int): Array<BundleSavedState?> {
|
||||||
|
return arrayOfNulls(size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,347 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
|
||||||
*
|
|
||||||
* This file is part of Loop Habit Tracker.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by the
|
|
||||||
* Free Software Foundation, either version 3 of the License, or (at your
|
|
||||||
* option) any later version.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
|
||||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
|
||||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
||||||
* more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License along
|
|
||||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.isoron.uhabits.activities.common.views;
|
|
||||||
|
|
||||||
import android.content.*;
|
|
||||||
import android.graphics.*;
|
|
||||||
import android.util.*;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
private int firstWeekday = Calendar.SUNDAY;
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setFirstWeekday(int firstWeekday)
|
|
||||||
{
|
|
||||||
this.firstWeekday = firstWeekday;
|
|
||||||
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.getStartOfTodayCalendarWithOffset();
|
|
||||||
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.getWeekdaySequence(firstWeekday);
|
|
||||||
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.getShortWeekdayNames(firstWeekday))
|
|
||||||
{
|
|
||||||
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.getStartOfTodayCalendarWithOffset();
|
|
||||||
|
|
||||||
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 = DateExtensionsKt.toSimpleDataFormat("MMM");
|
|
||||||
dfYear = DateExtensionsKt.toSimpleDataFormat("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,294 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
||||||
|
*
|
||||||
|
* This file is part of Loop Habit Tracker.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by the
|
||||||
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along
|
||||||
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.isoron.uhabits.activities.common.views
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Paint
|
||||||
|
import android.graphics.RectF
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import org.isoron.uhabits.R
|
||||||
|
import org.isoron.uhabits.core.models.Timestamp
|
||||||
|
import org.isoron.uhabits.core.utils.DateUtils.Companion.getShortWeekdayNames
|
||||||
|
import org.isoron.uhabits.core.utils.DateUtils.Companion.getStartOfTodayCalendar
|
||||||
|
import org.isoron.uhabits.core.utils.DateUtils.Companion.getStartOfTodayCalendarWithOffset
|
||||||
|
import org.isoron.uhabits.core.utils.DateUtils.Companion.getWeekdaySequence
|
||||||
|
import org.isoron.uhabits.utils.ColorUtils.mixColors
|
||||||
|
import org.isoron.uhabits.utils.StyledResources
|
||||||
|
import org.isoron.uhabits.utils.toSimpleDataFormat
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Calendar
|
||||||
|
import java.util.GregorianCalendar
|
||||||
|
import java.util.Locale
|
||||||
|
import java.util.Random
|
||||||
|
import kotlin.collections.HashMap
|
||||||
|
import kotlin.math.max
|
||||||
|
import kotlin.math.min
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
class FrequencyChart : ScrollableChart {
|
||||||
|
private var pGrid: Paint? = null
|
||||||
|
private var em = 0f
|
||||||
|
private var dfMonth: SimpleDateFormat? = null
|
||||||
|
private var dfYear: SimpleDateFormat? = null
|
||||||
|
private var pText: Paint? = null
|
||||||
|
private var pGraph: Paint? = null
|
||||||
|
private var rect: RectF? = null
|
||||||
|
private var prevRect: RectF? = null
|
||||||
|
private var baseSize = 0
|
||||||
|
private var internalPaddingTop = 0
|
||||||
|
private var columnWidth = 0f
|
||||||
|
private var columnHeight = 0
|
||||||
|
private var nColumns = 0
|
||||||
|
private var textColor = 0
|
||||||
|
private var gridColor = 0
|
||||||
|
private lateinit var colors: IntArray
|
||||||
|
private var primaryColor = 0
|
||||||
|
private var isBackgroundTransparent = false
|
||||||
|
private lateinit var frequency: HashMap<Timestamp, Array<Int>>
|
||||||
|
private var maxFreq = 0
|
||||||
|
private var firstWeekday = Calendar.SUNDAY
|
||||||
|
|
||||||
|
constructor(context: Context?) : super(context) {
|
||||||
|
init()
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) {
|
||||||
|
frequency = HashMap()
|
||||||
|
init()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setColor(color: Int) {
|
||||||
|
primaryColor = color
|
||||||
|
initColors()
|
||||||
|
postInvalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setFrequency(frequency: java.util.HashMap<Timestamp, Array<Int>>) {
|
||||||
|
this.frequency = frequency
|
||||||
|
maxFreq = getMaxFreq(frequency)
|
||||||
|
postInvalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setFirstWeekday(firstWeekday: Int) {
|
||||||
|
this.firstWeekday = firstWeekday
|
||||||
|
postInvalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getMaxFreq(frequency: HashMap<Timestamp, Array<Int>>): Int {
|
||||||
|
var maxValue = 1
|
||||||
|
for (values in frequency.values) for (value in values) maxValue = max(
|
||||||
|
value,
|
||||||
|
maxValue
|
||||||
|
)
|
||||||
|
return maxValue
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setIsBackgroundTransparent(isBackgroundTransparent: Boolean) {
|
||||||
|
this.isBackgroundTransparent = isBackgroundTransparent
|
||||||
|
initColors()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initPaints() {
|
||||||
|
pText = Paint()
|
||||||
|
pText!!.isAntiAlias = true
|
||||||
|
pGraph = Paint()
|
||||||
|
pGraph!!.textAlign = Paint.Align.CENTER
|
||||||
|
pGraph!!.isAntiAlias = true
|
||||||
|
pGrid = Paint()
|
||||||
|
pGrid!!.isAntiAlias = true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDraw(canvas: Canvas) {
|
||||||
|
super.onDraw(canvas)
|
||||||
|
rect!![0f, 0f, nColumns * columnWidth] = columnHeight.toFloat()
|
||||||
|
rect!!.offset(0f, internalPaddingTop.toFloat())
|
||||||
|
drawGrid(canvas, rect)
|
||||||
|
pText!!.textAlign = Paint.Align.CENTER
|
||||||
|
pText!!.color = textColor
|
||||||
|
pGraph!!.color = primaryColor
|
||||||
|
prevRect!!.setEmpty()
|
||||||
|
val currentDate: GregorianCalendar =
|
||||||
|
getStartOfTodayCalendarWithOffset()
|
||||||
|
currentDate[Calendar.DAY_OF_MONTH] = 1
|
||||||
|
currentDate.add(Calendar.MONTH, -nColumns + 2 - dataOffset)
|
||||||
|
for (i in 0 until nColumns - 1) {
|
||||||
|
rect!![0f, 0f, columnWidth] = columnHeight.toFloat()
|
||||||
|
rect!!.offset(i * columnWidth, 0f)
|
||||||
|
drawColumn(canvas, rect, currentDate)
|
||||||
|
currentDate.add(Calendar.MONTH, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||||
|
val width = MeasureSpec.getSize(widthMeasureSpec)
|
||||||
|
val height = MeasureSpec.getSize(heightMeasureSpec)
|
||||||
|
setMeasuredDimension(width, height)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSizeChanged(
|
||||||
|
width: Int,
|
||||||
|
height: Int,
|
||||||
|
oldWidth: Int,
|
||||||
|
oldHeight: Int
|
||||||
|
) {
|
||||||
|
var height = height
|
||||||
|
if (height < 9) height = 200
|
||||||
|
baseSize = height / 8
|
||||||
|
setScrollerBucketSize(baseSize)
|
||||||
|
pText!!.textSize = baseSize * 0.4f
|
||||||
|
pGraph!!.textSize = baseSize * 0.4f
|
||||||
|
pGraph!!.strokeWidth = baseSize * 0.1f
|
||||||
|
pGrid!!.strokeWidth = baseSize * 0.05f
|
||||||
|
em = pText!!.fontSpacing
|
||||||
|
columnWidth = baseSize.toFloat()
|
||||||
|
columnWidth = max(columnWidth, maxMonthWidth * 1.2f)
|
||||||
|
columnHeight = 8 * baseSize
|
||||||
|
nColumns = (width / columnWidth).toInt()
|
||||||
|
internalPaddingTop = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun drawColumn(canvas: Canvas, rect: RectF?, date: GregorianCalendar) {
|
||||||
|
val values = frequency[Timestamp(date)]
|
||||||
|
val rowHeight = rect!!.height() / 8.0f
|
||||||
|
prevRect!!.set(rect)
|
||||||
|
val localeWeekdayList: Array<Int> = getWeekdaySequence(firstWeekday)
|
||||||
|
for (j in localeWeekdayList.indices) {
|
||||||
|
rect[0f, 0f, baseSize.toFloat()] = baseSize.toFloat()
|
||||||
|
rect.offset(prevRect!!.left, prevRect!!.top + baseSize * j)
|
||||||
|
val i = localeWeekdayList[j] % 7
|
||||||
|
if (values != null) drawMarker(canvas, rect, values[i])
|
||||||
|
rect.offset(0f, rowHeight)
|
||||||
|
}
|
||||||
|
drawFooter(canvas, rect, date)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun drawFooter(canvas: Canvas, rect: RectF?, date: GregorianCalendar) {
|
||||||
|
val time = date.time
|
||||||
|
canvas.drawText(
|
||||||
|
dfMonth!!.format(time),
|
||||||
|
rect!!.centerX(),
|
||||||
|
rect.centerY() - 0.1f * em,
|
||||||
|
pText!!
|
||||||
|
)
|
||||||
|
if (date[Calendar.MONTH] == 1) canvas.drawText(
|
||||||
|
dfYear!!.format(time),
|
||||||
|
rect.centerX(),
|
||||||
|
rect.centerY() + 0.9f * em,
|
||||||
|
pText!!
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun drawGrid(canvas: Canvas, rGrid: RectF?) {
|
||||||
|
val nRows = 7
|
||||||
|
val rowHeight = rGrid!!.height() / (nRows + 1)
|
||||||
|
pText!!.textAlign = Paint.Align.LEFT
|
||||||
|
pText!!.color = textColor
|
||||||
|
pGrid!!.color = gridColor
|
||||||
|
for (day in getShortWeekdayNames(firstWeekday)) {
|
||||||
|
canvas.drawText(
|
||||||
|
day,
|
||||||
|
rGrid.right - columnWidth,
|
||||||
|
rGrid.top + rowHeight / 2 + 0.25f * em,
|
||||||
|
pText!!
|
||||||
|
)
|
||||||
|
pGrid!!.strokeWidth = 1f
|
||||||
|
canvas.drawLine(
|
||||||
|
rGrid.left,
|
||||||
|
rGrid.top,
|
||||||
|
rGrid.right,
|
||||||
|
rGrid.top,
|
||||||
|
pGrid!!
|
||||||
|
)
|
||||||
|
rGrid.offset(0f, rowHeight)
|
||||||
|
}
|
||||||
|
canvas.drawLine(rGrid.left, rGrid.top, rGrid.right, rGrid.top, pGrid!!)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun drawMarker(canvas: Canvas, rect: RectF?, value: Int?) {
|
||||||
|
val padding = rect!!.height() * 0.2f
|
||||||
|
// maximal allowed mark radius
|
||||||
|
val maxRadius = (rect.height() - 2 * padding) / 2.0f
|
||||||
|
// the real mark radius is scaled down by a factor depending on the maximal frequency
|
||||||
|
val scale = 1.0f / maxFreq * value!!
|
||||||
|
val radius = maxRadius * scale
|
||||||
|
val colorIndex = min((colors.size - 1), ((colors.size - 1) * scale).roundToInt())
|
||||||
|
pGraph!!.color = colors[colorIndex]
|
||||||
|
canvas.drawCircle(rect.centerX(), rect.centerY(), radius, pGraph!!)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val maxMonthWidth: Float
|
||||||
|
get() {
|
||||||
|
var maxMonthWidth = 0f
|
||||||
|
val day: GregorianCalendar =
|
||||||
|
getStartOfTodayCalendarWithOffset()
|
||||||
|
for (i in 0..11) {
|
||||||
|
day[Calendar.MONTH] = i
|
||||||
|
val monthWidth = pText!!.measureText(dfMonth!!.format(day.time))
|
||||||
|
maxMonthWidth = max(maxMonthWidth, monthWidth)
|
||||||
|
}
|
||||||
|
return maxMonthWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun init() {
|
||||||
|
initPaints()
|
||||||
|
initColors()
|
||||||
|
initDateFormats()
|
||||||
|
initRects()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initColors() {
|
||||||
|
val res = StyledResources(context)
|
||||||
|
textColor = res.getColor(R.attr.mediumContrastTextColor)
|
||||||
|
gridColor = res.getColor(R.attr.lowContrastTextColor)
|
||||||
|
colors = IntArray(4)
|
||||||
|
colors[0] = gridColor
|
||||||
|
colors[3] = primaryColor
|
||||||
|
colors[1] = mixColors(colors[0], colors[3], 0.66f)
|
||||||
|
colors[2] = mixColors(colors[0], colors[3], 0.33f)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initDateFormats() {
|
||||||
|
if (isInEditMode) {
|
||||||
|
dfMonth = SimpleDateFormat("MMM", Locale.getDefault())
|
||||||
|
dfYear = SimpleDateFormat("yyyy", Locale.getDefault())
|
||||||
|
} else {
|
||||||
|
dfMonth = "MMM".toSimpleDataFormat()
|
||||||
|
dfYear = "yyyy".toSimpleDataFormat()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initRects() {
|
||||||
|
rect = RectF()
|
||||||
|
prevRect = RectF()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun populateWithRandomData() {
|
||||||
|
val date: GregorianCalendar = getStartOfTodayCalendar()
|
||||||
|
date[Calendar.DAY_OF_MONTH] = 1
|
||||||
|
val rand = Random()
|
||||||
|
frequency.clear()
|
||||||
|
for (i in 0..39) {
|
||||||
|
val values = IntArray(7) { rand.nextInt(5) }.toTypedArray()
|
||||||
|
frequency[Timestamp(date)] = values
|
||||||
|
date.add(Calendar.MONTH, -1)
|
||||||
|
}
|
||||||
|
maxFreq = getMaxFreq(frequency)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,10 +16,11 @@
|
|||||||
* You should have received a copy of the GNU General Public License along
|
* You should have received a copy of the GNU General Public License along
|
||||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
package org.isoron.uhabits.activities.common.views
|
||||||
|
|
||||||
package org.isoron.uhabits.core.ui.callbacks;
|
import org.isoron.uhabits.core.models.Habit
|
||||||
|
|
||||||
public interface OnConfirmedCallback
|
interface HabitChart {
|
||||||
{
|
fun setHabit(habit: Habit?)
|
||||||
void onConfirmed();
|
fun refreshData()
|
||||||
}
|
}
|
||||||
@@ -1,272 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
|
||||||
*
|
|
||||||
* This file is part of Loop Habit Tracker.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by the
|
|
||||||
* Free Software Foundation, either version 3 of the License, or (at your
|
|
||||||
* option) any later version.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
|
||||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
|
||||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
||||||
* more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License along
|
|
||||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.isoron.uhabits.activities.common.views;
|
|
||||||
|
|
||||||
import android.content.*;
|
|
||||||
import android.graphics.*;
|
|
||||||
import android.text.*;
|
|
||||||
import android.util.*;
|
|
||||||
import android.view.*;
|
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
import org.isoron.uhabits.*;
|
|
||||||
import org.isoron.uhabits.utils.*;
|
|
||||||
|
|
||||||
import static org.isoron.uhabits.utils.AttributeSetUtils.*;
|
|
||||||
import static org.isoron.uhabits.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.cardBgColor);
|
|
||||||
|
|
||||||
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,213 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
||||||
|
*
|
||||||
|
* This file is part of Loop Habit Tracker.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by the
|
||||||
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along
|
||||||
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.isoron.uhabits.activities.common.views
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.Paint
|
||||||
|
import android.graphics.PorterDuff
|
||||||
|
import android.graphics.PorterDuffXfermode
|
||||||
|
import android.graphics.RectF
|
||||||
|
import android.text.TextPaint
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.View
|
||||||
|
import org.isoron.uhabits.R
|
||||||
|
import org.isoron.uhabits.utils.AttributeSetUtils.getAttribute
|
||||||
|
import org.isoron.uhabits.utils.AttributeSetUtils.getBooleanAttribute
|
||||||
|
import org.isoron.uhabits.utils.AttributeSetUtils.getColorAttribute
|
||||||
|
import org.isoron.uhabits.utils.AttributeSetUtils.getFloatAttribute
|
||||||
|
import org.isoron.uhabits.utils.ColorUtils.setAlpha
|
||||||
|
import org.isoron.uhabits.utils.InterfaceUtils.dpToPixels
|
||||||
|
import org.isoron.uhabits.utils.InterfaceUtils.getDimension
|
||||||
|
import org.isoron.uhabits.utils.InterfaceUtils.getFontAwesome
|
||||||
|
import org.isoron.uhabits.utils.InterfaceUtils.spToPixels
|
||||||
|
import org.isoron.uhabits.utils.PaletteUtils.getAndroidTestColor
|
||||||
|
import org.isoron.uhabits.utils.StyledResources
|
||||||
|
import kotlin.math.min
|
||||||
|
import kotlin.math.roundToLong
|
||||||
|
|
||||||
|
class RingView : View {
|
||||||
|
private var color: Int
|
||||||
|
private var precision: Float
|
||||||
|
private var percentage: Float
|
||||||
|
private var diameter = 0
|
||||||
|
private var thickness: Float
|
||||||
|
private var rect: RectF? = null
|
||||||
|
private var pRing: TextPaint? = null
|
||||||
|
private var backgroundColor: Int? = null
|
||||||
|
private var inactiveColor: Int? = null
|
||||||
|
private var em = 0f
|
||||||
|
private var text: String?
|
||||||
|
private var textSize: Float
|
||||||
|
private var enableFontAwesome = false
|
||||||
|
private var internalDrawingCache: Bitmap? = null
|
||||||
|
private var cacheCanvas: Canvas? = null
|
||||||
|
private var isTransparencyEnabled = false
|
||||||
|
|
||||||
|
constructor(context: Context?) : super(context) {
|
||||||
|
percentage = 0.0f
|
||||||
|
precision = 0.01f
|
||||||
|
color = getAndroidTestColor(0)
|
||||||
|
thickness = dpToPixels(getContext(), 2f)
|
||||||
|
text = ""
|
||||||
|
textSize = getDimension(context!!, R.dimen.smallTextSize)
|
||||||
|
init()
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(ctx: Context?, attrs: AttributeSet?) : super(ctx, attrs) {
|
||||||
|
percentage = getFloatAttribute(ctx!!, attrs!!, "percentage", 0f)
|
||||||
|
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", 0f)
|
||||||
|
thickness = dpToPixels(ctx, thickness)
|
||||||
|
val 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 fun setBackgroundColor(backgroundColor: Int) {
|
||||||
|
this.backgroundColor = backgroundColor
|
||||||
|
invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setColor(color: Int) {
|
||||||
|
this.color = color
|
||||||
|
invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getColor(): Int {
|
||||||
|
return color
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setIsTransparencyEnabled(isTransparencyEnabled: Boolean) {
|
||||||
|
this.isTransparencyEnabled = isTransparencyEnabled
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setPercentage(percentage: Float) {
|
||||||
|
this.percentage = percentage
|
||||||
|
invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setPrecision(precision: Float) {
|
||||||
|
this.precision = precision
|
||||||
|
invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setText(text: String?) {
|
||||||
|
this.text = text
|
||||||
|
invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setTextSize(textSize: Float) {
|
||||||
|
this.textSize = textSize
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setThickness(thickness: Float) {
|
||||||
|
this.thickness = thickness
|
||||||
|
invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDraw(canvas: Canvas) {
|
||||||
|
super.onDraw(canvas)
|
||||||
|
val activeCanvas: Canvas?
|
||||||
|
if (isTransparencyEnabled) {
|
||||||
|
if (internalDrawingCache == null) reallocateCache()
|
||||||
|
activeCanvas = cacheCanvas
|
||||||
|
internalDrawingCache!!.eraseColor(Color.TRANSPARENT)
|
||||||
|
} else {
|
||||||
|
activeCanvas = canvas
|
||||||
|
}
|
||||||
|
pRing!!.color = color
|
||||||
|
rect!![0f, 0f, diameter.toFloat()] = diameter.toFloat()
|
||||||
|
val angle = 360 * (percentage / precision).roundToLong() * precision
|
||||||
|
activeCanvas!!.drawArc(rect!!, -90f, angle, true, pRing!!)
|
||||||
|
pRing!!.color = inactiveColor!!
|
||||||
|
activeCanvas.drawArc(rect!!, angle - 90, 360 - angle, true, pRing!!)
|
||||||
|
if (thickness > 0) {
|
||||||
|
if (isTransparencyEnabled) pRing!!.xfermode = XFERMODE_CLEAR else pRing!!.color =
|
||||||
|
backgroundColor!!
|
||||||
|
rect!!.inset(thickness, thickness)
|
||||||
|
activeCanvas.drawArc(rect!!, 0f, 360f, true, pRing!!)
|
||||||
|
pRing!!.xfermode = null
|
||||||
|
pRing!!.color = color
|
||||||
|
pRing!!.textSize = textSize
|
||||||
|
if (enableFontAwesome) pRing!!.typeface = getFontAwesome(context)
|
||||||
|
activeCanvas.drawText(
|
||||||
|
text!!,
|
||||||
|
rect!!.centerX(),
|
||||||
|
rect!!.centerY() + 0.4f * em,
|
||||||
|
pRing!!
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (activeCanvas !== canvas) canvas.drawBitmap(internalDrawingCache!!, 0f, 0f, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||||
|
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
||||||
|
val width = MeasureSpec.getSize(widthMeasureSpec)
|
||||||
|
val height = MeasureSpec.getSize(heightMeasureSpec)
|
||||||
|
diameter = min(height, width)
|
||||||
|
pRing!!.textSize = textSize
|
||||||
|
em = pRing!!.measureText("M")
|
||||||
|
setMeasuredDimension(diameter, diameter)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
||||||
|
super.onSizeChanged(w, h, oldw, oldh)
|
||||||
|
if (isTransparencyEnabled) reallocateCache()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun init() {
|
||||||
|
pRing = TextPaint()
|
||||||
|
pRing!!.isAntiAlias = true
|
||||||
|
pRing!!.color = color
|
||||||
|
pRing!!.textAlign = Paint.Align.CENTER
|
||||||
|
val res = StyledResources(context)
|
||||||
|
if (backgroundColor == null) backgroundColor = res.getColor(R.attr.cardBgColor)
|
||||||
|
if (inactiveColor == null) inactiveColor = res.getColor(R.attr.highContrastTextColor)
|
||||||
|
inactiveColor = setAlpha(inactiveColor!!, 0.1f)
|
||||||
|
rect = RectF()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun reallocateCache() {
|
||||||
|
if (internalDrawingCache != null) internalDrawingCache!!.recycle()
|
||||||
|
val newDrawingCache = Bitmap.createBitmap(diameter, diameter, Bitmap.Config.ARGB_8888)
|
||||||
|
internalDrawingCache = newDrawingCache
|
||||||
|
cacheCanvas = Canvas(newDrawingCache)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getPercentage(): Float {
|
||||||
|
return percentage
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getPrecision(): Float {
|
||||||
|
return precision
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val XFERMODE_CLEAR = PorterDuffXfermode(PorterDuff.Mode.CLEAR)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,452 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
|
||||||
*
|
|
||||||
* This file is part of Loop Habit Tracker.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by the
|
|
||||||
* Free Software Foundation, either version 3 of the License, or (at your
|
|
||||||
* option) any later version.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
|
||||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
|
||||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
||||||
* more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License along
|
|
||||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.isoron.uhabits.activities.common.views;
|
|
||||||
|
|
||||||
import android.content.*;
|
|
||||||
import android.graphics.*;
|
|
||||||
import android.util.*;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
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.uhabits.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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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.getStartOfTodayCalendarWithOffset();
|
|
||||||
|
|
||||||
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.getStartOfTodayCalendarWithOffset();
|
|
||||||
|
|
||||||
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.cardBgColor);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 = DateExtensionsKt.toSimpleDataFormat("MMM");
|
|
||||||
dfYear = DateExtensionsKt.toSimpleDataFormat("yyyy");
|
|
||||||
dfDay = DateExtensionsKt.toSimpleDataFormat("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,377 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
||||||
|
*
|
||||||
|
* This file is part of Loop Habit Tracker.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by the
|
||||||
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along
|
||||||
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.isoron.uhabits.activities.common.views
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.Paint
|
||||||
|
import android.graphics.PorterDuff
|
||||||
|
import android.graphics.PorterDuffXfermode
|
||||||
|
import android.graphics.RectF
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import org.isoron.uhabits.R
|
||||||
|
import org.isoron.uhabits.core.models.Score
|
||||||
|
import org.isoron.uhabits.core.models.Timestamp
|
||||||
|
import org.isoron.uhabits.core.utils.DateUtils.Companion.getStartOfTodayCalendarWithOffset
|
||||||
|
import org.isoron.uhabits.core.utils.DateUtils.Companion.getToday
|
||||||
|
import org.isoron.uhabits.utils.InterfaceUtils.dpToPixels
|
||||||
|
import org.isoron.uhabits.utils.InterfaceUtils.getDimension
|
||||||
|
import org.isoron.uhabits.utils.StyledResources
|
||||||
|
import org.isoron.uhabits.utils.toSimpleDataFormat
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Calendar
|
||||||
|
import java.util.GregorianCalendar
|
||||||
|
import java.util.LinkedList
|
||||||
|
import java.util.Locale
|
||||||
|
import java.util.Random
|
||||||
|
import kotlin.math.max
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
class ScoreChart : ScrollableChart {
|
||||||
|
private var pGrid: Paint? = null
|
||||||
|
private var em = 0f
|
||||||
|
private var dfMonth: SimpleDateFormat? = null
|
||||||
|
private var dfDay: SimpleDateFormat? = null
|
||||||
|
private var dfYear: SimpleDateFormat? = null
|
||||||
|
private var pText: Paint? = null
|
||||||
|
private var pGraph: Paint? = null
|
||||||
|
private var rect: RectF? = null
|
||||||
|
private var prevRect: RectF? = null
|
||||||
|
private var baseSize = 0
|
||||||
|
private var internalPaddingTop = 0
|
||||||
|
private var columnWidth = 0f
|
||||||
|
private var columnHeight = 0
|
||||||
|
private var nColumns = 0
|
||||||
|
private var textColor = 0
|
||||||
|
private var gridColor = 0
|
||||||
|
private var scores: List<Score>? = null
|
||||||
|
private var primaryColor = 0
|
||||||
|
|
||||||
|
@Deprecated("")
|
||||||
|
private var bucketSize = 7
|
||||||
|
private var internalBackgroundColor = 0
|
||||||
|
private var internalDrawingCache: Bitmap? = null
|
||||||
|
private var cacheCanvas: Canvas? = null
|
||||||
|
private var isTransparencyEnabled = false
|
||||||
|
private var skipYear = 0
|
||||||
|
private var previousYearText: String? = null
|
||||||
|
private var previousMonthText: String? = null
|
||||||
|
|
||||||
|
constructor(context: Context?) : super(context) {
|
||||||
|
init()
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) {
|
||||||
|
init()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun populateWithRandomData() {
|
||||||
|
val random = Random()
|
||||||
|
val newScores = LinkedList<Score>()
|
||||||
|
var previous = 0.5
|
||||||
|
val timestamp: Timestamp = getToday()
|
||||||
|
for (i in 1..99) {
|
||||||
|
val step = 0.1
|
||||||
|
var current = previous + random.nextDouble() * step * 2 - step
|
||||||
|
current = max(0.0, min(1.0, current))
|
||||||
|
newScores.add(Score(timestamp.minus(i), current))
|
||||||
|
previous = current
|
||||||
|
}
|
||||||
|
scores = newScores
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setBucketSize(bucketSize: Int) {
|
||||||
|
this.bucketSize = bucketSize
|
||||||
|
postInvalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setIsTransparencyEnabled(enabled: Boolean) {
|
||||||
|
isTransparencyEnabled = enabled
|
||||||
|
postInvalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setColor(primaryColor: Int) {
|
||||||
|
this.primaryColor = primaryColor
|
||||||
|
postInvalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setScores(scores: List<Score>) {
|
||||||
|
this.scores = scores
|
||||||
|
postInvalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDraw(canvas: Canvas) {
|
||||||
|
super.onDraw(canvas)
|
||||||
|
val activeCanvas: Canvas?
|
||||||
|
if (isTransparencyEnabled) {
|
||||||
|
if (internalDrawingCache == null) initCache(width, height)
|
||||||
|
activeCanvas = cacheCanvas
|
||||||
|
internalDrawingCache!!.eraseColor(Color.TRANSPARENT)
|
||||||
|
} else {
|
||||||
|
activeCanvas = canvas
|
||||||
|
}
|
||||||
|
if (scores == null) return
|
||||||
|
rect!![0f, 0f, nColumns * columnWidth] = columnHeight.toFloat()
|
||||||
|
rect!!.offset(0f, internalPaddingTop.toFloat())
|
||||||
|
drawGrid(activeCanvas, rect)
|
||||||
|
pText!!.color = textColor
|
||||||
|
pGraph!!.color = primaryColor
|
||||||
|
prevRect!!.setEmpty()
|
||||||
|
previousMonthText = ""
|
||||||
|
previousYearText = ""
|
||||||
|
skipYear = 0
|
||||||
|
for (k in 0 until nColumns) {
|
||||||
|
val offset = nColumns - k - 1 + dataOffset
|
||||||
|
if (offset >= scores!!.size) continue
|
||||||
|
val score = scores!![offset].value
|
||||||
|
val timestamp = scores!![offset].timestamp
|
||||||
|
val height = (columnHeight * score).toInt()
|
||||||
|
rect!![0f, 0f, baseSize.toFloat()] = baseSize.toFloat()
|
||||||
|
rect!!.offset(
|
||||||
|
k * columnWidth + (columnWidth - baseSize) / 2,
|
||||||
|
(
|
||||||
|
internalPaddingTop + columnHeight - height - baseSize / 2
|
||||||
|
).toFloat()
|
||||||
|
)
|
||||||
|
if (!prevRect!!.isEmpty) {
|
||||||
|
drawLine(activeCanvas, prevRect, rect)
|
||||||
|
drawMarker(activeCanvas, prevRect)
|
||||||
|
}
|
||||||
|
if (k == nColumns - 1) drawMarker(activeCanvas, rect)
|
||||||
|
prevRect!!.set(rect)
|
||||||
|
rect!![0f, 0f, columnWidth] = columnHeight.toFloat()
|
||||||
|
rect!!.offset(k * columnWidth, internalPaddingTop.toFloat())
|
||||||
|
drawFooter(activeCanvas, rect, timestamp)
|
||||||
|
}
|
||||||
|
if (activeCanvas !== canvas) canvas.drawBitmap(internalDrawingCache!!, 0f, 0f, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||||
|
val width = MeasureSpec.getSize(widthMeasureSpec)
|
||||||
|
val height = MeasureSpec.getSize(heightMeasureSpec)
|
||||||
|
setMeasuredDimension(width, height)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSizeChanged(
|
||||||
|
width: Int,
|
||||||
|
height: Int,
|
||||||
|
oldWidth: Int,
|
||||||
|
oldHeight: Int
|
||||||
|
) {
|
||||||
|
var height = height
|
||||||
|
if (height < 9) height = 200
|
||||||
|
val maxTextSize = getDimension(context, R.dimen.tinyTextSize)
|
||||||
|
val textSize = height * 0.06f
|
||||||
|
pText!!.textSize = min(textSize, maxTextSize)
|
||||||
|
em = pText!!.fontSpacing
|
||||||
|
val footerHeight = (3 * em).toInt()
|
||||||
|
internalPaddingTop = em.toInt()
|
||||||
|
baseSize = (height - footerHeight - internalPaddingTop) / 8
|
||||||
|
columnWidth = baseSize.toFloat()
|
||||||
|
columnWidth = max(columnWidth, maxDayWidth * 1.5f)
|
||||||
|
columnWidth = max(columnWidth, maxMonthWidth * 1.2f)
|
||||||
|
nColumns = (width / columnWidth).toInt()
|
||||||
|
columnWidth = width.toFloat() / nColumns
|
||||||
|
setScrollerBucketSize(columnWidth.toInt())
|
||||||
|
columnHeight = 8 * baseSize
|
||||||
|
val minStrokeWidth = dpToPixels(context, 1f)
|
||||||
|
pGraph!!.textSize = baseSize * 0.5f
|
||||||
|
pGraph!!.strokeWidth = baseSize * 0.1f
|
||||||
|
pGrid!!.strokeWidth = min(minStrokeWidth, baseSize * 0.05f)
|
||||||
|
if (isTransparencyEnabled) initCache(width, height)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun drawFooter(canvas: Canvas?, rect: RectF?, currentDate: Timestamp) {
|
||||||
|
val yearText = dfYear!!.format(currentDate.toJavaDate())
|
||||||
|
val monthText = dfMonth!!.format(currentDate.toJavaDate())
|
||||||
|
val dayText = dfDay!!.format(currentDate.toJavaDate())
|
||||||
|
val calendar = currentDate.toCalendar()
|
||||||
|
val text: String
|
||||||
|
val year = calendar[Calendar.YEAR]
|
||||||
|
var shouldPrintYear = true
|
||||||
|
if (yearText == previousYearText) shouldPrintYear = false
|
||||||
|
if (bucketSize >= 365 && year % 2 != 0) shouldPrintYear = false
|
||||||
|
if (skipYear > 0) {
|
||||||
|
skipYear--
|
||||||
|
shouldPrintYear = false
|
||||||
|
}
|
||||||
|
if (shouldPrintYear) {
|
||||||
|
previousYearText = yearText
|
||||||
|
previousMonthText = ""
|
||||||
|
pText!!.textAlign = Paint.Align.CENTER
|
||||||
|
canvas!!.drawText(
|
||||||
|
yearText,
|
||||||
|
rect!!.centerX(),
|
||||||
|
rect.bottom + em * 2.2f,
|
||||||
|
pText!!
|
||||||
|
)
|
||||||
|
skipYear = 1
|
||||||
|
}
|
||||||
|
if (bucketSize < 365) {
|
||||||
|
if (monthText != previousMonthText) {
|
||||||
|
previousMonthText = monthText
|
||||||
|
text = monthText
|
||||||
|
} else {
|
||||||
|
text = dayText
|
||||||
|
}
|
||||||
|
pText!!.textAlign = Paint.Align.CENTER
|
||||||
|
canvas!!.drawText(
|
||||||
|
text,
|
||||||
|
rect!!.centerX(),
|
||||||
|
rect.bottom + em * 1.2f,
|
||||||
|
pText!!
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun drawGrid(canvas: Canvas?, rGrid: RectF?) {
|
||||||
|
val nRows = 5
|
||||||
|
val rowHeight = rGrid!!.height() / nRows
|
||||||
|
pText!!.textAlign = Paint.Align.LEFT
|
||||||
|
pText!!.color = textColor
|
||||||
|
pGrid!!.color = gridColor
|
||||||
|
for (i in 0 until nRows) {
|
||||||
|
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(0f, rowHeight)
|
||||||
|
}
|
||||||
|
canvas!!.drawLine(rGrid.left, rGrid.top, rGrid.right, rGrid.top, pGrid!!)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun drawLine(canvas: Canvas?, rectFrom: RectF?, rectTo: RectF?) {
|
||||||
|
pGraph!!.color = primaryColor
|
||||||
|
canvas!!.drawLine(
|
||||||
|
rectFrom!!.centerX(),
|
||||||
|
rectFrom.centerY(),
|
||||||
|
rectTo!!.centerX(),
|
||||||
|
rectTo.centerY(),
|
||||||
|
pGraph!!
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun drawMarker(canvas: Canvas?, rect: RectF?) {
|
||||||
|
rect!!.inset(baseSize * 0.225f, baseSize * 0.225f)
|
||||||
|
setModeOrColor(pGraph, XFERMODE_CLEAR, internalBackgroundColor)
|
||||||
|
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!!.xfermode = XFERMODE_SRC
|
||||||
|
}
|
||||||
|
|
||||||
|
private val maxDayWidth: Float
|
||||||
|
private get() {
|
||||||
|
var maxDayWidth = 0f
|
||||||
|
val day: GregorianCalendar =
|
||||||
|
getStartOfTodayCalendarWithOffset()
|
||||||
|
for (i in 0..27) {
|
||||||
|
day[Calendar.DAY_OF_MONTH] = i
|
||||||
|
val monthWidth = pText!!.measureText(dfMonth!!.format(day.time))
|
||||||
|
maxDayWidth = max(maxDayWidth, monthWidth)
|
||||||
|
}
|
||||||
|
return maxDayWidth
|
||||||
|
}
|
||||||
|
private val maxMonthWidth: Float
|
||||||
|
private get() {
|
||||||
|
var maxMonthWidth = 0f
|
||||||
|
val day: GregorianCalendar =
|
||||||
|
getStartOfTodayCalendarWithOffset()
|
||||||
|
for (i in 0..11) {
|
||||||
|
day[Calendar.MONTH] = i
|
||||||
|
val monthWidth = pText!!.measureText(dfMonth!!.format(day.time))
|
||||||
|
maxMonthWidth = max(maxMonthWidth, monthWidth)
|
||||||
|
}
|
||||||
|
return maxMonthWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun init() {
|
||||||
|
initPaints()
|
||||||
|
initColors()
|
||||||
|
initDateFormats()
|
||||||
|
initRects()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initCache(width: Int, height: Int) {
|
||||||
|
if (internalDrawingCache != null) internalDrawingCache!!.recycle()
|
||||||
|
val newDrawingCache = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
||||||
|
internalDrawingCache = newDrawingCache
|
||||||
|
cacheCanvas = Canvas(newDrawingCache)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initColors() {
|
||||||
|
val res = StyledResources(context)
|
||||||
|
primaryColor = Color.BLACK
|
||||||
|
textColor = res.getColor(R.attr.mediumContrastTextColor)
|
||||||
|
gridColor = res.getColor(R.attr.lowContrastTextColor)
|
||||||
|
internalBackgroundColor = res.getColor(R.attr.cardBgColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initDateFormats() {
|
||||||
|
if (isInEditMode) {
|
||||||
|
dfMonth = SimpleDateFormat("MMM", Locale.getDefault())
|
||||||
|
dfYear = SimpleDateFormat("yyyy", Locale.getDefault())
|
||||||
|
dfDay = SimpleDateFormat("d", Locale.getDefault())
|
||||||
|
} else {
|
||||||
|
dfMonth = "MMM".toSimpleDataFormat()
|
||||||
|
dfYear = "yyyy".toSimpleDataFormat()
|
||||||
|
dfDay = "d".toSimpleDataFormat()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initPaints() {
|
||||||
|
pText = Paint()
|
||||||
|
pText!!.isAntiAlias = true
|
||||||
|
pGraph = Paint()
|
||||||
|
pGraph!!.textAlign = Paint.Align.CENTER
|
||||||
|
pGraph!!.isAntiAlias = true
|
||||||
|
pGrid = Paint()
|
||||||
|
pGrid!!.isAntiAlias = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initRects() {
|
||||||
|
rect = RectF()
|
||||||
|
prevRect = RectF()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setModeOrColor(p: Paint?, mode: PorterDuffXfermode, color: Int) {
|
||||||
|
if (isTransparencyEnabled) p!!.xfermode = mode else p!!.color = color
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val XFERMODE_CLEAR = PorterDuffXfermode(PorterDuff.Mode.CLEAR)
|
||||||
|
private val XFERMODE_SRC = PorterDuffXfermode(PorterDuff.Mode.SRC)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,245 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
|
||||||
*
|
|
||||||
* This file is part of Loop Habit Tracker.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by the
|
|
||||||
* Free Software Foundation, either version 3 of the License, or (at your
|
|
||||||
* option) any later version.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
|
||||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
|
||||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
||||||
* more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License along
|
|
||||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.isoron.uhabits.activities.common.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() {};
|
|
||||||
}
|
|
||||||
|
|
||||||
public void reset()
|
|
||||||
{
|
|
||||||
scroller.setFinalX(0);
|
|
||||||
scroller.computeScrollOffset();
|
|
||||||
updateDataOffset();
|
|
||||||
}
|
|
||||||
|
|
||||||
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,199 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
||||||
|
*
|
||||||
|
* This file is part of Loop Habit Tracker.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by the
|
||||||
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along
|
||||||
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.isoron.uhabits.activities.common.views
|
||||||
|
|
||||||
|
import android.animation.ValueAnimator
|
||||||
|
import android.animation.ValueAnimator.AnimatorUpdateListener
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Parcelable
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.GestureDetector
|
||||||
|
import android.view.MotionEvent
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.Scroller
|
||||||
|
import kotlin.math.abs
|
||||||
|
import kotlin.math.max
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
abstract class ScrollableChart : View, GestureDetector.OnGestureListener, AnimatorUpdateListener {
|
||||||
|
var dataOffset = 0
|
||||||
|
private set
|
||||||
|
private var scrollerBucketSize = 1
|
||||||
|
private var direction = 1
|
||||||
|
private lateinit var detector: GestureDetector
|
||||||
|
private lateinit var scroller: Scroller
|
||||||
|
private lateinit var scrollAnimator: ValueAnimator
|
||||||
|
private lateinit var scrollController: ScrollController
|
||||||
|
private var maxDataOffset = 10000
|
||||||
|
|
||||||
|
constructor(context: Context?) : super(context) {
|
||||||
|
init(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) {
|
||||||
|
init(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAnimationUpdate(animation: ValueAnimator) {
|
||||||
|
if (!scroller.isFinished) {
|
||||||
|
scroller.computeScrollOffset()
|
||||||
|
updateDataOffset()
|
||||||
|
} else {
|
||||||
|
scrollAnimator.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDown(e: MotionEvent): Boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFling(
|
||||||
|
e1: MotionEvent,
|
||||||
|
e2: MotionEvent,
|
||||||
|
velocityX: Float,
|
||||||
|
velocityY: Float
|
||||||
|
): Boolean {
|
||||||
|
scroller.fling(
|
||||||
|
scroller.currX,
|
||||||
|
scroller.currY,
|
||||||
|
direction * velocityX.toInt() / 2,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
maxX,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
)
|
||||||
|
invalidate()
|
||||||
|
scrollAnimator.duration = scroller.duration.toLong()
|
||||||
|
scrollAnimator.start()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private val maxX: Int
|
||||||
|
get() = maxDataOffset * scrollerBucketSize
|
||||||
|
|
||||||
|
public override fun onRestoreInstanceState(state: Parcelable) {
|
||||||
|
if (state !is BundleSavedState) {
|
||||||
|
super.onRestoreInstanceState(state)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val x = state.bundle!!.getInt("x")
|
||||||
|
val y = state.bundle.getInt("y")
|
||||||
|
direction = state.bundle.getInt("direction")
|
||||||
|
dataOffset = state.bundle.getInt("dataOffset")
|
||||||
|
maxDataOffset = state.bundle.getInt("maxDataOffset")
|
||||||
|
scroller.startScroll(0, 0, x, y, 0)
|
||||||
|
scroller.computeScrollOffset()
|
||||||
|
super.onRestoreInstanceState(state.superState)
|
||||||
|
}
|
||||||
|
|
||||||
|
public override fun onSaveInstanceState(): Parcelable? {
|
||||||
|
val superState = super.onSaveInstanceState()
|
||||||
|
val bundle = Bundle().apply {
|
||||||
|
putInt("x", scroller.currX)
|
||||||
|
putInt("y", scroller.currY)
|
||||||
|
putInt("dataOffset", dataOffset)
|
||||||
|
putInt("direction", direction)
|
||||||
|
putInt("maxDataOffset", maxDataOffset)
|
||||||
|
}
|
||||||
|
return BundleSavedState(superState, bundle)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onScroll(e1: MotionEvent?, e2: MotionEvent?, dx: Float, dy: Float): Boolean {
|
||||||
|
var dx = dx
|
||||||
|
if (scrollerBucketSize == 0) return false
|
||||||
|
if (abs(dx) > abs(dy)) {
|
||||||
|
val parent = parent
|
||||||
|
parent?.requestDisallowInterceptTouchEvent(true)
|
||||||
|
}
|
||||||
|
dx *= -direction
|
||||||
|
dx = min(dx, (maxX - scroller.currX).toFloat())
|
||||||
|
scroller.startScroll(
|
||||||
|
scroller.currX,
|
||||||
|
scroller.currY,
|
||||||
|
dx.toInt(),
|
||||||
|
dy.toInt(),
|
||||||
|
0
|
||||||
|
)
|
||||||
|
scroller.computeScrollOffset()
|
||||||
|
updateDataOffset()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onShowPress(e: MotionEvent) {}
|
||||||
|
override fun onSingleTapUp(e: MotionEvent): Boolean {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTouchEvent(event: MotionEvent): Boolean {
|
||||||
|
return detector.onTouchEvent(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setScrollDirection(direction: Int) {
|
||||||
|
require(!(direction != 1 && direction != -1))
|
||||||
|
this.direction = direction
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLongPress(e: MotionEvent) {}
|
||||||
|
fun setMaxDataOffset(maxDataOffset: Int) {
|
||||||
|
this.maxDataOffset = maxDataOffset
|
||||||
|
dataOffset = min(dataOffset, maxDataOffset)
|
||||||
|
scrollController.onDataOffsetChanged(dataOffset)
|
||||||
|
postInvalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setScrollController(scrollController: ScrollController) {
|
||||||
|
this.scrollController = scrollController
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setScrollerBucketSize(scrollerBucketSize: Int) {
|
||||||
|
this.scrollerBucketSize = scrollerBucketSize
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun init(context: Context?) {
|
||||||
|
detector = GestureDetector(context, this)
|
||||||
|
scroller = Scroller(context, null, true)
|
||||||
|
val newScrollAnimator = ValueAnimator.ofFloat(0f, 1f)
|
||||||
|
newScrollAnimator.addUpdateListener(this)
|
||||||
|
scrollAnimator = newScrollAnimator
|
||||||
|
scrollController = object : ScrollController {}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun reset() {
|
||||||
|
scroller.finalX = 0
|
||||||
|
scroller.computeScrollOffset()
|
||||||
|
updateDataOffset()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateDataOffset() {
|
||||||
|
var newDataOffset = scroller.currX / scrollerBucketSize
|
||||||
|
newDataOffset = max(0, newDataOffset)
|
||||||
|
newDataOffset = min(maxDataOffset, newDataOffset)
|
||||||
|
if (newDataOffset != dataOffset) {
|
||||||
|
dataOffset = newDataOffset
|
||||||
|
scrollController.onDataOffsetChanged(dataOffset)
|
||||||
|
postInvalidate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ScrollController {
|
||||||
|
fun onDataOffsetChanged(newDataOffset: Int) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,313 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
|
||||||
*
|
|
||||||
* This file is part of Loop Habit Tracker.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by the
|
|
||||||
* Free Software Foundation, either version 3 of the License, or (at your
|
|
||||||
* option) any later version.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
|
||||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
|
||||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
||||||
* more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License along
|
|
||||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.isoron.uhabits.activities.common.views;
|
|
||||||
|
|
||||||
import android.content.*;
|
|
||||||
import android.graphics.*;
|
|
||||||
import android.util.*;
|
|
||||||
import android.view.*;
|
|
||||||
import android.view.ViewGroup.*;
|
|
||||||
|
|
||||||
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 android.view.View.MeasureSpec.*;
|
|
||||||
import static org.isoron.uhabits.utils.InterfaceUtils.*;
|
|
||||||
|
|
||||||
public class StreakChart extends View
|
|
||||||
{
|
|
||||||
private Paint paint;
|
|
||||||
|
|
||||||
private long minLength;
|
|
||||||
|
|
||||||
private long maxLength;
|
|
||||||
|
|
||||||
private int[] colors;
|
|
||||||
|
|
||||||
private int[] textColors;
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
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));
|
|
||||||
|
|
||||||
float round = dpToPixels(getContext(), 2);
|
|
||||||
canvas.drawRoundRect(rect.left + gap,
|
|
||||||
rect.top + paddingTopBottom,
|
|
||||||
rect.right - gap,
|
|
||||||
rect.bottom - paddingTopBottom,
|
|
||||||
round,
|
|
||||||
round,
|
|
||||||
paint);
|
|
||||||
|
|
||||||
float yOffset = rect.centerY() + 0.3f * em;
|
|
||||||
|
|
||||||
paint.setColor(percentageToTextColor(percentage));
|
|
||||||
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(textColors[1]);
|
|
||||||
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);
|
|
||||||
|
|
||||||
textColors = new int[3];
|
|
||||||
textColors[2] = res.getColor(R.attr.highContrastReverseTextColor);
|
|
||||||
textColors[1] = res.getColor(R.attr.mediumContrastTextColor);
|
|
||||||
textColors[0] = res.getColor(R.attr.lowContrastReverseTextColor);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 int percentageToTextColor(float percentage)
|
|
||||||
{
|
|
||||||
if (percentage >= 0.5f) return textColors[2];
|
|
||||||
return textColors[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
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,249 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
||||||
|
*
|
||||||
|
* This file is part of Loop Habit Tracker.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by the
|
||||||
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along
|
||||||
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.isoron.uhabits.activities.common.views
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.Paint
|
||||||
|
import android.graphics.RectF
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import org.isoron.uhabits.R
|
||||||
|
import org.isoron.uhabits.core.models.Streak
|
||||||
|
import org.isoron.uhabits.core.models.Timestamp
|
||||||
|
import org.isoron.uhabits.core.utils.DateUtils.Companion.getToday
|
||||||
|
import org.isoron.uhabits.utils.InterfaceUtils.dpToPixels
|
||||||
|
import org.isoron.uhabits.utils.InterfaceUtils.getDimension
|
||||||
|
import org.isoron.uhabits.utils.StyledResources
|
||||||
|
import java.text.DateFormat
|
||||||
|
import java.util.LinkedList
|
||||||
|
import java.util.Random
|
||||||
|
import java.util.TimeZone
|
||||||
|
import kotlin.math.floor
|
||||||
|
import kotlin.math.max
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
class StreakChart : View {
|
||||||
|
private var paint: Paint? = null
|
||||||
|
private var minLength: Long = 0
|
||||||
|
private var maxLength: Long = 0
|
||||||
|
private lateinit var colors: IntArray
|
||||||
|
private lateinit var textColors: IntArray
|
||||||
|
private var rect: RectF? = null
|
||||||
|
private var baseSize = 0
|
||||||
|
private var primaryColor = 0
|
||||||
|
private var streaks: List<Streak>? = null
|
||||||
|
private var isBackgroundTransparent = false
|
||||||
|
private var dateFormat: DateFormat? = null
|
||||||
|
private var internalWidth = 0
|
||||||
|
private var em = 0f
|
||||||
|
private var maxLabelWidth = 0f
|
||||||
|
private var textMargin = 0f
|
||||||
|
private var shouldShowLabels = false
|
||||||
|
private val textColor = 0
|
||||||
|
|
||||||
|
constructor(context: Context?) : super(context) {
|
||||||
|
init()
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(context: Context?, attrs: AttributeSet?) : 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
|
||||||
|
*/
|
||||||
|
val maxStreakCount: Int
|
||||||
|
get() = floor((measuredHeight / baseSize).toDouble()).toInt()
|
||||||
|
|
||||||
|
fun populateWithRandomData() {
|
||||||
|
var start: Timestamp = getToday()
|
||||||
|
val streaks: MutableList<Streak> = LinkedList()
|
||||||
|
for (i in 0..9) {
|
||||||
|
val length = Random().nextInt(100)
|
||||||
|
val end = start.plus(length)
|
||||||
|
streaks.add(Streak(start, end))
|
||||||
|
start = end.plus(1)
|
||||||
|
}
|
||||||
|
setStreaks(streaks)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setColor(color: Int) {
|
||||||
|
primaryColor = color
|
||||||
|
postInvalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setIsBackgroundTransparent(isBackgroundTransparent: Boolean) {
|
||||||
|
this.isBackgroundTransparent = isBackgroundTransparent
|
||||||
|
initColors()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setStreaks(streaks: List<Streak>?) {
|
||||||
|
this.streaks = streaks
|
||||||
|
initColors()
|
||||||
|
updateMaxMinLengths()
|
||||||
|
requestLayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDraw(canvas: Canvas) {
|
||||||
|
super.onDraw(canvas)
|
||||||
|
if (streaks!!.isEmpty()) return
|
||||||
|
rect!![0f, 0f, internalWidth.toFloat()] = baseSize.toFloat()
|
||||||
|
for (s in streaks!!) {
|
||||||
|
drawRow(canvas, s, rect)
|
||||||
|
rect!!.offset(0f, baseSize.toFloat())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMeasure(widthSpec: Int, heightSpec: Int) {
|
||||||
|
var widthSpec = widthSpec
|
||||||
|
var heightSpec = heightSpec
|
||||||
|
val params = layoutParams
|
||||||
|
if (params != null && params.height == ViewGroup.LayoutParams.WRAP_CONTENT) {
|
||||||
|
val width = MeasureSpec.getSize(widthSpec)
|
||||||
|
val height = streaks!!.size * baseSize
|
||||||
|
heightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
|
||||||
|
widthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY)
|
||||||
|
}
|
||||||
|
setMeasuredDimension(widthSpec, heightSpec)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSizeChanged(
|
||||||
|
width: Int,
|
||||||
|
height: Int,
|
||||||
|
oldWidth: Int,
|
||||||
|
oldHeight: Int
|
||||||
|
) {
|
||||||
|
this.internalWidth = width
|
||||||
|
val context = context
|
||||||
|
val minTextSize = getDimension(context, R.dimen.tinyTextSize)
|
||||||
|
val maxTextSize = getDimension(context, R.dimen.regularTextSize)
|
||||||
|
val textSize = baseSize * 0.5f
|
||||||
|
paint!!.textSize = max(min(textSize, maxTextSize), minTextSize)
|
||||||
|
em = paint!!.fontSpacing
|
||||||
|
textMargin = 0.5f * em
|
||||||
|
updateMaxMinLengths()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun drawRow(canvas: Canvas, streak: Streak, rect: RectF?) {
|
||||||
|
if (maxLength == 0L) return
|
||||||
|
val percentage = streak.length.toFloat() / maxLength
|
||||||
|
var availableWidth = internalWidth - 2 * maxLabelWidth
|
||||||
|
if (shouldShowLabels) availableWidth -= 2 * textMargin
|
||||||
|
var barWidth = percentage * availableWidth
|
||||||
|
val minBarWidth = paint!!.measureText(streak.length.toLong().toString()) + em
|
||||||
|
barWidth = max(barWidth, minBarWidth)
|
||||||
|
val gap = (internalWidth - barWidth) / 2
|
||||||
|
val paddingTopBottom = baseSize * 0.05f
|
||||||
|
paint!!.color = percentageToColor(percentage)
|
||||||
|
val round = dpToPixels(context, 2f)
|
||||||
|
canvas.drawRoundRect(
|
||||||
|
rect!!.left + gap,
|
||||||
|
rect.top + paddingTopBottom,
|
||||||
|
rect.right - gap,
|
||||||
|
rect.bottom - paddingTopBottom,
|
||||||
|
round,
|
||||||
|
round,
|
||||||
|
paint!!
|
||||||
|
)
|
||||||
|
val yOffset = rect.centerY() + 0.3f * em
|
||||||
|
paint!!.color = percentageToTextColor(percentage)
|
||||||
|
paint!!.textAlign = Paint.Align.CENTER
|
||||||
|
canvas.drawText(
|
||||||
|
streak.length.toLong().toString(),
|
||||||
|
rect.centerX(),
|
||||||
|
yOffset,
|
||||||
|
paint!!
|
||||||
|
)
|
||||||
|
if (shouldShowLabels) {
|
||||||
|
val startLabel = dateFormat!!.format(streak.start.toJavaDate())
|
||||||
|
val endLabel = dateFormat!!.format(streak.end.toJavaDate())
|
||||||
|
paint!!.color = textColors[1]
|
||||||
|
paint!!.textAlign = Paint.Align.RIGHT
|
||||||
|
canvas.drawText(startLabel, gap - textMargin, yOffset, paint!!)
|
||||||
|
paint!!.textAlign = Paint.Align.LEFT
|
||||||
|
canvas.drawText(endLabel, internalWidth - gap + textMargin, yOffset, paint!!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun init() {
|
||||||
|
initPaints()
|
||||||
|
initColors()
|
||||||
|
streaks = emptyList()
|
||||||
|
val newDateFormat = DateFormat.getDateInstance(DateFormat.MEDIUM)
|
||||||
|
if (!isInEditMode) newDateFormat.timeZone = TimeZone.getTimeZone("GMT")
|
||||||
|
dateFormat = newDateFormat
|
||||||
|
rect = RectF()
|
||||||
|
baseSize = resources.getDimensionPixelSize(R.dimen.baseSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initColors() {
|
||||||
|
val red = Color.red(primaryColor)
|
||||||
|
val green = Color.green(primaryColor)
|
||||||
|
val blue = Color.blue(primaryColor)
|
||||||
|
val res = StyledResources(context)
|
||||||
|
colors = IntArray(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)
|
||||||
|
textColors = IntArray(3)
|
||||||
|
textColors[2] = res.getColor(R.attr.highContrastReverseTextColor)
|
||||||
|
textColors[1] = res.getColor(R.attr.mediumContrastTextColor)
|
||||||
|
textColors[0] = res.getColor(R.attr.lowContrastReverseTextColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initPaints() {
|
||||||
|
paint = Paint()
|
||||||
|
paint!!.textAlign = Paint.Align.CENTER
|
||||||
|
paint!!.isAntiAlias = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun percentageToColor(percentage: Float): Int {
|
||||||
|
if (percentage >= 1.0f) return colors[3]
|
||||||
|
if (percentage >= 0.8f) return colors[2]
|
||||||
|
return if (percentage >= 0.5f) colors[1] else colors[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun percentageToTextColor(percentage: Float): Int {
|
||||||
|
return if (percentage >= 0.5f) textColors[2] else textColors[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateMaxMinLengths() {
|
||||||
|
maxLength = 0
|
||||||
|
minLength = Long.MAX_VALUE
|
||||||
|
shouldShowLabels = true
|
||||||
|
for (s in streaks!!) {
|
||||||
|
maxLength = max(maxLength, s.length.toLong())
|
||||||
|
minLength = min(minLength, s.length.toLong())
|
||||||
|
val lw1 = paint!!.measureText(dateFormat!!.format(s.start.toJavaDate()))
|
||||||
|
val lw2 = paint!!.measureText(dateFormat!!.format(s.end.toJavaDate()))
|
||||||
|
maxLabelWidth = max(maxLabelWidth, max(lw1, lw2))
|
||||||
|
}
|
||||||
|
if (internalWidth - 2 * maxLabelWidth < internalWidth * 0.25f) {
|
||||||
|
maxLabelWidth = 0f
|
||||||
|
shouldShowLabels = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,226 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
|
||||||
*
|
|
||||||
* This file is part of Loop Habit Tracker.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by the
|
|
||||||
* Free Software Foundation, either version 3 of the License, or (at your
|
|
||||||
* option) any later version.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
|
||||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
|
||||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
||||||
* more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License along
|
|
||||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.isoron.uhabits.activities.common.views;
|
|
||||||
|
|
||||||
import android.content.*;
|
|
||||||
import android.graphics.*;
|
|
||||||
import android.util.*;
|
|
||||||
import android.view.*;
|
|
||||||
|
|
||||||
import org.isoron.uhabits.*;
|
|
||||||
import org.isoron.uhabits.activities.habits.list.views.*;
|
|
||||||
import org.isoron.uhabits.utils.*;
|
|
||||||
|
|
||||||
import java.util.*;
|
|
||||||
|
|
||||||
import static android.view.View.MeasureSpec.*;
|
|
||||||
import static org.isoron.uhabits.utils.InterfaceUtils.*;
|
|
||||||
|
|
||||||
public class TargetChart extends View
|
|
||||||
{
|
|
||||||
private Paint paint;
|
|
||||||
private int baseSize;
|
|
||||||
private int primaryColor;
|
|
||||||
private int mediumContrastTextColor;
|
|
||||||
private int highContrastReverseTextColor;
|
|
||||||
private int lowContrastTextColor;
|
|
||||||
private RectF rect = new RectF();
|
|
||||||
private RectF barRect = new RectF();
|
|
||||||
private List<Double> values = Collections.emptyList();
|
|
||||||
private List<String> labels = Collections.emptyList();
|
|
||||||
private List<Double> targets = Collections.emptyList();
|
|
||||||
private float maxLabelSize;
|
|
||||||
private float tinyTextSize;
|
|
||||||
|
|
||||||
public TargetChart(Context context)
|
|
||||||
{
|
|
||||||
super(context);
|
|
||||||
init();
|
|
||||||
}
|
|
||||||
|
|
||||||
public TargetChart(Context context, AttributeSet attrs)
|
|
||||||
{
|
|
||||||
super(context, attrs);
|
|
||||||
init();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void populateWithRandomData()
|
|
||||||
{
|
|
||||||
labels = new ArrayList<>();
|
|
||||||
values = new ArrayList<>();
|
|
||||||
targets = new ArrayList<>();
|
|
||||||
for (int i = 0; i < 5; i++) {
|
|
||||||
double percentage = new Random().nextDouble();
|
|
||||||
targets.add(new Random().nextDouble() * 1000.0);
|
|
||||||
values.add(targets.get(i) * percentage * 1.2);
|
|
||||||
labels.add(String.format(Locale.US, "Label %d", i + 1));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setColor(int color)
|
|
||||||
{
|
|
||||||
this.primaryColor = color;
|
|
||||||
postInvalidate();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onDraw(Canvas canvas)
|
|
||||||
{
|
|
||||||
super.onDraw(canvas);
|
|
||||||
if (labels.size() == 0) return;
|
|
||||||
|
|
||||||
maxLabelSize = 0;
|
|
||||||
for (String label : labels) {
|
|
||||||
paint.setTextSize(tinyTextSize);
|
|
||||||
float len = paint.measureText(label);
|
|
||||||
maxLabelSize = Math.max(maxLabelSize, len);
|
|
||||||
}
|
|
||||||
|
|
||||||
float marginTop = (getHeight() - baseSize * labels.size()) / 2.0f;
|
|
||||||
rect.set(0, marginTop, getWidth(), marginTop + baseSize);
|
|
||||||
for (int i = 0; i < labels.size(); i++) {
|
|
||||||
drawRow(canvas, i, rect);
|
|
||||||
rect.offset(0, baseSize);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onMeasure(int widthSpec, int heightSpec)
|
|
||||||
{
|
|
||||||
baseSize = getResources().getDimensionPixelSize(R.dimen.baseSize);
|
|
||||||
|
|
||||||
int width = getSize(widthSpec);
|
|
||||||
int height = labels.size() * baseSize;
|
|
||||||
|
|
||||||
ViewGroup.LayoutParams params = getLayoutParams();
|
|
||||||
if (params != null && params.height == ViewGroup.LayoutParams.MATCH_PARENT) {
|
|
||||||
height = getSize(heightSpec);
|
|
||||||
if (labels.size() > 0) baseSize = height / labels.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
heightSpec = makeMeasureSpec(height, EXACTLY);
|
|
||||||
widthSpec = makeMeasureSpec(width, EXACTLY);
|
|
||||||
setMeasuredDimension(widthSpec, heightSpec);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void drawRow(Canvas canvas, int row, RectF rect)
|
|
||||||
{
|
|
||||||
float padding = dpToPixels(getContext(), 4);
|
|
||||||
float round = dpToPixels(getContext(), 2);
|
|
||||||
float stop = maxLabelSize + padding * 2;
|
|
||||||
|
|
||||||
paint.setColor(mediumContrastTextColor);
|
|
||||||
|
|
||||||
// Draw label
|
|
||||||
paint.setTextSize(tinyTextSize);
|
|
||||||
paint.setTextAlign(Paint.Align.RIGHT);
|
|
||||||
float yTextAdjust = (paint.descent() + paint.ascent()) / 2.0f;
|
|
||||||
canvas.drawText(labels.get(row),
|
|
||||||
rect.left + stop - padding,
|
|
||||||
rect.centerY() - yTextAdjust,
|
|
||||||
paint);
|
|
||||||
|
|
||||||
// Draw background box
|
|
||||||
paint.setColor(lowContrastTextColor);
|
|
||||||
barRect.set(rect.left + stop + padding,
|
|
||||||
rect.top + baseSize * 0.05f,
|
|
||||||
rect.right - padding,
|
|
||||||
rect.bottom - baseSize * 0.05f);
|
|
||||||
canvas.drawRoundRect(barRect, round, round, paint);
|
|
||||||
|
|
||||||
float percentage = (float) (values.get(row) / targets.get(row));
|
|
||||||
percentage = Math.min(1.0f, percentage);
|
|
||||||
|
|
||||||
// Draw completed box
|
|
||||||
float completedWidth = percentage * barRect.width();
|
|
||||||
if (completedWidth > 0 && completedWidth < 2 * round) {
|
|
||||||
completedWidth = 2 * round;
|
|
||||||
}
|
|
||||||
float remainingWidth = barRect.width() - completedWidth;
|
|
||||||
|
|
||||||
paint.setColor(primaryColor);
|
|
||||||
barRect.set(barRect.left,
|
|
||||||
barRect.top,
|
|
||||||
barRect.left + completedWidth,
|
|
||||||
barRect.bottom);
|
|
||||||
canvas.drawRoundRect(barRect, round, round, paint);
|
|
||||||
|
|
||||||
// Draw values
|
|
||||||
paint.setColor(Color.WHITE);
|
|
||||||
paint.setTextSize(tinyTextSize);
|
|
||||||
paint.setTextAlign(Paint.Align.CENTER);
|
|
||||||
yTextAdjust = (paint.descent() + paint.ascent()) / 2.0f;
|
|
||||||
|
|
||||||
double remaining = targets.get(row) - values.get(row);
|
|
||||||
String completedText = NumberButtonViewKt.toShortString(values.get(row));
|
|
||||||
String remainingText = NumberButtonViewKt.toShortString(remaining);
|
|
||||||
|
|
||||||
if (completedWidth > paint.measureText(completedText) + 2 * padding) {
|
|
||||||
paint.setColor(highContrastReverseTextColor);
|
|
||||||
canvas.drawText(completedText,
|
|
||||||
barRect.centerX(),
|
|
||||||
barRect.centerY() - yTextAdjust,
|
|
||||||
paint);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (remainingWidth > paint.measureText(remainingText) + 2 * padding) {
|
|
||||||
paint.setColor(mediumContrastTextColor);
|
|
||||||
barRect.set(rect.left + stop + padding + completedWidth,
|
|
||||||
barRect.top,
|
|
||||||
rect.right - padding,
|
|
||||||
barRect.bottom);
|
|
||||||
canvas.drawText(remainingText,
|
|
||||||
barRect.centerX(),
|
|
||||||
barRect.centerY() - yTextAdjust,
|
|
||||||
paint);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void init()
|
|
||||||
{
|
|
||||||
paint = new Paint();
|
|
||||||
paint.setTextAlign(Paint.Align.CENTER);
|
|
||||||
paint.setAntiAlias(true);
|
|
||||||
|
|
||||||
StyledResources res = new StyledResources(getContext());
|
|
||||||
lowContrastTextColor = res.getColor(R.attr.lowContrastTextColor);
|
|
||||||
mediumContrastTextColor = res.getColor(R.attr.mediumContrastTextColor);
|
|
||||||
highContrastReverseTextColor = res.getColor(R.attr.highContrastReverseTextColor);
|
|
||||||
tinyTextSize = getDimension(getContext(), R.dimen.tinyTextSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setValues(List<Double> values)
|
|
||||||
{
|
|
||||||
this.values = values;
|
|
||||||
requestLayout();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setLabels(List<String> labels)
|
|
||||||
{
|
|
||||||
this.labels = labels;
|
|
||||||
requestLayout();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setTargets(List<Double> targets)
|
|
||||||
{
|
|
||||||
this.targets = targets;
|
|
||||||
requestLayout();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
||||||
|
*
|
||||||
|
* This file is part of Loop Habit Tracker.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by the
|
||||||
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along
|
||||||
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.isoron.uhabits.activities.common.views
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.Paint
|
||||||
|
import android.graphics.RectF
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import org.isoron.uhabits.R
|
||||||
|
import org.isoron.uhabits.activities.habits.list.views.toShortString
|
||||||
|
import org.isoron.uhabits.utils.InterfaceUtils.dpToPixels
|
||||||
|
import org.isoron.uhabits.utils.InterfaceUtils.getDimension
|
||||||
|
import org.isoron.uhabits.utils.StyledResources
|
||||||
|
import kotlin.math.max
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
class TargetChart : View {
|
||||||
|
private var paint: Paint? = null
|
||||||
|
private var baseSize = 0
|
||||||
|
private var primaryColor = 0
|
||||||
|
private var mediumContrastTextColor = 0
|
||||||
|
private var highContrastReverseTextColor = 0
|
||||||
|
private var lowContrastTextColor = 0
|
||||||
|
private val rect = RectF()
|
||||||
|
private val barRect = RectF()
|
||||||
|
private var values = emptyList<Double>()
|
||||||
|
private var labels = emptyList<String>()
|
||||||
|
private var targets = emptyList<Double>()
|
||||||
|
private var maxLabelSize = 0f
|
||||||
|
private var tinyTextSize = 0f
|
||||||
|
|
||||||
|
constructor(context: Context?) : super(context) {
|
||||||
|
init()
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) {
|
||||||
|
init()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setColor(color: Int) {
|
||||||
|
primaryColor = color
|
||||||
|
postInvalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDraw(canvas: Canvas) {
|
||||||
|
super.onDraw(canvas)
|
||||||
|
if (labels.isEmpty()) return
|
||||||
|
maxLabelSize = 0f
|
||||||
|
for (label in labels) {
|
||||||
|
paint!!.textSize = tinyTextSize
|
||||||
|
val len = paint!!.measureText(label)
|
||||||
|
maxLabelSize = max(maxLabelSize, len)
|
||||||
|
}
|
||||||
|
val marginTop = (height - baseSize * labels.size) / 2.0f
|
||||||
|
rect[0f, marginTop, width.toFloat()] = marginTop + baseSize
|
||||||
|
for (i in labels.indices) {
|
||||||
|
drawRow(canvas, i, rect)
|
||||||
|
rect.offset(0f, baseSize.toFloat())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMeasure(widthSpec: Int, heightSpec: Int) {
|
||||||
|
var widthSpec = widthSpec
|
||||||
|
var heightSpec = heightSpec
|
||||||
|
baseSize = resources.getDimensionPixelSize(R.dimen.baseSize)
|
||||||
|
val width = MeasureSpec.getSize(widthSpec)
|
||||||
|
var height = labels.size * baseSize
|
||||||
|
val params = layoutParams
|
||||||
|
if (params != null && params.height == ViewGroup.LayoutParams.MATCH_PARENT) {
|
||||||
|
height = MeasureSpec.getSize(heightSpec)
|
||||||
|
if (labels.isNotEmpty()) baseSize = height / labels.size
|
||||||
|
}
|
||||||
|
heightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
|
||||||
|
widthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY)
|
||||||
|
setMeasuredDimension(widthSpec, heightSpec)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun drawRow(canvas: Canvas, row: Int, rect: RectF) {
|
||||||
|
val padding = dpToPixels(context, 4f)
|
||||||
|
val round = dpToPixels(context, 2f)
|
||||||
|
val stop = maxLabelSize + padding * 2
|
||||||
|
paint!!.color = mediumContrastTextColor
|
||||||
|
|
||||||
|
// Draw label
|
||||||
|
paint!!.textSize = tinyTextSize
|
||||||
|
paint!!.textAlign = Paint.Align.RIGHT
|
||||||
|
var yTextAdjust = (paint!!.descent() + paint!!.ascent()) / 2.0f
|
||||||
|
canvas.drawText(
|
||||||
|
labels[row],
|
||||||
|
rect.left + stop - padding,
|
||||||
|
rect.centerY() - yTextAdjust,
|
||||||
|
paint!!
|
||||||
|
)
|
||||||
|
|
||||||
|
// Draw background box
|
||||||
|
paint!!.color = lowContrastTextColor
|
||||||
|
barRect[rect.left + stop + padding, rect.top + baseSize * 0.05f, rect.right - padding] =
|
||||||
|
rect.bottom - baseSize * 0.05f
|
||||||
|
canvas.drawRoundRect(barRect, round, round, paint!!)
|
||||||
|
var percentage = (values[row] / targets[row]).toFloat()
|
||||||
|
percentage = min(1.0f, percentage)
|
||||||
|
|
||||||
|
// Draw completed box
|
||||||
|
var completedWidth = percentage * barRect.width()
|
||||||
|
if (completedWidth > 0 && completedWidth < 2 * round) {
|
||||||
|
completedWidth = 2 * round
|
||||||
|
}
|
||||||
|
val remainingWidth = barRect.width() - completedWidth
|
||||||
|
paint!!.color = primaryColor
|
||||||
|
barRect[barRect.left, barRect.top, barRect.left + completedWidth] = barRect.bottom
|
||||||
|
canvas.drawRoundRect(barRect, round, round, paint!!)
|
||||||
|
|
||||||
|
// Draw values
|
||||||
|
paint!!.color = Color.WHITE
|
||||||
|
paint!!.textSize = tinyTextSize
|
||||||
|
paint!!.textAlign = Paint.Align.CENTER
|
||||||
|
yTextAdjust = (paint!!.descent() + paint!!.ascent()) / 2.0f
|
||||||
|
val remaining = targets[row] - values[row]
|
||||||
|
val completedText = values[row].toShortString()
|
||||||
|
val remainingText = remaining.toShortString()
|
||||||
|
if (completedWidth > paint!!.measureText(completedText) + 2 * padding) {
|
||||||
|
paint!!.color = highContrastReverseTextColor
|
||||||
|
canvas.drawText(
|
||||||
|
completedText,
|
||||||
|
barRect.centerX(),
|
||||||
|
barRect.centerY() - yTextAdjust,
|
||||||
|
paint!!
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (remainingWidth > paint!!.measureText(remainingText) + 2 * padding) {
|
||||||
|
paint!!.color = mediumContrastTextColor
|
||||||
|
barRect[rect.left + stop + padding + completedWidth, barRect.top, rect.right - padding] =
|
||||||
|
barRect.bottom
|
||||||
|
canvas.drawText(
|
||||||
|
remainingText,
|
||||||
|
barRect.centerX(),
|
||||||
|
barRect.centerY() - yTextAdjust,
|
||||||
|
paint!!
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun init() {
|
||||||
|
paint = Paint()
|
||||||
|
paint!!.textAlign = Paint.Align.CENTER
|
||||||
|
paint!!.isAntiAlias = true
|
||||||
|
val res = StyledResources(context)
|
||||||
|
lowContrastTextColor = res.getColor(R.attr.lowContrastTextColor)
|
||||||
|
mediumContrastTextColor = res.getColor(R.attr.mediumContrastTextColor)
|
||||||
|
highContrastReverseTextColor = res.getColor(R.attr.highContrastReverseTextColor)
|
||||||
|
tinyTextSize = getDimension(context, R.dimen.tinyTextSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setValues(values: List<Double>) {
|
||||||
|
this.values = values
|
||||||
|
requestLayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setLabels(labels: List<String>) {
|
||||||
|
this.labels = labels
|
||||||
|
requestLayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setTargets(targets: List<Double>) {
|
||||||
|
this.targets = targets
|
||||||
|
requestLayout()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -40,8 +40,8 @@ class TaskProgressBar(
|
|||||||
isIndeterminate = true
|
isIndeterminate = true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onTaskStarted(task: Task?) = update()
|
override fun onTaskStarted(task: Task) = update()
|
||||||
override fun onTaskFinished(task: Task?) = update()
|
override fun onTaskFinished(task: Task) = update()
|
||||||
|
|
||||||
override fun onAttachedToWindow() {
|
override fun onAttachedToWindow() {
|
||||||
super.onAttachedToWindow()
|
super.onAttachedToWindow()
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ import org.isoron.uhabits.activities.habits.list.views.HabitCardListAdapter
|
|||||||
import org.isoron.uhabits.core.preferences.Preferences
|
import org.isoron.uhabits.core.preferences.Preferences
|
||||||
import org.isoron.uhabits.core.sync.SyncManager
|
import org.isoron.uhabits.core.sync.SyncManager
|
||||||
import org.isoron.uhabits.core.tasks.TaskRunner
|
import org.isoron.uhabits.core.tasks.TaskRunner
|
||||||
import org.isoron.uhabits.core.ui.ThemeSwitcher.THEME_DARK
|
import org.isoron.uhabits.core.ui.ThemeSwitcher.Companion.THEME_DARK
|
||||||
import org.isoron.uhabits.core.utils.MidnightTimer
|
import org.isoron.uhabits.core.utils.MidnightTimer
|
||||||
import org.isoron.uhabits.database.AutoBackup
|
import org.isoron.uhabits.database.AutoBackup
|
||||||
import org.isoron.uhabits.inject.ActivityContextModule
|
import org.isoron.uhabits.inject.ActivityContextModule
|
||||||
|
|||||||
@@ -173,8 +173,8 @@ class ListHabitsScreen
|
|||||||
ConfirmDeleteDialog(activity, callback, quantity).show()
|
ConfirmDeleteDialog(activity, callback, quantity).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun showEditHabitsScreen(habits: List<Habit>) {
|
override fun showEditHabitsScreen(selected: List<Habit>) {
|
||||||
val intent = intentFactory.startEditActivity(activity, habits[0])
|
val intent = intentFactory.startEditActivity(activity, selected[0])
|
||||||
activity.startActivity(intent)
|
activity.startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,8 +183,8 @@ class ListHabitsScreen
|
|||||||
activity.startActivity(intent)
|
activity.startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun showHabitScreen(habit: Habit) {
|
override fun showHabitScreen(h: Habit) {
|
||||||
val intent = intentFactory.startShowHabitActivity(activity, habit)
|
val intent = intentFactory.startShowHabitActivity(activity, h)
|
||||||
activity.startActivity(intent)
|
activity.startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,10 +230,7 @@ class ListHabitsScreen
|
|||||||
activity.startActivityForResult(intent, REQUEST_SETTINGS)
|
activity.startActivityForResult(intent, REQUEST_SETTINGS)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun showColorPicker(
|
override fun showColorPicker(defaultColor: PaletteColor, callback: OnColorPickedCallback) {
|
||||||
defaultColor: PaletteColor,
|
|
||||||
callback: OnColorPickedCallback
|
|
||||||
) {
|
|
||||||
val picker = colorPickerFactory.create(defaultColor)
|
val picker = colorPickerFactory.create(defaultColor)
|
||||||
picker.setListener(callback)
|
picker.setListener(callback)
|
||||||
picker.show(activity.supportFragmentManager, "picker")
|
picker.show(activity.supportFragmentManager, "picker")
|
||||||
@@ -290,13 +287,17 @@ class ListHabitsScreen
|
|||||||
private fun onImportData(file: File, onFinished: () -> Unit) {
|
private fun onImportData(file: File, onFinished: () -> Unit) {
|
||||||
taskRunner.execute(
|
taskRunner.execute(
|
||||||
importTaskFactory.create(file) { result ->
|
importTaskFactory.create(file) { result ->
|
||||||
if (result == ImportDataTask.SUCCESS) {
|
when (result) {
|
||||||
adapter.refresh()
|
ImportDataTask.SUCCESS -> {
|
||||||
activity.showMessage(activity.resources.getString(R.string.habits_imported))
|
adapter.refresh()
|
||||||
} else if (result == ImportDataTask.NOT_RECOGNIZED) {
|
activity.showMessage(activity.resources.getString(R.string.habits_imported))
|
||||||
activity.showMessage(activity.resources.getString(R.string.file_not_recognized))
|
}
|
||||||
} else {
|
ImportDataTask.NOT_RECOGNIZED -> {
|
||||||
activity.showMessage(activity.resources.getString(R.string.could_not_import))
|
activity.showMessage(activity.resources.getString(R.string.file_not_recognized))
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
activity.showMessage(activity.resources.getString(R.string.could_not_import))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
onFinished()
|
onFinished()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ class HabitCardListAdapter @Inject constructor(
|
|||||||
ListHabitsSelectionMenuBehavior.Adapter {
|
ListHabitsSelectionMenuBehavior.Adapter {
|
||||||
val observable: ModelObservable = ModelObservable()
|
val observable: ModelObservable = ModelObservable()
|
||||||
private var listView: HabitCardListView? = null
|
private var listView: HabitCardListView? = null
|
||||||
private val selected: LinkedList<Habit> = LinkedList()
|
override val selected: LinkedList<Habit> = LinkedList()
|
||||||
override fun atMidnight() {
|
override fun atMidnight() {
|
||||||
cache.refreshAllHabits()
|
cache.refreshAllHabits()
|
||||||
}
|
}
|
||||||
@@ -90,10 +90,6 @@ class HabitCardListAdapter @Inject constructor(
|
|||||||
return getItem(position)!!.id!!
|
return getItem(position)!!.id!!
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getSelected(): List<Habit> {
|
|
||||||
return LinkedList(selected)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns whether list of selected items is empty.
|
* Returns whether list of selected items is empty.
|
||||||
*
|
*
|
||||||
@@ -158,8 +154,8 @@ class HabitCardListAdapter @Inject constructor(
|
|||||||
observable.notifyListeners()
|
observable.notifyListeners()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onItemMoved(fromPosition: Int, toPosition: Int) {
|
override fun onItemMoved(oldPosition: Int, newPosition: Int) {
|
||||||
notifyItemMoved(fromPosition, toPosition)
|
notifyItemMoved(oldPosition, newPosition)
|
||||||
observable.notifyListeners()
|
observable.notifyListeners()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,10 +178,10 @@ class HabitCardListAdapter @Inject constructor(
|
|||||||
* database operation to finish, the cache can be modified to reflect the
|
* database operation to finish, the cache can be modified to reflect the
|
||||||
* changes immediately.
|
* changes immediately.
|
||||||
*
|
*
|
||||||
* @param habits list of habits to be removed
|
* @param selected list of habits to be removed
|
||||||
*/
|
*/
|
||||||
override fun performRemove(habits: List<Habit>) {
|
override fun performRemove(selected: List<Habit>) {
|
||||||
for (habit in habits) cache.remove(habit.id!!)
|
for (habit in selected) cache.remove(habit.id!!)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -225,19 +221,19 @@ class HabitCardListAdapter @Inject constructor(
|
|||||||
this.listView = listView
|
this.listView = listView
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setPrimaryOrder(order: HabitList.Order) {
|
override var primaryOrder: HabitList.Order
|
||||||
cache.primaryOrder = order
|
get() = cache.primaryOrder
|
||||||
preferences.defaultPrimaryOrder = order
|
set(value) {
|
||||||
}
|
cache.primaryOrder = value
|
||||||
|
preferences.defaultPrimaryOrder = value
|
||||||
|
}
|
||||||
|
|
||||||
override fun setSecondaryOrder(order: HabitList.Order) {
|
override var secondaryOrder: HabitList.Order
|
||||||
cache.secondaryOrder = order
|
get() = cache.secondaryOrder
|
||||||
preferences.defaultSecondaryOrder = order
|
set(value) {
|
||||||
}
|
cache.secondaryOrder = value
|
||||||
|
preferences.defaultSecondaryOrder = value
|
||||||
override fun getPrimaryOrder(): HabitList.Order {
|
}
|
||||||
return cache.primaryOrder
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Selects or deselects the item at a given position.
|
* Selects or deselects the item at a given position.
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ class HabitCardListView(
|
|||||||
super.onRestoreInstanceState(state)
|
super.onRestoreInstanceState(state)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
dataOffset = state.bundle.getInt("dataOffset")
|
dataOffset = state.bundle!!.getInt("dataOffset")
|
||||||
super.onRestoreInstanceState(state.superState)
|
super.onRestoreInstanceState(state.superState)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,12 +20,11 @@
|
|||||||
package org.isoron.uhabits.activities.habits.list.views
|
package org.isoron.uhabits.activities.habits.list.views
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.graphics.text.LineBreaker.BREAK_STRATEGY_BALANCED
|
||||||
import android.os.Build.VERSION.SDK_INT
|
import android.os.Build.VERSION.SDK_INT
|
||||||
import android.os.Build.VERSION_CODES.LOLLIPOP
|
|
||||||
import android.os.Build.VERSION_CODES.M
|
import android.os.Build.VERSION_CODES.M
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.text.Layout
|
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import android.view.Gravity
|
import android.view.Gravity
|
||||||
import android.view.View
|
import android.view.View
|
||||||
@@ -90,10 +89,10 @@ class HabitCardView(
|
|||||||
}
|
}
|
||||||
|
|
||||||
var score
|
var score
|
||||||
get() = scoreRing.percentage.toDouble()
|
get() = scoreRing.getPercentage().toDouble()
|
||||||
set(value) {
|
set(value) {
|
||||||
scoreRing.percentage = value.toFloat()
|
scoreRing.setPercentage(value.toFloat())
|
||||||
scoreRing.precision = 1.0f / 16
|
scoreRing.setPrecision(1.0f / 16)
|
||||||
}
|
}
|
||||||
|
|
||||||
var unit
|
var unit
|
||||||
@@ -137,7 +136,7 @@ class HabitCardView(
|
|||||||
maxLines = 2
|
maxLines = 2
|
||||||
ellipsize = TextUtils.TruncateAt.END
|
ellipsize = TextUtils.TruncateAt.END
|
||||||
layoutParams = LinearLayout.LayoutParams(0, WRAP_CONTENT, 1f)
|
layoutParams = LinearLayout.LayoutParams(0, WRAP_CONTENT, 1f)
|
||||||
if (SDK_INT >= M) breakStrategy = Layout.BREAK_STRATEGY_BALANCED
|
if (SDK_INT >= M) breakStrategy = BREAK_STRATEGY_BALANCED
|
||||||
}
|
}
|
||||||
|
|
||||||
checkmarkPanel = checkmarkPanelFactory.create().apply {
|
checkmarkPanel = checkmarkPanelFactory.create().apply {
|
||||||
@@ -159,7 +158,7 @@ class HabitCardView(
|
|||||||
gravity = Gravity.CENTER_VERTICAL
|
gravity = Gravity.CENTER_VERTICAL
|
||||||
orientation = LinearLayout.HORIZONTAL
|
orientation = LinearLayout.HORIZONTAL
|
||||||
layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
|
layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
|
||||||
if (SDK_INT >= LOLLIPOP) elevation = dp(1f)
|
elevation = dp(1f)
|
||||||
|
|
||||||
addView(scoreRing)
|
addView(scoreRing)
|
||||||
addView(label)
|
addView(label)
|
||||||
@@ -167,8 +166,7 @@ class HabitCardView(
|
|||||||
addView(numberPanel)
|
addView(numberPanel)
|
||||||
|
|
||||||
setOnTouchListener { v, event ->
|
setOnTouchListener { v, event ->
|
||||||
if (SDK_INT >= LOLLIPOP)
|
v.background.setHotspot(event.x, event.y)
|
||||||
v.background.setHotspot(event.x, event.y)
|
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -225,7 +223,7 @@ class HabitCardView(
|
|||||||
setTextColor(c)
|
setTextColor(c)
|
||||||
}
|
}
|
||||||
scoreRing.apply {
|
scoreRing.apply {
|
||||||
color = c
|
setColor(c)
|
||||||
}
|
}
|
||||||
checkmarkPanel.apply {
|
checkmarkPanel.apply {
|
||||||
color = c
|
color = c
|
||||||
@@ -247,7 +245,7 @@ class HabitCardView(
|
|||||||
|
|
||||||
private fun triggerRipple(x: Float, y: Float) {
|
private fun triggerRipple(x: Float, y: Float) {
|
||||||
val background = innerFrame.background
|
val background = innerFrame.background
|
||||||
if (SDK_INT >= LOLLIPOP) background.setHotspot(x, y)
|
background.setHotspot(x, y)
|
||||||
background.state = intArrayOf(
|
background.state = intArrayOf(
|
||||||
android.R.attr.state_pressed,
|
android.R.attr.state_pressed,
|
||||||
android.R.attr.state_enabled
|
android.R.attr.state_enabled
|
||||||
@@ -256,14 +254,6 @@ class HabitCardView(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun updateBackground(isSelected: Boolean) {
|
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) {
|
val background = when (isSelected) {
|
||||||
true -> R.drawable.selected_box
|
true -> R.drawable.selected_box
|
||||||
|
|||||||
@@ -25,8 +25,6 @@ import android.graphics.Color
|
|||||||
import android.graphics.Paint
|
import android.graphics.Paint
|
||||||
import android.graphics.RectF
|
import android.graphics.RectF
|
||||||
import android.graphics.Typeface
|
import android.graphics.Typeface
|
||||||
import android.os.Build.VERSION.SDK_INT
|
|
||||||
import android.os.Build.VERSION_CODES.LOLLIPOP
|
|
||||||
import android.text.TextPaint
|
import android.text.TextPaint
|
||||||
import android.view.View.MeasureSpec.EXACTLY
|
import android.view.View.MeasureSpec.EXACTLY
|
||||||
import org.isoron.uhabits.R
|
import org.isoron.uhabits.R
|
||||||
@@ -60,7 +58,7 @@ class HeaderView(
|
|||||||
init {
|
init {
|
||||||
setScrollerBucketSize(dim(R.dimen.checkmarkWidth).toInt())
|
setScrollerBucketSize(dim(R.dimen.checkmarkWidth).toInt())
|
||||||
setBackgroundColor(sres.getColor(R.attr.headerBackgroundColor))
|
setBackgroundColor(sres.getColor(R.attr.headerBackgroundColor))
|
||||||
if (SDK_INT >= LOLLIPOP) elevation = dp(2.0f)
|
elevation = dp(2.0f)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun atMidnight() {
|
override fun atMidnight() {
|
||||||
|
|||||||
@@ -161,15 +161,19 @@ class NumberButtonView(
|
|||||||
val label: String
|
val label: String
|
||||||
val typeface: Typeface
|
val typeface: Typeface
|
||||||
|
|
||||||
if (value >= 0) {
|
when {
|
||||||
label = value.toShortString()
|
value >= 0 -> {
|
||||||
typeface = BOLD_TYPEFACE
|
label = value.toShortString()
|
||||||
} else if (preferences.areQuestionMarksEnabled()) {
|
typeface = BOLD_TYPEFACE
|
||||||
label = resources.getString(R.string.fa_question)
|
}
|
||||||
typeface = getFontAwesome()
|
preferences.areQuestionMarksEnabled() -> {
|
||||||
} else {
|
label = resources.getString(R.string.fa_question)
|
||||||
label = "0"
|
typeface = getFontAwesome()
|
||||||
typeface = BOLD_TYPEFACE
|
}
|
||||||
|
else -> {
|
||||||
|
label = "0"
|
||||||
|
typeface = BOLD_TYPEFACE
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pBold.color = activeColor
|
pBold.color = activeColor
|
||||||
|
|||||||
@@ -49,8 +49,9 @@ class OverviewCardView(context: Context, attrs: AttributeSet) : LinearLayout(con
|
|||||||
binding.monthDiffLabel.text = formatPercentageDiff(state.scoreMonthDiff)
|
binding.monthDiffLabel.text = formatPercentageDiff(state.scoreMonthDiff)
|
||||||
binding.scoreLabel.setTextColor(androidColor)
|
binding.scoreLabel.setTextColor(androidColor)
|
||||||
binding.scoreLabel.text = String.format("%.0f%%", state.scoreToday * 100)
|
binding.scoreLabel.text = String.format("%.0f%%", state.scoreToday * 100)
|
||||||
binding.scoreRing.color = androidColor
|
binding.scoreRing.setColor(androidColor)
|
||||||
binding.scoreRing.percentage = state.scoreToday
|
binding.scoreRing.setPercentage(state.scoreToday)
|
||||||
|
|
||||||
binding.title.setTextColor(androidColor)
|
binding.title.setTextColor(androidColor)
|
||||||
binding.totalCountLabel.setTextColor(androidColor)
|
binding.totalCountLabel.setTextColor(androidColor)
|
||||||
binding.totalCountLabel.text = state.totalCount.toString()
|
binding.totalCountLabel.text = state.totalCount.toString()
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ class SettingsFragment : PreferenceFragmentCompat(), OnSharedPreferenceChangeLis
|
|||||||
setResultOnPreferenceClick("bugReport", RESULT_BUG_REPORT)
|
setResultOnPreferenceClick("bugReport", RESULT_BUG_REPORT)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreatePreferences(bundle: Bundle, s: String) {
|
override fun onCreatePreferences(bundle: Bundle?, s: String?) {
|
||||||
// NOP
|
// NOP
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -57,10 +57,10 @@ class AndroidNotificationTray
|
|||||||
Log.d("AndroidNotificationTray", msg)
|
Log.d("AndroidNotificationTray", msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun removeNotification(id: Int) {
|
override fun removeNotification(notificationId: Int) {
|
||||||
val manager = NotificationManagerCompat.from(context)
|
val manager = NotificationManagerCompat.from(context)
|
||||||
manager.cancel(id)
|
manager.cancel(notificationId)
|
||||||
active.remove(id)
|
active.remove(notificationId)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun showNotification(
|
override fun showNotification(
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ class SnoozeDelayPickerActivity : FragmentActivity(), OnItemClickListener {
|
|||||||
private var dialog: AlertDialog? = null
|
private var dialog: AlertDialog? = null
|
||||||
override fun onCreate(bundle: Bundle?) {
|
override fun onCreate(bundle: Bundle?) {
|
||||||
super.onCreate(bundle)
|
super.onCreate(bundle)
|
||||||
|
val intent = intent
|
||||||
if (intent == null) finish()
|
if (intent == null) finish()
|
||||||
if (intent.data == null) finish()
|
if (intent.data == null) finish()
|
||||||
val app = applicationContext as HabitsApplication
|
val app = applicationContext as HabitsApplication
|
||||||
@@ -79,7 +80,7 @@ class SnoozeDelayPickerActivity : FragmentActivity(), OnItemClickListener {
|
|||||||
override fun onItemClick(parent: AdapterView<*>?, view: View, position: Int, id: Long) {
|
override fun onItemClick(parent: AdapterView<*>?, view: View, position: Int, id: Long) {
|
||||||
val snoozeValues = resources.getIntArray(R.array.snooze_picker_values)
|
val snoozeValues = resources.getIntArray(R.array.snooze_picker_values)
|
||||||
if (snoozeValues[position] >= 0) {
|
if (snoozeValues[position] >= 0) {
|
||||||
reminderController!!.onSnoozeDelayPicked(habit, snoozeValues[position])
|
reminderController!!.onSnoozeDelayPicked(habit!!, snoozeValues[position])
|
||||||
finish()
|
finish()
|
||||||
} else showTimePicker()
|
} else showTimePicker()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ class ReminderController @Inject constructor(
|
|||||||
|
|
||||||
fun onShowReminder(
|
fun onShowReminder(
|
||||||
habit: Habit,
|
habit: Habit,
|
||||||
timestamp: Timestamp?,
|
timestamp: Timestamp,
|
||||||
reminderTime: Long
|
reminderTime: Long
|
||||||
) {
|
) {
|
||||||
notificationTray.show(habit, timestamp, reminderTime)
|
notificationTray.show(habit, timestamp, reminderTime)
|
||||||
@@ -54,9 +54,9 @@ class ReminderController @Inject constructor(
|
|||||||
showSnoozeDelayPicker(habit, context)
|
showSnoozeDelayPicker(habit, context)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onSnoozeDelayPicked(habit: Habit?, delayInMinutes: Int) {
|
fun onSnoozeDelayPicked(habit: Habit, delayInMinutes: Int) {
|
||||||
reminderScheduler.snoozeReminder(habit, delayInMinutes.toLong())
|
reminderScheduler.snoozeReminder(habit, delayInMinutes.toLong())
|
||||||
notificationTray.cancel(habit!!)
|
notificationTray.cancel(habit)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onSnoozeTimePicked(habit: Habit?, hour: Int, minute: Int) {
|
fun onSnoozeTimePicked(habit: Habit?, hour: Int, minute: Int) {
|
||||||
|
|||||||
@@ -1,143 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
|
||||||
*
|
|
||||||
* This file is part of Loop Habit Tracker.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by the
|
|
||||||
* Free Software Foundation, either version 3 of the License, or (at your
|
|
||||||
* option) any later version.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
|
||||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
|
||||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
||||||
* more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License along
|
|
||||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.isoron.uhabits.tasks;
|
|
||||||
|
|
||||||
import android.os.*;
|
|
||||||
|
|
||||||
import org.isoron.uhabits.core.*;
|
|
||||||
import org.isoron.uhabits.core.tasks.*;
|
|
||||||
|
|
||||||
import java.util.*;
|
|
||||||
|
|
||||||
import dagger.Module;
|
|
||||||
import dagger.Provides;
|
|
||||||
|
|
||||||
@Module
|
|
||||||
public class AndroidTaskRunner implements TaskRunner
|
|
||||||
{
|
|
||||||
private final LinkedList<CustomAsyncTask> activeTasks;
|
|
||||||
|
|
||||||
private final HashMap<Task, CustomAsyncTask> taskToAsyncTask;
|
|
||||||
|
|
||||||
private LinkedList<Listener> listeners;
|
|
||||||
|
|
||||||
public AndroidTaskRunner()
|
|
||||||
{
|
|
||||||
activeTasks = new LinkedList<>();
|
|
||||||
taskToAsyncTask = new HashMap<>();
|
|
||||||
listeners = new LinkedList<>();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Provides
|
|
||||||
@AppScope
|
|
||||||
public static TaskRunner provideTaskRunner()
|
|
||||||
{
|
|
||||||
return new AndroidTaskRunner();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void addListener(Listener listener)
|
|
||||||
{
|
|
||||||
listeners.add(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void execute(Task task)
|
|
||||||
{
|
|
||||||
task.onAttached(this);
|
|
||||||
new CustomAsyncTask(task).execute();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getActiveTaskCount()
|
|
||||||
{
|
|
||||||
return activeTasks.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void publishProgress(Task task, int progress)
|
|
||||||
{
|
|
||||||
CustomAsyncTask asyncTask = taskToAsyncTask.get(task);
|
|
||||||
if (asyncTask == null) return;
|
|
||||||
asyncTask.publish(progress);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void removeListener(Listener listener)
|
|
||||||
{
|
|
||||||
listeners.remove(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
private class CustomAsyncTask extends AsyncTask<Void, Integer, Void>
|
|
||||||
{
|
|
||||||
private final Task task;
|
|
||||||
private boolean isCancelled = false;
|
|
||||||
|
|
||||||
public CustomAsyncTask(Task task)
|
|
||||||
{
|
|
||||||
this.task = task;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task getTask()
|
|
||||||
{
|
|
||||||
return task;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void publish(int progress)
|
|
||||||
{
|
|
||||||
publishProgress(progress);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Void doInBackground(Void... params)
|
|
||||||
{
|
|
||||||
if(isCancelled) return null;
|
|
||||||
task.doInBackground();
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPostExecute(Void aVoid)
|
|
||||||
{
|
|
||||||
if(isCancelled) return;
|
|
||||||
task.onPostExecute();
|
|
||||||
activeTasks.remove(this);
|
|
||||||
taskToAsyncTask.remove(task);
|
|
||||||
for (Listener l : listeners) l.onTaskFinished(task);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPreExecute()
|
|
||||||
{
|
|
||||||
isCancelled = task.isCanceled();
|
|
||||||
if(isCancelled) return;
|
|
||||||
for (Listener l : listeners) l.onTaskStarted(task);
|
|
||||||
activeTasks.add(this);
|
|
||||||
taskToAsyncTask.put(task, this);
|
|
||||||
task.onPreExecute();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onProgressUpdate(Integer... values)
|
|
||||||
{
|
|
||||||
task.onProgressUpdate(values[0]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
||||||
|
*
|
||||||
|
* This file is part of Loop Habit Tracker.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by the
|
||||||
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along
|
||||||
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.isoron.uhabits.tasks
|
||||||
|
|
||||||
|
import android.os.AsyncTask
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.Provides
|
||||||
|
import org.isoron.uhabits.core.AppScope
|
||||||
|
import org.isoron.uhabits.core.tasks.Task
|
||||||
|
import org.isoron.uhabits.core.tasks.TaskRunner
|
||||||
|
import java.util.HashMap
|
||||||
|
import java.util.LinkedList
|
||||||
|
|
||||||
|
// TODO: @Module not needed?
|
||||||
|
@Module
|
||||||
|
class AndroidTaskRunner : TaskRunner {
|
||||||
|
private val activeTasks: LinkedList<CustomAsyncTask> = LinkedList()
|
||||||
|
private val taskToAsyncTask: HashMap<Task, CustomAsyncTask> = HashMap()
|
||||||
|
private val listeners: LinkedList<TaskRunner.Listener> = LinkedList<TaskRunner.Listener>()
|
||||||
|
override fun addListener(listener: TaskRunner.Listener) {
|
||||||
|
listeners.add(listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun execute(task: Task) {
|
||||||
|
task.onAttached(this)
|
||||||
|
CustomAsyncTask(task).execute()
|
||||||
|
}
|
||||||
|
|
||||||
|
override val activeTaskCount: Int
|
||||||
|
get() = activeTasks.size
|
||||||
|
|
||||||
|
override fun publishProgress(task: Task, progress: Int) {
|
||||||
|
val asyncTask = taskToAsyncTask[task] ?: return
|
||||||
|
asyncTask.publish(progress)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun removeListener(listener: TaskRunner.Listener) {
|
||||||
|
listeners.remove(listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class CustomAsyncTask(val task: Task) : AsyncTask<Void?, Int?, Void?>() {
|
||||||
|
|
||||||
|
fun publish(progress: Int) {
|
||||||
|
publishProgress(progress)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun doInBackground(vararg params: Void?): Void? {
|
||||||
|
if (isCancelled) return null
|
||||||
|
task.doInBackground()
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPostExecute(aVoid: Void?) {
|
||||||
|
if (isCancelled) return
|
||||||
|
task.onPostExecute()
|
||||||
|
activeTasks.remove(this)
|
||||||
|
taskToAsyncTask.remove(task)
|
||||||
|
for (l in listeners) l.onTaskFinished(task)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPreExecute() {
|
||||||
|
if (isCancelled) return
|
||||||
|
for (l in listeners) l.onTaskStarted(task)
|
||||||
|
activeTasks.add(this)
|
||||||
|
taskToAsyncTask[task] = this
|
||||||
|
task.onPreExecute()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onProgressUpdate(vararg values: Int?) {
|
||||||
|
values[0]?.let { task.onProgressUpdate(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Module
|
||||||
|
companion object {
|
||||||
|
@JvmStatic
|
||||||
|
@Provides
|
||||||
|
@AppScope
|
||||||
|
fun provideTaskRunner(): TaskRunner {
|
||||||
|
return AndroidTaskRunner()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
|
||||||
*
|
|
||||||
* This file is part of Loop Habit Tracker.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by the
|
|
||||||
* Free Software Foundation, either version 3 of the License, or (at your
|
|
||||||
* option) any later version.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
|
||||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
|
||||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
||||||
* more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License along
|
|
||||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.isoron.uhabits.tasks;
|
|
||||||
|
|
||||||
import android.content.*;
|
|
||||||
|
|
||||||
import androidx.annotation.*;
|
|
||||||
|
|
||||||
import org.isoron.uhabits.*;
|
|
||||||
import org.isoron.uhabits.core.tasks.*;
|
|
||||||
import org.isoron.uhabits.inject.*;
|
|
||||||
import org.isoron.uhabits.utils.*;
|
|
||||||
|
|
||||||
import java.io.*;
|
|
||||||
|
|
||||||
public class ExportDBTask implements Task
|
|
||||||
{
|
|
||||||
private String filename;
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
private Context context;
|
|
||||||
|
|
||||||
private AndroidDirFinder system;
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
private final Listener listener;
|
|
||||||
|
|
||||||
public ExportDBTask(@AppContext @NonNull Context context,
|
|
||||||
@NonNull AndroidDirFinder system,
|
|
||||||
@NonNull Listener listener)
|
|
||||||
{
|
|
||||||
this.system = system;
|
|
||||||
this.listener = listener;
|
|
||||||
this.context = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void doInBackground()
|
|
||||||
{
|
|
||||||
filename = null;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
File dir = system.getFilesDir("Backups");
|
|
||||||
if (dir == null) return;
|
|
||||||
|
|
||||||
filename = DatabaseUtils.saveDatabaseCopy(context, dir);
|
|
||||||
}
|
|
||||||
catch (IOException e)
|
|
||||||
{
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPostExecute()
|
|
||||||
{
|
|
||||||
listener.onExportDBFinished(filename);
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface Listener
|
|
||||||
{
|
|
||||||
void onExportDBFinished(@Nullable String filename);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
||||||
|
*
|
||||||
|
* This file is part of Loop Habit Tracker.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by the
|
||||||
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along
|
||||||
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.isoron.uhabits.tasks
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import org.isoron.uhabits.AndroidDirFinder
|
||||||
|
import org.isoron.uhabits.core.tasks.Task
|
||||||
|
import org.isoron.uhabits.inject.AppContext
|
||||||
|
import org.isoron.uhabits.utils.DatabaseUtils.saveDatabaseCopy
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
class ExportDBTask(
|
||||||
|
@param:AppContext private val context: Context,
|
||||||
|
private val system: AndroidDirFinder,
|
||||||
|
private val listener: Listener
|
||||||
|
) : Task {
|
||||||
|
private var filename: String? = null
|
||||||
|
override fun doInBackground() {
|
||||||
|
filename = null
|
||||||
|
filename = try {
|
||||||
|
val dir = system.getFilesDir("Backups") ?: return
|
||||||
|
saveDatabaseCopy(context, dir)
|
||||||
|
} catch (e: IOException) {
|
||||||
|
throw RuntimeException(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPostExecute() {
|
||||||
|
listener.onExportDBFinished(filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun interface Listener {
|
||||||
|
fun onExportDBFinished(filename: String?)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
|
||||||
*
|
|
||||||
* This file is part of Loop Habit Tracker.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by the
|
|
||||||
* Free Software Foundation, either version 3 of the License, or (at your
|
|
||||||
* option) any later version.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
|
||||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
|
||||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
||||||
* more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License along
|
|
||||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.isoron.uhabits.tasks;
|
|
||||||
|
|
||||||
import android.util.*;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
import org.isoron.uhabits.core.io.*;
|
|
||||||
import org.isoron.uhabits.core.models.ModelFactory;
|
|
||||||
import org.isoron.uhabits.core.models.sqlite.SQLModelFactory;
|
|
||||||
import org.isoron.uhabits.core.tasks.*;
|
|
||||||
|
|
||||||
import java.io.*;
|
|
||||||
|
|
||||||
public class ImportDataTask implements Task
|
|
||||||
{
|
|
||||||
public static final int FAILED = 3;
|
|
||||||
|
|
||||||
public static final int NOT_RECOGNIZED = 2;
|
|
||||||
|
|
||||||
public static final int SUCCESS = 1;
|
|
||||||
|
|
||||||
private int result;
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
private final File file;
|
|
||||||
|
|
||||||
private GenericImporter importer;
|
|
||||||
|
|
||||||
private SQLModelFactory modelFactory;
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
private final Listener listener;
|
|
||||||
|
|
||||||
public ImportDataTask(@NonNull GenericImporter importer,
|
|
||||||
@NonNull ModelFactory modelFactory,
|
|
||||||
@NonNull File file,
|
|
||||||
@NonNull Listener listener)
|
|
||||||
{
|
|
||||||
this.importer = importer;
|
|
||||||
this.modelFactory = (SQLModelFactory) modelFactory;
|
|
||||||
this.listener = listener;
|
|
||||||
this.file = file;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void doInBackground()
|
|
||||||
{
|
|
||||||
modelFactory.getDatabase().beginTransaction();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (importer.canHandle(file))
|
|
||||||
{
|
|
||||||
importer.importHabitsFromFile(file);
|
|
||||||
result = SUCCESS;
|
|
||||||
modelFactory.getDatabase().setTransactionSuccessful();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
result = NOT_RECOGNIZED;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
result = FAILED;
|
|
||||||
Log.e("ImportDataTask", "Import failed", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
modelFactory.getDatabase().endTransaction();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPostExecute()
|
|
||||||
{
|
|
||||||
listener.onImportDataFinished(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface Listener
|
|
||||||
{
|
|
||||||
void onImportDataFinished(int result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
||||||
|
*
|
||||||
|
* This file is part of Loop Habit Tracker.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by the
|
||||||
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along
|
||||||
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.isoron.uhabits.tasks
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import org.isoron.uhabits.core.io.GenericImporter
|
||||||
|
import org.isoron.uhabits.core.models.ModelFactory
|
||||||
|
import org.isoron.uhabits.core.models.sqlite.SQLModelFactory
|
||||||
|
import org.isoron.uhabits.core.tasks.Task
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class ImportDataTask(
|
||||||
|
private val importer: GenericImporter,
|
||||||
|
modelFactory: ModelFactory,
|
||||||
|
private val file: File,
|
||||||
|
private val listener: Listener
|
||||||
|
) : Task {
|
||||||
|
private var result = 0
|
||||||
|
private val modelFactory: SQLModelFactory = modelFactory as SQLModelFactory
|
||||||
|
override fun doInBackground() {
|
||||||
|
modelFactory.database.beginTransaction()
|
||||||
|
try {
|
||||||
|
if (importer.canHandle(file)) {
|
||||||
|
importer.importHabitsFromFile(file)
|
||||||
|
result = SUCCESS
|
||||||
|
modelFactory.database.setTransactionSuccessful()
|
||||||
|
} else {
|
||||||
|
result = NOT_RECOGNIZED
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
result = FAILED
|
||||||
|
Log.e("ImportDataTask", "Import failed", e)
|
||||||
|
}
|
||||||
|
modelFactory.database.endTransaction()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPostExecute() {
|
||||||
|
listener.onImportDataFinished(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun interface Listener {
|
||||||
|
fun onImportDataFinished(result: Int)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val FAILED = 3
|
||||||
|
const val NOT_RECOGNIZED = 2
|
||||||
|
const val SUCCESS = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,7 +34,7 @@ import org.isoron.uhabits.core.preferences.WidgetPreferences
|
|||||||
import org.isoron.uhabits.intents.PendingIntentFactory
|
import org.isoron.uhabits.intents.PendingIntentFactory
|
||||||
|
|
||||||
abstract class BaseWidget(val context: Context, val id: Int) {
|
abstract class BaseWidget(val context: Context, val id: Int) {
|
||||||
protected val widgetPrefs: WidgetPreferences
|
private val widgetPrefs: WidgetPreferences
|
||||||
protected val prefs: Preferences
|
protected val prefs: Preferences
|
||||||
protected val pendingIntentFactory: PendingIntentFactory
|
protected val pendingIntentFactory: PendingIntentFactory
|
||||||
protected val commandRunner: CommandRunner
|
protected val commandRunner: CommandRunner
|
||||||
@@ -120,9 +120,9 @@ abstract class BaseWidget(val context: Context, val id: Int) {
|
|||||||
return remoteViews
|
return remoteViews
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun measureView(view: View, width: Int, height: Int) {
|
private fun measureView(view: View, w: Int, h: Int) {
|
||||||
var width = width
|
var width = w
|
||||||
var height = height
|
var height = h
|
||||||
val inflater = LayoutInflater.from(context)
|
val inflater = LayoutInflater.from(context)
|
||||||
val entireView = inflater.inflate(R.layout.widget_wrapper, null)
|
val entireView = inflater.inflate(R.layout.widget_wrapper, null)
|
||||||
var specWidth = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY)
|
var specWidth = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY)
|
||||||
@@ -144,7 +144,7 @@ abstract class BaseWidget(val context: Context, val id: Int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected val preferedBackgroundAlpha: Int
|
protected val preferedBackgroundAlpha: Int
|
||||||
protected get() = prefs.widgetOpacity
|
get() = prefs.widgetOpacity
|
||||||
|
|
||||||
init {
|
init {
|
||||||
val app = context.applicationContext as HabitsApplication
|
val app = context.applicationContext as HabitsApplication
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ abstract class BaseWidgetProvider : AppWidgetProvider() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected fun getHabitsFromWidgetId(widgetId: Int): List<Habit> {
|
protected fun getHabitsFromWidgetId(widgetId: Int): List<Habit> {
|
||||||
val selectedIds = widgetPrefs.getHabitIdsFromWidgetId(widgetId)
|
val selectedIds = widgetPrefs.getHabitIdsFromWidgetId(widgetId)!!
|
||||||
val selectedHabits = ArrayList<Habit>(selectedIds.size)
|
val selectedHabits = ArrayList<Habit>(selectedIds.size)
|
||||||
for (id in selectedIds) {
|
for (id in selectedIds) {
|
||||||
val h = habits.getById(id) ?: throw HabitNotFoundException()
|
val h = habits.getById(id) ?: throw HabitNotFoundException()
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ class WidgetUpdater
|
|||||||
val modifiedWidgetIds = when (modifiedHabitId) {
|
val modifiedWidgetIds = when (modifiedHabitId) {
|
||||||
null -> widgetIds.toList()
|
null -> widgetIds.toList()
|
||||||
else -> widgetIds.filter { w ->
|
else -> widgetIds.filter { w ->
|
||||||
widgetPrefs.getHabitIdsFromWidgetId(w).contains(modifiedHabitId)
|
widgetPrefs.getHabitIdsFromWidgetId(w)!!.contains(modifiedHabitId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -83,8 +83,8 @@ class CheckmarkWidgetView : HabitWidgetView {
|
|||||||
setShadowAlpha(0x00)
|
setShadowAlpha(0x00)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ring.percentage = percentage
|
ring.setPercentage(percentage)
|
||||||
ring.color = fgColor
|
ring.setColor(fgColor)
|
||||||
ring.setBackgroundColor(bgColor)
|
ring.setBackgroundColor(bgColor)
|
||||||
ring.setText(text)
|
ring.setText(text)
|
||||||
label.text = name
|
label.text = name
|
||||||
@@ -117,8 +117,6 @@ class CheckmarkWidgetView : HabitWidgetView {
|
|||||||
get() = R.layout.widget_checkmark
|
get() = R.layout.widget_checkmark
|
||||||
|
|
||||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||||
var widthMeasureSpec = widthMeasureSpec
|
|
||||||
var heightMeasureSpec = heightMeasureSpec
|
|
||||||
val width = MeasureSpec.getSize(widthMeasureSpec)
|
val width = MeasureSpec.getSize(widthMeasureSpec)
|
||||||
val height = MeasureSpec.getSize(heightMeasureSpec)
|
val height = MeasureSpec.getSize(heightMeasureSpec)
|
||||||
var w = width.toFloat()
|
var w = width.toFloat()
|
||||||
@@ -128,15 +126,15 @@ class CheckmarkWidgetView : HabitWidgetView {
|
|||||||
h *= scale
|
h *= scale
|
||||||
if (h < getDimension(context, R.dimen.checkmarkWidget_heightBreakpoint)) ring.visibility =
|
if (h < getDimension(context, R.dimen.checkmarkWidget_heightBreakpoint)) ring.visibility =
|
||||||
GONE else ring.visibility = VISIBLE
|
GONE else ring.visibility = VISIBLE
|
||||||
widthMeasureSpec = MeasureSpec.makeMeasureSpec(w.toInt(), MeasureSpec.EXACTLY)
|
val newWidthMeasureSpec = MeasureSpec.makeMeasureSpec(w.toInt(), MeasureSpec.EXACTLY)
|
||||||
heightMeasureSpec = MeasureSpec.makeMeasureSpec(h.toInt(), MeasureSpec.EXACTLY)
|
val newHeightMeasureSpec = MeasureSpec.makeMeasureSpec(h.toInt(), MeasureSpec.EXACTLY)
|
||||||
var textSize = 0.15f * h
|
var textSize = 0.15f * h
|
||||||
val maxTextSize = getDimension(context, R.dimen.smallerTextSize)
|
val maxTextSize = getDimension(context, R.dimen.smallerTextSize)
|
||||||
textSize = min(textSize, maxTextSize)
|
textSize = min(textSize, maxTextSize)
|
||||||
label.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
|
label.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
|
||||||
ring.setTextSize(textSize)
|
ring.setTextSize(textSize)
|
||||||
ring.setThickness(0.15f * textSize)
|
ring.setThickness(0.15f * textSize)
|
||||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
super.onMeasure(newWidthMeasureSpec, newHeightMeasureSpec)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun init() {
|
private fun init() {
|
||||||
|
|||||||
@@ -168,6 +168,7 @@ class Repository<T>(
|
|||||||
get() {
|
get() {
|
||||||
val fields: MutableList<Pair<Field, Column>> = ArrayList()
|
val fields: MutableList<Pair<Field, Column>> = ArrayList()
|
||||||
for (f in klass.declaredFields) {
|
for (f in klass.declaredFields) {
|
||||||
|
f.isAccessible = true
|
||||||
for (annotation in f.annotations) {
|
for (annotation in f.annotations) {
|
||||||
if (annotation !is Column) continue
|
if (annotation !is Column) continue
|
||||||
fields.add(ImmutablePair(f, annotation))
|
fields.add(ImmutablePair(f, annotation))
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ class HabitsCSVExporter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun sanitizeFilename(name: String): String {
|
private fun sanitizeFilename(name: String): String {
|
||||||
val s = name.replace("[^ a-zA-Z0-9\\._-]+".toRegex(), "")
|
val s = name.replace("[^ a-zA-Z0-9._-]+".toRegex(), "")
|
||||||
return s.substring(0, min(s.length, 100))
|
return s.substring(0, min(s.length, 100))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -100,9 +100,9 @@ class LoopDBImporter
|
|||||||
habit = habitList.getByUUID(habitRecord.uuid)
|
habit = habitList.getByUUID(habitRecord.uuid)
|
||||||
|
|
||||||
for (r in entryRecords) {
|
for (r in entryRecords) {
|
||||||
val t = Timestamp(r.timestamp)
|
val t = Timestamp(r.timestamp!!)
|
||||||
val (_, value) = habit!!.originalEntries.get(t)
|
val (_, value) = habit!!.originalEntries.get(t)
|
||||||
if (value != r.value) CreateRepetitionCommand(habitList, habit, t, r.value).run()
|
if (value != r.value) CreateRepetitionCommand(habitList, habit, t, r.value!!).run()
|
||||||
}
|
}
|
||||||
|
|
||||||
runner.notifyListeners(command)
|
runner.notifyListeners(command)
|
||||||
|
|||||||
@@ -16,51 +16,44 @@
|
|||||||
* You should have received a copy of the GNU General Public License along
|
* You should have received a copy of the GNU General Public License along
|
||||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
package org.isoron.uhabits.core.models
|
||||||
|
|
||||||
package org.isoron.uhabits.core.models;
|
import com.opencsv.CSVWriter
|
||||||
|
import java.io.IOException
|
||||||
import androidx.annotation.*;
|
import java.io.Writer
|
||||||
|
import java.util.LinkedList
|
||||||
import com.opencsv.*;
|
import javax.annotation.concurrent.ThreadSafe
|
||||||
|
|
||||||
import java.io.*;
|
|
||||||
import java.util.*;
|
|
||||||
|
|
||||||
import javax.annotation.concurrent.*;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An ordered collection of {@link Habit}s.
|
* An ordered collection of [Habit]s.
|
||||||
*/
|
*/
|
||||||
@ThreadSafe
|
@ThreadSafe
|
||||||
public abstract class HabitList implements Iterable<Habit>
|
abstract class HabitList : Iterable<Habit> {
|
||||||
{
|
val observable: ModelObservable
|
||||||
private final ModelObservable observable;
|
|
||||||
|
|
||||||
@NonNull
|
@JvmField
|
||||||
protected final HabitMatcher filter;
|
protected val filter: HabitMatcher
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new HabitList.
|
* Creates a new HabitList.
|
||||||
* <p>
|
*
|
||||||
* Depending on the implementation, this list can either be empty or be
|
* Depending on the implementation, this list can either be empty or be
|
||||||
* populated by some pre-existing habits, for example, from a certain
|
* populated by some pre-existing habits, for example, from a certain
|
||||||
* database.
|
* database.
|
||||||
*/
|
*/
|
||||||
public HabitList()
|
constructor() {
|
||||||
{
|
observable = ModelObservable()
|
||||||
observable = new ModelObservable();
|
filter = HabitMatcherBuilder().setArchivedAllowed(true).build()
|
||||||
filter = new HabitMatcherBuilder().setArchivedAllowed(true).build();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected HabitList(@NonNull HabitMatcher filter)
|
protected constructor(filter: HabitMatcher) {
|
||||||
{
|
observable = ModelObservable()
|
||||||
observable = new ModelObservable();
|
this.filter = filter
|
||||||
this.filter = filter;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inserts a new habit in the list.
|
* Inserts a new habit in the list.
|
||||||
* <p>
|
*
|
||||||
* If the id of the habit is null, the list will assign it a new id, which
|
* If the id of the habit is null, the list will assign it a new id, which
|
||||||
* is guaranteed to be unique in the scope of the list. If id is not null,
|
* is guaranteed to be unique in the scope of the list. If id is not null,
|
||||||
* the caller should make sure that the list does not already contain
|
* the caller should make sure that the list does not already contain
|
||||||
@@ -69,8 +62,8 @@ public abstract class HabitList implements Iterable<Habit>
|
|||||||
* @param habit the habit to be inserted
|
* @param habit the habit to be inserted
|
||||||
* @throws IllegalArgumentException if the habit is already on the list.
|
* @throws IllegalArgumentException if the habit is already on the list.
|
||||||
*/
|
*/
|
||||||
public abstract void add(@NonNull Habit habit)
|
@Throws(IllegalArgumentException::class)
|
||||||
throws IllegalArgumentException;
|
abstract fun add(habit: Habit)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the habit with specified id.
|
* Returns the habit with specified id.
|
||||||
@@ -78,8 +71,7 @@ public abstract class HabitList implements Iterable<Habit>
|
|||||||
* @param id the id of the habit
|
* @param id the id of the habit
|
||||||
* @return the habit, or null if none exist
|
* @return the habit, or null if none exist
|
||||||
*/
|
*/
|
||||||
@Nullable
|
abstract fun getById(id: Long): Habit?
|
||||||
public abstract Habit getById(long id);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the habit with specified UUID.
|
* Returns the habit with specified UUID.
|
||||||
@@ -87,8 +79,7 @@ public abstract class HabitList implements Iterable<Habit>
|
|||||||
* @param uuid the UUID of the habit
|
* @param uuid the UUID of the habit
|
||||||
* @return the habit, or null if none exist
|
* @return the habit, or null if none exist
|
||||||
*/
|
*/
|
||||||
@Nullable
|
abstract fun getByUUID(uuid: String?): Habit?
|
||||||
public abstract Habit getByUUID(String uuid);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the habit that occupies a certain position.
|
* Returns the habit that occupies a certain position.
|
||||||
@@ -97,8 +88,7 @@ public abstract class HabitList implements Iterable<Habit>
|
|||||||
* @return the habit at that position
|
* @return the habit at that position
|
||||||
* @throws IndexOutOfBoundsException when the position is invalid
|
* @throws IndexOutOfBoundsException when the position is invalid
|
||||||
*/
|
*/
|
||||||
@NonNull
|
abstract fun getByPosition(position: Int): Habit
|
||||||
public abstract Habit getByPosition(int position);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the list of habits that match a given condition.
|
* Returns the list of habits that match a given condition.
|
||||||
@@ -106,31 +96,9 @@ public abstract class HabitList implements Iterable<Habit>
|
|||||||
* @param matcher the matcher that checks the condition
|
* @param matcher the matcher that checks the condition
|
||||||
* @return the list of matching habits
|
* @return the list of matching habits
|
||||||
*/
|
*/
|
||||||
@NonNull
|
abstract fun getFiltered(matcher: HabitMatcher?): HabitList
|
||||||
public abstract HabitList getFiltered(HabitMatcher matcher);
|
abstract var primaryOrder: Order
|
||||||
|
abstract var secondaryOrder: Order
|
||||||
public ModelObservable getObservable()
|
|
||||||
{
|
|
||||||
return observable;
|
|
||||||
}
|
|
||||||
|
|
||||||
public abstract Order getPrimaryOrder();
|
|
||||||
|
|
||||||
public abstract Order getSecondaryOrder();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Changes the order of the elements on the list.
|
|
||||||
*
|
|
||||||
* @param order the new order criterion
|
|
||||||
*/
|
|
||||||
public abstract void setPrimaryOrder(@NonNull Order order);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Changes the previous order of the elements on the list.
|
|
||||||
*
|
|
||||||
* @param order the new order criterion
|
|
||||||
*/
|
|
||||||
public abstract void setSecondaryOrder(@NonNull Order order);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the index of the given habit in the list, or -1 if the list does
|
* Returns the index of the given habit in the list, or -1 if the list does
|
||||||
@@ -139,31 +107,27 @@ public abstract class HabitList implements Iterable<Habit>
|
|||||||
* @param h the habit
|
* @param h the habit
|
||||||
* @return the index of the habit, or -1 if not in the list
|
* @return the index of the habit, or -1 if not in the list
|
||||||
*/
|
*/
|
||||||
public abstract int indexOf(@NonNull Habit h);
|
abstract fun indexOf(h: Habit): Int
|
||||||
|
val isEmpty: Boolean
|
||||||
public boolean isEmpty()
|
get() = size() == 0
|
||||||
{
|
|
||||||
return size() == 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes the given habit from the list.
|
* Removes the given habit from the list.
|
||||||
* <p>
|
*
|
||||||
* If the given habit is not in the list, does nothing.
|
* If the given habit is not in the list, does nothing.
|
||||||
*
|
*
|
||||||
* @param h the habit to be removed.
|
* @param h the habit to be removed.
|
||||||
*/
|
*/
|
||||||
public abstract void remove(@NonNull Habit h);
|
abstract fun remove(h: Habit)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes all the habits from the list.
|
* Removes all the habits from the list.
|
||||||
*/
|
*/
|
||||||
public void removeAll()
|
open fun removeAll() {
|
||||||
{
|
val copy: MutableList<Habit> = LinkedList()
|
||||||
List<Habit> copy = new LinkedList<>();
|
for (h in this) copy.add(h)
|
||||||
for (Habit h : this) copy.add(h);
|
for (h in copy) remove(h)
|
||||||
for (Habit h : copy) remove(h);
|
observable.notifyListeners()
|
||||||
observable.notifyListeners();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -172,40 +136,36 @@ public abstract class HabitList implements Iterable<Habit>
|
|||||||
* @param from the habit that should be moved
|
* @param from the habit that should be moved
|
||||||
* @param to the habit that currently occupies the desired position
|
* @param to the habit that currently occupies the desired position
|
||||||
*/
|
*/
|
||||||
public abstract void reorder(@NonNull Habit from, @NonNull Habit to);
|
abstract fun reorder(from: Habit, to: Habit)
|
||||||
|
open fun repair() {}
|
||||||
public void repair()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the number of habits in this list.
|
* Returns the number of habits in this list.
|
||||||
*
|
*
|
||||||
* @return number of habits
|
* @return number of habits
|
||||||
*/
|
*/
|
||||||
public abstract int size();
|
abstract fun size(): Int
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Notifies the list that a certain list of habits has been modified.
|
* Notifies the list that a certain list of habits has been modified.
|
||||||
* <p>
|
*
|
||||||
* Depending on the implementation, this operation might trigger a write to
|
* Depending on the implementation, this operation might trigger a write to
|
||||||
* disk, or do nothing at all. To make sure that the habits get persisted,
|
* disk, or do nothing at all. To make sure that the habits get persisted,
|
||||||
* this operation must be called.
|
* this operation must be called.
|
||||||
*
|
*
|
||||||
* @param habits the list of habits that have been modified.
|
* @param habits the list of habits that have been modified.
|
||||||
*/
|
*/
|
||||||
public abstract void update(List<Habit> habits);
|
abstract fun update(habits: List<Habit>)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Notifies the list that a certain habit has been modified.
|
* Notifies the list that a certain habit has been modified.
|
||||||
* <p>
|
*
|
||||||
* See {@link #update(List)} for more details.
|
* See [.update] for more details.
|
||||||
*
|
*
|
||||||
* @param habit the habit that has been modified.
|
* @param habit the habit that has been modified.
|
||||||
*/
|
*/
|
||||||
public void update(@NonNull Habit habit)
|
fun update(habit: Habit) {
|
||||||
{
|
update(listOf(habit))
|
||||||
update(Collections.singletonList(habit));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -217,9 +177,9 @@ public abstract class HabitList implements Iterable<Habit>
|
|||||||
* @param out the writer that will receive the result
|
* @param out the writer that will receive the result
|
||||||
* @throws IOException if write operations fail
|
* @throws IOException if write operations fail
|
||||||
*/
|
*/
|
||||||
public void writeCSV(@NonNull Writer out) throws IOException
|
@Throws(IOException::class)
|
||||||
{
|
fun writeCSV(out: Writer) {
|
||||||
String header[] = {
|
val header = arrayOf(
|
||||||
"Position",
|
"Position",
|
||||||
"Name",
|
"Name",
|
||||||
"Question",
|
"Question",
|
||||||
@@ -227,35 +187,27 @@ public abstract class HabitList implements Iterable<Habit>
|
|||||||
"NumRepetitions",
|
"NumRepetitions",
|
||||||
"Interval",
|
"Interval",
|
||||||
"Color"
|
"Color"
|
||||||
};
|
)
|
||||||
|
val csv = CSVWriter(out)
|
||||||
CSVWriter csv = new CSVWriter(out);
|
csv.writeNext(header, false)
|
||||||
csv.writeNext(header, false);
|
for (habit in this) {
|
||||||
|
val (numerator, denominator) = habit.frequency
|
||||||
for (Habit habit : this)
|
val cols = arrayOf(
|
||||||
{
|
|
||||||
Frequency freq = habit.getFrequency();
|
|
||||||
|
|
||||||
String[] cols = {
|
|
||||||
String.format("%03d", indexOf(habit) + 1),
|
String.format("%03d", indexOf(habit) + 1),
|
||||||
habit.getName(),
|
habit.name,
|
||||||
habit.getQuestion(),
|
habit.question,
|
||||||
habit.getDescription(),
|
habit.description,
|
||||||
Integer.toString(freq.getNumerator()),
|
numerator.toString(),
|
||||||
Integer.toString(freq.getDenominator()),
|
denominator.toString(),
|
||||||
habit.getColor().toCsvColor(),
|
habit.color.toCsvColor()
|
||||||
};
|
)
|
||||||
|
csv.writeNext(cols, false)
|
||||||
csv.writeNext(cols, false);
|
|
||||||
}
|
}
|
||||||
|
csv.close()
|
||||||
csv.close();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract void resort();
|
abstract fun resort()
|
||||||
|
enum class Order {
|
||||||
public enum Order
|
|
||||||
{
|
|
||||||
BY_NAME_ASC,
|
BY_NAME_ASC,
|
||||||
BY_NAME_DESC,
|
BY_NAME_DESC,
|
||||||
BY_COLOR_ASC,
|
BY_COLOR_ASC,
|
||||||
@@ -16,74 +16,70 @@
|
|||||||
* You should have received a copy of the GNU General Public License along
|
* You should have received a copy of the GNU General Public License along
|
||||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
package org.isoron.uhabits.core.models
|
||||||
|
|
||||||
package org.isoron.uhabits.core.models;
|
import java.util.LinkedList
|
||||||
|
import javax.annotation.concurrent.ThreadSafe
|
||||||
import java.util.*;
|
|
||||||
|
|
||||||
import javax.annotation.concurrent.*;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A ModelObservable allows objects to subscribe themselves to it and receive
|
* A ModelObservable allows objects to subscribe themselves to it and receive
|
||||||
* notifications whenever the model is changed.
|
* notifications whenever the model is changed.
|
||||||
*/
|
*/
|
||||||
@ThreadSafe
|
@ThreadSafe
|
||||||
public class ModelObservable
|
class ModelObservable {
|
||||||
{
|
private val listeners: MutableList<Listener>
|
||||||
private List<Listener> listeners;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new ModelObservable with no listeners.
|
|
||||||
*/
|
|
||||||
public ModelObservable()
|
|
||||||
{
|
|
||||||
super();
|
|
||||||
listeners = new LinkedList<>();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds the given listener to the observable.
|
* Adds the given listener to the observable.
|
||||||
*
|
*
|
||||||
* @param l the listener to be added.
|
* @param l the listener to be added.
|
||||||
*/
|
*/
|
||||||
public synchronized void addListener(Listener l)
|
@Synchronized
|
||||||
{
|
fun addListener(l: Listener) {
|
||||||
listeners.add(l);
|
listeners.add(l)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Notifies every listener that the model has changed.
|
* Notifies every listener that the model has changed.
|
||||||
* <p>
|
*
|
||||||
|
*
|
||||||
* Only models should call this method.
|
* Only models should call this method.
|
||||||
*/
|
*/
|
||||||
public synchronized void notifyListeners()
|
@Synchronized
|
||||||
{
|
fun notifyListeners() {
|
||||||
for (Listener l : listeners) l.onModelChange();
|
for (l in listeners) l.onModelChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes the given listener.
|
* Removes the given listener.
|
||||||
* <p>
|
*
|
||||||
|
*
|
||||||
* The listener will no longer be notified when the model changes. If the
|
* The listener will no longer be notified when the model changes. If the
|
||||||
* given listener is not subscribed to this observable, does nothing.
|
* given listener is not subscribed to this observable, does nothing.
|
||||||
*
|
*
|
||||||
* @param l the listener to be removed
|
* @param l the listener to be removed
|
||||||
*/
|
*/
|
||||||
public synchronized void removeListener(Listener l)
|
@Synchronized
|
||||||
{
|
fun removeListener(l: Listener) {
|
||||||
listeners.remove(l);
|
listeners.remove(l)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface implemented by objects that want to be notified when the model
|
* Interface implemented by objects that want to be notified when the model
|
||||||
* changes.
|
* changes.
|
||||||
*/
|
*/
|
||||||
public interface Listener
|
fun interface Listener {
|
||||||
{
|
|
||||||
/**
|
/**
|
||||||
* Called whenever the model associated to this observable has been
|
* Called whenever the model associated to this observable has been
|
||||||
* modified.
|
* modified.
|
||||||
*/
|
*/
|
||||||
void onModelChange();
|
fun onModelChange()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new ModelObservable with no listeners.
|
||||||
|
*/
|
||||||
|
init {
|
||||||
|
listeners = LinkedList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,159 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
|
||||||
*
|
|
||||||
* This file is part of Loop Habit Tracker.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by the
|
|
||||||
* Free Software Foundation, either version 3 of the License, or (at your
|
|
||||||
* option) any later version.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
|
||||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
|
||||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
||||||
* more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License along
|
|
||||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.isoron.uhabits.core.models;
|
|
||||||
|
|
||||||
import org.isoron.platform.time.LocalDate;
|
|
||||||
import org.apache.commons.lang3.builder.*;
|
|
||||||
import org.isoron.uhabits.core.utils.*;
|
|
||||||
|
|
||||||
import java.util.*;
|
|
||||||
|
|
||||||
import static java.util.Calendar.*;
|
|
||||||
|
|
||||||
public final class Timestamp implements Comparable<Timestamp> {
|
|
||||||
public static final long DAY_LENGTH = 86400000;
|
|
||||||
|
|
||||||
public static final Timestamp ZERO = new Timestamp(0);
|
|
||||||
|
|
||||||
private final long unixTime;
|
|
||||||
|
|
||||||
public static Timestamp fromLocalDate(LocalDate date) {
|
|
||||||
return new Timestamp(946684800000L + date.getDaysSince2000() * 86400000L);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Timestamp(long unixTime) {
|
|
||||||
if (unixTime < 0)
|
|
||||||
throw new IllegalArgumentException(
|
|
||||||
"Invalid unix time: " + unixTime);
|
|
||||||
|
|
||||||
if (unixTime % DAY_LENGTH != 0)
|
|
||||||
unixTime = (unixTime / DAY_LENGTH) * DAY_LENGTH;
|
|
||||||
|
|
||||||
this.unixTime = unixTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Timestamp(GregorianCalendar cal) {
|
|
||||||
this(cal.getTimeInMillis());
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Timestamp from(int year, int javaMonth, int day) {
|
|
||||||
GregorianCalendar cal = DateUtils.getStartOfTodayCalendar();
|
|
||||||
cal.set(year, javaMonth, day, 0, 0, 0);
|
|
||||||
return new Timestamp(cal.getTimeInMillis());
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getUnixTime() {
|
|
||||||
return unixTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
public LocalDate toLocalDate() {
|
|
||||||
long millisSince2000 = unixTime - 946684800000L;
|
|
||||||
int daysSince2000 = (int) (millisSince2000 / 86400000);
|
|
||||||
return new LocalDate(daysSince2000);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns -1 if this timestamp is older than the given timestamp, 1 if this
|
|
||||||
* timestamp is newer, or zero if they are equal.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public int compareTo(Timestamp other) {
|
|
||||||
return Long.signum(this.unixTime - other.unixTime);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean equals(Object o) {
|
|
||||||
if (this == o) return true;
|
|
||||||
|
|
||||||
if (o == null || getClass() != o.getClass()) return false;
|
|
||||||
|
|
||||||
Timestamp timestamp = (Timestamp) o;
|
|
||||||
|
|
||||||
return new EqualsBuilder()
|
|
||||||
.append(unixTime, timestamp.unixTime)
|
|
||||||
.isEquals();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int hashCode() {
|
|
||||||
return new HashCodeBuilder(17, 37).append(unixTime).toHashCode();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Given two timestamps, returns whichever timestamp is the oldest one.
|
|
||||||
*/
|
|
||||||
public static Timestamp oldest(Timestamp first, Timestamp second) {
|
|
||||||
return first.unixTime < second.unixTime ? first : second;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Timestamp minus(int days) {
|
|
||||||
return plus(-days);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Timestamp plus(int days) {
|
|
||||||
return new Timestamp(unixTime + DAY_LENGTH * days);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the number of days between this timestamp and the given one. If
|
|
||||||
* the other timestamp equals this one, returns zero. If the other timestamp
|
|
||||||
* is older than this one, returns a negative number.
|
|
||||||
*/
|
|
||||||
public int daysUntil(Timestamp other) {
|
|
||||||
return (int) ((other.unixTime - this.unixTime) / DAY_LENGTH);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isNewerThan(Timestamp other) {
|
|
||||||
return compareTo(other) > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isOlderThan(Timestamp other) {
|
|
||||||
return compareTo(other) < 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public Date toJavaDate() {
|
|
||||||
return new Date(unixTime);
|
|
||||||
}
|
|
||||||
|
|
||||||
public GregorianCalendar toCalendar() {
|
|
||||||
GregorianCalendar day =
|
|
||||||
new GregorianCalendar(TimeZone.getTimeZone("GMT"));
|
|
||||||
day.setTimeInMillis(unixTime);
|
|
||||||
return day;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return DateFormats.getCSVDateFormat().format(new Date(unixTime));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an integer corresponding to the day of the week. Saturday maps
|
|
||||||
* to 0, Sunday maps to 1, and so on.
|
|
||||||
*/
|
|
||||||
public int getWeekday() {
|
|
||||||
return toCalendar().get(DAY_OF_WEEK) % 7;
|
|
||||||
}
|
|
||||||
|
|
||||||
Timestamp truncate(DateUtils.TruncateField field, int firstWeekday) {
|
|
||||||
return new Timestamp(DateUtils.truncate(field, unixTime, firstWeekday));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
||||||
|
*
|
||||||
|
* This file is part of Loop Habit Tracker.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by the
|
||||||
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along
|
||||||
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.isoron.uhabits.core.models
|
||||||
|
|
||||||
|
import org.isoron.platform.time.LocalDate
|
||||||
|
import org.isoron.uhabits.core.utils.DateFormats.Companion.getCSVDateFormat
|
||||||
|
import org.isoron.uhabits.core.utils.DateUtils
|
||||||
|
import org.isoron.uhabits.core.utils.DateUtils.Companion.getStartOfTodayCalendar
|
||||||
|
import org.isoron.uhabits.core.utils.DateUtils.Companion.truncate
|
||||||
|
import java.util.Calendar
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.GregorianCalendar
|
||||||
|
import java.util.TimeZone
|
||||||
|
|
||||||
|
data class Timestamp(var unixTime: Long) : Comparable<Timestamp> {
|
||||||
|
|
||||||
|
constructor(cal: GregorianCalendar) : this(cal.timeInMillis)
|
||||||
|
|
||||||
|
fun toLocalDate(): LocalDate {
|
||||||
|
val millisSince2000 = unixTime - 946684800000L
|
||||||
|
val daysSince2000 = (millisSince2000 / 86400000).toInt()
|
||||||
|
return LocalDate(daysSince2000)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns -1 if this timestamp is older than the given timestamp, 1 if this
|
||||||
|
* timestamp is newer, or zero if they are equal.
|
||||||
|
*/
|
||||||
|
override fun compareTo(other: Timestamp): Int {
|
||||||
|
return java.lang.Long.signum(unixTime - other.unixTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
operator fun minus(days: Int): Timestamp {
|
||||||
|
return plus(-days)
|
||||||
|
}
|
||||||
|
|
||||||
|
operator fun plus(days: Int): Timestamp {
|
||||||
|
return Timestamp(unixTime + DAY_LENGTH * days)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the number of days between this timestamp and the given one. If
|
||||||
|
* the other timestamp equals this one, returns zero. If the other timestamp
|
||||||
|
* is older than this one, returns a negative number.
|
||||||
|
*/
|
||||||
|
fun daysUntil(other: Timestamp): Int {
|
||||||
|
return ((other.unixTime - unixTime) / DAY_LENGTH).toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isNewerThan(other: Timestamp): Boolean {
|
||||||
|
return compareTo(other) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isOlderThan(other: Timestamp): Boolean {
|
||||||
|
return compareTo(other) < 0
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toJavaDate(): Date {
|
||||||
|
return Date(unixTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toCalendar(): GregorianCalendar {
|
||||||
|
val day = GregorianCalendar(TimeZone.getTimeZone("GMT"))
|
||||||
|
day.timeInMillis = unixTime
|
||||||
|
return day
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return getCSVDateFormat().format(Date(unixTime))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an integer corresponding to the day of the week. Saturday maps
|
||||||
|
* to 0, Sunday maps to 1, and so on.
|
||||||
|
*/
|
||||||
|
val weekday: Int
|
||||||
|
get() = toCalendar()[Calendar.DAY_OF_WEEK] % 7
|
||||||
|
|
||||||
|
fun truncate(field: DateUtils.TruncateField?, firstWeekday: Int): Timestamp {
|
||||||
|
return Timestamp(
|
||||||
|
truncate(
|
||||||
|
field!!,
|
||||||
|
unixTime,
|
||||||
|
firstWeekday
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val DAY_LENGTH: Long = 86400000
|
||||||
|
val ZERO = Timestamp(0)
|
||||||
|
fun fromLocalDate(date: LocalDate): Timestamp {
|
||||||
|
return Timestamp(946684800000L + date.daysSince2000 * 86400000L)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun from(year: Int, javaMonth: Int, day: Int): Timestamp {
|
||||||
|
val cal = getStartOfTodayCalendar()
|
||||||
|
cal[year, javaMonth, day, 0, 0] = 0
|
||||||
|
return Timestamp(cal.timeInMillis)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given two timestamps, returns whichever timestamp is the oldest one.
|
||||||
|
*/
|
||||||
|
fun oldest(first: Timestamp, second: Timestamp): Timestamp {
|
||||||
|
return if (first.unixTime < second.unixTime) first else second
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
require(unixTime >= 0) { "Invalid unix time: $unixTime" }
|
||||||
|
if (unixTime % DAY_LENGTH != 0L) unixTime = unixTime / DAY_LENGTH * DAY_LENGTH
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,294 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
|
||||||
*
|
|
||||||
* This file is part of Loop Habit Tracker.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by the
|
|
||||||
* Free Software Foundation, either version 3 of the License, or (at your
|
|
||||||
* option) any later version.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
|
||||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
|
||||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
||||||
* more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License along
|
|
||||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.isoron.uhabits.core.models.memory;
|
|
||||||
|
|
||||||
import androidx.annotation.*;
|
|
||||||
|
|
||||||
import org.isoron.uhabits.core.models.*;
|
|
||||||
import org.isoron.uhabits.core.utils.*;
|
|
||||||
|
|
||||||
import java.util.*;
|
|
||||||
|
|
||||||
import static org.isoron.uhabits.core.models.HabitList.Order.*;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* In-memory implementation of {@link HabitList}.
|
|
||||||
*/
|
|
||||||
public class MemoryHabitList extends HabitList
|
|
||||||
{
|
|
||||||
@NonNull
|
|
||||||
private LinkedList<Habit> list = new LinkedList<>();
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
private Order primaryOrder = Order.BY_POSITION;
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
private Order secondaryOrder = Order.BY_NAME_ASC;
|
|
||||||
|
|
||||||
private Comparator<Habit> comparator = getComposedComparatorByOrder(primaryOrder, secondaryOrder);
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
private MemoryHabitList parent = null;
|
|
||||||
|
|
||||||
public MemoryHabitList()
|
|
||||||
{
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected MemoryHabitList(@NonNull HabitMatcher matcher,
|
|
||||||
Comparator<Habit> comparator,
|
|
||||||
@NonNull MemoryHabitList parent)
|
|
||||||
{
|
|
||||||
super(matcher);
|
|
||||||
this.parent = parent;
|
|
||||||
this.comparator = comparator;
|
|
||||||
this.primaryOrder = parent.primaryOrder;
|
|
||||||
this.secondaryOrder = parent.secondaryOrder;
|
|
||||||
parent.getObservable().addListener(this::loadFromParent);
|
|
||||||
loadFromParent();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public synchronized void add(@NonNull Habit habit)
|
|
||||||
throws IllegalArgumentException
|
|
||||||
{
|
|
||||||
throwIfHasParent();
|
|
||||||
if (list.contains(habit))
|
|
||||||
throw new IllegalArgumentException("habit already added");
|
|
||||||
|
|
||||||
Long id = habit.getId();
|
|
||||||
if (id != null && getById(id) != null)
|
|
||||||
throw new RuntimeException("duplicate id");
|
|
||||||
|
|
||||||
if (id == null) habit.setId((long) list.size());
|
|
||||||
list.addLast(habit);
|
|
||||||
resort();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public synchronized Habit getById(long id)
|
|
||||||
{
|
|
||||||
for (Habit h : list)
|
|
||||||
{
|
|
||||||
if (h.getId() == null) throw new IllegalStateException();
|
|
||||||
if (h.getId() == id) return h;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public synchronized Habit getByUUID(String uuid)
|
|
||||||
{
|
|
||||||
for (Habit h : list) if (h.getUuid().equals(uuid)) return h;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public synchronized Habit getByPosition(int position)
|
|
||||||
{
|
|
||||||
return list.get(position);
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public synchronized HabitList getFiltered(HabitMatcher matcher)
|
|
||||||
{
|
|
||||||
return new MemoryHabitList(matcher, comparator, this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public synchronized Order getPrimaryOrder()
|
|
||||||
{
|
|
||||||
return primaryOrder;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public synchronized Order getSecondaryOrder()
|
|
||||||
{
|
|
||||||
return secondaryOrder;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public synchronized void setPrimaryOrder(@NonNull Order order)
|
|
||||||
{
|
|
||||||
this.primaryOrder = order;
|
|
||||||
this.comparator = getComposedComparatorByOrder(this.primaryOrder, this.secondaryOrder);
|
|
||||||
resort();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setSecondaryOrder(@NonNull Order order)
|
|
||||||
{
|
|
||||||
this.secondaryOrder = order;
|
|
||||||
this.comparator = getComposedComparatorByOrder(this.primaryOrder, this.secondaryOrder);
|
|
||||||
resort();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Comparator<Habit> getComposedComparatorByOrder(Order firstOrder, Order secondOrder)
|
|
||||||
{
|
|
||||||
return (h1, h2) -> {
|
|
||||||
int firstResult = getComparatorByOrder(firstOrder).compare(h1, h2);
|
|
||||||
|
|
||||||
if (firstResult != 0 || secondOrder == null) {
|
|
||||||
return firstResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
return getComparatorByOrder(secondOrder).compare(h1, h2);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private Comparator<Habit> getComparatorByOrder(Order order) {
|
|
||||||
Comparator<Habit> nameComparatorAsc = (h1, h2) ->
|
|
||||||
h1.getName().compareTo(h2.getName());
|
|
||||||
|
|
||||||
Comparator<Habit> nameComparatorDesc = (h1, h2) ->
|
|
||||||
nameComparatorAsc.compare(h2, h1);
|
|
||||||
|
|
||||||
Comparator<Habit> colorComparatorAsc = (h1, h2) ->
|
|
||||||
h1.getColor().compareTo(h2.getColor());
|
|
||||||
|
|
||||||
Comparator<Habit> colorComparatorDesc = (h1, h2) ->
|
|
||||||
colorComparatorAsc.compare(h2, h1);
|
|
||||||
|
|
||||||
Comparator<Habit> scoreComparatorDesc = (h1, h2) ->
|
|
||||||
{
|
|
||||||
Timestamp today = DateUtils.getTodayWithOffset();
|
|
||||||
return Double.compare(
|
|
||||||
h1.getScores().get(today).getValue(),
|
|
||||||
h2.getScores().get(today).getValue());
|
|
||||||
};
|
|
||||||
|
|
||||||
Comparator<Habit> scoreComparatorAsc = (h1, h2) ->
|
|
||||||
scoreComparatorDesc.compare(h2, h1);
|
|
||||||
|
|
||||||
Comparator<Habit> positionComparator = (h1, h2) ->
|
|
||||||
Integer.compare(h1.getPosition(), h2.getPosition());
|
|
||||||
|
|
||||||
Comparator<Habit> statusComparatorDesc = (h1, h2) ->
|
|
||||||
{
|
|
||||||
if (h1.isCompletedToday() != h2.isCompletedToday()) {
|
|
||||||
return h1.isCompletedToday() ? -1 : 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (h1.isNumerical() != h2.isNumerical()) {
|
|
||||||
return h1.isNumerical() ? -1 : 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
Timestamp today = DateUtils.getTodayWithOffset();
|
|
||||||
Integer v1 = h1.getComputedEntries().get(today).getValue();
|
|
||||||
Integer v2 = h2.getComputedEntries().get(today).getValue();
|
|
||||||
|
|
||||||
return v2.compareTo(v1);
|
|
||||||
};
|
|
||||||
|
|
||||||
Comparator<Habit> statusComparatorAsc = (h1, h2) -> statusComparatorDesc.compare(h2, h1);
|
|
||||||
|
|
||||||
if (order == BY_POSITION) return positionComparator;
|
|
||||||
if (order == BY_NAME_ASC) return nameComparatorAsc;
|
|
||||||
if (order == BY_NAME_DESC) return nameComparatorDesc;
|
|
||||||
if (order == BY_COLOR_ASC) return colorComparatorAsc;
|
|
||||||
if (order == BY_COLOR_DESC) return colorComparatorDesc;
|
|
||||||
if (order == BY_SCORE_DESC) return scoreComparatorDesc;
|
|
||||||
if (order == BY_SCORE_ASC) return scoreComparatorAsc;
|
|
||||||
if (order == BY_STATUS_DESC) return statusComparatorDesc;
|
|
||||||
if (order == BY_STATUS_ASC) return statusComparatorAsc;
|
|
||||||
throw new IllegalStateException();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public synchronized int indexOf(@NonNull Habit h)
|
|
||||||
{
|
|
||||||
return list.indexOf(h);
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public synchronized Iterator<Habit> iterator()
|
|
||||||
{
|
|
||||||
return new ArrayList<>(list).iterator();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public synchronized void remove(@NonNull Habit habit)
|
|
||||||
{
|
|
||||||
throwIfHasParent();
|
|
||||||
list.remove(habit);
|
|
||||||
getObservable().notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public synchronized void reorder(@NonNull Habit from, @NonNull Habit to)
|
|
||||||
{
|
|
||||||
throwIfHasParent();
|
|
||||||
if (primaryOrder != BY_POSITION) throw new IllegalStateException(
|
|
||||||
"cannot reorder automatically sorted list");
|
|
||||||
|
|
||||||
if (indexOf(from) < 0) throw new IllegalArgumentException(
|
|
||||||
"list does not contain (from) habit");
|
|
||||||
|
|
||||||
int toPos = indexOf(to);
|
|
||||||
if (toPos < 0) throw new IllegalArgumentException(
|
|
||||||
"list does not contain (to) habit");
|
|
||||||
|
|
||||||
list.remove(from);
|
|
||||||
list.add(toPos, from);
|
|
||||||
|
|
||||||
int position = 0;
|
|
||||||
for(Habit h : list)
|
|
||||||
h.setPosition(position++);
|
|
||||||
|
|
||||||
getObservable().notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public synchronized int size()
|
|
||||||
{
|
|
||||||
return list.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public synchronized void update(List<Habit> habits)
|
|
||||||
{
|
|
||||||
resort();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void throwIfHasParent()
|
|
||||||
{
|
|
||||||
if (parent != null) throw new IllegalStateException(
|
|
||||||
"Filtered lists cannot be modified directly. " +
|
|
||||||
"You should modify the parent list instead.");
|
|
||||||
}
|
|
||||||
|
|
||||||
private synchronized void loadFromParent()
|
|
||||||
{
|
|
||||||
if (parent == null) throw new IllegalStateException();
|
|
||||||
|
|
||||||
list.clear();
|
|
||||||
for (Habit h : parent) if (filter.matches(h)) list.add(h);
|
|
||||||
resort();
|
|
||||||
}
|
|
||||||
|
|
||||||
public synchronized void resort()
|
|
||||||
{
|
|
||||||
if (comparator != null) Collections.sort(list, comparator);
|
|
||||||
getObservable().notifyListeners();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,229 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
||||||
|
*
|
||||||
|
* This file is part of Loop Habit Tracker.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by the
|
||||||
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along
|
||||||
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.isoron.uhabits.core.models.memory
|
||||||
|
|
||||||
|
import org.isoron.uhabits.core.models.Habit
|
||||||
|
import org.isoron.uhabits.core.models.HabitList
|
||||||
|
import org.isoron.uhabits.core.models.HabitMatcher
|
||||||
|
import org.isoron.uhabits.core.utils.DateUtils.Companion.getTodayWithOffset
|
||||||
|
import java.util.ArrayList
|
||||||
|
import java.util.Comparator
|
||||||
|
import java.util.LinkedList
|
||||||
|
import java.util.Objects
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In-memory implementation of [HabitList].
|
||||||
|
*/
|
||||||
|
class MemoryHabitList : HabitList {
|
||||||
|
private val list = LinkedList<Habit>()
|
||||||
|
|
||||||
|
@get:Synchronized
|
||||||
|
override var primaryOrder = Order.BY_POSITION
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
comparator = getComposedComparatorByOrder(primaryOrder, secondaryOrder)
|
||||||
|
resort()
|
||||||
|
}
|
||||||
|
|
||||||
|
@get:Synchronized
|
||||||
|
override var secondaryOrder = Order.BY_NAME_ASC
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
comparator = getComposedComparatorByOrder(primaryOrder, secondaryOrder)
|
||||||
|
resort()
|
||||||
|
}
|
||||||
|
|
||||||
|
private var comparator: Comparator<Habit>? =
|
||||||
|
getComposedComparatorByOrder(primaryOrder, secondaryOrder)
|
||||||
|
private var parent: MemoryHabitList? = null
|
||||||
|
|
||||||
|
constructor() : super()
|
||||||
|
constructor(
|
||||||
|
matcher: HabitMatcher,
|
||||||
|
comparator: Comparator<Habit>?,
|
||||||
|
parent: MemoryHabitList
|
||||||
|
) : super(matcher) {
|
||||||
|
this.parent = parent
|
||||||
|
this.comparator = comparator
|
||||||
|
primaryOrder = parent.primaryOrder
|
||||||
|
secondaryOrder = parent.secondaryOrder
|
||||||
|
parent.observable.addListener { loadFromParent() }
|
||||||
|
loadFromParent()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
@Throws(IllegalArgumentException::class)
|
||||||
|
override fun add(habit: Habit) {
|
||||||
|
throwIfHasParent()
|
||||||
|
require(!list.contains(habit)) { "habit already added" }
|
||||||
|
val id = habit.id
|
||||||
|
if (id != null && getById(id) != null) throw RuntimeException("duplicate id")
|
||||||
|
if (id == null) habit.id = list.size.toLong()
|
||||||
|
list.addLast(habit)
|
||||||
|
resort()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun getById(id: Long): Habit? {
|
||||||
|
for (h in list) {
|
||||||
|
checkNotNull(h.id)
|
||||||
|
if (h.id == id) return h
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun getByUUID(uuid: String?): Habit? {
|
||||||
|
for (h in list) if (Objects.requireNonNull(h.uuid) == uuid) return h
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun getByPosition(position: Int): Habit {
|
||||||
|
return list[position]
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun getFiltered(matcher: HabitMatcher?): HabitList {
|
||||||
|
return MemoryHabitList(matcher!!, comparator, this)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getComposedComparatorByOrder(
|
||||||
|
firstOrder: Order,
|
||||||
|
secondOrder: Order?
|
||||||
|
): Comparator<Habit> {
|
||||||
|
return Comparator { h1: Habit, h2: Habit ->
|
||||||
|
val firstResult = getComparatorByOrder(firstOrder).compare(h1, h2)
|
||||||
|
if (firstResult != 0 || secondOrder == null) {
|
||||||
|
return@Comparator firstResult
|
||||||
|
}
|
||||||
|
getComparatorByOrder(secondOrder).compare(h1, h2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getComparatorByOrder(order: Order): Comparator<Habit> {
|
||||||
|
val nameComparatorAsc = Comparator<Habit> { habit1, habit2 ->
|
||||||
|
habit1.name.compareTo(habit2.name)
|
||||||
|
}
|
||||||
|
val nameComparatorDesc =
|
||||||
|
Comparator { h1: Habit, h2: Habit -> nameComparatorAsc.compare(h2, h1) }
|
||||||
|
val colorComparatorAsc = Comparator<Habit> { (color1), (color2) ->
|
||||||
|
color1.compareTo(color2)
|
||||||
|
}
|
||||||
|
val colorComparatorDesc =
|
||||||
|
Comparator { h1: Habit, h2: Habit -> colorComparatorAsc.compare(h2, h1) }
|
||||||
|
val scoreComparatorDesc =
|
||||||
|
Comparator<Habit> { habit1, habit2 ->
|
||||||
|
val today = getTodayWithOffset()
|
||||||
|
habit1.scores[today].value.compareTo(habit2.scores[today].value)
|
||||||
|
}
|
||||||
|
val scoreComparatorAsc =
|
||||||
|
Comparator { h1: Habit, h2: Habit -> scoreComparatorDesc.compare(h2, h1) }
|
||||||
|
val positionComparator =
|
||||||
|
Comparator<Habit> { habit1, habit2 -> habit1.position.compareTo(habit2.position) }
|
||||||
|
val statusComparatorDesc = Comparator { h1: Habit, h2: Habit ->
|
||||||
|
if (h1.isCompletedToday() != h2.isCompletedToday()) {
|
||||||
|
return@Comparator if (h1.isCompletedToday()) -1 else 1
|
||||||
|
}
|
||||||
|
if (h1.isNumerical != h2.isNumerical) {
|
||||||
|
return@Comparator if (h1.isNumerical) -1 else 1
|
||||||
|
}
|
||||||
|
val today = getTodayWithOffset()
|
||||||
|
val v1 = h1.computedEntries.get(today).value
|
||||||
|
val v2 = h2.computedEntries.get(today).value
|
||||||
|
v2.compareTo(v1)
|
||||||
|
}
|
||||||
|
val statusComparatorAsc =
|
||||||
|
Comparator { h1: Habit, h2: Habit -> statusComparatorDesc.compare(h2, h1) }
|
||||||
|
return when {
|
||||||
|
order === Order.BY_POSITION -> positionComparator
|
||||||
|
order === Order.BY_NAME_ASC -> nameComparatorAsc
|
||||||
|
order === Order.BY_NAME_DESC -> nameComparatorDesc
|
||||||
|
order === Order.BY_COLOR_ASC -> colorComparatorAsc
|
||||||
|
order === Order.BY_COLOR_DESC -> colorComparatorDesc
|
||||||
|
order === Order.BY_SCORE_DESC -> scoreComparatorDesc
|
||||||
|
order === Order.BY_SCORE_ASC -> scoreComparatorAsc
|
||||||
|
order === Order.BY_STATUS_DESC -> statusComparatorDesc
|
||||||
|
order === Order.BY_STATUS_ASC -> statusComparatorAsc
|
||||||
|
else -> throw IllegalStateException()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun indexOf(h: Habit): Int {
|
||||||
|
return list.indexOf(h)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun iterator(): Iterator<Habit> {
|
||||||
|
return ArrayList(list).iterator()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun remove(h: Habit) {
|
||||||
|
throwIfHasParent()
|
||||||
|
list.remove(h)
|
||||||
|
observable.notifyListeners()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun reorder(from: Habit, to: Habit) {
|
||||||
|
throwIfHasParent()
|
||||||
|
check(!(primaryOrder !== Order.BY_POSITION)) { "cannot reorder automatically sorted list" }
|
||||||
|
require(indexOf(from) >= 0) { "list does not contain (from) habit" }
|
||||||
|
val toPos = indexOf(to)
|
||||||
|
require(toPos >= 0) { "list does not contain (to) habit" }
|
||||||
|
list.remove(from)
|
||||||
|
list.add(toPos, from)
|
||||||
|
var position = 0
|
||||||
|
for (h in list) h.position = position++
|
||||||
|
observable.notifyListeners()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun size(): Int {
|
||||||
|
return list.size
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun update(habits: List<Habit>) {
|
||||||
|
resort()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun throwIfHasParent() {
|
||||||
|
check(parent == null) {
|
||||||
|
"Filtered lists cannot be modified directly. " +
|
||||||
|
"You should modify the parent list instead."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
private fun loadFromParent() {
|
||||||
|
checkNotNull(parent)
|
||||||
|
list.clear()
|
||||||
|
for (h in parent!!) if (filter.matches(h)) list.add(h)
|
||||||
|
resort()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun resort() {
|
||||||
|
if (comparator != null) list.sortWith(comparator!!)
|
||||||
|
observable.notifyListeners()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,294 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
|
||||||
*
|
|
||||||
* This file is part of Loop Habit Tracker.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by the
|
|
||||||
* Free Software Foundation, either version 3 of the License, or (at your
|
|
||||||
* option) any later version.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
|
||||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
|
||||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
||||||
* more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License along
|
|
||||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.isoron.uhabits.core.models.sqlite;
|
|
||||||
|
|
||||||
import androidx.annotation.*;
|
|
||||||
|
|
||||||
import org.isoron.uhabits.core.database.*;
|
|
||||||
import org.isoron.uhabits.core.models.*;
|
|
||||||
import org.isoron.uhabits.core.models.memory.*;
|
|
||||||
import org.isoron.uhabits.core.models.sqlite.records.*;
|
|
||||||
|
|
||||||
import java.util.*;
|
|
||||||
|
|
||||||
import javax.inject.*;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Implementation of a {@link HabitList} that is backed by SQLite.
|
|
||||||
*/
|
|
||||||
public class SQLiteHabitList extends HabitList
|
|
||||||
{
|
|
||||||
@NonNull
|
|
||||||
private final Repository<HabitRecord> repository;
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
private final ModelFactory modelFactory;
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
private final MemoryHabitList list;
|
|
||||||
|
|
||||||
private boolean loaded = false;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
public SQLiteHabitList(@NonNull ModelFactory modelFactory)
|
|
||||||
{
|
|
||||||
super();
|
|
||||||
this.modelFactory = modelFactory;
|
|
||||||
this.list = new MemoryHabitList();
|
|
||||||
this.repository = modelFactory.buildHabitListRepository();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void loadRecords()
|
|
||||||
{
|
|
||||||
if (loaded) return;
|
|
||||||
loaded = true;
|
|
||||||
|
|
||||||
list.removeAll();
|
|
||||||
List<HabitRecord> records = repository.findAll("order by position");
|
|
||||||
|
|
||||||
int expectedPosition = 0;
|
|
||||||
boolean shouldRebuildOrder = false;
|
|
||||||
for (HabitRecord rec : records)
|
|
||||||
{
|
|
||||||
if (rec.position != expectedPosition) shouldRebuildOrder = true;
|
|
||||||
expectedPosition++;
|
|
||||||
|
|
||||||
Habit h = modelFactory.buildHabit();
|
|
||||||
rec.copyTo(h);
|
|
||||||
((SQLiteEntryList) h.getOriginalEntries()).setHabitId(h.getId());
|
|
||||||
list.add(h);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(shouldRebuildOrder) rebuildOrder();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public synchronized void add(@NonNull Habit habit)
|
|
||||||
{
|
|
||||||
loadRecords();
|
|
||||||
habit.setPosition(size());
|
|
||||||
|
|
||||||
HabitRecord record = new HabitRecord();
|
|
||||||
record.copyFrom(habit);
|
|
||||||
repository.save(record);
|
|
||||||
habit.setId(record.id);
|
|
||||||
((SQLiteEntryList) habit.getOriginalEntries()).setHabitId(record.id);
|
|
||||||
|
|
||||||
list.add(habit);
|
|
||||||
getObservable().notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Nullable
|
|
||||||
public synchronized Habit getById(long id)
|
|
||||||
{
|
|
||||||
loadRecords();
|
|
||||||
return list.getById(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Nullable
|
|
||||||
public synchronized Habit getByUUID(String uuid)
|
|
||||||
{
|
|
||||||
loadRecords();
|
|
||||||
return list.getByUUID(uuid);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@NonNull
|
|
||||||
public synchronized Habit getByPosition(int position)
|
|
||||||
{
|
|
||||||
loadRecords();
|
|
||||||
return list.getByPosition(position);
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public synchronized HabitList getFiltered(HabitMatcher filter)
|
|
||||||
{
|
|
||||||
loadRecords();
|
|
||||||
return list.getFiltered(filter);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@NonNull
|
|
||||||
public Order getPrimaryOrder()
|
|
||||||
{
|
|
||||||
return list.getPrimaryOrder();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Order getSecondaryOrder()
|
|
||||||
{
|
|
||||||
return list.getSecondaryOrder();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public synchronized void setPrimaryOrder(@NonNull Order order)
|
|
||||||
{
|
|
||||||
list.setPrimaryOrder(order);
|
|
||||||
getObservable().notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public synchronized void setSecondaryOrder(@NonNull Order order)
|
|
||||||
{
|
|
||||||
list.setSecondaryOrder(order);
|
|
||||||
getObservable().notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public synchronized int indexOf(@NonNull Habit h)
|
|
||||||
{
|
|
||||||
loadRecords();
|
|
||||||
return list.indexOf(h);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public synchronized Iterator<Habit> iterator()
|
|
||||||
{
|
|
||||||
loadRecords();
|
|
||||||
return list.iterator();
|
|
||||||
}
|
|
||||||
|
|
||||||
private synchronized void rebuildOrder()
|
|
||||||
{
|
|
||||||
List<HabitRecord> records = repository.findAll("order by position");
|
|
||||||
repository.executeAsTransaction(() ->
|
|
||||||
{
|
|
||||||
int pos = 0;
|
|
||||||
for (HabitRecord r : records)
|
|
||||||
{
|
|
||||||
if (r.position != pos)
|
|
||||||
{
|
|
||||||
r.position = pos;
|
|
||||||
repository.save(r);
|
|
||||||
}
|
|
||||||
pos++;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public synchronized void remove(@NonNull Habit habit)
|
|
||||||
{
|
|
||||||
loadRecords();
|
|
||||||
|
|
||||||
reorder(habit, list.getByPosition(size() - 1));
|
|
||||||
|
|
||||||
list.remove(habit);
|
|
||||||
|
|
||||||
HabitRecord record = repository.find(habit.getId());
|
|
||||||
if (record == null) throw new RuntimeException("habit not in database");
|
|
||||||
repository.executeAsTransaction(() ->
|
|
||||||
{
|
|
||||||
habit.getOriginalEntries().clear();
|
|
||||||
repository.remove(record);
|
|
||||||
});
|
|
||||||
|
|
||||||
getObservable().notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public synchronized void removeAll()
|
|
||||||
{
|
|
||||||
list.removeAll();
|
|
||||||
repository.execSQL("delete from habits");
|
|
||||||
repository.execSQL("delete from repetitions");
|
|
||||||
getObservable().notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public synchronized void reorder(@NonNull Habit from, @NonNull Habit to)
|
|
||||||
{
|
|
||||||
loadRecords();
|
|
||||||
list.reorder(from, to);
|
|
||||||
|
|
||||||
HabitRecord fromRecord = repository.find(from.getId());
|
|
||||||
HabitRecord toRecord = repository.find(to.getId());
|
|
||||||
|
|
||||||
if (fromRecord == null)
|
|
||||||
throw new RuntimeException("habit not in database");
|
|
||||||
if (toRecord == null)
|
|
||||||
throw new RuntimeException("habit not in database");
|
|
||||||
|
|
||||||
if (toRecord.position < fromRecord.position)
|
|
||||||
{
|
|
||||||
repository.execSQL("update habits set position = position + 1 " +
|
|
||||||
"where position >= ? and position < ?",
|
|
||||||
toRecord.position, fromRecord.position);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
repository.execSQL("update habits set position = position - 1 " +
|
|
||||||
"where position > ? and position <= ?",
|
|
||||||
fromRecord.position, toRecord.position);
|
|
||||||
}
|
|
||||||
|
|
||||||
fromRecord.position = toRecord.position;
|
|
||||||
repository.save(fromRecord);
|
|
||||||
|
|
||||||
getObservable().notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public synchronized void repair()
|
|
||||||
{
|
|
||||||
loadRecords();
|
|
||||||
rebuildOrder();
|
|
||||||
getObservable().notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public synchronized int size()
|
|
||||||
{
|
|
||||||
loadRecords();
|
|
||||||
return list.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public synchronized void update(List<Habit> habits)
|
|
||||||
{
|
|
||||||
loadRecords();
|
|
||||||
list.update(habits);
|
|
||||||
|
|
||||||
for (Habit h : habits)
|
|
||||||
{
|
|
||||||
HabitRecord record = repository.find(h.getId());
|
|
||||||
if (record == null) continue;
|
|
||||||
record.copyFrom(h);
|
|
||||||
repository.save(record);
|
|
||||||
}
|
|
||||||
|
|
||||||
getObservable().notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void resort()
|
|
||||||
{
|
|
||||||
list.resort();
|
|
||||||
getObservable().notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
public synchronized void reload()
|
|
||||||
{
|
|
||||||
loaded = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,220 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
||||||
|
*
|
||||||
|
* This file is part of Loop Habit Tracker.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by the
|
||||||
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along
|
||||||
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.isoron.uhabits.core.models.sqlite
|
||||||
|
|
||||||
|
import org.isoron.uhabits.core.database.Repository
|
||||||
|
import org.isoron.uhabits.core.models.Habit
|
||||||
|
import org.isoron.uhabits.core.models.HabitList
|
||||||
|
import org.isoron.uhabits.core.models.HabitMatcher
|
||||||
|
import org.isoron.uhabits.core.models.ModelFactory
|
||||||
|
import org.isoron.uhabits.core.models.memory.MemoryHabitList
|
||||||
|
import org.isoron.uhabits.core.models.sqlite.records.HabitRecord
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of a [HabitList] that is backed by SQLite.
|
||||||
|
*/
|
||||||
|
class SQLiteHabitList @Inject constructor(private val modelFactory: ModelFactory) : HabitList() {
|
||||||
|
private val repository: Repository<HabitRecord> = modelFactory.buildHabitListRepository()
|
||||||
|
private val list: MemoryHabitList = MemoryHabitList()
|
||||||
|
private var loaded = false
|
||||||
|
private fun loadRecords() {
|
||||||
|
if (loaded) return
|
||||||
|
loaded = true
|
||||||
|
list.removeAll()
|
||||||
|
val records = repository.findAll("order by position")
|
||||||
|
var shouldRebuildOrder = false
|
||||||
|
for ((expectedPosition, rec) in records.withIndex()) {
|
||||||
|
if (rec.position != expectedPosition) shouldRebuildOrder = true
|
||||||
|
val h = modelFactory.buildHabit()
|
||||||
|
rec.copyTo(h)
|
||||||
|
(h.originalEntries as SQLiteEntryList).habitId = h.id
|
||||||
|
list.add(h)
|
||||||
|
}
|
||||||
|
if (shouldRebuildOrder) rebuildOrder()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun add(habit: Habit) {
|
||||||
|
loadRecords()
|
||||||
|
habit.position = size()
|
||||||
|
val record = HabitRecord()
|
||||||
|
record.copyFrom(habit)
|
||||||
|
repository.save(record)
|
||||||
|
habit.id = record.id
|
||||||
|
(habit.originalEntries as SQLiteEntryList).habitId = record.id
|
||||||
|
list.add(habit)
|
||||||
|
observable.notifyListeners()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun getById(id: Long): Habit? {
|
||||||
|
loadRecords()
|
||||||
|
return list.getById(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun getByUUID(uuid: String?): Habit? {
|
||||||
|
loadRecords()
|
||||||
|
return list.getByUUID(uuid)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun getByPosition(position: Int): Habit {
|
||||||
|
loadRecords()
|
||||||
|
return list.getByPosition(position)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun getFiltered(matcher: HabitMatcher?): HabitList {
|
||||||
|
loadRecords()
|
||||||
|
return list.getFiltered(matcher)
|
||||||
|
}
|
||||||
|
|
||||||
|
@set:Synchronized
|
||||||
|
override var primaryOrder: Order
|
||||||
|
get() = list.primaryOrder
|
||||||
|
set(order) {
|
||||||
|
list.primaryOrder = order
|
||||||
|
observable.notifyListeners()
|
||||||
|
}
|
||||||
|
|
||||||
|
@set:Synchronized
|
||||||
|
override var secondaryOrder: Order
|
||||||
|
get() = list.secondaryOrder
|
||||||
|
set(order) {
|
||||||
|
list.secondaryOrder = order
|
||||||
|
observable.notifyListeners()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun indexOf(h: Habit): Int {
|
||||||
|
loadRecords()
|
||||||
|
return list.indexOf(h)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun iterator(): Iterator<Habit> {
|
||||||
|
loadRecords()
|
||||||
|
return list.iterator()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
private fun rebuildOrder() {
|
||||||
|
val records = repository.findAll("order by position")
|
||||||
|
repository.executeAsTransaction {
|
||||||
|
for ((pos, r) in records.withIndex()) {
|
||||||
|
if (r.position != pos) {
|
||||||
|
r.position = pos
|
||||||
|
repository.save(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun remove(h: Habit) {
|
||||||
|
loadRecords()
|
||||||
|
reorder(h, list.getByPosition(size() - 1))
|
||||||
|
list.remove(h)
|
||||||
|
val record = repository.find(
|
||||||
|
h.id!!
|
||||||
|
) ?: throw RuntimeException("habit not in database")
|
||||||
|
repository.executeAsTransaction {
|
||||||
|
h.originalEntries.clear()
|
||||||
|
repository.remove(record)
|
||||||
|
}
|
||||||
|
observable.notifyListeners()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun removeAll() {
|
||||||
|
list.removeAll()
|
||||||
|
repository.execSQL("delete from habits")
|
||||||
|
repository.execSQL("delete from repetitions")
|
||||||
|
observable.notifyListeners()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun reorder(from: Habit, to: Habit) {
|
||||||
|
loadRecords()
|
||||||
|
list.reorder(from, to)
|
||||||
|
val fromRecord = repository.find(
|
||||||
|
from.id!!
|
||||||
|
)
|
||||||
|
val toRecord = repository.find(
|
||||||
|
to.id!!
|
||||||
|
)
|
||||||
|
if (fromRecord == null) throw RuntimeException("habit not in database")
|
||||||
|
if (toRecord == null) throw RuntimeException("habit not in database")
|
||||||
|
if (toRecord.position!! < fromRecord.position!!) {
|
||||||
|
repository.execSQL(
|
||||||
|
"update habits set position = position + 1 " +
|
||||||
|
"where position >= ? and position < ?",
|
||||||
|
toRecord.position!!,
|
||||||
|
fromRecord.position!!
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
repository.execSQL(
|
||||||
|
"update habits set position = position - 1 " +
|
||||||
|
"where position > ? and position <= ?",
|
||||||
|
fromRecord.position!!,
|
||||||
|
toRecord.position!!
|
||||||
|
)
|
||||||
|
}
|
||||||
|
fromRecord.position = toRecord.position
|
||||||
|
repository.save(fromRecord)
|
||||||
|
observable.notifyListeners()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun repair() {
|
||||||
|
loadRecords()
|
||||||
|
rebuildOrder()
|
||||||
|
observable.notifyListeners()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun size(): Int {
|
||||||
|
loadRecords()
|
||||||
|
return list.size()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun update(habits: List<Habit>) {
|
||||||
|
loadRecords()
|
||||||
|
list.update(habits)
|
||||||
|
for (h in habits) {
|
||||||
|
val record = repository.find(h.id!!) ?: continue
|
||||||
|
record.copyFrom(h)
|
||||||
|
repository.save(record)
|
||||||
|
}
|
||||||
|
observable.notifyListeners()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun resort() {
|
||||||
|
list.resort()
|
||||||
|
observable.notifyListeners()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun reload() {
|
||||||
|
loaded = false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
|
||||||
*
|
|
||||||
* This file is part of Loop Habit Tracker.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by the
|
|
||||||
* Free Software Foundation, either version 3 of the License, or (at your
|
|
||||||
* option) any later version.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
|
||||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
|
||||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
||||||
* more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License along
|
|
||||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides SQLite implementations of the core models.
|
|
||||||
*/
|
|
||||||
package org.isoron.uhabits.core.models.sqlite;
|
|
||||||
@@ -16,40 +16,37 @@
|
|||||||
* You should have received a copy of the GNU General Public License along
|
* You should have received a copy of the GNU General Public License along
|
||||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
package org.isoron.uhabits.core.models.sqlite.records
|
||||||
|
|
||||||
package org.isoron.uhabits.core.models.sqlite.records;
|
import org.isoron.uhabits.core.database.Column
|
||||||
|
import org.isoron.uhabits.core.database.Table
|
||||||
import org.isoron.uhabits.core.database.*;
|
import org.isoron.uhabits.core.models.Entry
|
||||||
import org.isoron.uhabits.core.models.*;
|
import org.isoron.uhabits.core.models.Timestamp
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The SQLite database record corresponding to a {@link Entry}.
|
* The SQLite database record corresponding to a [Entry].
|
||||||
*/
|
*/
|
||||||
@Table(name = "Repetitions")
|
@Table(name = "Repetitions")
|
||||||
public class EntryRecord
|
class EntryRecord {
|
||||||
{
|
var habit: HabitRecord? = null
|
||||||
public HabitRecord habit;
|
|
||||||
|
|
||||||
@Column(name = "habit")
|
@field:Column(name = "habit")
|
||||||
public Long habitId;
|
var habitId: Long? = null
|
||||||
|
|
||||||
@Column
|
@field:Column
|
||||||
public Long timestamp;
|
var timestamp: Long? = null
|
||||||
|
|
||||||
@Column
|
@field:Column
|
||||||
public Integer value;
|
var value: Int? = null
|
||||||
|
|
||||||
@Column
|
@field:Column
|
||||||
public Long id;
|
var id: Long? = null
|
||||||
|
fun copyFrom(entry: Entry) {
|
||||||
public void copyFrom(Entry entry)
|
timestamp = entry.timestamp.unixTime
|
||||||
{
|
value = entry.value
|
||||||
timestamp = entry.getTimestamp().getUnixTime();
|
|
||||||
value = entry.getValue();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Entry toEntry()
|
fun toEntry(): Entry {
|
||||||
{
|
return Entry(Timestamp(timestamp!!), value!!)
|
||||||
return new Entry(new Timestamp(timestamp), value);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,139 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
|
||||||
*
|
|
||||||
* This file is part of Loop Habit Tracker.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by the
|
|
||||||
* Free Software Foundation, either version 3 of the License, or (at your
|
|
||||||
* option) any later version.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
|
||||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
|
||||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
||||||
* more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License along
|
|
||||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.isoron.uhabits.core.models.sqlite.records;
|
|
||||||
|
|
||||||
import org.isoron.uhabits.core.database.*;
|
|
||||||
import org.isoron.uhabits.core.models.*;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The SQLite database record corresponding to a {@link Habit}.
|
|
||||||
*/
|
|
||||||
@Table(name = "habits")
|
|
||||||
public class HabitRecord
|
|
||||||
{
|
|
||||||
@Column
|
|
||||||
public String description;
|
|
||||||
|
|
||||||
@Column
|
|
||||||
public String question;
|
|
||||||
|
|
||||||
@Column
|
|
||||||
public String name;
|
|
||||||
|
|
||||||
@Column(name = "freq_num")
|
|
||||||
public Integer freqNum;
|
|
||||||
|
|
||||||
@Column(name = "freq_den")
|
|
||||||
public Integer freqDen;
|
|
||||||
|
|
||||||
@Column
|
|
||||||
public Integer color;
|
|
||||||
|
|
||||||
@Column
|
|
||||||
public Integer position;
|
|
||||||
|
|
||||||
@Column(name = "reminder_hour")
|
|
||||||
public Integer reminderHour;
|
|
||||||
|
|
||||||
@Column(name = "reminder_min")
|
|
||||||
public Integer reminderMin;
|
|
||||||
|
|
||||||
@Column(name = "reminder_days")
|
|
||||||
public Integer reminderDays;
|
|
||||||
|
|
||||||
@Column
|
|
||||||
public Integer highlight;
|
|
||||||
|
|
||||||
@Column
|
|
||||||
public Integer archived;
|
|
||||||
|
|
||||||
@Column
|
|
||||||
public Integer type;
|
|
||||||
|
|
||||||
@Column(name = "target_value")
|
|
||||||
public Double targetValue;
|
|
||||||
|
|
||||||
@Column(name = "target_type")
|
|
||||||
public Integer targetType;
|
|
||||||
|
|
||||||
@Column
|
|
||||||
public String unit;
|
|
||||||
|
|
||||||
@Column
|
|
||||||
public Long id;
|
|
||||||
|
|
||||||
@Column
|
|
||||||
public String uuid;
|
|
||||||
|
|
||||||
public void copyFrom(Habit model)
|
|
||||||
{
|
|
||||||
this.id = model.getId();
|
|
||||||
this.name = model.getName();
|
|
||||||
this.description = model.getDescription();
|
|
||||||
this.highlight = 0;
|
|
||||||
this.color = model.getColor().getPaletteIndex();
|
|
||||||
this.archived = model.isArchived() ? 1 : 0;
|
|
||||||
this.type = model.getType();
|
|
||||||
this.targetType = model.getTargetType();
|
|
||||||
this.targetValue = model.getTargetValue();
|
|
||||||
this.unit = model.getUnit();
|
|
||||||
this.position = model.getPosition();
|
|
||||||
this.question = model.getQuestion();
|
|
||||||
this.uuid = model.getUuid();
|
|
||||||
|
|
||||||
Frequency freq = model.getFrequency();
|
|
||||||
this.freqNum = freq.getNumerator();
|
|
||||||
this.freqDen = freq.getDenominator();
|
|
||||||
this.reminderDays = 0;
|
|
||||||
this.reminderMin = null;
|
|
||||||
this.reminderHour = null;
|
|
||||||
|
|
||||||
if (model.hasReminder())
|
|
||||||
{
|
|
||||||
Reminder reminder = model.getReminder();
|
|
||||||
this.reminderHour = reminder.getHour();
|
|
||||||
this.reminderMin = reminder.getMinute();
|
|
||||||
this.reminderDays = reminder.getDays().toInteger();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void copyTo(Habit habit)
|
|
||||||
{
|
|
||||||
habit.setId(this.id);
|
|
||||||
habit.setName(this.name);
|
|
||||||
habit.setDescription(this.description);
|
|
||||||
habit.setQuestion(this.question);
|
|
||||||
habit.setFrequency(new Frequency(this.freqNum, this.freqDen));
|
|
||||||
habit.setColor(new PaletteColor(this.color));
|
|
||||||
habit.setArchived(this.archived != 0);
|
|
||||||
habit.setType(this.type);
|
|
||||||
habit.setTargetType(this.targetType);
|
|
||||||
habit.setTargetValue(this.targetValue);
|
|
||||||
habit.setUnit(this.unit);
|
|
||||||
habit.setPosition(this.position);
|
|
||||||
habit.setUuid(this.uuid);
|
|
||||||
|
|
||||||
if (reminderHour != null && reminderMin != null)
|
|
||||||
{
|
|
||||||
habit.setReminder(new Reminder(reminderHour, reminderMin,
|
|
||||||
new WeekdayList(reminderDays)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user