Convert task edit screen to kotlin

pull/996/head
Alex Baker 4 years ago
parent a6d10ed479
commit 47d414f20f

@ -1,185 +0,0 @@
/*
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.files;
import static android.app.Activity.RESULT_OK;
import static org.tasks.dialogs.AddAttachmentDialog.REQUEST_AUDIO;
import static org.tasks.dialogs.AddAttachmentDialog.REQUEST_CAMERA;
import static org.tasks.dialogs.AddAttachmentDialog.REQUEST_GALLERY;
import static org.tasks.dialogs.AddAttachmentDialog.REQUEST_STORAGE;
import static org.tasks.dialogs.AddAttachmentDialog.newAddAttachmentDialog;
import static org.tasks.files.FileHelper.copyToUri;
import android.annotation.SuppressLint;
import android.content.ClipData;
import android.content.ClipData.Item;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.Nullable;
import butterknife.BindView;
import butterknife.OnClick;
import com.todoroo.astrid.data.Task;
import java.util.ArrayList;
import javax.inject.Inject;
import org.tasks.R;
import org.tasks.data.TaskAttachment;
import org.tasks.data.TaskAttachmentDao;
import org.tasks.dialogs.DialogBuilder;
import org.tasks.files.FileHelper;
import org.tasks.injection.ForActivity;
import org.tasks.injection.FragmentComponent;
import org.tasks.preferences.Preferences;
import org.tasks.ui.TaskEditControlFragment;
public class FilesControlSet extends TaskEditControlFragment {
public static final int TAG = R.string.TEA_ctrl_files_pref;
private static final String FRAG_TAG_ADD_ATTACHMENT_DIALOG = "frag_tag_add_attachment_dialog";
private static final char LEFT_TO_RIGHT_MARK = '\u200e';
@Inject TaskAttachmentDao taskAttachmentDao;
@Inject DialogBuilder dialogBuilder;
@Inject @ForActivity Context context;
@Inject Preferences preferences;
@BindView(R.id.attachment_container)
LinearLayout attachmentContainer;
@BindView(R.id.add_attachment)
TextView addAttachment;
private String taskUuid;
@Nullable
@Override
public View onCreateView(
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = super.onCreateView(inflater, container, savedInstanceState);
taskUuid = task.getUuid();
for (TaskAttachment attachment : taskAttachmentDao.getAttachments(taskUuid)) {
addAttachment(attachment);
}
if (savedInstanceState == null) {
if (task.hasTransitory(TaskAttachment.KEY)) {
for (Uri uri : (ArrayList<Uri>) task.getTransitory(TaskAttachment.KEY)) {
newAttachment(uri);
}
}
}
return view;
}
@OnClick(R.id.add_attachment)
void addAttachment() {
newAddAttachmentDialog(this).show(getParentFragmentManager(), FRAG_TAG_ADD_ATTACHMENT_DIALOG);
}
@Override
protected int getLayout() {
return R.layout.control_set_files;
}
@Override
public int getIcon() {
return R.drawable.ic_outline_attachment_24px;
}
@Override
public int controlId() {
return TAG;
}
@Override
public void apply(Task task) {}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_CAMERA || requestCode == REQUEST_AUDIO) {
if (resultCode == RESULT_OK) {
Uri uri = data.getData();
copyToAttachmentDirectory(uri);
FileHelper.delete(context, uri);
}
} else if (requestCode == REQUEST_STORAGE || requestCode == REQUEST_GALLERY) {
if (resultCode == RESULT_OK) {
ClipData clip = data.getClipData();
if (clip != null) {
for (int i = 0; i < clip.getItemCount(); i++) {
Item item = clip.getItemAt(i);
copyToAttachmentDirectory(item.getUri());
}
} else {
copyToAttachmentDirectory(data.getData());
}
}
} else {
super.onActivityResult(requestCode, resultCode, data);
}
}
private void addAttachment(TaskAttachment taskAttachment) {
View fileRow =
getActivity().getLayoutInflater().inflate(R.layout.file_row, attachmentContainer, false);
fileRow.setTag(taskAttachment);
attachmentContainer.addView(fileRow);
addAttachment(taskAttachment, fileRow);
}
private void addAttachment(final TaskAttachment taskAttachment, final View fileRow) {
TextView nameView = fileRow.findViewById(R.id.file_text);
String name = LEFT_TO_RIGHT_MARK + taskAttachment.getName();
nameView.setText(name);
nameView.setOnClickListener(v -> showFile(taskAttachment));
View clearFile = fileRow.findViewById(R.id.clear);
clearFile.setOnClickListener(
v ->
dialogBuilder
.newDialog(R.string.premium_remove_file_confirm)
.setPositiveButton(
android.R.string.ok,
(dialog, which) -> {
taskAttachmentDao.delete(taskAttachment);
FileHelper.delete(context, taskAttachment.parseUri());
attachmentContainer.removeView(fileRow);
})
.setNegativeButton(android.R.string.cancel, null)
.show());
}
@Override
protected void inject(FragmentComponent component) {
component.inject(this);
}
@SuppressLint("NewApi")
private void showFile(final TaskAttachment m) {
FileHelper.startActionView(getActivity(), m.parseUri());
}
private void copyToAttachmentDirectory(Uri input) {
newAttachment(copyToUri(context, preferences.getAttachmentsDirectory(), input));
}
private void newAttachment(Uri output) {
TaskAttachment attachment =
new TaskAttachment(taskUuid, output, FileHelper.getFilename(context, output));
taskAttachmentDao.createNew(attachment);
addAttachment(attachment);
}
}

@ -0,0 +1,153 @@
/*
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.files
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.TextView
import butterknife.BindView
import butterknife.OnClick
import com.todoroo.astrid.data.Task
import org.tasks.R
import org.tasks.data.TaskAttachment
import org.tasks.data.TaskAttachmentDao
import org.tasks.dialogs.AddAttachmentDialog
import org.tasks.dialogs.DialogBuilder
import org.tasks.files.FileHelper
import org.tasks.injection.ForActivity
import org.tasks.injection.FragmentComponent
import org.tasks.preferences.Preferences
import org.tasks.ui.TaskEditControlFragment
import java.util.*
import javax.inject.Inject
class FilesControlSet : TaskEditControlFragment() {
@Inject @ForActivity lateinit var activity: Context
@Inject lateinit var taskAttachmentDao: TaskAttachmentDao
@Inject lateinit var dialogBuilder: DialogBuilder
@Inject lateinit var preferences: Preferences
@BindView(R.id.attachment_container)
lateinit var attachmentContainer: LinearLayout
@BindView(R.id.add_attachment)
lateinit var addAttachment: TextView
private var taskUuid: String? = null
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = super.onCreateView(inflater, container, savedInstanceState)
taskUuid = task.uuid
for (attachment in taskAttachmentDao.getAttachments(taskUuid!!)) {
addAttachment(attachment)
}
if (savedInstanceState == null) {
if (task.hasTransitory(TaskAttachment.KEY)) {
for (uri in (task.getTransitory<ArrayList<Uri>>(TaskAttachment.KEY))!!) {
newAttachment(uri)
}
}
}
return view
}
@OnClick(R.id.add_attachment)
fun addAttachment() {
AddAttachmentDialog.newAddAttachmentDialog(this).show(parentFragmentManager, FRAG_TAG_ADD_ATTACHMENT_DIALOG)
}
override val layout: Int
get() = R.layout.control_set_files
override val icon: Int
get() = R.drawable.ic_outline_attachment_24px
override fun controlId() = TAG
override fun apply(task: Task) {}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == AddAttachmentDialog.REQUEST_CAMERA || requestCode == AddAttachmentDialog.REQUEST_AUDIO) {
if (resultCode == Activity.RESULT_OK) {
val uri = data!!.data
copyToAttachmentDirectory(uri)
FileHelper.delete(activity, uri)
}
} else if (requestCode == AddAttachmentDialog.REQUEST_STORAGE || requestCode == AddAttachmentDialog.REQUEST_GALLERY) {
if (resultCode == Activity.RESULT_OK) {
val clip = data!!.clipData
if (clip != null) {
for (i in 0 until clip.itemCount) {
val item = clip.getItemAt(i)
copyToAttachmentDirectory(item.uri)
}
} else {
copyToAttachmentDirectory(data.data)
}
}
} else {
super.onActivityResult(requestCode, resultCode, data)
}
}
private fun addAttachment(taskAttachment: TaskAttachment) {
val fileRow = requireActivity().layoutInflater.inflate(R.layout.file_row, attachmentContainer, false)
fileRow.tag = taskAttachment
attachmentContainer.addView(fileRow)
addAttachment(taskAttachment, fileRow)
}
private fun addAttachment(taskAttachment: TaskAttachment, fileRow: View) {
val nameView = fileRow.findViewById<TextView>(R.id.file_text)
val name = LEFT_TO_RIGHT_MARK.toString() + taskAttachment.name
nameView.text = name
nameView.setOnClickListener { showFile(taskAttachment) }
val clearFile = fileRow.findViewById<View>(R.id.clear)
clearFile.setOnClickListener {
dialogBuilder
.newDialog(R.string.premium_remove_file_confirm)
.setPositiveButton(android.R.string.ok) { _, _ ->
taskAttachmentDao.delete(taskAttachment)
FileHelper.delete(context, taskAttachment.parseUri())
attachmentContainer.removeView(fileRow)
}
.setNegativeButton(android.R.string.cancel, null)
.show()
}
}
override fun inject(component: FragmentComponent) = component.inject(this)
@SuppressLint("NewApi")
private fun showFile(m: TaskAttachment) {
FileHelper.startActionView(requireActivity(), m.parseUri())
}
private fun copyToAttachmentDirectory(input: Uri?) {
newAttachment(FileHelper.copyToUri(context, preferences.attachmentsDirectory, input))
}
private fun newAttachment(output: Uri) {
val attachment = TaskAttachment(taskUuid!!, output, FileHelper.getFilename(context, output)!!)
taskAttachmentDao.createNew(attachment)
addAttachment(attachment)
}
companion object {
const val TAG = R.string.TEA_ctrl_files_pref
private const val FRAG_TAG_ADD_ATTACHMENT_DIALOG = "frag_tag_add_attachment_dialog"
private const val LEFT_TO_RIGHT_MARK = '\u200e'
}
}

@ -1,259 +0,0 @@
/*
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.repeats;
import static com.google.common.collect.Lists.newArrayList;
import static com.google.ical.values.Frequency.MONTHLY;
import static org.tasks.Strings.isNullOrEmpty;
import static org.tasks.repeats.BasicRecurrenceDialog.newBasicRecurrenceDialog;
import static org.tasks.time.DateTimeUtils.currentTimeMillis;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.LinearLayout;
import android.widget.Spinner;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import butterknife.BindView;
import butterknife.OnItemSelected;
import com.google.ical.values.RRule;
import com.google.ical.values.WeekdayNum;
import com.todoroo.astrid.data.Task;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javax.inject.Inject;
import org.tasks.R;
import org.tasks.Strings;
import org.tasks.analytics.Firebase;
import org.tasks.dialogs.DialogBuilder;
import org.tasks.injection.ForActivity;
import org.tasks.injection.FragmentComponent;
import org.tasks.repeats.RepeatRuleToString;
import org.tasks.themes.Theme;
import org.tasks.time.DateTime;
import org.tasks.ui.HiddenTopArrayAdapter;
import org.tasks.ui.TaskEditControlFragment;
/**
* Control Set for managing repeats
*
* @author Tim Su <tim@todoroo.com>
*/
public class RepeatControlSet extends TaskEditControlFragment {
public static final int TAG = R.string.TEA_ctrl_repeat_pref;
private static final int TYPE_DUE_DATE = 1;
private static final int TYPE_COMPLETION_DATE = 2;
private static final String FRAG_TAG_BASIC_RECURRENCE = "frag_tag_basic_recurrence";
private static final String EXTRA_RECURRENCE = "extra_recurrence";
private static final String EXTRA_DUE_DATE = "extra_due_date";
private static final String EXTRA_REPEAT_AFTER_COMPLETION = "extra_repeat_after_completion";
private final List<String> repeatTypes = new ArrayList<>();
@Inject DialogBuilder dialogBuilder;
@Inject @ForActivity Context context;
@Inject Theme theme;
@Inject Firebase firebase;
@Inject RepeatRuleToString repeatRuleToString;
@BindView(R.id.display_row_edit)
TextView displayView;
@BindView(R.id.repeatType)
Spinner typeSpinner;
@BindView(R.id.repeatTypeContainer)
LinearLayout repeatTypeContainer;
private RRule rrule;
private HiddenTopArrayAdapter<String> typeAdapter;
private long dueDate;
private boolean repeatAfterCompletion;
public void onSelected(RRule rrule) {
this.rrule = rrule;
refreshDisplayView();
}
public void onDueDateChanged(long dueDate) {
this.dueDate = dueDate > 0 ? dueDate : currentTimeMillis();
if (rrule != null && rrule.getFreq() == MONTHLY && !rrule.getByDay().isEmpty()) {
WeekdayNum weekdayNum = rrule.getByDay().get(0);
DateTime dateTime = new DateTime(this.dueDate);
int num;
int dayOfWeekInMonth = dateTime.getDayOfWeekInMonth();
if (weekdayNum.num == -1 || dayOfWeekInMonth == 5) {
num = dayOfWeekInMonth == dateTime.getMaxDayOfWeekInMonth() ? -1 : dayOfWeekInMonth;
} else {
num = dayOfWeekInMonth;
}
rrule.setByDay(newArrayList(new WeekdayNum(num, dateTime.getWeekday())));
refreshDisplayView();
}
}
@Nullable
@Override
public View onCreateView(
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = super.onCreateView(inflater, container, savedInstanceState);
if (savedInstanceState == null) {
repeatAfterCompletion = task.repeatAfterCompletion();
dueDate = task.getDueDate();
if (dueDate <= 0) {
dueDate = currentTimeMillis();
}
String recurrenceWithoutFrom = task.getRecurrenceWithoutFrom();
if (Strings.isNullOrEmpty(recurrenceWithoutFrom)) {
rrule = null;
} else {
try {
rrule = new RRule(recurrenceWithoutFrom);
rrule.setUntil(new DateTime(task.getRepeatUntil()).toDateValue());
} catch (ParseException e) {
rrule = null;
}
}
} else {
String recurrence = savedInstanceState.getString(EXTRA_RECURRENCE);
dueDate = savedInstanceState.getLong(EXTRA_DUE_DATE);
if (isNullOrEmpty(recurrence)) {
rrule = null;
} else {
try {
rrule = new RRule(recurrence);
} catch (ParseException e) {
rrule = null;
}
}
repeatAfterCompletion = savedInstanceState.getBoolean(EXTRA_REPEAT_AFTER_COMPLETION);
}
repeatTypes.add("");
repeatTypes.addAll(Arrays.asList(getResources().getStringArray(R.array.repeat_type)));
typeAdapter =
new HiddenTopArrayAdapter<String>(context, 0, repeatTypes) {
@NonNull
@Override
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
int selectedItemPosition = position;
if (parent instanceof AdapterView) {
selectedItemPosition = ((AdapterView) parent).getSelectedItemPosition();
}
TextView tv =
(TextView) inflater.inflate(android.R.layout.simple_spinner_item, parent, false);
tv.setPadding(0, 0, 0, 0);
tv.setText(repeatTypes.get(selectedItemPosition));
return tv;
}
};
Drawable drawable = context.getDrawable(R.drawable.textfield_underline_black).mutate();
drawable.setTint(context.getColor(R.color.text_primary));
typeSpinner.setBackgroundDrawable(drawable);
typeSpinner.setAdapter(typeAdapter);
typeSpinner.setSelection(repeatAfterCompletion ? TYPE_COMPLETION_DATE : TYPE_DUE_DATE);
refreshDisplayView();
return view;
}
@OnItemSelected(R.id.repeatType)
public void onRepeatTypeChanged(int position) {
repeatAfterCompletion = position == TYPE_COMPLETION_DATE;
repeatTypes.set(0, repeatAfterCompletion ? repeatTypes.get(2) : repeatTypes.get(1));
typeAdapter.notifyDataSetChanged();
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString(EXTRA_RECURRENCE, rrule == null ? "" : rrule.toIcal());
outState.putBoolean(EXTRA_REPEAT_AFTER_COMPLETION, repeatAfterCompletion);
outState.putLong(EXTRA_DUE_DATE, dueDate);
}
@Override
protected void inject(FragmentComponent component) {
component.inject(this);
}
@Override
protected void onRowClick() {
newBasicRecurrenceDialog(this, rrule, dueDate)
.show(getParentFragmentManager(), FRAG_TAG_BASIC_RECURRENCE);
}
@Override
protected boolean isClickable() {
return true;
}
@Override
protected int getLayout() {
return R.layout.control_set_repeat_display;
}
@Override
public int getIcon() {
return R.drawable.ic_outline_repeat_24px;
}
@Override
public int controlId() {
return TAG;
}
@Override
public boolean hasChanges(Task original) {
return !getRecurrenceValue().equals(original.getRecurrence())
|| original.getRepeatUntil()
!= (rrule == null ? 0 : DateTime.from(rrule.getUntil()).getMillis());
}
@Override
public void apply(Task task) {
task.setRepeatUntil(rrule == null ? 0 : DateTime.from(rrule.getUntil()).getMillis());
task.setRecurrence(getRecurrenceValue());
}
private String getRecurrenceValue() {
if (rrule == null) {
return "";
}
RRule copy;
try {
copy = new RRule(rrule.toIcal());
} catch (ParseException e) {
return "";
}
copy.setUntil(null);
String result = copy.toIcal();
if (repeatAfterCompletion && !isNullOrEmpty(result)) {
result += ";FROM=COMPLETION"; // $NON-NLS-1$
}
return result;
}
private void refreshDisplayView() {
if (rrule == null) {
displayView.setText(null);
repeatTypeContainer.setVisibility(View.GONE);
} else {
displayView.setText(repeatRuleToString.toString(rrule));
repeatTypeContainer.setVisibility(View.VISIBLE);
}
}
}

@ -0,0 +1,227 @@
/*
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.repeats
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.LinearLayout
import android.widget.Spinner
import android.widget.TextView
import butterknife.BindView
import butterknife.OnItemSelected
import com.google.ical.values.Frequency
import com.google.ical.values.RRule
import com.google.ical.values.WeekdayNum
import com.todoroo.astrid.data.Task
import org.tasks.R
import org.tasks.Strings.isNullOrEmpty
import org.tasks.analytics.Firebase
import org.tasks.dialogs.DialogBuilder
import org.tasks.injection.ForActivity
import org.tasks.injection.FragmentComponent
import org.tasks.repeats.BasicRecurrenceDialog
import org.tasks.repeats.RepeatRuleToString
import org.tasks.themes.Theme
import org.tasks.time.DateTime
import org.tasks.time.DateTimeUtils
import org.tasks.ui.HiddenTopArrayAdapter
import org.tasks.ui.TaskEditControlFragment
import java.text.ParseException
import java.util.*
import javax.inject.Inject
/**
* Control Set for managing repeats
*
* @author Tim Su <tim></tim>@todoroo.com>
*/
class RepeatControlSet : TaskEditControlFragment() {
private val repeatTypes: MutableList<String> = ArrayList()
@Inject @ForActivity lateinit var activity: Context
@Inject lateinit var dialogBuilder: DialogBuilder
@Inject lateinit var theme: Theme
@Inject lateinit var firebase: Firebase
@Inject lateinit var repeatRuleToString: RepeatRuleToString
@BindView(R.id.display_row_edit)
lateinit var displayView: TextView
@BindView(R.id.repeatType)
lateinit var typeSpinner: Spinner
@BindView(R.id.repeatTypeContainer)
lateinit var repeatTypeContainer: LinearLayout
private var rrule: RRule? = null
private lateinit var typeAdapter: HiddenTopArrayAdapter<String>
private var dueDate: Long = 0
private var repeatAfterCompletion = false
fun onSelected(rrule: RRule?) {
this.rrule = rrule
refreshDisplayView()
}
fun onDueDateChanged(dueDate: Long) {
this.dueDate = if (dueDate > 0) dueDate else DateTimeUtils.currentTimeMillis()
if (rrule != null && rrule!!.freq == Frequency.MONTHLY && rrule!!.byDay.isNotEmpty()) {
val weekdayNum = rrule!!.byDay[0]
val dateTime = DateTime(this.dueDate)
val num: Int
val dayOfWeekInMonth = dateTime.dayOfWeekInMonth
num = if (weekdayNum.num == -1 || dayOfWeekInMonth == 5) {
if (dayOfWeekInMonth == dateTime.maxDayOfWeekInMonth) -1 else dayOfWeekInMonth
} else {
dayOfWeekInMonth
}
rrule!!.byDay = listOf((WeekdayNum(num, dateTime.weekday)))
refreshDisplayView()
}
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = super.onCreateView(inflater, container, savedInstanceState)
if (savedInstanceState == null) {
repeatAfterCompletion = task.repeatAfterCompletion()
dueDate = task.dueDate
if (dueDate <= 0) {
dueDate = DateTimeUtils.currentTimeMillis()
}
val recurrenceWithoutFrom = task.getRecurrenceWithoutFrom()
if (isNullOrEmpty(recurrenceWithoutFrom)) {
rrule = null
} else {
try {
rrule = RRule(recurrenceWithoutFrom)
rrule!!.until = DateTime(task.repeatUntil).toDateValue()
} catch (e: ParseException) {
rrule = null
}
}
} else {
val recurrence = savedInstanceState.getString(EXTRA_RECURRENCE)
dueDate = savedInstanceState.getLong(EXTRA_DUE_DATE)
rrule = if (isNullOrEmpty(recurrence)) {
null
} else {
try {
RRule(recurrence)
} catch (e: ParseException) {
null
}
}
repeatAfterCompletion = savedInstanceState.getBoolean(EXTRA_REPEAT_AFTER_COMPLETION)
}
repeatTypes.add("")
repeatTypes.addAll(listOf(*resources.getStringArray(R.array.repeat_type)))
typeAdapter = object : HiddenTopArrayAdapter<String>(activity, 0, repeatTypes) {
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
var selectedItemPosition = position
if (parent is AdapterView<*>) {
selectedItemPosition = parent.selectedItemPosition
}
val tv = inflater.inflate(android.R.layout.simple_spinner_item, parent, false) as TextView
tv.setPadding(0, 0, 0, 0)
tv.text = repeatTypes[selectedItemPosition]
return tv
}
}
val drawable = activity.getDrawable(R.drawable.textfield_underline_black)!!.mutate()
drawable.setTint(activity.getColor(R.color.text_primary))
typeSpinner.setBackgroundDrawable(drawable)
typeSpinner.adapter = typeAdapter
typeSpinner.setSelection(if (repeatAfterCompletion) TYPE_COMPLETION_DATE else TYPE_DUE_DATE)
refreshDisplayView()
return view
}
@OnItemSelected(R.id.repeatType)
fun onRepeatTypeChanged(position: Int) {
repeatAfterCompletion = position == TYPE_COMPLETION_DATE
repeatTypes[0] = if (repeatAfterCompletion) repeatTypes[2] else repeatTypes[1]
typeAdapter.notifyDataSetChanged()
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putString(EXTRA_RECURRENCE, if (rrule == null) "" else rrule!!.toIcal())
outState.putBoolean(EXTRA_REPEAT_AFTER_COMPLETION, repeatAfterCompletion)
outState.putLong(EXTRA_DUE_DATE, dueDate)
}
override fun inject(component: FragmentComponent) = component.inject(this)
override fun onRowClick() {
BasicRecurrenceDialog.newBasicRecurrenceDialog(this, rrule, dueDate)
.show(parentFragmentManager, FRAG_TAG_BASIC_RECURRENCE)
}
override val isClickable: Boolean
get() = true
override val layout: Int
get() = R.layout.control_set_repeat_display
override val icon: Int
get() = R.drawable.ic_outline_repeat_24px
override fun controlId() = TAG
override fun hasChanges(original: Task): Boolean {
return (recurrenceValue != original.recurrence
|| original.repeatUntil
!= if (rrule == null) 0 else DateTime.from(rrule!!.until).millis)
}
override fun apply(task: Task) {
task.repeatUntil = if (rrule == null) 0 else DateTime.from(rrule!!.until).millis
task.recurrence = recurrenceValue
}
private val recurrenceValue: String
get() {
if (rrule == null) {
return ""
}
val copy: RRule = try {
RRule(rrule!!.toIcal())
} catch (e: ParseException) {
return ""
}
copy.until = null
var result = copy.toIcal()
if (repeatAfterCompletion && !isNullOrEmpty(result)) {
result += ";FROM=COMPLETION" // $NON-NLS-1$
}
return result
}
private fun refreshDisplayView() {
if (rrule == null) {
displayView.text = null
repeatTypeContainer.visibility = View.GONE
} else {
displayView.text = repeatRuleToString.toString(rrule)
repeatTypeContainer.visibility = View.VISIBLE
}
}
companion object {
const val TAG = R.string.TEA_ctrl_repeat_pref
private const val TYPE_DUE_DATE = 1
private const val TYPE_COMPLETION_DATE = 2
private const val FRAG_TAG_BASIC_RECURRENCE = "frag_tag_basic_recurrence"
private const val EXTRA_RECURRENCE = "extra_recurrence"
private const val EXTRA_DUE_DATE = "extra_due_date"
private const val EXTRA_REPEAT_AFTER_COMPLETION = "extra_repeat_after_completion"
}
}

@ -1,188 +0,0 @@
/*
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.tags;
import static android.app.Activity.RESULT_OK;
import static com.google.common.collect.FluentIterable.from;
import static com.google.common.collect.Lists.newArrayList;
import static com.google.common.collect.Sets.newHashSet;
import android.content.Intent;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.Nullable;
import butterknife.BindView;
import com.google.android.material.chip.Chip;
import com.google.android.material.chip.ChipGroup;
import com.google.common.base.Predicates;
import com.google.common.collect.Ordering;
import com.todoroo.andlib.utility.DateUtilities;
import com.todoroo.astrid.data.Task;
import java.util.ArrayList;
import java.util.Set;
import javax.inject.Inject;
import org.tasks.R;
import org.tasks.data.TagDao;
import org.tasks.data.TagData;
import org.tasks.data.TagDataDao;
import org.tasks.injection.FragmentComponent;
import org.tasks.tags.TagPickerActivity;
import org.tasks.ui.ChipProvider;
import org.tasks.ui.TaskEditControlFragment;
/**
* Control set to manage adding and removing tags
*
* @author Tim Su <tim@todoroo.com>
*/
public final class TagsControlSet extends TaskEditControlFragment {
public static final int TAG = R.string.TEA_ctrl_lists_pref;
private static final String EXTRA_ORIGINAL_TAGS = "extra_original_tags";
private static final String EXTRA_SELECTED_TAGS = "extra_selected_tags";
private static final int REQUEST_TAG_PICKER_ACTIVITY = 10582;
private final Ordering<TagData> orderByName =
new Ordering<TagData>() {
@Override
public int compare(TagData left, TagData right) {
return left.getName().compareTo(right.getName());
}
};
@Inject TagDao tagDao;
@Inject TagDataDao tagDataDao;
@Inject ChipProvider chipProvider;
@BindView(R.id.no_tags)
TextView tagsDisplay;
@BindView(R.id.chip_group)
ChipGroup chipGroup;
private ArrayList<TagData> originalTags;
private ArrayList<TagData> selectedTags;
@Nullable
@Override
public View onCreateView(
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = super.onCreateView(inflater, container, savedInstanceState);
if (savedInstanceState != null) {
selectedTags = savedInstanceState.getParcelableArrayList(EXTRA_SELECTED_TAGS);
originalTags = savedInstanceState.getParcelableArrayList(EXTRA_ORIGINAL_TAGS);
} else {
originalTags =
newArrayList(
task.isNew()
? from(task.getTags())
.transform(tagDataDao::getTagByName)
.filter(Predicates.notNull())
: tagDataDao.getTagDataForTask(task.getId()));
selectedTags = new ArrayList<>(originalTags);
}
refreshDisplayView();
return view;
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putParcelableArrayList(EXTRA_SELECTED_TAGS, selectedTags);
outState.putParcelableArrayList(EXTRA_ORIGINAL_TAGS, originalTags);
}
@Override
protected int getLayout() {
return R.layout.control_set_tags;
}
@Override
public void apply(Task task) {
if (tagDao.applyTags(task, tagDataDao, selectedTags)) {
task.setModificationDate(DateUtilities.now());
}
}
@Override
protected void onRowClick() {
Intent intent = new Intent(getContext(), TagPickerActivity.class);
intent.putParcelableArrayListExtra(TagPickerActivity.EXTRA_SELECTED, selectedTags);
startActivityForResult(intent, REQUEST_TAG_PICKER_ACTIVITY);
}
@Override
protected boolean isClickable() {
return true;
}
@Override
public int getIcon() {
return R.drawable.ic_outline_label_24px;
}
@Override
public int controlId() {
return TAG;
}
@Override
public boolean hasChanges(Task original) {
Set<TagData> originalSet = newHashSet(originalTags);
Set<TagData> selectedSet = newHashSet(selectedTags);
return !originalSet.equals(selectedSet);
}
private void refreshDisplayView() {
if (selectedTags.isEmpty()) {
chipGroup.setVisibility(View.GONE);
tagsDisplay.setVisibility(View.VISIBLE);
} else {
tagsDisplay.setVisibility(View.GONE);
chipGroup.setVisibility(View.VISIBLE);
chipGroup.removeAllViews();
for (TagData tagData : orderByName.sortedCopy(selectedTags)) {
if (tagData == null) {
continue;
}
Chip chip = chipProvider.newClosableChip(tagData);
chipProvider.apply(chip, tagData);
chip.setOnClickListener(view -> onRowClick());
chip.setOnCloseIconClickListener(
view -> {
selectedTags.remove(tagData);
refreshDisplayView();
});
chipGroup.addView(chip);
}
}
}
@Override
public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
if (requestCode == REQUEST_TAG_PICKER_ACTIVITY) {
if (resultCode == RESULT_OK && data != null) {
selectedTags = data.getParcelableArrayListExtra(TagPickerActivity.EXTRA_SELECTED);
refreshDisplayView();
}
} else {
super.onActivityResult(requestCode, resultCode, data);
}
}
@Override
protected void inject(FragmentComponent component) {
component.inject(this);
}
@Override
public boolean requiresId() {
return true;
}
}

@ -0,0 +1,152 @@
/*
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.tags
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import butterknife.BindView
import com.google.android.material.chip.ChipGroup
import com.google.common.collect.Ordering
import com.todoroo.andlib.utility.DateUtilities
import com.todoroo.astrid.data.Task
import org.tasks.R
import org.tasks.data.TagDao
import org.tasks.data.TagData
import org.tasks.data.TagDataDao
import org.tasks.injection.FragmentComponent
import org.tasks.tags.TagPickerActivity
import org.tasks.ui.ChipProvider
import org.tasks.ui.TaskEditControlFragment
import java.util.*
import javax.inject.Inject
/**
* Control set to manage adding and removing tags
*
* @author Tim Su <tim></tim>@todoroo.com>
*/
class TagsControlSet : TaskEditControlFragment() {
private val orderByName: Ordering<TagData> = object : Ordering<TagData>() {
override fun compare(left: TagData?, right: TagData?): Int {
return left!!.name!!.compareTo(right!!.name!!)
}
}
@Inject lateinit var tagDao: TagDao
@Inject lateinit var tagDataDao: TagDataDao
@Inject lateinit var chipProvider: ChipProvider
@BindView(R.id.no_tags)
lateinit var tagsDisplay: TextView
@BindView(R.id.chip_group)
lateinit var chipGroup: ChipGroup
private lateinit var originalTags: ArrayList<TagData>
private lateinit var selectedTags: ArrayList<TagData>
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = super.onCreateView(inflater, container, savedInstanceState)
if (savedInstanceState != null) {
selectedTags = savedInstanceState.getParcelableArrayList(EXTRA_SELECTED_TAGS)!!
originalTags = savedInstanceState.getParcelableArrayList(EXTRA_ORIGINAL_TAGS)!!
} else {
originalTags = ArrayList(if (task.isNew) {
task.tags.mapNotNull(tagDataDao::getTagByName)
} else {
tagDataDao.getTagDataForTask(task.id)
})
selectedTags = ArrayList(originalTags)
}
refreshDisplayView()
return view
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putParcelableArrayList(EXTRA_SELECTED_TAGS, selectedTags)
outState.putParcelableArrayList(EXTRA_ORIGINAL_TAGS, originalTags)
}
override val layout: Int
get() = R.layout.control_set_tags
override fun apply(task: Task) {
if (tagDao.applyTags(task, tagDataDao, selectedTags)) {
task.modificationDate = DateUtilities.now()
}
}
override fun onRowClick() {
val intent = Intent(context, TagPickerActivity::class.java)
intent.putParcelableArrayListExtra(TagPickerActivity.EXTRA_SELECTED, selectedTags)
startActivityForResult(intent, REQUEST_TAG_PICKER_ACTIVITY)
}
override val isClickable: Boolean
get() = true
override val icon: Int
get() = R.drawable.ic_outline_label_24px
override fun controlId() = TAG
override fun hasChanges(original: Task): Boolean {
return HashSet(originalTags) != HashSet(selectedTags)
}
private fun refreshDisplayView() {
if (selectedTags.isEmpty()) {
chipGroup.visibility = View.GONE
tagsDisplay.visibility = View.VISIBLE
} else {
tagsDisplay.visibility = View.GONE
chipGroup.visibility = View.VISIBLE
chipGroup.removeAllViews()
for (tagData in orderByName.sortedCopy(selectedTags)) {
if (tagData == null) {
continue
}
val chip = chipProvider.newClosableChip(tagData)
chipProvider.apply(chip, tagData)
chip.setOnClickListener { onRowClick() }
chip.setOnCloseIconClickListener {
selectedTags.remove(tagData)
refreshDisplayView()
}
chipGroup.addView(chip)
}
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == REQUEST_TAG_PICKER_ACTIVITY) {
if (resultCode == Activity.RESULT_OK && data != null) {
selectedTags = data.getParcelableArrayListExtra(TagPickerActivity.EXTRA_SELECTED)!!
refreshDisplayView()
}
} else {
super.onActivityResult(requestCode, resultCode, data)
}
}
override fun inject(component: FragmentComponent) = component.inject(this)
override fun requiresId() = true
companion object {
const val TAG = R.string.TEA_ctrl_lists_pref
private const val EXTRA_ORIGINAL_TAGS = "extra_original_tags"
private const val EXTRA_SELECTED_TAGS = "extra_selected_tags"
private const val REQUEST_TAG_PICKER_ACTIVITY = 10582
}
}

@ -1,246 +0,0 @@
/*
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.timers;
import static org.tasks.Strings.isNullOrEmpty;
import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
import android.os.SystemClock;
import android.text.format.DateFormat;
import android.text.format.DateUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Chronometer;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import butterknife.BindView;
import butterknife.OnClick;
import com.todoroo.andlib.utility.DateUtilities;
import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.ui.TimeDurationControlSet;
import javax.inject.Inject;
import org.tasks.R;
import org.tasks.dialogs.DialogBuilder;
import org.tasks.injection.ForActivity;
import org.tasks.injection.FragmentComponent;
import org.tasks.themes.Theme;
import org.tasks.ui.TaskEditControlFragment;
/**
* Control Set for managing repeats
*
* @author Tim Su <tim@todoroo.com>
*/
public class TimerControlSet extends TaskEditControlFragment {
public static final int TAG = R.string.TEA_ctrl_timer_pref;
private static final String EXTRA_STARTED = "extra_started";
private static final String EXTRA_ESTIMATED = "extra_estimated";
private static final String EXTRA_ELAPSED = "extra_elapsed";
@Inject DialogBuilder dialogBuilder;
@Inject @ForActivity Context context;
@Inject Theme theme;
@BindView(R.id.display_row_edit)
TextView displayEdit;
@BindView(R.id.timer)
Chronometer chronometer;
@BindView(R.id.timer_button)
ImageView timerButton;
private TimeDurationControlSet estimated;
private TimeDurationControlSet elapsed;
private long timerStarted;
private AlertDialog dialog;
private View dialogView;
private TimerControlSetCallback callback;
@Nullable
@Override
public View onCreateView(
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = super.onCreateView(inflater, container, savedInstanceState);
int elapsedSeconds;
int estimatedSeconds;
if (savedInstanceState == null) {
timerStarted = task.getTimerStart();
elapsedSeconds = task.getElapsedSeconds();
estimatedSeconds = task.getEstimatedSeconds();
} else {
timerStarted = savedInstanceState.getLong(EXTRA_STARTED);
elapsedSeconds = savedInstanceState.getInt(EXTRA_ELAPSED);
estimatedSeconds = savedInstanceState.getInt(EXTRA_ESTIMATED);
}
dialogView = inflater.inflate(R.layout.control_set_timers_dialog, null);
estimated = new TimeDurationControlSet(context, dialogView, R.id.estimatedDuration, theme);
elapsed = new TimeDurationControlSet(context, dialogView, R.id.elapsedDuration, theme);
estimated.setTimeDuration(estimatedSeconds);
elapsed.setTimeDuration(elapsedSeconds);
refresh();
return view;
}
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
callback = (TimerControlSetCallback) activity;
}
@Override
protected void inject(FragmentComponent component) {
component.inject(this);
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putInt(EXTRA_ELAPSED, elapsed.getTimeDurationInSeconds());
outState.putInt(EXTRA_ESTIMATED, estimated.getTimeDurationInSeconds());
outState.putLong(EXTRA_STARTED, timerStarted);
}
@Override
protected void onRowClick() {
if (dialog == null) {
dialog = buildDialog();
}
dialog.show();
}
@Override
protected boolean isClickable() {
return true;
}
private AlertDialog buildDialog() {
return dialogBuilder
.newDialog()
.setView(dialogView)
.setPositiveButton(android.R.string.ok, (dialog12, which) -> refreshDisplayView())
.setOnCancelListener(dialog1 -> refreshDisplayView())
.create();
}
@OnClick(R.id.timer_container)
void timerClicked() {
if (timerActive()) {
Task task = callback.stopTimer();
elapsed.setTimeDuration(task.getElapsedSeconds());
timerStarted = 0;
chronometer.stop();
refreshDisplayView();
} else {
Task task = callback.startTimer();
timerStarted = task.getTimerStart();
chronometer.start();
}
updateChronometer();
}
@Override
protected int getLayout() {
return R.layout.control_set_timers;
}
@Override
public int getIcon() {
return R.drawable.ic_outline_timer_24px;
}
@Override
public int controlId() {
return TAG;
}
@Override
public boolean hasChanges(Task original) {
return elapsed.getTimeDurationInSeconds() != original.getElapsedSeconds()
|| estimated.getTimeDurationInSeconds() != original.getEstimatedSeconds();
}
@Override
public void apply(Task task) {
task.setElapsedSeconds(elapsed.getTimeDurationInSeconds());
task.setEstimatedSeconds(estimated.getTimeDurationInSeconds());
}
private void refresh() {
refreshDisplayView();
updateChronometer();
}
private void refreshDisplayView() {
String est = null;
int estimatedSeconds = estimated.getTimeDurationInSeconds();
if (estimatedSeconds > 0) {
est = getString(R.string.TEA_timer_est, DateUtils.formatElapsedTime(estimatedSeconds));
}
String elap = null;
int elapsedSeconds = elapsed.getTimeDurationInSeconds();
if (elapsedSeconds > 0) {
elap = getString(R.string.TEA_timer_elap, DateUtils.formatElapsedTime(elapsedSeconds));
}
String toDisplay;
if (!isNullOrEmpty(est) && !isNullOrEmpty(elap)) {
toDisplay = est + ", " + elap; // $NON-NLS-1$
} else if (!isNullOrEmpty(est)) {
toDisplay = est;
} else if (!isNullOrEmpty(elap)) {
toDisplay = elap;
} else {
toDisplay = null;
}
displayEdit.setText(toDisplay);
}
private void updateChronometer() {
timerButton.setImageResource(
timerActive() ? R.drawable.ic_outline_pause_24px : R.drawable.ic_outline_play_arrow_24px);
long elapsed = this.elapsed.getTimeDurationInSeconds() * 1000L;
if (timerActive()) {
chronometer.setVisibility(View.VISIBLE);
elapsed += DateUtilities.now() - timerStarted;
chronometer.setBase(SystemClock.elapsedRealtime() - elapsed);
if (elapsed > DateUtilities.ONE_DAY) {
chronometer.setOnChronometerTickListener(
cArg -> {
long t = SystemClock.elapsedRealtime() - cArg.getBase();
cArg.setText(DateFormat.format("d'd' h:mm", t)); // $NON-NLS-1$
});
}
chronometer.start();
} else {
chronometer.setVisibility(View.GONE);
chronometer.stop();
}
}
private boolean timerActive() {
return timerStarted > 0;
}
public interface TimerControlSetCallback {
Task stopTimer();
Task startTimer();
}
}

@ -0,0 +1,217 @@
/*
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.timers
import android.app.Activity
import android.content.Context
import android.os.Bundle
import android.os.SystemClock
import android.text.format.DateFormat
import android.text.format.DateUtils
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Chronometer
import android.widget.Chronometer.OnChronometerTickListener
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import butterknife.BindView
import butterknife.OnClick
import com.todoroo.andlib.utility.DateUtilities
import com.todoroo.astrid.data.Task
import com.todoroo.astrid.ui.TimeDurationControlSet
import org.tasks.R
import org.tasks.Strings.isNullOrEmpty
import org.tasks.dialogs.DialogBuilder
import org.tasks.injection.ForActivity
import org.tasks.injection.FragmentComponent
import org.tasks.themes.Theme
import org.tasks.ui.TaskEditControlFragment
import javax.inject.Inject
/**
* Control Set for managing repeats
*
* @author Tim Su <tim></tim>@todoroo.com>
*/
class TimerControlSet : TaskEditControlFragment() {
@Inject @ForActivity lateinit var activity: Context
@Inject lateinit var dialogBuilder: DialogBuilder
@Inject lateinit var theme: Theme
@BindView(R.id.display_row_edit)
lateinit var displayEdit: TextView
@BindView(R.id.timer)
lateinit var chronometer: Chronometer
@BindView(R.id.timer_button)
lateinit var timerButton: ImageView
private lateinit var estimated: TimeDurationControlSet
private lateinit var elapsed: TimeDurationControlSet
private var timerStarted: Long = 0
private var dialog: AlertDialog? = null
private lateinit var dialogView: View
private var callback: TimerControlSetCallback? = null
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = super.onCreateView(inflater, container, savedInstanceState)
val elapsedSeconds: Int
val estimatedSeconds: Int
if (savedInstanceState == null) {
timerStarted = task.timerStart
elapsedSeconds = task.elapsedSeconds
estimatedSeconds = task.estimatedSeconds
} else {
timerStarted = savedInstanceState.getLong(EXTRA_STARTED)
elapsedSeconds = savedInstanceState.getInt(EXTRA_ELAPSED)
estimatedSeconds = savedInstanceState.getInt(EXTRA_ESTIMATED)
}
dialogView = inflater.inflate(R.layout.control_set_timers_dialog, null)
estimated = TimeDurationControlSet(activity, dialogView, R.id.estimatedDuration, theme)
elapsed = TimeDurationControlSet(activity, dialogView, R.id.elapsedDuration, theme)
estimated.setTimeDuration(estimatedSeconds)
elapsed.setTimeDuration(elapsedSeconds)
refresh()
return view
}
override fun onAttach(activity: Activity) {
super.onAttach(activity)
callback = activity as TimerControlSetCallback
}
override fun inject(component: FragmentComponent) = component.inject(this)
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putInt(EXTRA_ELAPSED, elapsed.timeDurationInSeconds)
outState.putInt(EXTRA_ESTIMATED, estimated.timeDurationInSeconds)
outState.putLong(EXTRA_STARTED, timerStarted)
}
override fun onRowClick() {
if (dialog == null) {
dialog = buildDialog()
}
dialog!!.show()
}
override val isClickable: Boolean
get() = true
private fun buildDialog(): AlertDialog {
return dialogBuilder
.newDialog()
.setView(dialogView)
.setPositiveButton(android.R.string.ok) { _, _ -> refreshDisplayView() }
.setOnCancelListener { refreshDisplayView() }
.create()
}
@OnClick(R.id.timer_container)
fun timerClicked() {
if (timerActive()) {
val task = callback!!.stopTimer()
elapsed.setTimeDuration(task.elapsedSeconds)
timerStarted = 0
chronometer.stop()
refreshDisplayView()
} else {
val task = callback!!.startTimer()
timerStarted = task.timerStart
chronometer.start()
}
updateChronometer()
}
override val layout: Int
get() = R.layout.control_set_timers
override val icon: Int
get() = R.drawable.ic_outline_timer_24px
override fun controlId() = TAG
override fun hasChanges(original: Task): Boolean {
return (elapsed.timeDurationInSeconds != original.elapsedSeconds
|| estimated.timeDurationInSeconds != original.estimatedSeconds)
}
override fun apply(task: Task) {
task.elapsedSeconds = elapsed.timeDurationInSeconds
task.estimatedSeconds = estimated.timeDurationInSeconds
}
private fun refresh() {
refreshDisplayView()
updateChronometer()
}
private fun refreshDisplayView() {
var est: String? = null
val estimatedSeconds = estimated.timeDurationInSeconds
if (estimatedSeconds > 0) {
est = getString(R.string.TEA_timer_est, DateUtils.formatElapsedTime(estimatedSeconds.toLong()))
}
var elap: String? = null
val elapsedSeconds = elapsed.timeDurationInSeconds
if (elapsedSeconds > 0) {
elap = getString(R.string.TEA_timer_elap, DateUtils.formatElapsedTime(elapsedSeconds.toLong()))
}
val toDisplay: String?
toDisplay = if (!isNullOrEmpty(est) && !isNullOrEmpty(elap)) {
"$est, $elap" // $NON-NLS-1$
} else if (!isNullOrEmpty(est)) {
est
} else if (!isNullOrEmpty(elap)) {
elap
} else {
null
}
displayEdit.text = toDisplay
}
private fun updateChronometer() {
timerButton.setImageResource(
if (timerActive()) R.drawable.ic_outline_pause_24px else R.drawable.ic_outline_play_arrow_24px)
var elapsed = elapsed.timeDurationInSeconds * 1000L
if (timerActive()) {
chronometer.visibility = View.VISIBLE
elapsed += DateUtilities.now() - timerStarted
chronometer.base = SystemClock.elapsedRealtime() - elapsed
if (elapsed > DateUtilities.ONE_DAY) {
chronometer.onChronometerTickListener = OnChronometerTickListener { cArg: Chronometer ->
val t = SystemClock.elapsedRealtime() - cArg.base
cArg.text = DateFormat.format("d'd' h:mm", t) // $NON-NLS-1$
}
}
chronometer.start()
} else {
chronometer.visibility = View.GONE
chronometer.stop()
}
}
private fun timerActive(): Boolean {
return timerStarted > 0
}
interface TimerControlSetCallback {
fun stopTimer(): Task
fun startTimer(): Task
}
companion object {
const val TAG = R.string.TEA_ctrl_timer_pref
private const val EXTRA_STARTED = "extra_started"
private const val EXTRA_ESTIMATED = "extra_estimated"
private const val EXTRA_ELAPSED = "extra_elapsed"
}
}

@ -1,370 +0,0 @@
/*
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.ui;
import static com.todoroo.astrid.data.Task.HIDE_UNTIL_DAY_BEFORE;
import static com.todoroo.astrid.data.Task.HIDE_UNTIL_DUE;
import static com.todoroo.astrid.data.Task.HIDE_UNTIL_NONE;
import static com.todoroo.astrid.data.Task.HIDE_UNTIL_WEEK_BEFORE;
import static java.util.Arrays.asList;
import static org.tasks.date.DateTimeUtils.newDateTime;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.Spinner;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import butterknife.BindView;
import butterknife.OnClick;
import com.todoroo.andlib.utility.DateUtilities;
import com.todoroo.astrid.data.Task;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import org.tasks.R;
import org.tasks.activities.DateAndTimePickerActivity;
import org.tasks.dialogs.MyTimePickerDialog;
import org.tasks.injection.ForActivity;
import org.tasks.injection.FragmentComponent;
import org.tasks.locale.Locale;
import org.tasks.preferences.Preferences;
import org.tasks.themes.ThemeBase;
import org.tasks.time.DateTime;
import org.tasks.ui.HiddenTopArrayAdapter;
import org.tasks.ui.TaskEditControlFragment;
/**
* Control set for specifying when a task should be hidden
*
* @author Tim Su <tim@todoroo.com>
*/
public class HideUntilControlSet extends TaskEditControlFragment implements OnItemSelectedListener {
public static final int TAG = R.string.TEA_ctrl_hide_until_pref;
private static final String EXTRA_CUSTOM = "extra_custom";
private static final String EXTRA_SELECTION = "extra_selection";
private static final int SPECIFIC_DATE = -1;
private static final int EXISTING_TIME_UNSET = -2;
private static final int REQUEST_HIDE_UNTIL = 11011;
private final List<HideUntilValue> spinnerItems = new ArrayList<>();
@Inject @ForActivity Context context;
@Inject ThemeBase themeBase;
@Inject Preferences preferences;
@Inject Locale locale;
// private final CheckBox enabled;
@BindView(R.id.hideUntil)
Spinner spinner;
@BindView(R.id.clear)
ImageView clearButton;
private ArrayAdapter<HideUntilValue> adapter;
private int previousSetting = Task.HIDE_UNTIL_NONE;
private int selection;
private long existingDate = EXISTING_TIME_UNSET;
private HideUntilValue selectedValue;
@OnClick(R.id.clear)
void clearHideUntil() {
updateSpinnerOptions(0);
selection = 0;
spinner.setSelection(selection);
refreshDisplayView();
}
@Override
protected void onRowClick() {
spinner.performClick();
}
@Override
protected boolean isClickable() {
return true;
}
@Nullable
@Override
public View onCreateView(
final LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = super.onCreateView(inflater, container, savedInstanceState);
adapter =
new HiddenTopArrayAdapter<HideUntilValue>(
context, android.R.layout.simple_spinner_item, spinnerItems) {
@NonNull
@Override
public View getView(int position, View convertView, @NonNull ViewGroup parent) {
int selectedItemPosition = position;
if (parent instanceof AdapterView) {
selectedItemPosition = ((AdapterView) parent).getSelectedItemPosition();
}
TextView tv =
(TextView) inflater.inflate(android.R.layout.simple_spinner_item, parent, false);
tv.setPadding(0, 0, 0, 0);
HideUntilValue value = getItem(selectedItemPosition);
if (value.setting == Task.HIDE_UNTIL_NONE) {
clearButton.setVisibility(View.GONE);
tv.setText(value.labelDisplay);
tv.setTextColor(context.getColor(R.color.text_tertiary));
} else {
String display = value.labelDisplay;
tv.setText(getString(R.string.TEA_hideUntil_display, display));
tv.setTextColor(context.getColor(R.color.text_primary));
}
return tv;
}
};
if (savedInstanceState == null) {
long dueDate = task.getDueDate();
long hideUntil = task.getHideUntil();
DateTime dueDay =
newDateTime(dueDate)
.withHourOfDay(0)
.withMinuteOfHour(0)
.withSecondOfMinute(0)
.withMillisOfSecond(0);
// For the hide until due case, we need the time component
long dueTime = dueDate / 1000L * 1000L;
if (hideUntil <= 0) {
selection = 0;
hideUntil = 0;
if (task.isNew()) {
int defaultHideUntil =
preferences.getIntegerFromString(R.string.p_default_hideUntil_key, HIDE_UNTIL_NONE);
switch (defaultHideUntil) {
case HIDE_UNTIL_DUE:
selection = 1;
break;
case HIDE_UNTIL_DAY_BEFORE:
selection = 3;
break;
case HIDE_UNTIL_WEEK_BEFORE:
selection = 4;
break;
}
}
} else if (hideUntil == dueDay.getMillis()) {
selection = 1;
hideUntil = 0;
} else if (hideUntil == dueTime) {
selection = 2;
hideUntil = 0;
} else if (hideUntil + DateUtilities.ONE_DAY == dueDay.getMillis()) {
selection = 3;
hideUntil = 0;
} else if (hideUntil + DateUtilities.ONE_WEEK == dueDay.getMillis()) {
selection = 4;
hideUntil = 0;
}
updateSpinnerOptions(hideUntil);
} else {
updateSpinnerOptions(savedInstanceState.getLong(EXTRA_CUSTOM));
selection = savedInstanceState.getInt(EXTRA_SELECTION);
}
spinner.setAdapter(adapter);
spinner.setSelection(selection);
spinner.setOnItemSelectedListener(this);
refreshDisplayView();
return view;
}
@Override
protected int getLayout() {
return R.layout.control_set_hide;
}
@Override
protected int getIcon() {
return R.drawable.ic_outline_visibility_off_24px;
}
@Override
public int controlId() {
return TAG;
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_HIDE_UNTIL) {
if (resultCode == Activity.RESULT_OK) {
setCustomDate(data.getLongExtra(MyTimePickerDialog.EXTRA_TIMESTAMP, 0L));
}
} else {
super.onActivityResult(requestCode, resultCode, data);
}
}
@Override
public void apply(Task task) {
task.setHideUntil(getHideUntil(task));
}
@Override
public boolean hasChanges(Task original) {
return original.getHideUntil() != getHideUntil(original);
}
private long getHideUntil(Task task) {
return task.createHideUntil(selectedValue.setting, selectedValue.date);
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putLong(EXTRA_CUSTOM, existingDate);
outState.putInt(EXTRA_SELECTION, selection);
}
@Override
protected void inject(FragmentComponent component) {
component.inject(this);
}
private void updateSpinnerOptions(long specificDate) {
spinnerItems.clear();
// set up base values
String[] labelsSpinner = getResources().getStringArray(R.array.TEA_hideUntil_spinner);
String[] labelsDisplay = getResources().getStringArray(R.array.TEA_hideUntil_display);
spinnerItems.addAll(
new ArrayList<>(
asList(
new HideUntilValue(labelsSpinner[0], labelsDisplay[0], Task.HIDE_UNTIL_DUE),
new HideUntilValue(labelsSpinner[1], labelsDisplay[1], Task.HIDE_UNTIL_DUE_TIME),
new HideUntilValue(labelsSpinner[2], labelsDisplay[2], Task.HIDE_UNTIL_DAY_BEFORE),
new HideUntilValue(labelsSpinner[3], labelsDisplay[3], Task.HIDE_UNTIL_WEEK_BEFORE),
new HideUntilValue(
labelsSpinner[4],
"",
Task.HIDE_UNTIL_SPECIFIC_DAY,
-1)))); // no need for a string for display here, since the chosen day will be
// displayed
if (specificDate > 0) {
spinnerItems.add(0, getHideUntilValue(specificDate));
existingDate = specificDate;
} else {
spinnerItems.add(
0, new HideUntilValue(getString(R.string.TEA_hideUntil_label), Task.HIDE_UNTIL_NONE));
existingDate = EXISTING_TIME_UNSET;
}
adapter.notifyDataSetChanged();
}
private HideUntilValue getHideUntilValue(long timestamp) {
DateTime hideUntilAsDate = newDateTime(timestamp);
if (hideUntilAsDate.getHourOfDay() == 0
&& hideUntilAsDate.getMinuteOfHour() == 0
&& hideUntilAsDate.getSecondOfMinute() == 0) {
return new HideUntilValue(
DateUtilities.getDateString(context, newDateTime(timestamp)),
Task.HIDE_UNTIL_SPECIFIC_DAY,
timestamp);
} else {
return new HideUntilValue(
DateUtilities.getLongDateStringWithTime(timestamp, locale.getLocale()),
Task.HIDE_UNTIL_SPECIFIC_DAY_TIME,
timestamp);
}
}
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
// if specific date selected, show dialog
// ... at conclusion of dialog, update our list
HideUntilValue item = adapter.getItem(position);
if (item.date == SPECIFIC_DATE) {
final DateTime customDate =
newDateTime(existingDate == EXISTING_TIME_UNSET ? DateUtilities.now() : existingDate)
.withSecondOfMinute(0);
final Activity activity = getActivity();
Intent intent = new Intent(activity, DateAndTimePickerActivity.class);
intent.putExtra(DateAndTimePickerActivity.EXTRA_TIMESTAMP, customDate.getMillis());
startActivityForResult(intent, REQUEST_HIDE_UNTIL);
spinner.setSelection(previousSetting);
} else {
previousSetting = position;
}
selection = spinner.getSelectedItemPosition();
refreshDisplayView();
}
// --- listening for events
private void setCustomDate(long timestamp) {
updateSpinnerOptions(timestamp);
spinner.setSelection(0);
refreshDisplayView();
}
@Override
public void onNothingSelected(AdapterView<?> arg0) {
// ignore
}
private void refreshDisplayView() {
selectedValue = adapter.getItem(selection);
clearButton.setVisibility(
selectedValue.setting == Task.HIDE_UNTIL_NONE ? View.GONE : View.VISIBLE);
}
// --- setting up values
/**
* Container class for urgencies
*
* @author Tim Su <tim@todoroo.com>
*/
private static class HideUntilValue {
final String labelSpinner;
final String labelDisplay;
final int setting;
final long date;
HideUntilValue(String label, int setting) {
this(label, label, setting, 0);
}
HideUntilValue(String labelSpinner, String labelDisplay, int setting) {
this(labelSpinner, labelDisplay, setting, 0);
}
HideUntilValue(String label, int setting, long date) {
this(label, label, setting, date);
}
HideUntilValue(String labelSpinner, String labelDisplay, int setting, long date) {
this.labelSpinner = labelSpinner;
this.labelDisplay = labelDisplay;
this.setting = setting;
this.date = date;
}
@Override
public String toString() {
return labelSpinner;
}
}
}

@ -0,0 +1,285 @@
/*
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.ui
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.*
import android.widget.AdapterView.OnItemSelectedListener
import butterknife.BindView
import butterknife.OnClick
import com.todoroo.andlib.utility.DateUtilities
import com.todoroo.astrid.data.Task
import org.tasks.R
import org.tasks.activities.DateAndTimePickerActivity
import org.tasks.date.DateTimeUtils
import org.tasks.dialogs.MyTimePickerDialog
import org.tasks.injection.ForActivity
import org.tasks.injection.FragmentComponent
import org.tasks.locale.Locale
import org.tasks.preferences.Preferences
import org.tasks.themes.ThemeBase
import org.tasks.ui.HiddenTopArrayAdapter
import org.tasks.ui.TaskEditControlFragment
import java.util.*
import javax.inject.Inject
/**
* Control set for specifying when a task should be hidden
*
* @author Tim Su <tim></tim>@todoroo.com>
*/
class HideUntilControlSet : TaskEditControlFragment(), OnItemSelectedListener {
private val spinnerItems: MutableList<HideUntilValue> = ArrayList()
@Inject @ForActivity lateinit var activity: Context
@Inject lateinit var themeBase: ThemeBase
@Inject lateinit var preferences: Preferences
@Inject lateinit var locale: Locale
@BindView(R.id.hideUntil)
lateinit var spinner: Spinner
@BindView(R.id.clear)
lateinit var clearButton: ImageView
private lateinit var adapter: ArrayAdapter<HideUntilValue>
private var previousSetting = Task.HIDE_UNTIL_NONE
private var selection = 0
private var existingDate = EXISTING_TIME_UNSET.toLong()
private var selectedValue: HideUntilValue? = null
@OnClick(R.id.clear)
fun clearHideUntil() {
updateSpinnerOptions(0)
selection = 0
spinner.setSelection(selection)
refreshDisplayView()
}
override fun onRowClick() {
spinner.performClick()
}
override val isClickable: Boolean
get() = true
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = super.onCreateView(inflater, container, savedInstanceState)
adapter = object : HiddenTopArrayAdapter<HideUntilValue>(
activity, android.R.layout.simple_spinner_item, spinnerItems) {
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
var selectedItemPosition = position
if (parent is AdapterView<*>) {
selectedItemPosition = parent.selectedItemPosition
}
val tv = inflater.inflate(android.R.layout.simple_spinner_item, parent, false) as TextView
tv.setPadding(0, 0, 0, 0)
val value = getItem(selectedItemPosition)
if (value!!.setting == Task.HIDE_UNTIL_NONE) {
clearButton.visibility = View.GONE
tv.text = value.labelDisplay
tv.setTextColor(activity.getColor(R.color.text_tertiary))
} else {
val display = value.labelDisplay
tv.text = getString(R.string.TEA_hideUntil_display, display)
tv.setTextColor(activity.getColor(R.color.text_primary))
}
return tv
}
}
if (savedInstanceState == null) {
val dueDate = task.dueDate
var hideUntil = task.hideUntil
val dueDay = DateTimeUtils.newDateTime(dueDate)
.withHourOfDay(0)
.withMinuteOfHour(0)
.withSecondOfMinute(0)
.withMillisOfSecond(0)
// For the hide until due case, we need the time component
val dueTime = dueDate / 1000L * 1000L
if (hideUntil <= 0) {
selection = 0
hideUntil = 0
if (task.isNew) {
when (preferences.getIntegerFromString(R.string.p_default_hideUntil_key, Task.HIDE_UNTIL_NONE)) {
Task.HIDE_UNTIL_DUE -> selection = 1
Task.HIDE_UNTIL_DAY_BEFORE -> selection = 3
Task.HIDE_UNTIL_WEEK_BEFORE -> selection = 4
}
}
} else if (hideUntil == dueDay.millis) {
selection = 1
hideUntil = 0
} else if (hideUntil == dueTime) {
selection = 2
hideUntil = 0
} else if (hideUntil + DateUtilities.ONE_DAY == dueDay.millis) {
selection = 3
hideUntil = 0
} else if (hideUntil + DateUtilities.ONE_WEEK == dueDay.millis) {
selection = 4
hideUntil = 0
}
updateSpinnerOptions(hideUntil)
} else {
updateSpinnerOptions(savedInstanceState.getLong(EXTRA_CUSTOM))
selection = savedInstanceState.getInt(EXTRA_SELECTION)
}
spinner.adapter = adapter
spinner.setSelection(selection)
spinner.onItemSelectedListener = this
refreshDisplayView()
return view
}
override val layout: Int
get() = R.layout.control_set_hide
override val icon: Int
get() = R.drawable.ic_outline_visibility_off_24px
override fun controlId(): Int {
return TAG
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == REQUEST_HIDE_UNTIL) {
if (resultCode == Activity.RESULT_OK) {
setCustomDate(data!!.getLongExtra(MyTimePickerDialog.EXTRA_TIMESTAMP, 0L))
}
} else {
super.onActivityResult(requestCode, resultCode, data)
}
}
override fun apply(task: Task) {
task.hideUntil = getHideUntil(task)
}
override fun hasChanges(original: Task): Boolean {
return original.hideUntil != getHideUntil(original)
}
private fun getHideUntil(task: Task): Long {
return task.createHideUntil(selectedValue!!.setting, selectedValue!!.date)
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putLong(EXTRA_CUSTOM, existingDate)
outState.putInt(EXTRA_SELECTION, selection)
}
override fun inject(component: FragmentComponent) {
component.inject(this)
}
private fun updateSpinnerOptions(specificDate: Long) {
spinnerItems.clear()
// set up base values
val labelsSpinner = resources.getStringArray(R.array.TEA_hideUntil_spinner)
val labelsDisplay = resources.getStringArray(R.array.TEA_hideUntil_display)
spinnerItems.addAll(
ArrayList(
listOf(
HideUntilValue(labelsSpinner[0], labelsDisplay[0], Task.HIDE_UNTIL_DUE),
HideUntilValue(labelsSpinner[1], labelsDisplay[1], Task.HIDE_UNTIL_DUE_TIME),
HideUntilValue(labelsSpinner[2], labelsDisplay[2], Task.HIDE_UNTIL_DAY_BEFORE),
HideUntilValue(labelsSpinner[3], labelsDisplay[3], Task.HIDE_UNTIL_WEEK_BEFORE),
HideUntilValue(
labelsSpinner[4],
"",
Task.HIDE_UNTIL_SPECIFIC_DAY,
-1)))) // no need for a string for display here, since the chosen day will be
// displayed
existingDate = if (specificDate > 0) {
spinnerItems.add(0, getHideUntilValue(specificDate))
specificDate
} else {
spinnerItems.add(
0, HideUntilValue(getString(R.string.TEA_hideUntil_label), Task.HIDE_UNTIL_NONE))
EXISTING_TIME_UNSET.toLong()
}
adapter.notifyDataSetChanged()
}
private fun getHideUntilValue(timestamp: Long): HideUntilValue {
val hideUntilAsDate = DateTimeUtils.newDateTime(timestamp)
return if (hideUntilAsDate.hourOfDay == 0 && hideUntilAsDate.minuteOfHour == 0 && hideUntilAsDate.secondOfMinute == 0) {
HideUntilValue(
DateUtilities.getDateString(context, DateTimeUtils.newDateTime(timestamp)),
Task.HIDE_UNTIL_SPECIFIC_DAY,
timestamp)
} else {
HideUntilValue(
DateUtilities.getLongDateStringWithTime(timestamp, locale.locale),
Task.HIDE_UNTIL_SPECIFIC_DAY_TIME,
timestamp)
}
}
override fun onItemSelected(parent: AdapterView<*>, view: View?, position: Int, id: Long) {
// if specific date selected, show dialog
// ... at conclusion of dialog, update our list
val item = adapter.getItem(position)
if (item!!.date == SPECIFIC_DATE.toLong()) {
val customDate = DateTimeUtils.newDateTime(if (existingDate == EXISTING_TIME_UNSET.toLong()) DateUtilities.now() else existingDate)
.withSecondOfMinute(0)
val intent = Intent(activity, DateAndTimePickerActivity::class.java)
intent.putExtra(DateAndTimePickerActivity.EXTRA_TIMESTAMP, customDate.millis)
startActivityForResult(intent, REQUEST_HIDE_UNTIL)
spinner.setSelection(previousSetting)
} else {
previousSetting = position
}
selection = spinner.selectedItemPosition
refreshDisplayView()
}
// --- listening for events
private fun setCustomDate(timestamp: Long) {
updateSpinnerOptions(timestamp)
spinner.setSelection(0)
refreshDisplayView()
}
override fun onNothingSelected(arg0: AdapterView<*>?) {
// ignore
}
private fun refreshDisplayView() {
selectedValue = adapter.getItem(selection)
clearButton.visibility = if (selectedValue!!.setting == Task.HIDE_UNTIL_NONE) View.GONE else View.VISIBLE
}
private class HideUntilValue @JvmOverloads internal constructor(val labelSpinner: String, val labelDisplay: String, val setting: Int, val date: Long = 0) {
internal constructor(label: String, setting: Int) : this(label, label, setting, 0)
internal constructor(label: String, setting: Int, date: Long) : this(label, label, setting, date)
override fun toString(): String {
return labelSpinner
}
}
companion object {
const val TAG = R.string.TEA_ctrl_hide_until_pref
private const val EXTRA_CUSTOM = "extra_custom"
private const val EXTRA_SELECTION = "extra_selection"
private const val SPECIFIC_DATE = -1
private const val EXISTING_TIME_UNSET = -2
private const val REQUEST_HIDE_UNTIL = 11011
}
}

@ -1,70 +0,0 @@
/*
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.ui;
import android.content.Context;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Spinner;
import com.todoroo.andlib.utility.DateUtilities;
import org.tasks.R;
/**
* Control set dealing with random reminder settings
*
* @author Tim Su <tim@todoroo.com>
*/
class RandomReminderControlSet {
private final int[] hours;
private int selectedIndex;
public RandomReminderControlSet(Context context, View parentView, long reminderPeriod) {
Spinner periodSpinner = parentView.findViewById(R.id.reminder_random_interval);
periodSpinner.setVisibility(View.VISIBLE);
// create adapter
ArrayAdapter<String> adapter =
new ArrayAdapter<>(
context,
android.R.layout.simple_spinner_item,
context.getResources().getStringArray(R.array.TEA_reminder_random));
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
periodSpinner.setAdapter(adapter);
periodSpinner.setOnItemSelectedListener(
new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
selectedIndex = position;
}
@Override
public void onNothingSelected(AdapterView<?> parent) {}
});
// create hour array
String[] hourStrings = context.getResources().getStringArray(R.array.TEA_reminder_random_hours);
hours = new int[hourStrings.length];
for (int i = 0; i < hours.length; i++) {
hours[i] = Integer.parseInt(hourStrings[i]);
}
int i;
for (i = 0; i < hours.length - 1; i++) {
if (hours[i] * DateUtilities.ONE_HOUR >= reminderPeriod) {
break;
}
}
periodSpinner.setSelection(i);
}
public long getReminderPeriod() {
int hourValue = hours[selectedIndex];
return hourValue * DateUtilities.ONE_HOUR;
}
}

@ -0,0 +1,64 @@
/*
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.ui
import android.content.Context
import android.view.View
import android.widget.AdapterView
import android.widget.AdapterView.OnItemSelectedListener
import android.widget.ArrayAdapter
import android.widget.Spinner
import com.todoroo.andlib.utility.DateUtilities
import org.tasks.R
/**
* Control set dealing with random reminder settings
*
* @author Tim Su <tim></tim>@todoroo.com>
*/
internal class RandomReminderControlSet(context: Context, parentView: View, reminderPeriod: Long) {
private val hours: IntArray
private var selectedIndex = 0
val reminderPeriod: Long
get() {
val hourValue = hours[selectedIndex]
return hourValue * DateUtilities.ONE_HOUR
}
init {
val periodSpinner = parentView.findViewById<Spinner>(R.id.reminder_random_interval)
periodSpinner.visibility = View.VISIBLE
// create adapter
val adapter = ArrayAdapter(
context,
android.R.layout.simple_spinner_item,
context.resources.getStringArray(R.array.TEA_reminder_random))
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
periodSpinner.adapter = adapter
periodSpinner.onItemSelectedListener = object : OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View, position: Int, id: Long) {
selectedIndex = position
}
override fun onNothingSelected(parent: AdapterView<*>?) {}
}
// create hour array
val hourStrings = context.resources.getStringArray(R.array.TEA_reminder_random_hours)
hours = IntArray(hourStrings.size)
for (i in hours.indices) {
hours[i] = hourStrings[i].toInt()
}
var i = 0
while (i < hours.size - 1) {
if (hours[i] * DateUtilities.ONE_HOUR >= reminderPeriod) {
break
}
i++
}
periodSpinner.setSelection(i)
}
}

@ -1,352 +0,0 @@
/*
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.ui;
import static com.google.common.collect.Lists.transform;
import static com.google.common.collect.Sets.newHashSet;
import static com.todoroo.andlib.utility.DateUtilities.getLongDateStringWithTime;
import static com.todoroo.astrid.data.Task.NO_ID;
import static java.util.Collections.emptyList;
import static org.tasks.date.DateTimeUtils.newDateTime;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.graphics.Paint;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import butterknife.BindView;
import butterknife.OnClick;
import com.google.common.primitives.Longs;
import com.todoroo.andlib.utility.DateUtilities;
import com.todoroo.astrid.alarms.AlarmService;
import com.todoroo.astrid.data.Task;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import org.tasks.R;
import org.tasks.activities.DateAndTimePickerActivity;
import org.tasks.data.Alarm;
import org.tasks.dialogs.DialogBuilder;
import org.tasks.dialogs.MyTimePickerDialog;
import org.tasks.injection.ForActivity;
import org.tasks.injection.FragmentComponent;
import org.tasks.locale.Locale;
import org.tasks.ui.TaskEditControlFragment;
/**
* Control set dealing with reminder settings
*
* @author Tim Su <tim@todoroo.com>
*/
public class ReminderControlSet extends TaskEditControlFragment {
public static final int TAG = R.string.TEA_ctrl_reminders_pref;
private static final int REQUEST_NEW_ALARM = 12152;
private static final String EXTRA_FLAGS = "extra_flags";
private static final String EXTRA_RANDOM_REMINDER = "extra_random_reminder";
private static final String EXTRA_ALARMS = "extra_alarms";
private final Set<Long> alarms = new LinkedHashSet<>();
@Inject AlarmService alarmService;
@Inject @ForActivity Context context;
@Inject Locale locale;
@Inject DialogBuilder dialogBuilder;
@BindView(R.id.alert_container)
LinearLayout alertContainer;
@BindView(R.id.reminder_alarm)
TextView mode;
private long taskId;
private int flags;
private long randomReminder;
private int ringMode;
private RandomReminderControlSet randomControlSet;
private boolean whenDue;
private boolean whenOverdue;
@Nullable
@Override
public View onCreateView(
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = super.onCreateView(inflater, container, savedInstanceState);
mode.setPaintFlags(mode.getPaintFlags() | Paint.UNDERLINE_TEXT_FLAG);
taskId = task.getId();
if (savedInstanceState == null) {
flags = task.getReminderFlags();
randomReminder = task.getReminderPeriod();
setup(currentAlarms());
} else {
flags = savedInstanceState.getInt(EXTRA_FLAGS);
randomReminder = savedInstanceState.getLong(EXTRA_RANDOM_REMINDER);
setup(Longs.asList(savedInstanceState.getLongArray(EXTRA_ALARMS)));
}
return view;
}
private List<Long> currentAlarms() {
return taskId == NO_ID
? emptyList()
: transform(alarmService.getAlarms(taskId), Alarm::getTime);
}
@OnClick(R.id.reminder_alarm)
void onClickRingType() {
String[] modes = getResources().getStringArray(R.array.reminder_ring_modes);
dialogBuilder
.newDialog()
.setSingleChoiceItems(modes, ringMode, (dialog, which) -> {
setRingMode(which);
dialog.dismiss();
})
.show();
}
private void setRingMode(int ringMode) {
this.ringMode = ringMode;
mode.setText(getRingModeString(ringMode));
}
private @StringRes int getRingModeString(int ringMode) {
switch (ringMode) {
case 2:
return R.string.ring_nonstop;
case 1:
return R.string.ring_five_times;
default:
return R.string.ring_once;
}
}
private void addAlarm(String selected) {
if (selected.equals(getString(R.string.when_due))) {
addDue();
} else if (selected.equals(getString(R.string.when_overdue))) {
addOverdue();
} else if (selected.equals(getString(R.string.randomly))) {
addRandomReminder(TimeUnit.DAYS.toMillis(14));
} else if (selected.equals(getString(R.string.pick_a_date_and_time))) {
addNewAlarm();
}
}
@OnClick(R.id.alarms_add)
void addAlarm() {
List<String> options = getOptions();
if (options.size() == 1) {
addNewAlarm();
} else {
dialogBuilder
.newDialog()
.setItems(
options,
(dialog, which) -> {
addAlarm(options.get(which));
dialog.dismiss();
})
.show();
}
}
@Override
protected int getLayout() {
return R.layout.control_set_reminders;
}
@Override
public int getIcon() {
return R.drawable.ic_outline_notifications_24px;
}
@Override
public int controlId() {
return TAG;
}
private void setup(List<Long> alarms) {
setValue(flags);
alertContainer.removeAllViews();
if (whenDue) {
addDue();
}
if (whenOverdue) {
addOverdue();
}
if (randomReminder > 0) {
addRandomReminder(randomReminder);
}
for (long timestamp : alarms) {
addAlarmRow(timestamp);
}
}
@Override
public boolean hasChanges(Task original) {
return getFlags() != original.getReminderFlags()
|| getRandomReminderPeriod() != original.getReminderPeriod()
|| !newHashSet(currentAlarms()).equals(alarms);
}
@Override
public boolean requiresId() {
return true;
}
@Override
public void apply(Task task) {
task.setReminderFlags(getFlags());
task.setReminderPeriod(getRandomReminderPeriod());
if (alarmService.synchronizeAlarms(task.getId(), alarms)) {
task.setModificationDate(DateUtilities.now());
}
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putInt(EXTRA_FLAGS, getFlags());
outState.putLong(EXTRA_RANDOM_REMINDER, getRandomReminderPeriod());
outState.putLongArray(EXTRA_ALARMS, Longs.toArray(alarms));
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_NEW_ALARM) {
if (resultCode == Activity.RESULT_OK) {
addAlarmRow(data.getLongExtra(MyTimePickerDialog.EXTRA_TIMESTAMP, 0L));
}
} else {
super.onActivityResult(requestCode, resultCode, data);
}
}
private void addAlarmRow(final Long timestamp) {
if (alarms.add(timestamp)) {
addAlarmRow(getLongDateStringWithTime(timestamp, locale.getLocale()), v -> alarms.remove(timestamp));
}
}
private int getFlags() {
int value = 0;
if (whenDue) {
value |= Task.NOTIFY_AT_DEADLINE;
}
if (whenOverdue) {
value |= Task.NOTIFY_AFTER_DEADLINE;
}
value &= ~(Task.NOTIFY_MODE_FIVE | Task.NOTIFY_MODE_NONSTOP);
if (ringMode == 2) {
value |= Task.NOTIFY_MODE_NONSTOP;
} else if (ringMode == 1) {
value |= Task.NOTIFY_MODE_FIVE;
}
return value;
}
private long getRandomReminderPeriod() {
return randomControlSet == null ? 0L : randomControlSet.getReminderPeriod();
}
private void addNewAlarm() {
Intent intent = new Intent(getActivity(), DateAndTimePickerActivity.class);
intent.putExtra(
DateAndTimePickerActivity.EXTRA_TIMESTAMP, newDateTime().noon().getMillis());
startActivityForResult(intent, REQUEST_NEW_ALARM);
}
private View addAlarmRow(String text, final OnClickListener onRemove) {
final View alertItem = getActivity().getLayoutInflater().inflate(R.layout.alarm_edit_row, null);
alertContainer.addView(alertItem);
addAlarmRow(alertItem, text, onRemove);
return alertItem;
}
private void addAlarmRow(final View alertItem, String text, final View.OnClickListener onRemove) {
TextView display = alertItem.findViewById(R.id.alarm_string);
display.setText(text);
alertItem
.findViewById(R.id.clear)
.setOnClickListener(
v -> {
alertContainer.removeView(alertItem);
if (onRemove != null) {
onRemove.onClick(v);
}
});
}
private List<String> getOptions() {
List<String> options = new ArrayList<>();
if (!whenDue) {
options.add(getString(R.string.when_due));
}
if (!whenOverdue) {
options.add(getString(R.string.when_overdue));
}
if (randomControlSet == null) {
options.add(getString(R.string.randomly));
}
options.add(getString(R.string.pick_a_date_and_time));
return options;
}
private void addDue() {
whenDue = true;
addAlarmRow(getString(R.string.when_due), v -> whenDue = false);
}
private void addOverdue() {
whenOverdue = true;
addAlarmRow(getString(R.string.when_overdue), v -> whenOverdue = false);
}
private void addRandomReminder(long reminderPeriod) {
View alarmRow =
addAlarmRow(getString(R.string.randomly_once) + " ", v -> randomControlSet = null);
randomControlSet = new RandomReminderControlSet(context, alarmRow, reminderPeriod);
}
private void setValue(int flags) {
whenDue = (flags & Task.NOTIFY_AT_DEADLINE) > 0;
whenOverdue = (flags & Task.NOTIFY_AFTER_DEADLINE) > 0;
if ((flags & Task.NOTIFY_MODE_NONSTOP) > 0) {
setRingMode(2);
} else if ((flags & Task.NOTIFY_MODE_FIVE) > 0) {
setRingMode(1);
} else {
setRingMode(0);
}
}
@Override
protected void inject(FragmentComponent component) {
component.inject(this);
}
}

@ -0,0 +1,303 @@
/*
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.ui
import android.app.Activity
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.graphics.Paint
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.TextView
import androidx.annotation.StringRes
import butterknife.BindView
import butterknife.OnClick
import com.todoroo.andlib.utility.DateUtilities
import com.todoroo.astrid.alarms.AlarmService
import com.todoroo.astrid.data.Task
import org.tasks.R
import org.tasks.activities.DateAndTimePickerActivity
import org.tasks.data.Alarm
import org.tasks.date.DateTimeUtils
import org.tasks.dialogs.DialogBuilder
import org.tasks.dialogs.MyTimePickerDialog
import org.tasks.injection.ForActivity
import org.tasks.injection.FragmentComponent
import org.tasks.locale.Locale
import org.tasks.ui.TaskEditControlFragment
import java.util.*
import java.util.concurrent.TimeUnit
import javax.inject.Inject
/**
* Control set dealing with reminder settings
*
* @author Tim Su <tim></tim>@todoroo.com>
*/
class ReminderControlSet : TaskEditControlFragment() {
private val alarms: MutableSet<Long> = LinkedHashSet()
@Inject @ForActivity lateinit var activity: Context
@Inject lateinit var alarmService: AlarmService
@Inject lateinit var locale: Locale
@Inject lateinit var dialogBuilder: DialogBuilder
@BindView(R.id.alert_container)
lateinit var alertContainer: LinearLayout
@BindView(R.id.reminder_alarm)
lateinit var mode: TextView
private var taskId: Long = 0
private var flags = 0
private var randomReminder: Long = 0
private var ringMode = 0
private var randomControlSet: RandomReminderControlSet? = null
private var whenDue = false
private var whenOverdue = false
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = super.onCreateView(inflater, container, savedInstanceState)
mode.paintFlags = mode.paintFlags or Paint.UNDERLINE_TEXT_FLAG
taskId = task.id
if (savedInstanceState == null) {
flags = task.reminderFlags
randomReminder = task.reminderPeriod
setup(currentAlarms())
} else {
flags = savedInstanceState.getInt(EXTRA_FLAGS)
randomReminder = savedInstanceState.getLong(EXTRA_RANDOM_REMINDER)
setup(savedInstanceState.getLongArray(EXTRA_ALARMS)!!.toList())
}
return view
}
private fun currentAlarms(): List<Long> {
return if (taskId == Task.NO_ID) {
emptyList()
} else {
alarmService.getAlarms(taskId).map(Alarm::time)
}
}
@OnClick(R.id.reminder_alarm)
fun onClickRingType() {
val modes = resources.getStringArray(R.array.reminder_ring_modes)
dialogBuilder
.newDialog()
.setSingleChoiceItems(modes, ringMode) { dialog: DialogInterface, which: Int ->
setRingMode(which)
dialog.dismiss()
}
.show()
}
private fun setRingMode(ringMode: Int) {
this.ringMode = ringMode
mode.setText(getRingModeString(ringMode))
}
@StringRes
private fun getRingModeString(ringMode: Int): Int {
return when (ringMode) {
2 -> R.string.ring_nonstop
1 -> R.string.ring_five_times
else -> R.string.ring_once
}
}
private fun addAlarm(selected: String) {
when (selected) {
getString(R.string.when_due) -> addDue()
getString(R.string.when_overdue) -> addOverdue()
getString(R.string.randomly) -> addRandomReminder(TimeUnit.DAYS.toMillis(14))
getString(R.string.pick_a_date_and_time) -> addNewAlarm()
}
}
@OnClick(R.id.alarms_add)
fun addAlarm() {
val options = options
if (options.size == 1) {
addNewAlarm()
} else {
dialogBuilder
.newDialog()
.setItems(
options
) { dialog: DialogInterface, which: Int ->
addAlarm(options[which])
dialog.dismiss()
}
.show()
}
}
override val layout: Int
get() = R.layout.control_set_reminders
override val icon: Int
get() = R.drawable.ic_outline_notifications_24px
override fun controlId(): Int {
return TAG
}
private fun setup(alarms: List<Long>) {
setValue(flags)
alertContainer.removeAllViews()
if (whenDue) {
addDue()
}
if (whenOverdue) {
addOverdue()
}
if (randomReminder > 0) {
addRandomReminder(randomReminder)
}
for (timestamp in alarms) {
addAlarmRow(timestamp)
}
}
override fun hasChanges(original: Task): Boolean {
return getFlags() != original.reminderFlags || randomReminderPeriod != original.reminderPeriod || HashSet(currentAlarms()) != alarms
}
override fun requiresId() = true
override fun apply(task: Task) {
task.reminderFlags = getFlags()
task.reminderPeriod = randomReminderPeriod
if (alarmService.synchronizeAlarms(task.id, alarms)) {
task.modificationDate = DateUtilities.now()
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putInt(EXTRA_FLAGS, getFlags())
outState.putLong(EXTRA_RANDOM_REMINDER, randomReminderPeriod)
outState.putLongArray(EXTRA_ALARMS, alarms.toLongArray())
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == REQUEST_NEW_ALARM) {
if (resultCode == Activity.RESULT_OK) {
addAlarmRow(data!!.getLongExtra(MyTimePickerDialog.EXTRA_TIMESTAMP, 0L))
}
} else {
super.onActivityResult(requestCode, resultCode, data)
}
}
private fun addAlarmRow(timestamp: Long) {
if (alarms.add(timestamp)) {
addAlarmRow(DateUtilities.getLongDateStringWithTime(timestamp, locale.locale), View.OnClickListener { alarms.remove(timestamp) })
}
}
private fun getFlags(): Int {
var value = 0
if (whenDue) {
value = value or Task.NOTIFY_AT_DEADLINE
}
if (whenOverdue) {
value = value or Task.NOTIFY_AFTER_DEADLINE
}
value = value and (Task.NOTIFY_MODE_FIVE or Task.NOTIFY_MODE_NONSTOP).inv()
if (ringMode == 2) {
value = value or Task.NOTIFY_MODE_NONSTOP
} else if (ringMode == 1) {
value = value or Task.NOTIFY_MODE_FIVE
}
return value
}
private val randomReminderPeriod: Long
get() = if (randomControlSet == null) 0L else randomControlSet!!.reminderPeriod
private fun addNewAlarm() {
val intent = Intent(activity, DateAndTimePickerActivity::class.java)
intent.putExtra(
DateAndTimePickerActivity.EXTRA_TIMESTAMP, DateTimeUtils.newDateTime().noon().millis)
startActivityForResult(intent, REQUEST_NEW_ALARM)
}
private fun addAlarmRow(text: String, onRemove: View.OnClickListener): View {
val alertItem = requireActivity().layoutInflater.inflate(R.layout.alarm_edit_row, null)
alertContainer.addView(alertItem)
addAlarmRow(alertItem, text, onRemove)
return alertItem
}
private fun addAlarmRow(alertItem: View, text: String, onRemove: View.OnClickListener?) {
val display = alertItem.findViewById<TextView>(R.id.alarm_string)
display.text = text
alertItem
.findViewById<View>(R.id.clear)
.setOnClickListener { v: View? ->
alertContainer.removeView(alertItem)
onRemove?.onClick(v)
}
}
private val options: List<String>
get() {
val options: MutableList<String> = ArrayList()
if (!whenDue) {
options.add(getString(R.string.when_due))
}
if (!whenOverdue) {
options.add(getString(R.string.when_overdue))
}
if (randomControlSet == null) {
options.add(getString(R.string.randomly))
}
options.add(getString(R.string.pick_a_date_and_time))
return options
}
private fun addDue() {
whenDue = true
addAlarmRow(getString(R.string.when_due), View.OnClickListener { whenDue = false })
}
private fun addOverdue() {
whenOverdue = true
addAlarmRow(getString(R.string.when_overdue), View.OnClickListener { whenOverdue = false })
}
private fun addRandomReminder(reminderPeriod: Long) {
val alarmRow = addAlarmRow(getString(R.string.randomly_once) + " ", View.OnClickListener { randomControlSet = null })
randomControlSet = RandomReminderControlSet(activity, alarmRow, reminderPeriod)
}
private fun setValue(flags: Int) {
whenDue = flags and Task.NOTIFY_AT_DEADLINE > 0
whenOverdue = flags and Task.NOTIFY_AFTER_DEADLINE > 0
when {
flags and Task.NOTIFY_MODE_NONSTOP > 0 -> setRingMode(2)
flags and Task.NOTIFY_MODE_FIVE > 0 -> setRingMode(1)
else -> setRingMode(0)
}
}
override fun inject(component: FragmentComponent) = component.inject(this)
companion object {
const val TAG = R.string.TEA_ctrl_reminders_pref
private const val REQUEST_NEW_ALARM = 12152
private const val EXTRA_FLAGS = "extra_flags"
private const val EXTRA_RANDOM_REMINDER = "extra_random_reminder"
private const val EXTRA_ALARMS = "extra_alarms"
}
}

@ -1,102 +0,0 @@
/*
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.ui;
import android.content.Context;
import android.text.format.DateUtils;
import android.view.View;
import android.widget.TextView;
import com.todoroo.astrid.ui.NNumberPickerDialog.OnNNumberPickedListener;
import org.tasks.R;
import org.tasks.themes.Theme;
public class TimeDurationControlSet implements OnNNumberPickedListener, View.OnClickListener {
private final Context context;
private final Theme theme;
private final TextView timeButton;
private int timeDuration;
private int[] initialValues = null;
private NNumberPickerDialog dialog = null;
public TimeDurationControlSet(Context context, View view, int timeButtonId, Theme theme) {
this.context = context;
this.theme = theme;
timeButton = view.findViewById(timeButtonId);
((View) timeButton.getParent()).setOnClickListener(this);
}
public int getTimeDurationInSeconds() {
return timeDuration;
}
public void setTimeDuration(Integer timeDurationInSeconds) {
if (timeDurationInSeconds == null) {
timeDurationInSeconds = 0;
}
timeDuration = timeDurationInSeconds;
if (timeDurationInSeconds == 0) {
timeButton.setText(context.getString(R.string.WID_dateButtonUnset));
return;
}
timeButton.setText(DateUtils.formatElapsedTime(timeDuration));
int hours = timeDuration / 3600;
int minutes = timeDuration / 60 - 60 * hours;
initialValues = new int[] {hours, minutes};
}
/** Called when NumberPicker activity is completed */
@Override
public void onNumbersPicked(int[] values) {
setTimeDuration(values[0] * 3600 + values[1] * 60);
}
/** Called when time button is clicked */
@Override
public void onClick(View v) {
if (dialog == null) {
dialog =
new NNumberPickerDialog(
context,
this,
context.getString(R.string.DLG_hour_minutes),
new int[] {0, 0},
new int[] {1, 5},
new int[] {0, 0},
new int[] {999, 59},
new String[] {":", null});
final NumberPicker hourPicker = dialog.getPicker(0);
final NumberPicker minutePicker = dialog.getPicker(1);
minutePicker.setFormatter(value -> String.format("%02d", value));
minutePicker.setOnChangeListener(
newVal -> {
if (newVal < 0) {
if (hourPicker.getCurrent() == 0) {
return 0;
}
hourPicker.setCurrent(hourPicker.getCurrent() - 1);
return 60 + newVal;
} else if (newVal > 59) {
hourPicker.setCurrent(hourPicker.getCurrent() + 1);
return newVal % 60;
}
return newVal;
});
}
if (initialValues != null) {
dialog.setInitialValues(initialValues);
}
theme.applyToContext(dialog.getContext());
dialog.show();
}
}

@ -0,0 +1,74 @@
/*
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.ui
import android.content.Context
import android.text.format.DateUtils
import android.view.View
import android.widget.TextView
import com.todoroo.astrid.ui.NNumberPickerDialog.OnNNumberPickedListener
import org.tasks.R
import org.tasks.themes.Theme
class TimeDurationControlSet(private val context: Context, view: View, timeButtonId: Int, private val theme: Theme) : OnNNumberPickedListener, View.OnClickListener {
private val timeButton: TextView = view.findViewById(timeButtonId)
var timeDurationInSeconds = 0
private set
private var initialValues: IntArray? = null
private var dialog: NNumberPickerDialog? = null
fun setTimeDuration(timeDurationInSeconds: Int) {
this.timeDurationInSeconds = timeDurationInSeconds
if (timeDurationInSeconds == 0) {
timeButton.text = context.getString(R.string.WID_dateButtonUnset)
return
}
timeButton.text = DateUtils.formatElapsedTime(timeDurationInSeconds.toLong())
val hours = this.timeDurationInSeconds / 3600
val minutes = this.timeDurationInSeconds / 60 - 60 * hours
initialValues = intArrayOf(hours, minutes)
}
/** Called when NumberPicker activity is completed */
override fun onNumbersPicked(values: IntArray) {
setTimeDuration(values[0] * 3600 + values[1] * 60)
}
/** Called when time button is clicked */
override fun onClick(v: View) {
if (dialog == null) {
dialog = NNumberPickerDialog(
context,
this,
context.getString(R.string.DLG_hour_minutes), intArrayOf(0, 0), intArrayOf(1, 5), intArrayOf(0, 0), intArrayOf(999, 59), arrayOf(":", null))
val hourPicker = dialog!!.getPicker(0)
val minutePicker = dialog!!.getPicker(1)
minutePicker.setFormatter { value: Int -> String.format("%02d", value) }
minutePicker.setOnChangeListener { newVal: Int ->
if (newVal < 0) {
if (hourPicker.current == 0) {
return@setOnChangeListener 0
}
hourPicker.current = hourPicker.current - 1
return@setOnChangeListener 60 + newVal
} else if (newVal > 59) {
hourPicker.current = hourPicker.current + 1
return@setOnChangeListener newVal % 60
}
newVal
}
}
if (initialValues != null) {
dialog!!.setInitialValues(initialValues)
}
theme.applyToContext(dialog!!.context)
dialog!!.show()
}
init {
(timeButton.parent as View).setOnClickListener(this)
}
}

@ -49,7 +49,7 @@ class Location : Serializable, Parcelable {
val isDeparture: Boolean
get() = geofence.isDeparture
val displayName: String?
val displayName: String
get() = place.displayName
val displayAddress: String?

@ -94,13 +94,13 @@ class Place : Serializable, Parcelable {
this.icon = icon
}
val displayName: String?
val displayName: String
get() {
if (!Strings.isNullOrEmpty(name) && !COORDS.matcher(name!!).matches()) {
return name
return name!!
}
return if (!Strings.isNullOrEmpty(address)) {
address
address!!
} else {
"${formatCoordinate(latitude, true)} ${formatCoordinate(longitude, false)}"
}

@ -1,270 +0,0 @@
package org.tasks.fragments;
import static org.tasks.Strings.isNullOrEmpty;
import static org.tasks.files.ImageHelper.sampleBitmap;
import android.app.Activity;
import android.content.DialogInterface;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.util.TypedValue;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.EditorInfo;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.LinearLayout;
import androidx.annotation.Nullable;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
import butterknife.OnEditorAction;
import butterknife.OnTextChanged;
import com.todoroo.andlib.utility.AndroidUtilities;
import com.todoroo.astrid.data.Task;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import org.tasks.R;
import org.tasks.activities.CameraActivity;
import org.tasks.dialogs.DialogBuilder;
import org.tasks.injection.FragmentComponent;
import org.tasks.preferences.Device;
import org.tasks.preferences.Preferences;
import org.tasks.themes.ThemeColor;
import org.tasks.ui.TaskEditControlFragment;
public class CommentBarFragment extends TaskEditControlFragment {
public static final int TAG = R.string.TEA_ctrl_comments;
private static final int REQUEST_CODE_CAMERA = 60;
private static final String EXTRA_TEXT = "extra_text";
private static final String EXTRA_PICTURE = "extra_picture";
@Inject Activity activity;
@Inject DialogBuilder dialogBuilder;
@Inject Device device;
@Inject Preferences preferences;
@Inject ThemeColor themeColor;
@BindView(R.id.commentButton)
View commentButton;
@BindView(R.id.commentField)
EditText commentField;
@BindView(R.id.picture)
ImageView pictureButton;
@BindView(R.id.updatesFooter)
LinearLayout commentBar;
private CommentBarFragmentCallback callback;
private Uri pendingCommentPicture = null;
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
callback = (CommentBarFragmentCallback) activity;
}
@Override
protected void inject(FragmentComponent component) {
component.inject(this);
}
@Nullable
@Override
public View onCreateView(
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(getLayout(), container, false);
ButterKnife.bind(this, view);
if (savedInstanceState != null) {
String uri = savedInstanceState.getString(EXTRA_PICTURE);
if (uri != null) {
pendingCommentPicture = Uri.parse(uri);
setPictureButtonToPendingPicture();
}
commentField.setText(savedInstanceState.getString(EXTRA_TEXT));
}
commentField.setHorizontallyScrolling(false);
commentField.setMaxLines(Integer.MAX_VALUE);
if (!preferences.getBoolean(R.string.p_show_task_edit_comments, true)) {
commentBar.setVisibility(View.GONE);
}
commentBar.setBackgroundColor(themeColor.getPrimaryColor());
resetPictureButton();
return view;
}
@Override
protected int getLayout() {
return R.layout.fragment_comment_bar;
}
@Override
protected int getIcon() {
return 0;
}
@Override
public int controlId() {
return TAG;
}
@Override
public void apply(Task task) {}
@OnTextChanged(R.id.commentField)
void onTextChanged(CharSequence s) {
commentButton.setVisibility(
pendingCommentPicture == null && isNullOrEmpty(s.toString())
? View.GONE
: View.VISIBLE);
}
@OnEditorAction(R.id.commentField)
boolean onEditorAction(KeyEvent key) {
int actionId = key != null ? key.getAction() : 0;
if (actionId == EditorInfo.IME_ACTION_DONE || actionId == EditorInfo.IME_NULL) {
if (commentField.getText().length() > 0 || pendingCommentPicture != null) {
addComment();
return true;
}
}
return false;
}
@OnClick(R.id.commentButton)
void addClicked() {
addComment();
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString(EXTRA_TEXT, commentField.getText().toString());
if (pendingCommentPicture != null) {
outState.putString(EXTRA_PICTURE, pendingCommentPicture.toString());
}
}
@OnClick(R.id.picture)
void onClickPicture() {
if (pendingCommentPicture == null) {
showPictureLauncher(null);
} else {
showPictureLauncher(
() -> {
pendingCommentPicture = null;
resetPictureButton();
});
}
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_CODE_CAMERA) {
if (resultCode == Activity.RESULT_OK) {
pendingCommentPicture = data.getData();
setPictureButtonToPendingPicture();
commentField.requestFocus();
}
} else {
super.onActivityResult(requestCode, resultCode, data);
}
}
private void addComment() {
addComment(commentField.getText().toString());
AndroidUtilities.hideSoftInputForViews(activity, commentField);
}
private void setPictureButtonToPendingPicture() {
Bitmap bitmap =
sampleBitmap(
activity,
pendingCommentPicture,
pictureButton.getLayoutParams().width,
pictureButton.getLayoutParams().height);
pictureButton.setImageBitmap(bitmap);
commentButton.setVisibility(View.VISIBLE);
}
private void addComment(String message) {
// Allow for users to just add picture
if (isNullOrEmpty(message)) {
message = " ";
}
Uri picture = pendingCommentPicture;
if (commentField != null) {
commentField.setText(""); // $NON-NLS-1$
}
pendingCommentPicture = null;
resetPictureButton();
callback.addComment(message, picture);
}
private void resetPictureButton() {
TypedValue typedValue = new TypedValue();
getActivity().getTheme().resolveAttribute(R.attr.colorOnPrimary, typedValue, true);
Drawable drawable = getContext().getDrawable(R.drawable.ic_outline_photo_camera_24px).mutate();
drawable.setTint(typedValue.data);
pictureButton.setImageDrawable(drawable);
}
private void showPictureLauncher(final ClearImageCallback clearImageOption) {
final List<Runnable> runnables = new ArrayList<>();
List<String> options = new ArrayList<>();
final boolean cameraAvailable = device.hasCamera();
if (cameraAvailable) {
runnables.add(
() ->
startActivityForResult(
new Intent(activity, CameraActivity.class), REQUEST_CODE_CAMERA));
options.add(getString(R.string.take_a_picture));
}
if (clearImageOption != null) {
runnables.add(clearImageOption::clearImage);
options.add(getString(R.string.actfm_picture_clear));
}
if (runnables.size() == 1) {
runnables.get(0).run();
} else {
DialogInterface.OnClickListener listener =
(d, which) -> {
runnables.get(which).run();
d.dismiss();
};
// show a menu of available options
dialogBuilder.newDialog().setItems(options, listener).show().setOwnerActivity(activity);
}
}
public interface CommentBarFragmentCallback {
void addComment(String message, Uri picture);
}
interface ClearImageCallback {
void clearImage();
}
}

@ -0,0 +1,217 @@
package org.tasks.fragments
import android.app.Activity
import android.content.DialogInterface
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.util.TypedValue
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.widget.EditText
import android.widget.ImageView
import android.widget.LinearLayout
import butterknife.*
import com.todoroo.andlib.utility.AndroidUtilities
import com.todoroo.astrid.data.Task
import org.tasks.R
import org.tasks.Strings.isNullOrEmpty
import org.tasks.activities.CameraActivity
import org.tasks.dialogs.DialogBuilder
import org.tasks.files.ImageHelper
import org.tasks.injection.FragmentComponent
import org.tasks.preferences.Device
import org.tasks.preferences.Preferences
import org.tasks.themes.ThemeColor
import org.tasks.ui.TaskEditControlFragment
import java.util.*
import javax.inject.Inject
class CommentBarFragment : TaskEditControlFragment() {
@Inject lateinit var activity: Activity
@Inject lateinit var dialogBuilder: DialogBuilder
@Inject lateinit var device: Device
@Inject lateinit var preferences: Preferences
@Inject lateinit var themeColor: ThemeColor
@BindView(R.id.commentButton)
lateinit var commentButton: View
@BindView(R.id.commentField)
lateinit var commentField: EditText
@BindView(R.id.picture)
lateinit var pictureButton: ImageView
@BindView(R.id.updatesFooter)
lateinit var commentBar: LinearLayout
private lateinit var callback: CommentBarFragmentCallback
private var pendingCommentPicture: Uri? = null
override fun onAttach(activity: Activity) {
super.onAttach(activity)
callback = activity as CommentBarFragmentCallback
}
override fun inject(component: FragmentComponent) = component.inject(this)
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(layout, container, false)
ButterKnife.bind(this, view)
if (savedInstanceState != null) {
val uri = savedInstanceState.getString(EXTRA_PICTURE)
if (uri != null) {
pendingCommentPicture = Uri.parse(uri)
setPictureButtonToPendingPicture()
}
commentField.setText(savedInstanceState.getString(EXTRA_TEXT))
}
commentField.setHorizontallyScrolling(false)
commentField.maxLines = Int.MAX_VALUE
if (!preferences.getBoolean(R.string.p_show_task_edit_comments, true)) {
commentBar.visibility = View.GONE
}
commentBar.setBackgroundColor(themeColor.primaryColor)
resetPictureButton()
return view
}
override val layout: Int
get() = R.layout.fragment_comment_bar
override val icon: Int
get() = 0
override fun controlId() = TAG
override fun apply(task: Task) {}
@OnTextChanged(R.id.commentField)
fun onTextChanged(s: CharSequence) {
commentButton.visibility = if (pendingCommentPicture == null && isNullOrEmpty(s.toString())) View.GONE else View.VISIBLE
}
@OnEditorAction(R.id.commentField)
fun onEditorAction(key: KeyEvent?): Boolean {
val actionId = key?.action ?: 0
if (actionId == EditorInfo.IME_ACTION_DONE || actionId == EditorInfo.IME_NULL) {
if (commentField.text.isNotEmpty() || pendingCommentPicture != null) {
addComment()
return true
}
}
return false
}
@OnClick(R.id.commentButton)
fun addClicked() {
addComment()
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putString(EXTRA_TEXT, commentField.text.toString())
if (pendingCommentPicture != null) {
outState.putString(EXTRA_PICTURE, pendingCommentPicture.toString())
}
}
@OnClick(R.id.picture)
fun onClickPicture() {
if (pendingCommentPicture == null) {
showPictureLauncher(null)
} else {
showPictureLauncher {
pendingCommentPicture = null
resetPictureButton()
}
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == REQUEST_CODE_CAMERA) {
if (resultCode == Activity.RESULT_OK) {
pendingCommentPicture = data!!.data
setPictureButtonToPendingPicture()
commentField.requestFocus()
}
} else {
super.onActivityResult(requestCode, resultCode, data)
}
}
private fun addComment() {
addComment(commentField.text.toString())
AndroidUtilities.hideSoftInputForViews(activity, commentField)
}
private fun setPictureButtonToPendingPicture() {
val bitmap = ImageHelper.sampleBitmap(
activity,
pendingCommentPicture,
pictureButton.layoutParams.width,
pictureButton.layoutParams.height)
pictureButton.setImageBitmap(bitmap)
commentButton.visibility = View.VISIBLE
}
private fun addComment(message: String) {
val picture = pendingCommentPicture
commentField.setText("")
pendingCommentPicture = null
resetPictureButton()
callback.addComment(if (isNullOrEmpty(message)) " " else message, picture)
}
private fun resetPictureButton() {
val typedValue = TypedValue()
activity.theme.resolveAttribute(R.attr.colorOnPrimary, typedValue, true)
val drawable = activity.getDrawable(R.drawable.ic_outline_photo_camera_24px)!!.mutate()
drawable.setTint(typedValue.data)
pictureButton.setImageDrawable(drawable)
}
private fun showPictureLauncher(clearImageOption: (() -> Unit)?) {
val runnables: MutableList<() -> Unit> = ArrayList()
val options: MutableList<String> = ArrayList()
val cameraAvailable = device.hasCamera()
if (cameraAvailable) {
runnables.add {
startActivityForResult(
Intent(activity, CameraActivity::class.java), REQUEST_CODE_CAMERA)
}
options.add(getString(R.string.take_a_picture))
}
if (clearImageOption != null) {
runnables.add { clearImageOption.invoke() }
options.add(getString(R.string.actfm_picture_clear))
}
if (runnables.size == 1) {
runnables[0].invoke()
} else {
val listener = DialogInterface.OnClickListener { d: DialogInterface, which: Int ->
runnables[which].invoke()
d.dismiss()
}
// show a menu of available options
dialogBuilder.newDialog().setItems(options, listener).show().setOwnerActivity(activity)
}
}
interface CommentBarFragmentCallback {
fun addComment(message: String?, picture: Uri?)
}
companion object {
const val TAG = R.string.TEA_ctrl_comments
private const val REQUEST_CODE_CAMERA = 60
private const val EXTRA_TEXT = "extra_text"
private const val EXTRA_PICTURE = "extra_picture"
}
}

@ -1,339 +0,0 @@
package org.tasks.ui;
import static org.tasks.PermissionUtil.verifyPermissions;
import static org.tasks.Strings.isNullOrEmpty;
import static org.tasks.calendars.CalendarPicker.newCalendarPicker;
import android.app.Activity;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.provider.CalendarContract;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import butterknife.BindView;
import butterknife.OnClick;
import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.gcal.GCalHelper;
import javax.inject.Inject;
import org.tasks.R;
import org.tasks.analytics.Firebase;
import org.tasks.calendars.AndroidCalendar;
import org.tasks.calendars.CalendarEventProvider;
import org.tasks.calendars.CalendarPicker;
import org.tasks.calendars.CalendarProvider;
import org.tasks.dialogs.DialogBuilder;
import org.tasks.injection.ForActivity;
import org.tasks.injection.FragmentComponent;
import org.tasks.preferences.FragmentPermissionRequestor;
import org.tasks.preferences.PermissionChecker;
import org.tasks.preferences.Preferences;
import org.tasks.themes.ThemeBase;
import timber.log.Timber;
public class CalendarControlSet extends TaskEditControlFragment {
public static final int TAG = R.string.TEA_ctrl_gcal;
private static final String FRAG_TAG_CALENDAR_PICKER = "frag_tag_calendar_picker";
private static final int REQUEST_CODE_PICK_CALENDAR = 70;
private static final int REQUEST_CODE_OPEN_EVENT = 71;
private static final int REQUEST_CODE_CLEAR_EVENT = 72;
private static final String EXTRA_URI = "extra_uri";
private static final String EXTRA_ID = "extra_id";
@BindView(R.id.clear)
View cancelButton;
@BindView(R.id.calendar_display_which)
TextView calendar;
@Inject GCalHelper gcalHelper;
@Inject CalendarProvider calendarProvider;
@Inject Preferences preferences;
@Inject @ForActivity Context context;
@Inject PermissionChecker permissionChecker;
@Inject FragmentPermissionRequestor permissionRequestor;
@Inject Firebase firebase;
@Inject DialogBuilder dialogBuilder;
@Inject ThemeBase themeBase;
@Inject CalendarEventProvider calendarEventProvider;
private String calendarId;
private String eventUri;
@Nullable
@Override
public View onCreateView(
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = super.onCreateView(inflater, container, savedInstanceState);
boolean canAccessCalendars = permissionChecker.canAccessCalendars();
if (savedInstanceState != null) {
eventUri = savedInstanceState.getString(EXTRA_URI);
calendarId = savedInstanceState.getString(EXTRA_ID);
} else if (task.isNew() && canAccessCalendars) {
calendarId = preferences.getDefaultCalendar();
if (!isNullOrEmpty(calendarId)) {
try {
AndroidCalendar defaultCalendar = calendarProvider.getCalendar(calendarId);
if (defaultCalendar == null) {
calendarId = null;
}
} catch (Exception e) {
Timber.e(e);
firebase.reportException(e);
calendarId = null;
}
}
} else {
eventUri = task.getCalendarURI();
}
if (canAccessCalendars && !calendarEntryExists(eventUri)) {
eventUri = null;
}
refreshDisplayView();
return view;
}
@Override
protected int getLayout() {
return R.layout.control_set_gcal_display;
}
@Override
protected int getIcon() {
return R.drawable.ic_outline_event_24px;
}
@Override
public int controlId() {
return TAG;
}
@SuppressWarnings("SimplifiableIfStatement")
@Override
public boolean hasChanges(Task original) {
if (!permissionChecker.canAccessCalendars()) {
return false;
}
if (!isNullOrEmpty(calendarId)) {
return true;
}
String originalUri = original.getCalendarURI();
if (isNullOrEmpty(eventUri) && isNullOrEmpty(originalUri)) {
return false;
}
return !originalUri.equals(eventUri);
}
@Override
public void apply(Task task) {
if (!permissionChecker.canAccessCalendars()) {
return;
}
if (!isNullOrEmpty(task.getCalendarURI())) {
if (eventUri == null) {
calendarEventProvider.deleteEvent(task);
} else if (!calendarEntryExists(task.getCalendarURI())) {
task.setCalendarURI("");
}
}
if (!task.hasDueDate()) {
return;
}
if (calendarEntryExists(task.getCalendarURI())) {
ContentResolver cr = context.getContentResolver();
try {
ContentValues updateValues = new ContentValues();
// check if we need to update the item
updateValues.put(CalendarContract.Events.TITLE, task.getTitle());
updateValues.put(CalendarContract.Events.DESCRIPTION, task.getNotes());
gcalHelper.createStartAndEndDate(task, updateValues);
cr.update(Uri.parse(task.getCalendarURI()), updateValues, null, null);
} catch (Exception e) {
Timber.e(e, "unable-to-update-calendar: %s", task.getCalendarURI());
}
} else if (!isNullOrEmpty(calendarId)) {
try {
ContentValues values = new ContentValues();
values.put(CalendarContract.Events.CALENDAR_ID, calendarId);
Uri uri = gcalHelper.createTaskEvent(task, values);
if (uri != null) {
task.setCalendarURI(uri.toString());
}
} catch (Exception e) {
Timber.e(e);
}
}
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString(EXTRA_URI, eventUri);
outState.putString(EXTRA_ID, calendarId);
}
@OnClick(R.id.clear)
void clearCalendar() {
if (isNullOrEmpty(eventUri)) {
clear();
} else {
dialogBuilder
.newDialog(R.string.delete_calendar_event_confirmation)
.setPositiveButton(
R.string.delete,
(dialog, which) -> {
if (permissionRequestor.requestCalendarPermissions(REQUEST_CODE_CLEAR_EVENT)) {
clear();
}
})
.setNegativeButton(android.R.string.cancel, null)
.show();
}
}
private void clear() {
calendarId = null;
eventUri = null;
refreshDisplayView();
}
@Override
protected void onRowClick() {
if (isNullOrEmpty(eventUri)) {
newCalendarPicker(this, REQUEST_CODE_PICK_CALENDAR, getCalendarName())
.show(getParentFragmentManager(), FRAG_TAG_CALENDAR_PICKER);
} else {
if (permissionRequestor.requestCalendarPermissions(REQUEST_CODE_OPEN_EVENT)) {
openCalendarEvent();
}
}
}
@Override
protected boolean isClickable() {
return true;
}
private void openCalendarEvent() {
ContentResolver cr = getActivity().getContentResolver();
Uri uri = Uri.parse(eventUri);
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
try (Cursor cursor =
cr.query(
uri,
new String[] {CalendarContract.Events.DTSTART, CalendarContract.Events.DTEND},
null,
null,
null)) {
if (cursor.getCount() == 0) {
// event no longer exists
Toast.makeText(context, R.string.calendar_event_not_found, Toast.LENGTH_SHORT).show();
eventUri = null;
refreshDisplayView();
} else {
cursor.moveToFirst();
intent.putExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME, cursor.getLong(0));
intent.putExtra(CalendarContract.EXTRA_EVENT_END_TIME, cursor.getLong(1));
startActivity(intent);
}
} catch (Exception e) {
Timber.e(e);
Toast.makeText(getActivity(), R.string.gcal_TEA_error, Toast.LENGTH_LONG).show();
}
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_CODE_PICK_CALENDAR) {
if (resultCode == Activity.RESULT_OK) {
calendarId = data.getStringExtra(CalendarPicker.EXTRA_CALENDAR_ID);
refreshDisplayView();
}
} else {
super.onActivityResult(requestCode, resultCode, data);
}
}
private String getCalendarName() {
if (calendarId == null) {
return null;
}
AndroidCalendar calendar = calendarProvider.getCalendar(calendarId);
return calendar == null ? null : calendar.getName();
}
@Override
public void onRequestPermissionsResult(
int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
if (requestCode == REQUEST_CODE_OPEN_EVENT) {
if (verifyPermissions(grantResults)) {
openCalendarEvent();
}
} else if (requestCode == REQUEST_CODE_CLEAR_EVENT) {
if (verifyPermissions(grantResults)) {
clear();
}
} else {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
}
private void refreshDisplayView() {
if (!isNullOrEmpty(eventUri)) {
calendar.setText(R.string.gcal_TEA_showCalendar_label);
cancelButton.setVisibility(View.VISIBLE);
} else if (calendarId != null) {
calendar.setText(getCalendarName());
cancelButton.setVisibility(View.GONE);
} else {
calendar.setText(null);
cancelButton.setVisibility(View.GONE);
}
}
private boolean calendarEntryExists(String eventUri) {
if (isNullOrEmpty(eventUri)) {
return false;
}
try {
Uri uri = Uri.parse(eventUri);
ContentResolver contentResolver = context.getContentResolver();
try (Cursor cursor =
contentResolver.query(
uri, new String[] {CalendarContract.Events.DTSTART}, null, null, null)) {
if (cursor.getCount() != 0) {
return true;
}
}
} catch (Exception e) {
Timber.e(e, "%s: %s", eventUri, e.getMessage());
}
return false;
}
@Override
protected void inject(FragmentComponent component) {
component.inject(this);
}
}

@ -0,0 +1,299 @@
package org.tasks.ui
import android.app.Activity
import android.content.ContentValues
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.provider.CalendarContract
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import android.widget.Toast
import butterknife.BindView
import butterknife.OnClick
import com.todoroo.astrid.data.Task
import com.todoroo.astrid.gcal.GCalHelper
import org.tasks.PermissionUtil.verifyPermissions
import org.tasks.R
import org.tasks.Strings.isNullOrEmpty
import org.tasks.analytics.Firebase
import org.tasks.calendars.CalendarEventProvider
import org.tasks.calendars.CalendarPicker
import org.tasks.calendars.CalendarProvider
import org.tasks.dialogs.DialogBuilder
import org.tasks.injection.ForActivity
import org.tasks.injection.FragmentComponent
import org.tasks.preferences.FragmentPermissionRequestor
import org.tasks.preferences.PermissionChecker
import org.tasks.preferences.Preferences
import org.tasks.themes.ThemeBase
import timber.log.Timber
import javax.inject.Inject
class CalendarControlSet : TaskEditControlFragment() {
@BindView(R.id.clear)
lateinit var cancelButton: View
@BindView(R.id.calendar_display_which)
lateinit var calendar: TextView
@Inject @ForActivity lateinit var activity: Context
@Inject lateinit var gcalHelper: GCalHelper
@Inject lateinit var calendarProvider: CalendarProvider
@Inject lateinit var preferences: Preferences
@Inject lateinit var permissionChecker: PermissionChecker
@Inject lateinit var permissionRequestor: FragmentPermissionRequestor
@Inject lateinit var firebase: Firebase
@Inject lateinit var dialogBuilder: DialogBuilder
@Inject lateinit var themeBase: ThemeBase
@Inject lateinit var calendarEventProvider: CalendarEventProvider
private var calendarId: String? = null
private var eventUri: String? = null
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = super.onCreateView(inflater, container, savedInstanceState)
val canAccessCalendars = permissionChecker.canAccessCalendars()
if (savedInstanceState != null) {
eventUri = savedInstanceState.getString(EXTRA_URI)
calendarId = savedInstanceState.getString(EXTRA_ID)
} else if (task.isNew && canAccessCalendars) {
calendarId = preferences.defaultCalendar
if (!isNullOrEmpty(calendarId)) {
try {
val defaultCalendar = calendarProvider.getCalendar(calendarId)
if (defaultCalendar == null) {
calendarId = null
}
} catch (e: Exception) {
Timber.e(e)
firebase.reportException(e)
calendarId = null
}
}
} else {
eventUri = task.calendarURI
}
if (canAccessCalendars && !calendarEntryExists(eventUri)) {
eventUri = null
}
refreshDisplayView()
return view
}
override val layout: Int
get() = R.layout.control_set_gcal_display
override val icon: Int
get() = R.drawable.ic_outline_event_24px
override fun controlId() = TAG
override fun hasChanges(original: Task): Boolean {
if (!permissionChecker.canAccessCalendars()) {
return false
}
if (!isNullOrEmpty(calendarId)) {
return true
}
val originalUri = original.calendarURI
return if (isNullOrEmpty(eventUri) && isNullOrEmpty(originalUri)) {
false
} else originalUri != eventUri
}
override fun apply(task: Task) {
if (!permissionChecker.canAccessCalendars()) {
return
}
if (!isNullOrEmpty(task.calendarURI)) {
if (eventUri == null) {
calendarEventProvider.deleteEvent(task)
} else if (!calendarEntryExists(task.calendarURI)) {
task.calendarURI = ""
}
}
if (!task.hasDueDate()) {
return
}
if (calendarEntryExists(task.calendarURI)) {
val cr = activity.contentResolver
try {
val updateValues = ContentValues()
// check if we need to update the item
updateValues.put(CalendarContract.Events.TITLE, task.title)
updateValues.put(CalendarContract.Events.DESCRIPTION, task.notes)
gcalHelper.createStartAndEndDate(task, updateValues)
cr.update(Uri.parse(task.calendarURI), updateValues, null, null)
} catch (e: Exception) {
Timber.e(e, "unable-to-update-calendar: %s", task.calendarURI)
}
} else if (!isNullOrEmpty(calendarId)) {
try {
val values = ContentValues()
values.put(CalendarContract.Events.CALENDAR_ID, calendarId)
val uri = gcalHelper.createTaskEvent(task, values)
if (uri != null) {
task.calendarURI = uri.toString()
}
} catch (e: Exception) {
Timber.e(e)
}
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putString(EXTRA_URI, eventUri)
outState.putString(EXTRA_ID, calendarId)
}
@OnClick(R.id.clear)
fun clearCalendar() {
if (isNullOrEmpty(eventUri)) {
clear()
} else {
dialogBuilder
.newDialog(R.string.delete_calendar_event_confirmation)
.setPositiveButton(R.string.delete) { _, _ ->
if (permissionRequestor.requestCalendarPermissions(REQUEST_CODE_CLEAR_EVENT)) {
clear()
}
}
.setNegativeButton(android.R.string.cancel, null)
.show()
}
}
private fun clear() {
calendarId = null
eventUri = null
refreshDisplayView()
}
override fun onRowClick() {
if (isNullOrEmpty(eventUri)) {
CalendarPicker.newCalendarPicker(this, REQUEST_CODE_PICK_CALENDAR, calendarName)
.show(parentFragmentManager, FRAG_TAG_CALENDAR_PICKER)
} else {
if (permissionRequestor.requestCalendarPermissions(REQUEST_CODE_OPEN_EVENT)) {
openCalendarEvent()
}
}
}
override val isClickable: Boolean
get() = true
private fun openCalendarEvent() {
val cr = activity.contentResolver
val uri = Uri.parse(eventUri)
val intent = Intent(Intent.ACTION_VIEW, uri)
try {
cr.query(
uri, arrayOf(CalendarContract.Events.DTSTART, CalendarContract.Events.DTEND),
null,
null,
null).use { cursor ->
if (cursor!!.count == 0) {
// event no longer exists
Toast.makeText(activity, R.string.calendar_event_not_found, Toast.LENGTH_SHORT).show()
eventUri = null
refreshDisplayView()
} else {
cursor.moveToFirst()
intent.putExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME, cursor.getLong(0))
intent.putExtra(CalendarContract.EXTRA_EVENT_END_TIME, cursor.getLong(1))
startActivity(intent)
}
}
} catch (e: Exception) {
Timber.e(e)
Toast.makeText(activity, R.string.gcal_TEA_error, Toast.LENGTH_LONG).show()
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == REQUEST_CODE_PICK_CALENDAR) {
if (resultCode == Activity.RESULT_OK) {
calendarId = data!!.getStringExtra(CalendarPicker.EXTRA_CALENDAR_ID)
refreshDisplayView()
}
} else {
super.onActivityResult(requestCode, resultCode, data)
}
}
private val calendarName: String?
get() {
if (calendarId == null) {
return null
}
val calendar = calendarProvider.getCalendar(calendarId)
return calendar?.name
}
override fun onRequestPermissionsResult(
requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
if (requestCode == REQUEST_CODE_OPEN_EVENT) {
if (verifyPermissions(grantResults)) {
openCalendarEvent()
}
} else if (requestCode == REQUEST_CODE_CLEAR_EVENT) {
if (verifyPermissions(grantResults)) {
clear()
}
} else {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
}
}
private fun refreshDisplayView() {
if (!isNullOrEmpty(eventUri)) {
calendar.setText(R.string.gcal_TEA_showCalendar_label)
cancelButton.visibility = View.VISIBLE
} else if (calendarId != null) {
calendar.text = calendarName
cancelButton.visibility = View.GONE
} else {
calendar.text = null
cancelButton.visibility = View.GONE
}
}
private fun calendarEntryExists(eventUri: String?): Boolean {
if (isNullOrEmpty(eventUri)) {
return false
}
try {
val uri = Uri.parse(eventUri)
val contentResolver = activity.contentResolver
contentResolver.query(
uri, arrayOf(CalendarContract.Events.DTSTART), null, null, null).use { cursor ->
if (cursor!!.count != 0) {
return true
}
}
} catch (e: Exception) {
Timber.e(e, "%s: %s", eventUri, e.message)
}
return false
}
override fun inject(component: FragmentComponent) = component.inject(this)
companion object {
const val TAG = R.string.TEA_ctrl_gcal
private const val FRAG_TAG_CALENDAR_PICKER = "frag_tag_calendar_picker"
private const val REQUEST_CODE_PICK_CALENDAR = 70
private const val REQUEST_CODE_OPEN_EVENT = 71
private const val REQUEST_CODE_CLEAR_EVENT = 72
private const val EXTRA_URI = "extra_uri"
private const val EXTRA_ID = "extra_id"
}
}

@ -1,174 +0,0 @@
package org.tasks.ui;
import static org.tasks.date.DateTimeUtils.newDateTime;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentManager;
import butterknife.BindView;
import com.todoroo.andlib.utility.DateUtilities;
import com.todoroo.astrid.data.Task;
import javax.inject.Inject;
import org.tasks.R;
import org.tasks.dialogs.DateTimePicker;
import org.tasks.injection.ForActivity;
import org.tasks.injection.FragmentComponent;
import org.tasks.locale.Locale;
import org.tasks.preferences.Preferences;
import org.tasks.time.DateTime;
import org.threeten.bp.format.FormatStyle;
public class DeadlineControlSet extends TaskEditControlFragment {
public static final int TAG = R.string.TEA_ctrl_when_pref;
private static final int REQUEST_DATE = 504;
private static final String EXTRA_DATE = "extra_date";
private static final String FRAG_TAG_DATE_PICKER = "frag_tag_date_picker";
@Inject @ForActivity Context context;
@Inject Locale locale;
@Inject Preferences preferences;
@BindView(R.id.due_date)
TextView dueDate;
private DueDateChangeListener callback;
private long date = 0;
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
callback = (DueDateChangeListener) activity;
}
@Override
protected void inject(FragmentComponent component) {
component.inject(this);
}
@Nullable
@Override
public View onCreateView(
final LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = super.onCreateView(inflater, container, savedInstanceState);
if (savedInstanceState == null) {
date = task.getDueDate();
} else {
date = savedInstanceState.getLong(EXTRA_DATE);
}
refreshDisplayView();
return view;
}
@Override
protected void onRowClick() {
FragmentManager fragmentManager = getParentFragmentManager();
if (fragmentManager.findFragmentByTag(FRAG_TAG_DATE_PICKER) == null) {
DateTimePicker.Companion.newDateTimePicker(
this,
REQUEST_DATE,
getDueDateTime(),
preferences.getBoolean(R.string.p_auto_dismiss_datetime_edit_screen, false))
.show(fragmentManager, FRAG_TAG_DATE_PICKER);
}
}
@Override
protected boolean isClickable() {
return true;
}
@Override
protected int getLayout() {
return R.layout.control_set_deadline;
}
@Override
protected int getIcon() {
return R.drawable.ic_outline_schedule_24px;
}
@Override
public int controlId() {
return TAG;
}
@Override
public boolean hasChanges(Task original) {
return original.getDueDate() != getDueDateTime();
}
@Override
public void apply(Task task) {
long dueDate = getDueDateTime();
if (dueDate != task.getDueDate()) {
task.setReminderSnooze(0L);
}
task.setDueDate(dueDate);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_DATE) {
if (resultCode == Activity.RESULT_OK) {
long timestamp = data.getLongExtra(DateTimePicker.EXTRA_TIMESTAMP, 0L);
DateTime dateTime = new DateTime(timestamp);
date = dateTime.getMillis();
callback.dueDateChanged(getDueDateTime());
}
refreshDisplayView();
} else {
super.onActivityResult(requestCode, resultCode, data);
}
}
private long getDueDateTime() {
return date == 0
? 0
: Task.createDueDate(
Task.hasDueTime(date) ? Task.URGENCY_SPECIFIC_DAY_TIME : Task.URGENCY_SPECIFIC_DAY,
date);
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putLong(EXTRA_DATE, date);
}
private void refreshDisplayView() {
if (date == 0) {
dueDate.setText("");
setTextColor(false);
} else {
dueDate.setText(
DateUtilities.getRelativeDateTime(context, date, locale.getLocale(), FormatStyle.FULL));
setTextColor(
Task.hasDueTime(date)
? newDateTime(date).isBeforeNow()
: newDateTime(date).endOfDay().isBeforeNow());
}
}
private void setTextColor(boolean overdue) {
dueDate.setTextColor(
context.getColor(overdue ? R.color.overdue : R.color.text_primary));
}
public interface DueDateChangeListener {
void dueDateChanged(long dateTime);
}
}

@ -0,0 +1,139 @@
package org.tasks.ui
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import butterknife.BindView
import com.todoroo.andlib.utility.DateUtilities
import com.todoroo.astrid.data.Task
import com.todoroo.astrid.data.Task.Companion.createDueDate
import com.todoroo.astrid.data.Task.Companion.hasDueTime
import org.tasks.R
import org.tasks.date.DateTimeUtils
import org.tasks.dialogs.DateTimePicker
import org.tasks.dialogs.DateTimePicker.Companion.newDateTimePicker
import org.tasks.injection.ForActivity
import org.tasks.injection.FragmentComponent
import org.tasks.locale.Locale
import org.tasks.preferences.Preferences
import org.tasks.time.DateTime
import org.threeten.bp.format.FormatStyle
import javax.inject.Inject
class DeadlineControlSet : TaskEditControlFragment() {
@Inject @ForActivity lateinit var activity: Context
@Inject lateinit var locale: Locale
@Inject lateinit var preferences: Preferences
@BindView(R.id.due_date)
lateinit var dueDate: TextView
private lateinit var callback: DueDateChangeListener
private var date: Long = 0
override fun onAttach(activity: Activity) {
super.onAttach(activity)
callback = activity as DueDateChangeListener
}
override fun inject(component: FragmentComponent) = component.inject(this)
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = super.onCreateView(inflater, container, savedInstanceState)
date = savedInstanceState?.getLong(EXTRA_DATE) ?: task.dueDate
refreshDisplayView()
return view
}
override fun onRowClick() {
val fragmentManager = parentFragmentManager
if (fragmentManager.findFragmentByTag(FRAG_TAG_DATE_PICKER) == null) {
newDateTimePicker(
this,
REQUEST_DATE,
dueDateTime,
preferences.getBoolean(R.string.p_auto_dismiss_datetime_edit_screen, false))
.show(fragmentManager, FRAG_TAG_DATE_PICKER)
}
}
override val isClickable: Boolean
get() = true
override val layout: Int
get() = R.layout.control_set_deadline
override val icon: Int
get() = R.drawable.ic_outline_schedule_24px
override fun controlId() = TAG
override fun hasChanges(original: Task): Boolean {
return original.dueDate != dueDateTime
}
override fun apply(task: Task) {
val dueDate = dueDateTime
if (dueDate != task.dueDate) {
task.reminderSnooze = 0L
}
task.dueDate = dueDate
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == REQUEST_DATE) {
if (resultCode == Activity.RESULT_OK) {
val timestamp = data!!.getLongExtra(DateTimePicker.EXTRA_TIMESTAMP, 0L)
val dateTime = DateTime(timestamp)
date = dateTime.millis
callback.dueDateChanged(dueDateTime)
}
refreshDisplayView()
} else {
super.onActivityResult(requestCode, resultCode, data)
}
}
private val dueDateTime: Long
get() = if (date == 0L) 0 else createDueDate(
if (hasDueTime(date)) Task.URGENCY_SPECIFIC_DAY_TIME else Task.URGENCY_SPECIFIC_DAY,
date)
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putLong(EXTRA_DATE, date)
}
private fun refreshDisplayView() {
if (date == 0L) {
dueDate.text = ""
setTextColor(false)
} else {
dueDate.text = DateUtilities.getRelativeDateTime(activity, date, locale.locale, FormatStyle.FULL)
setTextColor(
if (hasDueTime(date)) DateTimeUtils.newDateTime(date).isBeforeNow else DateTimeUtils.newDateTime(date).endOfDay().isBeforeNow)
}
}
private fun setTextColor(overdue: Boolean) {
dueDate.setTextColor(
activity.getColor(if (overdue) R.color.overdue else R.color.text_primary))
}
interface DueDateChangeListener {
fun dueDateChanged(dateTime: Long)
}
companion object {
const val TAG = R.string.TEA_ctrl_when_pref
private const val REQUEST_DATE = 504
private const val EXTRA_DATE = "extra_date"
private const val FRAG_TAG_DATE_PICKER = "frag_tag_date_picker"
}
}

@ -1,101 +0,0 @@
package org.tasks.ui;
import static org.tasks.Strings.isNullOrEmpty;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import androidx.annotation.Nullable;
import butterknife.BindView;
import butterknife.OnTextChanged;
import com.todoroo.astrid.data.Task;
import javax.inject.Inject;
import org.tasks.R;
import org.tasks.dialogs.Linkify;
import org.tasks.injection.FragmentComponent;
import org.tasks.preferences.Preferences;
public class DescriptionControlSet extends TaskEditControlFragment {
public static final int TAG = R.string.TEA_ctrl_notes_pref;
private static final String EXTRA_DESCRIPTION = "extra_description";
@Inject Linkify linkify;
@Inject Preferences preferences;
@BindView(R.id.notes)
EditText editText;
private String description;
static String stripCarriageReturns(@Nullable String original) {
return original == null ? null : original.replaceAll("\\r\\n?", "\n");
}
@Nullable
@Override
public View onCreateView(
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = super.onCreateView(inflater, container, savedInstanceState);
if (savedInstanceState == null) {
description = stripCarriageReturns(task.getNotes());
} else {
description = savedInstanceState.getString(EXTRA_DESCRIPTION);
}
if (!isNullOrEmpty(description)) {
editText.setTextKeepState(description);
}
if (preferences.getBoolean(R.string.p_linkify_task_edit, false)) {
linkify.linkify(editText);
}
return view;
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString(EXTRA_DESCRIPTION, description);
}
@Override
protected int getLayout() {
return R.layout.control_set_description;
}
@Override
protected int getIcon() {
return R.drawable.ic_outline_notes_24px;
}
@Override
public int controlId() {
return TAG;
}
@OnTextChanged(R.id.notes)
void textChanged(CharSequence text) {
description = text.toString().trim();
}
@Override
public void apply(Task task) {
task.setNotes(description);
}
@Override
public boolean hasChanges(Task original) {
return !(isNullOrEmpty(description)
? isNullOrEmpty(original.getNotes())
: description.equals(stripCarriageReturns(original.getNotes())));
}
@Override
protected void inject(FragmentComponent component) {
component.inject(this);
}
}

@ -0,0 +1,79 @@
package org.tasks.ui
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.EditText
import butterknife.BindView
import butterknife.OnTextChanged
import com.todoroo.astrid.data.Task
import org.tasks.R
import org.tasks.Strings.isNullOrEmpty
import org.tasks.dialogs.Linkify
import org.tasks.injection.FragmentComponent
import org.tasks.preferences.Preferences
import javax.inject.Inject
class DescriptionControlSet : TaskEditControlFragment() {
@Inject lateinit var linkify: Linkify
@Inject lateinit var preferences: Preferences
@BindView(R.id.notes)
lateinit var editText: EditText
private var description: String? = null
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = super.onCreateView(inflater, container, savedInstanceState)
description = if (savedInstanceState == null) {
stripCarriageReturns(task.notes)
} else {
savedInstanceState.getString(EXTRA_DESCRIPTION)
}
if (!isNullOrEmpty(description)) {
editText.setTextKeepState(description)
}
if (preferences.getBoolean(R.string.p_linkify_task_edit, false)) {
linkify.linkify(editText)
}
return view
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putString(EXTRA_DESCRIPTION, description)
}
override val layout: Int
get() = R.layout.control_set_description
override val icon: Int
get() = R.drawable.ic_outline_notes_24px
override fun controlId() = TAG
@OnTextChanged(R.id.notes)
fun textChanged(text: CharSequence) {
description = text.toString().trim { it <= ' ' }
}
override fun apply(task: Task) {
task.notes = description
}
override fun hasChanges(original: Task): Boolean {
return !if (isNullOrEmpty(description)) isNullOrEmpty(original.notes) else description == stripCarriageReturns(original.notes)
}
override fun inject(component: FragmentComponent) = component.inject(this)
companion object {
const val TAG = R.string.TEA_ctrl_notes_pref
private const val EXTRA_DESCRIPTION = "extra_description"
fun stripCarriageReturns(original: String?): String? {
return original?.replace("\\r\\n?".toRegex(), "\n")
}
}
}

@ -1,325 +0,0 @@
package org.tasks.ui;
import static com.google.common.collect.Lists.transform;
import static com.todoroo.astrid.data.SyncFlags.FORCE_CALDAV_SYNC;
import static org.tasks.PermissionUtil.verifyPermissions;
import static org.tasks.Strings.isNullOrEmpty;
import static org.tasks.dialogs.GeofenceDialog.newGeofenceDialog;
import static org.tasks.location.LocationPickerActivity.EXTRA_PLACE;
import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.os.Parcelable;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.style.ClickableSpan;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.util.Pair;
import butterknife.BindView;
import butterknife.OnClick;
import com.todoroo.andlib.utility.DateUtilities;
import com.todoroo.astrid.data.Task;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import org.tasks.R;
import org.tasks.data.Geofence;
import org.tasks.data.Location;
import org.tasks.data.LocationDao;
import org.tasks.data.Place;
import org.tasks.dialogs.DialogBuilder;
import org.tasks.dialogs.GeofenceDialog;
import org.tasks.injection.FragmentComponent;
import org.tasks.location.GeofenceApi;
import org.tasks.location.LocationPickerActivity;
import org.tasks.preferences.Device;
import org.tasks.preferences.FragmentPermissionRequestor;
import org.tasks.preferences.PermissionChecker;
import org.tasks.preferences.PermissionRequestor;
import org.tasks.preferences.Preferences;
public class LocationControlSet extends TaskEditControlFragment {
public static final int TAG = R.string.TEA_ctrl_locations_pref;
private static final int REQUEST_LOCATION_REMINDER = 12153;
private static final int REQUEST_GEOFENCE_DETAILS = 12154;
private static final String FRAG_TAG_LOCATION_DIALOG = "location_dialog";
private static final String EXTRA_ORIGINAL = "extra_original_location";
private static final String EXTRA_LOCATION = "extra_new_location";
@Inject Preferences preferences;
@Inject DialogBuilder dialogBuilder;
@Inject GeofenceApi geofenceApi;
@Inject LocationDao locationDao;
@Inject Device device;
@Inject FragmentPermissionRequestor permissionRequestor;
@Inject PermissionChecker permissionChecker;
@BindView(R.id.location_name)
TextView locationName;
@BindView(R.id.location_address)
TextView locationAddress;
@BindView(R.id.geofence_options)
ImageView geofenceOptions;
private Location original;
private Location location;
@Nullable
@Override
public View onCreateView(
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = super.onCreateView(inflater, container, savedInstanceState);
if (savedInstanceState == null) {
if (task.isNew()) {
if (task.hasTransitory(Place.KEY)) {
Place place = locationDao.getPlace(task.getTransitory(Place.KEY));
if (place != null) {
original = new Location(new Geofence(place.getUid(), preferences), place);
}
}
} else {
original = locationDao.getGeofences(task.getId());
}
if (original != null) {
location = new Location(original.geofence, original.place);
}
} else {
original = savedInstanceState.getParcelable(EXTRA_ORIGINAL);
location = savedInstanceState.getParcelable(EXTRA_LOCATION);
}
return view;
}
@Override
public void onResume() {
super.onResume();
updateUi();
}
private void setLocation(@Nullable Location location) {
this.location = location;
updateUi();
}
private void updateUi() {
if (this.location == null) {
locationName.setText("");
geofenceOptions.setVisibility(View.GONE);
locationAddress.setVisibility(View.GONE);
} else {
geofenceOptions.setVisibility(device.supportsGeofences() ? View.VISIBLE : View.GONE);
geofenceOptions.setImageResource(
permissionChecker.canAccessLocation()
&& (this.location.isArrival() || this.location.isDeparture())
? R.drawable.ic_outline_notifications_24px
: R.drawable.ic_outline_notifications_off_24px);
String name = this.location.getDisplayName();
String address = this.location.getDisplayAddress();
if (!isNullOrEmpty(address) && !address.equals(name)) {
locationAddress.setText(address);
locationAddress.setVisibility(View.VISIBLE);
} else {
locationAddress.setVisibility(View.GONE);
}
SpannableString spannableString = new SpannableString(name);
spannableString.setSpan(
new ClickableSpan() {
@Override
public void onClick(@NonNull View view) {}
},
0,
name.length(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
locationName.setText(spannableString);
}
}
@Override
protected void onRowClick() {
if (location == null) {
chooseLocation();
} else {
List<Pair<Integer, Runnable>> options = new ArrayList<>();
options.add(Pair.create(R.string.open_map, () -> location.open(getActivity())));
if (!isNullOrEmpty(location.getPhone())) {
options.add(Pair.create(R.string.action_call, this::call));
}
if (!isNullOrEmpty(location.getUrl())) {
options.add(Pair.create(R.string.visit_website, this::openWebsite));
}
options.add(Pair.create(R.string.choose_new_location, this::chooseLocation));
options.add(Pair.create(R.string.delete, () -> setLocation(null)));
dialogBuilder
.newDialog(location.getDisplayName())
.setItems(
new ArrayList<>(transform(options, o -> getString(o.first))),
(dialog, which) -> options.get(which).second.run())
.show();
}
}
@Override
protected boolean isClickable() {
return true;
}
private void chooseLocation() {
Intent intent = new Intent(getActivity(), LocationPickerActivity.class);
if (location != null) {
intent.putExtra(EXTRA_PLACE, (Parcelable) location.place);
}
startActivityForResult(intent, REQUEST_LOCATION_REMINDER);
}
@OnClick(R.id.geofence_options)
void geofenceOptions() {
if (permissionRequestor.requestFineLocation()) {
showGeofenceOptions();
}
}
@Override
public void onRequestPermissionsResult(
int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
if (requestCode == PermissionRequestor.REQUEST_LOCATION) {
if (verifyPermissions(grantResults)) {
showGeofenceOptions();
} else {
dialogBuilder
.newDialog(R.string.missing_permissions)
.setMessage(R.string.location_permission_required_geofence)
.setPositiveButton(android.R.string.ok, null)
.show();
}
} else {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
}
private void showGeofenceOptions() {
GeofenceDialog dialog = newGeofenceDialog(location);
dialog.setTargetFragment(this, REQUEST_GEOFENCE_DETAILS);
dialog.show(getParentFragmentManager(), FRAG_TAG_LOCATION_DIALOG);
}
@Override
protected int getLayout() {
return R.layout.location_row;
}
@Override
public int getIcon() {
return R.drawable.ic_outline_place_24px;
}
@Override
public int controlId() {
return TAG;
}
private void openWebsite() {
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(location.getUrl())));
}
private void call() {
startActivity(new Intent(Intent.ACTION_DIAL, Uri.parse("tel:" + location.getPhone())));
}
@Override
public boolean hasChanges(Task task) {
if (original == null) {
return location != null;
}
if (location == null) {
return true;
}
if (!original.place.equals(location.place)) {
return true;
}
return original.isDeparture() != location.isDeparture()
|| original.isArrival() != location.isArrival()
|| original.getRadius() != location.getRadius();
}
@Override
public boolean requiresId() {
return true;
}
@Override
public void apply(Task task) {
if ((original == null || location == null || !original.place.equals(location.place))) {
task.putTransitory(FORCE_CALDAV_SYNC, true);
}
if (original != null) {
locationDao.delete(original.geofence);
geofenceApi.update(original.place);
}
if (location != null) {
Place place = location.place;
Geofence geofence = location.geofence;
geofence.setTask(task.getId());
geofence.setPlace(place.getUid());
geofence.setId(locationDao.insert(geofence));
geofenceApi.update(place);
}
task.setModificationDate(DateUtilities.now());
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putParcelable(EXTRA_ORIGINAL, original);
outState.putParcelable(EXTRA_LOCATION, location);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_LOCATION_REMINDER) {
if (resultCode == Activity.RESULT_OK) {
Place place = data.getParcelableExtra(EXTRA_PLACE);
Geofence geofence;
if (location == null) {
geofence = new Geofence(place.getUid(), preferences);
} else {
Geofence existing = location.geofence;
geofence =
new Geofence(
place.getUid(),
existing.isArrival(),
existing.isDeparture(),
existing.getRadius());
}
setLocation(new Location(geofence, place));
}
} else if (requestCode == REQUEST_GEOFENCE_DETAILS) {
if (resultCode == Activity.RESULT_OK) {
location.geofence = data.getParcelableExtra(GeofenceDialog.EXTRA_GEOFENCE);
updateUi();
}
} else {
super.onActivityResult(requestCode, resultCode, data);
}
}
@Override
protected void inject(FragmentComponent component) {
component.inject(this);
}
}

@ -0,0 +1,283 @@
package org.tasks.ui
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.os.Parcelable
import android.text.SpannableString
import android.text.Spanned
import android.text.style.ClickableSpan
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.core.util.Pair
import butterknife.BindView
import butterknife.OnClick
import com.todoroo.andlib.utility.DateUtilities
import com.todoroo.astrid.data.SyncFlags
import com.todoroo.astrid.data.Task
import org.tasks.PermissionUtil.verifyPermissions
import org.tasks.R
import org.tasks.Strings.isNullOrEmpty
import org.tasks.data.Geofence
import org.tasks.data.Location
import org.tasks.data.LocationDao
import org.tasks.data.Place
import org.tasks.dialogs.DialogBuilder
import org.tasks.dialogs.GeofenceDialog
import org.tasks.injection.FragmentComponent
import org.tasks.location.GeofenceApi
import org.tasks.location.LocationPickerActivity
import org.tasks.preferences.*
import java.util.*
import javax.inject.Inject
class LocationControlSet : TaskEditControlFragment() {
@Inject lateinit var preferences: Preferences
@Inject lateinit var dialogBuilder: DialogBuilder
@Inject lateinit var geofenceApi: GeofenceApi
@Inject lateinit var locationDao: LocationDao
@Inject lateinit var device: Device
@Inject lateinit var permissionRequestor: FragmentPermissionRequestor
@Inject lateinit var permissionChecker: PermissionChecker
@BindView(R.id.location_name)
lateinit var locationName: TextView
@BindView(R.id.location_address)
lateinit var locationAddress: TextView
@BindView(R.id.geofence_options)
lateinit var geofenceOptions: ImageView
private var original: Location? = null
private var location: Location? = null
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = super.onCreateView(inflater, container, savedInstanceState)
if (savedInstanceState == null) {
if (task.isNew) {
if (task.hasTransitory(Place.KEY)) {
val place = locationDao.getPlace(task.getTransitory<String>(Place.KEY)!!)
if (place != null) {
original = Location(Geofence(place.uid, preferences), place)
}
}
} else {
original = locationDao.getGeofences(task.id)
}
if (original != null) {
location = Location(original!!.geofence, original!!.place)
}
} else {
original = savedInstanceState.getParcelable(EXTRA_ORIGINAL)
location = savedInstanceState.getParcelable(EXTRA_LOCATION)
}
return view
}
override fun onResume() {
super.onResume()
updateUi()
}
private fun setLocation(location: Location?) {
this.location = location
updateUi()
}
private fun updateUi() {
if (location == null) {
locationName.text = ""
geofenceOptions.visibility = View.GONE
locationAddress.visibility = View.GONE
} else {
geofenceOptions.visibility = if (device.supportsGeofences()) View.VISIBLE else View.GONE
geofenceOptions.setImageResource(
if (permissionChecker.canAccessLocation()
&& (location!!.isArrival || location!!.isDeparture)) R.drawable.ic_outline_notifications_24px else R.drawable.ic_outline_notifications_off_24px)
val name = location!!.displayName
val address = location!!.displayAddress
if (!isNullOrEmpty(address) && address != name) {
locationAddress.text = address
locationAddress.visibility = View.VISIBLE
} else {
locationAddress.visibility = View.GONE
}
val spannableString = SpannableString(name)
spannableString.setSpan(
object : ClickableSpan() {
override fun onClick(view: View) {}
},
0,
name.length,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
locationName.text = spannableString
}
}
override fun onRowClick() {
if (location == null) {
chooseLocation()
} else {
val options: MutableList<Pair<Int, () -> Unit>> = ArrayList()
options.add(Pair.create(R.string.open_map, { location!!.open(activity) }))
if (!isNullOrEmpty(location!!.phone)) {
options.add(Pair.create(R.string.action_call, { call() }))
}
if (!isNullOrEmpty(location!!.url)) {
options.add(Pair.create(R.string.visit_website, { openWebsite() }))
}
options.add(Pair.create(R.string.choose_new_location, { chooseLocation() }))
options.add(Pair.create(R.string.delete, { setLocation(null) }))
val items = options.map { requireContext().getString(it.first!!) }
dialogBuilder
.newDialog(location!!.displayName)
.setItems(items) { _, which: Int ->
options[which].second!!.invoke()
}
.show()
}
}
override val isClickable: Boolean
get() = true
private fun chooseLocation() {
val intent = Intent(activity, LocationPickerActivity::class.java)
if (location != null) {
intent.putExtra(LocationPickerActivity.EXTRA_PLACE, location!!.place as Parcelable)
}
startActivityForResult(intent, REQUEST_LOCATION_REMINDER)
}
@OnClick(R.id.geofence_options)
fun geofenceOptions() {
if (permissionRequestor.requestFineLocation()) {
showGeofenceOptions()
}
}
override fun onRequestPermissionsResult(
requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
if (requestCode == PermissionRequestor.REQUEST_LOCATION) {
if (verifyPermissions(grantResults)) {
showGeofenceOptions()
} else {
dialogBuilder
.newDialog(R.string.missing_permissions)
.setMessage(R.string.location_permission_required_geofence)
.setPositiveButton(android.R.string.ok, null)
.show()
}
} else {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
}
}
private fun showGeofenceOptions() {
val dialog = GeofenceDialog.newGeofenceDialog(location)
dialog.setTargetFragment(this, REQUEST_GEOFENCE_DETAILS)
dialog.show(parentFragmentManager, FRAG_TAG_LOCATION_DIALOG)
}
override val layout: Int
get() = R.layout.location_row
override val icon: Int
get() = R.drawable.ic_outline_place_24px
override fun controlId() = TAG
private fun openWebsite() {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(location!!.url)))
}
private fun call() {
startActivity(Intent(Intent.ACTION_DIAL, Uri.parse("tel:" + location!!.phone)))
}
override fun hasChanges(task: Task): Boolean {
if (original == null) {
return location != null
}
if (location == null) {
return true
}
return if (original!!.place != location!!.place) {
true
} else {
original!!.isDeparture != location!!.isDeparture
|| original!!.isArrival != location!!.isArrival
|| original!!.radius != location!!.radius
}
}
override fun requiresId() = true
override fun apply(task: Task) {
if (original == null || location == null || original!!.place != location!!.place) {
task.putTransitory(SyncFlags.FORCE_CALDAV_SYNC, true)
}
if (original != null) {
locationDao.delete(original!!.geofence)
geofenceApi.update(original!!.place)
}
if (location != null) {
val place = location!!.place
val geofence = location!!.geofence
geofence.task = task.id
geofence.place = place.uid
geofence.id = locationDao.insert(geofence)
geofenceApi.update(place)
}
task.modificationDate = DateUtilities.now()
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putParcelable(EXTRA_ORIGINAL, original)
outState.putParcelable(EXTRA_LOCATION, location)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == REQUEST_LOCATION_REMINDER) {
if (resultCode == Activity.RESULT_OK) {
val place: Place = data!!.getParcelableExtra(LocationPickerActivity.EXTRA_PLACE)!!
val geofence = if (location == null) {
Geofence(place.uid, preferences)
} else {
val existing = location!!.geofence
Geofence(
place.uid,
existing.isArrival,
existing.isDeparture,
existing.radius)
}
setLocation(Location(geofence, place))
}
} else if (requestCode == REQUEST_GEOFENCE_DETAILS) {
if (resultCode == Activity.RESULT_OK) {
location!!.geofence = data!!.getParcelableExtra(GeofenceDialog.EXTRA_GEOFENCE)!!
updateUi()
}
} else {
super.onActivityResult(requestCode, resultCode, data)
}
}
override fun inject(component: FragmentComponent) = component.inject(this)
companion object {
const val TAG = R.string.TEA_ctrl_locations_pref
private const val REQUEST_LOCATION_REMINDER = 12153
private const val REQUEST_GEOFENCE_DETAILS = 12154
private const val FRAG_TAG_LOCATION_DIALOG = "location_dialog"
private const val EXTRA_ORIGINAL = "extra_original_location"
private const val EXTRA_LOCATION = "extra_new_location"
}
}

@ -1,133 +0,0 @@
package org.tasks.ui;
import android.content.Context;
import android.content.res.ColorStateList;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.AppCompatRadioButton;
import butterknife.BindView;
import butterknife.OnClick;
import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.data.Task.Priority;
import javax.inject.Inject;
import org.tasks.R;
import org.tasks.injection.ForApplication;
import org.tasks.injection.FragmentComponent;
import org.tasks.themes.ColorProvider;
public class PriorityControlSet extends TaskEditControlFragment {
public static final int TAG = R.string.TEA_ctrl_importance_pref;
private static final String EXTRA_PRIORITY = "extra_priority";
@Inject @ForApplication Context context;
@Inject ColorProvider colorProvider;
@BindView(R.id.priority_high)
AppCompatRadioButton priorityHigh;
@BindView(R.id.priority_medium)
AppCompatRadioButton priorityMedium;
@BindView(R.id.priority_low)
AppCompatRadioButton priorityLow;
@BindView(R.id.priority_none)
AppCompatRadioButton priorityNone;
private @Priority int priority;
@Override
protected void inject(FragmentComponent component) {
component.inject(this);
}
@OnClick({R.id.priority_high, R.id.priority_medium, R.id.priority_low, R.id.priority_none})
void onPriorityChanged() {
priority = getPriority();
}
@Nullable
@Override
public View onCreateView(
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
final View view = super.onCreateView(inflater, container, savedInstanceState);
if (savedInstanceState == null) {
priority = task.getPriority();
} else {
priority = savedInstanceState.getInt(EXTRA_PRIORITY);
}
if (priority == 0) {
priorityHigh.setChecked(true);
} else if (priority == 1) {
priorityMedium.setChecked(true);
} else if (priority == 2) {
priorityLow.setChecked(true);
} else {
priorityNone.setChecked(true);
}
tintRadioButton(priorityHigh, 0);
tintRadioButton(priorityMedium, 1);
tintRadioButton(priorityLow, 2);
tintRadioButton(priorityNone, 3);
return view;
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putInt(EXTRA_PRIORITY, priority);
}
@Override
protected int getLayout() {
return R.layout.control_set_priority;
}
@Override
protected int getIcon() {
return R.drawable.ic_outline_flag_24px;
}
@Override
public int controlId() {
return TAG;
}
@Override
public void apply(Task task) {
task.setPriority(priority);
}
@Override
public boolean hasChanges(Task original) {
return original.getPriority() != priority;
}
private void tintRadioButton(AppCompatRadioButton radioButton, int priority) {
int color = colorProvider.getPriorityColor(priority, true);
radioButton.setButtonTintList(
new ColorStateList(
new int[][] {
new int[] {-android.R.attr.state_checked}, new int[] {android.R.attr.state_checked}
},
new int[] {color, color}));
}
private @Priority int getPriority() {
if (priorityHigh.isChecked()) {
return Priority.HIGH;
}
if (priorityMedium.isChecked()) {
return Priority.MEDIUM;
}
if (priorityLow.isChecked()) {
return Priority.LOW;
}
return Priority.NONE;
}
}

@ -0,0 +1,104 @@
package org.tasks.ui
import android.content.res.ColorStateList
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.AppCompatRadioButton
import butterknife.BindView
import butterknife.OnClick
import com.todoroo.astrid.data.Task
import org.tasks.R
import org.tasks.injection.FragmentComponent
import org.tasks.themes.ColorProvider
import javax.inject.Inject
class PriorityControlSet : TaskEditControlFragment() {
@Inject lateinit var colorProvider: ColorProvider
@BindView(R.id.priority_high)
lateinit var priorityHigh: AppCompatRadioButton
@BindView(R.id.priority_medium)
lateinit var priorityMedium: AppCompatRadioButton
@BindView(R.id.priority_low)
lateinit var priorityLow: AppCompatRadioButton
@BindView(R.id.priority_none)
lateinit var priorityNone: AppCompatRadioButton
@Task.Priority
private var priority = 0
override fun inject(component: FragmentComponent) = component.inject(this)
@OnClick(R.id.priority_high, R.id.priority_medium, R.id.priority_low, R.id.priority_none)
fun onPriorityChanged() {
priority = getPriority()
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = super.onCreateView(inflater, container, savedInstanceState)
priority = savedInstanceState?.getInt(EXTRA_PRIORITY) ?: task.priority
when (priority) {
0 -> priorityHigh.isChecked = true
1 -> priorityMedium.isChecked = true
2 -> priorityLow.isChecked = true
else -> priorityNone.isChecked = true
}
tintRadioButton(priorityHigh, 0)
tintRadioButton(priorityMedium, 1)
tintRadioButton(priorityLow, 2)
tintRadioButton(priorityNone, 3)
return view
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putInt(EXTRA_PRIORITY, priority)
}
override val layout: Int
get() = R.layout.control_set_priority
override val icon: Int
get() = R.drawable.ic_outline_flag_24px
override fun controlId() = TAG
override fun apply(task: Task) {
task.priority = priority
}
override fun hasChanges(original: Task): Boolean {
return original.priority != priority
}
private fun tintRadioButton(radioButton: AppCompatRadioButton, priority: Int) {
val color = colorProvider.getPriorityColor(priority, true)
radioButton.buttonTintList = ColorStateList(arrayOf(intArrayOf(-android.R.attr.state_checked), intArrayOf(android.R.attr.state_checked)), intArrayOf(color, color))
}
@Task.Priority
private fun getPriority(): Int {
if (priorityHigh.isChecked) {
return Task.Priority.HIGH
}
if (priorityMedium.isChecked) {
return Task.Priority.MEDIUM
}
return if (priorityLow.isChecked) {
Task.Priority.LOW
} else {
Task.Priority.NONE
}
}
companion object {
const val TAG = R.string.TEA_ctrl_importance_pref
private const val EXTRA_PRIORITY = "extra_priority"
}
}

@ -1,233 +0,0 @@
package org.tasks.ui;
import static android.app.Activity.RESULT_OK;
import static org.tasks.activities.RemoteListPicker.newRemoteListSupportPicker;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.Nullable;
import butterknife.BindView;
import com.google.android.material.chip.Chip;
import com.google.android.material.chip.ChipGroup;
import com.google.common.collect.ImmutableList;
import com.todoroo.astrid.api.CaldavFilter;
import com.todoroo.astrid.api.Filter;
import com.todoroo.astrid.api.GtasksFilter;
import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.gtasks.GtasksListService;
import com.todoroo.astrid.service.TaskMover;
import java.util.Objects;
import javax.inject.Inject;
import org.tasks.R;
import org.tasks.activities.RemoteListPicker;
import org.tasks.data.CaldavCalendar;
import org.tasks.data.CaldavDao;
import org.tasks.data.CaldavTask;
import org.tasks.data.GoogleTask;
import org.tasks.data.GoogleTaskDao;
import org.tasks.data.GoogleTaskList;
import org.tasks.injection.FragmentComponent;
import org.tasks.preferences.DefaultFilterProvider;
public class RemoteListFragment extends TaskEditControlFragment {
public static final int TAG = R.string.TEA_ctrl_google_task_list;
private static final String FRAG_TAG_GOOGLE_TASK_LIST_SELECTION =
"frag_tag_google_task_list_selection";
private static final String EXTRA_ORIGINAL_LIST = "extra_original_list";
private static final String EXTRA_SELECTED_LIST = "extra_selected_list";
private static final int REQUEST_CODE_SELECT_LIST = 10101;
@BindView(R.id.dont_sync)
TextView textView;
@BindView(R.id.chip_group)
ChipGroup chipGroup;
@Inject GtasksListService gtasksListService;
@Inject GoogleTaskDao googleTaskDao;
@Inject CaldavDao caldavDao;
@Inject DefaultFilterProvider defaultFilterProvider;
@Inject TaskMover taskMover;
@Inject ChipProvider chipProvider;
@Nullable private Filter originalList;
@Nullable private Filter selectedList;
private OnListChanged callback;
public interface OnListChanged {
void onListchanged(@Nullable Filter filter);
}
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
callback = (OnListChanged) activity;
}
@Nullable
@Override
public View onCreateView(
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = super.onCreateView(inflater, container, savedInstanceState);
if (savedInstanceState != null) {
originalList = savedInstanceState.getParcelable(EXTRA_ORIGINAL_LIST);
setSelected(savedInstanceState.getParcelable(EXTRA_SELECTED_LIST));
} else {
if (task.isNew()) {
if (task.hasTransitory(GoogleTask.KEY)) {
GoogleTaskList googleTaskList =
gtasksListService.getList(task.getTransitory(GoogleTask.KEY));
if (googleTaskList != null) {
originalList = new GtasksFilter(googleTaskList);
}
} else if (task.hasTransitory(CaldavTask.KEY)) {
CaldavCalendar caldav = caldavDao.getCalendarByUuid(task.getTransitory(CaldavTask.KEY));
if (caldav != null) {
originalList = new CaldavFilter(caldav);
}
} else {
originalList = defaultFilterProvider.getDefaultRemoteList();
}
} else {
GoogleTask googleTask = googleTaskDao.getByTaskId(task.getId());
CaldavTask caldavTask = caldavDao.getTask(task.getId());
if (googleTask != null) {
GoogleTaskList googleTaskList = gtasksListService.getList(googleTask.getListId());
if (googleTaskList != null) {
originalList = new GtasksFilter(googleTaskList);
}
} else if (caldavTask != null) {
CaldavCalendar calendarByUuid = caldavDao.getCalendarByUuid(caldavTask.getCalendar());
if (calendarByUuid != null) {
originalList = new CaldavFilter(calendarByUuid);
}
}
}
setSelected(originalList);
}
return view;
}
private void setSelected(@Nullable Filter filter) {
selectedList = filter;
refreshView();
callback.onListchanged(filter);
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
if (originalList != null) {
outState.putParcelable(EXTRA_ORIGINAL_LIST, originalList);
}
if (selectedList != null) {
outState.putParcelable(EXTRA_SELECTED_LIST, selectedList);
}
}
@Override
protected int getLayout() {
return R.layout.control_set_remote_list;
}
@Override
protected int getIcon() {
return R.drawable.ic_outline_cloud_24px;
}
@Override
public int controlId() {
return TAG;
}
@Override
protected void onRowClick() {
openPicker();
}
@Override
protected boolean isClickable() {
return true;
}
private void openPicker() {
newRemoteListSupportPicker(selectedList, this, REQUEST_CODE_SELECT_LIST)
.show(getParentFragmentManager(), FRAG_TAG_GOOGLE_TASK_LIST_SELECTION);
}
@Override
public boolean requiresId() {
return true;
}
@Override
public void apply(Task task) {
if (isNew() || hasChanges()) {
task.setParent(0);
task.setParentUuid(null);
taskMover.move(ImmutableList.of(task.getId()), selectedList);
}
}
@Override
public boolean hasChanges(Task original) {
return hasChanges();
}
private boolean hasChanges() {
return !Objects.equals(selectedList, originalList);
}
@Override
protected void inject(FragmentComponent component) {
component.inject(this);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_CODE_SELECT_LIST) {
if (resultCode == RESULT_OK) {
setList(data.getParcelableExtra(RemoteListPicker.EXTRA_SELECTED_FILTER));
}
} else {
super.onActivityResult(requestCode, resultCode, data);
}
}
private void setList(Filter list) {
if (list == null) {
setSelected(null);
} else if (list instanceof GtasksFilter || list instanceof CaldavFilter) {
setSelected(list);
} else {
throw new RuntimeException("Unhandled filter type");
}
}
private void refreshView() {
if (selectedList == null) {
textView.setVisibility(View.VISIBLE);
chipGroup.setVisibility(View.GONE);
} else {
textView.setVisibility(View.GONE);
chipGroup.setVisibility(View.VISIBLE);
chipGroup.removeAllViews();
Chip chip =
chipProvider.newChip(selectedList, R.drawable.ic_outline_cloud_24px, true, true);
chip.setCloseIconVisible(true);
chip.setOnClickListener(v -> openPicker());
chip.setOnCloseIconClickListener(v -> setSelected(null));
chipGroup.addView(chip);
}
}
}

@ -0,0 +1,196 @@
package org.tasks.ui
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import butterknife.BindView
import com.google.android.material.chip.ChipGroup
import com.todoroo.astrid.api.CaldavFilter
import com.todoroo.astrid.api.Filter
import com.todoroo.astrid.api.GtasksFilter
import com.todoroo.astrid.data.Task
import com.todoroo.astrid.gtasks.GtasksListService
import com.todoroo.astrid.service.TaskMover
import org.tasks.R
import org.tasks.activities.RemoteListPicker
import org.tasks.data.CaldavDao
import org.tasks.data.CaldavTask
import org.tasks.data.GoogleTask
import org.tasks.data.GoogleTaskDao
import org.tasks.injection.FragmentComponent
import org.tasks.preferences.DefaultFilterProvider
import javax.inject.Inject
class RemoteListFragment : TaskEditControlFragment() {
@BindView(R.id.dont_sync)
lateinit var textView: TextView
@BindView(R.id.chip_group)
lateinit var chipGroup: ChipGroup
@Inject lateinit var gtasksListService: GtasksListService
@Inject lateinit var googleTaskDao: GoogleTaskDao
@Inject lateinit var caldavDao: CaldavDao
@Inject lateinit var defaultFilterProvider: DefaultFilterProvider
@Inject lateinit var taskMover: TaskMover
@Inject lateinit var chipProvider: ChipProvider
private var originalList: Filter? = null
private var selectedList: Filter? = null
private lateinit var callback: OnListChanged
interface OnListChanged {
fun onListchanged(filter: Filter?)
}
override fun onAttach(activity: Activity) {
super.onAttach(activity)
callback = activity as OnListChanged
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = super.onCreateView(inflater, container, savedInstanceState)
if (savedInstanceState != null) {
originalList = savedInstanceState.getParcelable(EXTRA_ORIGINAL_LIST)
setSelected(savedInstanceState.getParcelable(EXTRA_SELECTED_LIST))
} else {
if (task.isNew) {
if (task.hasTransitory(GoogleTask.KEY)) {
val listId = task.getTransitory<String>(GoogleTask.KEY)!!
val googleTaskList = gtasksListService.getList(listId)
if (googleTaskList != null) {
originalList = GtasksFilter(googleTaskList)
}
} else if (task.hasTransitory(CaldavTask.KEY)) {
val caldav = caldavDao.getCalendarByUuid(task.getTransitory(CaldavTask.KEY)!!)
if (caldav != null) {
originalList = CaldavFilter(caldav)
}
} else {
originalList = defaultFilterProvider.defaultRemoteList
}
} else {
val googleTask = googleTaskDao.getByTaskId(task.id)
val caldavTask = caldavDao.getTask(task.id)
if (googleTask != null) {
val googleTaskList = gtasksListService.getList(googleTask.listId)
if (googleTaskList != null) {
originalList = GtasksFilter(googleTaskList)
}
} else if (caldavTask != null) {
val calendarByUuid = caldavDao.getCalendarByUuid(caldavTask.calendar!!)
if (calendarByUuid != null) {
originalList = CaldavFilter(calendarByUuid)
}
}
}
setSelected(originalList)
}
return view
}
private fun setSelected(filter: Filter?) {
selectedList = filter
refreshView()
callback.onListchanged(filter)
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
if (originalList != null) {
outState.putParcelable(EXTRA_ORIGINAL_LIST, originalList)
}
if (selectedList != null) {
outState.putParcelable(EXTRA_SELECTED_LIST, selectedList)
}
}
override val layout: Int
get() = R.layout.control_set_remote_list
override val icon: Int
get() = R.drawable.ic_outline_cloud_24px
override fun controlId() = TAG
override fun onRowClick() {
openPicker()
}
override val isClickable: Boolean
get() = true
private fun openPicker() {
RemoteListPicker.newRemoteListSupportPicker(selectedList, this, REQUEST_CODE_SELECT_LIST)
.show(parentFragmentManager, FRAG_TAG_GOOGLE_TASK_LIST_SELECTION)
}
override fun requiresId() = true
override fun apply(task: Task) {
if (isNew || hasChanges()) {
task.parent = 0
task.parentUuid = null
taskMover.move(listOf(task.id), selectedList)
}
}
override fun hasChanges(original: Task): Boolean {
return hasChanges()
}
private fun hasChanges(): Boolean {
return selectedList != originalList
}
override fun inject(component: FragmentComponent) = component.inject(this)
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == REQUEST_CODE_SELECT_LIST) {
if (resultCode == Activity.RESULT_OK) {
setList(data?.getParcelableExtra(RemoteListPicker.EXTRA_SELECTED_FILTER))
}
} else {
super.onActivityResult(requestCode, resultCode, data)
}
}
private fun setList(list: Filter?) {
if (list == null) {
setSelected(null)
} else if (list is GtasksFilter || list is CaldavFilter) {
setSelected(list)
} else {
throw RuntimeException("Unhandled filter type")
}
}
private fun refreshView() {
if (selectedList == null) {
textView.visibility = View.VISIBLE
chipGroup.visibility = View.GONE
} else {
textView.visibility = View.GONE
chipGroup.visibility = View.VISIBLE
chipGroup.removeAllViews()
val chip = chipProvider.newChip(selectedList, R.drawable.ic_outline_cloud_24px, true, true)
chip.isCloseIconVisible = true
chip.setOnClickListener { openPicker() }
chip.setOnCloseIconClickListener { setSelected(null) }
chipGroup.addView(chip)
}
}
companion object {
const val TAG = R.string.TEA_ctrl_google_task_list
private const val FRAG_TAG_GOOGLE_TASK_LIST_SELECTION = "frag_tag_google_task_list_selection"
private const val EXTRA_ORIGINAL_LIST = "extra_original_list"
private const val EXTRA_SELECTED_LIST = "extra_selected_list"
private const val REQUEST_CODE_SELECT_LIST = 10101
}
}

@ -1,335 +0,0 @@
package org.tasks.ui;
import static com.todoroo.andlib.utility.DateUtilities.now;
import static org.tasks.Strings.isNullOrEmpty;
import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.graphics.Paint;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import android.widget.LinearLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.DefaultItemAnimator;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import butterknife.BindView;
import butterknife.OnClick;
import com.todoroo.andlib.sql.Criterion;
import com.todoroo.andlib.sql.Join;
import com.todoroo.andlib.sql.QueryTemplate;
import com.todoroo.astrid.activity.MainActivity;
import com.todoroo.astrid.api.CaldavFilter;
import com.todoroo.astrid.api.Filter;
import com.todoroo.astrid.api.GtasksFilter;
import com.todoroo.astrid.dao.TaskDao;
import com.todoroo.astrid.dao.TaskDao.TaskCriteria;
import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.service.TaskCompleter;
import com.todoroo.astrid.service.TaskCreator;
import com.todoroo.astrid.ui.CheckableImageView;
import java.util.ArrayList;
import javax.inject.Inject;
import org.tasks.LocalBroadcastManager;
import org.tasks.R;
import org.tasks.data.CaldavDao;
import org.tasks.data.CaldavTask;
import org.tasks.data.GoogleTask;
import org.tasks.data.GoogleTaskDao;
import org.tasks.injection.FragmentComponent;
import org.tasks.locale.Locale;
import org.tasks.preferences.Preferences;
import org.tasks.tasklist.SubtaskViewHolder.Callbacks;
import org.tasks.tasklist.SubtasksRecyclerAdapter;
public class SubtaskControlSet extends TaskEditControlFragment implements Callbacks {
public static final int TAG = R.string.TEA_ctrl_subtask_pref;
private static final String EXTRA_NEW_SUBTASKS = "extra_new_subtasks";
@BindView(R.id.recycler_view)
RecyclerView recyclerView;
@BindView(R.id.new_subtasks)
LinearLayout newSubtaskContainer;
@Inject Activity activity;
@Inject TaskCompleter taskCompleter;
@Inject LocalBroadcastManager localBroadcastManager;
@Inject GoogleTaskDao googleTaskDao;
@Inject Toaster toaster;
@Inject Preferences preferences;
@Inject TaskCreator taskCreator;
@Inject CaldavDao caldavDao;
@Inject TaskDao taskDao;
@Inject Locale locale;
@Inject CheckBoxProvider checkBoxProvider;
@Inject ChipProvider chipProvider;
private TaskListViewModel viewModel;
private final RefreshReceiver refreshReceiver = new RefreshReceiver();
private Filter remoteList;
private GoogleTask googleTask;
private SubtasksRecyclerAdapter recyclerAdapter;
@Override
protected void inject(FragmentComponent component) {
component.inject(this);
viewModel = new ViewModelProvider(this).get(TaskListViewModel.class);
component.inject(viewModel);
}
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putParcelableArrayList(EXTRA_NEW_SUBTASKS, getNewSubtasks());
}
@Nullable
@Override
public View onCreateView(
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
final View view = super.onCreateView(inflater, container, savedInstanceState);
if (savedInstanceState != null) {
for (Task task : savedInstanceState.<Task>getParcelableArrayList(EXTRA_NEW_SUBTASKS)) {
addSubtask(task);
}
}
recyclerAdapter = new SubtasksRecyclerAdapter(activity, chipProvider, checkBoxProvider, this);
if (task.getId() > 0) {
recyclerAdapter.submitList(viewModel.getValue());
viewModel.setFilter(new Filter("subtasks", getQueryTemplate(task)));
((DefaultItemAnimator) recyclerView.getItemAnimator()).setSupportsChangeAnimations(false);
recyclerView.setLayoutManager(new LinearLayoutManager(activity));
recyclerView.setNestedScrollingEnabled(false);
viewModel.observe(this, recyclerAdapter::submitList);
recyclerView.setAdapter(recyclerAdapter);
}
return view;
}
private static QueryTemplate getQueryTemplate(Task task) {
return new QueryTemplate()
.join(
Join.left(
GoogleTask.TABLE,
Criterion.and(
GoogleTask.PARENT.eq(task.getId()),
GoogleTask.TASK.eq(Task.ID),
GoogleTask.DELETED.eq(0))))
.where(
Criterion.and(
TaskCriteria.activeAndVisible(),
Criterion.or(Task.PARENT.eq(task.getId()), GoogleTask.TASK.gt(0))));
}
@Override
protected int getLayout() {
return R.layout.control_set_subtasks;
}
@Override
protected int getIcon() {
return R.drawable.ic_subdirectory_arrow_right_black_24dp;
}
@Override
public int controlId() {
return TAG;
}
@Override
public boolean requiresId() {
return true;
}
@Override
public void apply(Task task) {
for (Task subtask: getNewSubtasks()) {
if (isNullOrEmpty(subtask.getTitle())) {
continue;
}
subtask.setCompletionDate(task.getCompletionDate());
taskDao.createNew(subtask);
if (remoteList instanceof GtasksFilter) {
GoogleTask googleTask =
new GoogleTask(subtask.getId(), ((GtasksFilter) remoteList).getRemoteId());
googleTask.setParent(task.getId());
googleTask.setMoved(true);
googleTaskDao.insertAndShift(googleTask, preferences.addGoogleTasksToTop());
} else if (remoteList instanceof CaldavFilter) {
CaldavTask caldavTask =
new CaldavTask(subtask.getId(), ((CaldavFilter) remoteList).getUuid());
subtask.setParent(task.getId());
caldavTask.setRemoteParent(caldavDao.getRemoteIdForTask(task.getId()));
taskDao.save(subtask);
caldavDao.insert(caldavTask);
} else {
subtask.setParent(task.getId());
subtask.setParentUuid(task.getUuid());
taskDao.save(subtask);
}
}
}
@Override
public boolean hasChanges(Task original) {
return !getNewSubtasks().isEmpty();
}
private ArrayList<Task> getNewSubtasks() {
ArrayList<Task> subtasks = new ArrayList<>();
int children = newSubtaskContainer.getChildCount();
for (int i = 0 ; i < children ; i++) {
View view = newSubtaskContainer.getChildAt(i);
EditText title = view.findViewById(R.id.title);
CheckableImageView completed = view.findViewById(R.id.completeBox);
Task subtask = taskCreator.createWithValues(title.getText().toString());
if (completed.isChecked()) {
subtask.setCompletionDate(now());
}
subtasks.add(subtask);
}
return subtasks;
}
@Override
public void onResume() {
super.onResume();
localBroadcastManager.registerRefreshReceiver(refreshReceiver);
googleTask = googleTaskDao.getByTaskId(task.getId());
updateUI();
}
@Override
public void onPause() {
super.onPause();
localBroadcastManager.unregisterReceiver(refreshReceiver);
}
@OnClick(R.id.add_subtask)
void addSubtask() {
if (isGoogleTaskChild()) {
toaster.longToast(R.string.subtasks_multilevel_google_task);
return;
}
EditText editText = addSubtask(taskCreator.createWithValues(""));
editText.requestFocus();
InputMethodManager imm =
(InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
imm.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT);
}
private EditText addSubtask(Task task) {
ViewGroup view =
(ViewGroup)
LayoutInflater.from(activity)
.inflate(R.layout.editable_subtask_adapter_row_body, newSubtaskContainer, false);
view.findViewById(R.id.clear).setOnClickListener(v -> newSubtaskContainer.removeView(view));
EditText editText = view.findViewById(R.id.title);
editText.setTextKeepState(task.getTitle());
editText.setHorizontallyScrolling(false);
editText.setLines(1);
editText.setMaxLines(Integer.MAX_VALUE);
editText.setFocusable(true);
editText.setEnabled(true);
editText.setOnEditorActionListener(
(arg0, actionId, arg2) -> {
if (actionId == EditorInfo.IME_ACTION_NEXT) {
if (editText.getText().length() != 0) {
addSubtask();
}
return true;
}
return false;
});
CheckableImageView completeBox = view.findViewById(R.id.completeBox);
completeBox.setChecked(task.isCompleted());
updateCompleteBox(task, completeBox, editText);
completeBox.setOnClickListener(v -> updateCompleteBox(task, completeBox, editText));
newSubtaskContainer.addView(view);
return editText;
}
private void updateCompleteBox(Task task, CheckableImageView completeBox, EditText editText) {
boolean isComplete = completeBox.isChecked();
completeBox.setImageDrawable(
checkBoxProvider.getCheckBox(isComplete, false, task.getPriority()));
if (isComplete) {
editText.setPaintFlags(editText.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
} else {
editText.setPaintFlags(editText.getPaintFlags() & ~Paint.STRIKE_THRU_TEXT_FLAG);
}
}
private boolean isGoogleTaskChild() {
return remoteList instanceof GtasksFilter
&& googleTask != null
&& googleTask.getParent() > 0
&& googleTask.getListId().equals(((GtasksFilter) remoteList).getRemoteId());
}
private void updateUI() {
if (isGoogleTaskChild()) {
recyclerView.setVisibility(View.GONE);
newSubtaskContainer.setVisibility(View.GONE);
} else {
recyclerView.setVisibility(View.VISIBLE);
newSubtaskContainer.setVisibility(View.VISIBLE);
recyclerAdapter.setMultiLevelSubtasksEnabled(!(remoteList instanceof GtasksFilter));
refresh();
}
}
public void onRemoteListChanged(@Nullable Filter filter) {
this.remoteList = filter;
if (recyclerView != null) {
updateUI();
}
}
private void refresh() {
viewModel.invalidate();
}
@Override
public void openSubtask(Task task) {
((MainActivity) getActivity()).getTaskListFragment().onTaskListItemClicked(task);
}
@Override
public void toggleSubtask(long taskId, boolean collapsed) {
taskDao.setCollapsed(taskId, collapsed);
localBroadcastManager.broadcastRefresh();
}
@Override
public void complete(Task task, boolean completed) {
taskCompleter.setComplete(task, completed);
}
protected class RefreshReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
refresh();
}
}
}

@ -0,0 +1,296 @@
package org.tasks.ui
import android.app.Activity
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.graphics.Paint
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import android.widget.EditText
import android.widget.LinearLayout
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import butterknife.BindView
import butterknife.OnClick
import com.todoroo.andlib.sql.Criterion
import com.todoroo.andlib.sql.Join
import com.todoroo.andlib.sql.QueryTemplate
import com.todoroo.andlib.utility.DateUtilities
import com.todoroo.astrid.activity.MainActivity
import com.todoroo.astrid.api.CaldavFilter
import com.todoroo.astrid.api.Filter
import com.todoroo.astrid.api.GtasksFilter
import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.dao.TaskDao.TaskCriteria.activeAndVisible
import com.todoroo.astrid.data.Task
import com.todoroo.astrid.service.TaskCompleter
import com.todoroo.astrid.service.TaskCreator
import com.todoroo.astrid.ui.CheckableImageView
import org.tasks.LocalBroadcastManager
import org.tasks.R
import org.tasks.Strings.isNullOrEmpty
import org.tasks.data.*
import org.tasks.injection.FragmentComponent
import org.tasks.locale.Locale
import org.tasks.preferences.Preferences
import org.tasks.tasklist.SubtaskViewHolder
import org.tasks.tasklist.SubtasksRecyclerAdapter
import java.util.*
import javax.inject.Inject
class SubtaskControlSet : TaskEditControlFragment(), SubtaskViewHolder.Callbacks {
@JvmField
@BindView(R.id.recycler_view)
var recyclerView: RecyclerView? = null
@BindView(R.id.new_subtasks)
lateinit var newSubtaskContainer: LinearLayout
@Inject lateinit var activity: Activity
@Inject lateinit var taskCompleter: TaskCompleter
@Inject lateinit var localBroadcastManager: LocalBroadcastManager
@Inject lateinit var googleTaskDao: GoogleTaskDao
@Inject lateinit var toaster: Toaster
@Inject lateinit var preferences: Preferences
@Inject lateinit var taskCreator: TaskCreator
@Inject lateinit var caldavDao: CaldavDao
@Inject lateinit var taskDao: TaskDao
@Inject lateinit var locale: Locale
@Inject lateinit var checkBoxProvider: CheckBoxProvider
@Inject lateinit var chipProvider: ChipProvider
private lateinit var viewModel: TaskListViewModel
private val refreshReceiver = RefreshReceiver()
private var remoteList: Filter? = null
private var googleTask: GoogleTask? = null
private lateinit var recyclerAdapter: SubtasksRecyclerAdapter
override fun inject(component: FragmentComponent) {
component.inject(this)
viewModel = ViewModelProvider(this).get(TaskListViewModel::class.java)
component.inject(viewModel)
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putParcelableArrayList(EXTRA_NEW_SUBTASKS, newSubtasks)
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = super.onCreateView(inflater, container, savedInstanceState)
if (savedInstanceState != null) {
for (task in savedInstanceState.getParcelableArrayList<Task>(EXTRA_NEW_SUBTASKS)!!) {
addSubtask(task)
}
}
recyclerAdapter = SubtasksRecyclerAdapter(activity, chipProvider, checkBoxProvider, this)
if (task.id > 0) {
recyclerAdapter.submitList(viewModel.value)
viewModel.setFilter(Filter("subtasks", getQueryTemplate(task)))
(recyclerView!!.itemAnimator as DefaultItemAnimator).supportsChangeAnimations = false
recyclerView!!.layoutManager = LinearLayoutManager(activity)
recyclerView!!.isNestedScrollingEnabled = false
viewModel.observe(this, Observer { list: List<TaskContainer?>? -> recyclerAdapter.submitList(list) })
recyclerView!!.adapter = recyclerAdapter
}
return view
}
override val layout: Int
get() = R.layout.control_set_subtasks
override val icon: Int
get() = R.drawable.ic_subdirectory_arrow_right_black_24dp
override fun controlId() = TAG
override fun requiresId() = true
override fun apply(task: Task) {
for (subtask in newSubtasks) {
if (isNullOrEmpty(subtask.title)) {
continue
}
subtask.completionDate = task.completionDate
taskDao.createNew(subtask)
when (remoteList) {
is GtasksFilter -> {
val googleTask = GoogleTask(subtask.id, (remoteList as GtasksFilter).remoteId)
googleTask.parent = task.id
googleTask.isMoved = true
googleTaskDao.insertAndShift(googleTask, preferences.addGoogleTasksToTop())
}
is CaldavFilter -> {
val caldavTask = CaldavTask(subtask.id, (remoteList as CaldavFilter).uuid)
subtask.parent = task.id
caldavTask.remoteParent = caldavDao.getRemoteIdForTask(task.id)
taskDao.save(subtask)
caldavDao.insert(caldavTask)
}
else -> {
subtask.parent = task.id
subtask.parentUuid = task.uuid
taskDao.save(subtask)
}
}
}
}
override fun hasChanges(original: Task): Boolean {
return newSubtasks.isNotEmpty()
}
private val newSubtasks: ArrayList<Task>
get() {
val subtasks = ArrayList<Task>()
val children = newSubtaskContainer.childCount
for (i in 0 until children) {
val view = newSubtaskContainer.getChildAt(i)
val title = view.findViewById<EditText>(R.id.title)
val completed: CheckableImageView = view.findViewById(R.id.completeBox)
val subtask = taskCreator.createWithValues(title.text.toString())
if (completed.isChecked) {
subtask.completionDate = DateUtilities.now()
}
subtasks.add(subtask)
}
return subtasks
}
override fun onResume() {
super.onResume()
localBroadcastManager.registerRefreshReceiver(refreshReceiver)
googleTask = googleTaskDao.getByTaskId(task.id)
updateUI()
}
override fun onPause() {
super.onPause()
localBroadcastManager.unregisterReceiver(refreshReceiver)
}
@OnClick(R.id.add_subtask)
fun addSubtask() {
if (isGoogleTaskChild) {
toaster.longToast(R.string.subtasks_multilevel_google_task)
return
}
val editText = addSubtask(taskCreator.createWithValues(""))
editText.requestFocus()
val imm = activity.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT)
}
private fun addSubtask(task: Task): EditText {
val view = LayoutInflater.from(activity)
.inflate(R.layout.editable_subtask_adapter_row_body, newSubtaskContainer, false) as ViewGroup
view.findViewById<View>(R.id.clear).setOnClickListener { newSubtaskContainer.removeView(view) }
val editText = view.findViewById<EditText>(R.id.title)
editText.setTextKeepState(task.title)
editText.setHorizontallyScrolling(false)
editText.setLines(1)
editText.maxLines = Int.MAX_VALUE
editText.isFocusable = true
editText.isEnabled = true
editText.setOnEditorActionListener { _, actionId: Int, _ ->
if (actionId == EditorInfo.IME_ACTION_NEXT) {
if (editText.text.isNotEmpty()) {
addSubtask()
}
return@setOnEditorActionListener true
}
false
}
val completeBox: CheckableImageView = view.findViewById(R.id.completeBox)
completeBox.isChecked = task.isCompleted
updateCompleteBox(task, completeBox, editText)
completeBox.setOnClickListener { updateCompleteBox(task, completeBox, editText) }
newSubtaskContainer.addView(view)
return editText
}
private fun updateCompleteBox(task: Task, completeBox: CheckableImageView, editText: EditText) {
val isComplete = completeBox.isChecked
completeBox.setImageDrawable(
checkBoxProvider.getCheckBox(isComplete, false, task.priority))
editText.paintFlags = if (isComplete) {
editText.paintFlags or Paint.STRIKE_THRU_TEXT_FLAG
} else {
editText.paintFlags and Paint.STRIKE_THRU_TEXT_FLAG.inv()
}
}
private val isGoogleTaskChild: Boolean
get() = (remoteList is GtasksFilter
&& googleTask != null && googleTask!!.parent > 0 && googleTask!!.listId == (remoteList as GtasksFilter).remoteId)
private fun updateUI() {
if (isGoogleTaskChild) {
recyclerView!!.visibility = View.GONE
newSubtaskContainer.visibility = View.GONE
} else {
recyclerView!!.visibility = View.VISIBLE
newSubtaskContainer.visibility = View.VISIBLE
recyclerAdapter.setMultiLevelSubtasksEnabled(remoteList !is GtasksFilter)
refresh()
}
}
fun onRemoteListChanged(filter: Filter?) {
remoteList = filter
if (recyclerView != null) {
updateUI()
}
}
private fun refresh() {
viewModel.invalidate()
}
override fun openSubtask(task: Task) {
(activity as MainActivity).taskListFragment.onTaskListItemClicked(task)
}
override fun toggleSubtask(taskId: Long, collapsed: Boolean) {
taskDao.setCollapsed(taskId, collapsed)
localBroadcastManager.broadcastRefresh()
}
override fun complete(task: Task, completed: Boolean) {
taskCompleter.setComplete(task, completed)
}
private inner class RefreshReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
refresh()
}
}
companion object {
const val TAG = R.string.TEA_ctrl_subtask_pref
private const val EXTRA_NEW_SUBTASKS = "extra_new_subtasks"
private fun getQueryTemplate(task: Task): QueryTemplate {
return QueryTemplate()
.join(
Join.left(
GoogleTask.TABLE,
Criterion.and(
GoogleTask.PARENT.eq(task.id),
GoogleTask.TASK.eq(Task.ID),
GoogleTask.DELETED.eq(0))))
.where(
Criterion.and(
activeAndVisible(),
Criterion.or(Task.PARENT.eq(task.id), GoogleTask.TASK.gt(0))))
}
}
}

@ -1,76 +0,0 @@
package org.tasks.ui;
import android.app.Activity;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.LinearLayout;
import androidx.annotation.Nullable;
import butterknife.ButterKnife;
import com.todoroo.astrid.data.Task;
import org.tasks.R;
import org.tasks.injection.InjectingFragment;
public abstract class TaskEditControlFragment extends InjectingFragment {
public static final String EXTRA_TASK = "extra_task";
public static final String EXTRA_IS_NEW = "extra_is_new";
protected Task task;
private boolean isNew;
@Nullable
@Override
public View onCreateView(
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
final View view = inflater.inflate(R.layout.control_set_template, null);
LinearLayout content = view.findViewById(R.id.content);
inflater.inflate(getLayout(), content);
ImageView icon = view.findViewById(R.id.icon);
icon.setImageResource(getIcon());
if (isClickable()) {
content.setOnClickListener(v -> onRowClick());
}
ButterKnife.bind(this, view);
return view;
}
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
Bundle arguments = getArguments();
if (arguments != null) {
task = arguments.getParcelable(EXTRA_TASK);
isNew = arguments.getBoolean(EXTRA_IS_NEW);
}
}
protected void onRowClick() {}
protected boolean isClickable() {
return false;
}
protected abstract int getLayout();
protected abstract int getIcon();
public abstract int controlId();
public boolean requiresId() {
return false;
}
public abstract void apply(Task task);
public boolean hasChanges(Task original) {
return false;
}
boolean isNew() {
return isNew;
}
}

@ -0,0 +1,63 @@
package org.tasks.ui
import android.app.Activity
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.LinearLayout
import butterknife.ButterKnife
import com.todoroo.astrid.data.Task
import org.tasks.R
import org.tasks.injection.InjectingFragment
abstract class TaskEditControlFragment : InjectingFragment() {
protected lateinit var task: Task
var isNew = false
private set
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.control_set_template, null)
val content = view.findViewById<LinearLayout>(R.id.content)
inflater.inflate(layout, content)
val icon = view.findViewById<ImageView>(R.id.icon)
icon.setImageResource(this.icon)
if (isClickable) {
content.setOnClickListener { onRowClick() }
}
ButterKnife.bind(this, view)
return view
}
override fun onAttach(activity: Activity) {
super.onAttach(activity)
val args = requireArguments()
task = args.getParcelable(EXTRA_TASK)!!
isNew = args.getBoolean(EXTRA_IS_NEW)
}
protected open fun onRowClick() {}
protected open val isClickable: Boolean
get() = false
protected abstract val layout: Int
protected abstract val icon: Int
abstract fun controlId(): Int
open fun requiresId(): Boolean {
return false
}
abstract fun apply(task: Task)
open fun hasChanges(original: Task): Boolean {
return false
}
companion object {
const val EXTRA_TASK = "extra_task"
const val EXTRA_IS_NEW = "extra_is_new"
}
}
Loading…
Cancel
Save