You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
tasks/src/com/timsu/astrid/activities/TaskListSubActivity.java

1140 lines
43 KiB
Java

/*
* ASTRID: Android's Simple Task Recording Dashboard
*
* Copyright (c) 2009 Tim Su
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package com.timsu.astrid.activities;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Map;
import java.util.Random;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.database.Cursor;
import android.database.StaleDataException;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
import android.view.ContextMenu;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.View.OnCreateContextMenuListener;
import android.widget.Button;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import com.timsu.astrid.R;
import com.timsu.astrid.activities.TaskListAdapter.TaskListAdapterHooks;
import com.timsu.astrid.data.tag.TagController;
import com.timsu.astrid.data.tag.TagIdentifier;
import com.timsu.astrid.data.tag.TagModelForView;
import com.timsu.astrid.data.task.TaskController;
import com.timsu.astrid.data.task.TaskIdentifier;
import com.timsu.astrid.data.task.TaskModelForList;
import com.timsu.astrid.sync.SynchronizationService;
import com.timsu.astrid.sync.Synchronizer;
import com.timsu.astrid.sync.Synchronizer.SynchronizerListener;
import com.timsu.astrid.utilities.Constants;
import com.timsu.astrid.utilities.DialogUtilities;
import com.timsu.astrid.utilities.Notifications;
import com.timsu.astrid.utilities.Preferences;
import com.timsu.astrid.widget.NumberPicker;
import com.timsu.astrid.widget.NumberPickerDialog;
import com.timsu.astrid.widget.NNumberPickerDialog.OnNNumberPickedListener;
import com.timsu.astrid.widget.NumberPickerDialog.OnNumberPickedListener;
/**
* Primary view for the Astrid Application. Lists all of the tasks in the
* system, and allows users to interact with them.
*
* @author timsu
*
*/
public class TaskListSubActivity extends SubActivity {
// bundle tokens
public static final String TAG_TOKEN = "tag";
public static final String FROM_NOTIFICATION_TOKEN = "notify";
public static final String NOTIF_FLAGS_TOKEN = "notif_flags";
public static final String NOTIF_REPEAT_TOKEN = "notif_repeat";
public static final String LOAD_INSTANCE_TOKEN = "id";
// activities
private static final int ACTIVITY_CREATE = 0;
private static final int ACTIVITY_EDIT = 1;
private static final int ACTIVITY_TAGS = 2;
private static final int ACTIVITY_SYNCHRONIZE = 3;
// menu codes
private static final int INSERT_ID = Menu.FIRST;
private static final int FILTERS_ID = Menu.FIRST + 1;
private static final int TAGS_ID = Menu.FIRST + 2;
private static final int SYNC_ID = Menu.FIRST + 3;
private static final int MORE_ID = Menu.FIRST + 4;
private static final int OPTIONS_SYNC_ID = Menu.FIRST + 10;
private static final int OPTIONS_SETTINGS_ID = Menu.FIRST + 11;
private static final int OPTIONS_HELP_ID = Menu.FIRST + 12;
private static final int OPTIONS_CLEANUP_ID = Menu.FIRST + 13;
private static final int OPTIONS_QUICK_TIPS = Menu.FIRST + 14;
private static final int CONTEXT_FILTER_HIDDEN = Menu.FIRST + 20;
private static final int CONTEXT_FILTER_DONE = Menu.FIRST + 21;
private static final int CONTEXT_FILTER_TAG = Menu.FIRST + 22;
private static final int CONTEXT_SORT_AUTO = Menu.FIRST + 23;
private static final int CONTEXT_SORT_ALPHA = Menu.FIRST + 24;
private static final int CONTEXT_SORT_DUEDATE = Menu.FIRST + 25;
private static final int CONTEXT_SORT_REVERSE = Menu.FIRST + 26;
private static final int CONTEXT_SORT_GROUP = Menu.FIRST;
// other constants
private static final int SORTFLAG_FILTERDONE = (1 << 5);
private static final int SORTFLAG_FILTERHIDDEN = (1 << 6);
private static final int HIDE_ADD_BTN_PORTRAIT = 4;
private static final int HIDE_ADD_BTN_LANDSCPE = 2;
private static final float POSTPONE_STAT_PCT = 0.4f;
private static final int AUTO_REFRESH_MAX_LIST_SIZE = 50;
// UI components
private ListView listView;
private Button addButton;
private View layout;
private TextView loadingText;
// indicator flag set if task list should be refreshed (something changed
// in another activity)
public static boolean shouldRefreshTaskList = false;
// indicator flag set if synchronization window has been opened & closed
static boolean syncPreferencesOpened = false;
// other instance variables
static class TaskListContext {
Map<TagIdentifier, TagModelForView> tagMap;
ArrayList<TaskModelForList> taskArray;
HashMap<Long, TaskModelForList> tasksById;
HashMap<TaskModelForList, String> taskTags;
TaskModelForList selectedTask = null;
Thread loadingThread = null;
TaskListAdapter listAdapter = null;
TagModelForView filterTag = null;
CharSequence windowTitle;
}
Handler handler = null;
Long selectedTaskId = null;
Runnable reLoadRunnable = null;
private TaskListContext context;
// display filters
private static boolean filterShowHidden = false;
private static boolean filterShowDone = false;
private static SortMode sortMode = SortMode.AUTO;
private static boolean sortReverse = false;
/* ======================================================================
* ======================================================= initialization
* ====================================================================== */
public TaskListSubActivity(TaskList parent, int code, View view) {
super(parent, code, view);
}
@Override
/** Called when loading up the activity */
public void onDisplay(final Bundle variables) {
// process task that's selected, if any
if(variables != null && variables.containsKey(LOAD_INSTANCE_TOKEN)) {
selectedTaskId = variables.getLong(LOAD_INSTANCE_TOKEN);
} else {
selectedTaskId = null;
}
setupUIComponents();
// declare the reload runnable, which is called when the task list
// wants to reload itself
reLoadRunnable = new Runnable() {
public void run() {
handler.post(new Runnable() {
public void run() {
loadingText.setText(getParent().getResources().getString(R.string.updating));
}
});
fillData();
}
};
// if we have a non-configuration instance (i.e. the screen was just
// rotated), use that instead of loading the whole task list again.
// this makes screen rotation an inexpensive operation
if (getLastNonConfigurationInstance() != null) {
context = (TaskListContext) getLastNonConfigurationInstance();
listView.setAdapter(context.listAdapter);
onTaskListLoaded();
return;
}
context = new TaskListContext();
if(selectedTaskId == null)
context.selectedTask = null;
// process tag to filter, if any (intercept UNTAGGED identifier, if applicable)
if(variables != null && variables.containsKey(TAG_TOKEN)) {
TagIdentifier identifier = new TagIdentifier(variables.getLong(TAG_TOKEN));
context.tagMap = getTagController().getAllTagsAsMap();
if(context.tagMap.containsKey(identifier))
context.filterTag = context.tagMap.get(identifier);
else if(identifier.equals(TagModelForView.UNTAGGED_IDENTIFIER))
context.filterTag = TagModelForView.getUntaggedModel();
else
Toast.makeText(getParent(), R.string.missing_tag, Toast.LENGTH_SHORT).show();
}
// time to go! creates a thread that loads the task list, then
// displays the reminder box if it is requested
context.loadingThread = new Thread(new Runnable() {
public void run() {
handler.post(new Runnable() {
public void run() {
loadingText.setVisibility(View.VISIBLE);
}
});
loadTaskListSort();
fillData();
// open up reminder box
if(variables != null && variables.containsKey(NOTIF_FLAGS_TOKEN) &&
context.selectedTask != null) {
handler.post(new Runnable() {
public void run() {
long repeatInterval = 0;
int flags = 0;
if(variables.containsKey(NOTIF_REPEAT_TOKEN))
repeatInterval = variables.getLong(NOTIF_REPEAT_TOKEN);
flags = variables.getInt(NOTIF_FLAGS_TOKEN);
showNotificationAlert(context.selectedTask,
repeatInterval, flags);
}
});
}
}
});
context.loadingThread.start();
}
/** Initialize UI components */
public void setupUIComponents() {
handler = new Handler();
listView = (ListView)findViewById(R.id.tasklist);
loadingText = (TextView)findViewById(R.id.loading);
addButton = (Button)findViewById(R.id.addtask);
addButton.setOnClickListener(new
View.OnClickListener() {
public void onClick(View v) {
createTask(null);
}
});
layout = getView();
layout.setOnCreateContextMenuListener(
new OnCreateContextMenuListener() {
public void onCreateContextMenu(ContextMenu menu, View v,
ContextMenuInfo menuInfo) {
if(menu.hasVisibleItems())
return;
onCreateMoreOptionsMenu(menu);
}
});
}
@Override
/** Create options menu (displayed when user presses menu key) */
public boolean onPrepareOptionsMenu(Menu menu) {
MenuItem item;
item = menu.add(Menu.NONE, INSERT_ID, Menu.NONE,
R.string.taskList_menu_insert);
item.setIcon(android.R.drawable.ic_menu_add);
item.setAlphabeticShortcut('n');
item = menu.add(Menu.NONE, FILTERS_ID, Menu.NONE,
R.string.taskList_menu_filters);
item.setIcon(android.R.drawable.ic_menu_view);
item.setAlphabeticShortcut('f');
item = menu.add(Menu.NONE, TAGS_ID, Menu.NONE,
R.string.taskList_menu_tags);
item.setIcon(android.R.drawable.ic_menu_myplaces);
item.setAlphabeticShortcut('t');
if(Preferences.shouldDisplaySyncButton(getParent())){
item = menu.add(Menu.NONE, SYNC_ID, Menu.NONE,
R.string.taskList_menu_syncshortcut);
item.setIcon(android.R.drawable.ic_menu_upload);
item.setAlphabeticShortcut('s');
}
item = menu.add(Menu.NONE, MORE_ID, Menu.NONE,
R.string.taskList_menu_more);
item.setIcon(android.R.drawable.ic_menu_more);
item.setAlphabeticShortcut('m');
return true;
}
/** Create 'more options' menu */
public boolean onCreateMoreOptionsMenu(Menu menu) {
MenuItem item;
item = menu.add(Menu.NONE, OPTIONS_SYNC_ID, Menu.NONE,
R.string.taskList_menu_sync);
item.setAlphabeticShortcut('s');
item = menu.add(Menu.NONE, OPTIONS_SETTINGS_ID, Menu.NONE,
R.string.taskList_menu_settings);
item.setAlphabeticShortcut('p');
item = menu.add(Menu.NONE, OPTIONS_CLEANUP_ID, Menu.NONE,
R.string.taskList_menu_cleanup);
item = menu.add(Menu.NONE, OPTIONS_QUICK_TIPS, Menu.NONE,
R.string.taskList_menu_tips);
item = menu.add(Menu.NONE, OPTIONS_HELP_ID, Menu.NONE,
R.string.taskList_menu_help);
item.setAlphabeticShortcut('h');
return true;
}
/**
* Enum that determines how the task list is sorted. Contains a comparison
* method that determines sorting order.
*
* @author timsu
*
*/
private enum SortMode {
ALPHA {
@Override
int compareTo(TaskModelForList arg0, TaskModelForList arg1) {
return arg0.getName().toLowerCase().compareTo(arg1.getName().toLowerCase());
}
},
DUEDATE {
long getDueDate(TaskModelForList task) {
Date definite = task.getDefiniteDueDate();
Date preferred = task.getPreferredDueDate();
if(definite != null && preferred != null) {
if(preferred.getTime() < System.currentTimeMillis())
return definite.getTime();
return preferred.getTime();
} else if(definite != null)
return definite.getTime();
else if(preferred != null)
return preferred.getTime();
else
return new Date(2020,1,1).getTime();
}
@Override
int compareTo(TaskModelForList arg0, TaskModelForList arg1) {
return (int)((getDueDate(arg0) - getDueDate(arg1))/1000);
}
},
AUTO {
@Override
int compareTo(TaskModelForList arg0, TaskModelForList arg1) {
return arg0.getTaskWeight() - arg1.getTaskWeight();
}
};
abstract int compareTo(TaskModelForList arg0, TaskModelForList arg1);
};
/* ======================================================================
* ======================================================== notifications
* ====================================================================== */
/** Called when user clicks on a notification to get here */
private void showNotificationAlert(final TaskModelForList task,
final long repeatInterval, final int flags) {
Resources r = getResources();
// clear notifications
Notifications.clearAllNotifications(getParent(), task.getTaskIdentifier());
String response;
if(Preferences.shouldShowNags(getParent())) {
String[] responses = r.getStringArray(R.array.reminder_responses);
response = responses[new Random().nextInt(responses.length)];
} else
response = r.getString(R.string.taskList_nonag_reminder);
new AlertDialog.Builder(getParent())
.setTitle(R.string.taskView_notifyTitle)
.setMessage(task.getName() + "\n\n" + response)
.setIcon(android.R.drawable.ic_dialog_alert)
// yes, i will do it: just closes this dialog
.setPositiveButton(R.string.notify_yes, null)
// no, i will ignore: quits application
.setNegativeButton(R.string.notify_no, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
TaskList.shouldCloseInstance = true;
closeActivity();
}
})
// snooze: sets a new temporary alert, closes application
.setNeutralButton(R.string.notify_snooze, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
snoozeAlert(task, repeatInterval, flags);
}
})
.show();
}
/** Helper method to "snooze" an alert (i.e. set a new one for some time
* from now.
*
* @param task
* @param repeatInterval
* @param flags
*/
private void snoozeAlert(final TaskModelForList task,
final long repeatInterval, final int flags) {
DialogUtilities.hourMinutePicker(getParent(),
getResources().getString(R.string.notify_snooze_title),
new OnNNumberPickedListener() {
public void onNumbersPicked(int[] values) {
int snoozeSeconds = values[0] * 3600 + values[1] * 60;
Notifications.createSnoozeAlarm(getParent(),
task.getTaskIdentifier(), snoozeSeconds, flags,
repeatInterval);
TaskList.shouldCloseInstance = true;
closeActivity();
}
});
}
/* ======================================================================
* ====================================================== populating list
* ====================================================================== */
/** Helper method returns true if the task is considered 'hidden' */
private boolean isTaskHidden(TaskModelForList task) {
if(task == context.selectedTask)
return false;
if(task.isHidden())
return true;
if(context.filterTag == null) {
if(context.taskTags.get(task).contains(TagModelForView.HIDDEN_FROM_MAIN_LIST_PREFIX))
return true;
}
return false;
}
/** Fill in the Task List with our tasks */
private synchronized void fillData() {
int hiddenTasks = 0; // # of tasks hidden
int completedTasks = 0; // # of tasks on list that are done
handler.post(new Runnable() {
public void run() {
loadingText.setVisibility(View.VISIBLE);
}
});
try {
// get a cursor to the task list
Cursor tasksCursor;
if(context.filterTag != null) { // Filter by TAG
LinkedList<TaskIdentifier> tasks;
// Check "named" Tag vs. "Untagged"
TagIdentifier tagId = context.filterTag.getTagIdentifier();
if (!tagId.equals(TagModelForView.UNTAGGED_IDENTIFIER)) {
tasks = getTagController().getTaggedTasks(tagId);
} else {
HashSet<TaskIdentifier> activeTasks =
getTaskController().getActiveTaskIdentifiers();
tasks = getTagController().getUntaggedTasks(activeTasks);
}
tasksCursor = getTaskController().getTaskListCursorById(tasks);
} else {
if(filterShowDone)
tasksCursor = getTaskController().getAllTaskListCursor();
else
tasksCursor = getTaskController().getActiveTaskListCursor();
}
startManagingCursor(tasksCursor);
context.taskArray = getTaskController().createTaskListFromCursor(tasksCursor);
// read tags and apply filters
context.tagMap = getTagController().getAllTagsAsMap();
context.taskTags = new HashMap<TaskModelForList, String>();
StringBuilder tagBuilder = new StringBuilder();
context.tasksById = new HashMap<Long, TaskModelForList>();
for(Iterator<TaskModelForList> i = context.taskArray.iterator(); i.hasNext();) {
if(Thread.interrupted())
return;
TaskModelForList task = i.next();
if(!filterShowDone) {
if(task.isTaskCompleted()) {
i.remove();
continue;
}
}
if(selectedTaskId != null && task.getTaskIdentifier().getId() == selectedTaskId) {
context.selectedTask = task;
}
// get list of tags
LinkedList<TagIdentifier> tagIds = getTagController().getTaskTags(
task.getTaskIdentifier());
tagBuilder.delete(0, tagBuilder.length());
for(Iterator<TagIdentifier> j = tagIds.iterator(); j.hasNext(); ) {
TagModelForView tag = context.tagMap.get(j.next());
tagBuilder.append(tag.getName());
if(j.hasNext())
tagBuilder.append(", ");
}
context.taskTags.put(task, tagBuilder.toString());
// hide hidden
if(!filterShowHidden) {
if(isTaskHidden(task)) {
hiddenTasks++;
i.remove();
continue;
}
}
context.tasksById.put(task.getTaskIdentifier().getId(), task);
if(task.isTaskCompleted())
completedTasks++;
}
} catch (StaleDataException e) {
// happens when you rotate the screen while the thread is
// still running. i don't think it's avoidable?
Log.w("astrid", "StaleDataException", e);
return;
} catch (final IllegalStateException e) {
// happens when you run out of memory usually
Log.e("astrid", "Error loading task list", e);
handler.post(new Runnable() {
public void run() {
if(!e.getMessage().contains("Couldn't init cursor window"))
return;
DialogUtilities.okDialog(getParent(), "Ran out of memory! " +
"Try restarting Astrid...", null);
}
});
return;
} catch (final Exception e) {
Log.e("astrid", "Error loading task list", e);
return;
}
int activeTasks = context.taskArray.size() - completedTasks;
// sort task list
Collections.sort(context.taskArray, new Comparator<TaskModelForList>() {
public int compare(TaskModelForList arg0, TaskModelForList arg1) {
return sortMode.compareTo(arg0, arg1);
}
});
if(sortReverse)
Collections.reverse(context.taskArray);
final int finalCompleted = completedTasks;
final int finalActive = activeTasks;
final int finalHidden = hiddenTasks;
handler.post(new Runnable() {
public void run() {
Resources r = getResources();
StringBuilder title = new StringBuilder().
append(r.getString(R.string.taskList_titlePrefix)).append(" ");
if(context.filterTag != null) {
title.append(r.getString(R.string.taskList_titleTagPrefix,
context.filterTag.getName())).append(" ");
}
if(finalCompleted > 0)
title.append(r.getQuantityString(R.plurals.NactiveTasks,
finalActive, finalActive, context.taskArray.size()));
else
title.append(r.getQuantityString(R.plurals.Ntasks,
context.taskArray.size(), context.taskArray.size()));
if(finalHidden > 0)
title.append(" (+").append(finalHidden).append(" ").
append(r.getString(R.string.taskList_hiddenSuffix)).append(")");
context.windowTitle = title;
}
});
onTaskListLoaded();
}
/** Sets up the interface after everything has been loaded */
private void onTaskListLoaded() {
handler.post(new Runnable() {
public void run() {
// hide "add" button if we have too many tasks
int threshold = HIDE_ADD_BTN_PORTRAIT;
if(getParent().getResources().getConfiguration().orientation ==
Configuration.ORIENTATION_LANDSCAPE)
threshold = HIDE_ADD_BTN_LANDSCPE;
if(context.taskArray.size() > threshold)
addButton.setVisibility(View.GONE);
else
addButton.setVisibility(View.VISIBLE);
}
});
// set up the title
handler.post(new Runnable() {
public void run() {
setTitle(context.windowTitle);
setUpListUI();
loadingText.setVisibility(View.GONE);
}
});
}
class TaskListHooks implements TaskListAdapterHooks {
private HashMap<TaskModelForList, String> myTaskTags;
private ArrayList<TaskModelForList> myTaskArray;
public TaskListHooks() {
this.myTaskTags = context.taskTags;
this.myTaskArray = context.taskArray;
}
public TagController tagController() {
return getTagController();
}
public String getTagsFor(TaskModelForList task) {
return myTaskTags.get(task);
}
public ArrayList<TaskModelForList> getTaskArray() {
return myTaskArray;
}
public TaskController taskController() {
return getTaskController();
}
public void performItemClick(View v, int position) {
listView.performItemClick(v, position, 0);
}
public void onCreatedTaskListView(View v, TaskModelForList task) {
v.setOnTouchListener(getGestureListener());
}
public void editItem(TaskModelForList task) {
editTask(task);
}
public void toggleTimerOnItem(TaskModelForList task) {
toggleTimer(task);
}
public void setSelectedItem(TaskIdentifier taskId) {
if(taskId == null) {
selectedTaskId = null;
context.selectedTask = null;
} else
selectedTaskId = taskId.getId();
}
}
/** Set up the adapter for our task list */
private void setUpListUI() {
// set up our adapter
context.listAdapter = new TaskListAdapter(getParent(),
R.layout.task_list_row, context.taskArray, new TaskListHooks());
listView.setAdapter(context.listAdapter);
listView.setItemsCanFocus(true);
if(context.selectedTask != null) {
try {
int selectedPosition = context.listAdapter.getPosition(context.selectedTask);
View v = listView.getChildAt(selectedPosition);
context.listAdapter.setExpanded(v, context.selectedTask, true);
listView.setSelection(selectedPosition);
} catch (Exception e) {
Log.e("astrid", "error with selected task", e);
}
}
// filters context menu
listView.setOnCreateContextMenuListener(new OnCreateContextMenuListener() {
public void onCreateContextMenu(ContextMenu menu, View v,
ContextMenuInfo menuInfo) {
if(menu.hasVisibleItems())
return;
Resources r = getResources();
menu.setHeaderTitle(R.string.taskList_filter_title);
MenuItem item = menu.add(Menu.NONE, CONTEXT_FILTER_HIDDEN,
Menu.NONE, R.string.taskList_filter_hidden);
item.setCheckable(true);
item.setChecked(filterShowHidden);
item = menu.add(Menu.NONE, CONTEXT_FILTER_DONE, Menu.NONE,
R.string.taskList_filter_done);
item.setCheckable(true);
item.setChecked(filterShowDone);
if(context.filterTag != null) {
item = menu.add(Menu.NONE, CONTEXT_FILTER_TAG, Menu.NONE,
r.getString(R.string.taskList_filter_tagged,
context.filterTag.getName()));
item.setCheckable(true);
item.setChecked(true);
}
item = menu.add(CONTEXT_SORT_GROUP, CONTEXT_SORT_AUTO, Menu.NONE,
R.string.taskList_sort_auto);
item.setChecked(sortMode == SortMode.AUTO);
item = menu.add(CONTEXT_SORT_GROUP, CONTEXT_SORT_ALPHA, Menu.NONE,
R.string.taskList_sort_alpha);
item.setChecked(sortMode == SortMode.ALPHA);
item = menu.add(CONTEXT_SORT_GROUP, CONTEXT_SORT_DUEDATE, Menu.NONE,
R.string.taskList_sort_duedate);
item.setChecked(sortMode == SortMode.DUEDATE);
menu.setGroupCheckable(CONTEXT_SORT_GROUP, true, true);
item = menu.add(CONTEXT_SORT_GROUP, CONTEXT_SORT_REVERSE, Menu.NONE,
R.string.taskList_sort_reverse);
item.setCheckable(true);
item.setChecked(sortReverse);
}
});
listView.setOnTouchListener(getGestureListener());
}
private void reloadList() {
if(context.loadingThread != null && context.loadingThread.isAlive()) {
context.loadingThread.interrupt();
context.loadingThread.stop();
}
context.loadingThread = new Thread(reLoadRunnable);
context.loadingThread.start();
}
/* ======================================================================
* ======================================================= event handlers
* ====================================================================== */
@Override
protected Object onRetainNonConfigurationInstance() {
return context;
}
@Override
protected boolean onKeyDown(int keyCode, KeyEvent event) {
if(keyCode == KeyEvent.KEYCODE_BACK) {
if(context.filterTag != null) {
showTagsView();
return true;
} else {
// close the app
getParent().finish();
}
}
if(keyCode >= KeyEvent.KEYCODE_A && keyCode <= KeyEvent.KEYCODE_Z) {
createTask((char)('A' + (keyCode - KeyEvent.KEYCODE_A)));
return true;
}
return false;
}
@Override
protected void onSaveInstanceState(Bundle outState) {
if(context.loadingThread != null && context.loadingThread.isAlive())
context.loadingThread.stop();
}
@Override
void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (hasFocus) {
if (shouldRefreshTaskList)
reloadList();
else if(syncPreferencesOpened) {
syncPreferencesOpened = false;
// stop & start synchronization service
SynchronizationService.stop();
SynchronizationService.start();
} else if (context.taskArray != null &&
context.taskArray.size() > 0 &&
context.taskArray.size() < AUTO_REFRESH_MAX_LIST_SIZE) {
// invalidate caches
for (TaskModelForList task : context.taskArray)
task.clearCache();
listView.invalidateViews();
}
}
shouldRefreshTaskList = false;
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if(resultCode == Constants.RESULT_SYNCHRONIZE) {
Synchronizer sync = new Synchronizer(false);
sync.setTagController(getTagController());
sync.setTaskController(getTaskController());
sync.synchronize(getParent(), new SynchronizerListener() {
public void onSynchronizerFinished(int numServicesSynced) {
if(numServicesSynced == 0) {
DialogUtilities.okDialog(getParent(), getResources().getString(
R.string.sync_no_synchronizers), null);
return;
}
reloadList();
}
});
} else if(requestCode == ACTIVITY_TAGS) {
switchToActivity(TaskList.AC_TAG_LIST, null);
}
}
/** Call an activity to create the given task */
private void createTask(Character startCharacter) {
Intent intent = new Intent(getParent(), TaskEdit.class);
if(context.filterTag != null)
intent.putExtra(TaskEdit.TAG_NAME_TOKEN, context.filterTag.getName());
if(startCharacter != null)
intent.putExtra(TaskEdit.START_CHAR_TOKEN, startCharacter);
launchActivity(intent, ACTIVITY_CREATE);
}
/** Show a dialog box and delete the task specified */
private void deleteTask(final TaskModelForList task) {
new AlertDialog.Builder(getParent())
.setTitle(R.string.delete_title)
.setMessage(R.string.delete_this_task_title)
.setIcon(android.R.drawable.ic_dialog_alert)
.setPositiveButton(android.R.string.ok,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
context.listAdapter.remove(task);
context.taskArray.remove(task);
getTaskController().deleteTask(task.getTaskIdentifier());
}
})
.setNegativeButton(android.R.string.cancel, null)
.show();
}
/** Take you to the task edit page */
private void editTask(TaskModelForList task) {
Intent intent = new Intent(getParent(), TaskEdit.class);
intent.putExtra(TaskEdit.LOAD_INSTANCE_TOKEN,
task.getTaskIdentifier().getId());
launchActivity(intent, ACTIVITY_EDIT);
}
/** Toggle the timer */
private void toggleTimer(TaskModelForList task) {
if(task.getTimerStart() == null)
task.setTimerStart(new Date());
else {
task.stopTimerAndUpdateElapsedTime();
}
getTaskController().saveTask(task);
context.listAdapter.refreshItem(listView, context.taskArray.indexOf(task));
}
/** Show the tags view */
public void showTagsView() {
switchToActivity(TaskList.AC_TAG_LIST, null);
}
@Override
public void launchActivity(Intent intent, int requestCode) {
super.launchActivity(intent, requestCode);
}
/** Save the sorting mode to the preferences */
private void saveTaskListSort() {
int sortId = sortMode.ordinal() + 1;
if(filterShowDone)
sortId |= SORTFLAG_FILTERDONE;
if(filterShowHidden)
sortId |= SORTFLAG_FILTERHIDDEN;
if(sortReverse)
sortId *= -1;
Preferences.setTaskListSort(getParent(), sortId);
}
/** Save the sorting mode to the preferences */
private void loadTaskListSort() {
int sortId = Preferences.getTaskListSort(getParent());
if(sortId == 0)
return;
sortReverse = sortId < 0;
sortId = Math.abs(sortId);
filterShowDone = (sortId & SORTFLAG_FILTERDONE) > 0;
filterShowHidden = (sortId & SORTFLAG_FILTERHIDDEN) > 0;
sortId = sortId & ~(SORTFLAG_FILTERDONE | SORTFLAG_FILTERHIDDEN);
sortMode = SortMode.values()[sortId - 1];
}
/** Compute date after postponing tasks */
private Date computePostponeDate(Date input, long postponeMillis,
boolean shiftFromTodayIfPast) {
if(input != null) {
if(shiftFromTodayIfPast && input.getTime() < System.currentTimeMillis())
input = new Date();
input = new Date(input.getTime() +
postponeMillis);
}
return input;
}
/** Show a dialog box and delete old tasks as requested */
private void cleanOldTasks() {
final Resources r = getResources();
new NumberPickerDialog(getParent(), new OnNumberPickedListener() {
public void onNumberPicked(NumberPicker view, int number) {
Date date = new Date(System.currentTimeMillis() - 24L*3600*1000*number);
int deleted = getTaskController().deleteCompletedTasksOlderThan(date);
DialogUtilities.okDialog(getParent(), r.getQuantityString(R.plurals.Ntasks,
deleted, deleted) + " " + r.getString(R.string.taskList_deleted),
null);
if(TaskListSubActivity.filterShowDone)
reloadList();
}
}, r.getString(R.string.taskList_cleanup_dialog),
30, 5, 0, 999).show();
}
/** Show a dialog box to postpone your tasks */
private void postponeTask(final TaskModelForList task) {
final Resources r = getResources();
DialogUtilities.dayHourPicker(getParent(), r.getString(R.string.taskList_postpone_dialog),
new OnNNumberPickedListener() {
public void onNumbersPicked(int[] values) {
long postponeMillis = (values[0] * 24 + values[1]) * 3600L * 1000;
if(postponeMillis <= 0)
return;
task.setPreferredDueDate(computePostponeDate(task
.getPreferredDueDate(), postponeMillis,
true));
task.setDefiniteDueDate(computePostponeDate(
task.getDefiniteDueDate(), postponeMillis, true));
task.setHiddenUntil(computePostponeDate(task.
getHiddenUntil(), postponeMillis, false));
// show nag
int postponeCount = getTaskController().fetchTaskPostponeCount(
task.getTaskIdentifier()) + 1;
if(Preferences.shouldShowNags(getParent())) {
Random random = new Random();
final String nagText;
if(postponeCount > 1 && random.nextFloat() < POSTPONE_STAT_PCT) {
nagText = r.getString(R.string.taskList_postpone_count,
postponeCount);
} else {
String[] nags = r.getStringArray(R.array.postpone_nags);
nagText = nags[random.nextInt(nags.length)];
}
handler.post(new Runnable() {
public void run() {
Toast.makeText(getParent(), nagText, Toast.LENGTH_LONG).show();
}
});
}
task.setPostponeCount(postponeCount);
getTaskController().saveTask(task);
getTaskController().updateAlarmForTask(
task.getTaskIdentifier());
context.listAdapter.refreshItem(listView,
context.taskArray.indexOf(task));
}
});
}
@Override
public boolean onMenuItemSelected(int featureId, final MenuItem item) {
final TaskModelForList task;
switch(item.getItemId()) {
// --- options menu items
case INSERT_ID:
createTask(null);
return true;
case FILTERS_ID:
listView.showContextMenu();
return true;
case TAGS_ID:
showTagsView();
return true;
case SYNC_ID:
onActivityResult(ACTIVITY_SYNCHRONIZE, Constants.RESULT_SYNCHRONIZE, null);
return true;
case MORE_ID:
layout.showContextMenu();
return true;
// --- more options menu items
case OPTIONS_SYNC_ID:
syncPreferencesOpened = true;
launchActivity(new Intent(getParent(), SyncPreferences.class),
ACTIVITY_SYNCHRONIZE);
return true;
case OPTIONS_SETTINGS_ID:
launchActivity(new Intent(getParent(), EditPreferences.class), 0);
return true;
case OPTIONS_HELP_ID:
Intent browserIntent = new Intent(Intent.ACTION_VIEW,
Uri.parse(Constants.HELP_URL));
launchActivity(browserIntent, 0);
return true;
case OPTIONS_CLEANUP_ID:
cleanOldTasks();
return true;
// --- list context menu items
case TaskListAdapter.CONTEXT_EDIT_ID:
task = context.tasksById.get((long)item.getGroupId());
editTask(task);
return true;
case TaskListAdapter.CONTEXT_DELETE_ID:
task = context.tasksById.get((long)item.getGroupId());
deleteTask(task);
return true;
case TaskListAdapter.CONTEXT_TIMER_ID:
task = context.tasksById.get((long)item.getGroupId());
toggleTimer(task);
return true;
case TaskListAdapter.CONTEXT_POSTPONE_ID:
task = context.tasksById.get((long) item.getGroupId());
postponeTask(task);
return true;
// --- display context menu items
case CONTEXT_FILTER_HIDDEN:
TaskListSubActivity.filterShowHidden = !filterShowHidden;
saveTaskListSort();
reloadList();
return true;
case CONTEXT_FILTER_DONE:
TaskListSubActivity.filterShowDone = !filterShowDone;
saveTaskListSort();
reloadList();
return true;
case CONTEXT_FILTER_TAG:
switchToActivity(TaskList.AC_TASK_LIST, null);
return true;
case CONTEXT_SORT_AUTO:
if(sortMode == SortMode.AUTO)
return true;
TaskListSubActivity.sortReverse = false;
TaskListSubActivity.sortMode = SortMode.AUTO;
saveTaskListSort();
reloadList();
return true;
case CONTEXT_SORT_ALPHA:
if(sortMode == SortMode.ALPHA)
return true;
TaskListSubActivity.sortReverse = false;
TaskListSubActivity.sortMode = SortMode.ALPHA;
saveTaskListSort();
reloadList();
return true;
case CONTEXT_SORT_DUEDATE:
if(sortMode == SortMode.DUEDATE)
return true;
TaskListSubActivity.sortReverse = false;
TaskListSubActivity.sortMode = SortMode.DUEDATE;
saveTaskListSort();
reloadList();
return true;
case CONTEXT_SORT_REVERSE:
TaskListSubActivity.sortReverse = !sortReverse;
saveTaskListSort();
reloadList();
return true;
}
return false;
}
/* ======================================================================
* ===================================================== getters / setters
* ====================================================================== */
public TagModelForView getFilterTag() {
return context.filterTag;
}
}