From 5f127779aa84c34a4490040e86bd902333f76139 Mon Sep 17 00:00:00 2001 From: Gabor Keszthelyi Date: Tue, 17 Oct 2017 21:23:08 +0200 Subject: [PATCH 1/6] Show subtasks on details view. #442 --- .idea/dictionaries/dictionary.xml | 1 + opentasks/build.gradle | 7 ++ opentasks/proguard.cfg | 5 +- .../java/org/dmfs/tasks/ViewTaskFragment.java | 16 +++ .../RowDataSubtaskViewParams.java | 94 ++++++++++++++++++ .../RowDataSubtasksViewParams.java | 55 +++++++++++ .../dmfs/tasks/detailsscreen/SubtaskView.java | 96 ++++++++++++++++++ .../tasks/detailsscreen/SubtasksSource.java | 54 +++++++++++ .../tasks/detailsscreen/SubtasksView.java | 97 +++++++++++++++++++ .../ContentProviderClientDisposable.java | 50 ++++++++++ .../readdata/ContentProviderClientSource.java | 56 +++++++++++ .../java/org/dmfs/tasks/readdata/CpQuery.java | 38 ++++++++ .../dmfs/tasks/readdata/CpQuerySource.java | 52 ++++++++++ .../dmfs/tasks/readdata/TaskContentUri.java | 51 ++++++++++ .../utils/DatabaseInitializedReceiver.java | 95 +++++++++++++++--- .../utils/rxjava/DelegatingDisposable.java | 50 ++++++++++ .../tasks/utils/rxjava/DelegatingSingle.java | 45 +++++++++ .../dmfs/tasks/utils/rxjava/Offloading.java | 42 ++++++++ .../dmfs/tasks/widget/PopulateableView.java | 33 +++++++ .../tasks/widget/PopulateableViewGroup.java | 48 +++++++++ .../dmfs/tasks/widget/UpdatedSmartViews.java | 48 +++++++++ .../res/layout/fragment_task_view_detail.xml | 1 + .../layout/opentasks_view_item_divider.xml | 12 +++ ...entasks_view_item_task_details_subtask.xml | 59 +++++++++++ ..._task_details_subtitles_section_header.xml | 14 +++ opentasks/src/main/res/values/strings.xml | 4 + .../dmfs/opentaskspal/readdata/TaskUri.java | 58 ----------- 27 files changed, 1108 insertions(+), 73 deletions(-) create mode 100644 opentasks/src/main/java/org/dmfs/tasks/detailsscreen/RowDataSubtaskViewParams.java create mode 100644 opentasks/src/main/java/org/dmfs/tasks/detailsscreen/RowDataSubtasksViewParams.java create mode 100644 opentasks/src/main/java/org/dmfs/tasks/detailsscreen/SubtaskView.java create mode 100644 opentasks/src/main/java/org/dmfs/tasks/detailsscreen/SubtasksSource.java create mode 100644 opentasks/src/main/java/org/dmfs/tasks/detailsscreen/SubtasksView.java create mode 100644 opentasks/src/main/java/org/dmfs/tasks/readdata/ContentProviderClientDisposable.java create mode 100644 opentasks/src/main/java/org/dmfs/tasks/readdata/ContentProviderClientSource.java create mode 100644 opentasks/src/main/java/org/dmfs/tasks/readdata/CpQuery.java create mode 100644 opentasks/src/main/java/org/dmfs/tasks/readdata/CpQuerySource.java create mode 100644 opentasks/src/main/java/org/dmfs/tasks/readdata/TaskContentUri.java create mode 100644 opentasks/src/main/java/org/dmfs/tasks/utils/rxjava/DelegatingDisposable.java create mode 100644 opentasks/src/main/java/org/dmfs/tasks/utils/rxjava/DelegatingSingle.java create mode 100644 opentasks/src/main/java/org/dmfs/tasks/utils/rxjava/Offloading.java create mode 100644 opentasks/src/main/java/org/dmfs/tasks/widget/PopulateableView.java create mode 100644 opentasks/src/main/java/org/dmfs/tasks/widget/PopulateableViewGroup.java create mode 100644 opentasks/src/main/java/org/dmfs/tasks/widget/UpdatedSmartViews.java create mode 100644 opentasks/src/main/res/layout/opentasks_view_item_divider.xml create mode 100644 opentasks/src/main/res/layout/opentasks_view_item_task_details_subtask.xml create mode 100644 opentasks/src/main/res/layout/opentasks_view_item_task_details_subtitles_section_header.xml delete mode 100644 opentaskspal/src/main/java/org/dmfs/opentaskspal/readdata/TaskUri.java diff --git a/.idea/dictionaries/dictionary.xml b/.idea/dictionaries/dictionary.xml index 7db7138b1..4a32d902f 100644 --- a/.idea/dictionaries/dictionary.xml +++ b/.idea/dictionaries/dictionary.xml @@ -2,6 +2,7 @@ opentasks + populateable subtask subtasks diff --git a/opentasks/build.gradle b/opentasks/build.gradle index de5be1922..4081ec689 100644 --- a/opentasks/build.gradle +++ b/opentasks/build.gradle @@ -57,10 +57,14 @@ android { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } + dataBinding { + enabled = true + } } dependencies { implementation project(':opentasks-provider') + implementation project(':opentaskspal') implementation deps.support_appcompat implementation deps.support_design implementation(deps.xml_magic) { @@ -81,6 +85,9 @@ dependencies { implementation deps.datetime implementation deps.bolts_color implementation deps.retention_magic + implementation deps.contentpal + implementation 'io.reactivex.rxjava2:rxjava:2.1.5' + implementation 'io.reactivex.rxjava2:rxandroid:2.0.1' testImplementation deps.junit testImplementation deps.robolectric diff --git a/opentasks/proguard.cfg b/opentasks/proguard.cfg index bd574eba9..5115fbfda 100644 --- a/opentasks/proguard.cfg +++ b/opentasks/proguard.cfg @@ -80,4 +80,7 @@ java.lang.String TAG; @org.dmfs.android.retentionmagic.annotations.* ; private long mId; -} \ No newline at end of file +} + +-dontwarn android.databinding.** +-keep class android.databinding.** { *; } diff --git a/opentasks/src/main/java/org/dmfs/tasks/ViewTaskFragment.java b/opentasks/src/main/java/org/dmfs/tasks/ViewTaskFragment.java index 8b3539c79..e35b787e5 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/ViewTaskFragment.java +++ b/opentasks/src/main/java/org/dmfs/tasks/ViewTaskFragment.java @@ -57,6 +57,10 @@ import org.dmfs.android.retentionmagic.annotations.Parameter; import org.dmfs.android.retentionmagic.annotations.Retain; import org.dmfs.tasks.contract.TaskContract.Tasks; +import org.dmfs.tasks.detailsscreen.RowDataSubtaskViewParams; +import org.dmfs.tasks.detailsscreen.RowDataSubtasksViewParams; +import org.dmfs.tasks.detailsscreen.SubtasksSource; +import org.dmfs.tasks.detailsscreen.SubtasksView; import org.dmfs.tasks.model.ContentSet; import org.dmfs.tasks.model.Model; import org.dmfs.tasks.model.OnContentChangeListener; @@ -74,6 +78,8 @@ import java.util.HashSet; import java.util.Set; +import io.reactivex.disposables.CompositeDisposable; + /** * A fragment representing a single Task detail screen. This fragment is either contained in a {@link TaskListActivity} in two-pane mode (on tablets) or in a @@ -135,6 +141,8 @@ public class ViewTaskFragment extends SupportFragment */ private TaskView mDetailView; + private CompositeDisposable mDisposables; + private int mListColor; private int mOldStatus = -1; private boolean mPinned = false; @@ -252,12 +260,14 @@ public void onDestroyView() mDetailView.setValues(null); } + mDisposables.dispose(); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + mDisposables = new CompositeDisposable(); mShowFloatingActionButton = !getResources().getBoolean(R.bool.has_two_panes); mRootView = inflater.inflate(R.layout.fragment_task_view_detail, container, false); @@ -442,6 +452,12 @@ private void updateView() ((TextView) mToolBar.findViewById(R.id.toolbar_title)).setText(TaskFieldAdapters.TITLE.get(mContentSet)); } } + + mDisposables.add(new SubtasksSource(mAppContext, mTaskUri, RowDataSubtaskViewParams.SUBTASK_PROJECTION) + .subscribe(subtasks -> + { + new SubtasksView(mContent).update(new RowDataSubtasksViewParams(new ValueColor(mListColor), subtasks)); + })); } diff --git a/opentasks/src/main/java/org/dmfs/tasks/detailsscreen/RowDataSubtaskViewParams.java b/opentasks/src/main/java/org/dmfs/tasks/detailsscreen/RowDataSubtaskViewParams.java new file mode 100644 index 000000000..4df4afc74 --- /dev/null +++ b/opentasks/src/main/java/org/dmfs/tasks/detailsscreen/RowDataSubtaskViewParams.java @@ -0,0 +1,94 @@ +/* + * Copyright 2018 dmfs GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dmfs.tasks.detailsscreen; + +import org.dmfs.android.bolts.color.Color; +import org.dmfs.android.contentpal.Projection; +import org.dmfs.android.contentpal.RowDataSnapshot; +import org.dmfs.android.contentpal.projections.Composite; +import org.dmfs.jems.optional.Optional; +import org.dmfs.opentaskspal.readdata.EffectiveDueDate; +import org.dmfs.opentaskspal.readdata.EffectiveTaskColor; +import org.dmfs.opentaskspal.readdata.Id; +import org.dmfs.opentaskspal.readdata.PercentComplete; +import org.dmfs.opentaskspal.readdata.TaskTitle; +import org.dmfs.rfc5545.DateTime; +import org.dmfs.tasks.contract.TaskContract; + + +/** + * {@link SubtasksView.Params} that reads the data from the given {@link RowDataSnapshot}. + * + * @author Gabor Keszthelyi + */ +public final class RowDataSubtaskViewParams implements SubtaskView.Params +{ + + /** + * The projection required for this adapter to work. + */ + public static final Projection SUBTASK_PROJECTION = new Composite<>( + Id.PROJECTION, + TaskTitle.PROJECTION, + EffectiveDueDate.PROJECTION, + EffectiveTaskColor.PROJECTION, + PercentComplete.PROJECTION + ); + + private final RowDataSnapshot mRowDataSnapshot; + + + public RowDataSubtaskViewParams(RowDataSnapshot rowDataSnapshot) + { + mRowDataSnapshot = rowDataSnapshot; + } + + + @Override + public Long id() + { + return new Id(mRowDataSnapshot).value(); + } + + + @Override + public Optional title() + { + return new TaskTitle(mRowDataSnapshot); + } + + + @Override + public Optional due() + { + return new EffectiveDueDate(mRowDataSnapshot); + } + + + @Override + public Color color() + { + return new EffectiveTaskColor(mRowDataSnapshot); + } + + + @Override + public Optional percentComplete() + { + return new PercentComplete(mRowDataSnapshot); + } +} diff --git a/opentasks/src/main/java/org/dmfs/tasks/detailsscreen/RowDataSubtasksViewParams.java b/opentasks/src/main/java/org/dmfs/tasks/detailsscreen/RowDataSubtasksViewParams.java new file mode 100644 index 000000000..edb2d11a8 --- /dev/null +++ b/opentasks/src/main/java/org/dmfs/tasks/detailsscreen/RowDataSubtasksViewParams.java @@ -0,0 +1,55 @@ +/* + * Copyright 2018 dmfs GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dmfs.tasks.detailsscreen; + +import org.dmfs.android.bolts.color.Color; +import org.dmfs.android.contentpal.RowDataSnapshot; +import org.dmfs.jems.iterable.decorators.Mapped; +import org.dmfs.tasks.contract.TaskContract; + + +/** + * {@link SubtasksView.Params} that adapts the given {@link RowDataSnapshot}s (and takes the list color). + * + * @author Gabor Keszthelyi + */ +public final class RowDataSubtasksViewParams implements SubtasksView.Params +{ + private final Color mTaskListColor; + private final Iterable> mSubtaskRows; + + + public RowDataSubtasksViewParams(Color taskListColor, Iterable> subtaskRows) + { + mTaskListColor = taskListColor; + mSubtaskRows = subtaskRows; + } + + + @Override + public Color taskListColor() + { + return mTaskListColor; + } + + + @Override + public Iterable subtasks() + { + return new Mapped<>(RowDataSubtaskViewParams::new, mSubtaskRows); + } +} diff --git a/opentasks/src/main/java/org/dmfs/tasks/detailsscreen/SubtaskView.java b/opentasks/src/main/java/org/dmfs/tasks/detailsscreen/SubtaskView.java new file mode 100644 index 000000000..7e77ce9c3 --- /dev/null +++ b/opentasks/src/main/java/org/dmfs/tasks/detailsscreen/SubtaskView.java @@ -0,0 +1,96 @@ +/* + * Copyright 2017 dmfs GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dmfs.tasks.detailsscreen; + +import android.content.Context; +import android.content.Intent; +import android.util.AttributeSet; +import android.view.View; +import android.widget.FrameLayout; + +import org.dmfs.android.bolts.color.Color; +import org.dmfs.jems.optional.Optional; +import org.dmfs.jems.single.combined.Backed; +import org.dmfs.rfc5545.DateTime; +import org.dmfs.tasks.R; +import org.dmfs.tasks.databinding.OpentasksViewItemTaskDetailsSubtaskBinding; +import org.dmfs.tasks.readdata.TaskContentUri; +import org.dmfs.tasks.utils.DateFormatter; +import org.dmfs.tasks.utils.DateFormatter.DateFormatContext; +import org.dmfs.tasks.widget.ProgressBackgroundView; +import org.dmfs.tasks.widget.SmartView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.databinding.DataBindingUtil; + + +/** + * {@link View} for showing a subtask on the details screen. + * + * @author Gabor Keszthelyi + */ +public final class SubtaskView extends FrameLayout implements SmartView +{ + + public interface Params // i.e. fields of the subtask + { + Long id(); + + Optional title(); + + Optional due(); + + Color color(); + + Optional percentComplete(); + } + + + public SubtaskView(@NonNull Context context, @Nullable AttributeSet attrs) + { + super(context, attrs); + } + + + @Override + public void update(Params subtask) + { + OpentasksViewItemTaskDetailsSubtaskBinding views = DataBindingUtil.bind(this); + + views.opentasksTaskDetailsSubtaskTitle.setText( + new Backed<>(subtask.title(), getContext().getString(R.string.opentasks_task_details_subtask_untitled)).value()); + + if (subtask.due().isPresent()) + { + views.opentasksTaskDetailsSubtaskDue.setText( + new DateFormatter(getContext()).format(subtask.due().value(), DateTime.now(), DateFormatContext.LIST_VIEW)); + } + + views.opentasksTaskDetailsSubtaskListRibbon.setBackgroundColor(subtask.color().argb()); + + new ProgressBackgroundView(views.opentasksTaskDetailsSubtaskProgressBackground) + .update(subtask.percentComplete()); + + views.getRoot().setOnClickListener((v) -> + { + Context ctx = v.getContext(); + // TODO Use BasicTaskDetailsUi class when #589 is merged + ctx.startActivity(new Intent(Intent.ACTION_VIEW, new TaskContentUri(subtask.id(), ctx).value())); + }); + } +} diff --git a/opentasks/src/main/java/org/dmfs/tasks/detailsscreen/SubtasksSource.java b/opentasks/src/main/java/org/dmfs/tasks/detailsscreen/SubtasksSource.java new file mode 100644 index 000000000..822a5aa78 --- /dev/null +++ b/opentasks/src/main/java/org/dmfs/tasks/detailsscreen/SubtasksSource.java @@ -0,0 +1,54 @@ +/* + * Copyright 2017 dmfs GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dmfs.tasks.detailsscreen; + +import android.content.Context; +import android.net.Uri; + +import org.dmfs.android.contentpal.Projection; +import org.dmfs.android.contentpal.RowDataSnapshot; +import org.dmfs.android.contentpal.references.RowUriReference; +import org.dmfs.opentaskspal.rowsets.Subtasks; +import org.dmfs.opentaskspal.views.TasksView; +import org.dmfs.tasks.contract.TaskContract; +import org.dmfs.tasks.contract.TaskContract.Tasks; +import org.dmfs.tasks.readdata.CpQuerySource; +import org.dmfs.tasks.utils.rxjava.DelegatingSingle; +import org.dmfs.tasks.utils.rxjava.Offloading; + +import io.reactivex.Single; + +import static org.dmfs.provider.tasks.AuthorityUtil.taskAuthority; + + +/** + * {@link Single} to get the subtasks of a task. + * + * @author Gabor Keszthelyi + */ +public final class SubtasksSource extends DelegatingSingle>> +{ + public SubtasksSource(Context context, Uri taskUri, Projection projection) + { + super(new Offloading<>( + new CpQuerySource<>( + context.getApplicationContext(), + (client, ctx) -> new Subtasks(new TasksView(taskAuthority(context), client), projection, new RowUriReference<>(taskUri))) + ) + ); + } +} diff --git a/opentasks/src/main/java/org/dmfs/tasks/detailsscreen/SubtasksView.java b/opentasks/src/main/java/org/dmfs/tasks/detailsscreen/SubtasksView.java new file mode 100644 index 000000000..80eede147 --- /dev/null +++ b/opentasks/src/main/java/org/dmfs/tasks/detailsscreen/SubtasksView.java @@ -0,0 +1,97 @@ +/* + * Copyright 2017 dmfs GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dmfs.tasks.detailsscreen; + +import android.view.LayoutInflater; +import android.view.ViewGroup; +import android.widget.TextView; + +import org.dmfs.android.bolts.color.Color; +import org.dmfs.tasks.R; +import org.dmfs.tasks.widget.PopulateableViewGroup; +import org.dmfs.tasks.widget.SmartView; +import org.dmfs.tasks.widget.UpdatedSmartViews; + + +/** + * {@link SmartView} for the subtasks section of the task details screen. + * + * @author Gabor Keszthelyi + */ +public final class SubtasksView implements SmartView +{ + public interface Params + { + Color taskListColor(); + + Iterable subtasks(); + } + + + private final ViewGroup mContentView; + + + public SubtasksView(ViewGroup contentView) + { + mContentView = contentView; + } + + + @Override + public void update(SubtasksView.Params params) + { + if (!params.subtasks().iterator().hasNext()) + { + // Don't show the subtasks UI section if there are no subtasks + return; + } + + LayoutInflater inflater = LayoutInflater.from(mContentView.getContext()); + + inflater.inflate(R.layout.opentasks_view_item_divider, mContentView); + + TextView sectionHeader = (TextView) inflater.inflate(R.layout.opentasks_view_item_task_details_subtitles_section_header, null); + sectionHeader.setTextColor(new Darkened(params.taskListColor()).argb()); + mContentView.addView(sectionHeader); + + new PopulateableViewGroup(mContentView) + .populate(new UpdatedSmartViews<>(params.subtasks(), inflater, R.layout.opentasks_view_item_task_details_subtask)); + } + + + // TODO Remove when #522 is merged, use the version from there + private static final class Darkened implements Color + { + private final Color mOriginal; + + + private Darkened(Color original) + { + mOriginal = original; + } + + + @Override + public int argb() + { + float[] hsv = new float[3]; + android.graphics.Color.colorToHSV(mOriginal.argb(), hsv); + hsv[2] = hsv[2] * 0.75f; + return android.graphics.Color.HSVToColor(hsv); + } + } +} diff --git a/opentasks/src/main/java/org/dmfs/tasks/readdata/ContentProviderClientDisposable.java b/opentasks/src/main/java/org/dmfs/tasks/readdata/ContentProviderClientDisposable.java new file mode 100644 index 000000000..de8c9bfc7 --- /dev/null +++ b/opentasks/src/main/java/org/dmfs/tasks/readdata/ContentProviderClientDisposable.java @@ -0,0 +1,50 @@ +/* + * Copyright 2017 dmfs GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dmfs.tasks.readdata; + +import android.content.ContentProviderClient; +import android.os.Build; + +import org.dmfs.tasks.utils.rxjava.DelegatingDisposable; + +import io.reactivex.disposables.Disposable; +import io.reactivex.disposables.Disposables; + + +/** + * {@link Disposable} for {@link ContentProviderClient}. + * + * @author Gabor Keszthelyi + */ +public final class ContentProviderClientDisposable extends DelegatingDisposable +{ + public ContentProviderClientDisposable(final ContentProviderClient client) + { + super(Disposables.fromRunnable(() -> + { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + { + client.close(); + } + else + { + client.release(); + } + })); + } + +} diff --git a/opentasks/src/main/java/org/dmfs/tasks/readdata/ContentProviderClientSource.java b/opentasks/src/main/java/org/dmfs/tasks/readdata/ContentProviderClientSource.java new file mode 100644 index 000000000..26a933864 --- /dev/null +++ b/opentasks/src/main/java/org/dmfs/tasks/readdata/ContentProviderClientSource.java @@ -0,0 +1,56 @@ +/* + * Copyright 2017 dmfs GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dmfs.tasks.readdata; + +import android.content.ContentProviderClient; +import android.content.Context; +import android.net.Uri; + +import org.dmfs.provider.tasks.AuthorityUtil; +import org.dmfs.tasks.contract.TaskContract; +import org.dmfs.tasks.utils.rxjava.DelegatingSingle; + +import io.reactivex.Single; +import io.reactivex.SingleEmitter; + + +/** + * {@link Single} for accessing a {@link ContentProviderClient} for the given {@link Uri}. + * Takes care of closing the client upon disposal. + * + * @author Gabor Keszthelyi + */ +public class ContentProviderClientSource extends DelegatingSingle +{ + + public ContentProviderClientSource(Context context, Uri uri) + { + super(Single.create((SingleEmitter emitter) -> + { + ContentProviderClient client = context.getContentResolver().acquireContentProviderClient(uri); + emitter.setDisposable(new ContentProviderClientDisposable(client)); + emitter.onSuccess(client); + })); + } + + + public ContentProviderClientSource(Context context) + { + this(context, TaskContract.getContentUri(AuthorityUtil.taskAuthority(context))); + } + +} diff --git a/opentasks/src/main/java/org/dmfs/tasks/readdata/CpQuery.java b/opentasks/src/main/java/org/dmfs/tasks/readdata/CpQuery.java new file mode 100644 index 000000000..b442cd2bb --- /dev/null +++ b/opentasks/src/main/java/org/dmfs/tasks/readdata/CpQuery.java @@ -0,0 +1,38 @@ +/* + * Copyright 2017 dmfs GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dmfs.tasks.readdata; + +import android.content.ContentProviderClient; +import android.content.Context; + +import org.dmfs.android.contentpal.RowSet; + + +/** + * Represents a ContentProvider query resulting in ContentPal's {@link RowSet}. + * + * @author Gabor Keszthelyi + */ +public interface CpQuery +{ + + /** + * Returns the {@link RowSet} that represent the result of this query. + */ + RowSet rowSet(ContentProviderClient client, Context appContext); + +} diff --git a/opentasks/src/main/java/org/dmfs/tasks/readdata/CpQuerySource.java b/opentasks/src/main/java/org/dmfs/tasks/readdata/CpQuerySource.java new file mode 100644 index 000000000..bbbabbac8 --- /dev/null +++ b/opentasks/src/main/java/org/dmfs/tasks/readdata/CpQuerySource.java @@ -0,0 +1,52 @@ +/* + * Copyright 2017 dmfs GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dmfs.tasks.readdata; + +import android.content.Context; + +import org.dmfs.android.contentpal.RowDataSnapshot; +import org.dmfs.android.contentpal.RowSet; +import org.dmfs.android.contentpal.RowSnapshot; +import org.dmfs.android.contentpal.rowsets.Frozen; +import org.dmfs.iterables.decorators.Mapped; +import org.dmfs.tasks.utils.rxjava.DelegatingSingle; + +import io.reactivex.Single; + + +/** + * {@link Single} that accesses the Tasks provider, runs the given {@link CpQuery} + * and delivers the the result {@link Iterable} of {@link RowDataSnapshot}s. + * + * @author Gabor Keszthelyi + */ +public final class CpQuerySource extends DelegatingSingle>> +{ + + public CpQuerySource(Context context, CpQuery cpQuery) + { + super(new ContentProviderClientSource(context) + .map(client -> + { + RowSet frozen = new Frozen<>(cpQuery.rowSet(client, context)); + frozen.iterator(); // To actually freeze it + + return new Mapped<>(frozen, RowSnapshot::values); + })); + } + +} diff --git a/opentasks/src/main/java/org/dmfs/tasks/readdata/TaskContentUri.java b/opentasks/src/main/java/org/dmfs/tasks/readdata/TaskContentUri.java new file mode 100644 index 000000000..29ba29d07 --- /dev/null +++ b/opentasks/src/main/java/org/dmfs/tasks/readdata/TaskContentUri.java @@ -0,0 +1,51 @@ +/* + * Copyright 2017 dmfs GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dmfs.tasks.readdata; + +import android.content.ContentUris; +import android.content.Context; +import android.net.Uri; + +import org.dmfs.jems.single.Single; +import org.dmfs.provider.tasks.AuthorityUtil; +import org.dmfs.tasks.contract.TaskContract; + + +/** + * Content Uri for a given task id. + * + * @author Gabor Keszthelyi + */ +public final class TaskContentUri implements Single +{ + private final Long mTaskId; + private final Context mAppContext; + + + public TaskContentUri(Long taskId, Context context) + { + mTaskId = taskId; + mAppContext = context.getApplicationContext(); + } + + + @Override + public Uri value() + { + return ContentUris.withAppendedId(TaskContract.Tasks.getContentUri(AuthorityUtil.taskAuthority(mAppContext)), mTaskId); + } +} diff --git a/opentasks/src/main/java/org/dmfs/tasks/utils/DatabaseInitializedReceiver.java b/opentasks/src/main/java/org/dmfs/tasks/utils/DatabaseInitializedReceiver.java index 9db6b5021..5bf43e7d0 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/utils/DatabaseInitializedReceiver.java +++ b/opentasks/src/main/java/org/dmfs/tasks/utils/DatabaseInitializedReceiver.java @@ -16,16 +16,29 @@ package org.dmfs.tasks.utils; +import android.accounts.Account; import android.content.BroadcastReceiver; -import android.content.ContentValues; import android.content.Context; import android.content.Intent; +import android.content.OperationApplicationException; import android.graphics.Color; +import android.os.RemoteException; +import org.dmfs.android.bolts.color.elementary.ValueColor; +import org.dmfs.android.contentpal.operations.Insert; +import org.dmfs.android.contentpal.rowdata.Composite; +import org.dmfs.android.contentpal.tables.Synced; +import org.dmfs.android.contentpal.transactions.BaseTransaction; +import org.dmfs.iterables.SingletonIterable; +import org.dmfs.opentaskspal.tables.TaskListsTable; +import org.dmfs.opentaskspal.tasklists.ColorData; +import org.dmfs.opentaskspal.tasklists.NameData; +import org.dmfs.opentaskspal.tasklists.OwnerData; +import org.dmfs.opentaskspal.tasklists.SyncStatusData; +import org.dmfs.opentaskspal.tasklists.VisibilityData; import org.dmfs.provider.tasks.AuthorityUtil; import org.dmfs.tasks.R; import org.dmfs.tasks.contract.TaskContract; -import org.dmfs.tasks.contract.TaskContract.TaskLists; public class DatabaseInitializedReceiver extends BroadcastReceiver @@ -36,18 +49,72 @@ public void onReceive(Context context, Intent intent) if (context.getResources().getBoolean(R.bool.opentasks_support_local_lists)) { // The database was just created, insert a local task list - ContentValues listValues = new ContentValues(5); - listValues.put(TaskLists.LIST_NAME, context.getString(R.string.initial_local_task_list_name)); - listValues.put(TaskLists.LIST_COLOR, Color.rgb(30, 136, 229) /* material blue 600 */); - listValues.put(TaskLists.VISIBLE, 1); - listValues.put(TaskLists.SYNC_ENABLED, 1); - listValues.put(TaskLists.OWNER, ""); - - context.getContentResolver().insert( - TaskContract.TaskLists.getContentUri(AuthorityUtil.taskAuthority(context)).buildUpon() - .appendQueryParameter(TaskContract.CALLER_IS_SYNCADAPTER, "true") - .appendQueryParameter(TaskContract.ACCOUNT_NAME, TaskContract.LOCAL_ACCOUNT_NAME) - .appendQueryParameter(TaskContract.ACCOUNT_TYPE, TaskContract.LOCAL_ACCOUNT_TYPE).build(), listValues); + try + { + new BaseTransaction().with(new SingletonIterable<>( + new Insert<>( + // the table to insert into + new Synced<>(new Account(TaskContract.LOCAL_ACCOUNT_NAME, TaskContract.LOCAL_ACCOUNT_TYPE), + new TaskListsTable(AuthorityUtil.taskAuthority(context))), + // the data to insert + new Composite<>( + new NameData(context.getString(R.string.initial_local_task_list_name)), + new VisibilityData(true), + new OwnerData(""), + new SyncStatusData(true), + new ColorData(new ValueColor(Color.rgb(30, 136, 229))))))) + .commit(context.getContentResolver().acquireContentProviderClient(AuthorityUtil.taskAuthority(context))); + } + catch (RemoteException | OperationApplicationException e) + { + throw new Error("Unable to create initial task list. Something seems to be broken badly.", e); + } + + + /* + + Table tasklistTable = new Synced<>(new AccountScoped<>(new Account(TaskContract.LOCAL_ACCOUNT_NAME, TaskContract.LOCAL_ACCOUNT_TYPE), + new TaskListsTable(AuthorityUtil.taskAuthority(context)))); + + RowSnapshot taskList = new VirtualRowSnapshot<>(tasklistTable); + + Table taskTable = new Synced<>(new TaskListScoped(taskList, new TasksTable(AuthorityUtil.taskAuthority(context)))); + Table taskProperties = new Synced<>(new PropertiesTable(AuthorityUtil.taskAuthority(context))); + + RowSnapshot task1 = new VirtualRowSnapshot<>(taskTable); + RowSnapshot task2 = new VirtualRowSnapshot<>(taskTable); + RowSnapshot task3 = new VirtualRowSnapshot<>(taskTable); + RowSnapshot task4 = new VirtualRowSnapshot<>(taskTable); + try + { + new BaseTransaction().with(new Seq<>( + new Put<>(taskList, new Composite<>( + new NameData(context.getString(R.string.initial_local_task_list_name)), + new VisibilityData(true), + new OwnerData(""), + new SyncStatusData(true), + new ColorData(new ValueColor(Color.rgb(30, 136, 229))))), + new Put<>(task1, new Composite<>( + new TitleData("Task1"))), + new Put<>(task2, new Composite<>( + new TitleData("Task2"))), + new Put<>(task3, new Composite<>( + new TitleData("Task3"))), + new Put<>(task4, new Composite<>( + new TitleData("Task4"))), + new Insert<>(taskProperties, new RelationData(task2, TaskContract.Property.Relation.RELTYPE_PARENT, task1)), + new Insert<>(taskProperties, new RelationData(task3, TaskContract.Property.Relation.RELTYPE_PARENT, task1)), + new Insert<>(taskProperties, new RelationData(task4, TaskContract.Property.Relation.RELTYPE_PARENT, task2)) + )).commit(context.getContentResolver().acquireContentProviderClient(AuthorityUtil.taskAuthority(context))); + } + catch (RemoteException e) + { + e.printStackTrace(); + } + catch (OperationApplicationException e) + { + e.printStackTrace(); + }*/ } } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/utils/rxjava/DelegatingDisposable.java b/opentasks/src/main/java/org/dmfs/tasks/utils/rxjava/DelegatingDisposable.java new file mode 100644 index 000000000..cea7b8b07 --- /dev/null +++ b/opentasks/src/main/java/org/dmfs/tasks/utils/rxjava/DelegatingDisposable.java @@ -0,0 +1,50 @@ +/* + * Copyright 2017 dmfs GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dmfs.tasks.utils.rxjava; + +import io.reactivex.disposables.Disposable; + + +/** + * {@link Disposable} that simply delegates to the given {@link Disposable}. + * + * @author Gabor Keszthelyi + */ +public abstract class DelegatingDisposable implements Disposable +{ + private final Disposable mDelegate; + + + protected DelegatingDisposable(Disposable delegate) + { + mDelegate = delegate; + } + + + @Override + public final void dispose() + { + mDelegate.dispose(); + } + + + @Override + public final boolean isDisposed() + { + return mDelegate.isDisposed(); + } +} diff --git a/opentasks/src/main/java/org/dmfs/tasks/utils/rxjava/DelegatingSingle.java b/opentasks/src/main/java/org/dmfs/tasks/utils/rxjava/DelegatingSingle.java new file mode 100644 index 000000000..7311c28db --- /dev/null +++ b/opentasks/src/main/java/org/dmfs/tasks/utils/rxjava/DelegatingSingle.java @@ -0,0 +1,45 @@ +/* + * Copyright 2017 dmfs GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dmfs.tasks.utils.rxjava; + +import io.reactivex.Single; +import io.reactivex.SingleObserver; +import io.reactivex.SingleSource; + + +/** + * Base class for {@link Single} that delegates to a {@link SingleSource}. + * + * @author Gabor Keszthelyi + */ +public abstract class DelegatingSingle extends Single +{ + private final SingleSource mDelegate; + + + protected DelegatingSingle(SingleSource delegate) + { + mDelegate = delegate; + } + + + @Override + protected final void subscribeActual(SingleObserver observer) + { + mDelegate.subscribe(observer); + } +} diff --git a/opentasks/src/main/java/org/dmfs/tasks/utils/rxjava/Offloading.java b/opentasks/src/main/java/org/dmfs/tasks/utils/rxjava/Offloading.java new file mode 100644 index 000000000..ae079dc75 --- /dev/null +++ b/opentasks/src/main/java/org/dmfs/tasks/utils/rxjava/Offloading.java @@ -0,0 +1,42 @@ +/* + * Copyright 2017 dmfs GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dmfs.tasks.utils.rxjava; + +import io.reactivex.Single; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.schedulers.Schedulers; + + +/** + * {@link Single} decorator that sets the threading so that + * the work is on {@link Schedulers#io()} and delivery is on main thread. + *

+ * Important: it has to be applied last normally, since .observeOn(mainThread) results in + * doing everything on main thread after this decorator is applied. + * + * @author Gabor Keszthelyi + */ +public final class Offloading extends DelegatingSingle +{ + + public Offloading(Single delegate) + { + super(delegate + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread())); + } +} diff --git a/opentasks/src/main/java/org/dmfs/tasks/widget/PopulateableView.java b/opentasks/src/main/java/org/dmfs/tasks/widget/PopulateableView.java new file mode 100644 index 000000000..de72ccaea --- /dev/null +++ b/opentasks/src/main/java/org/dmfs/tasks/widget/PopulateableView.java @@ -0,0 +1,33 @@ +/* + * Copyright 2017 dmfs GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dmfs.tasks.widget; + +import android.view.View; + + +/** + * Represents a View that can be populated with other Views, i.e. views can be added to it. + * + * @author Gabor Keszthelyi + */ +public interface PopulateableView +{ + /** + * Adds the given views to this view. + */ + void populate(Iterable views); +} \ No newline at end of file diff --git a/opentasks/src/main/java/org/dmfs/tasks/widget/PopulateableViewGroup.java b/opentasks/src/main/java/org/dmfs/tasks/widget/PopulateableViewGroup.java new file mode 100644 index 000000000..8a2dfc4b9 --- /dev/null +++ b/opentasks/src/main/java/org/dmfs/tasks/widget/PopulateableViewGroup.java @@ -0,0 +1,48 @@ +/* + * Copyright 2017 dmfs GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dmfs.tasks.widget; + +import android.view.View; +import android.view.ViewGroup; + + +/** + * {@link PopulateableView} that simply adds the views to the provided {@link ViewGroup} as child views. + * + * @author Gabor Keszthelyi + */ +public final class PopulateableViewGroup implements PopulateableView +{ + private final ViewGroup mViewGroup; + + + public PopulateableViewGroup(ViewGroup viewGroup) + { + mViewGroup = viewGroup; + } + + + @Override + public void populate(Iterable views) + { + for (V view : views) + { + mViewGroup.addView(view); + } + mViewGroup.requestLayout(); + } +} diff --git a/opentasks/src/main/java/org/dmfs/tasks/widget/UpdatedSmartViews.java b/opentasks/src/main/java/org/dmfs/tasks/widget/UpdatedSmartViews.java new file mode 100644 index 000000000..ac3f2b310 --- /dev/null +++ b/opentasks/src/main/java/org/dmfs/tasks/widget/UpdatedSmartViews.java @@ -0,0 +1,48 @@ +/* + * Copyright 2017 dmfs GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dmfs.tasks.widget; + +import android.view.LayoutInflater; +import android.view.View; + +import org.dmfs.iterables.decorators.DelegatingIterable; +import org.dmfs.jems.iterable.decorators.Mapped; + +import androidx.annotation.LayoutRes; + + +/** + * {@link Iterable} of {@link SmartView}s that are updated with the corresponding data items + * from the given {@link Iterable} of D. + * + * @author Gabor Keszthelyi + */ +public final class UpdatedSmartViews> extends DelegatingIterable +{ + + public UpdatedSmartViews(Iterable dataIterable, LayoutInflater inflater, @LayoutRes int layout) + { + super(new Mapped<>((dataItem) -> + { + //noinspection unchecked + V view = (V) inflater.inflate(layout, null); + view.update(dataItem); + return view; + }, dataIterable)); + } + +} diff --git a/opentasks/src/main/res/layout/fragment_task_view_detail.xml b/opentasks/src/main/res/layout/fragment_task_view_detail.xml index 0172b8870..6fd7bde9b 100644 --- a/opentasks/src/main/res/layout/fragment_task_view_detail.xml +++ b/opentasks/src/main/res/layout/fragment_task_view_detail.xml @@ -129,6 +129,7 @@ android:layout_width="match_parent" android:background="@android:color/white" android:layout_height="wrap_content" + android:paddingBottom="8dp" android:orientation="vertical"/> diff --git a/opentasks/src/main/res/layout/opentasks_view_item_divider.xml b/opentasks/src/main/res/layout/opentasks_view_item_divider.xml new file mode 100644 index 000000000..702bbf3b5 --- /dev/null +++ b/opentasks/src/main/res/layout/opentasks_view_item_divider.xml @@ -0,0 +1,12 @@ + + + + + + + \ No newline at end of file diff --git a/opentasks/src/main/res/layout/opentasks_view_item_task_details_subtask.xml b/opentasks/src/main/res/layout/opentasks_view_item_task_details_subtask.xml new file mode 100644 index 000000000..f396b0c0c --- /dev/null +++ b/opentasks/src/main/res/layout/opentasks_view_item_task_details_subtask.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/opentasks/src/main/res/layout/opentasks_view_item_task_details_subtitles_section_header.xml b/opentasks/src/main/res/layout/opentasks_view_item_task_details_subtitles_section_header.xml new file mode 100644 index 000000000..130993c3f --- /dev/null +++ b/opentasks/src/main/res/layout/opentasks_view_item_task_details_subtitles_section_header.xml @@ -0,0 +1,14 @@ + + \ No newline at end of file diff --git a/opentasks/src/main/res/values/strings.xml b/opentasks/src/main/res/values/strings.xml index 1a4090ca3..14dc04db6 100644 --- a/opentasks/src/main/res/values/strings.xml +++ b/opentasks/src/main/res/values/strings.xml @@ -42,6 +42,10 @@ No task selected Send Send to + + Untitled + + Subtasks Text diff --git a/opentaskspal/src/main/java/org/dmfs/opentaskspal/readdata/TaskUri.java b/opentaskspal/src/main/java/org/dmfs/opentaskspal/readdata/TaskUri.java deleted file mode 100644 index 84ecb209c..000000000 --- a/opentaskspal/src/main/java/org/dmfs/opentaskspal/readdata/TaskUri.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright 2017 dmfs GmbH - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.dmfs.opentaskspal.readdata; - -import android.content.ContentUris; -import android.net.Uri; -import android.provider.BaseColumns; - -import org.dmfs.android.contentpal.Projection; -import org.dmfs.android.contentpal.RowDataSnapshot; -import org.dmfs.jems.single.Single; -import org.dmfs.tasks.contract.TaskContract; -import org.dmfs.tasks.contract.TaskContract.Tasks; - -import androidx.annotation.NonNull; - - -/** - * {@link Single} for the content {@link Uri} that refers to the given {@link RowDataSnapshot}. - * - * @author Gabor Keszthelyi - */ -public final class TaskUri implements Single -{ - public static final Projection PROJECTION = Id.PROJECTION; - - private final RowDataSnapshot mRowDataSnapshot; - private final String mAuthority; - - - public TaskUri(@NonNull String authority, @NonNull RowDataSnapshot rowDataSnapshot) - { - mAuthority = authority; - mRowDataSnapshot = rowDataSnapshot; - } - - - @Override - public Uri value() - { - // TODO: use the instance URI one we support recurrence - return ContentUris.withAppendedId(Tasks.getContentUri(mAuthority), new Id(mRowDataSnapshot).value()); - } -} From 621fd35853511ed5c094dda1e5a20a4bb41ec911 Mon Sep 17 00:00:00 2001 From: Nik-Sch Date: Tue, 7 Apr 2020 21:41:52 +0200 Subject: [PATCH 2/6] Subtasks subtasks can now be added, checked/unchecked and don't appear in ByList as normal Task --- .../dmfs/tasks/QuickAddDialogFragment.java | 27 ++++++++++ .../java/org/dmfs/tasks/ViewTaskFragment.java | 7 +-- .../RowDataSubtaskViewParams.java | 10 ++++ .../RowDataSubtasksViewParams.java | 19 ++++++- .../dmfs/tasks/detailsscreen/SubtaskView.java | 46 +++++++++++++++++ .../tasks/detailsscreen/SubtasksView.java | 40 ++++++++++++--- .../java/org/dmfs/tasks/groupings/ByList.java | 2 +- ...entasks_view_item_task_details_subtask.xml | 23 ++++++++- ..._task_details_subtitles_section_header.xml | 50 ++++++++++++++----- 9 files changed, 198 insertions(+), 26 deletions(-) diff --git a/opentasks/src/main/java/org/dmfs/tasks/QuickAddDialogFragment.java b/opentasks/src/main/java/org/dmfs/tasks/QuickAddDialogFragment.java index ac9cad2ee..f4300c470 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/QuickAddDialogFragment.java +++ b/opentasks/src/main/java/org/dmfs/tasks/QuickAddDialogFragment.java @@ -58,6 +58,8 @@ import org.dmfs.tasks.utils.SafeFragmentUiRunnable; import org.dmfs.tasks.utils.TasksListCursorSpinnerAdapter; +import java.util.TimeZone; + /** * A quick add dialog. It allows the user to enter a new task without having to deal with the full blown editor interface. At present it support task with a @@ -80,6 +82,7 @@ public class QuickAddDialogFragment extends SupportDialogFragment private final static int COMPLETION_DELAY_MAX = 1500; // ms private final static String ARG_LIST_ID = "list_id"; + private final static String ARG_PARENT_ID = "parent_id"; private final static String ARG_CONTENT = "content"; public static final String LIST_LOADER_URI = "uri"; @@ -121,6 +124,9 @@ public interface OnTextInputListener @Parameter(key = ARG_LIST_ID) private long mListId = -1; + @Parameter(key = ARG_PARENT_ID) + private long mParentId = -1; + @Parameter(key = ARG_CONTENT) private ContentSet mInitialContent; @@ -160,6 +166,22 @@ public static QuickAddDialogFragment newInstance(long listId) QuickAddDialogFragment fragment = new QuickAddDialogFragment(); Bundle args = new Bundle(); args.putLong(ARG_LIST_ID, listId); + args.putLong(ARG_PARENT_ID, -1); + fragment.setArguments(args); + return fragment; + } + + /** + * Create a {@link QuickAddDialogFragment} with the given title and initial text value. + * + * @return A new {@link QuickAddDialogFragment}. + */ + public static QuickAddDialogFragment newInstance(long listId, long parentId) + { + QuickAddDialogFragment fragment = new QuickAddDialogFragment(); + Bundle args = new Bundle(); + args.putLong(ARG_LIST_ID, listId); + args.putLong(ARG_PARENT_ID, parentId); fragment.setArguments(args); return fragment; } @@ -176,6 +198,7 @@ public static QuickAddDialogFragment newInstance(ContentSet content) Bundle args = new Bundle(); args.putParcelable(ARG_CONTENT, content); args.putLong(ARG_LIST_ID, -1); + args.putLong(ARG_PARENT_ID, -1); fragment.setArguments(args); return fragment; } @@ -375,7 +398,11 @@ private ContentSet buildContentSet() task = new ContentSet(Tasks.getContentUri(mAuthority)); } task.put(Tasks.LIST_ID, mListSpinner.getSelectedItemId()); + if (mParentId != -1) { + task.put(Tasks.PARENT_ID, mParentId); + } TaskFieldAdapters.TITLE.set(task, mEditText.getText().toString()); + TaskFieldAdapters.TIMEZONE.set(task, TimeZone.getDefault()); return task; } diff --git a/opentasks/src/main/java/org/dmfs/tasks/ViewTaskFragment.java b/opentasks/src/main/java/org/dmfs/tasks/ViewTaskFragment.java index e35b787e5..36f25123c 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/ViewTaskFragment.java +++ b/opentasks/src/main/java/org/dmfs/tasks/ViewTaskFragment.java @@ -453,11 +453,11 @@ private void updateView() } } + Long listId = mContentSet.getAsLong(Tasks.LIST_ID); + Long parentId = mContentSet.getAsLong(Tasks._ID); mDisposables.add(new SubtasksSource(mAppContext, mTaskUri, RowDataSubtaskViewParams.SUBTASK_PROJECTION) .subscribe(subtasks -> - { - new SubtasksView(mContent).update(new RowDataSubtasksViewParams(new ValueColor(mListColor), subtasks)); - })); + new SubtasksView(mContent).update(new RowDataSubtasksViewParams(new ValueColor(mListColor), listId, parentId, subtasks)))); } @@ -582,6 +582,7 @@ public void onClick(DialogInterface dialog, int which) if (mContentSet != null) { // TODO: remove the task in a background task + // TODO: subtask: remove subtasks mContentSet.delete(mAppContext); mCallback.onTaskDeleted(mTaskUri); mTaskUri = null; diff --git a/opentasks/src/main/java/org/dmfs/tasks/detailsscreen/RowDataSubtaskViewParams.java b/opentasks/src/main/java/org/dmfs/tasks/detailsscreen/RowDataSubtaskViewParams.java index 4df4afc74..6cce03965 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/detailsscreen/RowDataSubtaskViewParams.java +++ b/opentasks/src/main/java/org/dmfs/tasks/detailsscreen/RowDataSubtaskViewParams.java @@ -25,10 +25,13 @@ import org.dmfs.opentaskspal.readdata.EffectiveTaskColor; import org.dmfs.opentaskspal.readdata.Id; import org.dmfs.opentaskspal.readdata.PercentComplete; +import org.dmfs.opentaskspal.readdata.TaskCompletionTime; import org.dmfs.opentaskspal.readdata.TaskTitle; import org.dmfs.rfc5545.DateTime; import org.dmfs.tasks.contract.TaskContract; +import java.util.Date; + /** * {@link SubtasksView.Params} that reads the data from the given {@link RowDataSnapshot}. @@ -86,6 +89,13 @@ public Color color() } + @Override + public Optional completionTime() + { + return new TaskCompletionTime(mRowDataSnapshot); + } + + @Override public Optional percentComplete() { diff --git a/opentasks/src/main/java/org/dmfs/tasks/detailsscreen/RowDataSubtasksViewParams.java b/opentasks/src/main/java/org/dmfs/tasks/detailsscreen/RowDataSubtasksViewParams.java index edb2d11a8..f43427a7f 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/detailsscreen/RowDataSubtasksViewParams.java +++ b/opentasks/src/main/java/org/dmfs/tasks/detailsscreen/RowDataSubtasksViewParams.java @@ -30,12 +30,16 @@ public final class RowDataSubtasksViewParams implements SubtasksView.Params { private final Color mTaskListColor; + private final Long mListId; + private final Long mParentId; private final Iterable> mSubtaskRows; - public RowDataSubtasksViewParams(Color taskListColor, Iterable> subtaskRows) + public RowDataSubtasksViewParams(Color taskListColor, Long listId, Long parentId, Iterable> subtaskRows) { mTaskListColor = taskListColor; + mListId = listId; + mParentId = parentId; mSubtaskRows = subtaskRows; } @@ -47,6 +51,19 @@ public Color taskListColor() } + @Override + public Long listId() + { + return mListId; + } + + @Override + public Long parentId() + { + return mParentId; + } + + @Override public Iterable subtasks() { diff --git a/opentasks/src/main/java/org/dmfs/tasks/detailsscreen/SubtaskView.java b/opentasks/src/main/java/org/dmfs/tasks/detailsscreen/SubtaskView.java index 7e77ce9c3..87397931c 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/detailsscreen/SubtaskView.java +++ b/opentasks/src/main/java/org/dmfs/tasks/detailsscreen/SubtaskView.java @@ -16,17 +16,25 @@ package org.dmfs.tasks.detailsscreen; +import android.content.ContentUris; +import android.content.ContentValues; import android.content.Context; import android.content.Intent; +import android.content.res.ColorStateList; +import android.net.Uri; +import android.os.Build; import android.util.AttributeSet; +import android.util.Log; import android.view.View; import android.widget.FrameLayout; import org.dmfs.android.bolts.color.Color; import org.dmfs.jems.optional.Optional; import org.dmfs.jems.single.combined.Backed; +import org.dmfs.provider.tasks.AuthorityUtil; import org.dmfs.rfc5545.DateTime; import org.dmfs.tasks.R; +import org.dmfs.tasks.contract.TaskContract; import org.dmfs.tasks.databinding.OpentasksViewItemTaskDetailsSubtaskBinding; import org.dmfs.tasks.readdata.TaskContentUri; import org.dmfs.tasks.utils.DateFormatter; @@ -57,6 +65,8 @@ public interface Params // i.e. fields of the subtask Color color(); + Optional completionTime(); + Optional percentComplete(); } @@ -83,6 +93,42 @@ public void update(Params subtask) views.opentasksTaskDetailsSubtaskListRibbon.setBackgroundColor(subtask.color().argb()); + // checkbox color + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + { + ColorStateList colorStateList = new ColorStateList( + new int[][] { + + new int[] { -subtask.color().argb() }, //disabled + new int[] { subtask.color().argb() } //enabled + }, + new int[] { + -subtask.color().argb(), //disabled + subtask.color().argb() //enabled + } + ); + views.opentasksTaskDetailsSubtaskCheckbox.setButtonTintList(colorStateList); + } + views.opentasksTaskDetailsSubtaskCheckbox.setTextColor(subtask.color().argb()); + if (subtask.completionTime().isPresent()) + { + views.opentasksTaskDetailsSubtaskDue.setText( + new DateFormatter(getContext()).format(subtask.completionTime().value(), DateTime.now(), DateFormatContext.LIST_VIEW)); + } + + views.opentasksTaskDetailsSubtaskCheckbox.setChecked(subtask.percentComplete().isPresent() && subtask.percentComplete().value() == 100); + views.opentasksTaskDetailsSubtaskCheckbox.setOnCheckedChangeListener((buttonView, completedValue) -> { + ContentValues values = new ContentValues(); + values.put(TaskContract.Tasks.STATUS, completedValue ? TaskContract.Tasks.STATUS_COMPLETED : TaskContract.Tasks.STATUS_IN_PROCESS); + if (!completedValue) + { + values.put(TaskContract.Tasks.PERCENT_COMPLETE, 0); + } + Context ctx = buttonView.getContext(); + Uri taskUri = ContentUris.withAppendedId(TaskContract.Instances.getContentUri(AuthorityUtil.taskAuthority(ctx)), subtask.id()); + boolean completed = ctx.getApplicationContext().getContentResolver().update(taskUri, values, null, null) != 0; + }); + new ProgressBackgroundView(views.opentasksTaskDetailsSubtaskProgressBackground) .update(subtask.percentComplete()); diff --git a/opentasks/src/main/java/org/dmfs/tasks/detailsscreen/SubtasksView.java b/opentasks/src/main/java/org/dmfs/tasks/detailsscreen/SubtasksView.java index 80eede147..a66522dc9 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/detailsscreen/SubtasksView.java +++ b/opentasks/src/main/java/org/dmfs/tasks/detailsscreen/SubtasksView.java @@ -16,16 +16,27 @@ package org.dmfs.tasks.detailsscreen; +import android.app.Activity; +import android.content.Context; +import android.content.ContextWrapper; import android.view.LayoutInflater; +import android.view.View; import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.RelativeLayout; import android.widget.TextView; import org.dmfs.android.bolts.color.Color; +import org.dmfs.tasks.QuickAddDialogFragment; import org.dmfs.tasks.R; +import org.dmfs.tasks.contract.TaskContract; +import org.dmfs.tasks.utils.BaseActivity; import org.dmfs.tasks.widget.PopulateableViewGroup; import org.dmfs.tasks.widget.SmartView; import org.dmfs.tasks.widget.UpdatedSmartViews; +import androidx.fragment.app.FragmentActivity; + /** * {@link SmartView} for the subtasks section of the task details screen. @@ -38,35 +49,48 @@ public interface Params { Color taskListColor(); + Long listId(); + + Long parentId(); + Iterable subtasks(); } private final ViewGroup mContentView; - public SubtasksView(ViewGroup contentView) { mContentView = contentView; } + private BaseActivity getActivity(Context context) { + while (context instanceof ContextWrapper) { + if (context instanceof BaseActivity) { + return (BaseActivity) context; + } + context = ((ContextWrapper)context).getBaseContext(); + } + return null; + } @Override public void update(SubtasksView.Params params) { - if (!params.subtasks().iterator().hasNext()) - { - // Don't show the subtasks UI section if there are no subtasks - return; - } LayoutInflater inflater = LayoutInflater.from(mContentView.getContext()); inflater.inflate(R.layout.opentasks_view_item_divider, mContentView); - TextView sectionHeader = (TextView) inflater.inflate(R.layout.opentasks_view_item_task_details_subtitles_section_header, null); + RelativeLayout headerLayout = (RelativeLayout) inflater.inflate(R.layout.opentasks_view_item_task_details_subtitles_section_header, null); + TextView sectionHeader = headerLayout.findViewById(R.id.opentasks_view_item_task_details_subtitles_section_header); sectionHeader.setTextColor(new Darkened(params.taskListColor()).argb()); - mContentView.addView(sectionHeader); + ImageView quickAddTask = headerLayout.findViewById(R.id.opentasks_view_item_task_details_subtitles_section_header_quick_add); + quickAddTask.setOnClickListener(v -> { + QuickAddDialogFragment.newInstance(params.listId(), params.parentId()) + .show(getActivity(v.getContext()).getSupportFragmentManager(), null); + }); + mContentView.addView(headerLayout); new PopulateableViewGroup(mContentView) .populate(new UpdatedSmartViews<>(params.subtasks(), inflater, R.layout.opentasks_view_item_task_details_subtask)); diff --git a/opentasks/src/main/java/org/dmfs/tasks/groupings/ByList.java b/opentasks/src/main/java/org/dmfs/tasks/groupings/ByList.java index abc06b011..6f438e5df 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/groupings/ByList.java +++ b/opentasks/src/main/java/org/dmfs/tasks/groupings/ByList.java @@ -326,7 +326,7 @@ public ByList(String authority, FragmentActivity activity) @Override public ExpandableChildDescriptor makeExpandableChildDescriptor(String authority) { - return new ExpandableChildDescriptor(Instances.getContentUri(authority), INSTANCE_PROJECTION, Instances.VISIBLE + "=1 and " + Instances.LIST_ID + "=?", + return new ExpandableChildDescriptor(Instances.getContentUri(authority), INSTANCE_PROJECTION, Instances.VISIBLE + "=1 and " + Instances.LIST_ID + "=? and " + Instances.PARENT_ID + " is null", Instances.INSTANCE_DUE_SORTING + " is null, " + Instances.INSTANCE_DUE_SORTING + ", " + Instances.TITLE + " COLLATE NOCASE ASC", 0) .setViewDescriptor(TASK_VIEW_DESCRIPTOR); } diff --git a/opentasks/src/main/res/layout/opentasks_view_item_task_details_subtask.xml b/opentasks/src/main/res/layout/opentasks_view_item_task_details_subtask.xml index f396b0c0c..45aba646b 100644 --- a/opentasks/src/main/res/layout/opentasks_view_item_task_details_subtask.xml +++ b/opentasks/src/main/res/layout/opentasks_view_item_task_details_subtask.xml @@ -25,9 +25,18 @@ android:paddingRight="4dp" android:layout_height="match_parent"/> + + + diff --git a/opentasks/src/main/res/layout/opentasks_view_item_task_details_subtitles_section_header.xml b/opentasks/src/main/res/layout/opentasks_view_item_task_details_subtitles_section_header.xml index 130993c3f..c51f3a64a 100644 --- a/opentasks/src/main/res/layout/opentasks_view_item_task_details_subtitles_section_header.xml +++ b/opentasks/src/main/res/layout/opentasks_view_item_task_details_subtitles_section_header.xml @@ -1,14 +1,40 @@ - \ No newline at end of file + android:layout_height="match_parent"> + + + + + + + + + + + \ No newline at end of file From 6aedd93b8ac16c9ef20c740cdd9669f2dce6f045 Mon Sep 17 00:00:00 2001 From: Nik-Sch Date: Tue, 7 Apr 2020 22:16:54 +0200 Subject: [PATCH 3/6] remove subtasks --- .../java/org/dmfs/tasks/ViewTaskFragment.java | 15 ++++++++++++++- .../org/dmfs/tasks/detailsscreen/SubtaskView.java | 7 +++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/opentasks/src/main/java/org/dmfs/tasks/ViewTaskFragment.java b/opentasks/src/main/java/org/dmfs/tasks/ViewTaskFragment.java index 36f25123c..7cc1c5c6b 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/ViewTaskFragment.java +++ b/opentasks/src/main/java/org/dmfs/tasks/ViewTaskFragment.java @@ -53,9 +53,11 @@ import org.dmfs.android.bolts.color.Color; import org.dmfs.android.bolts.color.elementary.ValueColor; +import org.dmfs.android.contentpal.RowDataSnapshot; import org.dmfs.android.retentionmagic.SupportFragment; import org.dmfs.android.retentionmagic.annotations.Parameter; import org.dmfs.android.retentionmagic.annotations.Retain; +import org.dmfs.opentaskspal.readdata.Id; import org.dmfs.tasks.contract.TaskContract.Tasks; import org.dmfs.tasks.detailsscreen.RowDataSubtaskViewParams; import org.dmfs.tasks.detailsscreen.RowDataSubtasksViewParams; @@ -67,6 +69,7 @@ import org.dmfs.tasks.model.Sources; import org.dmfs.tasks.model.TaskFieldAdapters; import org.dmfs.tasks.notification.ActionService; +import org.dmfs.tasks.readdata.TaskContentUri; import org.dmfs.tasks.share.ShareIntentFactory; import org.dmfs.tasks.utils.ContentValueMapper; import org.dmfs.tasks.utils.OnModelLoadedListener; @@ -79,6 +82,7 @@ import java.util.Set; import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.disposables.Disposable; /** @@ -582,7 +586,16 @@ public void onClick(DialogInterface dialog, int which) if (mContentSet != null) { // TODO: remove the task in a background task - // TODO: subtask: remove subtasks + // delete subtasks + mDisposables.add(new SubtasksSource(mAppContext, mTaskUri, RowDataSubtaskViewParams.SUBTASK_PROJECTION).subscribe(subtasks -> + { + for (RowDataSnapshot x : subtasks) { + Long subtaskId = new Id(x).value(); + ContentSet contentSet = new ContentSet(new TaskContentUri(subtaskId, mAppContext).value()); + contentSet.delete(mAppContext); + } + })); + mContentSet.delete(mAppContext); mCallback.onTaskDeleted(mTaskUri); mTaskUri = null; diff --git a/opentasks/src/main/java/org/dmfs/tasks/detailsscreen/SubtaskView.java b/opentasks/src/main/java/org/dmfs/tasks/detailsscreen/SubtaskView.java index 87397931c..97306262f 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/detailsscreen/SubtaskView.java +++ b/opentasks/src/main/java/org/dmfs/tasks/detailsscreen/SubtaskView.java @@ -36,6 +36,7 @@ import org.dmfs.tasks.R; import org.dmfs.tasks.contract.TaskContract; import org.dmfs.tasks.databinding.OpentasksViewItemTaskDetailsSubtaskBinding; +import org.dmfs.tasks.model.ContentSet; import org.dmfs.tasks.readdata.TaskContentUri; import org.dmfs.tasks.utils.DateFormatter; import org.dmfs.tasks.utils.DateFormatter.DateFormatContext; @@ -129,6 +130,12 @@ public void update(Params subtask) boolean completed = ctx.getApplicationContext().getContentResolver().update(taskUri, values, null, null) != 0; }); + views.opentasksTaskDetailsSubtaskQuickRemove.setOnClickListener(v -> { + Context ctx = v.getContext(); + ContentSet contentSet = new ContentSet(new TaskContentUri(subtask.id(), ctx).value()); + contentSet.delete(ctx); + }); + new ProgressBackgroundView(views.opentasksTaskDetailsSubtaskProgressBackground) .update(subtask.percentComplete()); From c950f34b7125b2c55a4b1097edd0e0e8ace10f61 Mon Sep 17 00:00:00 2001 From: Nik-Sch Date: Tue, 7 Apr 2020 22:49:32 +0200 Subject: [PATCH 4/6] sorting subtasks --- .../rowsets/QueryRowSetSorting.java | 193 ++++++++++++++++++ .../dmfs/opentaskspal/rowsets/Subtasks.java | 5 +- 2 files changed, 197 insertions(+), 1 deletion(-) create mode 100644 opentaskspal/src/main/java/org/dmfs/opentaskspal/rowsets/QueryRowSetSorting.java diff --git a/opentaskspal/src/main/java/org/dmfs/opentaskspal/rowsets/QueryRowSetSorting.java b/opentaskspal/src/main/java/org/dmfs/opentaskspal/rowsets/QueryRowSetSorting.java new file mode 100644 index 000000000..4682bfdb7 --- /dev/null +++ b/opentaskspal/src/main/java/org/dmfs/opentaskspal/rowsets/QueryRowSetSorting.java @@ -0,0 +1,193 @@ +/* + * Copyright 2020 dmfs GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dmfs.opentaskspal.rowsets; + + +import android.database.Cursor; +import android.os.RemoteException; + +import org.dmfs.android.contentpal.ClosableIterator; +import org.dmfs.android.contentpal.Predicate; +import org.dmfs.android.contentpal.Projection; +import org.dmfs.android.contentpal.RowSet; +import org.dmfs.android.contentpal.RowSnapshot; +import org.dmfs.android.contentpal.Table; +import org.dmfs.android.contentpal.View; +import org.dmfs.android.contentpal.rowdatasnapshots.MapRowDataSnapshot; +import org.dmfs.android.contentpal.rowsnapshots.ValuesRowSnapshot; +import org.dmfs.android.contentpal.tools.ClosableEmptyIterator; +import org.dmfs.android.contentpal.tools.uriparams.EmptyUriParams; +import org.dmfs.android.contentpal.transactions.contexts.EmptyTransactionContext; +import org.dmfs.iterators.AbstractBaseIterator; +import org.dmfs.jems.optional.Optional; + +import java.util.HashMap; +import java.util.Map; +import java.util.NoSuchElementException; + +import androidx.annotation.NonNull; + +import static org.dmfs.jems.optional.elementary.Absent.absent; + + +/** + * A base {@link RowSet} which returns a {@link RowSnapshot} for each row of the given {@link Table} that matches the given {@link Predicate}. + * + * @author Marten Gajda + */ +public final class QueryRowSetSorting implements RowSet +{ + private final View mView; + private final Projection mProjection; + private final Predicate mPredicate; + private Optional mSorting; + + + public QueryRowSetSorting(@NonNull View view, @NonNull Projection projection, @NonNull Predicate predicate, Optional sorting) + { + mView = view; + mProjection = projection; + mPredicate = predicate; + mSorting = sorting; + } + + + @NonNull + @Override + public ClosableIterator> iterator() + { + try + { + Cursor cursor = mView.rows(EmptyUriParams.INSTANCE, mProjection, mPredicate, mSorting); + if (!cursor.moveToFirst()) + { + cursor.close(); + return ClosableEmptyIterator.instance(); + } + return new QueryRowSetSorting.RowIterator<>(cursor, mView.table()); + } + catch (RemoteException e) + { + throw new RuntimeException( + String.format("Unable to execute query on view \"%s\" with selection \"%s\"", + mView.toString(), + mPredicate.selection(EmptyTransactionContext.INSTANCE).toString()), e); + } + } + + + private final static class RowIterator extends AbstractBaseIterator> implements ClosableIterator> + { + private final Cursor mCursor; + private final Table mTable; + + + private RowIterator(@NonNull Cursor cursor, @NonNull Table table) + { + mCursor = cursor; + mTable = table; + } + + + @Override + public boolean hasNext() + { + if (mCursor.isClosed()) + { + return false; + } + try + { + if (mCursor.isAfterLast()) + { + mCursor.close(); + return false; + } + } + catch (Exception e) + { + // we can't use finally here, because we want to close the cursor only in an error case. + mCursor.close(); + throw e; + } + + return true; + } + + + @NonNull + @Override + public RowSnapshot next() + { + try + { + if (!hasNext()) + { + throw new NoSuchElementException("No more rows to iterate"); + } + Map charData = new HashMap<>(); + Map byteData = new HashMap<>(); + String[] columnNames = mCursor.getColumnNames(); + for (int i = 0, count = mCursor.getColumnCount(); i < count; ++i) + { + String columnName = columnNames[i]; + if (mCursor.getType(i) == Cursor.FIELD_TYPE_BLOB) + { + byteData.put(columnName, mCursor.getBlob(i)); + } + else + { + charData.put(columnName, mCursor.getString(i)); + } + } + RowSnapshot rowSnapshot = new ValuesRowSnapshot<>(mTable, new MapRowDataSnapshot<>(charData, byteData)); + mCursor.moveToNext(); + return rowSnapshot; + } + catch (Exception e) + { + // we can't use finally here, because we want to close the cursor only in an error case. + mCursor.close(); + throw e; + } + } + + + @Override + protected void finalize() throws Throwable + { + // Note this only serves as the very last resort. Normally the cursor is closed when the last item is iterated or when close() is called. + // However if there is a crash during this the cursor might not be closed properly, hence we try that here. + if (!mCursor.isClosed()) + { + mCursor.close(); + } + super.finalize(); + } + + + @Override + public void close() + { + if (!mCursor.isClosed()) + { + mCursor.close(); + } + } + } + +} diff --git a/opentaskspal/src/main/java/org/dmfs/opentaskspal/rowsets/Subtasks.java b/opentaskspal/src/main/java/org/dmfs/opentaskspal/rowsets/Subtasks.java index 530a47db6..b76abfa7a 100644 --- a/opentaskspal/src/main/java/org/dmfs/opentaskspal/rowsets/Subtasks.java +++ b/opentaskspal/src/main/java/org/dmfs/opentaskspal/rowsets/Subtasks.java @@ -23,6 +23,8 @@ import org.dmfs.android.contentpal.predicates.ReferringTo; import org.dmfs.android.contentpal.rowsets.DelegatingRowSet; import org.dmfs.android.contentpal.rowsets.QueryRowSet; +import org.dmfs.jems.optional.Optional; +import org.dmfs.jems.optional.elementary.Present; import org.dmfs.tasks.contract.TaskContract.Tasks; import androidx.annotation.NonNull; @@ -36,11 +38,12 @@ public final class Subtasks extends DelegatingRowSet { + @SuppressWarnings("unchecked") public Subtasks(@NonNull View view, @NonNull Projection projection, @NonNull RowReference parentTask) { - super(new QueryRowSet<>(view, projection, new ReferringTo<>(Tasks.PARENT_ID, parentTask))); + super(new QueryRowSetSorting<>(view, projection, new ReferringTo<>(Tasks.PARENT_ID, parentTask), new Present(Tasks.STATUS + ", " + Tasks.TITLE))); } } From 8099bc67f29c5ee8f8b685b463a09af01d24846d Mon Sep 17 00:00:00 2001 From: Nik-Sch Date: Tue, 7 Apr 2020 23:30:52 +0200 Subject: [PATCH 5/6] refinements of quickAddDialog --- .../src/main/java/org/dmfs/tasks/QuickAddDialogFragment.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/opentasks/src/main/java/org/dmfs/tasks/QuickAddDialogFragment.java b/opentasks/src/main/java/org/dmfs/tasks/QuickAddDialogFragment.java index f4300c470..a7044364d 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/QuickAddDialogFragment.java +++ b/opentasks/src/main/java/org/dmfs/tasks/QuickAddDialogFragment.java @@ -79,7 +79,7 @@ public class QuickAddDialogFragment extends SupportDialogFragment /** * The maximum time to add for the first time the "Task completed" info is shown. */ - private final static int COMPLETION_DELAY_MAX = 1500; // ms + private final static int COMPLETION_DELAY_MAX = 1000; // ms private final static String ARG_LIST_ID = "list_id"; private final static String ARG_PARENT_ID = "parent_id"; @@ -323,7 +323,7 @@ public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { if (EditorInfo.IME_ACTION_DONE == actionId) { - notifyUser(true /* close afterwards */); + notifyUser(mParentId == -1 /* close if no parent */); createTask(); return true; } From 9c029fc7ee18632f36391c19ff4db787e08c3912 Mon Sep 17 00:00:00 2001 From: Nik-Sch Date: Sun, 12 Apr 2020 13:17:15 +0200 Subject: [PATCH 6/6] included suggestions and rebased --- .../dmfs/tasks/QuickAddDialogFragment.java | 2 +- .../rowsets/QueryRowSetSorting.java | 193 ------------------ .../dmfs/opentaskspal/rowsets/Subtasks.java | 8 +- 3 files changed, 7 insertions(+), 196 deletions(-) delete mode 100644 opentaskspal/src/main/java/org/dmfs/opentaskspal/rowsets/QueryRowSetSorting.java diff --git a/opentasks/src/main/java/org/dmfs/tasks/QuickAddDialogFragment.java b/opentasks/src/main/java/org/dmfs/tasks/QuickAddDialogFragment.java index a7044364d..869c88f6e 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/QuickAddDialogFragment.java +++ b/opentasks/src/main/java/org/dmfs/tasks/QuickAddDialogFragment.java @@ -79,7 +79,7 @@ public class QuickAddDialogFragment extends SupportDialogFragment /** * The maximum time to add for the first time the "Task completed" info is shown. */ - private final static int COMPLETION_DELAY_MAX = 1000; // ms + private final static int COMPLETION_DELAY_MAX = 1500; // ms private final static String ARG_LIST_ID = "list_id"; private final static String ARG_PARENT_ID = "parent_id"; diff --git a/opentaskspal/src/main/java/org/dmfs/opentaskspal/rowsets/QueryRowSetSorting.java b/opentaskspal/src/main/java/org/dmfs/opentaskspal/rowsets/QueryRowSetSorting.java deleted file mode 100644 index 4682bfdb7..000000000 --- a/opentaskspal/src/main/java/org/dmfs/opentaskspal/rowsets/QueryRowSetSorting.java +++ /dev/null @@ -1,193 +0,0 @@ -/* - * Copyright 2020 dmfs GmbH - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.dmfs.opentaskspal.rowsets; - - -import android.database.Cursor; -import android.os.RemoteException; - -import org.dmfs.android.contentpal.ClosableIterator; -import org.dmfs.android.contentpal.Predicate; -import org.dmfs.android.contentpal.Projection; -import org.dmfs.android.contentpal.RowSet; -import org.dmfs.android.contentpal.RowSnapshot; -import org.dmfs.android.contentpal.Table; -import org.dmfs.android.contentpal.View; -import org.dmfs.android.contentpal.rowdatasnapshots.MapRowDataSnapshot; -import org.dmfs.android.contentpal.rowsnapshots.ValuesRowSnapshot; -import org.dmfs.android.contentpal.tools.ClosableEmptyIterator; -import org.dmfs.android.contentpal.tools.uriparams.EmptyUriParams; -import org.dmfs.android.contentpal.transactions.contexts.EmptyTransactionContext; -import org.dmfs.iterators.AbstractBaseIterator; -import org.dmfs.jems.optional.Optional; - -import java.util.HashMap; -import java.util.Map; -import java.util.NoSuchElementException; - -import androidx.annotation.NonNull; - -import static org.dmfs.jems.optional.elementary.Absent.absent; - - -/** - * A base {@link RowSet} which returns a {@link RowSnapshot} for each row of the given {@link Table} that matches the given {@link Predicate}. - * - * @author Marten Gajda - */ -public final class QueryRowSetSorting implements RowSet -{ - private final View mView; - private final Projection mProjection; - private final Predicate mPredicate; - private Optional mSorting; - - - public QueryRowSetSorting(@NonNull View view, @NonNull Projection projection, @NonNull Predicate predicate, Optional sorting) - { - mView = view; - mProjection = projection; - mPredicate = predicate; - mSorting = sorting; - } - - - @NonNull - @Override - public ClosableIterator> iterator() - { - try - { - Cursor cursor = mView.rows(EmptyUriParams.INSTANCE, mProjection, mPredicate, mSorting); - if (!cursor.moveToFirst()) - { - cursor.close(); - return ClosableEmptyIterator.instance(); - } - return new QueryRowSetSorting.RowIterator<>(cursor, mView.table()); - } - catch (RemoteException e) - { - throw new RuntimeException( - String.format("Unable to execute query on view \"%s\" with selection \"%s\"", - mView.toString(), - mPredicate.selection(EmptyTransactionContext.INSTANCE).toString()), e); - } - } - - - private final static class RowIterator extends AbstractBaseIterator> implements ClosableIterator> - { - private final Cursor mCursor; - private final Table mTable; - - - private RowIterator(@NonNull Cursor cursor, @NonNull Table table) - { - mCursor = cursor; - mTable = table; - } - - - @Override - public boolean hasNext() - { - if (mCursor.isClosed()) - { - return false; - } - try - { - if (mCursor.isAfterLast()) - { - mCursor.close(); - return false; - } - } - catch (Exception e) - { - // we can't use finally here, because we want to close the cursor only in an error case. - mCursor.close(); - throw e; - } - - return true; - } - - - @NonNull - @Override - public RowSnapshot next() - { - try - { - if (!hasNext()) - { - throw new NoSuchElementException("No more rows to iterate"); - } - Map charData = new HashMap<>(); - Map byteData = new HashMap<>(); - String[] columnNames = mCursor.getColumnNames(); - for (int i = 0, count = mCursor.getColumnCount(); i < count; ++i) - { - String columnName = columnNames[i]; - if (mCursor.getType(i) == Cursor.FIELD_TYPE_BLOB) - { - byteData.put(columnName, mCursor.getBlob(i)); - } - else - { - charData.put(columnName, mCursor.getString(i)); - } - } - RowSnapshot rowSnapshot = new ValuesRowSnapshot<>(mTable, new MapRowDataSnapshot<>(charData, byteData)); - mCursor.moveToNext(); - return rowSnapshot; - } - catch (Exception e) - { - // we can't use finally here, because we want to close the cursor only in an error case. - mCursor.close(); - throw e; - } - } - - - @Override - protected void finalize() throws Throwable - { - // Note this only serves as the very last resort. Normally the cursor is closed when the last item is iterated or when close() is called. - // However if there is a crash during this the cursor might not be closed properly, hence we try that here. - if (!mCursor.isClosed()) - { - mCursor.close(); - } - super.finalize(); - } - - - @Override - public void close() - { - if (!mCursor.isClosed()) - { - mCursor.close(); - } - } - } - -} diff --git a/opentaskspal/src/main/java/org/dmfs/opentaskspal/rowsets/Subtasks.java b/opentaskspal/src/main/java/org/dmfs/opentaskspal/rowsets/Subtasks.java index b76abfa7a..b2dfac26a 100644 --- a/opentaskspal/src/main/java/org/dmfs/opentaskspal/rowsets/Subtasks.java +++ b/opentaskspal/src/main/java/org/dmfs/opentaskspal/rowsets/Subtasks.java @@ -23,6 +23,7 @@ import org.dmfs.android.contentpal.predicates.ReferringTo; import org.dmfs.android.contentpal.rowsets.DelegatingRowSet; import org.dmfs.android.contentpal.rowsets.QueryRowSet; +import org.dmfs.android.contentpal.views.Sorted; import org.dmfs.jems.optional.Optional; import org.dmfs.jems.optional.elementary.Present; import org.dmfs.tasks.contract.TaskContract.Tasks; @@ -38,12 +39,15 @@ public final class Subtasks extends DelegatingRowSet { - @SuppressWarnings("unchecked") public Subtasks(@NonNull View view, @NonNull Projection projection, @NonNull RowReference parentTask) { - super(new QueryRowSetSorting<>(view, projection, new ReferringTo<>(Tasks.PARENT_ID, parentTask), new Present(Tasks.STATUS + ", " + Tasks.TITLE))); + super( + new QueryRowSet<>( + new Sorted<>(Tasks.PERCENT_COMPLETE + ", " + Tasks.TITLE, view), + projection, + new ReferringTo<>(Tasks.PARENT_ID, parentTask))); } }