diff --git a/.gitignore b/.gitignore index 7665a4a..05477b1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ *.iml +.idea/ .gradle /local.properties /.idea/workspace.xml @@ -7,4 +8,4 @@ /build /captures .externalNativeBuild -/app/google-services.json \ No newline at end of file +/app/google-services.json diff --git a/.idea/assetWizardSettings.xml b/.idea/assetWizardSettings.xml deleted file mode 100644 index cb4e5a8..0000000 --- a/.idea/assetWizardSettings.xml +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/caches/build_file_checksums.ser b/.idea/caches/build_file_checksums.ser deleted file mode 100644 index e023764..0000000 Binary files a/.idea/caches/build_file_checksums.ser and /dev/null differ diff --git a/.idea/encodings.xml b/.idea/encodings.xml deleted file mode 100644 index 15a15b2..0000000 --- a/.idea/encodings.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml deleted file mode 100644 index 7ac24c7..0000000 --- a/.idea/gradle.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 37a7509..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 2450ea5..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml deleted file mode 100644 index 7f68460..0000000 --- a/.idea/runConfigurations.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 35eb1dd..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 7476c84..a64c78e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,7 +1,7 @@ apply plugin: 'com.android.application' apply plugin: 'com.google.gms.google-services' -apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-kapt' apply plugin: 'io.fabric' @@ -39,7 +39,7 @@ dependencies { // UI implementation "androidx.appcompat:appcompat:$rootProject.appcompatVersion" implementation "com.google.android.material:material:$rootProject.materialVersion" - implementation "androidx.cardview:cardview:$rootProject.androidxVersion" + implementation "androidx.cardview:cardview:$rootProject.cardViewVersion" implementation "androidx.preference:preference:$rootProject.androidxVersion" implementation "com.github.XunMengWinter:CircularAnim:$rootProject.circularAnimVersion" implementation "com.github.Daio-io:dresscode:$rootProject.dresscodeVersion" @@ -66,8 +66,18 @@ dependencies { // Lifecycle components implementation "android.arch.lifecycle:extensions:$rootProject.lifecycleVersion" - annotationProcessor "android.arch.lifecycle:compiler:$rootProject.lifecycleVersion" + kapt "android.arch.lifecycle:compiler:$rootProject.lifecycleVersion" // Tests testImplementation "junit:junit:$rootProject.junitVersion" + + // Core library + androidTestImplementation 'androidx.test:core:1.1.0' + + // AndroidJUnitRunner and JUnit Rules + androidTestImplementation 'androidx.test:runner:1.1.1' + androidTestImplementation 'androidx.test:rules:1.1.1' + + // Assertions + androidTestImplementation 'androidx.test.ext:junit:1.1.0' } \ No newline at end of file diff --git a/app/src/androidTest/java/apps/jizzu/simpletodo/TaskDAOTest.kt b/app/src/androidTest/java/apps/jizzu/simpletodo/TaskDAOTest.kt new file mode 100644 index 0000000..b8f094f --- /dev/null +++ b/app/src/androidTest/java/apps/jizzu/simpletodo/TaskDAOTest.kt @@ -0,0 +1,126 @@ +package apps.jizzu.simpletodo + +import android.content.Context +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import apps.jizzu.simpletodo.data.database.TaskDao +import apps.jizzu.simpletodo.data.database.TasksDatabase +import apps.jizzu.simpletodo.data.models.Task +import org.junit.After +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import java.io.IOException +import java.util.* + +@RunWith(AndroidJUnit4::class) +class TaskDAOTest { + + private lateinit var taskDao: TaskDao + private lateinit var tasksDatabase: TasksDatabase + + @Before + fun createDb() { + val context = ApplicationProvider.getApplicationContext() + tasksDatabase = Room.inMemoryDatabaseBuilder( + context, TasksDatabase::class.java + ).build() + taskDao = tasksDatabase.taskDAO() + } + + @Test + fun testSaveTask() { + val task = Task(1, "", "", Date().time, 1, Date().time) + taskDao.saveTask(task) + + val tasks = taskDao.getAllTasks() + assertEquals(1, tasks.size) + } + + @Test + fun testGetAllTasks() { + var task = Task(1, "", "", Date().time, 1, Date().time) + taskDao.saveTask(task) + + var tasks = taskDao.getAllTasks() + assertEquals(1, tasks.size) + + task = Task(2, "", "", Date().time, 2, Date().time, false) + taskDao.saveTask(task) + + tasks = taskDao.getAllTasks() + assertEquals(2, tasks.size) + + task = Task(3, "", "", Date().time, 2, Date().time, true) + taskDao.saveTask(task) + + tasks = taskDao.getAllTasks() + assertEquals(3, tasks.size) + } + + @Test + fun testGetAllOpenTasks() { + var task = Task(1, "", "", Date().time, 1, Date().time) + taskDao.saveTask(task) + + var tasks = taskDao.getAllOpenTasks() + assertEquals(1, tasks.size) + + task = Task(2, "", "", Date().time, 2, Date().time, false) + taskDao.saveTask(task) + + tasks = taskDao.getAllOpenTasks() + assertEquals(2, tasks.size) + + task = Task(3, "", "", Date().time, 2, Date().time, true) + taskDao.saveTask(task) + + tasks = taskDao.getAllOpenTasks() + assertEquals(2, tasks.size) + } + + @Test + fun testUpdateTask() { + var tasks = taskDao.getAllTasks() + assertEquals(0, tasks.size) + + val task = Task(1, "", "", Date().time, 1, Date().time) + taskDao.saveTask(task) + + tasks = taskDao.getAllOpenTasks() + assertEquals("", tasks[0].title) + + tasks[0].title = "Test" + taskDao.updateTask(tasks[0]) + + tasks = taskDao.getAllTasks() + assertEquals(1, tasks.size) + assertEquals("Test", tasks[0].title) + } + + @Test + fun testUpdateStatusTask() { + var tasks = taskDao.getAllTasks() + assertEquals(0, tasks.size) + + val task = Task(1, "Test Me", "Task to Complete", Date().time, 1, Date().time) + taskDao.saveTask(task) + + tasks = taskDao.getAllOpenTasks() + assertFalse(tasks[0].taskStatus) + + tasks[0].taskStatus = tasks[0].taskStatus.not() + taskDao.updateTask(tasks[0]) + + tasks = taskDao.getAllOpenTasks() + assertEquals(0, tasks.size) + } + + @After + @Throws(IOException::class) + fun closeDb() { + tasksDatabase.close() + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3a1fc74..3259d23 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,5 +1,6 @@ @@ -12,7 +13,8 @@ android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" - android:theme="@style/AppTheme.Light"> + android:theme="@style/AppTheme.Light" + tools:ignore="AllowBackup,GoogleAppIndexingWarning"> + + diff --git a/app/src/main/java/apps/jizzu/simpletodo/data/database/TaskDao.kt b/app/src/main/java/apps/jizzu/simpletodo/data/database/TaskDao.kt index 30a760b..ff128d1 100644 --- a/app/src/main/java/apps/jizzu/simpletodo/data/database/TaskDao.kt +++ b/app/src/main/java/apps/jizzu/simpletodo/data/database/TaskDao.kt @@ -21,12 +21,21 @@ interface TaskDao { @Query("DELETE FROM tasks_table") fun deleteAllTasks() + @Query("SELECT * FROM tasks_table where task_status=0 ORDER BY task_position") + fun getAllOpenTasksLiveData(): LiveData> + + @Query("SELECT * FROM tasks_table where task_status=1 ORDER BY task_position") + fun getAllCompletedTasksLiveData(): LiveData> + @Query("SELECT * FROM tasks_table ORDER BY task_position") fun getAllTasksLiveData(): LiveData> @Query("SELECT * FROM tasks_table ORDER BY task_position") fun getAllTasks(): List + @Query("SELECT * FROM tasks_table where task_status=0 ORDER BY task_position") + fun getAllOpenTasks(): List + @Query("SELECT * FROM tasks_table WHERE task_title LIKE '%' || :searchText || '%'") fun getTasksForSearch(searchText: String): LiveData> } \ No newline at end of file diff --git a/app/src/main/java/apps/jizzu/simpletodo/data/database/TaskListRepository.kt b/app/src/main/java/apps/jizzu/simpletodo/data/database/TaskListRepository.kt index e3248f7..b2fb195 100644 --- a/app/src/main/java/apps/jizzu/simpletodo/data/database/TaskListRepository.kt +++ b/app/src/main/java/apps/jizzu/simpletodo/data/database/TaskListRepository.kt @@ -9,9 +9,11 @@ import io.reactivex.schedulers.Schedulers class TaskListRepository(app: Application) { private val mTaskDao = TasksDatabase.getInstance(app).taskDAO() - private val mAllTasksLiveData = mTaskDao.getAllTasksLiveData() + private val mAllOpenTasksLiveData = mTaskDao.getAllOpenTasksLiveData() + private val mAllCompletedTasksLiveData = mTaskDao.getAllCompletedTasksLiveData() - fun getAllTasksLiveData() = mAllTasksLiveData + fun getAllOpenTasksLiveData() = mAllOpenTasksLiveData + fun getAllCompletedTasksLiveData() = mAllCompletedTasksLiveData fun deleteAllTasks() = Completable.fromCallable { mTaskDao.deleteAllTasks() }.subscribeOn(Schedulers.io()).subscribe()!! @@ -32,4 +34,12 @@ class TaskListRepository(app: Application) { .subscribeBy(onNext = { task -> taskList.add(task) }) return taskList } + + fun getAllOpenTasks(): ArrayList { + val taskList = arrayListOf() + Observable.fromCallable { mTaskDao.getAllOpenTasks() }.subscribeOn(Schedulers.io()) + .flatMap { tasks -> Observable.fromIterable(tasks) } + .subscribeBy(onNext = { task -> taskList.add(task) }) + return taskList + } } \ No newline at end of file diff --git a/app/src/main/java/apps/jizzu/simpletodo/data/database/TasksDatabase.kt b/app/src/main/java/apps/jizzu/simpletodo/data/database/TasksDatabase.kt index f116bb6..f43bbf9 100644 --- a/app/src/main/java/apps/jizzu/simpletodo/data/database/TasksDatabase.kt +++ b/app/src/main/java/apps/jizzu/simpletodo/data/database/TasksDatabase.kt @@ -9,7 +9,7 @@ import androidx.sqlite.db.SupportSQLiteDatabase import apps.jizzu.simpletodo.data.models.Task import apps.jizzu.simpletodo.utils.PreferenceHelper -@Database(entities = [Task::class], version = 3) +@Database(entities = [Task::class], version = 4) abstract class TasksDatabase : RoomDatabase() { abstract fun taskDAO(): TaskDao @@ -23,6 +23,7 @@ abstract class TasksDatabase : RoomDatabase() { private const val TEMP_TABLE = "temp_table" private const val TASK_ID_COLUMN = "_id" + private const val TASK_STATUS_COLUMN = "task_status" private const val TASK_TITLE_COLUMN = "task_title" private const val TASK_NOTE_COLUMN = "task_note" private const val TASK_DATE_COLUMN = "task_date" @@ -51,12 +52,25 @@ abstract class TasksDatabase : RoomDatabase() { } } + private val MIGRATION_3_4 = object : Migration(3, 4) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("CREATE TABLE $TEMP_TABLE ($TASK_ID_COLUMN INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, $TASK_TITLE_COLUMN TEXT NOT NULL, " + + "$TASK_DATE_COLUMN INTEGER NOT NULL, $TASK_POSITION_COLUMN INTEGER NOT NULL, $TASK_TIME_STAMP_COLUMN INTEGER NOT NULL, " + + "$TASK_NOTE_COLUMN TEXT DEFAULT '' NOT NULL, " + + "$TASK_STATUS_COLUMN INTEGER DEFAULT 0 NOT NULL);") + database.execSQL("INSERT INTO $TEMP_TABLE ($TASK_ID_COLUMN, $TASK_TITLE_COLUMN, $TASK_DATE_COLUMN, $TASK_POSITION_COLUMN, $TASK_TIME_STAMP_COLUMN, $TASK_NOTE_COLUMN) " + + "SELECT $TASK_ID_COLUMN, $TASK_TITLE_COLUMN, $TASK_DATE_COLUMN, $TASK_POSITION_COLUMN, $TASK_TIME_STAMP_COLUMN, $TASK_NOTE_COLUMN FROM $TASKS_TABLE") + database.execSQL("DROP TABLE $TASKS_TABLE") + database.execSQL("ALTER TABLE $TEMP_TABLE RENAME TO $TASKS_TABLE;") + } + } + fun getInstance(context: Context): TasksDatabase { synchronized(TasksDatabase::class.java) { if (mInstance == null) { mInstance = Room.databaseBuilder(context, TasksDatabase::class.java, DATABASE_NAME) - .addMigrations(MIGRATION_1_2, MIGRATION_2_3) + .addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4) .build() } } diff --git a/app/src/main/java/apps/jizzu/simpletodo/data/models/Task.kt b/app/src/main/java/apps/jizzu/simpletodo/data/models/Task.kt index 34556c3..cf16fdd 100644 --- a/app/src/main/java/apps/jizzu/simpletodo/data/models/Task.kt +++ b/app/src/main/java/apps/jizzu/simpletodo/data/models/Task.kt @@ -34,17 +34,22 @@ class Task : Serializable { @NonNull var timeStamp: Long = 0 + @ColumnInfo(name = "task_status") + @NonNull + var taskStatus: Boolean = false + @Ignore constructor() { this.timeStamp = Date().time } - constructor(id: Long, title: String, note: String, date: Long, position: Int, timeStamp: Long) { + constructor(id: Long, title: String, note: String, date: Long, position: Int, timeStamp: Long, taskStatus: Boolean = false) { this.id = id this.title = title this.note = note this.date = date this.position = position this.timeStamp = timeStamp + this.taskStatus = taskStatus } } diff --git a/app/src/main/java/apps/jizzu/simpletodo/ui/recycler/RecyclerViewAdapter.kt b/app/src/main/java/apps/jizzu/simpletodo/ui/recycler/RecyclerViewAdapter.kt index 951738b..044b215 100644 --- a/app/src/main/java/apps/jizzu/simpletodo/ui/recycler/RecyclerViewAdapter.kt +++ b/app/src/main/java/apps/jizzu/simpletodo/ui/recycler/RecyclerViewAdapter.kt @@ -6,8 +6,10 @@ import android.text.format.DateUtils import android.util.DisplayMetrics import android.util.Log import android.view.* +import android.widget.CompoundButton import android.widget.ImageView import android.widget.TextView +import androidx.appcompat.widget.AppCompatCheckBox import androidx.core.content.ContextCompat import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView @@ -50,13 +52,14 @@ class RecyclerViewAdapter : RecyclerView.Adapter(R.id.cbTaskStatus) val title = v.findViewById(R.id.tvTaskTitle) val note = v.findViewById(R.id.ivTaskNote) val date = v.findViewById(R.id.tvTaskDate) mContext = parent.context - return TaskViewHolder(v, title, note, date) + return TaskViewHolder(v, status, title, note, date) } override fun onBindViewHolder(holder: TaskViewHolder, position: Int) { @@ -68,6 +71,11 @@ class RecyclerViewAdapter : RecyclerView.Adapter> { response -> updateViewState(response) }) + + mRecyclerView.setHasFixedSize(true) + mRecyclerView.layoutManager = LinearLayoutManager(this) + mAdapter = RecyclerViewAdapter() + mRecyclerView.adapter = mAdapter + llEmptyView.visible() + createItemTouchHelper() + } + + override fun onResume() { + super.onResume() + + mAdapter.setOnItemClickListener(object : RecyclerViewAdapter.ClickListener { + override fun onTaskClick(v: View, position: Int) { + showTaskDetailsActivity(mAdapter.getTaskAtPosition(position)) + } + }) + + mAdapter.setTaskCompletionListener(object : RecyclerViewAdapter.TaskCompletionListener { + override fun onTaskStatusChanged(v: View, position: Int) { + moveTaskToOpen(position) + } + }) + } + + private fun createItemTouchHelper() { + val helper = ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT) { + + override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int { + val dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN // Flags for up and down movement + val swipeFlags = ItemTouchHelper.START or ItemTouchHelper.END // Flags for left and right movement + return ItemTouchHelper.Callback.makeMovementFlags(dragFlags, swipeFlags) + } + + override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean { + return true + } + + override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { + if (direction == ItemTouchHelper.END) { + moveTaskToOpen(viewHolder.adapterPosition) + } else if (direction == ItemTouchHelper.START){ + deleteTask(viewHolder.adapterPosition) + } + } + }) + helper.attachToRecyclerView(mRecyclerView) + } + + private fun moveTaskToOpen(position: Int) { + val completedTask = mAdapter.getTaskAtPosition(position) + val isCompletedTaskHasLastPosition = completedTask.position == mAdapter.itemCount - 1 + mAdapter.removeTask(position) + val alarmHelper = AlarmHelper.getInstance() + alarmHelper.removeAlarm(completedTask.timeStamp) + completedTask.taskStatus = completedTask.taskStatus.not() + mViewModel.updateTask(completedTask) + + var isUndoClicked = false + + mSnackbar = Snackbar.make(mRecyclerView, R.string.move_task_to_open, Snackbar.LENGTH_LONG) + mSnackbar?.setAction(R.string.snackbar_undo) { + completedTask.taskStatus = completedTask.taskStatus.not() + mViewModel.saveTask(completedTask) + if (completedTask.date != 0L && completedTask.date > Calendar.getInstance().timeInMillis) { + alarmHelper.setAlarm(completedTask) + } + isUndoClicked = true + + Handler().postDelayed({ + val firstCompletelyVisibleItem = (mRecyclerView.layoutManager as LinearLayoutManager).findFirstCompletelyVisibleItemPosition() + if (firstCompletelyVisibleItem != 0 && !RecyclerViewScrollListener.isShadowShown) { + setToolbarShadow(0f, 10f) + RecyclerViewScrollListener.isShadowShown = true + } + }, 100) + } + + mSnackbar?.view?.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener { + override fun onViewAttachedToWindow(view: View) { + val firstCompletelyVisibleItem = (mRecyclerView.layoutManager as LinearLayoutManager).findFirstCompletelyVisibleItemPosition() + val lastCompletelyVisibleItem = (mRecyclerView.layoutManager as LinearLayoutManager).findLastCompletelyVisibleItemPosition() + + if (firstCompletelyVisibleItem == 0 && lastCompletelyVisibleItem == MainActivity.mTaskList.size - 1 && RecyclerViewScrollListener.isShadowShown) { + setToolbarShadow(10f, 0f) + RecyclerViewScrollListener.isShadowShown = false + } + } + + override fun onViewDetachedFromWindow(view: View) { + if (!isUndoClicked) { + alarmHelper.removeNotification(completedTask.timeStamp, this@CompletedTasksActivity) + if (!isCompletedTaskHasLastPosition) recountTaskPositions() + } + } + }) + mSnackbar?.show() + } + + private fun deleteTask(position: Int) { + val deletedTask = mAdapter.getTaskAtPosition(position) + val isDeletedTaskHasLastPosition = deletedTask.position == mAdapter.itemCount - 1 + val alarmHelper = AlarmHelper.getInstance() + alarmHelper.removeAlarm(deletedTask.timeStamp) + mAdapter.removeTask(position) + mViewModel.deleteTask(deletedTask) + var isUndoClicked = false + + mSnackbar = Snackbar.make(mRecyclerView, R.string.snackbar_remove_task, Snackbar.LENGTH_LONG) + mSnackbar?.setAction(R.string.snackbar_undo) { + mViewModel.saveTask(deletedTask) + if (deletedTask.date != 0L && deletedTask.date > Calendar.getInstance().timeInMillis) { + alarmHelper.setAlarm(deletedTask) + } + isUndoClicked = true + + Handler().postDelayed({ + val firstCompletelyVisibleItem = (mRecyclerView.layoutManager as LinearLayoutManager).findFirstCompletelyVisibleItemPosition() + if (firstCompletelyVisibleItem != 0 && !RecyclerViewScrollListener.isShadowShown) { + setToolbarShadow(0f, 10f) + RecyclerViewScrollListener.isShadowShown = true + } + }, 100) + } + + mSnackbar?.view?.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener { + override fun onViewAttachedToWindow(view: View) { + val firstCompletelyVisibleItem = (mRecyclerView.layoutManager as LinearLayoutManager).findFirstCompletelyVisibleItemPosition() + val lastCompletelyVisibleItem = (mRecyclerView.layoutManager as LinearLayoutManager).findLastCompletelyVisibleItemPosition() + + if (firstCompletelyVisibleItem == 0 && lastCompletelyVisibleItem == MainActivity.mTaskList.size - 1 && RecyclerViewScrollListener.isShadowShown) { + setToolbarShadow(10f, 0f) + RecyclerViewScrollListener.isShadowShown = false + } + } + + override fun onViewDetachedFromWindow(view: View) { + if (!isUndoClicked) { + alarmHelper.removeNotification(deletedTask.timeStamp, this@CompletedTasksActivity) + if (!isDeletedTaskHasLastPosition) recountTaskPositions() + } + } + }) + mSnackbar?.show() + } + + private fun recountTaskPositions() { + for ((newPosition, task) in MainActivity.mTaskList.withIndex()) { + task.position = newPosition + } + mViewModel.updateTaskOrder(MainActivity.mTaskList) + } + + private fun updateViewState(tasks: List) = if (tasks.isEmpty()) showEmptyView() + else showTaskList(tasks) + + private fun showEmptyView() { + mAdapter.updateData(arrayListOf()) + llEmptyView.visible() + ivEmptyIllustration.setImageDrawable(AppCompatResources.getDrawable(this, + R.drawable.illustration_not_found)) + tvEmptyTitle.text = getString(R.string.search_view_not_found_text) + } + + private fun showTaskList(tasks: List) { + llEmptyView.gone() + mAdapter.updateData(tasks) + } + + private fun createViewModel() = ViewModelProviders.of(this).get(TaskListViewModel(application)::class.java) + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + if (item.itemId == android.R.id.home) { + onBackPressed() + return true + } + return false + } +} diff --git a/app/src/main/java/apps/jizzu/simpletodo/ui/view/MainActivity.kt b/app/src/main/java/apps/jizzu/simpletodo/ui/view/MainActivity.kt index 556a58d..b60c346 100644 --- a/app/src/main/java/apps/jizzu/simpletodo/ui/view/MainActivity.kt +++ b/app/src/main/java/apps/jizzu/simpletodo/ui/view/MainActivity.kt @@ -79,7 +79,7 @@ class MainActivity : BaseActivity() { AlarmHelper.getInstance().init(applicationContext) mViewModel = createViewModel() - mViewModel.liveData.observe(this, Observer> { response -> updateViewState(response) }) + mViewModel.allOpenTasksLiveData.observe(this, Observer> { response -> updateViewState(response) }) PreferenceHelper.getInstance().init(applicationContext) mPreferenceHelper = PreferenceHelper.getInstance() @@ -165,12 +165,66 @@ class MainActivity : BaseActivity() { } override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { - deleteTask(viewHolder.adapterPosition) + if (direction == ItemTouchHelper.END) { + completeTask(viewHolder.adapterPosition) + } else if (direction == ItemTouchHelper.START){ + deleteTask(viewHolder.adapterPosition) + } } }) helper.attachToRecyclerView(mRecyclerView) } + private fun completeTask(position: Int) { + val completedTask = mAdapter.getTaskAtPosition(position) + val isCompletedTaskHasLastPosition = completedTask.position == mAdapter.itemCount - 1 + val alarmHelper = AlarmHelper.getInstance() + alarmHelper.removeAlarm(completedTask.timeStamp) + completedTask.taskStatus = completedTask.taskStatus.not() + mViewModel.updateTask(completedTask) + mAdapter.removeTask(position) + var isUndoClicked = false + + mSnackbar = Snackbar.make(mRecyclerView, R.string.complete_task_status, Snackbar.LENGTH_LONG) + mSnackbar?.setAction(R.string.snackbar_undo) { + completedTask.taskStatus = completedTask.taskStatus.not() + mViewModel.saveTask(completedTask) + if (completedTask.date != 0L && completedTask.date > Calendar.getInstance().timeInMillis) { + alarmHelper.setAlarm(completedTask) + } + isUndoClicked = true + + Handler().postDelayed({ + val firstCompletelyVisibleItem = (mRecyclerView.layoutManager as LinearLayoutManager).findFirstCompletelyVisibleItemPosition() + if (firstCompletelyVisibleItem != 0 && !RecyclerViewScrollListener.isShadowShown) { + setToolbarShadow(0f, 10f) + RecyclerViewScrollListener.isShadowShown = true + } + }, 100) + } + + mSnackbar?.view?.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener { + override fun onViewAttachedToWindow(view: View) { + val firstCompletelyVisibleItem = (mRecyclerView.layoutManager as LinearLayoutManager).findFirstCompletelyVisibleItemPosition() + val lastCompletelyVisibleItem = (mRecyclerView.layoutManager as LinearLayoutManager).findLastCompletelyVisibleItemPosition() + + if (firstCompletelyVisibleItem == 0 && lastCompletelyVisibleItem == mTaskList.size - 1 && RecyclerViewScrollListener.isShadowShown) { + setToolbarShadow(10f, 0f) + RecyclerViewScrollListener.isShadowShown = false + } + } + + override fun onViewDetachedFromWindow(view: View) { + if (!isUndoClicked) { + alarmHelper.removeNotification(completedTask.timeStamp, this@MainActivity) + if (!isCompletedTaskHasLastPosition) recountTaskPositions() + } + } + }) + mSnackbar?.anchorView = mFab + mSnackbar?.show() + } + private fun deleteTask(position: Int) { val deletedTask = mAdapter.getTaskAtPosition(position) val isDeletedTaskHasLastPosition = deletedTask.position == mAdapter.itemCount - 1 @@ -465,6 +519,7 @@ class MainActivity : BaseActivity() { when (item.itemId) { android.R.id.home -> startActivity(Intent(this, SettingsActivity::class.java)) R.id.action_search -> startActivity(Intent(this, SearchActivity::class.java)) + R.id.action_view_completed_tasks -> startActivity(Intent(this, CompletedTasksActivity::class.java)) } return super.onOptionsItemSelected(item) } @@ -477,6 +532,12 @@ class MainActivity : BaseActivity() { showTaskDetailsActivity(mAdapter.getTaskAtPosition(position)) } }) + + mAdapter.setTaskCompletionListener(object : RecyclerViewAdapter.TaskCompletionListener { + override fun onTaskStatusChanged(v: View, position: Int) { + completeTask(position) + } + }) } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { diff --git a/app/src/main/java/apps/jizzu/simpletodo/ui/view/SearchActivity.kt b/app/src/main/java/apps/jizzu/simpletodo/ui/view/SearchActivity.kt index 68fb330..b0661f6 100644 --- a/app/src/main/java/apps/jizzu/simpletodo/ui/view/SearchActivity.kt +++ b/app/src/main/java/apps/jizzu/simpletodo/ui/view/SearchActivity.kt @@ -20,6 +20,7 @@ import apps.jizzu.simpletodo.ui.recycler.RecyclerViewAdapter import apps.jizzu.simpletodo.ui.view.base.BaseActivity import apps.jizzu.simpletodo.utils.PreferenceHelper import apps.jizzu.simpletodo.utils.gone +import apps.jizzu.simpletodo.utils.toast import apps.jizzu.simpletodo.utils.visible import apps.jizzu.simpletodo.vm.SearchTasksViewModel import daio.io.dresscode.dressCodeStyleId @@ -58,10 +59,23 @@ class SearchActivity : BaseActivity(), SearchView.OnQueryTextListener { showTaskDetailsActivity(task) } }) + + mAdapter.setTaskCompletionListener(object : RecyclerViewAdapter.TaskCompletionListener { + override fun onTaskStatusChanged(v: View, position: Int) { + val task = mAdapter.getTaskAtPosition(position) + task.taskStatus = task.taskStatus.not() + mViewModel.updateTask(task) + if (task.taskStatus) { + toast(getString(R.string.complete_task_status)) + } else { + toast(getString(R.string.move_task_to_open)) + } + } + }) } private fun updateViewState(tasks: List) = if (tasks.isEmpty()) showEmptyView(false) - else showTaskList(tasks) + else showTaskList(tasks) private fun showEmptyView(isSearchFieldEmpty: Boolean) { mAdapter.updateData(arrayListOf()) @@ -97,7 +111,7 @@ class SearchActivity : BaseActivity(), SearchView.OnQueryTextListener { searchText.setBackgroundResource(R.drawable.search_view_background) val view: View = searchView.findViewById(androidx.appcompat.R.id.search_plate) - when(dressCodeStyleId) { + when (dressCodeStyleId) { R.style.AppTheme_Light -> { searchText.setHintTextColor(ContextCompat.getColor(this, R.color.black)) view.setBackgroundColor(ContextCompat.getColor(this, R.color.white)) diff --git a/app/src/main/java/apps/jizzu/simpletodo/ui/view/base/BaseActivity.kt b/app/src/main/java/apps/jizzu/simpletodo/ui/view/base/BaseActivity.kt index 49b1040..88af016 100644 --- a/app/src/main/java/apps/jizzu/simpletodo/ui/view/base/BaseActivity.kt +++ b/app/src/main/java/apps/jizzu/simpletodo/ui/view/base/BaseActivity.kt @@ -90,6 +90,7 @@ abstract class BaseActivity : AppCompatActivity() { putExtra("note", task.note) putExtra("position", task.position) putExtra("time_stamp", task.timeStamp) + putExtra("task_status", task.taskStatus) if (task.date != 0L) { putExtra("date", task.date) } diff --git a/app/src/main/java/apps/jizzu/simpletodo/ui/view/task/AddTaskActivity.kt b/app/src/main/java/apps/jizzu/simpletodo/ui/view/task/AddTaskActivity.kt index 70403d8..c05d6be 100644 --- a/app/src/main/java/apps/jizzu/simpletodo/ui/view/task/AddTaskActivity.kt +++ b/app/src/main/java/apps/jizzu/simpletodo/ui/view/task/AddTaskActivity.kt @@ -49,6 +49,7 @@ class AddTaskActivity : BaseTaskActivity() { } else if (task.date != 0L) { AlarmHelper.getInstance().setAlarm(task) } + task.taskStatus = false val viewModel = createViewModel() viewModel.saveTask(task) finish() diff --git a/app/src/main/java/apps/jizzu/simpletodo/ui/view/task/EditTaskActivity.kt b/app/src/main/java/apps/jizzu/simpletodo/ui/view/task/EditTaskActivity.kt index eb6d7d7..654d75e 100644 --- a/app/src/main/java/apps/jizzu/simpletodo/ui/view/task/EditTaskActivity.kt +++ b/app/src/main/java/apps/jizzu/simpletodo/ui/view/task/EditTaskActivity.kt @@ -3,6 +3,7 @@ package apps.jizzu.simpletodo.ui.view.task import android.os.Bundle import android.view.Menu import android.view.MenuItem +import android.view.View import androidx.lifecycle.ViewModelProviders import apps.jizzu.simpletodo.R import apps.jizzu.simpletodo.data.models.Task @@ -10,6 +11,7 @@ import apps.jizzu.simpletodo.service.alarm.AlarmHelper import apps.jizzu.simpletodo.ui.dialogs.DeleteTaskDialogFragment import apps.jizzu.simpletodo.ui.view.base.BaseTaskActivity import apps.jizzu.simpletodo.utils.DateAndTimeFormatter +import apps.jizzu.simpletodo.utils.gone import apps.jizzu.simpletodo.utils.toast import apps.jizzu.simpletodo.utils.visible import apps.jizzu.simpletodo.vm.EditTaskViewModel @@ -18,6 +20,7 @@ import kotlinx.android.synthetic.main.toolbar.* import java.util.* class EditTaskActivity : BaseTaskActivity() { + private var mTaskStatus: Boolean = false private var mId: Long = 0 private var mDate: Long = 0 private var mPosition: Int = 0 @@ -41,6 +44,7 @@ class EditTaskActivity : BaseTaskActivity() { mNote = intent.getStringExtra("note") mDate = intent.getLongExtra("date", 0) mPosition = intent.getIntExtra("position", 0) + mTaskStatus = intent.getBooleanExtra("task_status", false) mTimeStamp = intent.getLongExtra("time_stamp", 0) mTitleEditText.setText(mTitle) @@ -65,7 +69,7 @@ class EditTaskActivity : BaseTaskActivity() { mTitleEditText.length() == 0 -> tilTaskTitle.error = getString(R.string.error_text_input) mTitleEditText.text.toString().trim { it <= ' ' }.isEmpty() -> tilTaskTitle.error = getString(R.string.error_spaces) else -> { - val task = Task(mId, mTitleEditText.text.toString(), tvTaskNote.text.toString(), mDate, mPosition, mTimeStamp) + val task = Task(mId, mTitleEditText.text.toString(), tvTaskNote.text.toString(), mDate, mPosition, mTimeStamp, mTaskStatus) if (tvTaskReminder.length() != 0) { task.date = mCalendar.timeInMillis @@ -88,6 +92,11 @@ class EditTaskActivity : BaseTaskActivity() { } hideKeyboard(mTitleEditText) } + + mTitleEditText.isEnabled = !mTaskStatus + tvTaskNote.isEnabled = !mTaskStatus + tvTaskReminder.isEnabled = !mTaskStatus + btnTaskConfirm.visibility = if (!mTaskStatus) View.VISIBLE else View.GONE } private fun showDeleteTaskDialog(task: Task) = DeleteTaskDialogFragment(task).show(supportFragmentManager, null) @@ -109,7 +118,7 @@ class EditTaskActivity : BaseTaskActivity() { R.id.action_delete -> { hideKeyboard(mTitleEditText) - showDeleteTaskDialog(Task(mId, mTitle, mNote, mDate, mPosition, mTimeStamp)) + showDeleteTaskDialog(Task(mId, mTitle, mNote, mDate, mPosition, mTimeStamp, mTaskStatus)) } } return super.onOptionsItemSelected(item) diff --git a/app/src/main/java/apps/jizzu/simpletodo/vm/SearchTasksViewModel.kt b/app/src/main/java/apps/jizzu/simpletodo/vm/SearchTasksViewModel.kt index 397a5c6..69bc1e7 100644 --- a/app/src/main/java/apps/jizzu/simpletodo/vm/SearchTasksViewModel.kt +++ b/app/src/main/java/apps/jizzu/simpletodo/vm/SearchTasksViewModel.kt @@ -16,4 +16,6 @@ class SearchTasksViewModel(app: Application) : BaseViewModel(app) { MutableLiveData() } } + + fun updateTask(task: Task) = repository.updateTask(task) } \ No newline at end of file diff --git a/app/src/main/java/apps/jizzu/simpletodo/vm/TaskListViewModel.kt b/app/src/main/java/apps/jizzu/simpletodo/vm/TaskListViewModel.kt index 993597b..77bed8f 100644 --- a/app/src/main/java/apps/jizzu/simpletodo/vm/TaskListViewModel.kt +++ b/app/src/main/java/apps/jizzu/simpletodo/vm/TaskListViewModel.kt @@ -5,11 +5,14 @@ import apps.jizzu.simpletodo.data.models.Task import apps.jizzu.simpletodo.vm.base.BaseViewModel class TaskListViewModel(app: Application) : BaseViewModel(app) { - val liveData = repository.getAllTasksLiveData() + val allOpenTasksLiveData = repository.getAllOpenTasksLiveData() + val completedTasksLiveData = repository.getAllCompletedTasksLiveData() fun updateTaskOrder(tasks: List) = repository.updateTaskOrder(tasks) fun deleteTask(task: Task) = repository.deleteTask(task) fun saveTask(task: Task) = repository.saveTask(task) + + fun updateTask(task: Task) = repository.updateTask(task) } \ No newline at end of file diff --git a/app/src/main/res/drawable-hdpi/round_check_circle_black_24.png b/app/src/main/res/drawable-hdpi/round_check_circle_black_24.png new file mode 100755 index 0000000..1e5d330 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/round_check_circle_black_24.png differ diff --git a/app/src/main/res/drawable-mdpi/round_check_circle_black_24.png b/app/src/main/res/drawable-mdpi/round_check_circle_black_24.png new file mode 100755 index 0000000..794ab70 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/round_check_circle_black_24.png differ diff --git a/app/src/main/res/drawable-xhdpi/round_check_circle_black_24.png b/app/src/main/res/drawable-xhdpi/round_check_circle_black_24.png new file mode 100755 index 0000000..aa9ba9c Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/round_check_circle_black_24.png differ diff --git a/app/src/main/res/drawable-xxhdpi/round_check_circle_black_24.png b/app/src/main/res/drawable-xxhdpi/round_check_circle_black_24.png new file mode 100755 index 0000000..58896a0 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/round_check_circle_black_24.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/round_check_circle_black_24.png b/app/src/main/res/drawable-xxxhdpi/round_check_circle_black_24.png new file mode 100755 index 0000000..915a10a Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/round_check_circle_black_24.png differ diff --git a/app/src/main/res/drawable/drawable_completed_task.xml b/app/src/main/res/drawable/drawable_completed_task.xml new file mode 100644 index 0000000..5c660f4 --- /dev/null +++ b/app/src/main/res/drawable/drawable_completed_task.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_completed_tasks.xml b/app/src/main/res/layout/activity_completed_tasks.xml new file mode 100644 index 0000000..a650336 --- /dev/null +++ b/app/src/main/res/layout/activity_completed_tasks.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/task_item_view.xml b/app/src/main/res/layout/task_item_view.xml new file mode 100755 index 0000000..305ca5f --- /dev/null +++ b/app/src/main/res/layout/task_item_view.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/menu.xml b/app/src/main/res/menu/menu.xml index 0f432d7..06bbb74 100644 --- a/app/src/main/res/menu/menu.xml +++ b/app/src/main/res/menu/menu.xml @@ -6,7 +6,14 @@ + + \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index c80a55e..eb45724 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -1,6 +1,7 @@ S I M P L E \u0020\u0020 T O D O Pas de tâches + Tâches terminées Vous pouvez créer de nouvelles tâches \n en utilisant le bouton + Rechercher des tâches particulières Aucune tâche trouvée :( @@ -158,4 +159,6 @@ ////////////////////////////// Ajouter une nouvelle tâche Rechercher + La tâche est terminée + La tâche est déplacée vers Ouvrir \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index aa0372e..64007c3 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -1,6 +1,7 @@ Нет задач + Завершенные задачи Создайте новые задачи \n с помощью кнопки + Введите поисковый запрос Совпадения не найдены :( @@ -158,4 +159,6 @@ ////////////////////////////// Добавить задачу Поиск + Задача выполнена + Задача перемещена в Открыть \ No newline at end of file diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 69276ea..8154839 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -1,6 +1,7 @@ S I M P L E \u0020\u0020 T O D O Hiç görev yok + Tamamlanan Görevler Yeni görevleri \n + tuşunu kullanarak ekleyebilirsiniz Belirli bir görev ara Hiç görev bulunamadı :( @@ -158,4 +159,6 @@ ////////////////////////////// Yeni görev ekle Ara + Görev tamamlandı + Görev Açık konumuna taşındı \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 71caf73..2f624db 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2,6 +2,7 @@ To-Do S I M P L E \u0020\u0020 T O D O No tasks + Completed Tasks You can create new tasks \n using the + button Search to find particular tasks No tasks found :( @@ -171,4 +172,6 @@ ////////////////////////////// Add new task Search + Task is completed + Task is moved to Open \ No newline at end of file diff --git a/build.gradle b/build.gradle index f29398a..f6f2cac 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlinVersion = '1.3.31' + ext.kotlinVersion = '1.3.61' repositories { jcenter() @@ -11,8 +11,8 @@ buildscript { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" classpath "org.jetbrains.kotlin:kotlin-android-extensions:$kotlinVersion" classpath 'com.android.tools.build:gradle:3.4.0' - classpath 'com.google.gms:google-services:4.2.0' - classpath 'io.fabric.tools:gradle:1.27.1' + classpath 'com.google.gms:google-services:4.3.3' + classpath 'io.fabric.tools:gradle:1.31.0' } } @@ -36,15 +36,16 @@ ext { versionName = "1.8.7" // App dependencies - appcompatVersion = "1.0.2" - materialVersion = "1.1.0-alpha04" - androidxVersion = "1.0.0" + appcompatVersion = "1.1.0-alpha05" + materialVersion = "1.2.0-alpha03" + androidxVersion = "1.1.0" + cardViewVersion = "1.0.0" circularAnimVersion = "0.3.4" dresscodeVersion = "1.0.0" materialIntroVersion = "1.6" constraintLayoutVersion = "1.1.3" - firebaseVersion = "16.0.7" - crashlyticsVersion = "2.9.9" + firebaseVersion = "17.2.1" + crashlyticsVersion = "2.10.1" kotterKnifeVersion = "0.1.0-SNAPSHOT" roomVersion = "2.1.0-alpha02" rxJavaVersion = '2.2.3' diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 742fda0..90e5d7d 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Fri Apr 19 14:11:48 NOVT 2019 +#Wed Jan 08 15:45:34 CST 2020 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip