mirror of https://github.com/iSoron/uhabits.git
Merge pull request #709 from hiqua/dev
commit
e671949dd2
@ -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")
|
||||||
|
}
|
||||||
|
}
|
@ -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%")
|
||||||
|
}
|
||||||
|
}
|
@ -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()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -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 -> }
|
||||||
|
}
|
||||||
|
}
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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…
Reference in new issue