Implement basic offline task creation and updating

This commit is contained in:
Phillip Thelen 2018-08-28 13:45:11 +02:00
parent 9cf2b1a71b
commit 43db91293b
15 changed files with 171 additions and 11 deletions

View file

@ -2,7 +2,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.habitrpg.android.habitica"
android:versionCode="1988"
android:versionCode="1990"
android:versionName="1.5"
android:screenOrientation="portrait"
android:installLocation="auto" >

View file

@ -147,6 +147,23 @@
</LinearLayout>
</LinearLayout>
<ProgressBar
android:id="@+id/syncing_view"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_gravity="center_vertical"
android:layout_marginLeft="@dimen/spacing_small"
android:layout_marginRight="@dimen/spacing_small"
style="@style/Widget.AppCompat.ProgressBar"/>
<ImageButton
android:id="@+id/error_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:background="@color/transparent"
android:src="@drawable/ic_warning_black"
android:layout_marginLeft="@dimen/spacing_small"
android:layout_marginRight="@dimen/spacing_small"/>
<LinearLayout
android:id="@+id/checklistIndicatorWrapper"
android:layout_width="@dimen/checklist_wrapper_width"

View file

@ -143,6 +143,23 @@
</LinearLayout>
<ProgressBar
android:id="@+id/syncing_view"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_gravity="center_vertical"
android:layout_marginLeft="@dimen/spacing_small"
android:layout_marginRight="@dimen/spacing_small"
style="@style/Widget.AppCompat.ProgressBar"/>
<ImageButton
android:id="@+id/error_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:background="@color/transparent"
android:src="@drawable/ic_warning_black"
android:layout_marginLeft="@dimen/spacing_small"
android:layout_marginRight="@dimen/spacing_small"/>
<FrameLayout
android:id="@+id/btnMinusWrapper"
android:layout_width="@dimen/button_width"

View file

@ -66,6 +66,23 @@
</LinearLayout>
<ProgressBar
android:id="@+id/syncing_view"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_gravity="center_vertical"
android:layout_marginLeft="@dimen/spacing_small"
android:layout_marginRight="@dimen/spacing_small"
style="@style/Widget.AppCompat.ProgressBar"/>
<ImageButton
android:id="@+id/error_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:background="@color/transparent"
android:src="@drawable/ic_warning_black"
android:layout_marginLeft="@dimen/spacing_small"
android:layout_marginRight="@dimen/spacing_small"/>
<LinearLayout
android:id="@+id/buyButton"
android:layout_width="48dp"

View file

@ -135,6 +135,23 @@
</LinearLayout>
</LinearLayout>
<ProgressBar
android:id="@+id/syncing_view"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_gravity="center_vertical"
android:layout_marginLeft="@dimen/spacing_small"
android:layout_marginRight="@dimen/spacing_small"
style="@style/Widget.AppCompat.ProgressBar"/>
<ImageButton
android:id="@+id/error_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:background="@color/transparent"
android:src="@drawable/ic_warning_black"
android:layout_marginLeft="@dimen/spacing_small"
android:layout_marginRight="@dimen/spacing_small"/>
<LinearLayout
android:id="@+id/checklistIndicatorWrapper"
android:layout_width="@dimen/checklist_wrapper_width"

View file

@ -8,6 +8,7 @@ import com.habitrpg.android.habitica.models.tasks.TasksOrder
import com.habitrpg.android.habitica.models.user.User
import io.reactivex.Flowable
import io.reactivex.Maybe
import io.reactivex.Single
import io.realm.Realm
import io.realm.RealmResults
import java.util.*
@ -56,4 +57,5 @@ interface TaskRepository : BaseRepository {
fun updateDailiesIsDue(date: Date): Flowable<TaskList>
fun retrieveCompletedTodos(userId: String): Flowable<TaskList>
fun syncErroredTasks(): Single<List<Task>>
}

View file

@ -9,8 +9,10 @@ import com.habitrpg.android.habitica.models.responses.TaskDirection
import com.habitrpg.android.habitica.models.responses.TaskScoringResult
import com.habitrpg.android.habitica.models.tasks.*
import com.habitrpg.android.habitica.models.user.User
import com.playseeds.android.sdk.inappmessaging.Log
import io.reactivex.Flowable
import io.reactivex.Maybe
import io.reactivex.Single
import io.reactivex.functions.Consumer
import io.realm.Realm
import io.realm.RealmList
@ -162,12 +164,26 @@ class TaskRepositoryImpl(localRepository: TaskLocalRepository, apiClient: ApiCli
}
}
task.isSaving = true
task.isCreating = true
task.hasErrored = false
task.userId = userID
if (task.id == null) {
task.id = UUID.randomUUID().toString()
}
localRepository.saveTask(task)
return apiClient.createTask(task)
.map { task1 ->
task1.dateCreated = Date()
task1
}
.doOnNext { localRepository.saveTask(it) }
.doOnError {
task.hasErrored = true
task.isSaving = false
localRepository.save(task)
}
}
@Suppress("ReturnCount")
@ -178,6 +194,10 @@ class TaskRepositoryImpl(localRepository: TaskLocalRepository, apiClient: ApiCli
}
lastTaskAction = now
val id = task.id ?: return Maybe.just(task)
task.isSaving = true
task.hasErrored = false
localRepository.saveTask(task)
return localRepository.getTaskCopy(id).firstElement()
.flatMap { task1 -> apiClient.updateTask(id, task1).singleElement() }
.map { task1 ->
@ -185,6 +205,11 @@ class TaskRepositoryImpl(localRepository: TaskLocalRepository, apiClient: ApiCli
task1
}
.doOnSuccess { localRepository.saveTask(it) }
.doOnError {
task.hasErrored = true
task.isSaving = false
localRepository.save(task)
}
}
override fun deleteTask(taskId: String): Flowable<Void> {
@ -241,4 +266,17 @@ class TaskRepositoryImpl(localRepository: TaskLocalRepository, apiClient: ApiCli
return apiClient.getTasks("dailys", formatter.format(date))
.flatMapMaybe { localRepository.updateIsdue(it) }
}
override fun syncErroredTasks(): Single<List<Task>> {
return localRepository.getErroredTasks(userID).firstElement()
.flatMapPublisher { Flowable.fromIterable(it) }
.map { localRepository.getUnmanagedCopy(it) }
.flatMap {
return@flatMap if (it.isCreating) {
createTask(it)
} else {
updateTask(it).toFlowable()
}
}.toList()
}
}

View file

@ -34,4 +34,5 @@ interface TaskLocalRepository : BaseLocalRepository {
fun updateTaskPositions(taskOrder: List<String>)
fun saveCompletedTodos(userId: String, tasks: MutableCollection<Task>)
fun getErroredTasks(userID: String): Flowable<RealmResults<Task>>
}

View file

@ -203,4 +203,14 @@ class RealmTaskLocalRepository(realm: Realm) : RealmBaseLocalRepository(realm),
}
}
}
override fun getErroredTasks(userID: String): Flowable<RealmResults<Task>> {
return realm.where(Task::class.java)
.equalTo("userId", userID)
.equalTo("hasErrored", true)
.sort("position")
.findAll()
.asFlowable()
.filter { it.isLoaded }
.retry(1) }
}

View file

@ -18,6 +18,9 @@ import org.json.JSONException
import java.util.*
open class Task : RealmObject, Parcelable {
@PrimaryKey
@SerializedName("_id")
var id: String? = null
var userId: String = ""
var priority: Float = 0.0f
var text: String = ""
@ -60,9 +63,6 @@ open class Task : RealmObject, Parcelable {
var parsedText: CharSequence? = null
@Ignore
var parsedNotes: CharSequence? = null
@PrimaryKey
@SerializedName("_id")
var id: String? = null
set(value) {
field = value
repeat?.taskId = id
@ -73,6 +73,11 @@ open class Task : RealmObject, Parcelable {
var nextDue: Date? = null
var yesterDaily: Boolean? = null
//Needed for offline creating/updating
var isSaving: Boolean = false
var hasErrored: Boolean = false
var isCreating: Boolean = false
private var daysOfMonthString: String? = null
private var weeksOfMonthString: String? = null

View file

@ -7,6 +7,11 @@ import android.view.ViewGroup
import com.habitrpg.android.habitica.helpers.TaskFilterHelper
import com.habitrpg.android.habitica.models.tasks.Task
import com.habitrpg.android.habitica.ui.viewHolders.tasks.BaseTaskViewHolder
import io.reactivex.BackpressureStrategy
import io.reactivex.Flowable
import io.reactivex.Observable
import io.reactivex.functions.Action
import io.reactivex.subjects.PublishSubject
import io.realm.OrderedRealmCollection
import io.realm.OrderedRealmCollectionChangeListener
import io.realm.RealmList
@ -45,6 +50,8 @@ abstract class RealmBaseTasksRecyclerViewAdapter<VH : BaseTaskViewHolder>(privat
var data: OrderedRealmCollection<Task>? = null
private set
private var errorButtonEventsSubject = PublishSubject.create<String>()
private val isDataValid: Boolean
get() = data?.isValid ?: false
@ -78,8 +85,6 @@ abstract class RealmBaseTasksRecyclerViewAdapter<VH : BaseTaskViewHolder>(privat
fun getItem(index: Int): Task? = if (isDataValid) data?.get(index) else null
override fun updateData(data: OrderedRealmCollection<Task>?) {
if (hasAutoUpdates) {
if (isDataValid) {
@ -132,6 +137,9 @@ abstract class RealmBaseTasksRecyclerViewAdapter<VH : BaseTaskViewHolder>(privat
val item = getItem(position)
if (item != null) {
holder.bindHolder(item, position)
holder.errorButtonClicked = Action {
errorButtonEventsSubject.onNext("")
}
}
}
@ -158,4 +166,8 @@ abstract class RealmBaseTasksRecyclerViewAdapter<VH : BaseTaskViewHolder>(privat
override fun getTaskIDAt(position: Int): String {
return data?.get(position)?.id ?: ""
}
override fun getErrorButtonEvents(): Flowable<String> {
return errorButtonEventsSubject.toFlowable(BackpressureStrategy.DROP)
}
}

View file

@ -15,6 +15,10 @@ import com.habitrpg.android.habitica.models.user.User;
import com.habitrpg.android.habitica.ui.viewHolders.ShopItemViewHolder;
import com.habitrpg.android.habitica.ui.viewHolders.tasks.RewardViewHolder;
import io.reactivex.BackpressureStrategy;
import io.reactivex.Flowable;
import io.reactivex.functions.Action;
import io.reactivex.subjects.PublishSubject;
import io.realm.OrderedRealmCollection;
public class RewardsRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> implements TaskRecyclerViewAdapter {
@ -30,6 +34,8 @@ public class RewardsRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerVie
@Nullable
private User user;
private PublishSubject<String> errorButtonEvents = PublishSubject.create();
public RewardsRecyclerViewAdapter(@Nullable OrderedRealmCollection<Task> data, Context context, int layoutResource, @Nullable User user) {
this.context = context;
this.layoutResource = layoutResource;
@ -91,6 +97,11 @@ public class RewardsRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerVie
updateData(data);
}
@Override
public Flowable<String> getErrorButtonEvents() {
return errorButtonEvents.toFlowable(BackpressureStrategy.DROP);
}
@Override
public int getItemCount() {
int rewardCount = getCustomRewardCount();

View file

@ -4,6 +4,8 @@ import com.habitrpg.android.habitica.models.tasks.Task;
import org.jetbrains.annotations.Nullable;
import io.reactivex.Flowable;
import io.reactivex.Observable;
import io.realm.OrderedRealmCollection;
import io.realm.RealmResults;
@ -22,4 +24,6 @@ public interface TaskRecyclerViewAdapter {
boolean getIgnoreUpdates();
void updateUnfilteredData(@Nullable OrderedRealmCollection<Task> data);
Flowable<String> getErrorButtonEvents();
}

View file

@ -83,9 +83,13 @@ open class TaskRecyclerViewFragment : BaseFragment(), View.OnClickListener, Swip
allowReordering()
}
recyclerAdapter = adapter as TaskRecyclerViewAdapter
recyclerAdapter = adapter as? TaskRecyclerViewAdapter
recyclerView.adapter = adapter
recyclerAdapter?.errorButtonEvents?.subscribe(Consumer {
taskRepository.syncErroredTasks().subscribe(Consumer {}, RxErrorHandler.handleEmptyError())
}, RxErrorHandler.handleEmptyError())
if (this.classType != null) {
taskRepository.getTasks(this.classType ?: "", userID).firstElement().subscribe(Consumer { this.recyclerAdapter?.updateUnfilteredData(it)
this.recyclerAdapter?.filter()

View file

@ -4,10 +4,7 @@ import android.content.Context
import android.support.v7.widget.RecyclerView
import android.text.method.LinkMovementMethod
import android.view.View
import android.widget.Button
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import android.widget.*
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.events.TaskTappedEvent
import com.habitrpg.android.habitica.ui.helpers.bindView
@ -20,6 +17,7 @@ import com.habitrpg.android.habitica.ui.helpers.bindOptionalView
import com.habitrpg.android.habitica.ui.views.EllipsisTextView
import io.reactivex.Single
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.functions.Action
import io.reactivex.functions.Consumer
import io.reactivex.schedulers.Schedulers
import net.pherth.android.emoji_library.EmojiTextView
@ -29,6 +27,7 @@ abstract class BaseTaskViewHolder constructor(itemView: View) : RecyclerView.Vie
var task: Task? = null
var errorButtonClicked: Action? = null
protected var context: Context
private val titleTextView: EllipsisTextView by bindView(itemView, R.id.checkedTextView)
private val notesTextView: EllipsisTextView by bindView(itemView, R.id.notesTextView)
@ -40,6 +39,8 @@ abstract class BaseTaskViewHolder constructor(itemView: View) : RecyclerView.Vie
private val taskIconWrapper: LinearLayout? by bindView(itemView, R.id.taskIconWrapper)
private val approvalRequiredTextView: TextView? by bindView(itemView, R.id.approvalRequiredTextField)
private val expandNotesButton: Button by bindView(R.id.expand_notes_button)
private val syncingView: ProgressBar by bindView(R.id.syncing_view)
private val errorIconView: ImageButton by bindView(R.id.error_icon)
protected val taskGray: Int by bindColor(itemView.context, R.color.task_gray)
private var openTaskDisabled: Boolean = false
@ -73,6 +74,8 @@ abstract class BaseTaskViewHolder constructor(itemView: View) : RecyclerView.Vie
itemView.setOnClickListener { onClick(it) }
itemView.isClickable = true
errorIconView.setOnClickListener { errorButtonClicked?.run()}
//Re enable when we find a way to only react when a link is tapped.
//notesTextView.movementMethod = LinkMovementMethod.getInstance()
//titleTextView.movementMethod = LinkMovementMethod.getInstance()
@ -163,6 +166,8 @@ abstract class BaseTaskViewHolder constructor(itemView: View) : RecyclerView.Vie
approvalRequiredTextView?.visibility = View.GONE
}
syncingView.visibility = if (task?.isSaving == true) View.VISIBLE else View.GONE
errorIconView.visibility = if (task?.hasErrored == true) View.VISIBLE else View.GONE
}