Merge pull request #709 from hiqua/dev

pull/718/head
Alinson S. Xavier 5 years ago committed by GitHub
commit e671949dd2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -49,7 +49,7 @@ Further resources:
## Code Style
For Kotlin, we follow [ktlint](https://ktlint.github.io/) style with default settings. This code style is enforced by our automated build pipeline. To make sure that IntelliJ and Android Studio are configured according to ktlint, run `./gradlew ktlintApplyToIdea`. To check that all code is properly formatted, run `./gradlew ktlintCheck`. See more details in [ktlint-gradle](https://github.com/jlleitschuh/ktlint-gradle).
For Kotlin, we follow [ktlint](https://ktlint.github.io/) style with default settings. This code style is enforced by our automated build pipeline. To make sure that IntelliJ and Android Studio are configured according to ktlint, run `./gradlew ktlintApplyToIdea`. To check that all code is properly formatted, run `./gradlew ktlintCheck`. You can install a Git pre-commit hook to ensure that the code is properly formatted when you commit using `./gradlew addKtlintFormatGitPreCommitHook`. See more details in [ktlint-gradle](https://github.com/jlleitschuh/ktlint-gradle).
For legacy Java code, we don't have strict guidelines. Please follow a code style similar to the file you are modifying. Note that new classes should be written in Kotlin. Pull requests converting existing Java code to Kotlin are also welcome.

@ -125,6 +125,7 @@ dependencies {
testImplementation "junit:junit:4.12"
testImplementation "org.mockito:mockito-core:2.28.2"
testImplementation "org.mockito:mockito-inline:2.8.9"
testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0"
}
kapt {

@ -38,19 +38,13 @@ import org.mockito.Mockito.mock
@Module
class TestModule {
@Provides fun ListHabitsBehavior() = mock(ListHabitsBehavior::class.java)
@Provides fun listHabitsBehavior(): ListHabitsBehavior = mock(ListHabitsBehavior::class.java)
}
@ActivityScope
@Component(
modules = arrayOf(
ActivityContextModule::class,
HabitsActivityModule::class,
ListHabitsModule::class,
HabitModule::class,
TestModule::class
),
dependencies = arrayOf(HabitsApplicationComponent::class)
modules = [ActivityContextModule::class, HabitsActivityModule::class, ListHabitsModule::class, HabitModule::class, TestModule::class],
dependencies = [HabitsApplicationComponent::class]
)
interface HabitsActivityTestComponent {
fun getCheckmarkPanelViewFactory(): CheckmarkPanelViewFactory

@ -1,56 +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.acceptance;
import androidx.test.filters.*;
import androidx.test.runner.*;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.isoron.uhabits.*;
import org.junit.*;
import org.junit.runner.*;
import static org.isoron.uhabits.acceptance.steps.CommonSteps.*;
import static org.isoron.uhabits.acceptance.steps.ListHabitsSteps.MenuItem.*;
import static org.isoron.uhabits.acceptance.steps.ListHabitsSteps.*;
@RunWith(AndroidJUnit4.class)
@LargeTest
public class AboutTest extends BaseUserInterfaceTest
{
@Test
public void shouldDisplayAboutScreen() {
launchApp();
clickMenu(ABOUT);
verifyDisplaysText("Loop Habit Tracker");
verifyDisplaysText("Rate this app on Google Play");
verifyDisplaysText("Developers");
verifyDisplaysText("Translators");
}
@Test
public void shouldDisplayAboutScreenFromSettings() {
launchApp();
clickMenu(SETTINGS);
clickText("About");
verifyDisplaysText("Translators");
}
}

@ -0,0 +1,52 @@
/*
* 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.acceptance
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import org.isoron.uhabits.BaseUserInterfaceTest
import org.isoron.uhabits.acceptance.steps.CommonSteps.launchApp
import org.isoron.uhabits.acceptance.steps.CommonSteps.verifyDisplaysText
import org.isoron.uhabits.acceptance.steps.ListHabitsSteps
import org.isoron.uhabits.acceptance.steps.ListHabitsSteps.clickMenu
import org.isoron.uhabits.acceptance.steps.WidgetSteps.clickText
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
@LargeTest
class AboutTest : BaseUserInterfaceTest() {
@Test
fun shouldDisplayAboutScreen() {
launchApp()
clickMenu(ListHabitsSteps.MenuItem.ABOUT)
verifyDisplaysText("Loop Habit Tracker")
verifyDisplaysText("Rate this app on Google Play")
verifyDisplaysText("Developers")
verifyDisplaysText("Translators")
}
@Test
fun shouldDisplayAboutScreenFromSettings() {
launchApp()
clickMenu(ListHabitsSteps.MenuItem.SETTINGS)
clickText("About")
verifyDisplaysText("Translators")
}
}

@ -27,6 +27,7 @@ import org.isoron.uhabits.acceptance.steps.CommonSteps.longClickText
import org.isoron.uhabits.acceptance.steps.CommonSteps.verifyDisplaysText
import org.isoron.uhabits.acceptance.steps.CommonSteps.verifyDoesNotDisplayText
import org.isoron.uhabits.acceptance.steps.ListHabitsSteps
import org.isoron.uhabits.acceptance.steps.ListHabitsSteps.clickMenu
import org.isoron.uhabits.acceptance.steps.clearBackupFolder
import org.isoron.uhabits.acceptance.steps.clearDownloadFolder
import org.isoron.uhabits.acceptance.steps.copyBackupToDownloadFolder
@ -45,7 +46,7 @@ class BackupTest : BaseUserInterfaceTest() {
copyBackupToDownloadFolder()
longClickText("Wake up early")
ListHabitsSteps.clickMenu(ListHabitsSteps.MenuItem.DELETE)
clickMenu(ListHabitsSteps.MenuItem.DELETE)
clickText("Yes")
verifyDoesNotDisplayText("Wake up early")

@ -1,200 +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.acceptance;
import androidx.test.filters.*;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.isoron.uhabits.*;
import org.junit.*;
import org.junit.runner.*;
import static org.isoron.uhabits.acceptance.steps.CommonSteps.Screen.*;
import static org.isoron.uhabits.acceptance.steps.CommonSteps.*;
import static org.isoron.uhabits.acceptance.steps.EditHabitSteps.*;
import static org.isoron.uhabits.acceptance.steps.ListHabitsSteps.MenuItem.*;
import static org.isoron.uhabits.acceptance.steps.ListHabitsSteps.*;
@RunWith(AndroidJUnit4.class)
@LargeTest
public class HabitsTest extends BaseUserInterfaceTest
{
@Test
public void shouldCreateHabit() throws Exception {
shouldCreateHabit("this is a test description");
}
@Test
public void shouldCreateHabitBlankDescription() throws Exception {
shouldCreateHabit("");
}
private void shouldCreateHabit(String description) throws Exception
{
launchApp();
verifyShowsScreen(LIST_HABITS);
clickMenu(ADD);
verifyShowsScreen(SELECT_HABIT_TYPE);
clickText("Yes or No");
verifyShowsScreen(EDIT_HABIT);
String testName = "Hello world";
typeName(testName);
typeQuestion("Did you say hello to the world today?");
typeDescription(description);
pickFrequency();
pickColor(5);
clickSave();
verifyShowsScreen(LIST_HABITS);
verifyDisplaysText(testName);
}
@Test
public void shouldShowHabitStatistics() throws Exception
{
launchApp();
verifyShowsScreen(LIST_HABITS);
clickText("Track time");
verifyShowsScreen(SHOW_HABIT);
verifyDisplayGraphs();
}
@Test
public void shouldDeleteHabit() throws Exception
{
launchApp();
verifyShowsScreen(LIST_HABITS);
longClickText("Track time");
clickMenu(DELETE);
clickText("Yes");
verifyDoesNotDisplayText("Track time");
}
@Test
public void shouldEditHabit() throws Exception {
shouldEditHabit("this is a test description");
}
@Test
public void shouldEditHabitBlankDescription() throws Exception {
shouldEditHabit("");
}
private void shouldEditHabit(String description) throws Exception
{
launchApp();
verifyShowsScreen(LIST_HABITS);
longClickText("Track time");
clickMenu(EDIT);
verifyShowsScreen(EDIT_HABIT);
typeName("Take a walk");
typeQuestion("Did you take a walk today?");
typeDescription(description);
clickSave();
verifyShowsScreen(LIST_HABITS);
verifyDisplaysTextInSequence("Wake up early", "Take a walk", "Meditate");
verifyDoesNotDisplayText("Track time");
}
@Test
public void shouldEditHabit_fromStatisticsScreen() throws Exception
{
launchApp();
verifyShowsScreen(LIST_HABITS);
clickText("Track time");
verifyShowsScreen(SHOW_HABIT);
clickMenu(EDIT);
verifyShowsScreen(EDIT_HABIT);
typeName("Take a walk");
typeQuestion("Did you take a walk today?");
pickColor(10);
clickSave();
verifyShowsScreen(SHOW_HABIT);
verifyDisplaysText("Take a walk");
pressBack();
verifyShowsScreen(LIST_HABITS);
verifyDisplaysText("Take a walk");
verifyDoesNotDisplayText("Track time");
}
@Test
public void shouldArchiveAndUnarchiveHabits() throws Exception
{
launchApp();
verifyShowsScreen(LIST_HABITS);
longClickText("Track time");
clickMenu(ARCHIVE);
verifyDoesNotDisplayText("Track time");
clickMenu(TOGGLE_ARCHIVED);
verifyDisplaysText("Track time");
longClickText("Track time");
clickMenu(UNARCHIVE);
clickMenu(TOGGLE_ARCHIVED);
verifyDisplaysText("Track time");
}
@Test
public void shouldToggleCheckmarksAndUpdateScore() throws Exception
{
launchApp();
verifyShowsScreen(LIST_HABITS);
longPressCheckmarks("Wake up early", 2);
clickText("Wake up early");
verifyShowsScreen(SHOW_HABIT);
verifyDisplaysText("10%");
}
@Test
public void shouldHideCompleted() throws Exception
{
launchApp();
verifyShowsScreen(LIST_HABITS);
verifyDisplaysText("Track time");
verifyDisplaysText("Wake up early");
clickMenu(TOGGLE_COMPLETED);
verifyDoesNotDisplayText("Track time");
verifyDisplaysText("Wake up early");
longPressCheckmarks("Wake up early", 1);
verifyDoesNotDisplayText("Wake up early");
clickMenu(TOGGLE_COMPLETED);
verifyDisplaysText("Track time");
verifyDisplaysText("Wake up early");
}
}

@ -0,0 +1,193 @@
/*
* 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.acceptance
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import org.isoron.uhabits.BaseUserInterfaceTest
import org.isoron.uhabits.acceptance.steps.CommonSteps
import org.isoron.uhabits.acceptance.steps.CommonSteps.clickText
import org.isoron.uhabits.acceptance.steps.CommonSteps.launchApp
import org.isoron.uhabits.acceptance.steps.CommonSteps.longClickText
import org.isoron.uhabits.acceptance.steps.CommonSteps.pressBack
import org.isoron.uhabits.acceptance.steps.CommonSteps.verifyDisplayGraphs
import org.isoron.uhabits.acceptance.steps.CommonSteps.verifyDisplaysText
import org.isoron.uhabits.acceptance.steps.CommonSteps.verifyDisplaysTextInSequence
import org.isoron.uhabits.acceptance.steps.CommonSteps.verifyDoesNotDisplayText
import org.isoron.uhabits.acceptance.steps.CommonSteps.verifyShowsScreen
import org.isoron.uhabits.acceptance.steps.EditHabitSteps.clickSave
import org.isoron.uhabits.acceptance.steps.EditHabitSteps.pickColor
import org.isoron.uhabits.acceptance.steps.EditHabitSteps.pickFrequency
import org.isoron.uhabits.acceptance.steps.EditHabitSteps.typeDescription
import org.isoron.uhabits.acceptance.steps.EditHabitSteps.typeName
import org.isoron.uhabits.acceptance.steps.EditHabitSteps.typeQuestion
import org.isoron.uhabits.acceptance.steps.ListHabitsSteps
import org.isoron.uhabits.acceptance.steps.ListHabitsSteps.clickMenu
import org.isoron.uhabits.acceptance.steps.ListHabitsSteps.longPressCheckmarks
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
@LargeTest
class HabitsTest : BaseUserInterfaceTest() {
@Test
@Throws(Exception::class)
fun shouldCreateHabit() {
shouldCreateHabit("this is a test description")
}
@Test
@Throws(Exception::class)
fun shouldCreateHabitBlankDescription() {
shouldCreateHabit("")
}
@Throws(Exception::class)
private fun shouldCreateHabit(description: String) {
launchApp()
verifyShowsScreen(CommonSteps.Screen.LIST_HABITS)
clickMenu(ListHabitsSteps.MenuItem.ADD)
verifyShowsScreen(CommonSteps.Screen.SELECT_HABIT_TYPE)
clickText("Yes or No")
verifyShowsScreen(CommonSteps.Screen.EDIT_HABIT)
val testName = "Hello world"
typeName(testName)
typeQuestion("Did you say hello to the world today?")
typeDescription(description)
pickFrequency()
pickColor(5)
clickSave()
verifyShowsScreen(CommonSteps.Screen.LIST_HABITS)
verifyDisplaysText(testName)
}
@Test
@Throws(Exception::class)
fun shouldShowHabitStatistics() {
launchApp()
verifyShowsScreen(CommonSteps.Screen.LIST_HABITS)
clickText("Track time")
verifyShowsScreen(CommonSteps.Screen.SHOW_HABIT)
verifyDisplayGraphs()
}
@Test
@Throws(Exception::class)
fun shouldDeleteHabit() {
launchApp()
verifyShowsScreen(CommonSteps.Screen.LIST_HABITS)
longClickText("Track time")
clickMenu(ListHabitsSteps.MenuItem.DELETE)
clickText("Yes")
verifyDoesNotDisplayText("Track time")
}
@Test
@Throws(Exception::class)
fun shouldEditHabit() {
shouldEditHabit("this is a test description")
}
@Test
@Throws(Exception::class)
fun shouldEditHabitBlankDescription() {
shouldEditHabit("")
}
@Throws(Exception::class)
private fun shouldEditHabit(description: String) {
launchApp()
verifyShowsScreen(CommonSteps.Screen.LIST_HABITS)
longClickText("Track time")
clickMenu(ListHabitsSteps.MenuItem.EDIT)
verifyShowsScreen(CommonSteps.Screen.EDIT_HABIT)
typeName("Take a walk")
typeQuestion("Did you take a walk today?")
typeDescription(description)
clickSave()
verifyShowsScreen(CommonSteps.Screen.LIST_HABITS)
verifyDisplaysTextInSequence("Wake up early", "Take a walk", "Meditate")
verifyDoesNotDisplayText("Track time")
}
@Test
@Throws(Exception::class)
fun shouldEditHabit_fromStatisticsScreen() {
launchApp()
verifyShowsScreen(CommonSteps.Screen.LIST_HABITS)
clickText("Track time")
verifyShowsScreen(CommonSteps.Screen.SHOW_HABIT)
clickMenu(ListHabitsSteps.MenuItem.EDIT)
verifyShowsScreen(CommonSteps.Screen.EDIT_HABIT)
typeName("Take a walk")
typeQuestion("Did you take a walk today?")
pickColor(10)
clickSave()
verifyShowsScreen(CommonSteps.Screen.SHOW_HABIT)
verifyDisplaysText("Take a walk")
pressBack()
verifyShowsScreen(CommonSteps.Screen.LIST_HABITS)
verifyDisplaysText("Take a walk")
verifyDoesNotDisplayText("Track time")
}
@Test
@Throws(Exception::class)
fun shouldArchiveAndUnarchiveHabits() {
launchApp()
verifyShowsScreen(CommonSteps.Screen.LIST_HABITS)
longClickText("Track time")
clickMenu(ListHabitsSteps.MenuItem.ARCHIVE)
verifyDoesNotDisplayText("Track time")
clickMenu(ListHabitsSteps.MenuItem.TOGGLE_ARCHIVED)
verifyDisplaysText("Track time")
longClickText("Track time")
clickMenu(ListHabitsSteps.MenuItem.UNARCHIVE)
clickMenu(ListHabitsSteps.MenuItem.TOGGLE_ARCHIVED)
verifyDisplaysText("Track time")
}
@Test
@Throws(Exception::class)
fun shouldToggleCheckmarksAndUpdateScore() {
launchApp()
verifyShowsScreen(CommonSteps.Screen.LIST_HABITS)
longPressCheckmarks("Wake up early", 2)
clickText("Wake up early")
verifyShowsScreen(CommonSteps.Screen.SHOW_HABIT)
verifyDisplaysText("10%")
}
@Test
@Throws(Exception::class)
fun shouldHideCompleted() {
launchApp()
verifyShowsScreen(CommonSteps.Screen.LIST_HABITS)
verifyDisplaysText("Track time")
verifyDisplaysText("Wake up early")
clickMenu(ListHabitsSteps.MenuItem.TOGGLE_COMPLETED)
verifyDoesNotDisplayText("Track time")
verifyDisplaysText("Wake up early")
longPressCheckmarks("Wake up early", 1)
verifyDoesNotDisplayText("Wake up early")
clickMenu(ListHabitsSteps.MenuItem.TOGGLE_COMPLETED)
verifyDisplaysText("Track time")
verifyDisplaysText("Wake up early")
}
}

@ -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.acceptance;
import androidx.test.filters.*;
import androidx.test.runner.*;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.isoron.uhabits.*;
import org.junit.*;
import org.junit.runner.*;
import static org.isoron.uhabits.acceptance.steps.CommonSteps.*;
import static org.isoron.uhabits.acceptance.steps.ListHabitsSteps.MenuItem.*;
import static org.isoron.uhabits.acceptance.steps.ListHabitsSteps.*;
@RunWith(AndroidJUnit4.class)
@LargeTest
public class LinksTest extends BaseUserInterfaceTest
{
@Test
public void shouldLinkToSourceCode() throws Exception
{
launchApp();
clickMenu(ABOUT);
clickText("View source code at GitHub");
verifyOpensWebsite("github.com");
}
@Test
public void shouldLinkToTranslationWebsite() throws Exception
{
launchApp();
clickMenu(ABOUT);
clickText("Help translate this app");
verifyOpensWebsite("translate.loophabits.org");
}
@Test
public void shouldLinkToHelp() throws Exception {
launchApp();
clickMenu(HELP);
verifyOpensWebsite("github.com");
}
@Test
public void shouldLinkToHelpFromSettings() throws Exception {
launchApp();
clickMenu(SETTINGS);
clickText("Help & FAQ");
verifyOpensWebsite("github.com");
}
}

@ -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.acceptance
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import org.isoron.uhabits.BaseUserInterfaceTest
import org.isoron.uhabits.acceptance.steps.CommonSteps.launchApp
import org.isoron.uhabits.acceptance.steps.CommonSteps.verifyOpensWebsite
import org.isoron.uhabits.acceptance.steps.ListHabitsSteps
import org.isoron.uhabits.acceptance.steps.ListHabitsSteps.clickMenu
import org.isoron.uhabits.acceptance.steps.WidgetSteps.clickText
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
@LargeTest
class LinksTest : BaseUserInterfaceTest() {
@Test
@Throws(Exception::class)
fun shouldLinkToSourceCode() {
launchApp()
clickMenu(ListHabitsSteps.MenuItem.ABOUT)
clickText("View source code at GitHub")
verifyOpensWebsite("github.com")
}
@Test
@Throws(Exception::class)
fun shouldLinkToTranslationWebsite() {
launchApp()
clickMenu(ListHabitsSteps.MenuItem.ABOUT)
clickText("Help translate this app")
verifyOpensWebsite("translate.loophabits.org")
}
@Test
@Throws(Exception::class)
fun shouldLinkToHelp() {
launchApp()
clickMenu(ListHabitsSteps.MenuItem.HELP)
verifyOpensWebsite("github.com")
}
@Test
@Throws(Exception::class)
fun shouldLinkToHelpFromSettings() {
launchApp()
clickMenu(ListHabitsSteps.MenuItem.SETTINGS)
clickText("Help & FAQ")
verifyOpensWebsite("github.com")
}
}

@ -1,55 +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.acceptance;
import androidx.test.filters.*;
import org.isoron.uhabits.*;
import org.junit.*;
import static org.isoron.uhabits.acceptance.steps.CommonSteps.*;
import static org.isoron.uhabits.acceptance.steps.WidgetSteps.*;
import static org.isoron.uhabits.acceptance.steps.WidgetSteps.clickText;
@LargeTest
public class WidgetTest extends BaseUserInterfaceTest
{
@Test
public void shouldCreateAndToggleCheckmarkWidget() throws Exception
{
dragCheckmarkWidgetToHomeScreen();
Thread.sleep(3000);
clickText("Wake up early");
clickText("Save");
verifyCheckmarkWidgetIsShown();
clickCheckmarkWidget();
launchApp();
clickText("Wake up early");
verifyDisplaysText("5%");
pressHome();
clickCheckmarkWidget();
launchApp();
clickText("Wake up early");
verifyDisplaysText("0%");
}
}

@ -0,0 +1,52 @@
/*
* 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.acceptance
import androidx.test.filters.LargeTest
import org.isoron.uhabits.BaseUserInterfaceTest
import org.isoron.uhabits.acceptance.steps.CommonSteps.launchApp
import org.isoron.uhabits.acceptance.steps.CommonSteps.pressHome
import org.isoron.uhabits.acceptance.steps.CommonSteps.verifyDisplaysText
import org.isoron.uhabits.acceptance.steps.WidgetSteps.clickCheckmarkWidget
import org.isoron.uhabits.acceptance.steps.WidgetSteps.clickText
import org.isoron.uhabits.acceptance.steps.WidgetSteps.dragCheckmarkWidgetToHomeScreen
import org.isoron.uhabits.acceptance.steps.WidgetSteps.verifyCheckmarkWidgetIsShown
import org.junit.Test
@LargeTest
class WidgetTest : BaseUserInterfaceTest() {
@Test
@Throws(Exception::class)
fun shouldCreateAndToggleCheckmarkWidget() {
dragCheckmarkWidgetToHomeScreen()
Thread.sleep(3000)
clickText("Wake up early")
clickText("Save")
verifyCheckmarkWidgetIsShown()
clickCheckmarkWidget()
launchApp()
clickText("Wake up early")
verifyDisplaysText("5%")
pressHome()
clickCheckmarkWidget()
launchApp()
clickText("Wake up early")
verifyDisplaysText("0%")
}
}

@ -20,8 +20,8 @@
package org.isoron.uhabits.acceptance.steps
import androidx.test.uiautomator.UiSelector
import org.isoron.uhabits.BaseUserInterfaceTest.device
import org.isoron.uhabits.acceptance.steps.CommonSteps.clickText
import org.isoron.uhabits.acceptance.steps.CommonSteps.device
import org.isoron.uhabits.acceptance.steps.ListHabitsSteps.MenuItem.SETTINGS
import org.isoron.uhabits.acceptance.steps.ListHabitsSteps.clickMenu

@ -1,177 +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.acceptance.steps;
import android.view.*;
import androidx.annotation.*;
import androidx.recyclerview.widget.*;
import androidx.test.espresso.*;
import androidx.test.espresso.contrib.*;
import androidx.test.uiautomator.*;
import org.hamcrest.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.R;
import org.isoron.uhabits.activities.habits.list.*;
import static android.os.Build.VERSION.*;
import static androidx.test.espresso.Espresso.*;
import static androidx.test.espresso.action.ViewActions.*;
import static androidx.test.espresso.assertion.PositionAssertions.*;
import static androidx.test.espresso.assertion.ViewAssertions.*;
import static androidx.test.espresso.matcher.ViewMatchers.*;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
public class CommonSteps extends BaseUserInterfaceTest
{
public static void pressBack()
{
device.pressBack();
}
public static void clickText(String text)
{
scrollToText(text);
onView(withText(text)).perform(click());
}
public static void clickText(@StringRes int id)
{
onView(withText(id)).perform(click());
}
public static void launchApp()
{
startActivity(ListHabitsActivity.class);
assertTrue(
device.wait(Until.hasObject(By.pkg("org.isoron.uhabits")), 5000));
device.waitForIdle();
}
public static void longClickText(String text)
{
scrollToText(text);
onView(withText(text)).perform(longClick());
}
public static void pressHome()
{
device.pressHome();
device.waitForIdle();
}
public static void scrollToText(String text)
{
try
{
if (device
.findObject(new UiSelector().className(RecyclerView.class))
.exists())
{
onView(instanceOf(RecyclerView.class)).perform(
RecyclerViewActions.scrollTo(
hasDescendant(withText(text))));
}
else
{
onView(withText(text)).perform(scrollTo());
}
}
catch (PerformException e)
{
//ignored
}
}
public static void verifyDisplayGraphs()
{
verifyDisplaysView("HistoryCard");
verifyDisplaysView("ScoreCard");
}
public static void verifyDisplaysText(String text)
{
scrollToText(text);
onView(withText(text)).check(matches(isEnabled()));
}
public static void verifyDisplaysTextInSequence(String... text)
{
verifyDisplaysText(text[0]);
for(int i = 1; i < text.length; i++) {
verifyDisplaysText(text[i]);
onView(withText(text[i])).check(isCompletelyBelow(withText(text[i-1])));
}
}
private static void verifyDisplaysView(String className)
{
onView(withClassName(endsWith(className))).check(matches(isEnabled()));
}
public static void verifyDoesNotDisplayText(String text)
{
onView(withText(text)).check(doesNotExist());
}
public static void verifyOpensWebsite(String url) throws Exception
{
String browser_pkg = "org.chromium.webview_shell";
if(SDK_INT <= 23) {
browser_pkg = "com.android.browser";
}
assertTrue(device.wait(Until.hasObject(By.pkg(browser_pkg)), 5000));
device.waitForIdle();
assertTrue(device.findObject(new UiSelector().textContains(url)).exists());
}
public enum Screen
{
LIST_HABITS, SHOW_HABIT, EDIT_HABIT, SELECT_HABIT_TYPE
}
public static void verifyShowsScreen(Screen screen)
{
switch(screen)
{
case LIST_HABITS:
onView(withClassName(endsWith("ListHabitsRootView")))
.check(matches(isDisplayed()));
break;
case SHOW_HABIT:
onView(withId(R.id.subtitleCard)).check(matches(isDisplayed()));
break;
case EDIT_HABIT:
onView(withId(R.id.questionInput)).check(matches(isDisplayed()));
break;
case SELECT_HABIT_TYPE:
onView(withText(R.string.yes_or_no_example)).check(matches(isDisplayed()));
break;
default:
throw new IllegalStateException();
}
}
}

@ -0,0 +1,157 @@
/*
* 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.acceptance.steps
import android.os.Build.VERSION
import androidx.annotation.StringRes
import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.Espresso
import androidx.test.espresso.PerformException
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.assertion.PositionAssertions
import androidx.test.espresso.assertion.ViewAssertions
import androidx.test.espresso.contrib.RecyclerViewActions
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiSelector
import androidx.test.uiautomator.Until
import junit.framework.Assert.assertTrue
import org.hamcrest.CoreMatchers
import org.isoron.uhabits.BaseUserInterfaceTest
import org.isoron.uhabits.R
import org.isoron.uhabits.activities.habits.list.ListHabitsActivity
object CommonSteps : BaseUserInterfaceTest() {
fun pressBack() {
device.pressBack()
}
fun clickText(text: String?) {
scrollToText(text)
Espresso.onView(ViewMatchers.withText(text)).perform(ViewActions.click())
}
fun clickText(@StringRes id: Int) {
Espresso.onView(ViewMatchers.withText(id)).perform(ViewActions.click())
}
fun launchApp() {
startActivity(ListHabitsActivity::class.java)
assertTrue(
device.wait(Until.hasObject(By.pkg("org.isoron.uhabits")), 5000)
)
device.waitForIdle()
}
fun longClickText(text: String?) {
scrollToText(text)
Espresso.onView(ViewMatchers.withText(text)).perform(ViewActions.longClick())
}
fun pressHome() {
device.pressHome()
device.waitForIdle()
}
fun scrollToText(text: String?) {
try {
if (device
.findObject(UiSelector().className(RecyclerView::class.java))
.exists()
) {
Espresso.onView(CoreMatchers.instanceOf(RecyclerView::class.java)).perform(
RecyclerViewActions.scrollTo<RecyclerView.ViewHolder>(
ViewMatchers.hasDescendant(ViewMatchers.withText(text))
)
)
} else {
Espresso.onView(ViewMatchers.withText(text)).perform(ViewActions.scrollTo())
}
} catch (e: PerformException) {
// ignored
}
}
fun verifyDisplayGraphs() {
verifyDisplaysView("HistoryCard")
verifyDisplaysView("ScoreCard")
}
fun verifyDisplaysText(text: String?) {
scrollToText(text)
Espresso.onView(ViewMatchers.withText(text))
.check(ViewAssertions.matches(ViewMatchers.isEnabled()))
}
fun verifyDisplaysTextInSequence(vararg text: String?) {
verifyDisplaysText(text[0])
for (i in 1 until text.size) {
verifyDisplaysText(text[i])
Espresso.onView(ViewMatchers.withText(text[i])).check(
PositionAssertions.isCompletelyBelow(
ViewMatchers.withText(
text[i - 1]
)
)
)
}
}
private fun verifyDisplaysView(className: String) {
Espresso.onView(ViewMatchers.withClassName(CoreMatchers.endsWith(className)))
.check(ViewAssertions.matches(ViewMatchers.isEnabled()))
}
fun verifyDoesNotDisplayText(text: String?) {
Espresso.onView(ViewMatchers.withText(text)).check(ViewAssertions.doesNotExist())
}
@Throws(Exception::class)
fun verifyOpensWebsite(url: String?) {
var browserPkg = "org.chromium.webview_shell"
if (VERSION.SDK_INT <= 23) {
browserPkg = "com.android.browser"
}
assertTrue(device.wait(Until.hasObject(By.pkg(browserPkg)), 5000))
device.waitForIdle()
assertTrue(device.findObject(UiSelector().textContains(url)).exists())
}
fun verifyShowsScreen(screen: Screen?) {
when (screen) {
Screen.LIST_HABITS ->
Espresso.onView(ViewMatchers.withClassName(CoreMatchers.endsWith("ListHabitsRootView")))
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
Screen.SHOW_HABIT ->
Espresso.onView(ViewMatchers.withId(R.id.subtitleCard))
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
Screen.EDIT_HABIT ->
Espresso.onView(ViewMatchers.withId(R.id.questionInput))
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
Screen.SELECT_HABIT_TYPE ->
Espresso.onView(ViewMatchers.withText(R.string.yes_or_no_example))
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
else -> throw IllegalStateException()
}
}
enum class Screen {
LIST_HABITS, SHOW_HABIT, EDIT_HABIT, SELECT_HABIT_TYPE
}
}

@ -1,92 +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.acceptance.steps;
import androidx.test.uiautomator.*;
import org.isoron.uhabits.*;
import static androidx.test.espresso.Espresso.*;
import static androidx.test.espresso.action.ViewActions.*;
import static androidx.test.espresso.action.ViewActions.closeSoftKeyboard;
import static androidx.test.espresso.matcher.ViewMatchers.*;
import static org.isoron.uhabits.BaseUserInterfaceTest.*;
public class EditHabitSteps
{
public static void clickSave()
{
onView(withId(R.id.buttonSave)).perform(click());
}
public static void pickFrequency()
{
onView(withId(R.id.boolean_frequency_picker)).perform(click());
onView(withText("SAVE")).perform(click());
}
public static void pickColor(int color)
{
onView(withId(R.id.colorButton)).perform(click());
device.findObject(By.descStartsWith(String.format("Color %d", color))).click();
}
public static void typeName(String name)
{
typeTextWithId(R.id.nameInput, name);
}
public static void typeQuestion(String name)
{
typeTextWithId(R.id.questionInput, name);
}
public static void typeDescription(String description)
{
typeTextWithId(R.id.notesInput, description);
}
public static void setReminder()
{
onView(withId(R.id.reminderTimePicker)).perform(click());
onView(withId(R.id.done_button)).perform(click());
}
public static void clickReminderDays()
{
onView(withId(R.id.reminderDatePicker)).perform(click());
}
public static void unselectAllDays()
{
onView(withText("Saturday")).perform(click());
onView(withText("Sunday")).perform(click());
onView(withText("Monday")).perform(click());
onView(withText("Tuesday")).perform(click());
onView(withText("Wednesday")).perform(click());
onView(withText("Thursday")).perform(click());
onView(withText("Friday")).perform(click());
}
private static void typeTextWithId(int id, String name)
{
onView(withId(id)).perform(clearText(), typeText(name), closeSoftKeyboard());
}
}

@ -0,0 +1,83 @@
/*
* 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.acceptance.steps
import androidx.test.espresso.Espresso
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.uiautomator.By
import org.isoron.uhabits.BaseUserInterfaceTest
import org.isoron.uhabits.R
object EditHabitSteps {
fun clickSave() {
Espresso.onView(ViewMatchers.withId(R.id.buttonSave)).perform(ViewActions.click())
}
fun pickFrequency() {
Espresso.onView(ViewMatchers.withId(R.id.boolean_frequency_picker))
.perform(ViewActions.click())
Espresso.onView(ViewMatchers.withText("SAVE")).perform(ViewActions.click())
}
fun pickColor(color: Int) {
Espresso.onView(ViewMatchers.withId(R.id.colorButton)).perform(ViewActions.click())
BaseUserInterfaceTest.device.findObject(By.descStartsWith(String.format("Color %d", color)))
.click()
}
fun typeName(name: String) {
typeTextWithId(R.id.nameInput, name)
}
fun typeQuestion(name: String) {
typeTextWithId(R.id.questionInput, name)
}
fun typeDescription(description: String) {
typeTextWithId(R.id.notesInput, description)
}
fun setReminder() {
Espresso.onView(ViewMatchers.withId(R.id.reminderTimePicker)).perform(ViewActions.click())
Espresso.onView(ViewMatchers.withId(R.id.done_button)).perform(ViewActions.click())
}
fun clickReminderDays() {
Espresso.onView(ViewMatchers.withId(R.id.reminderDatePicker)).perform(ViewActions.click())
}
fun unselectAllDays() {
Espresso.onView(ViewMatchers.withText("Saturday")).perform(ViewActions.click())
Espresso.onView(ViewMatchers.withText("Sunday")).perform(ViewActions.click())
Espresso.onView(ViewMatchers.withText("Monday")).perform(ViewActions.click())
Espresso.onView(ViewMatchers.withText("Tuesday")).perform(ViewActions.click())
Espresso.onView(ViewMatchers.withText("Wednesday")).perform(ViewActions.click())
Espresso.onView(ViewMatchers.withText("Thursday")).perform(ViewActions.click())
Espresso.onView(ViewMatchers.withText("Friday")).perform(ViewActions.click())
}
private fun typeTextWithId(id: Int, name: String) {
Espresso.onView(ViewMatchers.withId(id)).perform(
ViewActions.clearText(),
ViewActions.typeText(name),
ViewActions.closeSoftKeyboard()
)
}
}

@ -1,161 +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.acceptance.steps;
import androidx.test.espresso.*;
import android.view.*;
import org.hamcrest.*;
import org.isoron.uhabits.R;
import org.isoron.uhabits.activities.habits.list.views.*;
import java.util.*;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.matcher.ViewMatchers.hasDescendant;
import static androidx.test.espresso.matcher.ViewMatchers.isEnabled;
import static androidx.test.espresso.matcher.ViewMatchers.withClassName;
import static androidx.test.espresso.matcher.ViewMatchers.withContentDescription;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withParent;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static org.hamcrest.CoreMatchers.*;
import static org.isoron.uhabits.BaseUserInterfaceTest.device;
import static org.isoron.uhabits.acceptance.steps.CommonSteps.clickText;
public abstract class ListHabitsSteps
{
public static void clickMenu(MenuItem item)
{
switch (item)
{
case ABOUT:
clickTextInsideOverflowMenu(R.string.about);
break;
case HELP:
clickTextInsideOverflowMenu(R.string.help);
break;
case SETTINGS:
clickTextInsideOverflowMenu(R.string.settings);
break;
case ADD:
clickViewWithId(R.id.actionCreateHabit);
break;
case EDIT:
clickViewWithId(R.id.action_edit_habit);
break;
case DELETE:
clickTextInsideOverflowMenu(R.string.delete);
break;
case ARCHIVE:
clickTextInsideOverflowMenu(R.string.archive);
break;
case UNARCHIVE:
clickTextInsideOverflowMenu(R.string.unarchive);
break;
case TOGGLE_ARCHIVED:
clickViewWithId(R.id.action_filter);
clickText(R.string.hide_archived);
break;
case TOGGLE_COMPLETED:
clickViewWithId(R.id.action_filter);
clickText(R.string.hide_completed);
break;
}
}
private static void clickTextInsideOverflowMenu(int id) {
onView(allOf(withContentDescription("More options"), withParent(withParent(withClassName(endsWith("Toolbar")))))).perform(click());
onView(withText(id)).perform(click());
}
private static void clickViewWithId(int id)
{
onView(withId(id)).perform(click());
}
private static ViewAction longClickDescendantWithClass(Class cls, int count)
{
return new ViewAction()
{
@Override
public Matcher<View> getConstraints()
{
return isEnabled();
}
@Override
public String getDescription()
{
return "perform on children";
}
@Override
public void perform(UiController uiController, View view)
{
LinkedList<ViewGroup> stack = new LinkedList<>();
if (view instanceof ViewGroup) stack.push((ViewGroup) view);
int countRemaining = count;
while (!stack.isEmpty())
{
ViewGroup vg = stack.pop();
for (int i = 0; i < vg.getChildCount(); i++)
{
View v = vg.getChildAt(i);
if (v instanceof ViewGroup) stack.push((ViewGroup) v);
if (cls.isInstance(v) && countRemaining > 0)
{
v.performLongClick();
uiController.loopMainThreadUntilIdle();
countRemaining--;
}
}
}
}
};
}
public static void longPressCheckmarks(String habit, int count)
{
CommonSteps.scrollToText(habit);
onView(allOf(hasDescendant(withText(habit)),
withClassName(endsWith("HabitCardView")))).perform(
longClickDescendantWithClass(CheckmarkButtonView.class, count));
device.waitForIdle();
}
public enum MenuItem
{
ABOUT, HELP, SETTINGS, EDIT, DELETE, ARCHIVE, TOGGLE_ARCHIVED,
UNARCHIVE, TOGGLE_COMPLETED, ADD
}
}

@ -0,0 +1,124 @@
/*
* 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.acceptance.steps
import android.view.View
import android.view.ViewGroup
import androidx.test.espresso.Espresso
import androidx.test.espresso.UiController
import androidx.test.espresso.ViewAction
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.matcher.ViewMatchers
import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.endsWith
import org.hamcrest.Matcher
import org.isoron.uhabits.BaseUserInterfaceTest
import org.isoron.uhabits.R
import org.isoron.uhabits.activities.habits.list.views.CheckmarkButtonView
import java.util.LinkedList
object ListHabitsSteps {
fun clickMenu(item: MenuItem?) {
when (item) {
MenuItem.ABOUT -> clickTextInsideOverflowMenu(R.string.about)
MenuItem.HELP -> clickTextInsideOverflowMenu(R.string.help)
MenuItem.SETTINGS -> clickTextInsideOverflowMenu(R.string.settings)
MenuItem.ADD -> clickViewWithId(R.id.actionCreateHabit)
MenuItem.EDIT -> clickViewWithId(R.id.action_edit_habit)
MenuItem.DELETE -> clickTextInsideOverflowMenu(R.string.delete)
MenuItem.ARCHIVE -> clickTextInsideOverflowMenu(R.string.archive)
MenuItem.UNARCHIVE -> clickTextInsideOverflowMenu(R.string.unarchive)
MenuItem.TOGGLE_ARCHIVED -> {
clickViewWithId(R.id.action_filter)
CommonSteps.clickText(R.string.hide_archived)
}
MenuItem.TOGGLE_COMPLETED -> {
clickViewWithId(R.id.action_filter)
CommonSteps.clickText(R.string.hide_completed)
}
}
}
private fun clickTextInsideOverflowMenu(id: Int) {
Espresso.onView(
allOf(
ViewMatchers.withContentDescription("More options"),
ViewMatchers.withParent(
ViewMatchers.withParent(
ViewMatchers.withClassName(
endsWith("Toolbar")
)
)
)
)
).perform(ViewActions.click())
Espresso.onView(ViewMatchers.withText(id)).perform(ViewActions.click())
}
private fun clickViewWithId(id: Int) {
Espresso.onView(ViewMatchers.withId(id)).perform(ViewActions.click())
}
private fun longClickDescendantWithClass(cls: Class<*>, count: Int): ViewAction {
return object : ViewAction {
override fun getConstraints(): Matcher<View> {
return ViewMatchers.isEnabled()
}
override fun getDescription(): String {
return "perform on children"
}
override fun perform(uiController: UiController, view: View) {
val stack = LinkedList<ViewGroup>()
if (view is ViewGroup) stack.push(view)
var countRemaining = count
while (!stack.isEmpty()) {
val vg = stack.pop()
for (i in 0 until vg.childCount) {
val v = vg.getChildAt(i)
if (v is ViewGroup) stack.push(v)
if (cls.isInstance(v) && countRemaining > 0) {
v.performLongClick()
uiController.loopMainThreadUntilIdle()
countRemaining--
}
}
}
}
}
}
fun longPressCheckmarks(habit: String?, count: Int) {
CommonSteps.scrollToText(habit)
Espresso.onView(
allOf(
ViewMatchers.hasDescendant(ViewMatchers.withText(habit)),
ViewMatchers.withClassName(endsWith("HabitCardView"))
)
).perform(
longClickDescendantWithClass(CheckmarkButtonView::class.java, count)
)
BaseUserInterfaceTest.device.waitForIdle()
}
enum class MenuItem {
ABOUT, HELP, SETTINGS, EDIT, DELETE, ARCHIVE, TOGGLE_ARCHIVED, UNARCHIVE, TOGGLE_COMPLETED, ADD
}
}

@ -1,87 +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.acceptance.steps;
import androidx.test.uiautomator.*;
import static android.os.Build.VERSION.SDK_INT;
import static org.junit.Assert.*;
import static org.isoron.uhabits.BaseUserInterfaceTest.*;
public class WidgetSteps {
public static void clickCheckmarkWidget() throws Exception {
String view_id = "org.isoron.uhabits:id/imageView";
device.findObject(new UiSelector().resourceId(view_id)).click();
}
public static void clickText(String s) throws Exception {
UiObject object = device.findObject(new UiSelector().text(s));
if (!object.waitForExists(1000)) {
object = device.findObject(new UiSelector().text(s.toUpperCase()));
}
object.click();
}
public static void dragCheckmarkWidgetToHomeScreen() throws Exception {
openWidgetScreen();
dragWidgetToHomeScreen();
}
private static void dragWidgetToHomeScreen() throws Exception {
int height = device.getDisplayHeight();
int width = device.getDisplayWidth();
device.findObject(new UiSelector().text("Checkmark"))
.dragTo(width / 2, height / 2, 40);
}
private static void openWidgetScreen() throws Exception {
int h = device.getDisplayHeight();
int w = device.getDisplayWidth();
if (SDK_INT <= 21) {
device.pressHome();
device.waitForIdle();
device.findObject(new UiSelector().description("Apps")).click();
device.findObject(new UiSelector().description("Apps")).click();
device.findObject(new UiSelector().description("Widgets")).click();
} else {
String list_id = "com.android.launcher3:id/widgets_list_view";
device.pressHome();
device.waitForIdle();
device.drag(w / 2, h / 2, w / 2, h / 2, 8);
UiObject button = device.findObject(new UiSelector().text("WIDGETS"));
if(!button.waitForExists(1000)) {
button = device.findObject(new UiSelector().text("Widgets"));
}
button.click();
if (SDK_INT >= 28) {
new UiScrollable(new UiSelector().resourceId(list_id))
.scrollForward();
}
new UiScrollable(new UiSelector().resourceId(list_id))
.scrollIntoView(new UiSelector().text("Checkmark"));
}
}
public static void verifyCheckmarkWidgetIsShown() throws Exception {
String view_id = "org.isoron.uhabits:id/imageView";
assertTrue(device.findObject(new UiSelector().resourceId(view_id)).exists());
assertFalse(device.findObject(new UiSelector().textStartsWith("Habit deleted")).exists());
}
}

@ -0,0 +1,98 @@
/*
* 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.acceptance.steps
import android.os.Build.VERSION
import androidx.test.uiautomator.UiScrollable
import androidx.test.uiautomator.UiSelector
import junit.framework.Assert.assertFalse
import junit.framework.Assert.assertTrue
import org.isoron.uhabits.BaseUserInterfaceTest
object WidgetSteps {
@Throws(Exception::class)
fun clickCheckmarkWidget() {
val viewId = "org.isoron.uhabits:id/imageView"
BaseUserInterfaceTest.device.findObject(UiSelector().resourceId(viewId)).click()
}
@Throws(Exception::class)
fun clickText(s: String) {
var textObject = BaseUserInterfaceTest.device.findObject(UiSelector().text(s))
if (!textObject.waitForExists(1000)) {
textObject = BaseUserInterfaceTest.device.findObject(UiSelector().text(s.toUpperCase()))
}
textObject.click()
}
@Throws(Exception::class)
fun dragCheckmarkWidgetToHomeScreen() {
openWidgetScreen()
dragWidgetToHomeScreen()
}
@Throws(Exception::class)
private fun dragWidgetToHomeScreen() {
val height = BaseUserInterfaceTest.device.displayHeight
val width = BaseUserInterfaceTest.device.displayWidth
BaseUserInterfaceTest.device.findObject(UiSelector().text("Checkmark"))
.dragTo(width / 2, height / 2, 40)
}
@Throws(Exception::class)
private fun openWidgetScreen() {
val h = BaseUserInterfaceTest.device.displayHeight
val w = BaseUserInterfaceTest.device.displayWidth
if (VERSION.SDK_INT <= 21) {
BaseUserInterfaceTest.device.pressHome()
BaseUserInterfaceTest.device.waitForIdle()
BaseUserInterfaceTest.device.findObject(UiSelector().description("Apps")).click()
BaseUserInterfaceTest.device.findObject(UiSelector().description("Apps")).click()
BaseUserInterfaceTest.device.findObject(UiSelector().description("Widgets")).click()
} else {
val listId = "com.android.launcher3:id/widgets_list_view"
BaseUserInterfaceTest.device.pressHome()
BaseUserInterfaceTest.device.waitForIdle()
BaseUserInterfaceTest.device.drag(w / 2, h / 2, w / 2, h / 2, 8)
var button = BaseUserInterfaceTest.device.findObject(UiSelector().text("WIDGETS"))
if (!button.waitForExists(1000)) {
button = BaseUserInterfaceTest.device.findObject(UiSelector().text("Widgets"))
}
button.click()
if (VERSION.SDK_INT >= 28) {
UiScrollable(UiSelector().resourceId(listId))
.scrollForward()
}
UiScrollable(UiSelector().resourceId(listId))
.scrollIntoView(UiSelector().text("Checkmark"))
}
}
@Throws(Exception::class)
fun verifyCheckmarkWidgetIsShown() {
val viewId = "org.isoron.uhabits:id/imageView"
assertTrue(
BaseUserInterfaceTest.device.findObject(UiSelector().resourceId(viewId)).exists()
)
assertFalse(
BaseUserInterfaceTest.device.findObject(UiSelector().textStartsWith("Habit deleted"))
.exists()
)
}
}

@ -34,11 +34,12 @@ import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
private const val PATH = "habits/list/CheckmarkPanelView"
@RunWith(AndroidJUnit4::class)
@MediumTest
class EntryPanelViewTest : BaseViewTest() {
private val PATH = "habits/list/CheckmarkPanelView"
private lateinit var view: CheckmarkPanelView
@Before
@ -75,27 +76,6 @@ class EntryPanelViewTest : BaseViewTest() {
assertRenders(view, "$PATH/render.png")
}
// // Flaky test
// @Test
// fun testRender_withDifferentColor() {
// view.color = PaletteUtils.getAndroidTestColor(1)
// assertRenders(view, "$PATH/render_different_color.png")
// }
// // Flaky test
// @Test
// fun testRender_Reversed() {
// prefs.isCheckmarkSequenceReversed = true
// assertRenders(view, "$PATH/render_reversed.png")
// }
// // Flaky test
// @Test
// fun testRender_withOffset() {
// view.dataOffset = 3
// assertRenders(view, "$PATH/render_offset.png")
// }
@Test
fun testToggle() {
val timestamps = mutableListOf<Timestamp>()

@ -56,7 +56,7 @@ class HabitCardViewTest : BaseViewTest() {
view = component.getHabitCardViewFactory().create().apply {
habit = habit1
values = entries
score = habit1.scores.get(today).value
score = habit1.scores[today].value
isSelected = false
buttonCount = 5
}

@ -73,27 +73,6 @@ class NumberPanelViewTest : BaseViewTest() {
assertRenders(view, "$PATH/render.png")
}
// // Flaky test
// @Test
// fun testRender_withDifferentColor() {
// view.color = PaletteUtils.getAndroidTestColor(1)
// assertRenders(view, "$PATH/render_different_color.png")
// }
// // Flaky test
// @Test
// fun testRender_Reversed() {
// prefs.isCheckmarkSequenceReversed = true
// assertRenders(view, "$PATH/render_reversed.png")
// }
// // Flaky test
// @Test
// fun testRender_withOffset() {
// view.dataOffset = 3
// assertRenders(view, "$PATH/render_offset.png")
// }
@Test
fun testEdit() {
val timestamps = mutableListOf<Timestamp>()

@ -26,7 +26,7 @@ import org.isoron.uhabits.R
import org.isoron.uhabits.core.models.Frequency
import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.models.Reminder
import org.isoron.uhabits.core.models.WeekdayList.EVERY_DAY
import org.isoron.uhabits.core.models.WeekdayList.Companion.EVERY_DAY
import org.isoron.uhabits.core.ui.screens.habits.show.views.SubtitleCardState
import org.junit.Before
import org.junit.Test

@ -103,9 +103,8 @@ class IntentSchedulerTest : BaseAndroidTest() {
assertNull(ReminderReceiver.lastReceivedIntent)
setSystemTime("America/Chicago", 2020, JUNE, 2, 22, 46)
val intent = ReminderReceiver.lastReceivedIntent
assertNotNull(intent)
assertThat(parseId(intent?.data!!), equalTo(habit.id))
val intent = ReminderReceiver.lastReceivedIntent!!
assertThat(parseId(intent.data!!), equalTo(habit.id))
}
@Test
@ -123,7 +122,6 @@ class IntentSchedulerTest : BaseAndroidTest() {
assertNull(WidgetReceiver.lastReceivedIntent)
setSystemTime("America/Chicago", 2020, JUNE, 2, 22, 46)
val intent = WidgetReceiver.lastReceivedIntent
assertNotNull(intent)
WidgetReceiver.lastReceivedIntent!!
}
}

@ -68,13 +68,6 @@ public class CheckmarkWidgetViewTest extends BaseViewTest
assertRenders(view, PATH + "checked.png");
}
// @Test
// public void testRender_implicitlyChecked() throws IOException
// {
// view.setCheckmarkValue(Checkmark.YES_AUTO);
// view.refresh();
// assertRenders(view, PATH + "implicitly_checked.png");
// }
@Test
public void testRender_largeSize() throws IOException
@ -83,11 +76,4 @@ public class CheckmarkWidgetViewTest extends BaseViewTest
assertRenders(view, PATH + "large_size.png");
}
// @Test
// public void testRender_unchecked() throws IOException
// {
// view.setCheckmarkValue(Checkmark.NO);
// view.refresh();
// assertRenders(view, PATH + "unchecked.png");
// }
}

@ -114,7 +114,7 @@ public class DatePickerDialog extends DialogFragment implements
public interface OnDateSetListener {
/**
* @param view The view associated with this listener.
* @param dialog The dialog associated with this listener.
* @param year The year that was set.
* @param monthOfYear The month that was set (0-11) for compatibility
* with {@link java.util.Calendar}.

@ -167,7 +167,7 @@ public abstract class DayPickerView extends ListView implements OnScrollListener
* the list will not be scrolled unless forceScroll is true. This time may
* optionally be highlighted as selected as well.
*
* @param time The time to move to
* @param day The day to move to
* @param animate Whether to scroll to the given time or just redraw at the
* new location
* @param setSelected Whether to set the given time as selected

@ -392,7 +392,7 @@ public class RadialPickerLayout extends FrameLayout implements OnTouchListener {
* Returns mapping of any input degrees (0 to 360) to one of 12 visible output degrees (all
* multiples of 30), where the input will be "snapped" to the closest visible degrees.
* @param degrees The input degrees
* @param forceAboveOrBelow The output may be forced to either the higher or lower step, or may
* @param forceHigherOrLower The output may be forced to either the higher or lower step, or may
* be allowed to snap to whichever is closer. Use 1 to force strictly higher, -1 to force
* strictly lower, and 0 to snap to the closer one.
* @return output degrees, will be a multiple of 30

@ -720,7 +720,7 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
/**
* Get out of keyboard mode. If there is nothing in typedTimes, revert to TimePicker's time.
*
* @param changeDisplays If true, update the displays with the relevant time.
* @param updateDisplays If true, update the displays with the relevant time.
*/
private void finishKbMode(boolean updateDisplays)
{

@ -25,6 +25,8 @@ import android.util.AttributeSet
import android.view.GestureDetector
import android.view.MotionEvent
import android.widget.Scroller
import kotlin.math.abs
import kotlin.math.max
/**
* An AndroidView that implements scrolling.
@ -71,7 +73,7 @@ class AndroidDataView(
dx: Float,
dy: Float,
): Boolean {
if (Math.abs(dx) > Math.abs(dy)) {
if (abs(dx) > abs(dy)) {
val parent = parent
parent?.requestDisallowInterceptTouchEvent(true)
}
@ -128,7 +130,7 @@ class AndroidDataView(
view?.let { v ->
var newDataOffset: Int =
scroller.currX / (v.dataColumnWidth * canvas.innerDensity).toInt()
newDataOffset = Math.max(0, newDataOffset)
newDataOffset = max(0, newDataOffset)
if (newDataOffset != v.dataOffset) {
v.dataOffset = newDataOffset
postInvalidate()

@ -42,7 +42,7 @@ class AndroidImage(private val bmp: Bitmap) : Image {
}
}
public fun Color.toInt(): Int {
fun Color.toInt(): Int {
return android.graphics.Color.argb(
(255 * this.alpha).roundToInt(),
(255 * this.red).roundToInt(),

@ -68,7 +68,7 @@ open class AndroidBugReporter @Inject constructor(@AppContext private val contex
if (log.size > maxLineCount) log.removeFirst()
}
for (l in log) {
builder.appendln(l)
builder.appendLine(l)
}
return builder.toString()
}
@ -99,18 +99,18 @@ open class AndroidBugReporter @Inject constructor(@AppContext private val contex
private fun getDeviceInfo(): String {
val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
return buildString {
appendln("App Version Name: ${BuildConfig.VERSION_NAME}")
appendln("App Version Code: ${BuildConfig.VERSION_CODE}")
appendln("OS Version: ${System.getProperty("os.version")} (${Build.VERSION.INCREMENTAL})")
appendln("OS API Level: ${Build.VERSION.SDK_INT}")
appendln("Device: ${Build.DEVICE}")
appendln("Model (Product): ${Build.MODEL} (${Build.PRODUCT})")
appendln("Manufacturer: ${Build.MANUFACTURER}")
appendln("Other tags: ${Build.TAGS}")
appendln("Screen Width: ${wm.defaultDisplay.width}")
appendln("Screen Height: ${wm.defaultDisplay.height}")
appendln("External storage state: ${Environment.getExternalStorageState()}")
appendln()
appendLine("App Version Name: ${BuildConfig.VERSION_NAME}")
appendLine("App Version Code: ${BuildConfig.VERSION_CODE}")
appendLine("OS Version: ${System.getProperty("os.version")} (${Build.VERSION.INCREMENTAL})")
appendLine("OS API Level: ${Build.VERSION.SDK_INT}")
appendLine("Device: ${Build.DEVICE}")
appendLine("Model (Product): ${Build.MODEL} (${Build.PRODUCT})")
appendLine("Manufacturer: ${Build.MANUFACTURER}")
appendLine("Other tags: ${Build.TAGS}")
appendLine("Screen Width: ${wm.defaultDisplay.width}")
appendLine("Screen Height: ${wm.defaultDisplay.height}")
appendLine("External storage state: ${Environment.getExternalStorageState()}")
appendLine()
}
}
}

@ -83,7 +83,7 @@ class HabitsApplication : Application() {
notificationTray.startListening()
val prefs = component.preferences
prefs.setLastAppVersion(BuildConfig.VERSION_CODE)
prefs.lastAppVersion = BuildConfig.VERSION_CODE
val taskRunner = component.taskRunner
taskRunner.execute {
@ -106,11 +106,11 @@ class HabitsApplication : Application() {
lateinit var component: HabitsApplicationComponent
fun isTestMode(): Boolean {
try {
return try {
Class.forName("org.isoron.uhabits.BaseAndroidTest")
return true
true
} catch (e: ClassNotFoundException) {
return false
false
}
}
}

@ -16,24 +16,20 @@
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.common.dialogs
package org.isoron.uhabits.activities.common.dialogs;
import org.isoron.uhabits.core.models.*;
import org.isoron.uhabits.core.ui.callbacks.*;
import org.isoron.uhabits.utils.*;
import com.android.colorpicker.ColorPickerDialog
import org.isoron.uhabits.core.ui.callbacks.OnColorPickedCallback
import org.isoron.uhabits.utils.toPaletteColor
/**
* Dialog that allows the user to choose a color.
*/
public class ColorPickerDialog extends com.android.colorpicker.ColorPickerDialog
{
public void setListener(OnColorPickedCallback callback)
{
super.setOnColorSelectedListener(c ->
{
PaletteColor pc = PaletteUtilsKt.toPaletteColor(c, getContext());
callback.onColorPicked(pc);
});
class ColorPickerDialog : ColorPickerDialog() {
fun setListener(callback: OnColorPickedCallback) {
super.setOnColorSelectedListener { c: Int ->
val pc = c.toPaletteColor(context!!)
callback.onColorPicked(pc)
}
}
}

@ -1,53 +0,0 @@
/*
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.common.dialogs;
import android.content.*;
import org.isoron.uhabits.R;
import org.isoron.uhabits.core.models.*;
import org.isoron.uhabits.inject.*;
import org.isoron.uhabits.utils.*;
import javax.inject.*;
@ActivityScope
public class ColorPickerDialogFactory
{
private final Context context;
@Inject
public ColorPickerDialogFactory(@ActivityContext Context context)
{
this.context = context;
}
public ColorPickerDialog create(PaletteColor color)
{
ColorPickerDialog dialog = new ColorPickerDialog();
StyledResources res = new StyledResources(context);
int androidColor = PaletteUtilsKt.toThemedAndroidColor(color, context);
dialog.initialize(R.string.color_picker_default_title, res.getPalette(),
androidColor, 4, com.android.colorpicker.ColorPickerDialog.SIZE_SMALL);
return dialog;
}
}

@ -0,0 +1,45 @@
/*
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.common.dialogs
import android.content.Context
import org.isoron.uhabits.R
import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.inject.ActivityContext
import org.isoron.uhabits.inject.ActivityScope
import org.isoron.uhabits.utils.StyledResources
import org.isoron.uhabits.utils.toThemedAndroidColor
import javax.inject.Inject
@ActivityScope
class ColorPickerDialogFactory @Inject constructor(@param:ActivityContext private val context: Context) {
fun create(color: PaletteColor): ColorPickerDialog {
val dialog = ColorPickerDialog()
val res = StyledResources(context)
val androidColor = color.toThemedAndroidColor(context)
dialog.initialize(
R.string.color_picker_default_title,
res.getPalette(),
androidColor,
4,
com.android.colorpicker.ColorPickerDialog.SIZE_SMALL
)
return dialog
}
}

@ -16,39 +16,34 @@
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.common.dialogs
package org.isoron.uhabits.activities.common.dialogs;
import android.content.*;
import android.content.res.*;
import androidx.annotation.*;
import androidx.appcompat.app.*;
import org.isoron.uhabits.R;
import org.isoron.uhabits.core.ui.callbacks.*;
import org.isoron.uhabits.inject.*;
import android.content.Context
import android.content.DialogInterface
import androidx.appcompat.app.AlertDialog
import org.isoron.uhabits.R
import org.isoron.uhabits.core.ui.callbacks.OnConfirmedCallback
import org.isoron.uhabits.inject.ActivityContext
/**
* Dialog that asks the user confirmation before executing a delete operation.
*/
public class ConfirmDeleteDialog extends AlertDialog
{
public ConfirmDeleteDialog(@ActivityContext Context context,
@NonNull OnConfirmedCallback callback,
int quantity)
{
super(context);
Resources res = context.getResources();
setTitle(res.getQuantityString(R.plurals.delete_habits_title, quantity));
setMessage(res.getQuantityString(R.plurals.delete_habits_message, quantity));
setButton(BUTTON_POSITIVE,
res.getString(R.string.yes),
(dialog, which) -> callback.onConfirmed()
);
setButton(BUTTON_NEGATIVE,
res.getString(R.string.no),
(dialog, which) -> { }
);
class ConfirmDeleteDialog(
@ActivityContext context: Context,
callback: OnConfirmedCallback,
quantity: Int
) : AlertDialog(context) {
init {
val res = context.resources
setTitle(res.getQuantityString(R.plurals.delete_habits_title, quantity))
setMessage(res.getQuantityString(R.plurals.delete_habits_message, quantity))
setButton(
BUTTON_POSITIVE,
res.getString(R.string.yes)
) { dialog: DialogInterface?, which: Int -> callback.onConfirmed() }
setButton(
BUTTON_NEGATIVE,
res.getString(R.string.no)
) { dialog: DialogInterface?, which: Int -> }
}
}

@ -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.activities.common.dialogs;
import android.content.*;
import android.content.res.*;
import androidx.annotation.*;
import androidx.appcompat.app.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.core.ui.callbacks.*;
import org.isoron.uhabits.inject.*;
public class ConfirmSyncKeyDialog extends AlertDialog
{
public ConfirmSyncKeyDialog(@ActivityContext Context context,
@NonNull OnConfirmedCallback callback)
{
super(context);
setTitle(R.string.device_sync);
Resources res = context.getResources();
setMessage(res.getString(R.string.sync_confirm));
setButton(BUTTON_POSITIVE,
res.getString(R.string.yes),
(dialog, which) -> callback.onConfirmed()
);
setButton(BUTTON_NEGATIVE,
res.getString(R.string.no),
(dialog, which) -> { }
);
}
}

@ -0,0 +1,45 @@
/*
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.common.dialogs
import android.content.Context
import android.content.DialogInterface
import androidx.appcompat.app.AlertDialog
import org.isoron.uhabits.R
import org.isoron.uhabits.core.ui.callbacks.OnConfirmedCallback
import org.isoron.uhabits.inject.ActivityContext
class ConfirmSyncKeyDialog(
@ActivityContext context: Context,
callback: OnConfirmedCallback
) : AlertDialog(context) {
init {
setTitle(R.string.device_sync)
val res = context.resources
setMessage(res.getString(R.string.sync_confirm))
setButton(
BUTTON_POSITIVE,
res.getString(R.string.yes)
) { dialog: DialogInterface?, which: Int -> callback.onConfirmed() }
setButton(
BUTTON_NEGATIVE,
res.getString(R.string.no)
) { dialog: DialogInterface?, which: Int -> }
}
}

@ -34,6 +34,7 @@ import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior
import org.isoron.uhabits.inject.ActivityContext
import org.isoron.uhabits.utils.InterfaceUtils
import javax.inject.Inject
import kotlin.math.roundToLong
class NumberPickerFactory
@Inject constructor(
@ -52,7 +53,7 @@ class NumberPickerFactory
val picker2 = view.findViewById<NumberPicker>(R.id.picker2)
val tvUnit = view.findViewById<TextView>(R.id.tvUnit)
val intValue = Math.round(value * 100).toInt()
val intValue = (value * 100).roundToLong().toInt()
picker.minValue = 0
picker.maxValue = Integer.MAX_VALUE / 100
@ -86,13 +87,12 @@ class NumberPickerFactory
}
InterfaceUtils.setupEditorAction(
picker,
TextView.OnEditorActionListener { _, actionId, _ ->
picker
) { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_DONE)
dialog.getButton(DialogInterface.BUTTON_POSITIVE).performClick()
false
}
)
return dialog
}

@ -1,107 +0,0 @@
/*
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.common.dialogs;
import android.app.Dialog;
import android.content.DialogInterface;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatDialogFragment;
import org.isoron.uhabits.R;
import org.isoron.uhabits.core.models.WeekdayList;
import org.isoron.uhabits.core.utils.DateUtils;
import java.util.Calendar;
/**
* Dialog that allows the user to pick one or more days of the week.
*/
public class WeekdayPickerDialog extends AppCompatDialogFragment implements
DialogInterface.OnMultiChoiceClickListener,
DialogInterface.OnClickListener
{
private static final String KEY_SELECTED_DAYS = "selectedDays";
private boolean[] selectedDays;
private OnWeekdaysPickedListener listener;
@Override
public void onClick(DialogInterface dialog, int which, boolean isChecked)
{
selectedDays[which] = isChecked;
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if(savedInstanceState != null){
selectedDays = savedInstanceState.getBooleanArray(KEY_SELECTED_DAYS);
}
}
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBooleanArray(KEY_SELECTED_DAYS, selectedDays);
}
@Override
public void onClick(DialogInterface dialog, int which)
{
if (listener != null)
listener.onWeekdaysSet(new WeekdayList(selectedDays));
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState)
{
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder
.setTitle(R.string.select_weekdays)
.setMultiChoiceItems(DateUtils.getLongWeekdayNames(Calendar.SATURDAY),
selectedDays,
this)
.setPositiveButton(android.R.string.yes, this)
.setNegativeButton(android.R.string.cancel, (dialog, which) -> {
dismiss();
});
return builder.create();
}
public void setListener(OnWeekdaysPickedListener listener)
{
this.listener = listener;
}
public void setSelectedDays(WeekdayList days)
{
this.selectedDays = days.toArray();
}
public interface OnWeekdaysPickedListener
{
void onWeekdaysSet(WeekdayList days);
}
}

@ -0,0 +1,94 @@
/*
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.common.dialogs
import android.app.Dialog
import android.content.DialogInterface
import android.content.DialogInterface.OnMultiChoiceClickListener
import android.os.Bundle
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatDialogFragment
import org.isoron.uhabits.R
import org.isoron.uhabits.core.models.WeekdayList
import org.isoron.uhabits.core.utils.DateUtils
import java.util.Calendar
/**
* Dialog that allows the user to pick one or more days of the week.
*/
class WeekdayPickerDialog :
AppCompatDialogFragment(),
OnMultiChoiceClickListener,
DialogInterface.OnClickListener {
private var selectedDays: BooleanArray? = null
private var listener: OnWeekdaysPickedListener? = null
override fun onClick(dialog: DialogInterface, which: Int, isChecked: Boolean) {
selectedDays!![which] = isChecked
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (savedInstanceState != null) {
selectedDays = savedInstanceState.getBooleanArray(KEY_SELECTED_DAYS)
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putBooleanArray(KEY_SELECTED_DAYS, selectedDays)
}
override fun onClick(dialog: DialogInterface, which: Int) {
if (listener != null) listener!!.onWeekdaysSet(WeekdayList(selectedDays))
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val builder = AlertDialog.Builder(
activity!!
)
builder
.setTitle(R.string.select_weekdays)
.setMultiChoiceItems(
DateUtils.getLongWeekdayNames(Calendar.SATURDAY),
selectedDays,
this
)
.setPositiveButton(android.R.string.yes, this)
.setNegativeButton(
android.R.string.cancel
) { _: DialogInterface?, _: Int -> dismiss() }
return builder.create()
}
fun setListener(listener: OnWeekdaysPickedListener?) {
this.listener = listener
}
fun setSelectedDays(days: WeekdayList) {
selectedDays = days.toArray()
}
fun interface OnWeekdaysPickedListener {
fun onWeekdaysSet(days: WeekdayList)
}
companion object {
private const val KEY_SELECTED_DAYS = "selectedDays"
}
}

@ -56,8 +56,7 @@ class TaskProgressBar(
fun update() {
val callback = {
val activeTaskCount = runner.activeTaskCount
val newVisibility = when (activeTaskCount) {
val newVisibility = when (runner.activeTaskCount) {
0 -> GONE
else -> VISIBLE
}

@ -197,7 +197,8 @@ class EditHabitActivity : AppCompatActivity() {
binding.reminderDatePicker.setOnClickListener {
val dialog = WeekdayPickerDialog()
dialog.setListener { days ->
dialog.setListener { days: WeekdayList ->
reminderDays = days
if (reminderDays.isEmpty) reminderDays = WeekdayList.EVERY_DAY
populateReminder()

@ -36,7 +36,7 @@ class HabitTypeDialog : AppCompatDialogFragment() {
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
): View {
val binding = SelectHabitTypeBinding.inflate(inflater, container, false)
binding.buttonYesNo.setOnClickListener {

@ -48,9 +48,9 @@ import org.isoron.uhabits.utils.dim
import org.isoron.uhabits.utils.dp
import org.isoron.uhabits.utils.setupToolbar
import org.isoron.uhabits.utils.sres
import java.lang.Math.max
import java.lang.Math.min
import javax.inject.Inject
import kotlin.math.max
import kotlin.math.min
const val MAX_CHECKMARK_COUNT = 60

@ -1,350 +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.annotation.NonNull;
import androidx.annotation.Nullable;
import android.view.*;
import androidx.recyclerview.widget.RecyclerView;
import org.isoron.uhabits.activities.habits.list.*;
import org.isoron.uhabits.core.models.*;
import org.isoron.uhabits.core.preferences.*;
import org.isoron.uhabits.core.ui.screens.habits.list.*;
import org.isoron.uhabits.core.utils.*;
import org.isoron.uhabits.inject.*;
import java.util.*;
import javax.inject.*;
/**
* Provides data that backs a {@link HabitCardListView}.
* <p>
* The data if fetched and cached by a {@link HabitCardListCache}. This adapter
* also holds a list of items that have been selected.
*/
@ActivityScope
public class HabitCardListAdapter
extends RecyclerView.Adapter<HabitCardViewHolder> implements
HabitCardListCache.Listener,
MidnightTimer.MidnightListener,
ListHabitsMenuBehavior.Adapter,
ListHabitsSelectionMenuBehavior.Adapter
{
@NonNull
private ModelObservable observable;
@Nullable
private HabitCardListView listView;
@NonNull
private final LinkedList<Habit> selected;
@NonNull
private final HabitCardListCache cache;
@NonNull
private Preferences preferences;
private final MidnightTimer midnightTimer;
@Inject
public HabitCardListAdapter(@NonNull HabitCardListCache cache,
@NonNull Preferences preferences,
@NonNull MidnightTimer midnightTimer)
{
this.preferences = preferences;
this.selected = new LinkedList<>();
this.observable = new ModelObservable();
this.cache = cache;
this.midnightTimer = midnightTimer;
cache.setListener(this);
cache.setCheckmarkCount(
ListHabitsRootViewKt.MAX_CHECKMARK_COUNT);
cache.setSecondaryOrder(preferences.getDefaultSecondaryOrder());
cache.setPrimaryOrder(preferences.getDefaultPrimaryOrder());
setHasStableIds(true);
}
@Override
public void atMidnight()
{
cache.refreshAllHabits();
}
public void cancelRefresh()
{
cache.cancelTasks();
}
/**
* Sets all items as not selected.
*/
@Override
public void clearSelection()
{
selected.clear();
notifyDataSetChanged();
observable.notifyListeners();
}
/**
* Returns the item that occupies a certain position on the list
*
* @param position position of the item
* @return the item at given position or null if position is invalid
*/
@Deprecated
@Nullable
public Habit getItem(int position)
{
return cache.getHabitByPosition(position);
}
@Override
public int getItemCount()
{
return cache.getHabitCount();
}
@Override
public long getItemId(int position)
{
return getItem(position).getId();
}
@NonNull
public ModelObservable getObservable()
{
return observable;
}
@Override
@NonNull
public List<Habit> getSelected()
{
return new LinkedList<>(selected);
}
/**
* Returns whether list of selected items is empty.
*
* @return true if selection is empty, false otherwise
*/
public boolean isSelectionEmpty()
{
return selected.isEmpty();
}
public boolean isSortable()
{
return cache.getPrimaryOrder() == HabitList.Order.BY_POSITION;
}
/**
* Notify the adapter that it has been attached to a ListView.
*/
public void onAttached()
{
cache.onAttached();
midnightTimer.addListener(this);
}
@Override
public void onBindViewHolder(@Nullable HabitCardViewHolder holder,
int position)
{
if (holder == null) return;
if (listView == null) return;
Habit habit = cache.getHabitByPosition(position);
double score = cache.getScore(habit.getId());
int checkmarks[] = cache.getCheckmarks(habit.getId());
boolean selected = this.selected.contains(habit);
listView.bindCardView(holder, habit, score, checkmarks, selected);
}
@Override
public void onViewAttachedToWindow(@Nullable HabitCardViewHolder holder)
{
if (listView == null) return;
listView.attachCardView(holder);
}
@Override
public void onViewDetachedFromWindow(@Nullable HabitCardViewHolder holder)
{
if (listView == null) return;
listView.detachCardView(holder);
}
@Override
public HabitCardViewHolder onCreateViewHolder(ViewGroup parent,
int viewType)
{
if (listView == null) return null;
View view = listView.createHabitCardView();
return new HabitCardViewHolder(view);
}
/**
* Notify the adapter that it has been detached from a ListView.
*/
public void onDetached()
{
cache.onDetached();
midnightTimer.removeListener(this);
}
@Override
public void onItemChanged(int position)
{
notifyItemChanged(position);
observable.notifyListeners();
}
@Override
public void onItemInserted(int position)
{
notifyItemInserted(position);
observable.notifyListeners();
}
@Override
public void onItemMoved(int fromPosition, int toPosition)
{
notifyItemMoved(fromPosition, toPosition);
observable.notifyListeners();
}
@Override
public void onItemRemoved(int position)
{
notifyItemRemoved(position);
observable.notifyListeners();
}
@Override
public void onRefreshFinished()
{
observable.notifyListeners();
}
/**
* Removes a list of habits from the adapter.
* <p>
* Note that this only has effect on the adapter cache. The database is not
* modified, and the change is lost when the cache is refreshed. This method
* is useful for making the ListView more responsive: while we wait for the
* database operation to finish, the cache can be modified to reflect the
* changes immediately.
*
* @param habits list of habits to be removed
*/
@Override
public void performRemove(List<Habit> habits)
{
for (Habit h : habits)
cache.remove(h.getId());
}
/**
* Changes the order of habits on the adapter.
* <p>
* Note that this only has effect on the adapter cache. The database is not
* modified, and the change is lost when the cache is refreshed. This method
* is useful for making the ListView more responsive: while we wait for the
* database operation to finish, the cache can be modified to reflect the
* changes immediately.
*
* @param from the habit that should be moved
* @param to the habit that currently occupies the desired position
*/
public void performReorder(int from, int to)
{
cache.reorder(from, to);
}
@Override
public void refresh()
{
cache.refreshAllHabits();
}
@Override
public void setFilter(HabitMatcher matcher)
{
cache.setFilter(matcher);
}
/**
* Sets the HabitCardListView that this adapter will provide data for.
* <p>
* This object will be used to generated new HabitCardViews, upon demand.
*
* @param listView the HabitCardListView associated with this adapter
*/
public void setListView(@Nullable HabitCardListView listView)
{
this.listView = listView;
}
@Override
public void setPrimaryOrder(HabitList.Order order)
{
cache.setPrimaryOrder(order);
preferences.setDefaultPrimaryOrder(order);
}
@Override
public void setSecondaryOrder(HabitList.Order order) {
cache.setSecondaryOrder(order);
preferences.setDefaultSecondaryOrder(order);
}
@Override
public HabitList.Order getPrimaryOrder()
{
return cache.getPrimaryOrder();
}
/**
* Selects or deselects the item at a given position.
*
* @param position position of the item to be toggled
*/
public void toggleSelection(int position)
{
Habit h = getItem(position);
if (h == null) return;
int k = selected.indexOf(h);
if (k < 0) selected.add(h);
else selected.remove(h);
notifyDataSetChanged();
}
}

@ -0,0 +1,263 @@
/*
* 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 android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import org.isoron.uhabits.activities.habits.list.MAX_CHECKMARK_COUNT
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.ModelObservable
import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.core.ui.screens.habits.list.HabitCardListCache
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsMenuBehavior
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsSelectionMenuBehavior
import org.isoron.uhabits.core.utils.MidnightTimer
import org.isoron.uhabits.inject.ActivityScope
import java.util.LinkedList
import javax.inject.Inject
/**
* Provides data that backs a [HabitCardListView].
*
*
* The data if fetched and cached by a [HabitCardListCache]. This adapter
* also holds a list of items that have been selected.
*/
@ActivityScope
class HabitCardListAdapter @Inject constructor(
private val cache: HabitCardListCache,
private val preferences: Preferences,
private val midnightTimer: MidnightTimer
) : RecyclerView.Adapter<HabitCardViewHolder?>(),
HabitCardListCache.Listener,
MidnightTimer.MidnightListener,
ListHabitsMenuBehavior.Adapter,
ListHabitsSelectionMenuBehavior.Adapter {
val observable: ModelObservable = ModelObservable()
private var listView: HabitCardListView? = null
private val selected: LinkedList<Habit> = LinkedList()
override fun atMidnight() {
cache.refreshAllHabits()
}
fun cancelRefresh() {
cache.cancelTasks()
}
/**
* Sets all items as not selected.
*/
override fun clearSelection() {
selected.clear()
notifyDataSetChanged()
observable.notifyListeners()
}
/**
* Returns the item that occupies a certain position on the list
*
* @param position position of the item
* @return the item at given position or null if position is invalid
*/
@Deprecated("")
fun getItem(position: Int): Habit? {
return cache.getHabitByPosition(position)
}
override fun getItemCount(): Int {
return cache.habitCount
}
override fun getItemId(position: Int): Long {
return getItem(position)!!.id!!
}
override fun getSelected(): List<Habit> {
return LinkedList(selected)
}
/**
* Returns whether list of selected items is empty.
*
* @return true if selection is empty, false otherwise
*/
val isSelectionEmpty: Boolean
get() = selected.isEmpty()
val isSortable: Boolean
get() = cache.primaryOrder == HabitList.Order.BY_POSITION
/**
* Notify the adapter that it has been attached to a ListView.
*/
fun onAttached() {
cache.onAttached()
midnightTimer.addListener(this)
}
override fun onBindViewHolder(
holder: HabitCardViewHolder,
position: Int
) {
if (listView == null) return
val habit = cache.getHabitByPosition(position)
val score = cache.getScore(habit!!.id!!)
val checkmarks = cache.getCheckmarks(habit.id!!)
val selected = selected.contains(habit)
listView!!.bindCardView(holder, habit, score, checkmarks, selected)
}
override fun onViewAttachedToWindow(holder: HabitCardViewHolder) {
listView!!.attachCardView(holder)
}
override fun onViewDetachedFromWindow(holder: HabitCardViewHolder) {
listView!!.detachCardView(holder)
}
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): HabitCardViewHolder {
val view = listView!!.createHabitCardView()
return HabitCardViewHolder(view)
}
/**
* Notify the adapter that it has been detached from a ListView.
*/
fun onDetached() {
cache.onDetached()
midnightTimer.removeListener(this)
}
override fun onItemChanged(position: Int) {
notifyItemChanged(position)
observable.notifyListeners()
}
override fun onItemInserted(position: Int) {
notifyItemInserted(position)
observable.notifyListeners()
}
override fun onItemMoved(fromPosition: Int, toPosition: Int) {
notifyItemMoved(fromPosition, toPosition)
observable.notifyListeners()
}
override fun onItemRemoved(position: Int) {
notifyItemRemoved(position)
observable.notifyListeners()
}
override fun onRefreshFinished() {
observable.notifyListeners()
}
/**
* Removes a list of habits from the adapter.
*
*
* Note that this only has effect on the adapter cache. The database is not
* modified, and the change is lost when the cache is refreshed. This method
* is useful for making the ListView more responsive: while we wait for the
* database operation to finish, the cache can be modified to reflect the
* changes immediately.
*
* @param habits list of habits to be removed
*/
override fun performRemove(habits: List<Habit>) {
for (habit in habits) cache.remove(habit.id!!)
}
/**
* Changes the order of habits on the adapter.
*
*
* Note that this only has effect on the adapter cache. The database is not
* modified, and the change is lost when the cache is refreshed. This method
* is useful for making the ListView more responsive: while we wait for the
* database operation to finish, the cache can be modified to reflect the
* changes immediately.
*
* @param from the habit that should be moved
* @param to the habit that currently occupies the desired position
*/
fun performReorder(from: Int, to: Int) {
cache.reorder(from, to)
}
override fun refresh() {
cache.refreshAllHabits()
}
override fun setFilter(matcher: HabitMatcher) {
cache.setFilter(matcher)
}
/**
* Sets the HabitCardListView that this adapter will provide data for.
*
*
* This object will be used to generated new HabitCardViews, upon demand.
*
* @param listView the HabitCardListView associated with this adapter
*/
fun setListView(listView: HabitCardListView?) {
this.listView = listView
}
override fun setPrimaryOrder(order: HabitList.Order) {
cache.primaryOrder = order
preferences.defaultPrimaryOrder = order
}
override fun setSecondaryOrder(order: HabitList.Order) {
cache.secondaryOrder = order
preferences.defaultSecondaryOrder = order
}
override fun getPrimaryOrder(): HabitList.Order {
return cache.primaryOrder
}
/**
* Selects or deselects the item at a given position.
*
* @param position position of the item to be toggled
*/
fun toggleSelection(position: Int) {
val h = getItem(position) ?: return
val k = selected.indexOf(h)
if (k < 0) selected.add(h) else selected.remove(h)
notifyDataSetChanged()
}
init {
cache.setListener(this)
cache.setCheckmarkCount(
MAX_CHECKMARK_COUNT
)
cache.secondaryOrder = preferences.defaultSecondaryOrder
cache.primaryOrder = preferences.defaultPrimaryOrder
setHasStableIds(true)
}
}

@ -39,12 +39,10 @@ class HabitCardListController @Inject constructor(
private val selectionMenu: Lazy<ListHabitsSelectionMenu>
) : HabitCardListView.Controller, ModelObservable.Listener {
private val NORMAL_MODE = NormalMode()
private val SELECTION_MODE = SelectionMode()
private var activeMode: Mode
init {
this.activeMode = NORMAL_MODE
this.activeMode = NormalMode()
adapter.observable.addListener(this)
}
@ -83,9 +81,9 @@ class HabitCardListController @Inject constructor(
activeMode.startDrag(position)
}
protected fun toggleSelection(position: Int) {
private fun toggleSelection(position: Int) {
adapter.toggleSelection(position)
activeMode = if (adapter.isSelectionEmpty) NORMAL_MODE else SELECTION_MODE
activeMode = if (adapter.isSelectionEmpty) NormalMode() else SelectionMode()
}
private fun cancelSelection() {
@ -116,8 +114,7 @@ class HabitCardListController @Inject constructor(
*/
internal inner class NormalMode : Mode {
override fun onItemClick(position: Int) {
val habit = adapter.getItem(position)
if (habit == null) return
val habit = adapter.getItem(position) ?: return
behavior.onClickHabit(habit)
}
@ -130,9 +127,9 @@ class HabitCardListController @Inject constructor(
startSelection(position)
}
protected fun startSelection(position: Int) {
private fun startSelection(position: Int) {
toggleSelection(position)
activeMode = SELECTION_MODE
activeMode = SelectionMode()
selectionMenu.get().onSelectionStart()
}
}
@ -158,8 +155,8 @@ class HabitCardListController @Inject constructor(
notifyListener()
}
protected fun notifyListener() {
if (activeMode === SELECTION_MODE)
private fun notifyListener() {
if (activeMode === SelectionMode())
selectionMenu.get().onSelectionChange()
else
selectionMenu.get().onSelectionFinish()

@ -170,22 +170,22 @@ class HabitCardListView(
inner class TouchHelperCallback : ItemTouchHelper.Callback() {
override fun getMovementFlags(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
viewHolder: ViewHolder
): Int {
return makeMovementFlags(UP or DOWN, START or END)
}
override fun onMove(
recyclerView: RecyclerView,
from: RecyclerView.ViewHolder,
to: RecyclerView.ViewHolder
from: ViewHolder,
to: ViewHolder
): Boolean {
controller.get().drop(from.adapterPosition, to.adapterPosition)
return true
}
override fun onSwiped(
viewHolder: RecyclerView.ViewHolder,
viewHolder: ViewHolder,
direction: Int
) {
}

@ -59,8 +59,8 @@ class HabitCardViewFactory
class HabitCardView(
@ActivityContext context: Context,
private val checkmarkPanelFactory: CheckmarkPanelViewFactory,
private val numberPanelFactory: NumberPanelViewFactory,
checkmarkPanelFactory: CheckmarkPanelViewFactory,
numberPanelFactory: NumberPanelViewFactory,
private val behavior: ListHabitsBehavior
) : FrameLayout(context),
ModelObservable.Listener {
@ -174,7 +174,7 @@ class HabitCardView(
}
clipToPadding = false
layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
layoutParams = LayoutParams(MATCH_PARENT, WRAP_CONTENT)
val margin = dp(3f).toInt()
setPadding(margin, 0, margin, margin)
addView(innerFrame)

@ -71,7 +71,7 @@ class NumberPanelView(
setupButtons()
}
override fun createButton() = buttonFactory.create()!!
override fun createButton() = buttonFactory.create()
@Synchronized
override fun setupButtons() {

@ -27,6 +27,7 @@ import org.isoron.uhabits.core.ui.screens.habits.show.views.OverviewCardState
import org.isoron.uhabits.databinding.ShowHabitOverviewBinding
import org.isoron.uhabits.utils.StyledResources
import org.isoron.uhabits.utils.toThemedAndroidColor
import kotlin.math.abs
class OverviewCardView(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) {
@ -36,7 +37,7 @@ class OverviewCardView(context: Context, attrs: AttributeSet) : LinearLayout(con
return String.format(
"%s%.0f%%",
if (percentageDiff >= 0) "+" else "\u2212",
Math.abs(percentageDiff) * 100
abs(percentageDiff) * 100
)
}

@ -1,236 +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.settings;
import android.app.backup.*;
import android.content.*;
import android.net.*;
import android.os.*;
import android.provider.*;
import android.util.*;
import androidx.annotation.*;
import androidx.preference.*;
import org.isoron.uhabits.R;
import org.isoron.uhabits.*;
import org.isoron.uhabits.core.preferences.*;
import org.isoron.uhabits.core.ui.*;
import org.isoron.uhabits.core.utils.*;
import org.isoron.uhabits.intents.*;
import org.isoron.uhabits.notifications.*;
import org.isoron.uhabits.widgets.*;
import java.util.*;
import static android.media.RingtoneManager.*;
import static android.os.Build.VERSION.*;
import static org.isoron.uhabits.activities.habits.list.ListHabitsScreenKt.*;
public class SettingsFragment extends PreferenceFragmentCompat
implements SharedPreferences.OnSharedPreferenceChangeListener
{
private static int RINGTONE_REQUEST_CODE = 1;
private SharedPreferences sharedPrefs;
private RingtoneManager ringtoneManager;
@NonNull
private Preferences prefs;
@Nullable
private WidgetUpdater widgetUpdater;
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data)
{
if (requestCode == RINGTONE_REQUEST_CODE)
{
ringtoneManager.update(data);
updateRingtoneDescription();
return;
}
super.onActivityResult(requestCode, resultCode, data);
}
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.preferences);
Context appContext = getContext().getApplicationContext();
if (appContext instanceof HabitsApplication)
{
HabitsApplication app = (HabitsApplication) appContext;
prefs = app.getComponent().getPreferences();
widgetUpdater = app.getComponent().getWidgetUpdater();
}
setResultOnPreferenceClick("importData", RESULT_IMPORT_DATA);
setResultOnPreferenceClick("exportCSV", RESULT_EXPORT_CSV);
setResultOnPreferenceClick("exportDB", RESULT_EXPORT_DB);
setResultOnPreferenceClick("repairDB", RESULT_REPAIR_DB);
setResultOnPreferenceClick("bugReport", RESULT_BUG_REPORT);
}
@Override
public void onCreatePreferences(Bundle bundle, String s)
{
// NOP
}
@Override
public void onPause()
{
sharedPrefs.unregisterOnSharedPreferenceChangeListener(this);
super.onPause();
}
@Override
public boolean onPreferenceTreeClick(Preference preference)
{
String key = preference.getKey();
if (key == null) return false;
if (key.equals("reminderSound"))
{
showRingtonePicker();
return true;
}
else if (key.equals("reminderCustomize"))
{
if (SDK_INT < Build.VERSION_CODES.O) return true;
AndroidNotificationTray.Companion.createAndroidNotificationChannel(getContext());
Intent intent = new Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS);
intent.putExtra(Settings.EXTRA_APP_PACKAGE, getContext().getPackageName());
intent.putExtra(Settings.EXTRA_CHANNEL_ID, NotificationTray.REMINDERS_CHANNEL_ID);
startActivity(intent);
return true;
}
else if (key.equals("pref_sync_enabled_dummy"))
{
if (prefs.isSyncEnabled())
{
prefs.disableSync();
}
else
{
Context context = getActivity();
context.startActivity(new IntentFactory().startSyncActivity(context));
}
}
return super.onPreferenceTreeClick(preference);
}
@Override
public void onResume()
{
super.onResume();
this.ringtoneManager = new RingtoneManager(getActivity());
sharedPrefs = getPreferenceManager().getSharedPreferences();
sharedPrefs.registerOnSharedPreferenceChangeListener(this);
if (!prefs.isDeveloper())
{
PreferenceCategory devCategory =
(PreferenceCategory) findPreference("devCategory");
devCategory.setVisible(false);
}
updateWeekdayPreference();
updateSyncPreferences();
// Temporarily disable this; we now always ask
findPreference("reminderSound").setVisible(false);
findPreference("pref_snooze_interval").setVisible(false);
}
private void updateSyncPreferences()
{
findPreference("pref_sync_display").setVisible(prefs.isSyncEnabled());
((CheckBoxPreference) findPreference("pref_sync_enabled_dummy")).setChecked(prefs.isSyncEnabled());
}
private void updateWeekdayPreference()
{
ListPreference weekdayPref = (ListPreference) findPreference("pref_first_weekday");
int currentFirstWeekday = prefs.getFirstWeekday().getDaysSinceSunday() + 1;
String[] dayNames = DateUtils.getLongWeekdayNames(Calendar.SATURDAY);
String[] dayValues = {"7", "1", "2", "3", "4", "5", "6"};
weekdayPref.setEntries(dayNames);
weekdayPref.setEntryValues(dayValues);
weekdayPref.setDefaultValue(Integer.toString(currentFirstWeekday));
weekdayPref.setSummary(dayNames[currentFirstWeekday % 7]);
}
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,
String key)
{
if (key.equals("pref_widget_opacity") && widgetUpdater != null)
{
Log.d("SettingsFragment", "updating widgets");
widgetUpdater.updateWidgets();
}
BackupManager.dataChanged("org.isoron.uhabits");
updateWeekdayPreference();
updateSyncPreferences();
}
private void setResultOnPreferenceClick(String key, final int result)
{
Preference pref = findPreference(key);
pref.setOnPreferenceClickListener(preference ->
{
getActivity().setResult(result);
getActivity().finish();
return true;
});
}
private void showRingtonePicker()
{
Uri existingRingtoneUri = ringtoneManager.getURI();
Uri defaultRingtoneUri = Settings.System.DEFAULT_NOTIFICATION_URI;
Intent intent = new Intent(ACTION_RINGTONE_PICKER);
intent.putExtra(EXTRA_RINGTONE_TYPE, TYPE_NOTIFICATION);
intent.putExtra(EXTRA_RINGTONE_SHOW_DEFAULT, true);
intent.putExtra(EXTRA_RINGTONE_SHOW_SILENT, true);
intent.putExtra(EXTRA_RINGTONE_DEFAULT_URI, defaultRingtoneUri);
intent.putExtra(EXTRA_RINGTONE_EXISTING_URI, existingRingtoneUri);
startActivityForResult(intent, RINGTONE_REQUEST_CODE);
}
private void updateRingtoneDescription()
{
String ringtoneName = ringtoneManager.getName();
if (ringtoneName == null) return;
Preference ringtonePreference = findPreference("reminderSound");
ringtonePreference.setSummary(ringtoneName);
}
}

@ -0,0 +1,201 @@
/*
* 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.settings
import android.app.backup.BackupManager
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
import android.os.Build
import android.os.Build.VERSION
import android.os.Bundle
import android.provider.Settings
import android.util.Log
import androidx.preference.CheckBoxPreference
import androidx.preference.ListPreference
import androidx.preference.Preference
import androidx.preference.PreferenceCategory
import androidx.preference.PreferenceFragmentCompat
import org.isoron.uhabits.HabitsApplication
import org.isoron.uhabits.R
import org.isoron.uhabits.activities.habits.list.RESULT_BUG_REPORT
import org.isoron.uhabits.activities.habits.list.RESULT_EXPORT_CSV
import org.isoron.uhabits.activities.habits.list.RESULT_EXPORT_DB
import org.isoron.uhabits.activities.habits.list.RESULT_IMPORT_DATA
import org.isoron.uhabits.activities.habits.list.RESULT_REPAIR_DB
import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.core.ui.NotificationTray
import org.isoron.uhabits.core.utils.DateUtils.Companion.getLongWeekdayNames
import org.isoron.uhabits.intents.IntentFactory
import org.isoron.uhabits.notifications.AndroidNotificationTray.Companion.createAndroidNotificationChannel
import org.isoron.uhabits.notifications.RingtoneManager
import org.isoron.uhabits.widgets.WidgetUpdater
import java.util.Calendar
class SettingsFragment : PreferenceFragmentCompat(), OnSharedPreferenceChangeListener {
private var sharedPrefs: SharedPreferences? = null
private var ringtoneManager: RingtoneManager? = null
private lateinit var prefs: Preferences
private var widgetUpdater: WidgetUpdater? = null
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == RINGTONE_REQUEST_CODE) {
ringtoneManager!!.update(data)
updateRingtoneDescription()
return
}
super.onActivityResult(requestCode, resultCode, data)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
addPreferencesFromResource(R.xml.preferences)
val appContext = context!!.applicationContext
if (appContext is HabitsApplication) {
prefs = appContext.component.preferences
widgetUpdater = appContext.component.widgetUpdater
}
setResultOnPreferenceClick("importData", RESULT_IMPORT_DATA)
setResultOnPreferenceClick("exportCSV", RESULT_EXPORT_CSV)
setResultOnPreferenceClick("exportDB", RESULT_EXPORT_DB)
setResultOnPreferenceClick("repairDB", RESULT_REPAIR_DB)
setResultOnPreferenceClick("bugReport", RESULT_BUG_REPORT)
}
override fun onCreatePreferences(bundle: Bundle, s: String) {
// NOP
}
override fun onPause() {
sharedPrefs!!.unregisterOnSharedPreferenceChangeListener(this)
super.onPause()
}
override fun onPreferenceTreeClick(preference: Preference): Boolean {
val key = preference.key ?: return false
if (key == "reminderSound") {
showRingtonePicker()
return true
} else if (key == "reminderCustomize") {
if (VERSION.SDK_INT < Build.VERSION_CODES.O) return true
createAndroidNotificationChannel(context!!)
val intent = Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS)
intent.putExtra(Settings.EXTRA_APP_PACKAGE, context!!.packageName)
intent.putExtra(Settings.EXTRA_CHANNEL_ID, NotificationTray.REMINDERS_CHANNEL_ID)
startActivity(intent)
return true
} else if (key == "pref_sync_enabled_dummy") {
if (prefs.isSyncEnabled) {
prefs.disableSync()
} else {
val context: Context? = activity
context!!.startActivity(IntentFactory().startSyncActivity(context))
}
}
return super.onPreferenceTreeClick(preference)
}
override fun onResume() {
super.onResume()
ringtoneManager = RingtoneManager(activity!!)
sharedPrefs = preferenceManager.sharedPreferences
sharedPrefs!!.registerOnSharedPreferenceChangeListener(this)
if (!prefs.isDeveloper) {
val devCategory = findPreference("devCategory") as PreferenceCategory
devCategory.isVisible = false
}
updateWeekdayPreference()
updateSyncPreferences()
// Temporarily disable this; we now always ask
findPreference("reminderSound").isVisible = false
findPreference("pref_snooze_interval").isVisible = false
}
private fun updateSyncPreferences() {
findPreference("pref_sync_display").isVisible = prefs.isSyncEnabled
(findPreference("pref_sync_enabled_dummy") as CheckBoxPreference).isChecked =
prefs.isSyncEnabled
}
private fun updateWeekdayPreference() {
val weekdayPref = findPreference("pref_first_weekday") as ListPreference
val currentFirstWeekday = prefs.firstWeekday.daysSinceSunday + 1
val dayNames = getLongWeekdayNames(Calendar.SATURDAY)
val dayValues = arrayOf("7", "1", "2", "3", "4", "5", "6")
weekdayPref.entries = dayNames
weekdayPref.entryValues = dayValues
weekdayPref.setDefaultValue(currentFirstWeekday.toString())
weekdayPref.summary = dayNames[currentFirstWeekday % 7]
}
override fun onSharedPreferenceChanged(
sharedPreferences: SharedPreferences,
key: String
) {
if (key == "pref_widget_opacity" && widgetUpdater != null) {
Log.d("SettingsFragment", "updating widgets")
widgetUpdater!!.updateWidgets()
}
BackupManager.dataChanged("org.isoron.uhabits")
updateWeekdayPreference()
updateSyncPreferences()
}
private fun setResultOnPreferenceClick(key: String, result: Int) {
val pref = findPreference(key)
pref.onPreferenceClickListener =
Preference.OnPreferenceClickListener {
activity!!.setResult(result)
activity!!.finish()
true
}
}
private fun showRingtonePicker() {
val existingRingtoneUri = ringtoneManager!!.getURI()
val defaultRingtoneUri = Settings.System.DEFAULT_NOTIFICATION_URI
val intent = Intent(android.media.RingtoneManager.ACTION_RINGTONE_PICKER)
intent.putExtra(
android.media.RingtoneManager.EXTRA_RINGTONE_TYPE,
android.media.RingtoneManager.TYPE_NOTIFICATION
)
intent.putExtra(android.media.RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true)
intent.putExtra(android.media.RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, true)
intent.putExtra(
android.media.RingtoneManager.EXTRA_RINGTONE_DEFAULT_URI,
defaultRingtoneUri
)
intent.putExtra(
android.media.RingtoneManager.EXTRA_RINGTONE_EXISTING_URI,
existingRingtoneUri
)
startActivityForResult(intent, RINGTONE_REQUEST_CODE)
}
private fun updateRingtoneDescription() {
val ringtoneName = ringtoneManager!!.getName() ?: return
val ringtonePreference = findPreference("reminderSound")
ringtonePreference.summary = ringtoneName
}
companion object {
private const val RINGTONE_REQUEST_CODE = 1
}
}

@ -68,7 +68,7 @@ class SyncActivity : AppCompatActivity(), SyncBehavior.Screen {
title = resources.getString(R.string.device_sync),
)
binding.syncLink.setOnClickListener { copyToClipboard() }
binding.instructions.setText(Html.fromHtml(resources.getString(R.string.sync_instructions)))
binding.instructions.text = Html.fromHtml(resources.getString(R.string.sync_instructions))
setContentView(binding.root)
}

@ -27,22 +27,22 @@ class AndroidCursor(private val cursor: android.database.Cursor) : Cursor {
override fun moveToNext() = cursor.moveToNext()
override fun getInt(index: Int): Int? {
if (cursor.isNull(index)) return null
else return cursor.getInt(index)
return if (cursor.isNull(index)) null
else cursor.getInt(index)
}
override fun getLong(index: Int): Long? {
if (cursor.isNull(index)) return null
else return cursor.getLong(index)
return if (cursor.isNull(index)) null
else cursor.getLong(index)
}
override fun getDouble(index: Int): Double? {
if (cursor.isNull(index)) return null
else return cursor.getDouble(index)
return if (cursor.isNull(index)) null
else cursor.getDouble(index)
}
override fun getString(index: Int): String? {
if (cursor.isNull(index)) return null
else return cursor.getString(index)
return if (cursor.isNull(index)) null
else cursor.getString(index)
}
}

@ -51,7 +51,7 @@ class AndroidDatabase(
return db.update(tableName, contValues, where, params)
}
override fun insert(tableName: String, values: Map<String, Any?>): Long? {
override fun insert(tableName: String, values: Map<String, Any?>): Long {
val contValues = mapToContentValues(values)
return db.insert(tableName, null, contValues)
}

@ -16,28 +16,14 @@
* 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.inject
package org.isoron.uhabits.inject;
import android.content.Context;
import dagger.Module;
import dagger.Provides;
import android.content.Context
import dagger.Module
import dagger.Provides
@Module
public class AppContextModule
{
private final Context context;
public AppContextModule(@AppContext Context context)
{
this.context = context;
}
@Provides
@AppContext
Context getContext()
{
return context;
}
}
class ActivityContextModule(
@get:Provides
@get:ActivityContext val context: Context
)

@ -16,29 +16,15 @@
* 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.inject
package org.isoron.uhabits.inject;
import android.content.Context;
import dagger.Module;
import dagger.Provides;
import android.content.Context
import dagger.Module
import dagger.Provides
@Module
public class ActivityContextModule
{
private Context context;
public ActivityContextModule(Context context)
{
this.context = context;
}
@Provides
@ActivityContext
public Context getContext()
{
return context;
}
}
class AppContextModule(
@get:Provides
@get:AppContext
@param:AppContext val context: Context
)

@ -32,13 +32,8 @@ import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior
@ActivityScope
@Component(
modules = arrayOf(
ActivityContextModule::class,
HabitsActivityModule::class,
ListHabitsModule::class,
HabitModule::class
),
dependencies = arrayOf(HabitsApplicationComponent::class)
modules = [ActivityContextModule::class, HabitsActivityModule::class, ListHabitsModule::class, HabitModule::class],
dependencies = [HabitsApplicationComponent::class]
)
interface HabitsActivityComponent {
val colorPickerDialogFactory: ColorPickerDialogFactory

@ -1,88 +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.inject;
import android.content.*;
import org.isoron.uhabits.core.*;
import org.isoron.uhabits.core.commands.*;
import org.isoron.uhabits.core.io.*;
import org.isoron.uhabits.core.models.*;
import org.isoron.uhabits.core.preferences.*;
import org.isoron.uhabits.core.reminders.*;
import org.isoron.uhabits.core.sync.*;
import org.isoron.uhabits.core.tasks.*;
import org.isoron.uhabits.core.ui.*;
import org.isoron.uhabits.core.ui.screens.habits.list.*;
import org.isoron.uhabits.core.utils.*;
import org.isoron.uhabits.intents.*;
import org.isoron.uhabits.receivers.*;
import org.isoron.uhabits.tasks.*;
import org.isoron.uhabits.widgets.*;
import dagger.*;
@AppScope
@Component(modules = {
AppContextModule.class,
HabitsModule.class,
AndroidTaskRunner.class,
})
public interface HabitsApplicationComponent
{
CommandRunner getCommandRunner();
@AppContext
Context getContext();
GenericImporter getGenericImporter();
HabitCardListCache getHabitCardListCache();
HabitList getHabitList();
IntentFactory getIntentFactory();
IntentParser getIntentParser();
Logging getLogging();
MidnightTimer getMidnightTimer();
ModelFactory getModelFactory();
NotificationTray getNotificationTray();
PendingIntentFactory getPendingIntentFactory();
Preferences getPreferences();
ReminderScheduler getReminderScheduler();
ReminderController getReminderController();
TaskRunner getTaskRunner();
WidgetPreferences getWidgetPreferences();
WidgetUpdater getWidgetUpdater();
SyncManager getSyncManager();
}

@ -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.inject
import android.content.Context
import dagger.Component
import org.isoron.uhabits.core.AppScope
import org.isoron.uhabits.core.commands.CommandRunner
import org.isoron.uhabits.core.io.GenericImporter
import org.isoron.uhabits.core.io.Logging
import org.isoron.uhabits.core.models.HabitList
import org.isoron.uhabits.core.models.ModelFactory
import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.core.preferences.WidgetPreferences
import org.isoron.uhabits.core.reminders.ReminderScheduler
import org.isoron.uhabits.core.sync.SyncManager
import org.isoron.uhabits.core.tasks.TaskRunner
import org.isoron.uhabits.core.ui.NotificationTray
import org.isoron.uhabits.core.ui.screens.habits.list.HabitCardListCache
import org.isoron.uhabits.core.utils.MidnightTimer
import org.isoron.uhabits.intents.IntentFactory
import org.isoron.uhabits.intents.IntentParser
import org.isoron.uhabits.intents.PendingIntentFactory
import org.isoron.uhabits.receivers.ReminderController
import org.isoron.uhabits.tasks.AndroidTaskRunner
import org.isoron.uhabits.widgets.WidgetUpdater
@AppScope
@Component(modules = [AppContextModule::class, HabitsModule::class, AndroidTaskRunner::class])
interface HabitsApplicationComponent {
val commandRunner: CommandRunner
@get:AppContext
val context: Context
val genericImporter: GenericImporter
val habitCardListCache: HabitCardListCache
val habitList: HabitList
val intentFactory: IntentFactory
val intentParser: IntentParser
val logging: Logging
val midnightTimer: MidnightTimer
val modelFactory: ModelFactory
val notificationTray: NotificationTray
val pendingIntentFactory: PendingIntentFactory
val preferences: Preferences
val reminderScheduler: ReminderScheduler
val reminderController: ReminderController
val taskRunner: TaskRunner
val widgetPreferences: WidgetPreferences
val widgetUpdater: WidgetUpdater
val syncManager: SyncManager
}

@ -44,9 +44,8 @@ class IntentParser
}
private fun parseHabit(uri: Uri): Habit {
val habit = habits.getById(parseId(uri))
return habits.getById(parseId(uri))
?: throw IllegalArgumentException("habit not found")
return habit
}
private fun parseTimestamp(intent: Intent): Timestamp {

@ -36,6 +36,7 @@ import org.isoron.uhabits.core.utils.DateFormats
import org.isoron.uhabits.inject.AppContext
import java.util.Date
import javax.inject.Inject
import kotlin.math.min
@AppScope
class IntentScheduler
@ -84,7 +85,7 @@ class IntentScheduler
}
private fun logReminderScheduled(habit: Habit, reminderTime: Long) {
val min = Math.min(5, habit.name.length)
val min = min(5, habit.name.length)
val name = habit.name.substring(0, min)
val df = DateFormats.getBackupDateFormat()
val time = df.format(Date(reminderTime))

@ -41,7 +41,7 @@ class PendingIntentFactory
) {
fun addCheckmark(habit: Habit, timestamp: Timestamp?): PendingIntent =
PendingIntent.getBroadcast(
getBroadcast(
context,
1,
Intent(context, WidgetReceiver::class.java).apply {
@ -53,7 +53,7 @@ class PendingIntentFactory
)
fun dismissNotification(habit: Habit): PendingIntent =
PendingIntent.getBroadcast(
getBroadcast(
context,
0,
Intent(context, ReminderReceiver::class.java).apply {
@ -64,7 +64,7 @@ class PendingIntentFactory
)
fun removeRepetition(habit: Habit): PendingIntent =
PendingIntent.getBroadcast(
getBroadcast(
context,
3,
Intent(context, WidgetReceiver::class.java).apply {
@ -90,7 +90,7 @@ class PendingIntentFactory
reminderTime: Long?,
timestamp: Long
): PendingIntent =
PendingIntent.getBroadcast(
getBroadcast(
context,
(habit.id!! % Integer.MAX_VALUE).toInt() + 1,
Intent(context, ReminderReceiver::class.java).apply {
@ -103,7 +103,7 @@ class PendingIntentFactory
)
fun snoozeNotification(habit: Habit): PendingIntent =
PendingIntent.getBroadcast(
getBroadcast(
context,
0,
Intent(context, ReminderReceiver::class.java).apply {
@ -114,7 +114,7 @@ class PendingIntentFactory
)
fun toggleCheckmark(habit: Habit, timestamp: Long?): PendingIntent =
PendingIntent.getBroadcast(
getBroadcast(
context,
2,
Intent(context, WidgetReceiver::class.java).apply {
@ -145,7 +145,7 @@ class PendingIntentFactory
)
fun updateWidgets(): PendingIntent =
PendingIntent.getBroadcast(
getBroadcast(
context,
0,
Intent(context, WidgetReceiver::class.java).apply {

@ -40,17 +40,17 @@ class RingtoneManager
PreferenceManager.getDefaultSharedPreferences(context)
fun getName(): String? {
try {
return try {
var ringtoneName = context.resources.getString(R.string.none)
val ringtoneUri = getURI()
if (ringtoneUri != null) {
val ringtone = getRingtone(context, ringtoneUri)
if (ringtone != null) ringtoneName = ringtone.getTitle(context)
}
return ringtoneName
ringtoneName
} catch (e: RuntimeException) {
e.printStackTrace()
return null
null
}
}

@ -80,8 +80,7 @@ class RemoteSyncServer(
try {
val url = "${preferences.syncBaseURL}/db/$key"
Log.i("RemoteSyncServer", "GET $url")
val data: SyncData = httpClient.get(url)
return@IO data
return@IO httpClient.get<SyncData>(url)
} catch (e: ServerResponseException) {
throw ServiceUnavailable()
} catch (e: ClientRequestException) {

@ -1,93 +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.utils;
import android.content.*;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.util.*;
import org.jetbrains.annotations.*;
public class AttributeSetUtils
{
public static final String ISORON_NAMESPACE = "http://isoron.org/android";
@Nullable
public static String getAttribute(@NonNull Context context,
@NonNull AttributeSet attrs,
@NonNull String name,
@Nullable String defaultValue)
{
int resId = attrs.getAttributeResourceValue(ISORON_NAMESPACE, name, 0);
if (resId != 0) return context.getResources().getString(resId);
String value = attrs.getAttributeValue(ISORON_NAMESPACE, name);
if (value != null) return value;
else return defaultValue;
}
public static boolean getBooleanAttribute(@NonNull Context context,
@NonNull AttributeSet attrs,
@NonNull String name,
boolean defaultValue)
{
String boolText = getAttribute(context, attrs, name, null);
if (boolText != null) return Boolean.parseBoolean(boolText);
else return defaultValue;
}
@Contract("_,_,_,!null -> !null")
public static Integer getColorAttribute(@NonNull Context context,
@NonNull AttributeSet attrs,
@NonNull String name,
@Nullable Integer defaultValue)
{
int resId = attrs.getAttributeResourceValue(ISORON_NAMESPACE, name, 0);
if (resId != 0) return context.getResources().getColor(resId);
else return defaultValue;
}
public static float getFloatAttribute(@NonNull Context context,
@NonNull AttributeSet attrs,
@NonNull String name,
float defaultValue)
{
try
{
String number = getAttribute(context, attrs, name, null);
if (number != null) return Float.parseFloat(number);
else return defaultValue;
} catch(NumberFormatException e) {
return defaultValue;
}
}
public static int getIntAttribute(@NonNull Context context,
@NonNull AttributeSet attrs,
@NonNull String name,
int defaultValue)
{
String number = getAttribute(context, attrs, name, null);
if (number != null) return Integer.parseInt(number);
else return defaultValue;
}
}

@ -0,0 +1,87 @@
/*
* 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.utils
import android.content.Context
import android.util.AttributeSet
import org.jetbrains.annotations.Contract
object AttributeSetUtils {
const val ISORON_NAMESPACE = "http://isoron.org/android"
@JvmStatic
fun getAttribute(
context: Context,
attrs: AttributeSet,
name: String,
defaultValue: String?
): String? {
val resId = attrs.getAttributeResourceValue(ISORON_NAMESPACE, name, 0)
if (resId != 0) return context.resources.getString(resId)
val value = attrs.getAttributeValue(ISORON_NAMESPACE, name)
return value ?: defaultValue
}
@JvmStatic
fun getBooleanAttribute(
context: Context,
attrs: AttributeSet,
name: String,
defaultValue: Boolean
): Boolean {
val boolText = getAttribute(context, attrs, name, null)
return if (boolText != null) java.lang.Boolean.parseBoolean(boolText) else defaultValue
}
@JvmStatic
@Contract("_,_,_,!null -> !null")
fun getColorAttribute(
context: Context,
attrs: AttributeSet,
name: String,
defaultValue: Int?
): Int? {
val resId = attrs.getAttributeResourceValue(ISORON_NAMESPACE, name, 0)
return if (resId != 0) context.resources.getColor(resId) else defaultValue
}
@JvmStatic
fun getFloatAttribute(
context: Context,
attrs: AttributeSet,
name: String,
defaultValue: Float
): Float {
return try {
val number = getAttribute(context, attrs, name, null)
number?.toFloat() ?: defaultValue
} catch (e: NumberFormatException) {
defaultValue
}
}
fun getIntAttribute(
context: Context,
attrs: AttributeSet,
name: String,
defaultValue: Int
): Int {
val number = getAttribute(context, attrs, name, null)
return number?.toInt() ?: defaultValue
}
}

@ -1,92 +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.utils;
import android.content.*;
import android.database.sqlite.*;
import android.util.*;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.isoron.uhabits.*;
import org.isoron.uhabits.core.utils.*;
import java.io.*;
import java.text.*;
import static org.isoron.uhabits.core.ConstantsKt.*;
public abstract class DatabaseUtils
{
@Nullable
private static HabitsDatabaseOpener opener = null;
@NonNull
public static File getDatabaseFile(Context context)
{
String databaseFilename = getDatabaseFilename();
String root = context.getFilesDir().getPath();
String format = "%s/../databases/%s";
String filename = String.format(format, root, databaseFilename);
return new File(filename);
}
@NonNull
public static String getDatabaseFilename()
{
String databaseFilename = DATABASE_FILENAME;
if (HabitsApplication.Companion.isTestMode()) databaseFilename = "test.db";
return databaseFilename;
}
@SuppressWarnings("unchecked")
public static void initializeDatabase(Context context)
{
opener = new HabitsDatabaseOpener(context, getDatabaseFilename(),
DATABASE_VERSION);
}
@SuppressWarnings("ResultOfMethodCallIgnored")
public static String saveDatabaseCopy(Context context, File dir)
throws IOException
{
SimpleDateFormat dateFormat = DateFormats.getBackupDateFormat();
String date = dateFormat.format(DateUtils.getLocalTime());
String format = "%s/Loop Habits Backup %s.db";
String filename = String.format(format, dir.getAbsolutePath(), date);
Log.i("DatabaseUtils", "Writing: " + filename);
File db = getDatabaseFile(context);
File dbCopy = new File(filename);
FileUtilsKt.copyTo(db, dbCopy);
return dbCopy.getAbsolutePath();
}
@NonNull
public static SQLiteDatabase openDatabase()
{
if (opener == null) throw new IllegalStateException();
return opener.getWritableDatabase();
}
}

@ -0,0 +1,75 @@
/*
* 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.utils
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.util.Log
import org.isoron.uhabits.HabitsApplication.Companion.isTestMode
import org.isoron.uhabits.HabitsDatabaseOpener
import org.isoron.uhabits.core.DATABASE_FILENAME
import org.isoron.uhabits.core.DATABASE_VERSION
import org.isoron.uhabits.core.utils.DateFormats.Companion.getBackupDateFormat
import org.isoron.uhabits.core.utils.DateUtils.Companion.getLocalTime
import java.io.File
import java.io.IOException
import java.text.SimpleDateFormat
object DatabaseUtils {
private var opener: HabitsDatabaseOpener? = null
@JvmStatic
fun getDatabaseFile(context: Context): File {
val databaseFilename = databaseFilename
val root = context.filesDir.path
return File("$root/../databases/$databaseFilename")
}
private val databaseFilename: String
get() {
var databaseFilename: String = DATABASE_FILENAME
if (isTestMode()) databaseFilename = "test.db"
return databaseFilename
}
fun initializeDatabase(context: Context?) {
opener = HabitsDatabaseOpener(
context!!,
databaseFilename,
DATABASE_VERSION
)
}
@JvmStatic
@Throws(IOException::class)
fun saveDatabaseCopy(context: Context, dir: File): String {
val dateFormat: SimpleDateFormat = getBackupDateFormat()
val date = dateFormat.format(getLocalTime())
val filename = "${dir.absolutePath}/Loop Habits Backup $date.db"
Log.i("DatabaseUtils", "Writing: $filename")
val db = getDatabaseFile(context)
val dbCopy = File(filename)
db.copyTo(dbCopy)
return dbCopy.absolutePath
}
fun openDatabase(): SQLiteDatabase {
checkNotNull(opener)
return opener!!.writableDatabase
}
}

@ -16,30 +16,24 @@
* 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.utils
package org.isoron.uhabits.utils;
import android.app.Activity
import android.app.KeyguardManager
import android.content.Context
import android.os.Build
import android.view.WindowManager
import android.app.*;
import android.content.*;
import android.os.*;
import android.view.*;
object SystemUtils {
val isAndroidOOrLater: Boolean
get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
public class SystemUtils
{
public static boolean isAndroidOOrLater()
{
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
}
public static void unlockScreen(Activity activity)
{
if (isAndroidOOrLater()) {
KeyguardManager km =
(KeyguardManager) activity.getSystemService(Context.KEYGUARD_SERVICE);
km.requestDismissKeyguard(activity, null);
fun unlockScreen(activity: Activity) {
if (isAndroidOOrLater) {
val km = activity.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
km.requestDismissKeyguard(activity, null)
} else {
activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD);
activity.window.addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD)
}
}
}

@ -1,214 +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.app.*;
import android.content.*;
import android.graphics.*;
import android.view.*;
import android.widget.*;
import androidx.annotation.NonNull;
import org.isoron.uhabits.*;
import org.isoron.uhabits.core.commands.*;
import org.isoron.uhabits.core.preferences.*;
import org.isoron.uhabits.intents.*;
import static android.view.View.MeasureSpec.makeMeasureSpec;
public abstract class BaseWidget
{
private final int id;
@NonNull
protected final WidgetPreferences widgetPrefs;
@NonNull
protected final Preferences prefs;
@NonNull
protected final PendingIntentFactory pendingIntentFactory;
@NonNull
private final Context context;
@NonNull
protected final CommandRunner commandRunner;
@NonNull
private WidgetDimensions dimensions;
public BaseWidget(@NonNull Context context, int id)
{
this.id = id;
this.context = context;
HabitsApplication app =
(HabitsApplication) context.getApplicationContext();
widgetPrefs = app.getComponent().getWidgetPreferences();
prefs = app.getComponent().getPreferences();
commandRunner = app.getComponent().getCommandRunner();
pendingIntentFactory = app.getComponent().getPendingIntentFactory();
dimensions = new WidgetDimensions(getDefaultWidth(), getDefaultHeight(),
getDefaultWidth(), getDefaultHeight());
}
public void delete()
{
widgetPrefs.removeWidget(id);
}
@NonNull
public Context getContext()
{
return context;
}
public int getId()
{
return id;
}
@NonNull
public RemoteViews getLandscapeRemoteViews()
{
return getRemoteViews(dimensions.getLandscapeWidth(),
dimensions.getLandscapeHeight());
}
public abstract PendingIntent getOnClickPendingIntent(Context context);
@NonNull
public RemoteViews getPortraitRemoteViews()
{
return getRemoteViews(dimensions.getPortraitWidth(),
dimensions.getPortraitHeight());
}
public abstract void refreshData(View widgetView);
public void setDimensions(@NonNull WidgetDimensions dimensions)
{
this.dimensions = dimensions;
}
protected abstract View buildView();
protected abstract int getDefaultHeight();
protected abstract int getDefaultWidth();
private void adjustRemoteViewsPadding(RemoteViews remoteViews,
View view,
int width,
int height)
{
int imageWidth = view.getMeasuredWidth();
int imageHeight = view.getMeasuredHeight();
int p[] = calculatePadding(width, height, imageWidth, imageHeight);
remoteViews.setViewPadding(R.id.buttonOverlay, p[0], p[1], p[2], p[3]);
}
private void buildRemoteViews(View view,
RemoteViews remoteViews,
int width,
int height)
{
Bitmap bitmap = getBitmapFromView(view);
remoteViews.setImageViewBitmap(R.id.imageView, bitmap);
adjustRemoteViewsPadding(remoteViews, view, width, height);
PendingIntent onClickIntent = getOnClickPendingIntent(context);
if (onClickIntent != null)
remoteViews.setOnClickPendingIntent(R.id.button, onClickIntent);
}
private int[] calculatePadding(int entireWidth,
int entireHeight,
int imageWidth,
int imageHeight)
{
int w = (int) (((float) entireWidth - imageWidth) / 2);
int h = (int) (((float) entireHeight - imageHeight) / 2);
return new int[]{w, h, w, h};
}
@NonNull
private Bitmap getBitmapFromView(View view)
{
view.invalidate();
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
view.draw(canvas);
return bitmap;
}
@NonNull
protected RemoteViews getRemoteViews(int width, int height)
{
View view = buildView();
measureView(view, width, height);
refreshData(view);
if (view.isLayoutRequested()) measureView(view, width, height);
RemoteViews remoteViews =
new RemoteViews(context.getPackageName(), R.layout.widget_wrapper);
buildRemoteViews(view, remoteViews, width, height);
return remoteViews;
}
private void measureView(View view, int width, int height)
{
LayoutInflater inflater = LayoutInflater.from(context);
View entireView = inflater.inflate(R.layout.widget_wrapper, null);
int specWidth = makeMeasureSpec(width, View.MeasureSpec.EXACTLY);
int specHeight = makeMeasureSpec(height, View.MeasureSpec.EXACTLY);
entireView.measure(specWidth, specHeight);
entireView.layout(0, 0, entireView.getMeasuredWidth(),
entireView.getMeasuredHeight());
View imageView = entireView.findViewById(R.id.imageView);
width = imageView.getMeasuredWidth();
height = imageView.getMeasuredHeight();
specWidth = makeMeasureSpec(width, View.MeasureSpec.EXACTLY);
specHeight = makeMeasureSpec(height, View.MeasureSpec.EXACTLY);
view.measure(specWidth, specHeight);
view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight());
}
protected int getPreferedBackgroundAlpha() {
return prefs.getWidgetOpacity();
}
}

@ -0,0 +1,162 @@
/*
* 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.app.PendingIntent
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Canvas
import android.view.LayoutInflater
import android.view.View
import android.view.View.MeasureSpec
import android.widget.RemoteViews
import org.isoron.uhabits.HabitsApplication
import org.isoron.uhabits.R
import org.isoron.uhabits.core.commands.CommandRunner
import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.core.preferences.WidgetPreferences
import org.isoron.uhabits.intents.PendingIntentFactory
abstract class BaseWidget(val context: Context, val id: Int) {
protected val widgetPrefs: WidgetPreferences
protected val prefs: Preferences
protected val pendingIntentFactory: PendingIntentFactory
protected val commandRunner: CommandRunner
private var dimensions: WidgetDimensions
fun delete() {
widgetPrefs.removeWidget(id)
}
val landscapeRemoteViews: RemoteViews
get() = getRemoteViews(
dimensions.landscapeWidth,
dimensions.landscapeHeight
)
abstract fun getOnClickPendingIntent(context: Context): PendingIntent?
val portraitRemoteViews: RemoteViews
get() = getRemoteViews(
dimensions.portraitWidth,
dimensions.portraitHeight
)
abstract fun refreshData(widgetView: View)
fun setDimensions(dimensions: WidgetDimensions) {
this.dimensions = dimensions
}
protected abstract fun buildView(): View?
protected abstract val defaultHeight: Int
protected abstract val defaultWidth: Int
private fun adjustRemoteViewsPadding(
remoteViews: RemoteViews,
view: View,
width: Int,
height: Int
) {
val imageWidth = view.measuredWidth
val imageHeight = view.measuredHeight
val p = calculatePadding(width, height, imageWidth, imageHeight)
remoteViews.setViewPadding(R.id.buttonOverlay, p[0], p[1], p[2], p[3])
}
private fun buildRemoteViews(
view: View,
remoteViews: RemoteViews,
width: Int,
height: Int
) {
val bitmap = getBitmapFromView(view)
remoteViews.setImageViewBitmap(R.id.imageView, bitmap)
adjustRemoteViewsPadding(remoteViews, view, width, height)
val onClickIntent = getOnClickPendingIntent(context)
if (onClickIntent != null) remoteViews.setOnClickPendingIntent(R.id.button, onClickIntent)
}
private fun calculatePadding(
entireWidth: Int,
entireHeight: Int,
imageWidth: Int,
imageHeight: Int
): IntArray {
val w = ((entireWidth.toFloat() - imageWidth) / 2).toInt()
val h = ((entireHeight.toFloat() - imageHeight) / 2).toInt()
return intArrayOf(w, h, w, h)
}
private fun getBitmapFromView(view: View): Bitmap {
view.invalidate()
val width = view.measuredWidth
val height = view.measuredHeight
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
view.draw(canvas)
return bitmap
}
protected open fun getRemoteViews(width: Int, height: Int): RemoteViews {
val view = buildView()!!
measureView(view, width, height)
refreshData(view)
if (view.isLayoutRequested) measureView(view, width, height)
val remoteViews = RemoteViews(context.packageName, R.layout.widget_wrapper)
buildRemoteViews(view, remoteViews, width, height)
return remoteViews
}
private fun measureView(view: View, width: Int, height: Int) {
var width = width
var height = height
val inflater = LayoutInflater.from(context)
val entireView = inflater.inflate(R.layout.widget_wrapper, null)
var specWidth = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY)
var specHeight = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
entireView.measure(specWidth, specHeight)
entireView.layout(
0,
0,
entireView.measuredWidth,
entireView.measuredHeight
)
val imageView = entireView.findViewById<View>(R.id.imageView)
width = imageView.measuredWidth
height = imageView.measuredHeight
specWidth = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY)
specHeight = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
view.measure(specWidth, specHeight)
view.layout(0, 0, view.measuredWidth, view.measuredHeight)
}
protected val preferedBackgroundAlpha: Int
protected get() = prefs.widgetOpacity
init {
val app = context.applicationContext as HabitsApplication
widgetPrefs = app.component.widgetPreferences
prefs = app.component.preferences
commandRunner = app.component.commandRunner
pendingIntentFactory = app.component.pendingIntentFactory
dimensions = WidgetDimensions(
defaultWidth,
defaultHeight,
defaultWidth,
defaultHeight
)
}
}

@ -1,206 +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.appwidget.*;
import android.content.*;
import android.os.*;
import android.widget.*;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.isoron.uhabits.*;
import org.isoron.uhabits.core.models.*;
import org.isoron.uhabits.core.preferences.*;
import java.util.*;
import static android.appwidget.AppWidgetManager.*;
import static org.isoron.uhabits.utils.InterfaceUtils.dpToPixels;
public abstract class BaseWidgetProvider extends AppWidgetProvider
{
private HabitList habits;
private Preferences preferences;
private WidgetPreferences widgetPrefs;
public static void updateAppWidget(@NonNull AppWidgetManager manager,
@NonNull BaseWidget widget)
{
RemoteViews landscape = widget.getLandscapeRemoteViews();
RemoteViews portrait = widget.getPortraitRemoteViews();
RemoteViews views = new RemoteViews(landscape, portrait);
manager.updateAppWidget(widget.getId(), views);
}
@NonNull
public WidgetDimensions getDimensionsFromOptions(@NonNull Context ctx,
@NonNull Bundle options)
{
int maxWidth =
(int) dpToPixels(ctx, options.getInt(OPTION_APPWIDGET_MAX_WIDTH));
int maxHeight =
(int) dpToPixels(ctx, options.getInt(OPTION_APPWIDGET_MAX_HEIGHT));
int minWidth =
(int) dpToPixels(ctx, options.getInt(OPTION_APPWIDGET_MIN_WIDTH));
int minHeight =
(int) dpToPixels(ctx, options.getInt(OPTION_APPWIDGET_MIN_HEIGHT));
return new WidgetDimensions(minWidth, maxHeight, maxWidth, minHeight);
}
@Override
public void onAppWidgetOptionsChanged(@Nullable Context context,
@Nullable AppWidgetManager manager,
int widgetId,
@Nullable Bundle options)
{
try
{
if (context == null) throw new RuntimeException("context is null");
if (manager == null) throw new RuntimeException("manager is null");
if (options == null) throw new RuntimeException("options is null");
updateDependencies(context);
context.setTheme(R.style.WidgetTheme);
BaseWidget widget = getWidgetFromId(context, widgetId);
WidgetDimensions dims = getDimensionsFromOptions(context, options);
widget.setDimensions(dims);
updateAppWidget(manager, widget);
}
catch (RuntimeException e)
{
drawErrorWidget(context, manager, widgetId, e);
e.printStackTrace();
}
}
@Override
public void onDeleted(@Nullable Context context, @Nullable int[] ids)
{
if (context == null) throw new RuntimeException("context is null");
if (ids == null) throw new RuntimeException("ids is null");
updateDependencies(context);
for (int id : ids)
{
try
{
BaseWidget widget = getWidgetFromId(context, id);
widget.delete();
}
catch (HabitNotFoundException e)
{
e.printStackTrace();
}
}
}
@Override
public void onUpdate(@Nullable Context context,
@Nullable AppWidgetManager manager,
@Nullable int[] widgetIds)
{
if (context == null) throw new RuntimeException("context is null");
if (manager == null) throw new RuntimeException("manager is null");
if (widgetIds == null) throw new RuntimeException("widgetIds is null");
updateDependencies(context);
context.setTheme(R.style.WidgetTheme);
new Thread(() ->
{
Looper.prepare();
for (int id : widgetIds)
update(context, manager, id);
}).start();
}
protected List<Habit> getHabitsFromWidgetId(int widgetId)
{
long selectedIds[] = widgetPrefs.getHabitIdsFromWidgetId(widgetId);
ArrayList<Habit> selectedHabits = new ArrayList<>(selectedIds.length);
for (long id : selectedIds)
{
Habit h = habits.getById(id);
if (h == null) throw new HabitNotFoundException();
selectedHabits.add(h);
}
return selectedHabits;
}
@NonNull
protected abstract BaseWidget getWidgetFromId(@NonNull Context context,
int id);
private void drawErrorWidget(Context context,
AppWidgetManager manager,
int widgetId,
RuntimeException e)
{
RemoteViews errorView =
new RemoteViews(context.getPackageName(), R.layout.widget_error);
if (e instanceof HabitNotFoundException)
{
errorView.setCharSequence(R.id.label, "setText",
context.getString(R.string.habit_not_found));
}
manager.updateAppWidget(widgetId, errorView);
}
private void update(@NonNull Context context,
@NonNull AppWidgetManager manager,
int widgetId)
{
try
{
BaseWidget widget = getWidgetFromId(context, widgetId);
Bundle options = manager.getAppWidgetOptions(widgetId);
widget.setDimensions(getDimensionsFromOptions(context, options));
updateAppWidget(manager, widget);
}
catch (RuntimeException e)
{
drawErrorWidget(context, manager, widgetId, e);
e.printStackTrace();
}
}
private void updateDependencies(Context context)
{
HabitsApplication app =
(HabitsApplication) context.getApplicationContext();
habits = app.getComponent().getHabitList();
preferences = app.getComponent().getPreferences();
widgetPrefs = app.getComponent().getWidgetPreferences();
}
public Preferences getPreferences()
{
return preferences;
}
}

@ -0,0 +1,177 @@
/*
* 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.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.Context
import android.os.Bundle
import android.os.Looper
import android.widget.RemoteViews
import org.isoron.uhabits.HabitsApplication
import org.isoron.uhabits.R
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.HabitList
import org.isoron.uhabits.core.models.HabitNotFoundException
import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.core.preferences.WidgetPreferences
import org.isoron.uhabits.utils.InterfaceUtils.dpToPixels
import java.util.ArrayList
abstract class BaseWidgetProvider : AppWidgetProvider() {
private lateinit var habits: HabitList
lateinit var preferences: Preferences
private set
private lateinit var widgetPrefs: WidgetPreferences
fun getDimensionsFromOptions(
ctx: Context,
options: Bundle
): WidgetDimensions {
val maxWidth = dpToPixels(
ctx,
options.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH).toFloat()
).toInt()
val maxHeight = dpToPixels(
ctx,
options.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT).toFloat()
).toInt()
val minWidth = dpToPixels(
ctx,
options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH).toFloat()
).toInt()
val minHeight = dpToPixels(
ctx,
options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT).toFloat()
).toInt()
return WidgetDimensions(minWidth, maxHeight, maxWidth, minHeight)
}
override fun onAppWidgetOptionsChanged(
context: Context,
manager: AppWidgetManager,
widgetId: Int,
options: Bundle
) {
try {
updateDependencies(context)
context.setTheme(R.style.WidgetTheme)
val widget = getWidgetFromId(context, widgetId)
val dims = getDimensionsFromOptions(context, options)
widget.setDimensions(dims)
updateAppWidget(manager, widget)
} catch (e: RuntimeException) {
drawErrorWidget(context, manager, widgetId, e)
e.printStackTrace()
}
}
override fun onDeleted(context: Context?, ids: IntArray?) {
if (context == null) throw RuntimeException("context is null")
if (ids == null) throw RuntimeException("ids is null")
updateDependencies(context)
for (id in ids) {
try {
val widget = getWidgetFromId(context, id)
widget.delete()
} catch (e: HabitNotFoundException) {
e.printStackTrace()
}
}
}
override fun onUpdate(
context: Context,
manager: AppWidgetManager,
widgetIds: IntArray
) {
updateDependencies(context)
context.setTheme(R.style.WidgetTheme)
Thread {
Looper.prepare()
for (id in widgetIds) update(context, manager, id)
}.start()
}
protected fun getHabitsFromWidgetId(widgetId: Int): List<Habit> {
val selectedIds = widgetPrefs.getHabitIdsFromWidgetId(widgetId)
val selectedHabits = ArrayList<Habit>(selectedIds.size)
for (id in selectedIds) {
val h = habits.getById(id) ?: throw HabitNotFoundException()
selectedHabits.add(h)
}
return selectedHabits
}
protected abstract fun getWidgetFromId(
context: Context,
id: Int
): BaseWidget
private fun drawErrorWidget(
context: Context,
manager: AppWidgetManager,
widgetId: Int,
e: RuntimeException
) {
val errorView = RemoteViews(context.packageName, R.layout.widget_error)
if (e is HabitNotFoundException) {
errorView.setCharSequence(
R.id.label,
"setText",
context.getString(R.string.habit_not_found)
)
}
manager.updateAppWidget(widgetId, errorView)
}
private fun update(
context: Context,
manager: AppWidgetManager,
widgetId: Int
) {
try {
val widget = getWidgetFromId(context, widgetId)
val options = manager.getAppWidgetOptions(widgetId)
widget.setDimensions(getDimensionsFromOptions(context, options))
updateAppWidget(manager, widget)
} catch (e: RuntimeException) {
drawErrorWidget(context, manager, widgetId, e)
e.printStackTrace()
}
}
private fun updateDependencies(context: Context) {
val app = context.applicationContext as HabitsApplication
habits = app.component.habitList
preferences = app.component.preferences
widgetPrefs = app.component.widgetPreferences
}
companion object {
fun updateAppWidget(
manager: AppWidgetManager,
widget: BaseWidget
) {
val landscape = widget.landscapeRemoteViews
val portrait = widget.portraitRemoteViews
val views = RemoteViews(landscape, portrait)
manager.updateAppWidget(widget.id, views)
}
}
}

@ -21,7 +21,9 @@ package org.isoron.uhabits.widgets
import android.app.PendingIntent
import android.content.Context
import android.os.Build
import android.view.View
import androidx.annotation.RequiresApi
import org.isoron.uhabits.core.models.Entry
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.utils.DateUtils
@ -31,10 +33,13 @@ import org.isoron.uhabits.widgets.views.CheckmarkWidgetView
open class CheckmarkWidget(
context: Context,
widgetId: Int,
protected val habit: Habit
protected val habit: Habit,
) : BaseWidget(context, widgetId) {
override fun getOnClickPendingIntent(context: Context): PendingIntent {
override val defaultHeight: Int = 125
override val defaultWidth: Int = 125
override fun getOnClickPendingIntent(context: Context): PendingIntent? {
return if (habit.isNumerical) {
pendingIntentFactory.setNumericalValue(context, habit, 10, null)
} else {
@ -42,20 +47,21 @@ open class CheckmarkWidget(
}
}
override fun refreshData(v: View) {
(v as CheckmarkWidgetView).apply {
@RequiresApi(Build.VERSION_CODES.O)
override fun refreshData(widgetView: View) {
(widgetView as CheckmarkWidgetView).apply {
val today = DateUtils.getTodayWithOffset()
setBackgroundAlpha(preferedBackgroundAlpha)
setActiveColor(habit.color.toThemedAndroidColor(context))
setName(habit.name)
setEntryValue(habit.computedEntries.get(today).value)
activeColor = habit.color.toThemedAndroidColor(context)
name = habit.name
entryValue = habit.computedEntries.get(today).value
if (habit.isNumerical) {
setNumerical(true)
setEntryState(getNumericalEntryState())
isNumerical = true
entryState = getNumericalEntryState()
} else {
setEntryState(habit.computedEntries.get(today).value)
entryState = habit.computedEntries.get(today).value
}
setPercentage(habit.scores.get(today).value.toFloat())
percentage = habit.scores[today].value.toFloat()
refresh()
}
}
@ -64,9 +70,6 @@ open class CheckmarkWidget(
return CheckmarkWidgetView(context)
}
override fun getDefaultHeight() = 125
override fun getDefaultWidth() = 125
private fun getNumericalEntryState(): Int {
return if (habit.isCompletedToday()) {
Entry.YES_MANUAL

@ -23,7 +23,7 @@ import android.content.Context
class CheckmarkWidgetProvider : BaseWidgetProvider() {
override fun getWidgetFromId(context: Context, id: Int): BaseWidget {
val habits = getHabitsFromWidgetId(id)
if (habits.size == 1) return CheckmarkWidget(context, id, habits[0])
else return StackWidget(context, id, StackWidgetType.CHECKMARK, habits)
return if (habits.size == 1) CheckmarkWidget(context, id, habits[0])
else StackWidget(context, id, StackWidgetType.CHECKMARK, habits)
}
}

@ -19,18 +19,19 @@
package org.isoron.uhabits.widgets
import android.app.PendingIntent
import android.content.Context
import android.view.View
import org.isoron.uhabits.widgets.views.EmptyWidgetView
class EmptyWidget(
context: Context,
widgetId: Int
widgetId: Int,
) : BaseWidget(context, widgetId) {
override val defaultHeight: Int = 200
override val defaultWidth: Int = 200
override fun getOnClickPendingIntent(context: Context) = null
override fun getOnClickPendingIntent(context: Context): PendingIntent? = null
override fun refreshData(v: View) {}
override fun buildView() = EmptyWidgetView(context)
override fun getDefaultHeight() = 200
override fun getDefaultWidth() = 200
}

@ -19,6 +19,7 @@
package org.isoron.uhabits.widgets
import android.app.PendingIntent
import android.content.Context
import android.view.View
import org.isoron.uhabits.activities.common.views.FrequencyChart
@ -30,10 +31,12 @@ class FrequencyWidget(
context: Context,
widgetId: Int,
private val habit: Habit,
private val firstWeekday: Int
private val firstWeekday: Int,
) : BaseWidget(context, widgetId) {
override val defaultHeight: Int = 200
override val defaultWidth: Int = 200
override fun getOnClickPendingIntent(context: Context) =
override fun getOnClickPendingIntent(context: Context): PendingIntent =
pendingIntentFactory.showHabit(habit)
override fun refreshData(v: View) {
@ -50,7 +53,4 @@ class FrequencyWidget(
override fun buildView() =
GraphWidgetView(context, FrequencyChart(context))
override fun getDefaultHeight() = 200
override fun getDefaultWidth() = 200
}

@ -24,12 +24,12 @@ import android.content.Context
class FrequencyWidgetProvider : BaseWidgetProvider() {
override fun getWidgetFromId(context: Context, id: Int): BaseWidget {
val habits = getHabitsFromWidgetId(id)
if (habits.size == 1) return FrequencyWidget(
return if (habits.size == 1) FrequencyWidget(
context,
id,
habits[0],
preferences.firstWeekdayInt
)
else return StackWidget(context, id, StackWidgetType.FREQUENCY, habits)
else StackWidget(context, id, StackWidgetType.FREQUENCY, habits)
}
}

@ -35,9 +35,12 @@ import java.util.Locale
class HistoryWidget(
context: Context,
id: Int,
private val habit: Habit
private val habit: Habit,
) : BaseWidget(context, id) {
override val defaultHeight: Int = 250
override val defaultWidth: Int = 250
override fun getOnClickPendingIntent(context: Context): PendingIntent {
return pendingIntentFactory.showHabit(habit)
}
@ -72,7 +75,4 @@ class HistoryWidget(
).apply {
setTitle(habit.name)
}
override fun getDefaultHeight() = 250
override fun getDefaultWidth() = 250
}

@ -23,11 +23,11 @@ import android.content.Context
class HistoryWidgetProvider : BaseWidgetProvider() {
override fun getWidgetFromId(context: Context, id: Int): BaseWidget {
val habits = getHabitsFromWidgetId(id)
if (habits.size == 1) return HistoryWidget(
return if (habits.size == 1) HistoryWidget(
context,
id,
habits[0]
)
else return StackWidget(context, id, StackWidgetType.HISTORY, habits)
else StackWidget(context, id, StackWidgetType.HISTORY, habits)
}
}

@ -19,6 +19,7 @@
package org.isoron.uhabits.widgets
import android.app.PendingIntent
import android.content.Context
import android.view.View
import org.isoron.uhabits.activities.common.views.ScoreChart
@ -30,10 +31,12 @@ import org.isoron.uhabits.widgets.views.GraphWidgetView
class ScoreWidget(
context: Context,
id: Int,
private val habit: Habit
private val habit: Habit,
) : BaseWidget(context, id) {
override val defaultHeight: Int = 300
override val defaultWidth: Int = 300
override fun getOnClickPendingIntent(context: Context) =
override fun getOnClickPendingIntent(context: Context): PendingIntent =
pendingIntentFactory.showHabit(habit)
override fun refreshData(view: View) {
@ -57,7 +60,4 @@ class ScoreWidget(
GraphWidgetView(context, ScoreChart(context)).apply {
setTitle(habit.name)
}
override fun getDefaultHeight() = 300
override fun getDefaultWidth() = 300
}

@ -23,7 +23,7 @@ import android.content.Context
class ScoreWidgetProvider : BaseWidgetProvider() {
override fun getWidgetFromId(context: Context, id: Int): BaseWidget {
val habits = getHabitsFromWidgetId(id)
if (habits.size == 1) return ScoreWidget(context, id, habits[0])
else return StackWidget(context, id, StackWidgetType.SCORE, habits)
return if (habits.size == 1) ScoreWidget(context, id, habits[0])
else StackWidget(context, id, StackWidgetType.SCORE, habits)
}
}

@ -19,6 +19,7 @@
package org.isoron.uhabits.widgets
import android.app.PendingIntent
import android.appwidget.AppWidgetManager
import android.content.Context
import android.content.Intent
@ -32,15 +33,22 @@ class StackWidget(
context: Context,
widgetId: Int,
private val widgetType: StackWidgetType,
private val habits: List<Habit>
private val habits: List<Habit>,
) : BaseWidget(context, widgetId) {
override val defaultHeight: Int = 0
override val defaultWidth: Int = 0
override fun getOnClickPendingIntent(context: Context) = null
override fun getOnClickPendingIntent(context: Context): PendingIntent? = null
override fun refreshData(v: View) {
// unused
}
override fun buildView(): View? {
// unused
return null
}
override fun getRemoteViews(width: Int, height: Int): RemoteViews {
val manager = AppWidgetManager.getInstance(context)
val remoteViews = RemoteViews(context.packageName, StackWidgetType.getStackWidgetLayoutId(widgetType))
@ -59,8 +67,4 @@ class StackWidget(
)
return remoteViews
}
override fun buildView() = null // unused
override fun getDefaultHeight() = 0 // unused
override fun getDefaultWidth() = 0 // unused
}

@ -1,186 +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.appwidget.*;
import android.content.*;
import android.os.*;
import android.util.Log;
import android.widget.*;
import androidx.annotation.NonNull;
import org.isoron.uhabits.*;
import org.isoron.uhabits.core.models.*;
import org.isoron.uhabits.core.preferences.*;
import org.isoron.uhabits.core.utils.*;
import java.util.*;
import static android.appwidget.AppWidgetManager.*;
import static org.isoron.uhabits.utils.InterfaceUtils.dpToPixels;
import static org.isoron.uhabits.widgets.StackWidgetService.*;
public class StackWidgetService extends RemoteViewsService
{
public static final String WIDGET_TYPE = "WIDGET_TYPE";
public static final String HABIT_IDS = "HABIT_IDS";
@Override
public RemoteViewsFactory onGetViewFactory(Intent intent)
{
return new StackRemoteViewsFactory(this.getApplicationContext(), intent);
}
}
class StackRemoteViewsFactory implements RemoteViewsService.RemoteViewsFactory
{
private Context context;
private int widgetId;
private long[] habitIds;
private StackWidgetType widgetType;
private ArrayList<RemoteViews> remoteViews = new ArrayList<>();
public StackRemoteViewsFactory(Context context, Intent intent)
{
this.context = context;
widgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
AppWidgetManager.INVALID_APPWIDGET_ID);
int widgetTypeValue = intent.getIntExtra(WIDGET_TYPE, -1);
String habitIdsStr = intent.getStringExtra(HABIT_IDS);
if (widgetTypeValue < 0) throw new RuntimeException("invalid widget type");
if (habitIdsStr == null) throw new RuntimeException("habitIdsStr is null");
widgetType = StackWidgetType.getWidgetTypeFromValue(widgetTypeValue);
habitIds = StringUtils.splitLongs(habitIdsStr);
}
public void onCreate()
{
}
public void onDestroy()
{
}
public int getCount()
{
return habitIds.length;
}
@NonNull
public WidgetDimensions getDimensionsFromOptions(@NonNull Context ctx,
@NonNull Bundle options)
{
int maxWidth = (int) dpToPixels(ctx, options.getInt(OPTION_APPWIDGET_MAX_WIDTH));
int maxHeight = (int) dpToPixels(ctx, options.getInt(OPTION_APPWIDGET_MAX_HEIGHT));
int minWidth = (int) dpToPixels(ctx, options.getInt(OPTION_APPWIDGET_MIN_WIDTH));
int minHeight = (int) dpToPixels(ctx, options.getInt(OPTION_APPWIDGET_MIN_HEIGHT));
return new WidgetDimensions(minWidth, maxHeight, maxWidth, minHeight);
}
public RemoteViews getViewAt(int position)
{
Log.i("StackRemoteViewsFactory", "getViewAt " + position);
if (position < 0 || position > remoteViews.size()) return null;
return remoteViews.get(position);
}
@NonNull
private BaseWidget constructWidget(@NonNull Habit habit,
@NonNull Preferences prefs)
{
switch (widgetType)
{
case CHECKMARK:
return new CheckmarkWidget(context, widgetId, habit);
case FREQUENCY:
return new FrequencyWidget(context, widgetId, habit, prefs.getFirstWeekdayInt());
case SCORE:
return new ScoreWidget(context, widgetId, habit);
case HISTORY:
return new HistoryWidget(context, widgetId, habit);
case STREAKS:
return new StreakWidget(context, widgetId, habit);
}
throw new IllegalStateException();
}
public RemoteViews getLoadingView()
{
Bundle options = AppWidgetManager.getInstance(context).getAppWidgetOptions(widgetId);
EmptyWidget widget = new EmptyWidget(context, widgetId);
widget.setDimensions(getDimensionsFromOptions(context, options));
RemoteViews landscapeViews = widget.getLandscapeRemoteViews();
RemoteViews portraitViews = widget.getPortraitRemoteViews();
return new RemoteViews(landscapeViews, portraitViews);
}
public int getViewTypeCount()
{
return 1;
}
public long getItemId(int position)
{
return habitIds[position];
}
public boolean hasStableIds()
{
return true;
}
public void onDataSetChanged()
{
Log.i("StackRemoteViewsFactory", "onDataSetChanged started");
HabitsApplication app = (HabitsApplication) context.getApplicationContext();
Preferences prefs = app.getComponent().getPreferences();
HabitList habitList = app.getComponent().getHabitList();
Bundle options = AppWidgetManager.getInstance(context).getAppWidgetOptions(widgetId);
ArrayList<RemoteViews> newRemoteViews = new ArrayList<>();
if (Looper.myLooper() == null) Looper.prepare();
for (long id : habitIds)
{
Habit h = habitList.getById(id);
if (h == null) throw new HabitNotFoundException();
BaseWidget widget = constructWidget(h, prefs);
widget.setDimensions(getDimensionsFromOptions(context, options));
RemoteViews landscapeViews = widget.getLandscapeRemoteViews();
RemoteViews portraitViews = widget.getPortraitRemoteViews();
newRemoteViews.add(new RemoteViews(landscapeViews, portraitViews));
Log.i("StackRemoteViewsFactory", "onDataSetChanged constructed widget " + id);
}
remoteViews = newRemoteViews;
Log.i("StackRemoteViewsFactory", "onDataSetChanged ended");
}
}

@ -0,0 +1,161 @@
/*
* 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.appwidget.AppWidgetManager
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.os.Looper
import android.util.Log
import android.widget.RemoteViews
import android.widget.RemoteViewsService
import android.widget.RemoteViewsService.RemoteViewsFactory
import org.isoron.uhabits.HabitsApplication
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.HabitNotFoundException
import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.core.utils.StringUtils.Companion.splitLongs
import org.isoron.uhabits.utils.InterfaceUtils.dpToPixels
import java.util.ArrayList
class StackWidgetService : RemoteViewsService() {
override fun onGetViewFactory(intent: Intent): RemoteViewsFactory {
return StackRemoteViewsFactory(this.applicationContext, intent)
}
companion object {
const val WIDGET_TYPE = "WIDGET_TYPE"
const val HABIT_IDS = "HABIT_IDS"
}
}
internal class StackRemoteViewsFactory(private val context: Context, intent: Intent) :
RemoteViewsFactory {
private val widgetId: Int = intent.getIntExtra(
AppWidgetManager.EXTRA_APPWIDGET_ID,
AppWidgetManager.INVALID_APPWIDGET_ID
)
private val habitIds: LongArray
private val widgetType: StackWidgetType?
private var remoteViews = ArrayList<RemoteViews>()
override fun onCreate() {}
override fun onDestroy() {}
override fun getCount(): Int {
return habitIds.size
}
fun getDimensionsFromOptions(
ctx: Context,
options: Bundle
): WidgetDimensions {
val maxWidth = dpToPixels(
ctx,
options.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH).toFloat()
).toInt()
val maxHeight = dpToPixels(
ctx,
options.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT).toFloat()
).toInt()
val minWidth = dpToPixels(
ctx,
options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH).toFloat()
).toInt()
val minHeight = dpToPixels(
ctx,
options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT).toFloat()
).toInt()
return WidgetDimensions(minWidth, maxHeight, maxWidth, minHeight)
}
override fun getViewAt(position: Int): RemoteViews? {
Log.i("StackRemoteViewsFactory", "getViewAt $position")
return if (position < 0 || position > remoteViews.size) null else remoteViews[position]
}
private fun constructWidget(
habit: Habit,
prefs: Preferences
): BaseWidget {
when (widgetType) {
StackWidgetType.CHECKMARK -> return CheckmarkWidget(context, widgetId, habit)
StackWidgetType.FREQUENCY -> return FrequencyWidget(
context,
widgetId,
habit,
prefs.firstWeekdayInt
)
StackWidgetType.SCORE -> return ScoreWidget(context, widgetId, habit)
StackWidgetType.HISTORY -> return HistoryWidget(context, widgetId, habit)
StackWidgetType.STREAKS -> return StreakWidget(context, widgetId, habit)
}
throw IllegalStateException()
}
override fun getLoadingView(): RemoteViews {
val options = AppWidgetManager.getInstance(context).getAppWidgetOptions(widgetId)
val widget = EmptyWidget(context, widgetId)
widget.setDimensions(getDimensionsFromOptions(context, options))
val landscapeViews = widget.landscapeRemoteViews
val portraitViews = widget.portraitRemoteViews
return RemoteViews(landscapeViews, portraitViews)
}
override fun getViewTypeCount(): Int {
return 1
}
override fun getItemId(position: Int): Long {
return habitIds[position]
}
override fun hasStableIds(): Boolean {
return true
}
override fun onDataSetChanged() {
Log.i("StackRemoteViewsFactory", "onDataSetChanged started")
val app = context.applicationContext as HabitsApplication
val prefs = app.component.preferences
val habitList = app.component.habitList
val options = AppWidgetManager.getInstance(context).getAppWidgetOptions(widgetId)
val newRemoteViews = ArrayList<RemoteViews>()
if (Looper.myLooper() == null) Looper.prepare()
for (id in habitIds) {
val h = habitList.getById(id) ?: throw HabitNotFoundException()
val widget = constructWidget(h, prefs)
widget.setDimensions(getDimensionsFromOptions(context, options))
val landscapeViews = widget.landscapeRemoteViews
val portraitViews = widget.portraitRemoteViews
newRemoteViews.add(RemoteViews(landscapeViews, portraitViews))
Log.i("StackRemoteViewsFactory", "onDataSetChanged constructed widget $id")
}
remoteViews = newRemoteViews
Log.i("StackRemoteViewsFactory", "onDataSetChanged ended")
}
init {
val widgetTypeValue = intent.getIntExtra(StackWidgetService.WIDGET_TYPE, -1)
val habitIdsStr = intent.getStringExtra(StackWidgetService.HABIT_IDS)
if (widgetTypeValue < 0) throw RuntimeException("invalid widget type")
if (habitIdsStr == null) throw RuntimeException("habitIdsStr is null")
widgetType = StackWidgetType.getWidgetTypeFromValue(widgetTypeValue)
habitIds = splitLongs(habitIdsStr)
}
}

@ -1,117 +0,0 @@
/*
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.widgets;
import org.isoron.uhabits.R;
/**
* Created by victoryu on 11/3/17.
*/
public enum StackWidgetType {
CHECKMARK(0),
FREQUENCY(1),
SCORE(2), // habit strength widget
HISTORY(3),
STREAKS(4),
TARGET(5);
private int value;
StackWidgetType(int value) {
this.value = value;
}
public int getValue() {
return value;
}
public static StackWidgetType getWidgetTypeFromValue(int value) {
if (CHECKMARK.getValue() == value) {
return CHECKMARK;
} else if (FREQUENCY.getValue() == value) {
return FREQUENCY;
} else if (SCORE.getValue() == value) {
return SCORE;
} else if (HISTORY.getValue() == value) {
return HISTORY;
} else if (STREAKS.getValue() == value) {
return STREAKS;
} else if (TARGET.getValue() == value) {
return TARGET;
}
return null;
}
public static int getStackWidgetLayoutId(StackWidgetType type) {
switch (type) {
case CHECKMARK:
return R.layout.checkmark_stackview_widget;
case FREQUENCY:
return R.layout.frequency_stackview_widget;
case SCORE:
return R.layout.score_stackview_widget;
case HISTORY:
return R.layout.history_stackview_widget;
case STREAKS:
return R.layout.streak_stackview_widget;
case TARGET:
return R.layout.target_stackview_widget;
}
return 0;
}
public static int getStackWidgetAdapterViewId(StackWidgetType type) {
switch (type) {
case CHECKMARK:
return R.id.checkmarkStackWidgetView;
case FREQUENCY:
return R.id.frequencyStackWidgetView;
case SCORE:
return R.id.scoreStackWidgetView;
case HISTORY:
return R.id.historyStackWidgetView;
case STREAKS:
return R.id.streakStackWidgetView;
case TARGET:
return R.id.targetStackWidgetView;
}
return 0;
}
public static int getStackWidgetEmptyViewId(StackWidgetType type) {
switch (type) {
case CHECKMARK:
return R.id.checkmarkStackWidgetEmptyView;
case FREQUENCY:
return R.id.frequencyStackWidgetEmptyView;
case SCORE:
return R.id.scoreStackWidgetEmptyView;
case HISTORY:
return R.id.historyStackWidgetEmptyView;
case STREAKS:
return R.id.streakStackWidgetEmptyView;
case TARGET:
return R.id.targetStackWidgetEmptyView;
}
return 0;
}
}

@ -0,0 +1,79 @@
/*
* 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 org.isoron.uhabits.R
/**
* Created by victoryu on 11/3/17.
*/
enum class StackWidgetType(val value: Int) {
CHECKMARK(0), FREQUENCY(1), SCORE(2), // habit strength widget
HISTORY(3), STREAKS(4), TARGET(5);
companion object {
fun getWidgetTypeFromValue(value: Int): StackWidgetType? {
return when {
CHECKMARK.value == value -> CHECKMARK
FREQUENCY.value == value -> FREQUENCY
SCORE.value == value -> SCORE
HISTORY.value == value -> HISTORY
STREAKS.value == value -> STREAKS
TARGET.value == value -> TARGET
else -> null
}
}
fun getStackWidgetLayoutId(type: StackWidgetType?): Int {
when (type) {
CHECKMARK -> return R.layout.checkmark_stackview_widget
FREQUENCY -> return R.layout.frequency_stackview_widget
SCORE -> return R.layout.score_stackview_widget
HISTORY -> return R.layout.history_stackview_widget
STREAKS -> return R.layout.streak_stackview_widget
TARGET -> return R.layout.target_stackview_widget
}
return 0
}
fun getStackWidgetAdapterViewId(type: StackWidgetType?): Int {
when (type) {
CHECKMARK -> return R.id.checkmarkStackWidgetView
FREQUENCY -> return R.id.frequencyStackWidgetView
SCORE -> return R.id.scoreStackWidgetView
HISTORY -> return R.id.historyStackWidgetView
STREAKS -> return R.id.streakStackWidgetView
TARGET -> return R.id.targetStackWidgetView
}
return 0
}
fun getStackWidgetEmptyViewId(type: StackWidgetType?): Int {
when (type) {
CHECKMARK -> return R.id.checkmarkStackWidgetEmptyView
FREQUENCY -> return R.id.frequencyStackWidgetEmptyView
SCORE -> return R.id.scoreStackWidgetEmptyView
HISTORY -> return R.id.historyStackWidgetEmptyView
STREAKS -> return R.id.streakStackWidgetEmptyView
TARGET -> return R.id.targetStackWidgetEmptyView
}
return 0
}
}
}

@ -19,6 +19,7 @@
package org.isoron.uhabits.widgets
import android.app.PendingIntent
import android.content.Context
import android.view.View
import android.view.ViewGroup.LayoutParams
@ -31,10 +32,12 @@ import org.isoron.uhabits.widgets.views.GraphWidgetView
class StreakWidget(
context: Context,
id: Int,
private val habit: Habit
private val habit: Habit,
) : BaseWidget(context, id) {
override val defaultHeight: Int = 200
override val defaultWidth: Int = 200
override fun getOnClickPendingIntent(context: Context) =
override fun getOnClickPendingIntent(context: Context): PendingIntent =
pendingIntentFactory.showHabit(habit)
override fun refreshData(view: View) {
@ -53,7 +56,4 @@ class StreakWidget(
layoutParams = LayoutParams(MATCH_PARENT, MATCH_PARENT)
}
}
override fun getDefaultHeight() = 200
override fun getDefaultWidth() = 200
}

@ -23,7 +23,7 @@ import android.content.Context
class StreakWidgetProvider : BaseWidgetProvider() {
override fun getWidgetFromId(context: Context, id: Int): BaseWidget {
val habits = getHabitsFromWidgetId(id)
if (habits.size == 1) return StreakWidget(context, id, habits[0])
else return StackWidget(context, id, StackWidgetType.STREAKS, habits)
return if (habits.size == 1) StreakWidget(context, id, habits[0])
else StackWidget(context, id, StackWidgetType.STREAKS, habits)
}
}

@ -19,6 +19,7 @@
package org.isoron.uhabits.widgets
import android.app.PendingIntent
import android.content.Context
import android.view.View
import android.view.ViewGroup.LayoutParams
@ -34,10 +35,12 @@ import org.isoron.uhabits.widgets.views.GraphWidgetView
class TargetWidget(
context: Context,
id: Int,
private val habit: Habit
private val habit: Habit,
) : BaseWidget(context, id) {
override val defaultHeight: Int = 200
override val defaultWidth: Int = 200
override fun getOnClickPendingIntent(context: Context) =
override fun getOnClickPendingIntent(context: Context): PendingIntent =
pendingIntentFactory.showHabit(habit)
override fun refreshData(view: View) = runBlocking {
@ -58,7 +61,4 @@ class TargetWidget(
layoutParams = LayoutParams(MATCH_PARENT, MATCH_PARENT)
}
}
override fun getDefaultHeight() = 200
override fun getDefaultWidth() = 200
}

@ -23,7 +23,7 @@ import android.content.Context
class TargetWidgetProvider : BaseWidgetProvider() {
override fun getWidgetFromId(context: Context, id: Int): BaseWidget {
val habits = getHabitsFromWidgetId(id)
if (habits.size == 1) return TargetWidget(context, id, habits[0])
else return StackWidget(context, id, StackWidgetType.TARGET, habits)
return if (habits.size == 1) TargetWidget(context, id, habits[0])
else StackWidget(context, id, StackWidgetType.TARGET, habits)
}
}

@ -1,225 +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 android.content.*;
import android.util.*;
import android.widget.*;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.isoron.uhabits.*;
import org.isoron.uhabits.activities.habits.list.views.*;
import org.isoron.uhabits.core.models.*;
import org.isoron.uhabits.activities.common.views.*;
import org.isoron.uhabits.core.preferences.*;
import org.isoron.uhabits.inject.*;
import org.isoron.uhabits.utils.*;
import static org.isoron.uhabits.utils.InterfaceUtils.getDimension;
public class CheckmarkWidgetView extends HabitWidgetView {
protected int activeColor;
protected float percentage;
@Nullable
protected String name;
protected RingView ring;
protected TextView label;
protected int entryValue;
protected int entryState;
protected boolean isNumerical;
private Preferences preferences;
public CheckmarkWidgetView(Context context)
{
super(context);
init();
}
public CheckmarkWidgetView(Context context, AttributeSet attrs)
{
super(context, attrs);
init();
}
public void refresh()
{
if (backgroundPaint == null || frame == null || ring == null) return;
StyledResources res = new StyledResources(getContext());
int bgColor;
int fgColor;
switch (entryState) {
case Entry.YES_MANUAL:
case Entry.SKIP:
bgColor = activeColor;
fgColor = res.getColor(R.attr.highContrastReverseTextColor);
setShadowAlpha(0x4f);
backgroundPaint.setColor(bgColor);
frame.setBackgroundDrawable(background);
break;
case Entry.YES_AUTO:
case Entry.NO:
case Entry.UNKNOWN:
default:
bgColor = res.getColor(R.attr.cardBgColor);
fgColor = res.getColor(R.attr.mediumContrastTextColor);
setShadowAlpha(0x00);
break;
}
ring.setPercentage(percentage);
ring.setColor(fgColor);
ring.setBackgroundColor(bgColor);
ring.setText(getText());
label.setText(name);
label.setTextColor(fgColor);
requestLayout();
postInvalidate();
}
public void setEntryState(int entryState)
{
this.entryState = entryState;
}
protected String getText()
{
if (isNumerical) return NumberButtonViewKt.toShortString(entryValue / 1000.0);
switch (entryState) {
case Entry.YES_MANUAL:
case Entry.YES_AUTO:
return getResources().getString(R.string.fa_check);
case Entry.SKIP:
return getResources().getString(R.string.fa_skipped);
case Entry.UNKNOWN:
{
if (preferences.areQuestionMarksEnabled())
return getResources().getString(R.string.fa_question);
else
getResources().getString(R.string.fa_times);
}
case Entry.NO:
default:
return getResources().getString(R.string.fa_times);
}
}
public void setActiveColor(int activeColor)
{
this.activeColor = activeColor;
}
public void setEntryValue(int entryValue)
{
this.entryValue = entryValue;
}
public void setName(@NonNull String name)
{
this.name = name;
}
public void setPercentage(float percentage)
{
this.percentage = percentage;
}
public void setNumerical(boolean isNumerical)
{
this.isNumerical = isNumerical;
}
@Override
@NonNull
protected Integer getInnerLayoutId()
{
return R.layout.widget_checkmark;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
float w = width;
float h = width * 1.25f;
float scale = Math.min(width / w, height / h);
w *= scale;
h *= scale;
if (h < getDimension(getContext(), R.dimen.checkmarkWidget_heightBreakpoint))
ring.setVisibility(GONE);
else
ring.setVisibility(VISIBLE);
widthMeasureSpec =
MeasureSpec.makeMeasureSpec((int) w, MeasureSpec.EXACTLY);
heightMeasureSpec =
MeasureSpec.makeMeasureSpec((int) h, MeasureSpec.EXACTLY);
float textSize = 0.15f * h;
float maxTextSize = getDimension(getContext(), R.dimen.smallerTextSize);
textSize = Math.min(textSize, maxTextSize);
label.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
ring.setTextSize(textSize);
ring.setThickness(0.15f * textSize);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
private void init()
{
HabitsApplicationComponent appComponent;
appComponent = ((HabitsApplication) getContext().getApplicationContext()).getComponent();
preferences = appComponent.getPreferences();
ring = (RingView) findViewById(R.id.scoreRing);
label = (TextView) findViewById(R.id.label);
if (ring != null) ring.setIsTransparencyEnabled(true);
if (isInEditMode())
{
percentage = 0.75f;
name = "Wake up early";
activeColor = PaletteUtils.getAndroidTestColor(6);
entryValue = Entry.YES_MANUAL;
refresh();
}
}
}

@ -0,0 +1,156 @@
/*
* 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 android.content.Context
import android.util.AttributeSet
import android.util.TypedValue
import android.view.View
import android.widget.TextView
import org.isoron.uhabits.HabitsApplication
import org.isoron.uhabits.R
import org.isoron.uhabits.activities.common.views.RingView
import org.isoron.uhabits.activities.habits.list.views.toShortString
import org.isoron.uhabits.core.models.Entry.Companion.NO
import org.isoron.uhabits.core.models.Entry.Companion.SKIP
import org.isoron.uhabits.core.models.Entry.Companion.UNKNOWN
import org.isoron.uhabits.core.models.Entry.Companion.YES_AUTO
import org.isoron.uhabits.core.models.Entry.Companion.YES_MANUAL
import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.inject.HabitsApplicationComponent
import org.isoron.uhabits.utils.InterfaceUtils.getDimension
import org.isoron.uhabits.utils.PaletteUtils.getAndroidTestColor
import org.isoron.uhabits.utils.StyledResources
import kotlin.math.min
class CheckmarkWidgetView : HabitWidgetView {
var activeColor: Int = 0
var percentage = 0f
var name: String? = null
private lateinit var ring: RingView
private lateinit var label: TextView
var entryValue = 0
var entryState = 0
var isNumerical = false
private var preferences: Preferences? = null
constructor(context: Context?) : super(context) {
init()
}
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) {
init()
}
fun refresh() {
if (backgroundPaint == null || frame == null) return
val res = StyledResources(context)
val bgColor: Int
val fgColor: Int
when (entryState) {
YES_MANUAL, SKIP -> {
bgColor = activeColor
fgColor = res.getColor(R.attr.highContrastReverseTextColor)
setShadowAlpha(0x4f)
backgroundPaint!!.color = bgColor
frame!!.setBackgroundDrawable(background)
}
YES_AUTO, NO, UNKNOWN -> {
bgColor = res.getColor(R.attr.cardBgColor)
fgColor = res.getColor(R.attr.mediumContrastTextColor)
setShadowAlpha(0x00)
}
else -> {
bgColor = res.getColor(R.attr.cardBgColor)
fgColor = res.getColor(R.attr.mediumContrastTextColor)
setShadowAlpha(0x00)
}
}
ring.percentage = percentage
ring.color = fgColor
ring.setBackgroundColor(bgColor)
ring.setText(text)
label.text = name
label.setTextColor(fgColor)
requestLayout()
postInvalidate()
}
private val text: String
get() = if (isNumerical) {
(entryValue / 1000.0).toShortString()
} else when (entryState) {
YES_MANUAL, YES_AUTO -> resources.getString(R.string.fa_check)
SKIP -> resources.getString(R.string.fa_skipped)
UNKNOWN -> {
run {
if (preferences!!.areQuestionMarksEnabled()) {
return resources.getString(R.string.fa_question)
} else {
resources.getString(R.string.fa_times)
}
}
resources.getString(R.string.fa_times)
}
NO -> resources.getString(R.string.fa_times)
else -> resources.getString(R.string.fa_times)
}
override val innerLayoutId: Int
get() = R.layout.widget_checkmark
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
var widthMeasureSpec = widthMeasureSpec
var heightMeasureSpec = heightMeasureSpec
val width = MeasureSpec.getSize(widthMeasureSpec)
val height = MeasureSpec.getSize(heightMeasureSpec)
var w = width.toFloat()
var h = width * 1.25f
val scale = min(width / w, height / h)
w *= scale
h *= scale
if (h < getDimension(context, R.dimen.checkmarkWidget_heightBreakpoint)) ring.visibility =
GONE else ring.visibility = VISIBLE
widthMeasureSpec = MeasureSpec.makeMeasureSpec(w.toInt(), MeasureSpec.EXACTLY)
heightMeasureSpec = MeasureSpec.makeMeasureSpec(h.toInt(), MeasureSpec.EXACTLY)
var textSize = 0.15f * h
val maxTextSize = getDimension(context, R.dimen.smallerTextSize)
textSize = min(textSize, maxTextSize)
label.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
ring.setTextSize(textSize)
ring.setThickness(0.15f * textSize)
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
}
private fun init() {
val appComponent: HabitsApplicationComponent = (context.applicationContext as HabitsApplication).component
preferences = appComponent.preferences
ring = findViewById<View>(R.id.scoreRing) as RingView
label = findViewById<View>(R.id.label) as TextView
ring.setIsTransparencyEnabled(true)
if (isInEditMode) {
percentage = 0.75f
name = "Wake up early"
activeColor = getAndroidTestColor(6)
entryValue = YES_MANUAL
refresh()
}
}
}

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

Loading…
Cancel
Save