diff --git a/app/build.gradle b/app/build.gradle index eb67859e..ebffe89c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -50,6 +50,7 @@ dependencies { implementation 'com.google.android.gms:play-services-maps:17.0.0' implementation 'com.google.android.gms:play-services-location:18.0.0' implementation 'jp.wasabeef:richeditor-android:2.0.0' + implementation 'androidx.legacy:legacy-support-v4:1.0.0' testImplementation 'junit:junit:4.13.2' testImplementation 'org.mockito:mockito-core:3.9.0' androidTestImplementation 'org.mockito:mockito-android:3.9.0' diff --git a/app/src/androidTest/java/com/github/steroidteam/todolist/DailyPlanningFragmentTest.java b/app/src/androidTest/java/com/github/steroidteam/todolist/DailyPlanningFragmentTest.java new file mode 100644 index 00000000..6acc8ba2 --- /dev/null +++ b/app/src/androidTest/java/com/github/steroidteam/todolist/DailyPlanningFragmentTest.java @@ -0,0 +1,124 @@ +package com.github.steroidteam.todolist; + +import static androidx.test.espresso.Espresso.onView; +import static androidx.test.espresso.action.ViewActions.click; +import static androidx.test.espresso.assertion.ViewAssertions.matches; +import static androidx.test.espresso.matcher.ViewMatchers.withId; +import static androidx.test.espresso.matcher.ViewMatchers.withText; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.doReturn; + +import android.content.Context; +import androidx.fragment.app.testing.FragmentScenario; +import androidx.test.espresso.intent.Intents; +import com.github.steroidteam.todolist.database.Database; +import com.github.steroidteam.todolist.database.DatabaseFactory; +import com.github.steroidteam.todolist.model.todo.Tag; +import com.github.steroidteam.todolist.model.todo.Task; +import com.github.steroidteam.todolist.model.todo.TodoList; +import com.github.steroidteam.todolist.model.todo.TodoListCollection; +import com.github.steroidteam.todolist.view.DailyPlanningFragment; +import com.github.steroidteam.todolist.viewmodel.ViewModelFactoryInjection; +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class DailyPlanningFragmentTest { + + private FragmentScenario scenario; + @Mock Database databaseMock; + + @Mock Context context; + + @Before + public void initIntents() { + Intents.init(); + } + + @After + public void releaseIntents() { + Intents.release(); + } + + @Before + public void setUp() { + // Mock context for Broadcast Reminder + doReturn(context).when(context).getApplicationContext(); + + TodoList todoList = new TodoList("Some random title"); + CompletableFuture todoListFuture = new CompletableFuture<>(); + todoListFuture.complete(todoList); + + Task task = new Task("Random task title"); + Task task2 = new Task("task2"); + todoList.addTask(task); + todoList.addTask(task2); + doReturn(todoListFuture).when(databaseMock).getTodoList(any()); + CompletableFuture taskFuture = new CompletableFuture<>(); + taskFuture.complete(task); + doReturn(taskFuture).when(databaseMock).putTask(any(), any(Task.class)); + doReturn(taskFuture).when(databaseMock).updateTask(any(), anyInt(), any(Task.class)); + doReturn(taskFuture).when(databaseMock).removeTask(any(), anyInt()); + doReturn(taskFuture).when(databaseMock).setTaskDone(any(), anyInt(), anyBoolean()); + doReturn(taskFuture).when(databaseMock).removeDoneTasks(any()); + + TodoListCollection collection = new TodoListCollection(); + collection.addUUID(UUID.randomUUID()); + CompletableFuture todoListCollectionFuture = new CompletableFuture<>(); + todoListCollectionFuture.complete(collection); + doReturn(todoListCollectionFuture).when(databaseMock).getTodoListCollection(); + + CompletableFuture> tagsFuture = new CompletableFuture<>(); + tagsFuture.complete(new ArrayList<>()); + doReturn(tagsFuture).when(databaseMock).getAllTags(); + doReturn(tagsFuture).when(databaseMock).getTagsFromIds(any()); + doReturn(tagsFuture).when(databaseMock).getAllTagsIds(); + doReturn(tagsFuture).when(databaseMock).getTagsFromList(any()); + + DatabaseFactory.setCustomDatabase(databaseMock); + + File fakeFile = new File("Fake pathname"); + doReturn(fakeFile).when(context).getCacheDir(); + ViewModelFactoryInjection.setCustomTodoListRepo(context, UUID.randomUUID()); + scenario = + FragmentScenario.launchInContainer( + DailyPlanningFragment.class, null, R.style.AsteroidTheme); + } + + @Test + public void setTaskForTodayWorks() { + final String TASK_DESCRIPTION = "Buy bananas"; + + TodoList todoList = new TodoList("Some random title"); + CompletableFuture todoListFuture = new CompletableFuture<>(); + todoListFuture.complete(todoList); + Task task = new Task(TASK_DESCRIPTION); + todoList.addTask(task); + doReturn(todoListFuture).when(databaseMock).getTodoList(any()); + + CompletableFuture taskFuture = new CompletableFuture<>(); + taskFuture.complete(task); + doReturn(taskFuture).when(databaseMock).getTask(any(), anyInt()); + + onView(withId(R.id.today_plan_button)).perform(click()); + onView(withId(R.id.today_none_button)).perform(click()); + } + + @Test + public void tasksProperlyCycle() { + onView(withId(R.id.today_plan_button)).perform(click()); + onView(withId(R.id.afternoon_button)).perform(click()); + onView(withId(R.id.task_description)).check(matches(withText("task2"))); + } +} diff --git a/app/src/main/java/com/github/steroidteam/todolist/view/DailyPlanningFragment.java b/app/src/main/java/com/github/steroidteam/todolist/view/DailyPlanningFragment.java new file mode 100644 index 00000000..f8fe1bae --- /dev/null +++ b/app/src/main/java/com/github/steroidteam/todolist/view/DailyPlanningFragment.java @@ -0,0 +1,206 @@ +package com.github.steroidteam.todolist.view; + +import android.os.Bundle; +import android.text.format.DateUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.constraintlayout.widget.ConstraintLayout; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; +import com.github.steroidteam.todolist.R; +import com.github.steroidteam.todolist.model.todo.Task; +import com.github.steroidteam.todolist.model.todo.TodoList; +import com.github.steroidteam.todolist.viewmodel.TodoListViewModel; +import com.github.steroidteam.todolist.viewmodel.TodoViewModelFactory; +import com.github.steroidteam.todolist.viewmodel.ViewModelFactoryInjection; +import java.util.Calendar; +import java.util.Date; + +public class DailyPlanningFragment extends Fragment { + + private TodoListViewModel viewModel; + private int currentTaskIndex = -1; + + @Override + public View onCreateView( + @NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + View root = inflater.inflate(R.layout.fragment_daily_planning, container, false); + + // Add a click listener to the "back" button to return to the previous activity. + root.findViewById(R.id.back_button) + .setOnClickListener(v -> getParentFragmentManager().popBackStack()); + + TodoViewModelFactory todoViewModelFactory = + ViewModelFactoryInjection.getTodoViewModelFactory(getContext()); + viewModel = + new ViewModelProvider(requireActivity(), todoViewModelFactory) + .get(TodoListViewModel.class); + + viewModel + .getTodoList() + .observe( + getViewLifecycleOwner(), + (todoList) -> { + TextView activityTitle = root.findViewById(R.id.activity_title); + activityTitle.setText(todoList.getTitle()); + }); + + setListeners(root); + + return root; + } + + @Override + public void onStart() { + super.onStart(); + findNextUnplannedTask(); + } + + public void setListeners(View root) { + root.findViewById(R.id.set_done_button).setOnClickListener(this::setDoneButtonListener); + root.findViewById(R.id.today_plan_button).setOnClickListener(this::switchToToday); + root.findViewById(R.id.other_day_plan_button).setOnClickListener(this::switchToOtherDay); + root.findViewById(R.id.delete_task_button).setOnClickListener(this::skipTaskInPlan); + + root.findViewById(R.id.today_none_button) + .setOnClickListener(new SetDateListener(Plan.TODAY)); + root.findViewById(R.id.midday_button).setOnClickListener(new SetDateListener(Plan.MIDDAY)); + root.findViewById(R.id.afternoon_button) + .setOnClickListener(new SetDateListener(Plan.AFTERNOON)); + root.findViewById(R.id.evening_button) + .setOnClickListener(new SetDateListener(Plan.EVENING)); + root.findViewById(R.id.night_button).setOnClickListener(new SetDateListener(Plan.NIGHT)); + root.findViewById(R.id.tomorrow_button) + .setOnClickListener(new SetDateListener(Plan.TOMORROW)); + root.findViewById(R.id.twodays_plan_button) + .setOnClickListener(new SetDateListener(Plan.TWODAYS)); + root.findViewById(R.id.week_plan_button).setOnClickListener(new SetDateListener(Plan.WEEK)); + root.findViewById(R.id.oneday_button).setOnClickListener(new SetDateListener(Plan.ONEDAY)); + } + + public void setDoneButtonListener(View view) { + viewModel.setTaskDone(currentTaskIndex, true); + findNextUnplannedTask(); + } + + public void switchToToday(View view) { + ConstraintLayout mainPlan = getView().findViewById(R.id.main_plan); + mainPlan.setVisibility(View.GONE); + ConstraintLayout todayPlan = getView().findViewById(R.id.today_plan); + todayPlan.setVisibility(View.VISIBLE); + } + + public void switchToOtherDay(View view) { + ConstraintLayout mainPlan = getView().findViewById(R.id.main_plan); + mainPlan.setVisibility(View.GONE); + ConstraintLayout otherDayPlan = getView().findViewById(R.id.other_day_plan); + otherDayPlan.setVisibility(View.VISIBLE); + } + + /** + * Used as placeholder waiting for more features (selecting day and hour / integrate viewmodel + */ + public void skipTaskInPlan(View view) { + findNextUnplannedTask(); + } + + /** + * Finds the next task to be planned, if there isn't anymore it goes back to ItemViewActivity + */ + private void findNextUnplannedTask() { + TodoList list = viewModel.getTodoList().getValue(); + while (currentTaskIndex + 1 < list.getSize()) { + currentTaskIndex++; + Task task = list.getTask(currentTaskIndex); + Date date = task.getDueDate(); + if (!task.isDone() + && (date == null + || DateUtils.isToday(date.getTime()) + || date.before(new Date()))) { + TextView text = getView().findViewById(R.id.task_description); + text.setText(task.getBody()); + return; + } + } + getParentFragmentManager().popBackStack(); + } + + private enum Plan { + TODAY, + TOMORROW, + TWODAYS, + WEEK, + ONEDAY, + MIDDAY, + AFTERNOON, + EVENING, + NIGHT + } + + private class SetDateListener implements View.OnClickListener { + private Plan plan; + + public SetDateListener(Plan plan) { + this.plan = plan; + } + + @Override + public void onClick(View v) { + Date date = new Date(); + Calendar c = Calendar.getInstance(); + c.setTime(date); + c.set(Calendar.MINUTE, 0); + c.set(Calendar.SECOND, 0); + + switch (plan) { + case TODAY: + // Nothing more to do + break; + case TOMORROW: + c.add(Calendar.DATE, 1); + date = c.getTime(); + break; + case TWODAYS: + c.add(Calendar.DATE, 2); + date = c.getTime(); + break; + case WEEK: + c.add(Calendar.DATE, 7); + date = c.getTime(); + break; + case ONEDAY: + date = null; + break; + case MIDDAY: + c.set(Calendar.HOUR_OF_DAY, 12); + date = c.getTime(); + break; + case AFTERNOON: + c.set(Calendar.HOUR_OF_DAY, 15); + date = c.getTime(); + break; + case EVENING: + c.set(Calendar.HOUR_OF_DAY, 19); + date = c.getTime(); + break; + case NIGHT: + c.set(Calendar.HOUR_OF_DAY, 22); + date = c.getTime(); + break; + } + + viewModel.setTaskDueDate(currentTaskIndex, date); + findNextUnplannedTask(); + ConstraintLayout mainPlan = getView().findViewById(R.id.main_plan); + mainPlan.setVisibility(View.VISIBLE); + ConstraintLayout todayPlan = getView().findViewById(R.id.today_plan); + todayPlan.setVisibility(View.GONE); + ConstraintLayout otherDayPlan = getView().findViewById(R.id.other_day_plan); + otherDayPlan.setVisibility(View.GONE); + } + } +} diff --git a/app/src/main/java/com/github/steroidteam/todolist/view/ItemViewFragment.java b/app/src/main/java/com/github/steroidteam/todolist/view/ItemViewFragment.java index d01744d8..37ab8e78 100644 --- a/app/src/main/java/com/github/steroidteam/todolist/view/ItemViewFragment.java +++ b/app/src/main/java/com/github/steroidteam/todolist/view/ItemViewFragment.java @@ -101,6 +101,7 @@ public View onCreateView( root.findViewById(R.id.new_task_btn).setOnClickListener(this::addTask); root.findViewById(R.id.remove_done_tasks_btn).setOnClickListener(this::removeDoneTasks); + root.findViewById(R.id.daily_planning_btn).setOnClickListener(this::goToDailyPlanning); ConstraintLayout updateLayout = root.findViewById(R.id.layout_update_task); updateLayout.setVisibility(View.GONE); @@ -361,6 +362,10 @@ public void checkBoxTaskListener(final int position, final boolean isChecked) { viewModel.setTaskDone(position, isChecked); } + public void goToDailyPlanning(View view) { + Navigation.findNavController(getView()).navigate(R.id.nav_daily_planning, new Bundle()); + } + @Override public void onRequestPermissionsResult( int requestCode, diff --git a/app/src/main/res/drawable/fab.xml b/app/src/main/res/drawable/fab.xml new file mode 100644 index 00000000..4e43c6f4 --- /dev/null +++ b/app/src/main/res/drawable/fab.xml @@ -0,0 +1,13 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_baseline_arrow_downward_24.xml b/app/src/main/res/drawable/ic_baseline_arrow_downward_24.xml new file mode 100644 index 00000000..8f9be44a --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_arrow_downward_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_arrow_forward_24.xml b/app/src/main/res/drawable/ic_baseline_arrow_forward_24.xml new file mode 100644 index 00000000..702e70c5 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_arrow_forward_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_check_24.xml b/app/src/main/res/drawable/ic_baseline_check_24.xml new file mode 100644 index 00000000..0432fa69 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_check_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_chevron_right_24.xml b/app/src/main/res/drawable/ic_baseline_chevron_right_24.xml new file mode 100644 index 00000000..fd2878a6 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_chevron_right_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_clear_24.xml b/app/src/main/res/drawable/ic_baseline_clear_24.xml new file mode 100644 index 00000000..16d6d37d --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_clear_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_last_page_24.xml b/app/src/main/res/drawable/ic_baseline_last_page_24.xml new file mode 100644 index 00000000..32b69adb --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_last_page_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/fragment_daily_planning.xml b/app/src/main/res/layout/fragment_daily_planning.xml new file mode 100644 index 00000000..026bd703 --- /dev/null +++ b/app/src/main/res/layout/fragment_daily_planning.xml @@ -0,0 +1,489 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_item_view.xml b/app/src/main/res/layout/fragment_item_view.xml index 26b8ae37..1459619c 100644 --- a/app/src/main/res/layout/fragment_item_view.xml +++ b/app/src/main/res/layout/fragment_item_view.xml @@ -39,6 +39,20 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> + + + tools:layout="@layout/fragment_item_view" > + + + + + Asteroid diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index f2485218..80b46bea 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -156,4 +156,9 @@ @android:color/black + + \ No newline at end of file