Merge branch '100616-replace-controllers'

Conflicts:
	astrid/default.properties
pull/14/head
Tim Su 16 years ago
commit 133033b193

@ -1,4 +1,4 @@
#Mon Jan 04 09:25:09 PST 2010
#Wed Jun 30 18:54:52 PDT 2010
eclipse.preferences.version=1
org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
@ -10,6 +10,8 @@ org.eclipse.jdt.core.compiler.debug.sourceFile=generate
org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning
org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
org.eclipse.jdt.core.compiler.problem.autoboxing=ignore
org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning
org.eclipse.jdt.core.compiler.problem.deadCode=warning
org.eclipse.jdt.core.compiler.problem.deprecation=warning
org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled
org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled
@ -29,8 +31,10 @@ org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore
org.eclipse.jdt.core.compiler.problem.localVariableHiding=warning
org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning
org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=ignore
org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=ignore
org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=ignore
org.eclipse.jdt.core.compiler.problem.missingSerialVersion=warning
org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=warning
org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning
org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning
org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=warning
@ -45,7 +49,7 @@ org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=ignore
org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled
org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning
org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled
org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=warning
org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore
org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning
org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=ignore
org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=warning

@ -1,4 +1,4 @@
#Thu Jul 02 10:50:56 PDT 2009
#Tue Jun 29 14:53:46 PDT 2010
cleanup.add_default_serial_version_id=true
cleanup.add_generated_serial_version_id=false
cleanup.add_missing_annotations=true
@ -52,6 +52,58 @@ cleanup.use_this_for_non_static_method_access_only_if_necessary=true
cleanup_profile=_Astrid
cleanup_settings_version=2
eclipse.preferences.version=1
editor_save_participant_org.eclipse.jdt.ui.postsavelistener.cleanup=true
formatter_profile=_Astrid
formatter_settings_version=11
org.eclipse.jdt.ui.text.custom_code_templates=<?xml version\="1.0" encoding\="UTF-8" standalone\="no"?><templates/>
sp_cleanup.add_default_serial_version_id=true
sp_cleanup.add_generated_serial_version_id=false
sp_cleanup.add_missing_annotations=true
sp_cleanup.add_missing_deprecated_annotations=true
sp_cleanup.add_missing_methods=false
sp_cleanup.add_missing_nls_tags=false
sp_cleanup.add_missing_override_annotations=true
sp_cleanup.add_serial_version_id=false
sp_cleanup.always_use_blocks=true
sp_cleanup.always_use_parentheses_in_expressions=false
sp_cleanup.always_use_this_for_non_static_field_access=false
sp_cleanup.always_use_this_for_non_static_method_access=false
sp_cleanup.convert_to_enhanced_for_loop=false
sp_cleanup.correct_indentation=false
sp_cleanup.format_source_code=false
sp_cleanup.format_source_code_changes_only=false
sp_cleanup.make_local_variable_final=false
sp_cleanup.make_parameters_final=false
sp_cleanup.make_private_fields_final=true
sp_cleanup.make_type_abstract_if_missing_method=false
sp_cleanup.make_variable_declarations_final=true
sp_cleanup.never_use_blocks=false
sp_cleanup.never_use_parentheses_in_expressions=true
sp_cleanup.on_save_use_additional_actions=true
sp_cleanup.organize_imports=true
sp_cleanup.qualify_static_field_accesses_with_declaring_class=false
sp_cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true
sp_cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true
sp_cleanup.qualify_static_member_accesses_with_declaring_class=false
sp_cleanup.qualify_static_method_accesses_with_declaring_class=false
sp_cleanup.remove_private_constructors=true
sp_cleanup.remove_trailing_whitespaces=true
sp_cleanup.remove_trailing_whitespaces_all=true
sp_cleanup.remove_trailing_whitespaces_ignore_empty=false
sp_cleanup.remove_unnecessary_casts=false
sp_cleanup.remove_unnecessary_nls_tags=true
sp_cleanup.remove_unused_imports=true
sp_cleanup.remove_unused_local_variables=false
sp_cleanup.remove_unused_private_fields=true
sp_cleanup.remove_unused_private_members=false
sp_cleanup.remove_unused_private_methods=true
sp_cleanup.remove_unused_private_types=true
sp_cleanup.sort_members=false
sp_cleanup.sort_members_all=false
sp_cleanup.use_blocks=false
sp_cleanup.use_blocks_only_for_return_and_throw=false
sp_cleanup.use_parentheses_in_expressions=false
sp_cleanup.use_this_for_non_static_field_access=false
sp_cleanup.use_this_for_non_static_field_access_only_if_necessary=true
sp_cleanup.use_this_for_non_static_method_access=false
sp_cleanup.use_this_for_non_static_method_access_only_if_necessary=true

@ -1,22 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.timsu.astrid"
android:versionName="2.14.4" android:versionCode="135">
android:versionName="3.0.0-beta" android:versionCode="136">
<!-- ============================ Metadata ============================ -->
<!-- ================================================== Used Permissions = -->
<!-- for notifications -->
<uses-permission android:name="android.permission.VIBRATE"/>
<!-- for synchronization -->
<uses-permission android:name="android.permission.INTERNET"/>
<!-- for google calendar integration -->
<uses-permission android:name="android.permission.WRITE_CALENDAR" />
<uses-permission android:name="android.permission.READ_CALENDAR" />
<!-- for creating shortcuts -->
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
<!-- for backup -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<!-- For Flurry analytics -->
<!-- for analytics -->
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<!-- For Tasks provider -->
<!-- ============================================== Exported Permissions = -->
<!-- for tasks provider -->
<permission android:name="com.timsu.astrid.permission.READ_TASKS"
android:permissionGroup="android.permission-group.MESSAGES"
android:protectionLevel="normal"
@ -24,63 +30,73 @@
android:description="@string/read_tasks_permission"/>
<uses-permission android:name="com.timsu.astrid.permission.READ_TASKS"/>
<!-- for reading data from plugins or astrid -->
<permission android:name="com.todoroo.astrid.READ"
android:description="@string/read_permission_desc"
android:protectionLevel="normal"
android:label="@string/read_permission_label" />
<uses-permission android:name="com.todoroo.astrid.READ" />
<!-- for writing data to plugins or astrid -->
<permission android:name="com.todoroo.astrid.WRITE"
android:description="@string/write_permission_desc"
android:protectionLevel="normal"
android:label="@string/write_permission_label" />
<uses-permission android:name="com.todoroo.astrid.WRITE" />
<!-- ========================================================== Metadata = -->
<uses-sdk android:minSdkVersion="3"
android:targetSdkVersion="4" />
<supports-screens />
<application android:icon="@drawable/icon" android:label="@string/app_name" android:debuggable="false">
<application android:icon="@drawable/icon" android:label="@string/app_name">
<!-- ======================== Activities ========================= -->
<!-- ====================================================== Activities = -->
<!-- Activity that displays the task list -->
<activity android:name=".activities.TaskList">
<!-- Activity that displays task list -->
<activity android:name="com.todoroo.astrid.activity.TaskListActivity"
android:theme="@style/Theme">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<!-- Activity that displays filter list -->
<activity android:name="com.todoroo.astrid.activity.FilterListActivity"
android:theme="@style/Theme">
<intent-filter>
<action android:name="android.intent.action.SEARCH" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<meta-data android:name="android.app.searchable"
android:resource="@xml/filter_list_searchable" />
</activity>
<!-- Activity that redirects to a task list, invoked by notifications -->
<activity android:name=".activities.TaskListNotify"
android:launchMode="singleTop" />
<!-- Activity that creates or edits tasks -->
<activity android:name=".activities.TaskEdit"
android:icon="@drawable/icon_add"
<activity android:name="com.todoroo.astrid.activity.TaskEditActivity"
android:label="@string/taskEdit_label">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="vnd.android.cursor.item/task" />
</intent-filter>
</activity>
<!-- Activity that views tags -->
<activity android:name=".activities.TagView">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<!-- Activity that lets users log in to sync providers -->
<activity android:name=".activities.SyncLoginActivity"/>
<!-- Activity that lets users edit app preferences -->
<activity android:name=".activities.EditPreferences"/>
<activity android:name="com.todoroo.astrid.activity.EditPreferences"/>
<!-- Activity that lets users edit synchronization preferences -->
<activity android:name=".activities.SyncPreferences"/>
<!-- activity that Locale displays to edit tag notification settings -->
<activity
android:name=".activities.LocaleEditAlerts"
<!-- Activity that Locale displays to edit tag notification settings -->
<activity android:name=".activities.LocaleEditAlerts"
android:label="@string/locale_edit_alerts_title"
android:icon="@drawable/icon_32"
android:exported="true" >
@ -89,9 +105,9 @@
</intent-filter>
</activity>
<!-- ======================== Receivers ========================= -->
<!-- ======================================================= Receivers = -->
<receiver android:name=".utilities.Notifications" />
<receiver android:name="com.todoroo.astrid.reminders.Notifications" />
<receiver android:name=".utilities.LocaleReceiver">
<intent-filter>
@ -99,13 +115,6 @@
</intent-filter>
</receiver>
<receiver android:name=".utilities.StartupReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</receiver>
<receiver android:name=".appwidget.AstridAppWidgetProvider" >
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
@ -114,24 +123,86 @@
android:resource="@xml/widget_provider_info" />
</receiver>
<!-- ======================== Services ========================== -->
<!-- ======================================================== Services = -->
<service android:name=".appwidget.AstridAppWidgetProvider$UpdateService"></service>
<service android:name=".sync.SynchronizationService" />
<service android:name=".utilities.BackupService"/>
<!-- ======================== Providers ========================== -->
<!-- ======================================================= Providers = -->
<provider
android:name=".provider.TasksProvider"
<provider android:name=".provider.TasksProvider"
android:authorities="com.timsu.astrid.tasksprovider"
android:multiprocess="true"
android:grantUriPermissions="true"
android:readPermission="com.timsu.astrid.permission.READ_TASKS"
/>
android:readPermission="com.timsu.astrid.permission.READ_TASKS"/>
<!-- ========================================================= Plugins = -->
<!-- core filters -->
<receiver android:name="com.todoroo.astrid.filters.CorePlugin">
<intent-filter>
<action android:name="com.todoroo.astrid.REQUEST_PLUGINS" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</receiver>
<receiver android:name="com.todoroo.astrid.filters.CoreFilterExposer">
<intent-filter>
<action android:name="com.todoroo.astrid.REQUEST_FILTERS" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</receiver>
<!-- extended filters -->
<receiver android:name="com.todoroo.astrid.filters.ExtendedPlugin">
<intent-filter>
<action android:name="com.todoroo.astrid.REQUEST_PLUGINS" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</receiver>
<receiver android:name="com.todoroo.astrid.filters.ExtendedFilterExposer">
<intent-filter>
<action android:name="com.todoroo.astrid.REQUEST_FILTERS" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</receiver>
<!-- tags -->
<receiver android:name="com.todoroo.astrid.tags.TagsPlugin">
<intent-filter>
<action android:name="com.todoroo.astrid.REQUEST_PLUGINS" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</receiver>
<receiver android:name="com.todoroo.astrid.tags.TagFilterExposer">
<intent-filter>
<action android:name="com.todoroo.astrid.REQUEST_FILTERS" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</receiver>
<receiver android:name="com.todoroo.astrid.tags.TagDetailExposer">
<intent-filter>
<action android:name="com.todoroo.astrid.REQUEST_DETAILS" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</receiver>
<!-- reminders -->
<activity android:name="com.todoroo.astrid.reminders.ReminderPreferences"
android:label="@string/rmd_EPr_alerts_header">
<intent-filter>
<action android:name="com.todoroo.astrid.SETTINGS" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<receiver android:name="com.todoroo.astrid.reminders.ReminderStartupService">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</receiver>
<activity android:name="com.todoroo.astrid.reminders.NotificationActivity"
android:taskAffinity="" />
</application>
</manifest>

@ -39,10 +39,22 @@ public class AstridApiConstants {
public static final String EXTRAS_ITEMS = "items";
/**
* Extras name for your plug-in name, used for logging errors to LogCat
* Extras name for plug-in object
*/
public static final String EXTRAS_PLUGIN = "plugin";
// --- Plug-ins API
/**
* Action name for broadcast intent requesting filters
*/
public static final String BROADCAST_REQUEST_PLUGINS = PACKAGE + ".REQUEST_PLUGINS";
/**
* Action name for broadcast intent sending filters back to Astrid
*/
public static final String BROADCAST_SEND_PLUGINS = PACKAGE + ".SEND_PLUGINS";
// --- Filters API
/**

@ -1,184 +0,0 @@
/**
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.api;
import android.net.Uri;
/**
* Constants for interfacing with Astrid's Content Providers.
*
* @author Tim Su <tim@todoroo.com>
*/
@SuppressWarnings("nls")
public class AstridContentProvider {
/**
* Content Provider
*/
public static final String PROVIDER = "com.todoroo.astrid.provider";
// --- methods for generating URI's for accessing Astrid data
/**
* URI for:
* <ul>
* <li>Queries on multiple tasks
* <li>Inserting new tasks
* <li>Deleting multiple tasks at a time
* <li>Updating multiple tasks at a time
* </ul>
* If your selection clause contains metadata columns, you need to use
* <code>allItemsWithMetadataUri</code> instead of this one.
*/
public static Uri allItemsUri() {
return Uri.parse("content://" + PROVIDER + "/items");
}
/**
* URI for:
* <ul>
* <li>Querying on tasks with metadata columns in selection
* <li>Deleting multiple tasks with metadata columns in selection
* <li>Updating multiple tasks with metadata columns in selection
* </ul>
* If, for example, you have defined metadata key 'tag' and wish to delete
* all tasks where 'tag' = 'deleteme', you would use this URI. For querying
* or insertion, use <code>allItemsUri</code>.
* <p>
* For queries, <code>allItemsUri</code> will be more efficient, but will
* not work if your selection clause contains columns not mentioned in your
* projection
*
* @param metadata
* array of metadata columns you wish to select using
*
*/
public static Uri allItemsWithMetadataUri(String[] metadata) {
if(metadata == null || metadata.length == 0)
throw new IllegalArgumentException("You must provide metadata");
StringBuilder builder = new StringBuilder();
for(int i = 0; i < metadata.length; i++)
builder.append(escapeUriSubComponent(metadata[i])).append(
SUB_COMPONENT_SEPARATOR);
return Uri.parse("content://" + PROVIDER + "/itemsWith/" +
escapeUriComponent(builder.toString()));
}
/**
* URI for:
* <ul>
* <li>Queries on a single task
* <li>Updating fields for a single task
* <li>Deleting a single task
* </ul>
*
* @param id
* id of task to fetch
*/
public static Uri singleItemUri(long id) {
return Uri.parse("content://" + PROVIDER + "/" + id);
}
/**
* URI for:
* <ul>
* <li>Queries on multiple tasks, grouped by column
* </ul>
* @param groupBy
* column name to group by
*/
public static Uri groupByUri(String groupBy) {
groupBy = escapeUriComponent(groupBy);
return Uri.parse("content://" + PROVIDER + "/groupby/" + groupBy);
}
// --- task built-in columns and constnats
/**
* A task in Astrid represents a single item in a user's task list
*
* @author Tim Su <tim@todoroo.com>
*/
public static class AstridTask {
// --- columns
/** long: Task id */
public static final String ID = "_id";
/** String: name of Task */
public static final String TITLE = "title";
/** int: Task Urgency setting (see <code>Task.URGENCY_*</code>) */
public static final String URGENCY = "urgency";
/** int: Task Importance setting (see <code>Task.IMPORTANCE_*</code>) */
public static final String IMPORTANCE = "importance";
/** int: unixtime Task is due, 0 if not set */
public static final String DUE_DATE = "dueDate";
/** int: unixtime Task should be hidden until, 0 if not set */
public static final String HIDDEN_UNTIL = "hiddenUntil";
/** int: unixtime Task was created */
public static final String CREATION_DATE = "creationDate";
/** int: unixtime Task was completed, 0 if task not completed */
public static final String COMPLETION_DATE = "completionDate";
/** int: unixtime Task was deleted, 0 if task not deleted */
public static final String DELETION_DATE = "deletionDate";
/** int: unixtime Task was modified */
public static final String MODIFICATION_DATE = "modificationDate";
// --- urgency settings
public static final int URGENCY_NONE = 0;
public static final int URGENCY_TODAY = 1;
public static final int URGENCY_THIS_WEEK = 2;
public static final int URGENCY_THIS_MONTH = 3;
public static final int URGENCY_WITHIN_THREE_MONTHS = 4;
public static final int URGENCY_WITHIN_SIX_MONTHS = 5;
public static final int URGENCY_WITHIN_A_YEAR = 6;
public static final int URGENCY_SPECIFIC_DAY = 7;
public static final int URGENCY_SPECIFIC_DAY_TIME = 8;
// --- importance settings
public static final int IMPORTANCE_DO_OR_DIE = 0;
public static final int IMPORTANCE_MUST_DO = 1;
public static final int IMPORTANCE_SHOULD_DO = 2;
public static final int IMPORTANCE_NONE = 3;
}
// --- internal methods
/**
* Escapes a string for use in a URI. Used internally to pass extra data
* to the content provider.
* @param component
* @return
*/
private static String escapeUriComponent(String component) {
return component.replace("%", "%o").replace("/", "%s");
}
private static final String SUB_COMPONENT_SEPARATOR = "|";
/**
* Escapes a string for use as part of a URI string. Used internally to pass extra data
* to the content provider.
* @param component
* @return
*/
private static String escapeUriSubComponent(String component) {
return component.replace("$", "$o").replace(SUB_COMPONENT_SEPARATOR, "$s");
}
}

@ -13,12 +13,12 @@ import android.os.Parcelable;
* @author Tim Su <tim@todoroo.com>
*
*/
public class EditOperation implements Parcelable {
public final class EditOperation implements Parcelable {
/**
* Plugin Id
*/
public String plugin = null;
public final String plugin;
/**
* Label
@ -33,13 +33,15 @@ public class EditOperation implements Parcelable {
/**
* Create an EditOperation object
*
* @param plugin
* {@link Plugin} identifier that encompasses object
* @param text
* label to display
* @param intent
* intent to invoke. {@link EXTRAS_TASK_ID} will be passed
*/
public EditOperation(String text, Intent intent) {
super();
public EditOperation(String plugin, String text, Intent intent) {
this.plugin = plugin;
this.text = text;
this.intent = intent;
}
@ -57,6 +59,7 @@ public class EditOperation implements Parcelable {
* {@inheritDoc}
*/
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(plugin);
dest.writeString(text);
dest.writeParcelable(intent, 0);
}
@ -69,8 +72,8 @@ public class EditOperation implements Parcelable {
* {@inheritDoc}
*/
public EditOperation createFromParcel(Parcel source) {
return new EditOperation(source.readString(), (Intent)source.readParcelable(
Intent.class.getClassLoader()));
return new EditOperation(source.readString(), source.readString(),
(Intent)source.readParcelable(Intent.class.getClassLoader()));
}
/**

@ -3,10 +3,11 @@
*/
package com.todoroo.astrid.api;
import android.content.ContentValues;
import android.os.Parcel;
import android.os.Parcelable;
import com.todoroo.astrid.api.AstridContentProvider.AstridTask;
import com.todoroo.andlib.sql.QueryTemplate;
/**
* A <code>FilterListFilter</code> allows users to display tasks that have
@ -19,7 +20,12 @@ import com.todoroo.astrid.api.AstridContentProvider.AstridTask;
* @author Tim Su <tim@todoroo.com>
*
*/
public class Filter extends FilterListItem {
public final class Filter extends FilterListItem {
/**
* Plug-in Identifier
*/
public final String plugin;
/**
* Expanded title of this filter. This is displayed at the top
@ -31,15 +37,14 @@ public class Filter extends FilterListItem {
/**
* SQL query for this filter. The query will be appended to the select
* statement after "<code>SELECT fields FROM table %s</code>". Use
* {@link AstridApiConstants.TASK_TABLE} and
* {@link AstridApiConstants.METADATA_TABLE} as table names,
* {@link AstridTask} for field names.
* statement after "<code>SELECT fields FROM table %s</code>". It is
* recommended that you use a {@link QueryTemplate} to construct your
* query.
* <p>
* Examples:
* <ul>
* <li><code>" WHERE completionDate = 0"</code>
* <li><code>" INNER JOIN " +
* <li><code>"WHERE completionDate = 0"</code>
* <li><code>"INNER JOIN " +
* Constants.TABLE_METADATA + " ON metadata.task = tasks.id WHERE
* metadata.namespace = " + NAMESPACE + " AND metadata.key = 'a' AND
* metadata.value = 'b' GROUP BY tasks.id ORDER BY tasks.title"</code>
@ -48,25 +53,18 @@ public class Filter extends FilterListItem {
public String sqlQuery;
/**
* SQL query to execute on a task when quick-creating a new task while viewing
* this filter. For example, when a user views tasks tagged 'ABC', the
* Values to apply to a task when quick-adding a task from this filter.
* For example, when a user views tasks tagged 'ABC', the
* tasks they create should also be tagged 'ABC'. If set to null, no
* query will be executed. In this string, $ID will be replaced with the
* task id.
* <p>
* Examples:
* <ul>
* <li><code>"INSERT INTO " + Constants.TABLE_METADATA + " (task,
* namespace, key, string) VALUES ($ID, " + ... + ")"</code>
* <li><code>"UPDATE " + Constants.TABLE_TASK + " SET urgency = 0
* WHERE _id = $ID"</code>
* </ul>
* additional values will be stored for a task.
*/
public String sqlForNewTasks = null;
public ContentValues valuesForNewTasks = null;
/**
* Utility constructor for creating a TaskList object
*
* @param plugin
* {@link Plugin} identifier that encompasses object
* @param listingTitle
* Title of this item as displayed on the lists page, e.g. Inbox
* @param title
@ -74,22 +72,26 @@ public class Filter extends FilterListItem {
* filter, e.g. Inbox (20 tasks)
* @param sqlQuery
* SQL query for this list (see {@link sqlQuery} for examples).
* @param sqlForNewTasks
* @param valuesForNewTasks
* see {@link sqlForNewTasks}
*/
public Filter(String listingTitle,
String title, String sqlQuery, String sqlForNewTasks) {
public Filter(String plugin, String listingTitle,
String title, QueryTemplate sqlQuery, ContentValues valuesForNewTasks) {
this.plugin = plugin;
this.listingTitle = listingTitle;
this.title = title;
this.sqlQuery = sqlQuery;
this.sqlForNewTasks = sqlForNewTasks;
this.sqlQuery = sqlQuery.toString();
this.valuesForNewTasks = valuesForNewTasks;
}
/**
* Blank constructor
* Utility constructor
*
* @param plugin
* {@link Plugin} identifier that encompasses object
*/
public Filter() {
//
protected Filter(String plugin) {
this.plugin = plugin;
}
// --- parcelable
@ -106,10 +108,11 @@ public class Filter extends FilterListItem {
*/
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(plugin);
super.writeToParcel(dest, flags);
dest.writeString(title);
dest.writeString(sqlQuery);
dest.writeString(sqlForNewTasks);
dest.writeParcelable(valuesForNewTasks, 0);
}
/**
@ -121,11 +124,11 @@ public class Filter extends FilterListItem {
* {@inheritDoc}
*/
public Filter createFromParcel(Parcel source) {
Filter item = new Filter();
Filter item = new Filter(source.readString());
item.readFromParcel(source);
item.title = source.readString();
item.sqlQuery = source.readString();
item.sqlForNewTasks = source.readString();
item.valuesForNewTasks = source.readParcelable(ContentValues.class.getClassLoader());
return item;
}

@ -15,6 +15,11 @@ import android.os.Parcelable;
*/
public class FilterCategory extends FilterListItem {
/**
* Plug-in Identifier
*/
public final String plugin;
/**
* {@link Filter}s contained by this category
*/
@ -23,22 +28,28 @@ public class FilterCategory extends FilterListItem {
/**
* Constructor for creating a new FilterCategory
*
* @param plugin
* {@link Plugin} identifier that encompasses object
* @param listingTitle
* Title of this item as displayed on the lists page, e.g. Inbox
* @param children
* filters belonging to this category
*/
public FilterCategory(String listingTitle,
public FilterCategory(String plugin, String listingTitle,
Filter[] children) {
this.plugin = plugin;
this.listingTitle = listingTitle;
this.children = children;
}
/**
* Blank constructor
* Constructor for creating a new FilterCategory
*
* @param plugin
* {@link Plugin} identifier that encompasses object
*/
public FilterCategory() {
//
protected FilterCategory(String plugin) {
this.plugin = plugin;
}
// --- parcelable
@ -55,6 +66,7 @@ public class FilterCategory extends FilterListItem {
*/
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(plugin);
super.writeToParcel(dest, flags);
dest.writeParcelableArray(children, 0);
}
@ -68,7 +80,7 @@ public class FilterCategory extends FilterListItem {
* {@inheritDoc}
*/
public FilterCategory createFromParcel(Parcel source) {
FilterCategory item = new FilterCategory();
FilterCategory item = new FilterCategory(source.readString());
item.readFromParcel(source);
Parcelable[] parcelableChildren = source.readParcelableArray(

@ -14,22 +14,33 @@ import android.os.Parcelable;
*/
public class FilterListHeader extends FilterListItem {
/**
* Plug-in Identifier
*/
public final String plugin;
/**
* Constructor for creating a new FilterListHeader
*
* @param plugin
* {@link Plugin} identifier that encompasses object
* @param listingTitle
* @param listingIconResource
* @param priority
*/
public FilterListHeader(String listingTitle) {
public FilterListHeader(String plugin, String listingTitle) {
this.plugin = plugin;
this.listingTitle = listingTitle;
}
/**
* Empty constructor
* Constructor for creating a new FilterListHeader
*
* @param plugin
* {@link Plugin} identifier that encompasses object
*/
public FilterListHeader() {
//
protected FilterListHeader(String plugin) {
this.plugin = plugin;
}
// --- parcelable
@ -38,14 +49,16 @@ public class FilterListHeader extends FilterListItem {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(plugin);
super.writeToParcel(dest, flags);
}
public static final Parcelable.Creator<FilterListHeader> CREATOR = new Parcelable.Creator<FilterListHeader>() {
public FilterListHeader createFromParcel(Parcel source) {
FilterListHeader item = new FilterListHeader();
FilterListHeader item = new FilterListHeader(source.readString());
item.readFromParcel(source);
return item;
}

@ -57,6 +57,7 @@ abstract public class FilterListItem implements Parcelable {
/**
* Utility method to read FilterListItem properties from a parcel.
*
* @param source
*/
public void readFromParcel(Parcel source) {

@ -0,0 +1,93 @@
/**
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.api;
import android.os.Parcel;
import android.os.Parcelable;
/**
* Represents a plug-in for Astrid. Users can enable or disable plug-ins,
* which affect all other extension points that share the same identifier.
*
* @author Tim Su <tim@todoroo.com>
*
*/
public class Plugin implements Parcelable {
/**
* Plug-in Identifier
*/
public String plugin = null;
/**
* Plug-in Title
*/
public String title = null;
/**
* Plug-in Author
*/
public String author = null;
/**
* Plug-in Description
*/
public String description = null;
/**
* Convenience constructor to generate a plug-in object
*
* @param plugin
* @param title
* @param author
* @param description
*/
public Plugin(String plugin, String title, String author, String description) {
this.plugin = plugin;
this.title = title;
this.author = author;
this.description = description;
}
// --- parcelable helpers
/**
* {@inheritDoc}
*/
public int describeContents() {
return 0;
}
/**
* {@inheritDoc}
*/
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(plugin);
dest.writeString(title);
dest.writeString(author);
dest.writeString(description);
}
/**
* Parcelable creator
*/
public static final Parcelable.Creator<Plugin> CREATOR = new Parcelable.Creator<Plugin>() {
/**
* {@inheritDoc}
*/
public Plugin createFromParcel(Parcel source) {
return new Plugin(source.readString(), source.readString(),
source.readString(), source.readString());
}
/**
* {@inheritDoc}
*/
public Plugin[] newArray(int size) {
return new Plugin[size];
};
};
}

@ -12,7 +12,12 @@ import android.os.Parcelable;
* @author Tim Su <tim@todoroo.com>
*
*/
public class TaskDetail implements Parcelable {
public final class TaskDetail implements Parcelable {
/**
* Plug-in Identifier
*/
public final String plugin;
/**
* Text of detail
@ -27,13 +32,15 @@ public class TaskDetail implements Parcelable {
/**
* Creates a TaskDetail object
*
* @param plugin
* {@link Plugin} identifier that encompasses object
* @param text
* text to display
* @param color
* color to use for text. Use <code>0</code> for default color
*/
public TaskDetail(String text, int color) {
super();
public TaskDetail(String plugin, String text, int color) {
this.plugin = plugin;
this.text = text;
this.color = color;
}
@ -44,8 +51,8 @@ public class TaskDetail implements Parcelable {
* @param text
* text to display
*/
public TaskDetail(String text) {
this(text, 0);
public TaskDetail(String plugin, String text) {
this(plugin, text, 0);
}
@ -63,6 +70,7 @@ public class TaskDetail implements Parcelable {
* {@inheritDoc}
*/
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(plugin);
dest.writeString(text);
dest.writeInt(color);
}
@ -75,7 +83,7 @@ public class TaskDetail implements Parcelable {
* {@inheritDoc}
*/
public TaskDetail createFromParcel(Parcel source) {
return new TaskDetail(source.readString(), source.readInt());
return new TaskDetail(source.readString(), source.readString(), source.readInt());
}
/**

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<launchConfiguration type="com.android.ide.eclipse.adt.debug.LaunchConfigType">
<stringAttribute key="bad_container_name" value="/astrid.launch"/>
<intAttribute key="com.android.ide.eclipse.adt.action" value="0"/>
<stringAttribute key="com.android.ide.eclipse.adt.commandline" value="-scale 0.7"/>
<intAttribute key="com.android.ide.eclipse.adt.delay" value="0"/>
<booleanAttribute key="com.android.ide.eclipse.adt.nobootanim" value="true"/>
<intAttribute key="com.android.ide.eclipse.adt.speed" value="0"/>
<booleanAttribute key="com.android.ide.eclipse.adt.target" value="true"/>
<booleanAttribute key="com.android.ide.eclipse.adt.wipedata" value="false"/>
<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
<listEntry value="/astrid"/>
<listEntry value="/astrid/AndroidManifest.xml"/>
</listAttribute>
<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES">
<listEntry value="4"/>
<listEntry value="1"/>
</listAttribute>
<listAttribute key="org.eclipse.debug.ui.favoriteGroups">
<listEntry value="org.eclipse.debug.ui.launchGroup.debug"/>
<listEntry value="org.eclipse.debug.ui.launchGroup.run"/>
</listAttribute>
<booleanAttribute key="org.eclipse.jdt.launching.ALLOW_TERMINATE" value="true"/>
<stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="astrid"/>
</launchConfiguration>

@ -148,11 +148,12 @@ abstract public class AbstractDatabase {
}
/**
* @return sql database. throws error if database was not opened
* @return sql database. opens database if not yet open
*/
public final SQLiteDatabase getDatabase() {
// open database if requested
if(database == null)
throw new IllegalStateException("Database was not opened!");
openForWriting();
return database;
}

@ -99,6 +99,18 @@ public abstract class AbstractModel implements Parcelable {
setValues = null;
}
/**
* Transfers all set values into values. This occurs when a task is
* saved - future saves will not need to write all the data as before.
*/
public void markSaved() {
if(values == null)
values = setValues;
else if(setValues != null)
values.putAll(setValues);
setValues = null;
}
/**
* Use merged values to compare two models to each other. Must be of
* exactly the same class.
@ -176,6 +188,13 @@ public abstract class AbstractModel implements Parcelable {
setValues.put(ID_PROPERTY_NAME, id);
}
/**
* @return true if this model has found Jesus (i.e. the database)
*/
public boolean isSaved() {
return getId() != NO_ID;
}
/**
* @param property
* @return true if setValues or values contains this property
@ -228,6 +247,16 @@ public abstract class AbstractModel implements Parcelable {
saver.save(property, setValues, value);
}
/**
* Merges content values with those coming from another source
*/
public synchronized <TYPE> void mergeWith(ContentValues other) {
if (setValues == null)
setValues = other;
else
setValues.putAll(other);
}
/**
* Clear the key for the given property
* @param property
@ -332,7 +361,7 @@ public abstract class AbstractModel implements Parcelable {
protected static final class ModelCreator<TYPE extends AbstractModel>
implements Parcelable.Creator<TYPE> {
private Class<TYPE> cls;
private final Class<TYPE> cls;
public ModelCreator(Class<TYPE> cls) {
super();

@ -11,8 +11,8 @@ import java.lang.reflect.InvocationTargetException;
import android.content.ContentValues;
import android.database.Cursor;
import com.todoroo.andlib.data.sql.Criterion;
import com.todoroo.andlib.data.sql.Query;
import com.todoroo.andlib.sql.Criterion;
import com.todoroo.andlib.sql.Query;
@ -24,7 +24,7 @@ import com.todoroo.andlib.data.sql.Query;
*/
public class GenericDao<TYPE extends AbstractModel> {
private Class<TYPE> modelClass;
private final Class<TYPE> modelClass;
private Table table;
@ -76,7 +76,7 @@ public class GenericDao<TYPE extends AbstractModel> {
* properties to read
* @param id
* id of item
* @return
* @return null if no item found
*/
public TYPE fetch(long id, Property<?>... properties) {
TodorooCursor<TYPE> cursor = fetchItem(id, properties);
@ -133,14 +133,14 @@ public class GenericDao<TYPE extends AbstractModel> {
*/
public boolean persist(AbstractModel item) {
if (item.getId() == AbstractModel.NO_ID) {
return createItem(item);
return createNew(item);
} else {
ContentValues values = item.getSetValues();
if (values.size() == 0) // nothing changed
return true;
return saveItem(item);
return saveExisting(item);
}
}
@ -154,7 +154,7 @@ public class GenericDao<TYPE extends AbstractModel> {
* item model
* @return returns true on success.
*/
public boolean createItem(AbstractModel item) {
public boolean createNew(AbstractModel item) {
long newRow = database.getDatabase().insert(table.name,
AbstractModel.ID_PROPERTY.name, item.getMergedValues());
item.setId(newRow);
@ -162,7 +162,7 @@ public class GenericDao<TYPE extends AbstractModel> {
}
/**
* Saves the given item.
* Saves the given item. Will not create a new item!
*
* @param database
* @param table
@ -171,7 +171,7 @@ public class GenericDao<TYPE extends AbstractModel> {
* item model
* @return returns true on success.
*/
public boolean saveItem(AbstractModel item) {
public boolean saveExisting(AbstractModel item) {
ContentValues values = item.getSetValues();
if(values.size() == 0) // nothing changed
return true;

@ -5,7 +5,7 @@
*/
package com.todoroo.andlib.data;
import com.todoroo.andlib.data.sql.Field;
import com.todoroo.andlib.sql.Field;
/**
* Property represents a typed column in a database.

@ -1,6 +1,7 @@
package com.todoroo.andlib.data;
import com.todoroo.andlib.data.sql.Field;
import com.todoroo.andlib.sql.Field;
import com.todoroo.andlib.sql.SqlTable;
/**
* Table class. Most fields are final, so methods such as <code>as</code> will
@ -9,7 +10,7 @@ import com.todoroo.andlib.data.sql.Field;
* @author Tim Su <tim@todoroo.com>
*
*/
public final class Table extends com.todoroo.andlib.data.sql.Table {
public final class Table extends SqlTable {
public final String name;
public final Class<? extends AbstractModel> modelClass;

@ -1,5 +0,0 @@
package com.todoroo.andlib.data.sql;
public enum OrderType {
DESC, ASC
}

@ -3,8 +3,6 @@ package com.todoroo.andlib.service;
import java.lang.reflect.Field;
import java.util.Arrays;
import android.util.Log;
/**
* Simple Dependency Injection Service for Android.
* <p>
@ -138,6 +136,5 @@ public class DependencyInjectionService {
*/
public synchronized void setInjectors(AbstractDependencyInjector[] injectors) {
this.injectors = injectors;
Log.e("INJECTION SETTING", "Set Injector List to: " + Arrays.asList(injectors)); // (debug)
}
}

@ -28,7 +28,7 @@ public interface NotificationManager {
*
*/
public static class AndroidNotificationManager implements NotificationManager {
final android.app.NotificationManager nm;
private final android.app.NotificationManager nm;
public AndroidNotificationManager(Context context) {
nm = (android.app.NotificationManager)
context.getSystemService(Context.NOTIFICATION_SERVICE);

@ -1,7 +1,7 @@
package com.todoroo.andlib.data.sql;
package com.todoroo.andlib.sql;
@SuppressWarnings("nls")
public class Constants {
public final class Constants {
static final String SELECT = "SELECT";
static final String SPACE = " ";
static final String AS = "AS";
@ -21,4 +21,5 @@ public class Constants {
static final String WHERE = "WHERE";
public static final String EXISTS = "EXISTS";
public static final String NOT = "NOT";
public static final String LIMIT = "LIMIT";
}

@ -1,12 +1,12 @@
package com.todoroo.andlib.data.sql;
package com.todoroo.andlib.sql;
import static com.todoroo.andlib.data.sql.Constants.AND;
import static com.todoroo.andlib.data.sql.Constants.EXISTS;
import static com.todoroo.andlib.data.sql.Constants.LEFT_PARENTHESIS;
import static com.todoroo.andlib.data.sql.Constants.NOT;
import static com.todoroo.andlib.data.sql.Constants.OR;
import static com.todoroo.andlib.data.sql.Constants.RIGHT_PARENTHESIS;
import static com.todoroo.andlib.data.sql.Constants.SPACE;
import static com.todoroo.andlib.sql.Constants.AND;
import static com.todoroo.andlib.sql.Constants.EXISTS;
import static com.todoroo.andlib.sql.Constants.LEFT_PARENTHESIS;
import static com.todoroo.andlib.sql.Constants.NOT;
import static com.todoroo.andlib.sql.Constants.OR;
import static com.todoroo.andlib.sql.Constants.RIGHT_PARENTHESIS;
import static com.todoroo.andlib.sql.Constants.SPACE;
public abstract class Criterion {
protected final Operator operator;
@ -15,6 +15,13 @@ public abstract class Criterion {
this.operator = operator;
}
public static Criterion all = new Criterion(Operator.exists) {
@Override
protected void populate(StringBuilder sb) {
sb.append(true);
}
};
public static Criterion and(final Criterion criterion, final Criterion... criterions) {
return new Criterion(Operator.and) {

@ -1,7 +1,7 @@
package com.todoroo.andlib.data.sql;
package com.todoroo.andlib.sql;
import static com.todoroo.andlib.data.sql.Constants.AS;
import static com.todoroo.andlib.data.sql.Constants.SPACE;
import static com.todoroo.andlib.sql.Constants.AS;
import static com.todoroo.andlib.sql.Constants.SPACE;
public abstract class DBObject<T extends DBObject<?>> implements Cloneable {
protected String alias;

@ -1,4 +1,4 @@
package com.todoroo.andlib.data.sql;
package com.todoroo.andlib.sql;
public class EqCriterion extends UnaryCriterion {
EqCriterion(Field field, Object value) {

@ -1,11 +1,11 @@
package com.todoroo.andlib.data.sql;
package com.todoroo.andlib.sql;
import static com.todoroo.andlib.data.sql.Constants.AND;
import static com.todoroo.andlib.data.sql.Constants.BETWEEN;
import static com.todoroo.andlib.data.sql.Constants.COMMA;
import static com.todoroo.andlib.data.sql.Constants.LEFT_PARENTHESIS;
import static com.todoroo.andlib.data.sql.Constants.RIGHT_PARENTHESIS;
import static com.todoroo.andlib.data.sql.Constants.SPACE;
import static com.todoroo.andlib.sql.Constants.AND;
import static com.todoroo.andlib.sql.Constants.BETWEEN;
import static com.todoroo.andlib.sql.Constants.COMMA;
import static com.todoroo.andlib.sql.Constants.LEFT_PARENTHESIS;
import static com.todoroo.andlib.sql.Constants.RIGHT_PARENTHESIS;
import static com.todoroo.andlib.sql.Constants.SPACE;
public class Field extends DBObject<Field> {
@ -18,10 +18,14 @@ public class Field extends DBObject<Field> {
}
public Criterion eq(Object value) {
if(value == null)
return UnaryCriterion.isNull(this);
return UnaryCriterion.eq(this, value);
}
public Criterion neq(Object value) {
if(value == null)
return UnaryCriterion.isNotNull(this);
return UnaryCriterion.neq(this, value);
}

@ -0,0 +1,15 @@
package com.todoroo.andlib.sql;
@SuppressWarnings("nls")
public final class Functions {
public static String caseStatement(Criterion when, Object ifTrue, Object ifFalse) {
return new StringBuilder("CASE WHEN ").
append(when.toString()).append(" THEN ").append(value(ifTrue)).
append(" ELSE ").append(value(ifFalse)).append(" END").toString();
}
private static String value(Object value) {
return value.toString();
}
}

@ -1,4 +1,4 @@
package com.todoroo.andlib.data.sql;
package com.todoroo.andlib.sql;
import java.util.List;
import java.util.ArrayList;

@ -1,33 +1,33 @@
package com.todoroo.andlib.data.sql;
package com.todoroo.andlib.sql;
import static com.todoroo.andlib.data.sql.Constants.JOIN;
import static com.todoroo.andlib.data.sql.Constants.ON;
import static com.todoroo.andlib.data.sql.Constants.SPACE;
import static com.todoroo.andlib.sql.Constants.JOIN;
import static com.todoroo.andlib.sql.Constants.ON;
import static com.todoroo.andlib.sql.Constants.SPACE;
public class Join {
private final Table joinTable;
private final SqlTable joinTable;
private final JoinType joinType;
private final Criterion[] criterions;
private Join(Table table, JoinType joinType, Criterion... criterions) {
private Join(SqlTable table, JoinType joinType, Criterion... criterions) {
joinTable = table;
this.joinType = joinType;
this.criterions = criterions;
}
public static Join inner(Table expression, Criterion... criterions) {
public static Join inner(SqlTable expression, Criterion... criterions) {
return new Join(expression, JoinType.INNER, criterions);
}
public static Join left(Table table, Criterion... criterions) {
public static Join left(SqlTable table, Criterion... criterions) {
return new Join(table, JoinType.LEFT, criterions);
}
public static Join right(Table table, Criterion... criterions) {
public static Join right(SqlTable table, Criterion... criterions) {
return new Join(table, JoinType.RIGHT, criterions);
}
public static Join out(Table table, Criterion... criterions) {
public static Join out(SqlTable table, Criterion... criterions) {
return new Join(table, JoinType.OUT, criterions);
}

@ -1,4 +1,4 @@
package com.todoroo.andlib.data.sql;
package com.todoroo.andlib.sql;
public enum JoinType {
INNER, LEFT, RIGHT, OUT

@ -1,6 +1,6 @@
package com.todoroo.andlib.data.sql;
package com.todoroo.andlib.sql;
import static com.todoroo.andlib.data.sql.Constants.SPACE;
import static com.todoroo.andlib.sql.Constants.SPACE;
import java.util.HashMap;
import java.util.Map;

@ -1,25 +1,25 @@
package com.todoroo.andlib.data.sql;
package com.todoroo.andlib.sql;
import static com.todoroo.andlib.data.sql.Constants.SPACE;
import static com.todoroo.andlib.sql.Constants.SPACE;
public class Order {
private final Field expression;
private final Object expression;
private final OrderType orderType;
private Order(Field expression) {
private Order(Object expression) {
this(expression, OrderType.ASC);
}
private Order(Field expression, OrderType orderType) {
private Order(Object expression, OrderType orderType) {
this.expression = expression;
this.orderType = orderType;
}
public static Order asc(Field expression) {
public static Order asc(Object expression) {
return new Order(expression);
}
public static Order desc(Field expression) {
public static Order desc(Object expression) {
return new Order(expression, OrderType.DESC);
}

@ -0,0 +1,5 @@
package com.todoroo.andlib.sql;
public enum OrderType {
DESC, ASC
}

@ -1,32 +1,32 @@
package com.todoroo.andlib.data.sql;
import static com.todoroo.andlib.data.sql.Constants.ALL;
import static com.todoroo.andlib.data.sql.Constants.COMMA;
import static com.todoroo.andlib.data.sql.Constants.FROM;
import static com.todoroo.andlib.data.sql.Constants.GROUP_BY;
import static com.todoroo.andlib.data.sql.Constants.LEFT_PARENTHESIS;
import static com.todoroo.andlib.data.sql.Constants.ORDER_BY;
import static com.todoroo.andlib.data.sql.Constants.RIGHT_PARENTHESIS;
import static com.todoroo.andlib.data.sql.Constants.SELECT;
import static com.todoroo.andlib.data.sql.Constants.SPACE;
import static com.todoroo.andlib.data.sql.Constants.WHERE;
import static com.todoroo.andlib.data.sql.Table.table;
package com.todoroo.andlib.sql;
import static com.todoroo.andlib.sql.Constants.ALL;
import static com.todoroo.andlib.sql.Constants.COMMA;
import static com.todoroo.andlib.sql.Constants.FROM;
import static com.todoroo.andlib.sql.Constants.GROUP_BY;
import static com.todoroo.andlib.sql.Constants.LEFT_PARENTHESIS;
import static com.todoroo.andlib.sql.Constants.ORDER_BY;
import static com.todoroo.andlib.sql.Constants.RIGHT_PARENTHESIS;
import static com.todoroo.andlib.sql.Constants.SELECT;
import static com.todoroo.andlib.sql.Constants.SPACE;
import static com.todoroo.andlib.sql.Constants.WHERE;
import static com.todoroo.andlib.sql.SqlTable.table;
import static java.util.Arrays.asList;
import java.util.ArrayList;
import java.util.List;
import com.todoroo.andlib.data.Property;
public class Query {
public final class Query {
private Table table;
private List<Criterion> criterions = new ArrayList<Criterion>();
private List<Field> fields = new ArrayList<Field>();
private List<Join> joins = new ArrayList<Join>();
private List<Field> groupBies = new ArrayList<Field>();
private List<Order> orders = new ArrayList<Order>();
private List<Criterion> havings = new ArrayList<Criterion>();
private SqlTable table;
private String queryTemplate = null;
private final ArrayList<Criterion> criterions = new ArrayList<Criterion>();
private final ArrayList<Field> fields = new ArrayList<Field>();
private final ArrayList<Join> joins = new ArrayList<Join>();
private final ArrayList<Field> groupBies = new ArrayList<Field>();
private final ArrayList<Order> orders = new ArrayList<Order>();
private final ArrayList<Criterion> havings = new ArrayList<Criterion>();
private Query(Field... fields) {
this.fields.addAll(asList(fields));
@ -36,7 +36,7 @@ public class Query {
return new Query(fields);
}
public Query from(Table fromTable) {
public Query from(SqlTable fromTable) {
this.table = fromTable;
return this;
}
@ -81,10 +81,19 @@ public class Query {
StringBuilder sql = new StringBuilder();
visitSelectClause(sql);
visitFromClause(sql);
if(queryTemplate == null) {
visitJoinClause(sql);
visitWhereClause(sql);
visitGroupByClause(sql);
visitOrderByClause(sql);
} else {
if(joins.size() > 0 || groupBies.size() > 0 || orders.size() > 0 ||
havings.size() > 0)
throw new IllegalStateException("Can't have extras AND query template"); //$NON-NLS-1$
sql.append(queryTemplate);
}
return sql.toString();
}
@ -154,7 +163,7 @@ public class Query {
sql.deleteCharAt(sql.length() - 1).append(SPACE);
}
public Table as(String alias) {
public SqlTable as(String alias) {
return table(LEFT_PARENTHESIS + this.toString() + RIGHT_PARENTHESIS).as(alias);
}
@ -170,4 +179,14 @@ public class Query {
public Property<?>[] getFields() {
return fields.toArray(new Property<?>[fields.size()]);
}
/**
* Add the SQL query template (comes after the "from")
* @param sqlQuery
* @return
*/
public Query withQueryTemplate(String template) {
queryTemplate = template;
return this;
}
}

@ -0,0 +1,117 @@
package com.todoroo.andlib.sql;
import static com.todoroo.andlib.sql.Constants.COMMA;
import static com.todoroo.andlib.sql.Constants.GROUP_BY;
import static com.todoroo.andlib.sql.Constants.LIMIT;
import static com.todoroo.andlib.sql.Constants.ORDER_BY;
import static com.todoroo.andlib.sql.Constants.SPACE;
import static com.todoroo.andlib.sql.Constants.WHERE;
import static java.util.Arrays.asList;
import java.util.ArrayList;
/**
* Query Template returns a bunch of criteria that allows a query to be
* constructed
*
* @author Tim Su <tim@todoroo.com>
*
*/
public final class QueryTemplate {
private final ArrayList<Criterion> criterions = new ArrayList<Criterion>();
private final ArrayList<Join> joins = new ArrayList<Join>();
private final ArrayList<Field> groupBies = new ArrayList<Field>();
private final ArrayList<Order> orders = new ArrayList<Order>();
private final ArrayList<Criterion> havings = new ArrayList<Criterion>();
private Integer limit = null;
public QueryTemplate join(Join... join) {
joins.addAll(asList(join));
return this;
}
public QueryTemplate where(Criterion criterion) {
criterions.add(criterion);
return this;
}
public QueryTemplate groupBy(Field... groupBy) {
groupBies.addAll(asList(groupBy));
return this;
}
public QueryTemplate orderBy(Order... order) {
orders.addAll(asList(order));
return this;
}
@Override
public String toString() {
StringBuilder sql = new StringBuilder();
visitJoinClause(sql);
visitWhereClause(sql);
visitGroupByClause(sql);
visitOrderByClause(sql);
if(limit != null)
sql.append(LIMIT).append(SPACE).append(limit);
return sql.toString();
}
private void visitOrderByClause(StringBuilder sql) {
if (orders.isEmpty()) {
return;
}
sql.append(ORDER_BY);
for (Order order : orders) {
sql.append(SPACE).append(order).append(COMMA);
}
sql.deleteCharAt(sql.length() - 1).append(SPACE);
}
@SuppressWarnings("nls")
private void visitGroupByClause(StringBuilder sql) {
if (groupBies.isEmpty()) {
return;
}
sql.append(GROUP_BY);
for (Field groupBy : groupBies) {
sql.append(SPACE).append(groupBy).append(COMMA);
}
sql.deleteCharAt(sql.length() - 1).append(SPACE);
if (havings.isEmpty()) {
return;
}
sql.append("HAVING");
for (Criterion havingCriterion : havings) {
sql.append(SPACE).append(havingCriterion).append(COMMA);
}
sql.deleteCharAt(sql.length() - 1).append(SPACE);
}
private void visitWhereClause(StringBuilder sql) {
if (criterions.isEmpty()) {
return;
}
sql.append(WHERE);
for (Criterion criterion : criterions) {
sql.append(SPACE).append(criterion).append(SPACE);
}
}
private void visitJoinClause(StringBuilder sql) {
for (Join join : joins) {
sql.append(join).append(SPACE);
}
}
public QueryTemplate having(Criterion criterion) {
this.havings.add(criterion);
return this;
}
public QueryTemplate limit(int limitValue) {
this.limit = limitValue;
return this;
}
}

@ -1,13 +1,13 @@
package com.todoroo.andlib.data.sql;
package com.todoroo.andlib.sql;
public class Table extends DBObject<Table> {
public class SqlTable extends DBObject<SqlTable> {
protected Table(String expression) {
protected SqlTable(String expression) {
super(expression);
}
public static Table table(String table) {
return new Table(table);
public static SqlTable table(String table) {
return new SqlTable(table);
}
@SuppressWarnings("nls")

@ -1,6 +1,6 @@
package com.todoroo.andlib.data.sql;
package com.todoroo.andlib.sql;
import static com.todoroo.andlib.data.sql.Constants.SPACE;
import static com.todoroo.andlib.sql.Constants.SPACE;
public class UnaryCriterion extends Criterion {
protected final Field expression;

@ -7,14 +7,11 @@ import java.net.URL;
import java.net.URLConnection;
import android.app.Activity;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.NetworkInfo.State;
@ -110,4 +107,37 @@ public class AndroidUtilities {
}
}
/**
* Put an arbitrary object into a {@link ContentValues}
* @param target
* @param key
* @param value
*/
public static void putInto(ContentValues target, String key, Object value) {
if(value instanceof String)
target.put(key, (String) value);
else if(value instanceof Long)
target.put(key, (Long) value);
else if(value instanceof Integer)
target.put(key, (Integer) value);
else if(value instanceof Double)
target.put(key, (Double) value);
else
throw new UnsupportedOperationException("Could not handle type " + //$NON-NLS-1$
value.getClass());
}
/**
* Return index of value in array
* @param array array to search
* @param value value to look for
* @return
*/
public static <TYPE> int indexOf(TYPE[] array, TYPE value) {
for(int i = 0; i < array.length; i++)
if(array[i].equals(value))
return i;
return -1;
}
}

@ -5,11 +5,15 @@
*/
package com.todoroo.andlib.utility;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import android.content.Context;
import android.content.res.Resources;
import com.todoroo.andlib.service.Autowired;
import com.todoroo.andlib.service.ContextManager;
import com.todoroo.andlib.service.DependencyInjectionService;
@ -21,6 +25,9 @@ public class DateUtilities {
@Autowired
public Integer monthsResource;
@Autowired
public Integer weeksResource;
@Autowired
public Integer daysResource;
@ -50,81 +57,209 @@ public class DateUtilities {
}
/* ======================================================================
* ============================================================ unix time
* ============================================================ long time
* ====================================================================== */
/** Convert unixtime into date */
public static final Date unixtimeToDate(int seconds) {
if(seconds == 0)
public static final Date unixtimeToDate(long millis) {
if(millis == 0)
return null;
return new Date(seconds * 1000L);
return new Date(millis);
}
/** Convert date into unixtime */
public static final int dateToUnixtime(Date date) {
public static final long dateToUnixtime(Date date) {
if(date == null)
return 0;
return (int)(date.getTime() / 1000);
return date.getTime();
}
/** Returns unixtime for current time */
public static final int now() {
return (int) (System.currentTimeMillis() / 1000L);
public static final long now() {
return System.currentTimeMillis();
}
/** Represents a single hour */
public static long ONE_HOUR = 3600000L;
/** Represents a single day */
public static long ONE_DAY = 24 * ONE_HOUR;
/** Represents a single week */
public static long ONE_WEEK = 7 * ONE_DAY;
/* ======================================================================
* =========================================================== formatters
* ====================================================================== */
@SuppressWarnings("nls")
public static boolean is24HourFormat(Context context) {
String value = android.provider.Settings.System.getString(context.getContentResolver(),
android.provider.Settings.System.TIME_12_24);
boolean b24 = !(value == null || value.equals("12"));
return b24;
}
/**
* @return time format (hours and minutes)
*/
public static SimpleDateFormat getTimeFormat(Context context) {
String value = getTimeFormatString(context);
return new SimpleDateFormat(value);
}
/**
* @return string used for time formatting
*/
@SuppressWarnings("nls")
private static String getTimeFormatString(Context context) {
String value;
if (is24HourFormat(context)) {
value = "H:mm";
} else {
value = "h:mm a";
}
return value;
}
/**
* @return string used for date formatting
*/
@SuppressWarnings("nls")
private static String getDateFormatString(Context context) {
String value = android.provider.Settings.System.getString(context.getContentResolver(),
android.provider.Settings.System.DATE_FORMAT);
if (value == null) {
// united states, you are special
if (Locale.US.equals(Locale.getDefault())
|| Locale.CANADA.equals(Locale.getDefault()))
value = "MMM d yyyy";
else
value = "d MMM yyyy";
}
return value;
}
/**
* @return date format (month, day, year)
*/
public static SimpleDateFormat getDateFormat(Context context) {
return new SimpleDateFormat(getDateFormatString(context));
}
/**
* @return date format as getDateFormat with weekday
*/
@SuppressWarnings("nls")
public static SimpleDateFormat getDateFormatWithWeekday(Context context) {
return new SimpleDateFormat("EEE, " + getDateFormatString(context));
}
/**
* @return date with time at the end
*/
@SuppressWarnings("nls")
public static SimpleDateFormat getDateWithTimeFormat(Context context) {
return new SimpleDateFormat(getDateFormatString(context) + " " +
getTimeFormatString(context));
}
/**
* @return formatted date (will contain month, day, year)
*/
public static String getFormattedDate(Context context, Date date) {
return getDateFormat(context).format(date);
}
/* ======================================================================
* ============================================================= duration
* ====================================================================== */
/**
* Convenience method for dropping the preposition argument.
* @param duration in millis
* @param unitsToShow number of units to show (i.e. if 2, then 5 hours
* 3 minutes 2 seconds is truncated to 5 hours 3 minutes)
*/
public String getDurationString(Resources r, int timeInSeconds,
int unitsToShow) {
return getDurationString(r, timeInSeconds, unitsToShow, false);
public String getDurationString(long duration, int unitsToShow) {
return getDurationString(duration, unitsToShow, false);
}
/**
* Format a time into the format: 5 days, 3 hours, 2 minutes
*
* @param r Resources to get strings from
* @param timeInSeconds
* @param duration in millis
* @param unitsToShow number of units to show (i.e. if 2, then 5 hours
* 3 minutes 2 seconds is truncated to 5 hours 3 minutes)
* @param withPreposition whether there is a preceding preposition
* @return
*/
public String getDurationString(Resources r, int timeInSeconds,
int unitsToShow, boolean withPreposition) {
int years, months, days, hours, minutes, seconds;
public String getDurationString(long duration, int unitsToShow, boolean withPreposition) {
Resources r = ContextManager.getContext().getResources();
short unitsDisplayed = 0;
timeInSeconds = Math.abs(timeInSeconds);
duration = Math.abs(duration);
if(timeInSeconds == 0)
if(duration == 0)
return r.getQuantityString(secondsResource, 0, 0);
Date now = new Date(80, 0, 1);
Date then = unixtimeToDate((int)(now.getTime() / 1000L) + timeInSeconds);
Date now = new Date(80, 1, 1);
Date then = new Date(now.getTime() + duration);
int[] values = new int[] {
then.getYear() - now.getYear(),
then.getMonth() - now.getMonth(),
(then.getDate() - now.getDate())/7,
(then.getDate() - now.getDate()) - (then.getDate() - now.getDate())/7*7,
then.getHours() - now.getHours(),
then.getMinutes() - now.getMinutes(),
then.getSeconds() - now.getSeconds(),
};
int[] maxValues = new int[] {
Integer.MAX_VALUE,
12,
5,
7,
24,
60,
60
};
// perform rounding (this is definitely magic... trust the unit tests)
int cursor = 0;
while(values[cursor] == 0 && ++cursor < values.length)
;
int postCursor = cursor + unitsToShow;
for(int i = values.length - 1; i >= postCursor; i--) {
if(values[i] >= maxValues[i]/2) {
values[i-1]++;
}
}
for(int i = Math.min(values.length, postCursor) - 1; i >= 1; i--) {
if(values[i] == maxValues[i]) {
values[i-1]++;
for(int j = i; j < values.length; j++)
values[j] = 0;
}
}
years = then.getYear() - now.getYear();
months = then.getMonth() - now.getMonth();
days = then.getDate() - now.getDate();
hours = then.getHours() - now.getHours();
minutes = then.getMinutes() - now.getMinutes();
seconds = then.getSeconds() - now.getSeconds();
StringBuilder result = new StringBuilder();
unitsDisplayed = displayUnits(r, yearsResource, unitsToShow, years, months >= 6,
unitsDisplayed = displayUnits(r, yearsResource, unitsToShow, values[0],
unitsDisplayed, result);
unitsDisplayed = displayUnits(r, monthsResource, unitsToShow, values[1],
unitsDisplayed, result);
unitsDisplayed = displayUnits(r, monthsResource, unitsToShow, months, days >= 15,
unitsDisplayed = displayUnits(r, weeksResource, unitsToShow, values[2],
unitsDisplayed, result);
unitsDisplayed = displayUnits(r, daysResource, unitsToShow, days, hours >= 12,
unitsDisplayed = displayUnits(r, daysResource, unitsToShow, values[3],
unitsDisplayed, result);
unitsDisplayed = displayUnits(r, hoursResource, unitsToShow, hours, minutes >= 30,
unitsDisplayed = displayUnits(r, hoursResource, unitsToShow, values[4],
unitsDisplayed, result);
unitsDisplayed = displayUnits(r, minutesResource, unitsToShow, minutes, seconds >= 30,
unitsDisplayed = displayUnits(r, minutesResource, unitsToShow, values[5],
unitsDisplayed, result);
unitsDisplayed = displayUnits(r, secondsResource, unitsToShow, seconds, false,
unitsDisplayed = displayUnits(r, secondsResource, unitsToShow, values[6],
unitsDisplayed, result);
return result.toString().trim();
@ -132,11 +267,8 @@ public class DateUtilities {
/** Display units, rounding up if necessary. Returns units to show */
private short displayUnits(Resources r, int resource, int unitsToShow, int value,
boolean shouldRound, short unitsDisplayed, StringBuilder result) {
short unitsDisplayed, StringBuilder result) {
if(unitsDisplayed < unitsToShow && value > 0) {
// round up if needed
if(unitsDisplayed + 1 == unitsToShow && shouldRound)
value++;
result.append(r.getQuantityString(resource, value, value)).
append(' ');
unitsDisplayed++;

@ -15,6 +15,9 @@ public class DialogUtilities {
@Autowired
public Integer informationDialogTitleResource;
@Autowired
public Integer confirmDialogTitleResource;
public DialogUtilities() {
DependencyInjectionService.getInstance().inject(this);
}
@ -32,7 +35,7 @@ public class DialogUtilities {
activity.runOnUiThread(new Runnable() {
public void run() {
new AlertDialog.Builder(activity)
.setTitle(informationDialogTitleResource)
.setTitle(confirmDialogTitleResource)
.setMessage(text)
.setView(view)
.setIcon(android.R.drawable.ic_dialog_alert)

@ -10,5 +10,5 @@
# Indicates whether an apk should be generated for each density.
split.density=false
# Project target.
target=android-8
target=Google Inc.:Google APIs:8
apk-configurations=

@ -38,8 +38,8 @@ public class Alarm extends AbstractModel {
public static final LongProperty TASK = new LongProperty(
TABLE, "task");
/** Alarm Time (seconds since epoch) */
public static final IntegerProperty TIME = new IntegerProperty(
/** Alarm Time */
public static final LongProperty TIME = new LongProperty(
TABLE, "time");
/** Alarm Type (see constants) */

@ -16,7 +16,7 @@ import com.todoroo.andlib.data.Table;
*
*/
@SuppressWarnings("nls")
public class AlarmsDatabase extends AbstractDatabase {
public class AlarmDatabase extends AbstractDatabase {
// --- constants

@ -1,8 +1,11 @@
package com.todoroo.astrid.alarms;
import android.content.Context;
import java.util.ArrayList;
import com.todoroo.andlib.data.GenericDao;
import com.todoroo.andlib.data.TodorooCursor;
import com.todoroo.andlib.service.DependencyInjectionService;
import com.todoroo.andlib.sql.Query;
/**
* Provides operations for working with alerts
@ -13,13 +16,44 @@ import com.todoroo.andlib.service.DependencyInjectionService;
@SuppressWarnings("nls")
public class AlarmService {
AlarmDatabase database = new AlarmDatabase();
GenericDao<Alarm> dao = new GenericDao<Alarm>(Alarm.class, database);
/**
* Metadata key for # of alarms
*/
public static final String ALARM_COUNT = "alarms-count";
public AlarmService(@SuppressWarnings("unused") Context context) {
public AlarmService() {
DependencyInjectionService.getInstance().inject(this);
}
/**
* Return alarms for the given task
*
* @param taskId
*/
public TodorooCursor<Alarm> getAlarms(long taskId) {
database.openForReading();
Query query = Query.select(Alarm.PROPERTIES).where(Alarm.TASK.eq(taskId));
return dao.query(query);
}
/**
* Save the given array of tags into the database
* @param taskId
* @param tags
*/
public void synchronizeAlarms(long taskId, ArrayList<Alarm> alarms) {
database.openForWriting();
dao.deleteWhere(Alarm.TASK.eq(taskId));
for(Alarm alarm : alarms) {
alarm.setId(Alarm.NO_ID);
alarm.setValue(Alarm.TASK, taskId);
dao.saveExisting(alarm);
}
}
}

@ -0,0 +1,80 @@
/**
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.filters;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.drawable.BitmapDrawable;
import com.timsu.astrid.R;
import com.todoroo.andlib.sql.Criterion;
import com.todoroo.andlib.sql.Functions;
import com.todoroo.andlib.sql.Order;
import com.todoroo.andlib.sql.QueryTemplate;
import com.todoroo.andlib.utility.DateUtilities;
import com.todoroo.astrid.activity.FilterListActivity;
import com.todoroo.astrid.api.AstridApiConstants;
import com.todoroo.astrid.api.Filter;
import com.todoroo.astrid.api.FilterListItem;
import com.todoroo.astrid.api.SearchFilter;
import com.todoroo.astrid.dao.TaskDao.TaskCriteria;
import com.todoroo.astrid.model.Task;
/**
* Exposes Astrid's built in filters to the {@link FilterListActivity}
*
* @author Tim Su <tim@todoroo.com>
*
*/
public final class CoreFilterExposer extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Resources r = context.getResources();
// build filters
Filter inbox = buildInboxFilter(r);
Filter all = new Filter(CorePlugin.IDENTIFIER, r.getString(R.string.BFE_All),
r.getString(R.string.BFE_All),
new QueryTemplate().where(Criterion.not(TaskCriteria.isDeleted())).
orderBy(Order.desc(Task.MODIFICATION_DATE)),
null);
all.listingIcon = ((BitmapDrawable)r.getDrawable(R.drawable.filter_all)).getBitmap();
SearchFilter searchFilter = new SearchFilter(CorePlugin.IDENTIFIER,
"Search");
searchFilter.listingIcon = ((BitmapDrawable)r.getDrawable(R.drawable.filter_search)).getBitmap();
// transmit filter list
FilterListItem[] list = new FilterListItem[3];
list[0] = inbox;
list[1] = all;
list[2] = searchFilter;
Intent broadcastIntent = new Intent(AstridApiConstants.BROADCAST_SEND_FILTERS);
broadcastIntent.putExtra(AstridApiConstants.EXTRAS_ITEMS, list);
context.sendBroadcast(broadcastIntent, AstridApiConstants.PERMISSION_READ);
}
/**
* Build inbox filter
* @return
*/
@SuppressWarnings("nls")
public static Filter buildInboxFilter(Resources r) {
Filter inbox = new Filter(CorePlugin.IDENTIFIER, r.getString(R.string.BFE_Inbox),
r.getString(R.string.BFE_Inbox_title),
new QueryTemplate().where(Criterion.and(TaskCriteria.isActive(),
TaskCriteria.isVisible(DateUtilities.now()))).orderBy(
Order.asc(Functions.caseStatement(Task.DUE_DATE.eq(0),
String.format("(%d + 1000000 * %s)", DateUtilities.now(), Task.IMPORTANCE),
String.format("(%s + 1000000 * %s)", Task.DUE_DATE, Task.IMPORTANCE)))),
null);
inbox.listingIcon = ((BitmapDrawable)r.getDrawable(R.drawable.filter_inbox)).getBitmap();
return inbox;
}
}

@ -0,0 +1,24 @@
package com.todoroo.astrid.filters;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import com.todoroo.astrid.api.AstridApiConstants;
import com.todoroo.astrid.api.Plugin;
public class CorePlugin extends BroadcastReceiver {
static final String IDENTIFIER = "core";
@Override
public void onReceive(Context context, Intent intent) {
Plugin plugin = new Plugin(IDENTIFIER, "Core Filters", "Todoroo",
"Provides 'Inbox' and 'All Tasks' Filters");
Intent broadcastIntent = new Intent(AstridApiConstants.BROADCAST_SEND_PLUGINS);
broadcastIntent.putExtra(AstridApiConstants.EXTRAS_PLUGIN, plugin);
context.sendBroadcast(broadcastIntent, AstridApiConstants.PERMISSION_READ);
}
}

@ -0,0 +1,89 @@
/**
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.filters;
import android.content.BroadcastReceiver;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.drawable.BitmapDrawable;
import com.timsu.astrid.R;
import com.todoroo.andlib.sql.Criterion;
import com.todoroo.andlib.sql.Order;
import com.todoroo.andlib.sql.QueryTemplate;
import com.todoroo.andlib.utility.DateUtilities;
import com.todoroo.astrid.activity.FilterListActivity;
import com.todoroo.astrid.api.AstridApiConstants;
import com.todoroo.astrid.api.Filter;
import com.todoroo.astrid.api.FilterListHeader;
import com.todoroo.astrid.api.FilterListItem;
import com.todoroo.astrid.dao.TaskDao.TaskCriteria;
import com.todoroo.astrid.model.Task;
/**
* Exposes Astrid's built in filters to the {@link FilterListActivity}
*
* @author Tim Su <tim@todoroo.com>
*
*/
public final class ExtendedFilterExposer extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Resources r = context.getResources();
// build filters
FilterListHeader header = new FilterListHeader(ExtendedPlugin.IDENTIFIER,
"Extended");
Filter alphabetical = new Filter(ExtendedPlugin.IDENTIFIER,
"Alphabetical",
"Alphabetical",
new QueryTemplate().where(Criterion.and(TaskCriteria.isActive(),
TaskCriteria.isVisible(DateUtilities.now()))).
orderBy(Order.asc(Task.TITLE)),
null);
alphabetical.listingIcon = ((BitmapDrawable)r.getDrawable(R.drawable.filter_alpha)).getBitmap();
Filter recent = new Filter(ExtendedPlugin.IDENTIFIER,
"Recently Modified",
"Recently Modified",
new QueryTemplate().orderBy(Order.desc(Task.MODIFICATION_DATE)).limit(15),
null);
recent.listingIcon = ((BitmapDrawable)r.getDrawable(R.drawable.filter_recent)).getBitmap();
ContentValues hiddenValues = new ContentValues();
hiddenValues.put(Task.HIDE_UNTIL.name, DateUtilities.now() + DateUtilities.ONE_DAY);
Filter hidden = new Filter(ExtendedPlugin.IDENTIFIER,
"Hidden Tasks",
"Hidden Tasks",
new QueryTemplate().where(Criterion.and(TaskCriteria.isActive(),
Criterion.not(TaskCriteria.isVisible(DateUtilities.now())))).
orderBy(Order.asc(Task.HIDE_UNTIL)),
hiddenValues);
hidden.listingIcon = ((BitmapDrawable)r.getDrawable(R.drawable.filter_hidden)).getBitmap();
Filter deleted = new Filter(ExtendedPlugin.IDENTIFIER,
"Deleted Tasks",
"Deleted Tasks",
new QueryTemplate().where(TaskCriteria.isDeleted()).
orderBy(Order.desc(Task.DELETION_DATE)),
null);
deleted.listingIcon = ((BitmapDrawable)r.getDrawable(R.drawable.filter_deleted)).getBitmap();
// transmit filter list
FilterListItem[] list = new FilterListItem[5];
list[0] = header;
list[1] = alphabetical;
list[2] = recent;
list[3] = hidden;
list[4] = deleted;
Intent broadcastIntent = new Intent(AstridApiConstants.BROADCAST_SEND_FILTERS);
broadcastIntent.putExtra(AstridApiConstants.EXTRAS_ITEMS, list);
context.sendBroadcast(broadcastIntent, AstridApiConstants.PERMISSION_READ);
}
}

@ -0,0 +1,24 @@
package com.todoroo.astrid.filters;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import com.todoroo.astrid.api.AstridApiConstants;
import com.todoroo.astrid.api.Plugin;
public class ExtendedPlugin extends BroadcastReceiver {
static final String IDENTIFIER = "extended";
@Override
public void onReceive(Context context, Intent intent) {
Plugin plugin = new Plugin(IDENTIFIER, "Extended Filters", "Todoroo",
"Provides extended filters for viewing subsets of your tasks");
Intent broadcastIntent = new Intent(AstridApiConstants.BROADCAST_SEND_PLUGINS);
broadcastIntent.putExtra(AstridApiConstants.EXTRAS_PLUGIN, plugin);
context.sendBroadcast(broadcastIntent, AstridApiConstants.PERMISSION_READ);
}
}

@ -17,11 +17,18 @@
* 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;
package com.todoroo.astrid.reminders;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.widget.Toast;
import com.timsu.astrid.R;
import com.todoroo.andlib.sql.QueryTemplate;
import com.todoroo.astrid.activity.TaskListActivity;
import com.todoroo.astrid.api.Filter;
import com.todoroo.astrid.dao.TaskDao.TaskCriteria;
/**
* This activity is launched when a user opens up a notification from the
@ -30,7 +37,14 @@ import android.os.Bundle;
* @author timsu
*
*/
public class TaskListNotify extends Activity {
public class NotificationActivity extends Activity {
// --- constants
/** task id from notification */
public static final String TOKEN_ID = "id"; //$NON-NLS-1$
// --- implementation
@Override
public void onCreate(Bundle savedInstanceState) {
@ -47,10 +61,23 @@ public class TaskListNotify extends Activity {
}
private void launchTaskList(Intent intent) {
Intent taskListIntent = new Intent(this, TaskList.class);
taskListIntent.putExtra(TaskList.VARIABLES_TAG, intent.getExtras());
long id = intent.getLongExtra(TOKEN_ID, -1);
if(id == -1)
return;
Intent taskListIntent = new Intent(this, TaskListActivity.class);
Filter itemFilter = new Filter(ReminderPlugin.IDENTIFIER,
getString(R.string.rmd_NoA_filter),
getString(R.string.rmd_NoA_filter),
new QueryTemplate().where(TaskCriteria.byId(id)),
null);
taskListIntent.putExtra(TaskListActivity.TOKEN_FILTER, itemFilter);
startActivity(taskListIntent);
String reminder = Notifications.getRandomReminder(getResources().getStringArray(R.array.responses));
Toast.makeText(this, reminder, Toast.LENGTH_LONG).show();
finish();
}
}

@ -0,0 +1,249 @@
package com.todoroo.astrid.reminders;
import java.util.Date;
import android.app.Activity;
import android.app.Notification;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.Color;
import android.media.AudioManager;
import android.net.Uri;
import android.util.Log;
import com.timsu.astrid.R;
import com.timsu.astrid.data.task.TaskIdentifier;
import com.timsu.astrid.utilities.Constants;
import com.timsu.astrid.utilities.Preferences;
import com.todoroo.andlib.service.Autowired;
import com.todoroo.andlib.service.ContextManager;
import com.todoroo.andlib.service.DependencyInjectionService;
import com.todoroo.andlib.service.ExceptionService;
import com.todoroo.andlib.service.NotificationManager;
import com.todoroo.andlib.service.NotificationManager.AndroidNotificationManager;
import com.todoroo.andlib.utility.DateUtilities;
import com.todoroo.astrid.dao.TaskDao;
import com.todoroo.astrid.model.Task;
import com.todoroo.astrid.service.AstridDependencyInjector;
public class Notifications extends BroadcastReceiver {
// --- constants
/** task id extra */
static final String ID_KEY = "id"; //$NON-NLS-1$
/** notification type extra */
static final String TYPE_KEY = "type"; //$NON-NLS-1$
// --- instance variables
@Autowired
private TaskDao taskDao;
@Autowired
private ExceptionService exceptionService;
public static NotificationManager notificationManager = null;
// --- alarm handling
static {
AstridDependencyInjector.initialize();
}
public Notifications() {
DependencyInjectionService.getInstance().inject(this);
}
@Override
/** Alarm intent */
public void onReceive(Context context, Intent intent) {
ContextManager.setContext(context);
long id = intent.getLongExtra(ID_KEY, 0);
int type = intent.getIntExtra(TYPE_KEY, (byte) 0);
Resources r = context.getResources();
String reminder;
if(type == ReminderService.TYPE_DUE || type == ReminderService.TYPE_OVERDUE)
reminder = getRandomReminder(r.getStringArray(R.array.reminders_due));
else if(type == ReminderService.TYPE_SNOOZE)
reminder = getRandomReminder(r.getStringArray(R.array.reminders_snooze));
else
reminder = getRandomReminder(r.getStringArray(R.array.reminders));
if(!showNotification(id, type, reminder)) {
notificationManager.cancel((int)id);
}
}
// --- notification creation
/** Clear notifications associated with this application */
public static void clearAllNotifications(Context context, TaskIdentifier taskId) {
NotificationManager nm = (NotificationManager)
context.getSystemService(Activity.NOTIFICATION_SERVICE);
nm.cancel((int)taskId.getId());
}
/** @return a random reminder string */
static String getRandomReminder(String[] reminders) {
int next = ReminderService.random.nextInt(reminders.length);
String reminder = reminders[next];
return reminder;
}
/**
* Schedule a new notification about the given task. Returns false if there was
* some sort of error or the alarm should be disabled.
*/
public boolean showNotification(long id, int type, String reminder) {
Context context = ContextManager.getContext();
if(notificationManager == null)
notificationManager = new AndroidNotificationManager(context);
Task task;
try {
task = taskDao.fetch(id, Task.TITLE, Task.HIDE_UNTIL, Task.COMPLETION_DATE,
Task.DELETION_DATE, Task.REMINDER_FLAGS);
if(task == null)
throw new IllegalArgumentException("cound not find item with id"); //$NON-NLS-1$
} catch (Exception e) {
exceptionService.reportError("show-notif", e); //$NON-NLS-1$
return false;
}
// you're done - don't sound, do delete
if(task.isCompleted() || task.isDeleted())
return false;
// it's hidden - don't sound, don't delete
if(task.isHidden() && type == ReminderService.TYPE_RANDOM)
return true;
// read properties
String taskTitle = task.getValue(Task.TITLE);
boolean nonstopMode = task.getFlag(Task.REMINDER_FLAGS, Task.NOTIFY_NONSTOP);
// update last reminder time
task.setValue(Task.REMINDER_LAST, DateUtilities.now());
taskDao.save(task, false);
// quiet hours? unless alarm clock
boolean quietHours = false;
Integer quietHoursStart = Preferences.getQuietHourStart(context);
Integer quietHoursEnd = Preferences.getQuietHourEnd(context);
if(quietHoursStart != null && quietHoursEnd != null && !nonstopMode) {
int hour = new Date().getHours();
if(quietHoursStart < quietHoursEnd) {
if(hour >= quietHoursStart && hour < quietHoursEnd)
quietHours = true;
} else { // wrap across 24/hour boundary
if(hour >= quietHoursStart || hour < quietHoursEnd)
quietHours = true;
}
}
Resources r = context.getResources();
Intent notifyIntent = new Intent(context, NotificationActivity.class);
notifyIntent.putExtra(NotificationActivity.TOKEN_ID, id);
notifyIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
PendingIntent pendingIntent = PendingIntent.getActivity(context,
(int)id, notifyIntent, PendingIntent.FLAG_ONE_SHOT);
// set up properties (name and icon) for the notification
String appName = r.getString(R.string.app_name);
int icon;
switch(Preferences.getNotificationIconTheme(context)) {
case Preferences.ICON_SET_PINK:
icon = R.drawable.notif_pink_alarm;
break;
case Preferences.ICON_SET_BORING:
icon = R.drawable.notif_boring_alarm;
break;
default:
icon = R.drawable.notif_astrid;
}
// create notification object
Notification notification = new Notification(
icon, reminder, System.currentTimeMillis());
notification.setLatestEventInfo(context,
appName,
reminder + " " + taskTitle, //$NON-NLS-1$
pendingIntent);
notification.flags |= Notification.FLAG_AUTO_CANCEL;
if(Preferences.isPersistenceMode(context)) {
notification.flags |= Notification.FLAG_NO_CLEAR |
Notification.FLAG_SHOW_LIGHTS;
notification.ledOffMS = 5000;
notification.ledOnMS = 700;
notification.ledARGB = Color.YELLOW;
}
else
notification.defaults = Notification.DEFAULT_LIGHTS;
AudioManager audioManager = (AudioManager)context.getSystemService(
Context.AUDIO_SERVICE);
// if non-stop mode is activated, set up the flags for insistent
// notification, and increase the volume to full volume, so the user
// will actually pay attention to the alarm
if(nonstopMode && (type != ReminderService.TYPE_RANDOM)) {
notification.flags |= Notification.FLAG_INSISTENT;
notification.audioStreamType = AudioManager.STREAM_ALARM;
audioManager.setStreamVolume(AudioManager.STREAM_ALARM,
audioManager.getStreamMaxVolume(AudioManager.STREAM_ALARM), 0);
} else {
notification.audioStreamType = AudioManager.STREAM_NOTIFICATION;
}
// quiet hours = no sound
if(quietHours) {
notification.sound = null;
} else {
Uri notificationSound = Preferences.getNotificationRingtone(context);
if(audioManager.getStreamVolume(AudioManager.STREAM_RING) == 0) {
notification.sound = null;
} else if(notificationSound != null &&
!notificationSound.toString().equals("")) { //$NON-NLS-1$
notification.sound = notificationSound;
} else {
notification.defaults |= Notification.DEFAULT_SOUND;
}
}
// quiet hours + periodic = no vibrate
if(quietHours && (type == ReminderService.TYPE_RANDOM)) {
notification.vibrate = null;
} else {
if (Preferences.shouldVibrate(context)
&& audioManager.shouldVibrate(AudioManager.VIBRATE_TYPE_NOTIFICATION)) {
notification.vibrate = new long[] {0, 1000, 500, 1000, 500, 1000};
} else {
notification.vibrate = null;
}
}
if(Constants.DEBUG)
Log.w("Astrid", "Logging notification: " + reminder); //$NON-NLS-1$ //$NON-NLS-2$
notificationManager.notify((int)id, notification);
return true;
}
// --- notification manager
public static void setNotificationManager(
NotificationManager notificationManager) {
Notifications.notificationManager = notificationManager;
}
}

@ -0,0 +1,21 @@
package com.todoroo.astrid.reminders;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
public class ReminderPlugin extends BroadcastReceiver {
static final String IDENTIFIER = "reminders"; //$NON-NLS-1$
@Override
public void onReceive(Context context, Intent intent) {
/*Plugin plugin = new Plugin(IDENTIFIER, "Reminders", "Todoroo",
"Provides notification reminders for tasks");
Intent broadcastIntent = new Intent(AstridApiConstants.BROADCAST_SEND_PLUGINS);
broadcastIntent.putExtra(AstridApiConstants.EXTRAS_PLUGIN, plugin);
context.sendBroadcast(broadcastIntent, AstridApiConstants.PERMISSION_READ);*/
}
}

@ -0,0 +1,119 @@
/**
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.reminders;
import android.content.res.Resources;
import android.os.Bundle;
import android.preference.CheckBoxPreference;
import android.preference.EditTextPreference;
import android.preference.ListPreference;
import android.preference.Preference;
import android.preference.PreferenceActivity;
import android.preference.PreferenceGroup;
import android.preference.PreferenceScreen;
import android.preference.Preference.OnPreferenceChangeListener;
import com.timsu.astrid.R;
/**
* Displays the preference screen for users to edit their preferences
*
* @author Tim Su <tim@todoroo.com>
*
*/
public class ReminderPreferences extends PreferenceActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.preferences_reminders);
PreferenceScreen screen = getPreferenceScreen();
initializePreference(screen);
}
private void initializePreference(Preference preference) {
if(preference instanceof PreferenceGroup) {
PreferenceGroup group = (PreferenceGroup)preference;
for(int i = 0; i < group.getPreferenceCount(); i++) {
initializePreference(group.getPreference(i));
}
} else {
Object value = null;
if(preference instanceof ListPreference)
value = ((ListPreference)preference).getValue();
else if(preference instanceof CheckBoxPreference)
value = ((CheckBoxPreference)preference).isChecked();
else if(preference instanceof EditTextPreference)
value = ((EditTextPreference)preference).getText();
if(value != null)
updatePreferences(preference, value);
preference.setOnPreferenceChangeListener(new OnPreferenceChangeListener() {
public boolean onPreferenceChange(Preference myPreference, Object newValue) {
updatePreferences(myPreference, newValue);
return true;
}
});
}
}
@Override
protected void onStart() {
super.onStart();
}
@Override
protected void onStop() {
super.onStop();
}
protected int valueToIndex(String value, String[] array) {
for(int i = 0; i < array.length; i++)
if(array[i].equals(value))
return i;
return -1;
}
/**
*
* @param resource if null, updates all resources
*/
protected void updatePreferences(Preference preference, Object value) {
Resources r = getResources();
if(r.getString(R.string.p_rmd_quietStart).equals(preference.getKey())) {
int index = valueToIndex((String)value, r.getStringArray(R.array.EPr_quiet_hours_start_values));
if(index == -1)
preference.setSummary(r.getString(R.string.rmd_EPr_quiet_hours_desc_none));
else {
String duration = r.getStringArray(R.array.EPr_quiet_hours_start)[index];
preference.setSummary(r.getString(R.string.rmd_EPr_quiet_hours_start_desc, duration));
}
} else if(r.getString(R.string.p_rmd_quietEnd).equals(preference.getKey())) {
int index = valueToIndex((String)value, r.getStringArray(R.array.EPr_quiet_hours_end_values));
if(index == -1)
preference.setSummary(r.getString(R.string.rmd_EPr_quiet_hours_desc_none));
else {
String duration = r.getStringArray(R.array.EPr_quiet_hours_end)[index];
preference.setSummary(r.getString(R.string.rmd_EPr_quiet_hours_end_desc, duration));
}
} else if(r.getString(R.string.p_rmd_ringtone).equals(preference.getKey())) {
if(value == null)
preference.setSummary(r.getString(R.string.rmd_EPr_ringtone_desc_default));
else
preference.setSummary(r.getString(R.string.rmd_EPr_ringtone_desc_custom));
} else if(r.getString(R.string.p_rmd_persistent).equals(preference.getKey())) {
if((Boolean)value)
preference.setSummary(r.getString(R.string.rmd_EPr_persistent_desc_true));
else
preference.setSummary(r.getString(R.string.rmd_EPr_persistent_desc_false));
}
}
}

@ -0,0 +1,301 @@
package com.todoroo.astrid.reminders;
import java.util.Date;
import java.util.Random;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.content.res.Resources;
import android.util.Log;
import com.timsu.astrid.R;
import com.todoroo.andlib.data.Property;
import com.todoroo.andlib.data.TodorooCursor;
import com.todoroo.andlib.service.Autowired;
import com.todoroo.andlib.service.ContextManager;
import com.todoroo.andlib.service.DependencyInjectionService;
import com.todoroo.andlib.sql.Criterion;
import com.todoroo.andlib.sql.Query;
import com.todoroo.andlib.utility.DateUtilities;
import com.todoroo.astrid.dao.TaskDao;
import com.todoroo.astrid.dao.TaskDao.TaskCriteria;
import com.todoroo.astrid.model.Task;
import com.todoroo.astrid.utility.Constants;
import com.todoroo.astrid.utility.Preferences;
/**
* Data service for reminders
*
* @author Tim Su <tim@todoroo.com>
*
*/
public final class ReminderService {
// --- constants
private static final Property<?>[] PROPERTIES = new Property<?>[] {
Task.DUE_DATE,
Task.REMINDER_FLAGS,
Task.REMINDER_PERIOD,
Task.REMINDER_LAST
};
/** flag for due date reminder */
static final int TYPE_DUE = 0;
/** flag for overdue reminder */
static final int TYPE_OVERDUE = 1;
/** flag for random reminder */
static final int TYPE_RANDOM = 2;
/** flag for a snoozed reminder */
static final int TYPE_SNOOZE = 3;
static final Random random = new Random();
// --- instance variables
@Autowired
private TaskDao taskDao;
private AlarmScheduler scheduler = new ReminderAlarmScheduler();
public ReminderService() {
DependencyInjectionService.getInstance().inject(this);
setPreferenceDefaults();
}
// --- preference handling
private static boolean preferencesInitialized = false;
/** Set preference defaults, if unset. called at startup */
public void setPreferenceDefaults() {
if(preferencesInitialized)
return;
Context context = ContextManager.getContext();
SharedPreferences prefs = Preferences.getPrefs(context);
Editor editor = prefs.edit();
Resources r = context.getResources();
Preferences.setIfUnset(prefs, editor, r, R.string.p_rmd_quietStart, 22);
Preferences.setIfUnset(prefs, editor, r, R.string.p_rmd_quietEnd, 10);
Preferences.setIfUnset(prefs, editor, r, R.string.p_rmd_default_random_hours, 0);
Preferences.setIfUnset(prefs, editor, r, R.string.p_rmd_time, 12);
Preferences.setIfUnset(prefs, editor, r, R.string.p_rmd_nagging, true);
Preferences.setIfUnset(prefs, editor, r, R.string.p_rmd_persistent, true);
editor.commit();
preferencesInitialized = true;
}
// --- reminder scheduling logic
/**
* Schedules all alarms
*/
public void scheduleAllAlarms() {
TodorooCursor<Task> cursor = getTasksWithReminders(PROPERTIES);
try {
Task task = new Task();
for(cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
task.readFromCursor(cursor);
scheduleAlarm(task, false);
}
} finally {
cursor.close();
}
}
private static final long NO_ALARM = Long.MAX_VALUE;
/**
* Schedules alarms for a single task
* @param task
*/
public void scheduleAlarm(Task task) {
scheduleAlarm(task, true);
}
/**
* Schedules alarms for a single task
*
* @param shouldPerformPropertyCheck
* whether to check if task has requisite properties
*/
private void scheduleAlarm(Task task, boolean shouldPerformPropertyCheck) {
if(task == null || !task.isSaved())
return;
// read data if necessary
if(shouldPerformPropertyCheck) {
for(Property<?> property : PROPERTIES) {
if(!task.containsValue(property)) {
task = taskDao.fetch(task.getId(), PROPERTIES);
if(task == null)
return;
break;
}
}
}
// random reminders
long whenRandom = calculateNextRandomReminder(task);
// notifications at due date
long whenDueDate = calculateNextDueDateReminder(task);
// notifications after due date
long whenOverdue = calculateNextOverdueReminder(task);
if(whenRandom < whenDueDate && whenRandom < whenOverdue)
scheduler.createAlarm(task, whenRandom, TYPE_RANDOM);
else if(whenDueDate < whenOverdue)
scheduler.createAlarm(task, whenDueDate, TYPE_DUE);
else if(whenOverdue != NO_ALARM)
scheduler.createAlarm(task, whenOverdue, TYPE_OVERDUE);
}
/**
* Calculate the next alarm time for overdue reminders. If the due date
* has passed, we schedule a reminder some time in the next 4 - 24 hours.
*
* @param task
* @return
*/
private long calculateNextOverdueReminder(Task task) {
if(task.hasDueDate() && task.getFlag(Task.REMINDER_FLAGS, Task.NOTIFY_AFTER_DEADLINE)) {
long dueDate = task.getValue(Task.DUE_DATE);
if(dueDate > DateUtilities.now())
return NO_ALARM;
return DateUtilities.now() + (long)((4 + 20 * random.nextFloat()) * DateUtilities.ONE_HOUR);
}
return NO_ALARM;
}
/**
* Calculate the next alarm time for due date reminders. If the due date
* has not already passed, we return the due date, altering the time
* if the date was indicated to not have a due time
*
* @param task
* @return
*/
private long calculateNextDueDateReminder(Task task) {
if(task.hasDueDate() && task.getFlag(Task.REMINDER_FLAGS, Task.NOTIFY_AT_DEADLINE)) {
long dueDate = task.getValue(Task.DUE_DATE);
if(dueDate < DateUtilities.now())
return NO_ALARM;
else if(task.hasDueTime())
// return due date straight up
return dueDate;
else {
// return notification time on this day
Date date = new Date(dueDate);
date.setHours(Preferences.getIntegerFromString(R.string.p_rmd_time));
date.setMinutes(0);
return date.getTime();
}
}
return NO_ALARM;
}
/**
* Calculate the next alarm time for random reminders. We take the last
* random reminder time and add approximately the reminder period, until
* we get a time that's in the future.
*
* @param task
* @return
*/
private long calculateNextRandomReminder(Task task) {
long reminderPeriod = task.getValue(Task.REMINDER_PERIOD);
if((reminderPeriod) > 0) {
long when = task.getValue(Task.REMINDER_LAST);
// get or make up a last notification time
if(when == 0) {
when = DateUtilities.now();
task.setValue(Task.REMINDER_LAST, when);
taskDao.save(task, false);
}
when += (long)(reminderPeriod * (0.85f + 0.3f * random.nextFloat()));
return when;
}
return NO_ALARM;
}
// --- alarm manager alarm creation
/**
* Interface for testing
*/
public interface AlarmScheduler {
public void createAlarm(Task task, long time, int type);
}
public void setScheduler(AlarmScheduler scheduler) {
this.scheduler = scheduler;
}
public AlarmScheduler getScheduler() {
return scheduler;
}
private class ReminderAlarmScheduler implements AlarmScheduler {
/**
* Create an alarm for the given task at the given type
*
* @param task
* @param time
* @param type
* @param flags
*/
@SuppressWarnings("nls")
public void createAlarm(Task task, long time, int type) {
if(time == 0 || time == NO_ALARM)
return;
if(time < DateUtilities.now()) {
time = DateUtilities.now() + (long)((0.5f +
4 * random.nextFloat()) * DateUtilities.ONE_HOUR);
}
Context context = ContextManager.getContext();
Intent intent = new Intent(context, Notifications.class);
intent.setType(Long.toString(task.getId()));
intent.setAction(Integer.toString(type));
intent.putExtra(Notifications.ID_KEY, task.getId());
intent.putExtra(Notifications.TYPE_KEY, type);
AlarmManager am = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0,
intent, 0);
if(Constants.DEBUG)
Log.e("Astrid", "Alarm (" + task.getId() + ", " + type +
") set for " + new Date(time));
am.set(AlarmManager.RTC_WAKEUP, time, pendingIntent);
}
}
// --- data fetching classes
/**
* Gets a listing of all tasks that are active &
* @param properties
* @return todoroo cursor. PLEASE CLOSE THIS CURSOR!
*/
private TodorooCursor<Task> getTasksWithReminders(Property<?>... properties) {
return taskDao.query(Query.select(properties).where(Criterion.and(TaskCriteria.isActive(),
Task.REMINDER_FLAGS.gt(0))));
}
}

@ -0,0 +1,30 @@
package com.todoroo.astrid.reminders;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import com.todoroo.andlib.service.ContextManager;
import com.todoroo.astrid.service.AstridDependencyInjector;
/**
* Service which handles jobs that need to be run when phone boots
*
* @author Tim Su <tim@todoroo.com>
*
*/
public class ReminderStartupService extends BroadcastReceiver {
static {
AstridDependencyInjector.initialize();
}
// --- system startup
@Override
/** Called when the system is started up */
public void onReceive(Context context, Intent intent) {
ContextManager.setContext(context);
new ReminderService().scheduleAllAlarms();
}
}

@ -0,0 +1,50 @@
/**
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.tags;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import com.timsu.astrid.R;
import com.todoroo.astrid.api.AstridApiConstants;
import com.todoroo.astrid.api.TaskDetail;
/**
* Exposes {@link TaskDetail} for tags, i.e. "Tags: frogs, animals"
*
* @author Tim Su <tim@todoroo.com>
*
*/
public class TagDetailExposer extends BroadcastReceiver {
private static TagService tagService = null;
@Override
public void onReceive(Context context, Intent intent) {
// get tags associated with this task
long taskId = intent.getLongExtra(AstridApiConstants.EXTRAS_TASK_ID, -1);
if(taskId == -1)
return;
if(tagService == null)
tagService = new TagService();
String tagList = tagService.getTagsAsString(taskId);
if(tagList.length() == 0)
return;
TaskDetail taskDetail = new TaskDetail(TagsPlugin.IDENTIFIER,
context.getString(R.string.tag_TLA_detail, tagList));
// transmit
TaskDetail[] details = new TaskDetail[1];
details[0] = taskDetail;
Intent broadcastIntent = new Intent(AstridApiConstants.BROADCAST_SEND_DETAILS);
broadcastIntent.putExtra(AstridApiConstants.EXTRAS_ITEMS, details);
broadcastIntent.putExtra(AstridApiConstants.EXTRAS_TASK_ID, taskId);
context.sendBroadcast(broadcastIntent, AstridApiConstants.PERMISSION_READ);
}
}

@ -0,0 +1,104 @@
/**
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.tags;
import android.content.BroadcastReceiver;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.drawable.BitmapDrawable;
import com.timsu.astrid.R;
import com.todoroo.andlib.sql.QueryTemplate;
import com.todoroo.astrid.api.AstridApiConstants;
import com.todoroo.astrid.api.Filter;
import com.todoroo.astrid.api.FilterCategory;
import com.todoroo.astrid.api.FilterListHeader;
import com.todoroo.astrid.api.FilterListItem;
import com.todoroo.astrid.tags.TagService.Tag;
/**
* Exposes filters based on tags
*
* @author Tim Su <tim@todoroo.com>
*
*/
public class TagFilterExposer extends BroadcastReceiver {
private TagService tagService;
@SuppressWarnings("nls")
private Filter filterFromTag(Context context, Tag tag) {
String listTitle = context.getString(R.string.tag_FEx_tag_w_size).
replace("$T", tag.tag).replace("$C", Integer.toString(tag.count));
String title = context.getString(R.string.tag_FEx_name, tag.tag);
QueryTemplate tagTemplate = tag.queryTemplate();
ContentValues contentValues = new ContentValues();
contentValues.put(TagService.KEY, tag.tag);
Filter filter = new Filter(TagsPlugin.IDENTIFIER,
listTitle, title,
tagTemplate,
contentValues);
// filters[0].contextMenuLabels = new String[] {
// "Rename Tag",
// "Delete Tag"
// };
// filters[0].contextMenuIntents = new Intent[] {
// new Intent(),
// new Intent()
// };
return filter;
}
@Override
public void onReceive(Context context, Intent intent) {
tagService = new TagService();
Tag[] tagsByAlpha = tagService.getGroupedTags(TagService.GROUPED_TAGS_BY_ALPHA);
// If user does not have any tags, don't show this section at all
if(tagsByAlpha.length == 0)
return;
Resources r = context.getResources();
Tag[] tagsBySize = tagService.getGroupedTags(TagService.GROUPED_TAGS_BY_SIZE);
Filter[] filtersByAlpha = new Filter[tagsByAlpha.length];
for(int i = 0; i < tagsByAlpha.length; i++)
filtersByAlpha[i] = filterFromTag(context, tagsByAlpha[i]);
Filter[] filtersBySize = new Filter[tagsBySize.length];
for(int i = 0; i < tagsBySize.length; i++)
filtersBySize[i] = filterFromTag(context, tagsBySize[i]);
FilterListHeader tagsHeader = new FilterListHeader(TagsPlugin.IDENTIFIER,
context.getString(R.string.tag_FEx_header));
Filter untagged = new Filter(TagsPlugin.IDENTIFIER,
"Untagged",
"Untagged",
tagService.untaggedTemplate(),
null);
untagged.listingIcon = ((BitmapDrawable)r.getDrawable(R.drawable.filter_untagged)).getBitmap();
FilterCategory tagsCategoryBySize = new FilterCategory(TagsPlugin.IDENTIFIER,
context.getString(R.string.tag_FEx_by_size), filtersBySize);
tagsCategoryBySize.listingIcon = ((BitmapDrawable)r.getDrawable(R.drawable.filter_tags1)).getBitmap();
FilterCategory tagsCategoryByAlpha = new FilterCategory(TagsPlugin.IDENTIFIER,
context.getString(R.string.tag_FEx_alpha), filtersByAlpha);
tagsCategoryByAlpha.listingIcon = ((BitmapDrawable)r.getDrawable(R.drawable.filter_tags2)).getBitmap();
// transmit filter list
FilterListItem[] list = new FilterListItem[4];
list[0] = tagsHeader;
list[1] = untagged;
list[2] = tagsCategoryBySize;
list[3] = tagsCategoryByAlpha;
Intent broadcastIntent = new Intent(AstridApiConstants.BROADCAST_SEND_FILTERS);
broadcastIntent.putExtra(AstridApiConstants.EXTRAS_ITEMS, list);
context.sendBroadcast(broadcastIntent, AstridApiConstants.PERMISSION_READ);
}
}

@ -2,19 +2,18 @@ package com.todoroo.astrid.tags;
import java.util.ArrayList;
import android.content.Context;
import com.todoroo.andlib.data.Property;
import com.todoroo.andlib.data.TodorooCursor;
import com.todoroo.andlib.data.Property.CountProperty;
import com.todoroo.andlib.data.sql.Criterion;
import com.todoroo.andlib.data.sql.Join;
import com.todoroo.andlib.data.sql.Order;
import com.todoroo.andlib.data.sql.Query;
import com.todoroo.andlib.service.Autowired;
import com.todoroo.andlib.service.DependencyInjectionService;
import com.todoroo.andlib.sql.Criterion;
import com.todoroo.andlib.sql.Join;
import com.todoroo.andlib.sql.Order;
import com.todoroo.andlib.sql.Query;
import com.todoroo.andlib.sql.QueryTemplate;
import com.todoroo.astrid.dao.MetadataDao;
import com.todoroo.astrid.dao.MetadataDao.MetadataCriteria;
import com.todoroo.astrid.dao.TaskDao.TaskCriteria;
import com.todoroo.astrid.model.Metadata;
import com.todoroo.astrid.model.Task;
import com.todoroo.astrid.service.MetadataService;
@ -39,7 +38,7 @@ public class TagService {
@Autowired
private MetadataService metadataService;
public TagService(@SuppressWarnings("unused") Context context) {
public TagService() {
DependencyInjectionService.getInstance().inject(this);
}
@ -57,7 +56,7 @@ public class TagService {
* @author Tim Su <tim@todoroo.com>
*
*/
public class Tag {
public final class Tag {
public String tag;
int count;
@ -65,6 +64,25 @@ public class TagService {
public String toString() {
return tag;
}
/**
* Return SQL selector query for getting tasks with a given tag
*
* @param tag
* @return
*/
public QueryTemplate queryTemplate() {
return new QueryTemplate().join(Join.inner(Metadata.TABLE,
Task.ID.eq(Metadata.TASK))).where(Criterion.and(
MetadataCriteria.withKey(KEY), Metadata.VALUE.eq(tag)));
}
}
public QueryTemplate untaggedTemplate() {
return new QueryTemplate().join(Join.left(Metadata.TABLE,
Task.ID.eq(Metadata.TASK))).where(Criterion.and(
TaskCriteria.isActive(), MetadataCriteria.withKey(KEY),
Metadata.VALUE.isNull()));
}
/**
@ -94,7 +112,7 @@ public class TagService {
* Return tags on the given task
*
* @param taskId
* @return empty array if no tags, otherwise array
* @return cursor. PLEASE CLOSE THE CURSOR!
*/
public TodorooCursor<Metadata> getTags(long taskId) {
Query query = Query.select(Metadata.VALUE).where(Criterion.and(MetadataCriteria.withKey(KEY),
@ -111,6 +129,7 @@ public class TagService {
public String getTagsAsString(long taskId) {
StringBuilder tagBuilder = new StringBuilder();
TodorooCursor<Metadata> tags = getTags(taskId);
try {
int length = tags.getCount();
Metadata metadata = new Metadata();
for (int i = 0; i < length; i++) {
@ -120,19 +139,10 @@ public class TagService {
if (i < length - 1)
tagBuilder.append(", ");
}
return tagBuilder.toString();
} finally {
tags.close();
}
/**
* Return SQL selector query for getting tasks with a given tag
*
* @param tag
* @return
*/
public Query tasksWithTag(String tag, Property<?>... properties) {
return Query.select(properties).join(Join.inner(Metadata.TABLE,
Task.ID.eq(Metadata.TASK))).where(Criterion.and(
MetadataCriteria.withKey(KEY), Metadata.VALUE.eq(tag)));
return tagBuilder.toString();
}
/**
@ -149,7 +159,7 @@ public class TagService {
metadata.setValue(Metadata.TASK, taskId);
for(String tag : tags) {
metadata.setValue(Metadata.VALUE, tag.trim());
metadataDao.createItem(metadata);
metadataDao.createNew(metadata);
}
}
}

@ -0,0 +1,24 @@
package com.todoroo.astrid.tags;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import com.todoroo.astrid.api.AstridApiConstants;
import com.todoroo.astrid.api.Plugin;
public class TagsPlugin extends BroadcastReceiver {
static final String IDENTIFIER = "tags";
@Override
public void onReceive(Context context, Intent intent) {
Plugin plugin = new Plugin(IDENTIFIER, "Tags", "Todoroo",
"Provides tagging support for tasks.");
Intent broadcastIntent = new Intent(AstridApiConstants.BROADCAST_SEND_PLUGINS);
broadcastIntent.putExtra(AstridApiConstants.EXTRAS_PLUGIN, plugin);
context.sendBroadcast(broadcastIntent, AstridApiConstants.PERMISSION_READ);
}
}

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:type="radial"
android:startColor="#373737"
android:endColor="#000000"
android:gradientRadius="300"
android:centerX="0.5"
android:centerY="0.5" />
</shape>

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:startColor="#ffffffff"
android:centerColor="#ff000000"
android:endColor="#ffffffff"
android:angle="0" />
</shape>

Binary file not shown.

After

Width:  |  Height:  |  Size: 733 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 666 B

@ -27,7 +27,7 @@
<item android:state_checked="false"
android:state_enabled="true"
android:drawable="@drawable/btn_check_0" />
android:drawable="@drawable/btn_check_off" />
<item android:state_checked="true"
android:state_enabled="true"
android:drawable="@drawable/btn_check_on" />

@ -1,35 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2008 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Enabled states -->
<item android:state_checked="true" android:state_pressed="true"
android:state_enabled="true"
android:drawable="@drawable/btn_check_on_pressed" />
<item android:state_checked="false" android:state_pressed="true"
android:state_enabled="true"
android:drawable="@drawable/btn_check_off_pressed" />
<item android:state_checked="false"
android:state_enabled="true"
android:drawable="@drawable/btn_check_50" />
<item android:state_checked="true"
android:state_enabled="true"
android:drawable="@drawable/btn_check_on" />
</selector>

@ -1,35 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2008 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
t
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Enabled states -->
<item android:state_checked="true" android:state_pressed="true"
android:state_enabled="true"
android:drawable="@drawable/btn_check_on_pressed" />
<item android:state_checked="false" android:state_pressed="true"
android:state_enabled="true"
android:drawable="@drawable/btn_check_off_pressed" />
<item android:state_checked="false"
android:state_enabled="true"
android:drawable="@drawable/btn_check_75" />
<item android:state_checked="true"
android:state_enabled="true"
android:drawable="@drawable/btn_check_on" />
</selector>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 222 B

@ -13,14 +13,8 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_weight="1"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<ToggleButton android:id="@+id/button"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:textSize="18dip"
android:disabledAlpha="0.3"/>
</FrameLayout>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:drawable="@drawable/none" />
</selector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 390 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 567 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 655 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 853 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 806 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 378 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 615 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 599 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 695 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 B

@ -16,20 +16,11 @@
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Enabled states -->
<item android:state_checked="true" android:state_pressed="true"
android:state_enabled="true"
android:drawable="@drawable/btn_check_on_pressed" />
<item android:state_checked="false" android:state_pressed="true"
android:state_enabled="true"
android:drawable="@drawable/btn_check_off_pressed" />
<item android:state_checked="false"
android:state_enabled="true"
android:drawable="@drawable/btn_check_25" />
<item android:state_checked="true"
android:state_enabled="true"
android:drawable="@drawable/btn_check_on" />
<item android:state_pressed="false" android:state_enabled="true"
android:state_focused="false" android:drawable="@drawable/ic_tasklist_back" />
<item android:state_pressed="true" android:state_enabled="true"
android:drawable="@drawable/ic_tasklist_back_pressed" />
<item android:state_pressed="false" android:state_enabled="true"
android:state_focused="true" android:drawable="@drawable/ic_tasklist_back_selected" />
</selector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save