Skip to content

Commit

Permalink
Add simple recurrence picker. Implements #993 (#1007)
Browse files Browse the repository at this point in the history
Add a simple popup recurrence picker offering some common recurrence
patterns. Since there is no way to set an end to recurring tasks yet,
this commit also ensures they can at least be deleted.
  • Loading branch information
dmfs authored Mar 8, 2021
1 parent b304284 commit ea88dc0
Show file tree
Hide file tree
Showing 22 changed files with 1,221 additions and 49 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -1211,6 +1211,15 @@ public static final class Instances implements TaskColumns, InstanceColumns
*/
public static final String VISIBLE = "visible";

/**
* Flag indicating that ths is an instance of a recurring task.
* <p>
* Value: Integer
* <p>
* read-only
*/
public static final String IS_RECURRING = "is_recurring";

public static final String CONTENT_URI_PATH = "instances";

public static final String DEFAULT_SORT_ORDER = INSTANCE_DUE_SORTING;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ public interface OnDatabaseOperationListener
/**
* The database version.
*/
private static final int DATABASE_VERSION = 22;
private static final int DATABASE_VERSION = 23;


/**
Expand Down Expand Up @@ -197,6 +197,8 @@ public interface CategoriesMapping
+ "null as " + Tasks.RRULE + ", "
+ "null as " + Tasks.RDATE + ", "
+ "null as " + Tasks.EXDATE + ", "
// this instance is part of a recurring task if either it has recurrence values or overrides an instance
+ "not (" + Tasks.RRULE + " is null and " + Tasks.RDATE + " is null and " + Tasks.ORIGINAL_INSTANCE_ID + " is null and " + Tasks.ORIGINAL_INSTANCE_SYNC_ID + " is null) as " + TaskContract.Instances.IS_RECURRING + ", "
+ Tables.TASKS + ".*, "
+ Tables.LISTS + "." + Tasks.ACCOUNT_NAME + ", "
+ Tables.LISTS + "." + Tasks.ACCOUNT_TYPE + ", "
Expand Down Expand Up @@ -875,6 +877,12 @@ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)
}
}

if (oldVersion < 23)
{
db.execSQL("drop view " + Tables.INSTANCE_CLIENT_VIEW + ";");
db.execSQL(SQL_CREATE_INSTANCE_CLIENT_VIEW);
}

// upgrade FTS
FTSDatabaseHelper.onUpgrade(db, oldVersion, newVersion);

Expand Down
1 change: 1 addition & 0 deletions opentasks/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ dependencies {
implementation 'io.reactivex.rxjava2:rxjava:2.2.21'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
implementation 'androidx.preference:preference:1.1.1'
implementation 'com.maltaisn:recurpicker:2.1.4'
}

if (project.hasProperty('PLAY_STORE_SERVICE_ACCOUNT_CREDENTIALS')) {
Expand Down
5 changes: 3 additions & 2 deletions opentasks/src/main/java/org/dmfs/tasks/EditTaskFragment.java
Original file line number Diff line number Diff line change
Expand Up @@ -141,9 +141,10 @@ private interface TASK_LIST_PROJECTION_VALUES

static final ContentValueMapper CONTENT_VALUE_MAPPER = new ContentValueMapper()
.addString(Tasks.ACCOUNT_TYPE, Tasks.ACCOUNT_NAME, Tasks.TITLE, Tasks.LOCATION, Tasks.DESCRIPTION, Tasks.GEO, Tasks.URL, Tasks.TZ, Tasks.DURATION,
Tasks.LIST_NAME)
Tasks.LIST_NAME, Tasks.RRULE, Tasks.RDATE)
.addInteger(Tasks.PRIORITY, Tasks.LIST_COLOR, Tasks.TASK_COLOR, Tasks.STATUS, Tasks.CLASSIFICATION, Tasks.PERCENT_COMPLETE, Tasks.IS_ALLDAY,
Tasks.IS_CLOSED, Tasks.PINNED).addLong(Tasks.LIST_ID, Tasks.DTSTART, Tasks.DUE, Tasks.COMPLETED, Tasks._ID);
Tasks.IS_CLOSED, Tasks.PINNED)
.addLong(Tasks.LIST_ID, Tasks.DTSTART, Tasks.DUE, Tasks.COMPLETED, Tasks._ID, Tasks.ORIGINAL_INSTANCE_ID);

private boolean mAppForEdit = true;
private TasksListCursorSpinnerAdapter mTaskListAdapter;
Expand Down
148 changes: 104 additions & 44 deletions opentasks/src/main/java/org/dmfs/tasks/ViewTaskFragment.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,27 +21,15 @@
import android.app.AlertDialog;
import android.content.ContentValues;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.Intent;
import android.content.OperationApplicationException;
import android.content.res.ColorStateList;
import android.database.ContentObserver;
import android.net.Uri;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.material.appbar.AppBarLayout;
import com.google.android.material.appbar.AppBarLayout.OnOffsetChangedListener;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.android.material.snackbar.Snackbar;
import androidx.core.app.ActivityCompat;
import androidx.core.view.MenuItemCompat;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.ShareActionProvider;
import androidx.appcompat.widget.Toolbar;
import androidx.appcompat.widget.Toolbar.OnMenuItemClickListener;
import android.os.RemoteException;
import android.text.TextUtils;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
Expand All @@ -51,11 +39,28 @@
import android.view.animation.AlphaAnimation;
import android.widget.TextView;

import com.google.android.material.appbar.AppBarLayout;
import com.google.android.material.appbar.AppBarLayout.OnOffsetChangedListener;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.android.material.snackbar.Snackbar;

import org.dmfs.android.bolts.color.Color;
import org.dmfs.android.bolts.color.elementary.ValueColor;
import org.dmfs.android.contentpal.Operation;
import org.dmfs.android.contentpal.operations.BulkDelete;
import org.dmfs.android.contentpal.predicates.AnyOf;
import org.dmfs.android.contentpal.predicates.EqArg;
import org.dmfs.android.contentpal.predicates.IdIn;
import org.dmfs.android.contentpal.transactions.BaseTransaction;
import org.dmfs.android.retentionmagic.SupportFragment;
import org.dmfs.android.retentionmagic.annotations.Parameter;
import org.dmfs.android.retentionmagic.annotations.Retain;
import org.dmfs.jems.iterable.adapters.PresentValues;
import org.dmfs.jems.optional.elementary.NullSafe;
import org.dmfs.jems.single.combined.Backed;
import org.dmfs.opentaskspal.tables.InstanceTable;
import org.dmfs.opentaskspal.tables.TasksTable;
import org.dmfs.tasks.contract.TaskContract;
import org.dmfs.tasks.contract.TaskContract.Tasks;
import org.dmfs.tasks.model.ContentSet;
import org.dmfs.tasks.model.Model;
Expand All @@ -70,9 +75,17 @@
import org.dmfs.tasks.utils.colors.AdjustedForFab;
import org.dmfs.tasks.widget.TaskView;

import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.ShareActionProvider;
import androidx.appcompat.widget.Toolbar;
import androidx.appcompat.widget.Toolbar.OnMenuItemClickListener;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import androidx.core.app.ActivityCompat;
import androidx.core.view.MenuItemCompat;


/**
Expand All @@ -88,16 +101,16 @@ public class ViewTaskFragment extends SupportFragment
private final static String ARG_URI = "uri";
private static final String ARG_STARTING_COLOR = "starting_color";

/**
* A set of values that may affect the recurrence set of a task. If one of these values changes we have to submit all of them.
*/
private final static Set<String> RECURRENCE_VALUES = new HashSet<String>(
Arrays.asList(Tasks.DUE, Tasks.DTSTART, Tasks.TZ, Tasks.IS_ALLDAY, Tasks.RRULE, Tasks.RDATE, Tasks.EXDATE));

/**
* The {@link ContentValueMapper} that knows how to map the values in a cursor to {@link ContentValues}.
*/
private static final ContentValueMapper CONTENT_VALUE_MAPPER = EditTaskFragment.CONTENT_VALUE_MAPPER;

private static final ContentValueMapper CONTENT_VALUE_MAPPER = new ContentValueMapper()
.addString(Tasks.ACCOUNT_TYPE, Tasks.ACCOUNT_NAME, Tasks.TITLE, Tasks.LOCATION, Tasks.DESCRIPTION, Tasks.GEO, Tasks.URL, Tasks.TZ, Tasks.DURATION,
Tasks.LIST_NAME, Tasks.RRULE, Tasks.RDATE)
.addInteger(Tasks.PRIORITY, Tasks.LIST_COLOR, Tasks.TASK_COLOR, Tasks.STATUS, Tasks.CLASSIFICATION, Tasks.PERCENT_COMPLETE, Tasks.IS_ALLDAY,
Tasks.IS_CLOSED, Tasks.PINNED, TaskContract.Instances.IS_RECURRING)
.addLong(Tasks.LIST_ID, Tasks.DTSTART, Tasks.DUE, Tasks.COMPLETED, Tasks._ID, Tasks.ORIGINAL_INSTANCE_ID, TaskContract.Instances.TASK_ID);

private static final float PERCENTAGE_TO_HIDE_TITLE_DETAILS = 0.3f;
private static final int ALPHA_ANIMATIONS_DURATION = 200;
Expand Down Expand Up @@ -550,36 +563,80 @@ public boolean onOptionsItemSelected(MenuItem item)
}
else if (itemId == R.id.delete_task)
{
new AlertDialog.Builder(getActivity()).setTitle(R.string.confirm_delete_title).setCancelable(true)
.setNegativeButton(android.R.string.cancel, new OnClickListener()
{
@Override
public void onClick(DialogInterface dialog, int which)
long originalInstanceId = new Backed<>(TaskFieldAdapters.ORIGINAL_INSTANCE_ID.get(mContentSet), () ->
Long.valueOf(TaskFieldAdapters.INSTANCE_TASK_ID.get(mContentSet))).value();
boolean isRecurring = TaskFieldAdapters.IS_RECURRING_INSTANCE.get(mContentSet);
AtomicReference<Operation<?>> operation = new AtomicReference<>(
new BulkDelete<>(
new InstanceTable(mTaskUri.getAuthority()),
new IdIn<>(mTaskUri.getLastPathSegment())));
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity())
.setCancelable(true)
.setNegativeButton(android.R.string.cancel, (dialog, which) -> {
// nothing to do here
})
.setTitle(isRecurring ? R.string.opentasks_task_details_delete_recurring_task : R.string.confirm_delete_title)
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
if (mContentSet != null)
{
// nothing to do here
// TODO: remove the task in a background task
try
{
new BaseTransaction()
.with(new PresentValues<>(new NullSafe<>(operation.get())))
.commit(getContext().getContentResolver().acquireContentProviderClient(mTaskUri));
}
catch (RemoteException | OperationApplicationException e)
{
Log.e(ViewTaskFragment.class.getSimpleName(), "Unable to delete task ", e);
}

mCallback.onTaskDeleted(mTaskUri);
mTaskUri = null;
}
}).setPositiveButton(android.R.string.ok, new OnClickListener()
});
if (isRecurring)
{
@Override
public void onClick(DialogInterface dialog, int which)
{
if (mContentSet != null)
{
// TODO: remove the task in a background task
mContentSet.delete(mAppContext);
mCallback.onTaskDeleted(mTaskUri);
mTaskUri = null;
}
}
}).setMessage(R.string.confirm_delete_message).create().show();
builder.setSingleChoiceItems(
new CharSequence[] {
getString(R.string.opentasks_task_details_delete_this_task),
getString(R.string.opentasks_task_details_delete_all_tasks)
},
0,
(dialog, which) -> {
switch (which)
{
case 0:
operation.set(new BulkDelete<>(
new InstanceTable(mTaskUri.getAuthority()),
new IdIn<>(mTaskUri.getLastPathSegment())));
case 1:
operation.set(new BulkDelete<>(
new TasksTable(mTaskUri.getAuthority()),
new AnyOf<>(
new IdIn<>(originalInstanceId),
new EqArg<>(Tasks.ORIGINAL_INSTANCE_ID, originalInstanceId))));

}
});
}
else
{
builder.setMessage(R.string.confirm_delete_message);
}
builder.create().show();

return true;

}
else if (itemId == R.id.complete_task)

{
completeTask();
return true;
}
else if (itemId == R.id.pin_task)

{
if (TaskFieldAdapters.PINNED.get(mContentSet))
{
Expand All @@ -595,14 +652,17 @@ else if (itemId == R.id.pin_task)
return true;
}
else if (itemId == R.id.opentasks_send_task)

{
setSendMenuIntent();
return false;
}
else

{
return super.onOptionsItemSelected(item);
}

}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ public class DefaultModel extends Model
private final static LayoutDescriptor BOOLEAN_EDIT = new LayoutDescriptor(R.layout.boolean_field_editor);
private final static LayoutDescriptor URL_VIEW = new LayoutDescriptor(R.layout.url_field_view);
private final static LayoutDescriptor URL_EDIT = new LayoutDescriptor(R.layout.url_field_editor);
private final static LayoutDescriptor RRULE_EDIT = new LayoutDescriptor(R.layout.opentasks_rrule_field_editor);

final static LayoutDescriptor LIST_COLOR_VIEW = new LayoutDescriptor(R.layout.list_color_view);

Expand Down Expand Up @@ -122,6 +123,10 @@ public void inflate()
// all day flag
addField(new FieldDescriptor(context, R.id.task_field_all_day, R.string.task_all_day, TaskFieldAdapters.ALLDAY).setEditorLayout(BOOLEAN_EDIT));

// rrule
addField(new FieldDescriptor(context, R.id.task_field_rrule, R.string.task_recurrence, TaskFieldAdapters.RRULE)
.setEditorLayout(RRULE_EDIT).setIcon(R.drawable.ic_baseline_repeat_24));

TimeZoneChoicesAdapter tzaca = new TimeZoneChoicesAdapter(context);
// time zone
addField(new FieldDescriptor(context, R.id.task_field_timezone, R.string.task_timezone, TaskFieldAdapters.TIMEZONE).setEditorLayout(CHOICES_EDIT)
Expand Down
26 changes: 25 additions & 1 deletion opentasks/src/main/java/org/dmfs/tasks/model/Model.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,22 @@
import android.content.ContentUris;
import android.content.Context;
import android.content.Intent;
import androidx.collection.SparseArrayCompat;
import android.text.TextUtils;

import org.dmfs.iterables.decorators.Sieved;
import org.dmfs.jems.optional.adapters.First;
import org.dmfs.jems.single.combined.Backed;
import org.dmfs.provider.tasks.AuthorityUtil;
import org.dmfs.provider.tasks.utils.Range;
import org.dmfs.tasks.ManageListActivity;
import org.dmfs.tasks.contract.TaskContract.TaskLists;

import java.util.ArrayList;
import java.util.List;

import androidx.annotation.IdRes;
import androidx.collection.SparseArrayCompat;


/**
* An abstract model class.
Expand Down Expand Up @@ -95,6 +101,24 @@ protected void addField(FieldDescriptor descriptor)
}


/**
* Adds another field (identified by its field descriptor) to this model.
*
* @param descriptor
* The {@link FieldDescriptor} of the field to add.
*/
protected void addFieldAfter(@IdRes int id, FieldDescriptor descriptor)
{
mFields.add(
new Backed<>(
new First<>(
new Sieved<>(i -> mFields.get(i).getFieldId() == id,
new Range(0, mFields.size()))), mFields::size).value(),
descriptor);
mFieldIndex.put(descriptor.getFieldId(), descriptor);
}


public FieldDescriptor getField(int fieldId)
{
return mFieldIndex.get(fieldId, null);
Expand Down
Loading

0 comments on commit ea88dc0

Please sign in to comment.