delegate)
+ {
+ mDelegate = delegate;
+ }
+
+
+ @Override
+ protected final void subscribeActual(SingleObserver super T> 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 0d52b7b4b..8af6e4716 100644
--- a/opentasks/src/main/res/values/strings.xml
+++ b/opentasks/src/main/res/values/strings.xml
@@ -46,6 +46,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 super BaseColumns> PROJECTION = Id.PROJECTION;
-
- private final RowDataSnapshot extends TaskContract.TaskColumns> mRowDataSnapshot;
- private final String mAuthority;
-
-
- public TaskUri(@NonNull String authority, @NonNull RowDataSnapshot extends TaskContract.TaskColumns> 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());
- }
-}