Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Unverified Commit a59ca624 authored by Marten Gajda's avatar Marten Gajda Committed by GitHub
Browse files

Update PARENT_ID of child task referring via UID, fixes #877 (#934)

When a child task refers to its parent via the parents UID and the parent is inserted after the child, we need to update the PARENT_ID of the child once the parent is available.
parent f5e56be8
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -1573,6 +1573,8 @@ public final class TaskContract
         * <p>
         * When writing a relation, exactly one of {@link #RELATED_ID} or {@link #RELATED_UID} must be present. The missing value and {@link
         * #RELATED_CONTENT_URI} will be populated automatically if possible.
         * <p>
         * {@link Tasks#PARENT_ID} is updated automatically if possible.
         */
        interface Relation extends PropertyColumns
        {
+184 −0
Original line number Diff line number Diff line
/*
 * 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.provider.tasks;

import android.accounts.Account;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.Context;
import android.os.Build;

import org.dmfs.android.contentpal.Operation;
import org.dmfs.android.contentpal.OperationsQueue;
import org.dmfs.android.contentpal.RowSnapshot;
import org.dmfs.android.contentpal.operations.Assert;
import org.dmfs.android.contentpal.operations.BulkAssert;
import org.dmfs.android.contentpal.operations.BulkDelete;
import org.dmfs.android.contentpal.operations.Counted;
import org.dmfs.android.contentpal.operations.Insert;
import org.dmfs.android.contentpal.operations.Put;
import org.dmfs.android.contentpal.predicates.AllOf;
import org.dmfs.android.contentpal.predicates.EqArg;
import org.dmfs.android.contentpal.predicates.ReferringTo;
import org.dmfs.android.contentpal.queues.BasicOperationsQueue;
import org.dmfs.android.contentpal.rowdata.CharSequenceRowData;
import org.dmfs.android.contentpal.rowdata.Composite;
import org.dmfs.android.contentpal.rowdata.Referring;
import org.dmfs.android.contentpal.rowsnapshots.VirtualRowSnapshot;
import org.dmfs.android.contentpal.tables.Synced;
import org.dmfs.android.contenttestpal.operations.AssertEmptyTable;
import org.dmfs.jems.iterable.elementary.Seq;
import org.dmfs.opentaskspal.tables.InstanceTable;
import org.dmfs.opentaskspal.tables.LocalTaskListsTable;
import org.dmfs.opentaskspal.tables.PropertiesTable;
import org.dmfs.opentaskspal.tables.TaskListScoped;
import org.dmfs.opentaskspal.tables.TaskListsTable;
import org.dmfs.opentaskspal.tables.TasksTable;
import org.dmfs.opentaskspal.tasklists.NameData;
import org.dmfs.opentaskspal.tasks.TitleData;
import org.dmfs.tasks.contract.TaskContract;
import org.dmfs.tasks.contract.TaskContract.TaskLists;
import org.dmfs.tasks.contract.TaskContract.Tasks;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

import androidx.test.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;

import static org.dmfs.android.contenttestpal.ContentMatcher.resultsIn;
import static org.junit.Assert.assertThat;


/**
 * Tests for {@link TaskProvider} reparenting feature.
 *
 * @author Marten Gajda
 */
@RunWith(AndroidJUnit4.class)
public class TaskProviderRelatingTest
{
    private ContentResolver mResolver;
    private String mAuthority;
    private Context mContext;
    private ContentProviderClient mClient;
    private final Account testAccount = new Account("foo", "bar");


    @Before
    public void setUp() throws Exception
    {
        mContext = InstrumentationRegistry.getTargetContext();
        mResolver = mContext.getContentResolver();
        mAuthority = AuthorityUtil.taskAuthority(mContext);
        mClient = mContext.getContentResolver().acquireContentProviderClient(mAuthority);

        // Assert that tables are empty:
        OperationsQueue queue = new BasicOperationsQueue(mClient);
        queue.enqueue(new Seq<Operation<?>>(
                new AssertEmptyTable<>(new TasksTable(mAuthority)),
                new AssertEmptyTable<>(new TaskListsTable(mAuthority)),
                new AssertEmptyTable<>(new PropertiesTable(mAuthority)),
                new AssertEmptyTable<>(new InstanceTable(mAuthority))));
        queue.flush();
    }


    @After
    public void tearDown() throws Exception
    {
        /*
        TODO When Test Orchestration is available, there will be no need for clean up here and check in setUp(), every test method will run in separate instrumentation
        https://android-developers.googleblog.com/2017/07/android-testing-support-library-10-is.html
        https://developer.android.com/training/testing/junit-runner.html#using-android-test-orchestrator
        */

        // Clear the DB:
        BasicOperationsQueue queue = new BasicOperationsQueue(mClient);
        queue.enqueue(new Seq<Operation<?>>(
                new BulkDelete<>(new LocalTaskListsTable(mAuthority)),
                new BulkDelete<>(new PropertiesTable(mAuthority)),
                new BulkDelete<>(new Synced<>(testAccount, new TaskListsTable(mAuthority)))));
        queue.flush();

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
        {
            mClient.close();
        }
        else
        {
            mClient.release();
        }
    }


    /**
     * Create 1 local task list, then create a child Task, related to a parent UID and finally the parent.
     */
    @Test
    public void testRelateTask()
    {
        RowSnapshot<TaskLists> taskList = new VirtualRowSnapshot<>(new Synced<>(testAccount, new TaskListsTable(mAuthority)));
        RowSnapshot<Tasks> taskChild = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new Synced<>(testAccount, new TasksTable(mAuthority))));
        RowSnapshot<Tasks> taskParent = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new Synced<>(testAccount, new TasksTable(mAuthority))));

        assertThat(new Seq<>(
                        new Put<>(taskList, new NameData("list1")),
                        new Put<>(taskChild, new Composite<>(
                                new TitleData("child"),
                                new CharSequenceRowData<>(Tasks._UID, "childUID"))),
                        new Insert<>(new PropertiesTable(mAuthority), new Composite<>(
                                new CharSequenceRowData<>(TaskContract.Property.Relation.MIMETYPE, TaskContract.Property.Relation.CONTENT_ITEM_TYPE),
                                new Referring<>(TaskContract.Property.Relation.TASK_ID, taskChild),
                                new CharSequenceRowData<>(TaskContract.Property.Relation.RELATED_UID, "parentUID"),
                                new CharSequenceRowData<>(TaskContract.Property.Relation.RELATED_TYPE, String.valueOf(TaskContract.Property.Relation.RELTYPE_PARENT))
                        )),
                        new Put<>(taskParent, new Composite<>(
                                new TitleData("parent"),
                                new CharSequenceRowData<>(Tasks._UID, "parentUID")))
                ),
                resultsIn(mClient,
                        new Assert<>(taskList, new NameData("list1")),
                        new Assert<>(taskChild, new Composite<>(
                                new TitleData("child"),
                                new CharSequenceRowData<>(Tasks._UID, "childUID"),
                                new Referring<>(Tasks.PARENT_ID, taskParent))),
                        new Assert<>(taskParent, new Composite<>(
                                new CharSequenceRowData<>(Tasks._UID, "parentUID"),
                                new TitleData("parent"))),
                        new Counted<>(1, new BulkAssert<>(
                                new PropertiesTable(mAuthority),
                                new Composite<>(
                                        new CharSequenceRowData<>(TaskContract.Property.Relation.RELATED_TYPE,
                                                String.valueOf(TaskContract.Property.Relation.RELTYPE_PARENT)),
                                        new CharSequenceRowData<>(TaskContract.Property.Relation.RELATED_UID, "parentUID"),
                                        new Referring<>(TaskContract.Property.Relation.RELATED_ID, taskParent)
                                ),
                                new AllOf<>(
                                        new EqArg<>(TaskContract.Properties.MIMETYPE, TaskContract.Property.Relation.CONTENT_ITEM_TYPE),
                                        new ReferringTo<>(TaskContract.Properties.TASK_ID, taskChild)
                                ))),
                        new Counted<>(0, new BulkAssert<>(
                                new PropertiesTable(mAuthority),
                                new AllOf<>(
                                        new EqArg<>(TaskContract.Properties.MIMETYPE, TaskContract.Property.Relation.CONTENT_ITEM_TYPE),
                                        new ReferringTo<>(TaskContract.Properties.TASK_ID, taskParent)
                                )))
                ));
    }
}
+29 −2
Original line number Diff line number Diff line
@@ -17,6 +17,7 @@
package org.dmfs.provider.tasks.processors.tasks;

import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;

import org.dmfs.provider.tasks.TaskDatabaseHelper;
@@ -35,7 +36,6 @@ import org.dmfs.tasks.contract.TaskContract;
 * It also updates {@link TaskContract.Property.Relation#RELATED_UID} when a tasks
 * is synced the first time and a UID has been set.
 * <p>
 * TODO: update {@link TaskContract.Tasks#PARENT_ID} of related tasks.
 *
 * @author Marten Gajda
 */
@@ -69,9 +69,35 @@ public final class Relating implements EntityProcessor<TaskAdapter>
            ContentValues v = new ContentValues(1);
            v.put(TaskContract.Property.Relation.RELATED_ID, result.id());

            db.update(TaskDatabaseHelper.Tables.PROPERTIES, v,
            int updates = db.update(TaskDatabaseHelper.Tables.PROPERTIES, v,
                    TaskContract.Property.Relation.MIMETYPE + "= ? AND " + TaskContract.Property.Relation.RELATED_UID + "=?", new String[] {
                            TaskContract.Property.Relation.CONTENT_ITEM_TYPE, uid });

            if (updates > 0)
            {
                // there were other relations pointing towards this task, update PARENT_IDs if necessary
                ContentValues parentIdValues = new ContentValues(1);
                parentIdValues.put(TaskContract.Tasks.PARENT_ID, result.id());
                // iterate over all tasks which refer to this as their parent and update their PARENT_ID
                try (Cursor c = db.query(
                        TaskDatabaseHelper.Tables.PROPERTIES, new String[] { TaskContract.Property.Relation.TASK_ID },
                        String.format("%s = ? and %s = ? and %s = ?",
                                TaskContract.Property.Relation.MIMETYPE,
                                TaskContract.Property.Relation.RELATED_ID,
                                TaskContract.Property.Relation.RELATED_TYPE),
                        new String[] {
                                TaskContract.Property.Relation.CONTENT_ITEM_TYPE,
                                String.valueOf(result.id()),
                                String.valueOf(TaskContract.Property.Relation.RELTYPE_PARENT) },
                        null, null, null))
                {
                    while (c.moveToNext())
                    {
                        db.update(TaskDatabaseHelper.Tables.TASKS, parentIdValues, TaskContract.Tasks._ID + " = ?", new String[] { c.getString(0) });
                    }
                }
                // TODO, way also may have to do this for all the siblings of these tasks.
            }
        }
        return result;
    }
@@ -82,6 +108,7 @@ public final class Relating implements EntityProcessor<TaskAdapter>
    {
        TaskAdapter result = mDelegate.update(db, task, isSyncAdapter);
        // A task has been updated and may have received a UID by the sync adapter. Update all by-id references to this task.
        // in this case we don't need to update any PARENT_ID because it should already be set.

        if (!isSyncAdapter)
        {