Merge branch 'master' into 130225_sb_facebook_sdk

Conflicts:
	astrid/plugin-src/com/todoroo/astrid/actfm/ActFmLoginActivity.java
pull/14/head
Sam Bosley 11 years ago
commit dfbad08199

@ -49,6 +49,8 @@ public abstract class Property<TYPE> extends Field implements Cloneable {
public static final int PROP_FLAG_BOOLEAN = 1 << 3;
/** Is this field a serialized JSON object? */
public static final int PROP_FLAG_JSON = 1 << 4;
/** Is this field for pictures? (usually as a json object containing "path" key or urls) */
public static final int PROP_FLAG_PICTURE = 1 << 5;
public int flags = 0;

@ -200,13 +200,13 @@ public class DialogUtilities {
* @param text
* @return
*/
public static ProgressDialog progressDialog(Context context, String text) {
public static ProgressDialog progressDialog(Activity context, String text) {
ProgressDialog dialog = new ProgressDialog(context);
dialog.setIndeterminate(true);
dialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
dialog.setMessage(text);
dialog.show();
dialog.setOwnerActivity((Activity)context);
dialog.setOwnerActivity(context);
return dialog;
}

@ -5,16 +5,10 @@
*/
package com.todoroo.astrid.data;
import java.util.ArrayList;
import java.util.HashSet;
import android.content.ContentValues;
import android.content.Context;
import com.todoroo.andlib.data.ContentResolverDao;
import com.todoroo.andlib.data.TodorooCursor;
import com.todoroo.andlib.sql.Criterion;
import com.todoroo.andlib.sql.Query;
/**
* Data access object for accessing Astrid's {@link Metadata} table. A
@ -52,55 +46,4 @@ public class MetadataApiDao extends ContentResolverDao<Metadata> {
}
/**
* Synchronize metadata for given task id. Deletes rows in database that
* are not identical to those in the metadata list, creates rows that
* have no match.
*
* @param taskId id of task to perform synchronization on
* @param metadata list of new metadata items to save
* @param metadataCriteria criteria to load data for comparison from metadata
*/
public void synchronizeMetadata(long taskId, ArrayList<Metadata> metadata,
Criterion metadataCriteria) {
HashSet<ContentValues> newMetadataValues = new HashSet<ContentValues>();
for(Metadata metadatum : metadata) {
metadatum.setValue(Metadata.TASK, taskId);
metadatum.clearValue(Metadata.ID);
newMetadataValues.add(metadatum.getMergedValues());
}
Metadata item = new Metadata();
TodorooCursor<Metadata> cursor = query(Query.select(Metadata.PROPERTIES).where(Criterion.and(MetadataCriteria.byTask(taskId),
metadataCriteria)));
try {
// try to find matches within our metadata list
for(cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
item.readFromCursor(cursor);
long id = item.getId();
// clear item id when matching with incoming values
item.clearValue(Metadata.ID);
ContentValues itemMergedValues = item.getMergedValues();
if(newMetadataValues.contains(itemMergedValues)) {
newMetadataValues.remove(itemMergedValues);
continue;
}
// not matched. cut it
delete(id);
}
} finally {
cursor.close();
}
// everything that remains shall be written
for(ContentValues values : newMetadataValues) {
item.clear();
item.mergeWith(values);
save(item);
}
}
}

@ -5,17 +5,25 @@
*/
package com.todoroo.astrid.data;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import org.json.JSONException;
import org.json.JSONObject;
import android.content.ContentValues;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.text.TextUtils;
import com.todoroo.andlib.data.AbstractModel;
import com.todoroo.andlib.data.Property.LongProperty;
import com.todoroo.andlib.data.Property.StringProperty;
import com.todoroo.andlib.data.TodorooCursor;
import com.todoroo.andlib.utility.AndroidUtilities;
import com.todoroo.andlib.utility.DateUtilities;
/**
* A model that is synchronized to a remote server and has a remote id
@ -77,6 +85,16 @@ abstract public class RemoteModel extends AbstractModel {
return NO_UUID;
}
public void setUuid(String uuid) {
if (setValues == null)
setValues = new ContentValues();
if(NO_UUID.equals(uuid))
clearValue(UUID_PROPERTY);
else
setValues.put(UUID_PROPERTY_NAME, uuid);
}
public static boolean isUuidEmpty(String uuid) {
return NO_UUID.equals(uuid) || TextUtils.isEmpty(uuid);
}
@ -97,12 +115,49 @@ abstract public class RemoteModel extends AbstractModel {
public static class PictureHelper {
public static final String PICTURES_DIRECTORY = "pictures"; //$NON-NLS-1$
@SuppressWarnings("nls")
public static JSONObject savePictureJson(Context context, Bitmap bitmap) {
try {
String name = DateUtilities.now() + ".jpg";
JSONObject jsonObject = new JSONObject();
jsonObject.put("name", name);
jsonObject.put("type", "image/jpeg");
File dir = context.getExternalFilesDir(PICTURES_DIRECTORY);
if (dir != null) {
File file = new File(dir + File.separator + DateUtilities.now() + ".jpg");
if (file.exists())
return null;
try {
FileOutputStream fos = new FileOutputStream(file);
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, fos);
fos.flush();
fos.close();
jsonObject.put("path", file.getAbsolutePath());
} catch (FileNotFoundException e) {
//
} catch (IOException e) {
//
}
return jsonObject;
} else {
return null;
}
} catch (JSONException e) {
//
}
return null;
}
public static String getPictureUrl(String value, String size) {
try {
if (value == null)
return null;
JSONObject pictureJson = new JSONObject(value);
if (pictureJson.has("data")) // Unpushed encoded bitmap //$NON-NLS-1$
if (pictureJson.has("path")) // Unpushed encoded bitmap //$NON-NLS-1$
return null;
return pictureJson.optString(size);
} catch (JSONException e) {
@ -115,11 +170,11 @@ abstract public class RemoteModel extends AbstractModel {
try {
if (value == null)
return null;
if (value.contains("data")) {
if (value.contains("path")) {
JSONObject pictureJson = new JSONObject(value);
if (pictureJson.has("data")) {
String data = pictureJson.getString("data");
return AndroidUtilities.decodeBase64Bitmap(data);
if (pictureJson.has("path")) {
String path = pictureJson.getString("path");
return BitmapFactory.decodeFile(path);
}
}
return null;
@ -133,18 +188,5 @@ abstract public class RemoteModel extends AbstractModel {
String value = cursor.get(pictureProperty);
return getPictureUrl(value, size);
}
@SuppressWarnings("nls")
public static JSONObject uploadPictureJson(Bitmap bitmap) {
try {
JSONObject pictureJson = new JSONObject();
pictureJson.put("name", "photo.jpg");
pictureJson.put("type", "image/jpeg");
pictureJson.put("data", AndroidUtilities.encodeBase64Bitmap(bitmap));
return pictureJson;
} catch (JSONException e) {
return null;
}
}
}
}

@ -63,7 +63,7 @@ public final class TagData extends RemoteModel {
/** Project picture */
public static final StringProperty PICTURE = new StringProperty(
TABLE, "picture", Property.PROP_FLAG_JSON);
TABLE, "picture", Property.PROP_FLAG_JSON | Property.PROP_FLAG_PICTURE);
/** Tag team array (JSON) */
@Deprecated public static final StringProperty MEMBERS = new StringProperty(

@ -54,7 +54,7 @@ public class UserActivity extends RemoteModel {
/** Picture */
public static final StringProperty PICTURE = new StringProperty(
TABLE, "picture", Property.PROP_FLAG_JSON);
TABLE, "picture", Property.PROP_FLAG_JSON | Property.PROP_FLAG_PICTURE);
/** Target id */
public static final StringProperty TARGET_ID = new StringProperty(

@ -77,13 +77,15 @@ abstract public class SyncV2BackgroundService extends Service {
if(!getSyncUtilities().isLoggedIn())
return;
getSyncProvider().synchronizeActiveTasks(false, new SyncResultCallbackAdapter() {
@Override
public void finished() {
getSyncUtilities().recordSuccessfulSync();
context.sendBroadcast(new Intent(AstridApiConstants.BROADCAST_EVENT_REFRESH));
}
});
SyncV2Provider provider = getSyncProvider();
if (provider.isActive())
provider.synchronizeActiveTasks(false, new SyncResultCallbackAdapter() {
@Override
public void finished() {
getSyncUtilities().recordSuccessfulSync();
context.sendBroadcast(new Intent(AstridApiConstants.BROADCAST_EVENT_REFRESH));
}
});
}
@Override

@ -86,7 +86,8 @@ abstract public class SyncV2Provider {
utilities.recordSuccessfulSync();
utilities.reportLastError();
utilities.stopOngoing();
callback.finished();
if (callback != null)
callback.finished();
}
@Override

@ -5,7 +5,6 @@
<classpathentry kind="src" path="common-src"/>
<classpathentry excluding="com/todoroo/astrid/actfm/ProjectDetailExposer.java|com/todoroo/astrid/actfm/ProjectListActivity.java|com/todoroo/astrid/actfm/ShowProjectExposer.java|com/todoroo/astrid/actfm/TaskFields.java|com/todoroo/astrid/rmilk/EditOperationExposer.java|com/todoroo/astrid/rmilk/MilkEditActivity.java" kind="src" path="plugin-src"/>
<classpathentry kind="src" path="gen"/>
<classpathentry kind="src" path="rmilk-src"/>
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.LIBRARIES"/>
<classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
<classpathentry exported="true" kind="lib" path="libs/commons-codec-1.3.jar"/>
@ -28,9 +27,9 @@
<classpathentry exported="true" kind="lib" path="libs/google-oauth-client-1.6.0-beta.jar"/>
<classpathentry exported="true" kind="lib" path="libs/google-oauth-client-extensions-1.6.0-beta.jar"/>
<classpathentry exported="true" kind="lib" path="libs/gson-1.7.1.jar"/>
<classpathentry kind="lib" path="libs/crittercism_v2_1_2.jar"/>
<classpathentry kind="lib" path="libs/findbugs-annotations.jar"/>
<classpathentry kind="lib" path="libs/CWAC-SackOfViewsAdapter.jar"/>
<classpathentry kind="lib" path="libs/gcm.jar" sourcepath="libs/gcm-src.jar"/>
<classpathentry kind="lib" path="libs/crittercism_v3_0_7_sdkonly.jar"/>
<classpathentry kind="output" path="bin/classes"/>
</classpath>

@ -189,11 +189,6 @@
android:theme="@android:style/Theme" />
<!-- Start of Crittercism.com Code -->
<activity android:name="com.crittercism.NewFeedbackSpringboardActivity" android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen"></activity>
<activity android:name="com.crittercism.NewFeedbackIssueListActivity" android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen"></activity>
<activity android:name="com.crittercism.NewFeedbackQuestionListActivity" android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen"></activity>
<activity android:name="com.crittercism.NewFeedbackItemDetailsActivity" android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen"></activity>
<activity android:name="com.crittercism.NewFeedbackCreateActivity" android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen"></activity>
<activity android:name="com.crittercism.NotificationActivity"/>
<service android:name="com.crittercism.service.CrittercismService"
@ -464,7 +459,8 @@
</receiver>
<activity android:name="com.todoroo.astrid.gtasks.GtasksPreferences"
android:theme="@android:style/Theme"
android:label="@string/gtasks_GPr_header">
android:label="@string/gtasks_GPr_header"
android:screenOrientation="portrait">
<meta-data android:name="category"
android:resource="@string/SyP_label" />
<meta-data android:name="syncAction"
@ -649,101 +645,6 @@
android:clearTaskOnLaunch="true">
</activity>
<service android:name="com.todoroo.astrid.reminders.ReminderSchedulingService"/>
<!-- producteev -->
<receiver android:name="com.todoroo.astrid.producteev.ProducteevFilterExposer">
<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.producteev.ProducteevCustomFilterCriteriaExposer">
<intent-filter>
<action android:name="com.todoroo.astrid.REQUEST_CUSTOM_FILTER_CRITERIA" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</receiver>
<activity android:name="com.todoroo.astrid.producteev.ProducteevPreferences"
android:theme="@android:style/Theme"
android:label="@string/producteev_PPr_header">
<meta-data android:name="category"
android:resource="@string/SyP_label" />
<meta-data android:name="syncAction"
android:value="true" />
<intent-filter>
<action android:name="com.todoroo.astrid.SETTINGS" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<activity android:name="com.todoroo.astrid.producteev.ProducteevLoginActivity"
android:configChanges="orientation|keyboardHidden" />
<service android:name="com.todoroo.astrid.producteev.ProducteevBackgroundService">
<intent-filter>
<action android:name="com.todoroo.astrid.producteev.SYNC" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</service>
<receiver android:name="com.todoroo.astrid.producteev.ProducteevStartupReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</receiver>
<receiver android:name="com.todoroo.astrid.producteev.ProducteevDetailExposer">
<intent-filter>
<action android:name="com.todoroo.astrid.REQUEST_DETAILS" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</receiver>
<receiver android:name="com.todoroo.astrid.producteev.ProducteevSyncActionExposer">
<intent-filter>
<action android:name="com.todoroo.astrid.REQUEST_SYNC_ACTIONS" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</receiver>
<!-- rmilk -->
<receiver android:name="org.weloveastrid.rmilk.MilkFilterExposer">
<intent-filter>
<action android:name="com.todoroo.astrid.REQUEST_FILTERS" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</receiver>
<receiver android:name="org.weloveastrid.rmilk.MilkDetailExposer">
<intent-filter>
<action android:name="com.todoroo.astrid.REQUEST_DETAILS" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</receiver>
<activity android:name="org.weloveastrid.rmilk.MilkLoginActivity" />
<activity android:name="org.weloveastrid.rmilk.MilkPreferences"
android:theme="@android:style/Theme"
android:icon="@drawable/icon"
android:label="@string/rmilk_MPr_header">
<meta-data android:name="category"
android:resource="@string/SyP_label" />
<intent-filter>
<action android:name="com.todoroo.astrid.SETTINGS" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<service android:name="org.weloveastrid.rmilk.MilkBackgroundService"/>
<receiver android:name="org.weloveastrid.rmilk.MilkStartupReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<intent-filter>
<action android:name="com.todoroo.astrid.STARTUP" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</receiver>
<receiver android:name="org.weloveastrid.rmilk.MilkSyncActionExposer">
<intent-filter>
<action android:name="com.todoroo.astrid.REQUEST_SYNC_ACTIONS" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</receiver>
<!-- other task actions -->
<receiver android:name="com.todoroo.astrid.core.LinkActionExposer">

@ -8,7 +8,7 @@ out.dir=antbuild
source.dir=src-combined
# Astrid source folders
astrid.sources=src,common-src,plugin-src,src-legacy,rmilk-src
astrid.sources=src,common-src,plugin-src,src-legacy
# Keystore
signjar.keystore=/etc/todoroo/keystore

@ -7,7 +7,10 @@ package com.todoroo.astrid.actfm;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map.Entry;
import java.util.Random;
import java.util.Set;
import org.json.JSONObject;
@ -52,10 +55,13 @@ import com.google.android.googlelogin.GoogleLoginServiceConstants;
import com.google.android.googlelogin.GoogleLoginServiceHelper;
import com.timsu.astrid.GCMIntentService;
import com.timsu.astrid.R;
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.service.ExceptionService;
import com.todoroo.andlib.sql.Criterion;
import com.todoroo.andlib.sql.Query;
import com.todoroo.andlib.utility.AndroidUtilities;
import com.todoroo.andlib.utility.DateUtilities;
import com.todoroo.andlib.utility.DialogUtilities;
@ -65,31 +71,47 @@ import com.todoroo.astrid.actfm.sync.ActFmPreferenceService;
import com.todoroo.astrid.actfm.sync.ActFmServiceException;
import com.todoroo.astrid.actfm.sync.ActFmSyncMonitor;
import com.todoroo.astrid.actfm.sync.messages.ConstructOutstandingTableFromMasterTable;
import com.todoroo.astrid.actfm.sync.messages.ConstructTaskOutstandingTableFromMasterTable;
import com.todoroo.astrid.actfm.sync.messages.NameMaps;
import com.todoroo.astrid.activity.Eula;
import com.todoroo.astrid.dao.Database;
import com.todoroo.astrid.dao.MetadataDao;
import com.todoroo.astrid.dao.MetadataDao.MetadataCriteria;
import com.todoroo.astrid.dao.RemoteModelDao;
import com.todoroo.astrid.dao.TagDataDao;
import com.todoroo.astrid.dao.TagMetadataDao;
import com.todoroo.astrid.dao.TagOutstandingDao;
import com.todoroo.astrid.dao.TaskAttachmentDao;
import com.todoroo.astrid.dao.TaskAttachmentOutstandingDao;
import com.todoroo.astrid.dao.TaskDao;
import com.todoroo.astrid.dao.TaskListMetadataDao;
import com.todoroo.astrid.dao.TaskListMetadataOutstandingDao;
import com.todoroo.astrid.dao.TaskOutstandingDao;
import com.todoroo.astrid.dao.UserActivityDao;
import com.todoroo.astrid.dao.UserActivityOutstandingDao;
import com.todoroo.astrid.dao.UserDao;
import com.todoroo.astrid.data.Metadata;
import com.todoroo.astrid.data.RemoteModel;
import com.todoroo.astrid.data.TagData;
import com.todoroo.astrid.data.TagOutstanding;
import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.data.TaskListMetadata;
import com.todoroo.astrid.data.TaskListMetadataOutstanding;
import com.todoroo.astrid.data.TaskOutstanding;
import com.todoroo.astrid.data.UserActivity;
import com.todoroo.astrid.data.UserActivityOutstanding;
import com.todoroo.astrid.gtasks.auth.ModernAuthManager;
import com.todoroo.astrid.helper.UUIDHelper;
import com.todoroo.astrid.service.AstridDependencyInjector;
import com.todoroo.astrid.service.MarketStrategy.AmazonMarketStrategy;
import com.todoroo.astrid.service.StatisticsConstants;
import com.todoroo.astrid.service.StatisticsService;
import com.todoroo.astrid.service.SyncV2Service;
import com.todoroo.astrid.service.TaskService;
import com.todoroo.astrid.subtasks.AstridOrderedListUpdater;
import com.todoroo.astrid.subtasks.AstridOrderedListUpdater.Node;
import com.todoroo.astrid.subtasks.SubtasksHelper;
import com.todoroo.astrid.subtasks.SubtasksHelper.TreeRemapHelper;
import com.todoroo.astrid.tags.TaskToTagMetadata;
/**
* This activity allows users to sign in or log in to Astrid.com
@ -99,6 +121,8 @@ import com.todoroo.astrid.service.TaskService;
*/
public class ActFmLoginActivity extends SherlockFragmentActivity {
@Autowired
protected Database database;
@Autowired
protected ExceptionService exceptionService;
@Autowired
@ -111,10 +135,16 @@ public class ActFmLoginActivity extends SherlockFragmentActivity {
@Autowired
private TaskOutstandingDao taskOutstandingDao;
@Autowired
private TaskAttachmentDao taskAttachmentDao;
@Autowired
private TaskAttachmentOutstandingDao taskAttachmentOutstandingDao;
@Autowired
private TagDataDao tagDataDao;
@Autowired
private TagOutstandingDao tagOutstandingDao;
@Autowired
private UserDao userDao;
@Autowired
private UserActivityDao userActivityDao;
@Autowired
private UserActivityOutstandingDao userActivityOutstandingDao;
@ -122,7 +152,10 @@ public class ActFmLoginActivity extends SherlockFragmentActivity {
private TaskListMetadataDao taskListMetadataDao;
@Autowired
private TaskListMetadataOutstandingDao taskListMetadataOutstandingDao;
@Autowired
private MetadataDao metadataDao;
@Autowired
private TagMetadataDao tagMetadataDao;
@Autowired protected SyncV2Service syncService;
@ -618,11 +651,11 @@ public class ActFmLoginActivity extends SherlockFragmentActivity {
StatisticsService.reportEvent(StatisticsConstants.ACTFM_NEW_USER, "provider", provider);
}
// Successful login, create outstanding entries
if (!TextUtils.isEmpty(token)) {
new ConstructOutstandingTableFromMasterTable<Task, TaskOutstanding>(NameMaps.TABLE_ID_TASKS, taskDao, taskOutstandingDao, Task.CREATION_DATE).execute();
new ConstructOutstandingTableFromMasterTable<TagData, TagOutstanding>(NameMaps.TABLE_ID_TAGS, tagDataDao, tagOutstandingDao, TagData.CREATION_DATE).execute();
new ConstructOutstandingTableFromMasterTable<UserActivity, UserActivityOutstanding>(NameMaps.TABLE_ID_USER_ACTIVITY, userActivityDao, userActivityOutstandingDao, UserActivity.CREATED_AT).execute();
new ConstructOutstandingTableFromMasterTable<TaskListMetadata, TaskListMetadataOutstanding>(NameMaps.TABLE_ID_TASK_LIST_METADATA, taskListMetadataDao, taskListMetadataOutstandingDao, null).execute();
long lastId = Preferences.getLong(ActFmPreferenceService.PREF_USER_ID, 0);
long newUser = result.optLong("id");
if (!TextUtils.isEmpty(token) && (lastId == 0 || lastId == newUser)) {
constructOutstandingTables();
}
runOnUiThread(new Runnable() {
public void run() {
@ -646,8 +679,192 @@ public class ActFmLoginActivity extends SherlockFragmentActivity {
}.start();
}
private void constructOutstandingTables() {
new ConstructTaskOutstandingTableFromMasterTable(NameMaps.TABLE_ID_TASKS, taskDao, taskOutstandingDao, metadataDao, Task.CREATION_DATE).execute();
new ConstructOutstandingTableFromMasterTable<TagData, TagOutstanding>(NameMaps.TABLE_ID_TAGS, tagDataDao, tagOutstandingDao, TagData.CREATION_DATE).execute();
new ConstructOutstandingTableFromMasterTable<UserActivity, UserActivityOutstanding>(NameMaps.TABLE_ID_USER_ACTIVITY, userActivityDao, userActivityOutstandingDao, UserActivity.CREATED_AT).execute();
new ConstructOutstandingTableFromMasterTable<TaskListMetadata, TaskListMetadataOutstanding>(NameMaps.TABLE_ID_TASK_LIST_METADATA, taskListMetadataDao, taskListMetadataOutstandingDao, null).execute();
}
@SuppressWarnings("nls")
protected void postAuthenticate(JSONObject result, String token) {
private void postAuthenticate(final JSONObject result, final String token) {
long lastLoggedInUser = Preferences.getLong(ActFmPreferenceService.PREF_USER_ID, 0);
if (lastLoggedInUser > 0) {
long newUserId = result.optLong("id");
if (lastLoggedInUser != newUserId) {
// In this case, we need to either make all data private or clear all data
// Prompt for choice
DialogUtilities.okCancelCustomDialog(this,
getString(R.string.actfm_logged_in_different_user_title),
getString(R.string.actfm_logged_in_different_user_body),
R.string.actfm_logged_in_different_user_keep_data,
R.string.actfm_logged_in_different_user_clear_data,
android.R.drawable.ic_dialog_alert,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// TODO: Make all data private
final ProgressDialog pd = DialogUtilities.progressDialog(ActFmLoginActivity.this, getString(R.string.actfm_logged_in_different_user_processing));
new Thread(new Runnable() {
@Override
public void run() {
// Delete all tasks not assigned to self
taskService.deleteWhere(Criterion.or(Task.USER_ID.neq(0), Task.DELETION_DATE.gt(0)));
// Delete user table
userDao.deleteWhere(Criterion.all);
// Delete attachments table
taskAttachmentDao.deleteWhere(Criterion.all);
// Delete deleted tags
tagDataDao.deleteWhere(TagData.DELETION_DATE.gt(0));
// Delete deleted metadata
metadataDao.deleteWhere(Metadata.DELETION_DATE.gt(0));
// Clear all outstanding tables
taskOutstandingDao.deleteWhere(Criterion.all);
tagOutstandingDao.deleteWhere(Criterion.all);
userActivityOutstandingDao.deleteWhere(Criterion.all);
taskListMetadataOutstandingDao.deleteWhere(Criterion.all);
taskAttachmentOutstandingDao.deleteWhere(Criterion.all);
// Make all tags private
tagMetadataDao.deleteWhere(Criterion.all);
// Generate new uuids for all tasks/tags/user activity/task list metadata and update links
generateNewUuids();
constructOutstandingTables();
runOnUiThread(new Runnable() {
@Override
public void run() {
finishSignIn(result, token, true);
}
});
pd.dismiss();
}
}).start();
}
},
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
deleteDatabase(database.getName());
finishSignIn(result, token, true);
}
});
} else {
finishSignIn(result, token, false);
}
} else {
finishSignIn(result, token, false);
}
}
private void generateNewUuids() {
final HashMap<String, String> uuidTaskMap = new HashMap<String, String>();
HashMap<String, String> uuidTagMap = new HashMap<String, String>();
HashMap<String, String> uuidUserActivityMap = new HashMap<String, String>();
HashMap<String, String> uuidTaskListMetadataMap = new HashMap<String, String>();
mapUuids(taskDao, uuidTaskMap);
mapUuids(tagDataDao, uuidTagMap);
mapUuids(userActivityDao, uuidUserActivityMap);
mapUuids(taskListMetadataDao, uuidTaskListMetadataMap);
Task t = new Task();
TagData td = new TagData();
Metadata m = new Metadata();
UserActivity ua = new UserActivity();
TaskListMetadata tlm = new TaskListMetadata();
Set<Entry<String, String>> entries = uuidTaskMap.entrySet();
for (Entry<String, String> e : entries) {
t.clear();
m.clear();
ua.clear();
String oldUuid = e.getKey();
String newUuid = e.getValue();
t.setValue(Task.UUID, newUuid);
ua.setValue(UserActivity.TARGET_ID, newUuid);
m.setValue(TaskToTagMetadata.TASK_UUID, newUuid);
taskDao.update(Task.UUID.eq(oldUuid), t);
metadataDao.update(Criterion.and(MetadataCriteria.withKey(TaskToTagMetadata.KEY), TaskToTagMetadata.TASK_UUID.eq(oldUuid)), m);
userActivityDao.update(UserActivity.TARGET_ID.eq(oldUuid), ua);
}
entries = uuidTagMap.entrySet();
for (Entry<String, String> e : entries) {
td.clear();
ua.clear();
m.clear();
tlm.clear();
String oldUuid = e.getKey();
String newUuid = e.getValue();
td.setValue(TagData.UUID, newUuid);
ua.setValue(UserActivity.TARGET_ID, newUuid);
m.setValue(TaskToTagMetadata.TAG_UUID, newUuid);
tlm.setValue(TaskListMetadata.TAG_UUID, newUuid);
tagDataDao.update(TagData.UUID.eq(oldUuid), td);
userActivityDao.update(UserActivity.TARGET_ID.eq(oldUuid), ua);
metadataDao.update(Criterion.and(MetadataCriteria.withKey(TaskToTagMetadata.KEY), TaskToTagMetadata.TAG_UUID.eq(oldUuid)), m);
taskListMetadataDao.update(TaskListMetadata.TAG_UUID.eq(oldUuid), tlm);
}
entries = uuidUserActivityMap.entrySet();
for (Entry<String, String> e : entries) {
ua.clear();
String oldUuid = e.getKey();
String newUuid = e.getValue();
ua.setValue(UserActivity.UUID, newUuid);
userActivityDao.update(UserActivity.UUID.eq(oldUuid), ua);
}
TodorooCursor<TaskListMetadata> tlmCursor = taskListMetadataDao.query(Query.select(TaskListMetadata.ID, TaskListMetadata.UUID, TaskListMetadata.TASK_IDS, TaskListMetadata.CHILD_TAG_IDS));
try {
for (tlmCursor.moveToFirst(); !tlmCursor.isAfterLast(); tlmCursor.moveToNext()) {
tlm.clear();
tlm.readFromCursor(tlmCursor);
tlm.setValue(TaskListMetadata.UUID, uuidTaskListMetadataMap.get(tlm.getUuid()));
String taskIds = tlm.getValue(TaskListMetadata.TASK_IDS);
if (!TaskListMetadata.taskIdsIsEmpty(taskIds)) {
Node root = AstridOrderedListUpdater.buildTreeModel(taskIds, null);
SubtasksHelper.remapTree(root, uuidTaskMap, new TreeRemapHelper<String>() {
public String getKeyFromOldUuid(String uuid) {
return uuid; // Old uuids are the keys
}
});
taskIds = AstridOrderedListUpdater.serializeTree(root);
tlm.setValue(TaskListMetadata.TASK_IDS, taskIds);
}
taskListMetadataDao.saveExisting(tlm);
}
} finally {
tlmCursor.close();
}
}
private <T extends RemoteModel> void mapUuids(RemoteModelDao<T> dao, HashMap<String, String> map) {
TodorooCursor<T> items = dao.query(Query.select(RemoteModel.UUID_PROPERTY));
try {
for (items.moveToFirst(); !items.isAfterLast(); items.moveToNext()) {
map.put(items.get(RemoteModel.UUID_PROPERTY), UUIDHelper.newUUID());
}
} finally {
items.close();
}
}
@SuppressWarnings("nls")
private void finishSignIn(JSONObject result, String token, boolean restart) {
actFmPreferenceService.setToken(token);
Preferences.setLong(ActFmPreferenceService.PREF_USER_ID,
@ -667,15 +884,21 @@ public class ActFmLoginActivity extends SherlockFragmentActivity {
ActFmPreferenceService.reloadThisUser();
GCMIntentService.register(this);
if (restart) {
System.exit(0);
return;
} else {
setResult(RESULT_OK);
finish();
}
ActFmSyncMonitor monitor = ActFmSyncMonitor.getInstance();
synchronized (monitor) {
monitor.notifyAll();
}
setResult(RESULT_OK);
finish();
GCMIntentService.register(this);
}
@SuppressWarnings("nls")

@ -325,7 +325,7 @@ public abstract class CommentsFragment extends SherlockListFragment {
protected void addComment() {
UserActivity update = createUpdate();
if (picture != null) {
JSONObject pictureJson = RemoteModel.PictureHelper.uploadPictureJson(picture);
JSONObject pictureJson = RemoteModel.PictureHelper.savePictureJson(getActivity(), picture);
if (pictureJson != null) {
update.setValue(UserActivity.PICTURE, pictureJson.toString());
}

@ -293,7 +293,7 @@ public class TagSettingsActivity extends SherlockFragmentActivity {
tagData.setValue(TagData.TAG_DESCRIPTION, newDesc);
if (setBitmap != null) {
JSONObject pictureJson = RemoteModel.PictureHelper.uploadPictureJson(setBitmap);
JSONObject pictureJson = RemoteModel.PictureHelper.savePictureJson(this, setBitmap);
if (pictureJson != null)
tagData.setValue(TagData.PICTURE, pictureJson.toString());
}

@ -12,14 +12,12 @@ import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.TimeZone;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.http.HttpEntity;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.protocol.HTTP;
import org.apache.http.entity.mime.MultipartEntity;
import org.apache.http.entity.mime.content.StringBody;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
@ -185,7 +183,7 @@ public class ActFmInvoker {
}
}
public JSONObject postSync(JSONArray data, String token) throws IOException,
public JSONObject postSync(JSONArray data, MultipartEntity entity, String token) throws IOException,
ActFmServiceException {
try {
String dataString = data.toString();
@ -194,11 +192,9 @@ public class ActFmInvoker {
String request = createFetchUrl("api/" + API_VERSION, "synchronize", "token", token, "data", dataString, "time", timeString);
if (SYNC_DEBUG)
Log.e("act-fm-post", request);
List<BasicNameValuePair> pairs = new ArrayList<BasicNameValuePair>();
pairs.add(new BasicNameValuePair("token", token));
pairs.add(new BasicNameValuePair("data", data.toString()));
pairs.add(new BasicNameValuePair("time", timeString));
UrlEncodedFormEntity entity = new UrlEncodedFormEntity(pairs, HTTP.UTF_8);
entity.addPart("token", new StringBody(token));
entity.addPart("data", new StringBody(data.toString()));
entity.addPart("time", new StringBody(timeString));
String response = restClient.post(request, entity);
JSONObject object = new JSONObject(response);

@ -3,22 +3,31 @@ package com.todoroo.astrid.actfm.sync;
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.http.entity.mime.MultipartEntity;
import org.json.JSONArray;
import org.json.JSONObject;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
import android.view.View;
import android.widget.ProgressBar;
import com.crittercism.app.Crittercism;
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.Query;
import com.todoroo.andlib.utility.AndroidUtilities;
import com.todoroo.andlib.utility.DateUtilities;
import com.todoroo.andlib.utility.Preferences;
import com.todoroo.astrid.actfm.sync.messages.BriefMe;
import com.todoroo.astrid.actfm.sync.messages.ChangesHappened;
@ -52,6 +61,7 @@ import com.todoroo.astrid.data.TaskListMetadataOutstanding;
import com.todoroo.astrid.data.TaskOutstanding;
import com.todoroo.astrid.data.User;
import com.todoroo.astrid.data.UserActivity;
import com.todoroo.astrid.widget.TasksWidget;
public class ActFmSyncThread {
@ -98,6 +108,12 @@ public class ActFmSyncThread {
private boolean isTimeForBackgroundSync = false;
private final Object progressBarLock = new Object();
private Activity activity = null;
private ProgressBar progressBar = null;
public static enum ModelType {
TYPE_TASK,
TYPE_TAG,
@ -164,6 +180,18 @@ public class ActFmSyncThread {
}
}
private final Runnable enqueueMessageProgressRunnable = new Runnable() {
@Override
public void run() {
synchronized (progressBarLock) {
if (progressBar != null) {
progressBar.setMax(progressBar.getMax() + 2);
progressBar.setVisibility(View.VISIBLE);
}
}
}
};
public synchronized void enqueueMessage(ClientToServerMessage<?> message, Runnable callback) {
if (!pendingMessages.contains(message)) {
pendingMessages.add(message);
@ -172,6 +200,51 @@ public class ActFmSyncThread {
synchronized(monitor) {
monitor.notifyAll();
}
if (activity != null) {
activity.runOnUiThread(enqueueMessageProgressRunnable);
}
}
}
public void setProgressBar(Activity activity, ProgressBar progressBar) {
synchronized(progressBarLock) {
int oldProgress = progressBar.getProgress();
int oldMax = progressBar.getMax();
this.activity = activity;
this.progressBar = progressBar;
if (this.activity != null && this.progressBar != null) {
this.progressBar.setMax(oldMax);
this.progressBar.setProgress(oldProgress);
if (oldProgress < oldMax && oldMax != 0) {
this.progressBar.setVisibility(View.VISIBLE);
} else {
this.progressBar.setVisibility(View.GONE);
}
}
}
}
private final Runnable incrementProgressRunnable = new Runnable() {
@Override
public void run() {
synchronized(progressBarLock) {
if (progressBar != null) {
progressBar.incrementProgressBy(1);
if (progressBar.getProgress() == progressBar.getMax()) {
progressBar.setMax(0);
progressBar.setVisibility(View.GONE);
}
}
}
}
};
private void incrementProgress() {
synchronized (progressBarLock) {
if (activity != null) {
activity.runOnUiThread(incrementProgressRunnable);
}
}
}
@ -224,33 +297,41 @@ public class ActFmSyncThread {
ClientToServerMessage<?> message = pendingMessages.remove(0);
if (message != null)
messageBatch.add(message);
incrementProgress();
}
if (!messageBatch.isEmpty() && checkForToken()) {
JSONArray payload = new JSONArray();
MultipartEntity entity = new MultipartEntity();
for (ClientToServerMessage<?> message : messageBatch) {
JSONObject serialized = message.serializeToJSON();
JSONObject serialized = message.serializeToJSON(entity);
if (serialized != null) {
payload.put(serialized);
syncLog("Sending: " + serialized);
} else {
incrementProgress(); // Don't let failed serialization mess up progress bar
}
}
if (payload.length() == 0)
if (payload.length() == 0) {
messageBatch.clear();
continue;
}
try {
JSONObject response = actFmInvoker.postSync(payload, token);
JSONObject response = actFmInvoker.postSync(payload, entity, token);
// process responses
String time = response.optString("time");
JSONArray serverMessagesJson = response.optJSONArray("messages");
if (serverMessagesJson != null) {
setWidgetSuppression(true);
for (int i = 0; i < serverMessagesJson.length(); i++) {
JSONObject serverMessageJson = serverMessagesJson.optJSONObject(i);
if (serverMessageJson != null) {
ServerToClientMessage serverMessage = ServerToClientMessage.instantiateMessage(serverMessageJson);
if (serverMessage != null) {
syncLog("Processing server message of type " + serverMessage.getClass().getSimpleName());
serverMessage.processMessage();
serverMessage.processMessage(time);
} else {
syncLog("Unable to instantiate message " + serverMessageJson.toString());
}
@ -259,6 +340,7 @@ public class ActFmSyncThread {
JSONArray errors = response.optJSONArray("errors");
boolean errorsExist = (errors != null && errors.length() > 0);
replayOutstandingChanges(errorsExist);
setWidgetSuppression(false);
}
batchSize = Math.min(batchSize, messageBatch.size()) * 2;
@ -267,29 +349,27 @@ public class ActFmSyncThread {
batchSize = Math.max(batchSize / 2, 1);
}
boolean didDefaultRefreshThisLoop = false;
Set<Runnable> callbacksExecutedThisLoop = new HashSet<Runnable>();
for (ClientToServerMessage<?> message : messageBatch) {
incrementProgress();
try {
Runnable r = pendingCallbacks.remove(message);
if (r != null) {
if (r == DEFAULT_REFRESH_RUNNABLE) {
if (didDefaultRefreshThisLoop)
continue;
didDefaultRefreshThisLoop = true;
}
if (r != null && !callbacksExecutedThisLoop.contains(r)) {
r.run();
callbacksExecutedThisLoop.add(r);
}
} catch (Exception e) {
Log.e(ERROR_TAG, "Unexpected exception executing sync callback", e);
}
}
messageBatch = new LinkedList<ClientToServerMessage<?>>();
messageBatch.clear();
}
}
} catch (Exception e) {
// In the worst case, restart thread if something goes wrong
Log.e(ERROR_TAG, "Unexpected sync thread exception", e);
Crittercism.logHandledException(e);
thread = null;
startSyncThread();
}
@ -309,6 +389,18 @@ public class ActFmSyncThread {
return isTimeForBackgroundSync;
}
private void setWidgetSuppression(boolean suppress) {
long date = suppress ? DateUtilities.now() : 0;
TasksWidget.suppressUpdateFlag = date;
if (date == 0) {
Context context = ContextManager.getContext();
if (context != null) {
TasksWidget.updateWidgets(context);
}
}
}
public void repopulateQueueFromOutstandingTables() {
syncLog("Constructing queue from outstanding tables"); //$NON-NLS-1$
constructChangesHappenedFromOutstandingTable(Task.class, taskDao, taskOutstandingDao);

@ -6,9 +6,7 @@
package com.todoroo.astrid.actfm.sync;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicInteger;
import org.json.JSONException;
import org.json.JSONObject;
import com.timsu.astrid.GCMIntentService;
@ -73,8 +71,6 @@ public class ActFmSyncV2Provider extends SyncV2Provider {
return actFmPreferenceService.isLoggedIn();
}
private static final String LAST_FEATURED_TAG_FETCH_TIME = "actfm_last_featuredTag"; //$NON-NLS-1$
// --- synchronize active tasks
@Override
@ -83,18 +79,10 @@ public class ActFmSyncV2Provider extends SyncV2Provider {
new Thread(new Runnable() {
public void run() {
callback.started();
callback.incrementMax(160);
final AtomicInteger finisher = new AtomicInteger(1);
updateUserStatus();
ActFmSyncThread.getInstance().setTimeForBackgroundSync(true);
startFeaturedListFetcher(callback, finisher);
callback.incrementProgress(50);
}
}).start();
}
@ -133,32 +121,6 @@ public class ActFmSyncV2Provider extends SyncV2Provider {
}
}
/** fetch changes to tags */
private void startFeaturedListFetcher(final SyncResultCallback callback,
final AtomicInteger finisher) {
new Thread(new Runnable() {
@Override
public void run() {
int time = Preferences.getInt(LAST_FEATURED_TAG_FETCH_TIME, 0);
try {
if (Preferences.getBoolean(R.string.p_show_featured_lists, false)) {
time = actFmSyncService.fetchFeaturedLists(time);
Preferences.setInt(LAST_FEATURED_TAG_FETCH_TIME, time);
}
} catch (JSONException e) {
handler.handleException("actfm-sync", e, e.toString()); //$NON-NLS-1$
} catch (IOException e) {
handler.handleException("actfm-sync", e, e.toString()); //$NON-NLS-1$
} finally {
callback.incrementProgress(20);
if(finisher.decrementAndGet() == 0) {
finishSync(callback);
}
}
}
}).start();
}
// --- synchronize list
@Override
public void synchronizeList(Object list, final boolean manual,

@ -292,6 +292,8 @@ public class AstridNewSyncMigrator {
attachment.putTransitory(SyncFlags.ACTFM_SUPPRESS_OUTSTANDING_ENTRIES, true);
}
if (!ActFmPreferenceService.isPremiumUser())
attachment.putTransitory(SyncFlags.ACTFM_SUPPRESS_OUTSTANDING_ENTRIES, true);
taskAttachmentDao.createNew(attachment);
}
@ -315,9 +317,7 @@ public class AstridNewSyncMigrator {
TaskListMetadata tlm = new TaskListMetadata();
tlm.setValue(TaskListMetadata.FILTER, TaskListMetadata.FILTER_ID_ALL);
tlm.setValue(TaskListMetadata.TASK_IDS, activeTasksOrder);
tlm.putTransitory(SyncFlags.ACTFM_SUPPRESS_OUTSTANDING_ENTRIES, true);
if (taskListMetadataDao.update(TaskListMetadata.FILTER.eq(TaskListMetadata.FILTER_ID_ALL), tlm) <= 0) {
tlm.putTransitory(SyncFlags.ACTFM_SUPPRESS_OUTSTANDING_ENTRIES, true);
taskListMetadataDao.createNew(tlm);
}
@ -330,9 +330,7 @@ public class AstridNewSyncMigrator {
tlm.setValue(TaskListMetadata.FILTER, TaskListMetadata.FILTER_ID_TODAY);
tlm.setValue(TaskListMetadata.TASK_IDS, todayTasksOrder);
tlm.putTransitory(SyncFlags.ACTFM_SUPPRESS_OUTSTANDING_ENTRIES, true);
if (taskListMetadataDao.update(TaskListMetadata.FILTER.eq(TaskListMetadata.FILTER_ID_TODAY), tlm) <= 0) {
tlm.putTransitory(SyncFlags.ACTFM_SUPPRESS_OUTSTANDING_ENTRIES, true);
taskListMetadataDao.createNew(tlm);
}
@ -404,6 +402,16 @@ public class AstridNewSyncMigrator {
Log.e(LOG_TAG, "Error validating task to tag metadata", e);
}
// --------------
// Delete all featured list data
// --------------
try {
tagDataDao.deleteWhere(Functions.bitwiseAnd(TagData.FLAGS, TagData.FLAG_FEATURED).gt(0));
} catch (Exception e) {
Log.e(LOG_TAG, "Error deleting featured list data", e);
}
// --------------
// Finally, create oustanding entries for tags on unsynced tasks
// --------------

@ -9,7 +9,7 @@ import com.todoroo.astrid.actfm.sync.messages.ClientToServerMessage;
public class SyncDatabaseListener<MTYPE extends AbstractModel> implements ModelUpdateListener<MTYPE> {
private final ModelType modelType;
private final ActFmSyncThread actFmSyncThread;
protected final ActFmSyncThread actFmSyncThread;
public SyncDatabaseListener(ActFmSyncThread actFmSyncThread, ModelType modelType) {
this.actFmSyncThread = actFmSyncThread;
this.modelType = modelType;

@ -0,0 +1,133 @@
package com.todoroo.astrid.actfm.sync;
import android.app.Activity;
import android.app.Dialog;
import android.content.Intent;
import android.util.TypedValue;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import com.timsu.astrid.R;
import com.todoroo.andlib.utility.DateUtilities;
import com.todoroo.andlib.utility.Preferences;
import com.todoroo.astrid.actfm.ActFmLoginActivity;
import com.todoroo.astrid.core.PluginServices;
public class SyncUpgradePrompt {
private static final String P_SYNC_UPGRADE_PROMPT = "p_sync_upgr_prompt"; //$NON-NLS-1$
private static long lastPromptDate = -1;
public static void showSyncUpgradePrompt(final Activity activity) {
if (lastPromptDate == -1)
lastPromptDate = Preferences.getLong(P_SYNC_UPGRADE_PROMPT, 0L);
Dialog d = null;
if (DateUtilities.now() - lastPromptDate > DateUtilities.ONE_WEEK * 3) {
if (!PluginServices.getActFmPreferenceService().isLoggedIn()) {
if (PluginServices.getGtasksPreferenceService().isLoggedIn()) {
// Logged into google but not astrid
d = getDialog(activity, R.string.sync_upgr_gtasks_only_title, R.string.sync_upgr_gtasks_only_body,
R.string.sync_upgr_gtasks_only_btn1, new Runnable() {
@Override
public void run() {
activity.startActivity(new Intent(activity, ActFmLoginActivity.class));
}
},
R.string.sync_upgr_gtasks_only_btn2,
null);
} else {
// Logged into neither
d = getDialog(activity, R.string.sync_upgr_neither_title, R.string.sync_upgr_neither_body,
R.string.sync_upgr_neither_btn1, new Runnable() {
@Override
public void run() {
activity.startActivity(new Intent(activity, ActFmLoginActivity.class));
}
});
}
setLastPromptDate(DateUtilities.now());
} else if (PluginServices.getGtasksPreferenceService().isLoggedIn()) {
// Logged into both
d = getDialog(activity, R.string.sync_upgr_both_title, R.string.sync_upgr_both_body,
R.string.sync_upgr_both_btn1,
null,
R.string.sync_upgr_both_btn2, new Runnable() {
@Override
public void run() {
new ActFmSyncV2Provider().signOut();
Toast.makeText(activity, R.string.sync_upgr_logged_out, Toast.LENGTH_LONG).show();
}
});
setLastPromptDate(Long.MAX_VALUE);
} else {
// Logged into just astrid--don't need to show prompts anymore
setLastPromptDate(Long.MAX_VALUE);
}
}
if (d != null)
d.show();
}
private static void setLastPromptDate(long date) {
lastPromptDate = date;
Preferences.setLong(P_SYNC_UPGRADE_PROMPT, lastPromptDate);
}
private static Dialog getDialog(Activity activity, int title, int body, Object... buttonsAndListeners) {
final Dialog d = new Dialog(activity, R.style.ReminderDialog);
d.setContentView(R.layout.astrid_reminder_view_portrait);
((TextView) d.findViewById(R.id.reminder_title)).setText(title);
((TextView) d.findViewById(R.id.reminder_message)).setText(body);
d.findViewById(R.id.dismiss).setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
d.dismiss();
}
});
d.findViewById(R.id.reminder_complete).setVisibility(View.GONE);
TypedValue tv = new TypedValue();
activity.getTheme().resolveAttribute(R.attr.asThemeTextColor, tv, false);
int button1 = (Integer) buttonsAndListeners[0];
final Runnable listener1 = (Runnable) buttonsAndListeners[1];
Button b1 = (Button) d.findViewById(R.id.reminder_edit);
b1.setText(button1);
b1.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
d.dismiss();
if (listener1 != null)
listener1.run();
}
});
b1.setBackgroundColor(activity.getResources().getColor(tv.data));
if (buttonsAndListeners.length < 3) {
d.findViewById(R.id.reminder_snooze).setVisibility(View.GONE);
} else {
int button2 = (Integer) buttonsAndListeners[2];
final Runnable listener2 = (Runnable) buttonsAndListeners[3];
Button b2 = (Button) d.findViewById(R.id.reminder_snooze);
b2.setText(button2);
b2.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
d.dismiss();
if (listener2 != null)
listener2.run();
}
});
b2.setBackgroundColor(activity.getResources().getColor(tv.data));
}
return d;
}
}

@ -18,7 +18,7 @@ public class TaskListMetadataSyncDatabaseListener extends SyncDatabaseListener<T
if (model.getSetValues().containsKey(TaskListMetadata.TASK_IDS.name))
waitingPool.enqueueMessage(message);
else
super.enqueueMessage(model, message);
actFmSyncThread.enqueueMessage(message, ActFmSyncThread.DEFAULT_REFRESH_RUNNABLE);
}
}

@ -36,7 +36,7 @@ public class AcknowledgeChange extends ServerToClientMessage {
}
@Override
public void processMessage() {
public void processMessage(String serverTime) {
JSONArray idsArray = json.optJSONArray("ids"); //$NON-NLS-1$
if (idsArray != null && dao != null) {
ArrayList<Long> idsList = new ArrayList<Long>();

@ -1,5 +1,6 @@
package com.todoroo.astrid.actfm.sync.messages;
import org.apache.http.entity.mime.MultipartEntity;
import org.json.JSONException;
import org.json.JSONObject;
@ -34,7 +35,7 @@ public class BriefMe<TYPE extends RemoteModel> extends ClientToServerMessage<TYP
}
@Override
protected boolean serializeExtrasToJSON(JSONObject serializeTo) throws JSONException {
protected boolean serializeExtrasToJSON(JSONObject serializeTo, MultipartEntity entity) throws JSONException {
if (extraParameters != null && extraParameters.length > 0) {
for (int i = 0; i < extraParameters.length - 1; i++) {
try {

@ -3,7 +3,10 @@ package com.todoroo.astrid.actfm.sync.messages;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.http.entity.mime.MultipartEntity;
import org.apache.http.entity.mime.content.FileBody;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
@ -11,12 +14,12 @@ import org.json.JSONObject;
import android.text.TextUtils;
import android.util.Log;
import com.crittercism.app.Crittercism;
import com.todoroo.andlib.data.Property;
import com.todoroo.andlib.data.Property.PropertyVisitor;
import com.todoroo.andlib.data.TodorooCursor;
import com.todoroo.andlib.sql.Order;
import com.todoroo.andlib.sql.Query;
import com.todoroo.andlib.utility.AndroidUtilities;
import com.todoroo.andlib.utility.DateUtilities;
import com.todoroo.astrid.actfm.sync.ActFmPreferenceService;
import com.todoroo.astrid.actfm.sync.ActFmSyncThread.ModelType;
@ -77,12 +80,14 @@ public class ChangesHappened<TYPE extends RemoteModel, OE extends OutstandingEnt
this.outstandingDao = outstandingDao;
this.changes = new ArrayList<OE>();
if (!foundEntity) // Stop sending changes for entities that don't exist anymore
outstandingDao.deleteWhere(OutstandingEntry.ENTITY_ID_PROPERTY.eq(id));
}
@Override
protected boolean serializeExtrasToJSON(JSONObject serializeTo) throws JSONException {
protected boolean serializeExtrasToJSON(JSONObject serializeTo, MultipartEntity entity) throws JSONException {
// Process changes list and serialize to JSON
JSONArray changesJson = changesToJSON();
JSONArray changesJson = changesToJSON(entity);
if (changesJson == null || changesJson.length() == 0)
return false;
serializeTo.put(CHANGES_KEY, changesJson);
@ -98,11 +103,12 @@ public class ChangesHappened<TYPE extends RemoteModel, OE extends OutstandingEnt
return changes;
}
private JSONArray changesToJSON() {
private JSONArray changesToJSON(MultipartEntity entity) {
if (!RemoteModel.NO_UUID.equals(uuid))
populateChanges();
JSONArray array = new JSONArray();
AtomicInteger uploadCounter = new AtomicInteger();
PropertyToJSONVisitor visitor = new PropertyToJSONVisitor();
for (OE change : changes) {
try {
@ -127,9 +133,12 @@ public class ChangesHappened<TYPE extends RemoteModel, OE extends OutstandingEnt
JSONObject fileJson = getFileJson(change.getValue(OutstandingEntry.VALUE_STRING_PROPERTY));
if (fileJson == null) {
PluginServices.getTaskAttachmentDao().delete(id);
continue;
} else {
changeJson.put("value", fileJson);
PluginServices.getTaskAttachmentOutstandingDao().deleteWhere(TaskAttachmentOutstanding.ENTITY_ID_PROPERTY.eq(id));
return null;
} else { // JSON has valid file path
String name = addToEntityFromFileJson(entity, fileJson, uploadCounter);
if (name != null)
changeJson.put("value", name);
}
} else {
Property<?> localProperty = NameMaps.localColumnNameToProperty(table, localColumn);
@ -140,7 +149,22 @@ public class ChangesHappened<TYPE extends RemoteModel, OE extends OutstandingEnt
if (serverColumn == null)
throw new RuntimeException("No server column found for local column " + localColumn + " in table " + table);
changeJson.put("value", localProperty.accept(visitor, change));
Object value = localProperty.accept(visitor, change);
if (!validateValue(localProperty, value))
return null;
if (value == null)
changeJson.put("value", JSONObject.NULL);
else {
if (localProperty.checkFlag(Property.PROP_FLAG_PICTURE) && value instanceof JSONObject) {
JSONObject json = (JSONObject) value;
String name = addToEntityFromFileJson(entity, json, uploadCounter);
if (name != null)
changeJson.put("value", name);
} else {
changeJson.put("value", value);
}
}
}
changeJson.put("column", serverColumn);
@ -151,11 +175,27 @@ public class ChangesHappened<TYPE extends RemoteModel, OE extends OutstandingEnt
array.put(changeJson);
} catch (JSONException e) {
Log.e(ERROR_TAG, "Error writing change to JSON", e);
Crittercism.logHandledException(e);
}
}
return array;
}
private String addToEntityFromFileJson(MultipartEntity entity, JSONObject json, AtomicInteger uploadCounter) {
if (json.has("path")) {
String path = json.optString("path");
String name = String.format("upload-%s-%s-%d", table, uuid, uploadCounter.get());
String type = json.optString("type");
File f = new File(path);
if (f.exists()) {
json.remove("path");
entity.addPart(name, new FileBody(f, type));
return name;
}
}
return null;
}
protected void populateChanges() {
TodorooCursor<OE> cursor = outstandingDao.query(Query.select(DaoReflectionHelpers.getModelProperties(outstandingClass))
.where(OutstandingEntry.ENTITY_ID_PROPERTY.eq(id)).orderBy(Order.asc(OutstandingEntry.CREATED_AT_PROPERTY)));
@ -176,6 +216,16 @@ public class ChangesHappened<TYPE extends RemoteModel, OE extends OutstandingEnt
}
}
// Return false if value is detected to be something that would definitely cause a server error
// (e.g. empty task title, etc.)
private boolean validateValue(Property<?> property, Object value) {
if (Task.TITLE.equals(property)) {
if (!(value instanceof String) || TextUtils.isEmpty((String) value))
return false;
}
return true;
}
private JSONObject getFileJson(String value) {
try {
JSONObject obj = new JSONObject(value);
@ -186,12 +236,6 @@ public class ChangesHappened<TYPE extends RemoteModel, OE extends OutstandingEnt
if (!f.exists())
return null;
String encodedFile = AndroidUtilities.encodeBase64File(f);
if (TextUtils.isEmpty(encodedFile))
return null;
obj.remove("path");
obj.put("data", encodedFile);
return obj;
} catch (JSONException e) {
return null;
@ -247,15 +291,18 @@ public class ChangesHappened<TYPE extends RemoteModel, OE extends OutstandingEnt
String value = getAsString(data);
if (RemoteModel.NO_UUID.equals(value) && property.checkFlag(Property.PROP_FLAG_USER_ID))
return ActFmPreferenceService.userId();
if (property.checkFlag(Property.PROP_FLAG_JSON))
if (property.checkFlag(Property.PROP_FLAG_JSON)) {
if (TextUtils.isEmpty(value))
return null;
try {
if (value != null && value.startsWith("["))
return new JSONArray(value);
else
return new JSONObject(value);
} catch (JSONException e) {
//
return null;
}
}
return value;
}

@ -1,8 +1,10 @@
package com.todoroo.astrid.actfm.sync.messages;
import org.apache.http.entity.mime.MultipartEntity;
import org.json.JSONException;
import org.json.JSONObject;
import com.crittercism.app.Crittercism;
import com.todoroo.andlib.data.AbstractModel;
import com.todoroo.andlib.data.Table;
import com.todoroo.andlib.utility.DateUtilities;
@ -18,6 +20,7 @@ public abstract class ClientToServerMessage<TYPE extends RemoteModel> {
protected final long id;
protected final String uuid;
protected final long pushedAt;
protected final boolean foundEntity;
public static final String TYPE_KEY = "type";
public static final String TABLE_KEY = "table";
@ -30,6 +33,7 @@ public abstract class ClientToServerMessage<TYPE extends RemoteModel> {
this.table = NameMaps.getServerNameForTable(tableClass);
this.uuid = uuid;
this.pushedAt = pushedAt;
this.foundEntity = true;
this.id = AbstractModel.NO_ID;
}
@ -40,6 +44,7 @@ public abstract class ClientToServerMessage<TYPE extends RemoteModel> {
this.table = NameMaps.getServerNameForTable(tableClass);
TYPE entity = getEntity(id, modelDao);
this.foundEntity = entity != null;
if (entity == null) {
this.uuid = RemoteModel.NO_UUID;
this.pushedAt = 0;
@ -61,7 +66,7 @@ public abstract class ClientToServerMessage<TYPE extends RemoteModel> {
return pushedAt;
}
public final JSONObject serializeToJSON() {
public final JSONObject serializeToJSON(MultipartEntity entity) {
JSONObject json = new JSONObject();
try {
json.put(TYPE_KEY, getTypeString());
@ -69,11 +74,12 @@ public abstract class ClientToServerMessage<TYPE extends RemoteModel> {
json.put(UUID_KEY, uuid);
String dateValue = DateUtilities.timeToIso8601(pushedAt, true);
json.put(PUSHED_AT_KEY, dateValue != null ? dateValue : 0);
if (serializeExtrasToJSON(json))
if (serializeExtrasToJSON(json, entity))
return json;
else
return null;
} catch (JSONException e) {
Crittercism.logHandledException(e);
return null;
}
}
@ -109,6 +115,6 @@ public abstract class ClientToServerMessage<TYPE extends RemoteModel> {
return true;
}
protected abstract boolean serializeExtrasToJSON(JSONObject serializeTo) throws JSONException;
protected abstract boolean serializeExtrasToJSON(JSONObject serializeTo, MultipartEntity entity) throws JSONException;
protected abstract String getTypeString();
}

@ -17,10 +17,10 @@ import com.todoroo.astrid.data.RemoteModel;
@SuppressWarnings("nls")
public class ConstructOutstandingTableFromMasterTable<TYPE extends RemoteModel, OE extends OutstandingEntry<TYPE>> {
private final String table;
private final RemoteModelDao<TYPE> dao;
private final OutstandingEntryDao<OE> outstandingDao;
private final LongProperty createdAtProperty;
protected final String table;
protected final RemoteModelDao<TYPE> dao;
protected final OutstandingEntryDao<OE> outstandingDao;
protected final LongProperty createdAtProperty;
public ConstructOutstandingTableFromMasterTable(String table, RemoteModelDao<TYPE> dao,
OutstandingEntryDao<OE> outstandingDao, LongProperty createdAtProperty) {
@ -30,24 +30,31 @@ public class ConstructOutstandingTableFromMasterTable<TYPE extends RemoteModel,
this.createdAtProperty = createdAtProperty;
}
protected void extras(long itemId, long createdAt) {
// Subclasses can override
}
public void execute() {
Property<?>[] syncableProperties = NameMaps.syncableProperties(table);
TodorooCursor<TYPE> items = dao.query(Query.select(AndroidUtilities.addToArray(syncableProperties, AbstractModel.ID_PROPERTY, RemoteModel.UUID_PROPERTY)));
try {
OE oe = outstandingDao.getModelClass().newInstance();
for (items.moveToFirst(); !items.isAfterLast(); items.moveToNext()) {
long createdAt;
if (createdAtProperty != null)
createdAt = items.get(createdAtProperty);
else
createdAt = DateUtilities.now();
long itemId = items.get(AbstractModel.ID_PROPERTY);
for (Property<?> p : syncableProperties) {
oe.clear();
oe.setValue(OutstandingEntry.ENTITY_ID_PROPERTY, itemId);
oe.setValue(OutstandingEntry.COLUMN_STRING_PROPERTY, p.name);
oe.setValue(OutstandingEntry.VALUE_STRING_PROPERTY, items.get(p).toString());
if (createdAtProperty != null)
oe.setValue(OutstandingEntry.CREATED_AT_PROPERTY, items.get(createdAtProperty));
else
oe.setValue(OutstandingEntry.CREATED_AT_PROPERTY, DateUtilities.now());
oe.setValue(OutstandingEntry.CREATED_AT_PROPERTY, createdAt);
outstandingDao.createNew(oe);
}
extras(itemId, createdAt);
}
} catch (IllegalAccessException e) {
Log.e("ConstructOutstanding", "Error instantiating outstanding model class", e);

@ -0,0 +1,50 @@
package com.todoroo.astrid.actfm.sync.messages;
import com.todoroo.andlib.data.Property.LongProperty;
import com.todoroo.andlib.data.TodorooCursor;
import com.todoroo.andlib.sql.Criterion;
import com.todoroo.andlib.sql.Query;
import com.todoroo.astrid.dao.MetadataDao;
import com.todoroo.astrid.dao.MetadataDao.MetadataCriteria;
import com.todoroo.astrid.dao.OutstandingEntryDao;
import com.todoroo.astrid.dao.RemoteModelDao;
import com.todoroo.astrid.data.Metadata;
import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.data.TaskOutstanding;
import com.todoroo.astrid.tags.TaskToTagMetadata;
public class ConstructTaskOutstandingTableFromMasterTable extends ConstructOutstandingTableFromMasterTable<Task, TaskOutstanding> {
private final MetadataDao metadataDao;
public ConstructTaskOutstandingTableFromMasterTable(String table, RemoteModelDao<Task> dao, OutstandingEntryDao<TaskOutstanding> outstandingDao, MetadataDao metadataDao, LongProperty createdAtProperty) {
super(table, dao, outstandingDao, createdAtProperty);
this.metadataDao = metadataDao;
}
@Override
protected void extras(long itemId, long createdAt) {
super.extras(itemId, createdAt);
TodorooCursor<Metadata> tagMetadata = metadataDao.query(Query.select(Metadata.PROPERTIES)
.where(Criterion.and(MetadataCriteria.byTaskAndwithKey(itemId, TaskToTagMetadata.KEY), Metadata.DELETION_DATE.eq(0))));
Metadata m = new Metadata();
try {
for (tagMetadata.moveToFirst(); !tagMetadata.isAfterLast(); tagMetadata.moveToNext()) {
m.clear();
m.readFromCursor(tagMetadata);
if (m.containsNonNullValue(TaskToTagMetadata.TAG_UUID)) {
TaskOutstanding oe = new TaskOutstanding();
oe.setValue(TaskOutstanding.ENTITY_ID_PROPERTY, itemId);
oe.setValue(TaskOutstanding.COLUMN_STRING, NameMaps.TAG_ADDED_COLUMN);
oe.setValue(TaskOutstanding.VALUE_STRING, m.getValue(TaskToTagMetadata.TAG_UUID));
oe.setValue(TaskOutstanding.CREATED_AT, createdAt);
outstandingDao.createNew(oe);
}
}
} finally {
tagMetadata.close();
}
}
}

@ -13,7 +13,7 @@ public class Debug extends ServerToClientMessage {
@Override
@SuppressWarnings("nls")
public void processMessage() {
public void processMessage(String serverTime) {
String message = json.optString("message");
if (!TextUtils.isEmpty(message))
Log.w("actfm-debug-message", message);

@ -9,7 +9,7 @@ public class DoubleCheck extends ServerToClientMessage {
}
@Override
public void processMessage() {
public void processMessage(String serverTime) {
// TODO Auto-generated method stub
}

@ -1,5 +1,6 @@
package com.todoroo.astrid.actfm.sync.messages;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Iterator;
@ -20,13 +21,13 @@ import com.todoroo.andlib.utility.Preferences;
import com.todoroo.astrid.actfm.sync.ActFmInvoker;
import com.todoroo.astrid.core.PluginServices;
import com.todoroo.astrid.dao.HistoryDao;
import com.todoroo.astrid.dao.MetadataDao.MetadataCriteria;
import com.todoroo.astrid.dao.RemoteModelDao;
import com.todoroo.astrid.dao.TagMetadataDao;
import com.todoroo.astrid.dao.TaskListMetadataDao;
import com.todoroo.astrid.dao.UserActivityDao;
import com.todoroo.astrid.data.History;
import com.todoroo.astrid.data.Metadata;
import com.todoroo.astrid.data.MetadataApiDao.MetadataCriteria;
import com.todoroo.astrid.data.RemoteModel;
import com.todoroo.astrid.data.SyncFlags;
import com.todoroo.astrid.data.TagData;
@ -65,7 +66,7 @@ public class MakeChanges<TYPE extends RemoteModel> extends ServerToClientMessage
return model;
}
private static <T extends RemoteModel> void saveOrUpdateModelAfterChanges(RemoteModelDao<T> dao, T model, String oldUuid, String uuid, Criterion orCriterion) {
private static <T extends RemoteModel> void saveOrUpdateModelAfterChanges(RemoteModelDao<T> dao, T model, String oldUuid, String uuid, String serverTime, Criterion orCriterion) {
Criterion uuidCriterion;
if (oldUuid == null)
uuidCriterion = RemoteModel.UUID_PROPERTY.eq(uuid);
@ -76,6 +77,16 @@ public class MakeChanges<TYPE extends RemoteModel> extends ServerToClientMessage
uuidCriterion = Criterion.or(uuidCriterion, orCriterion);
if (model.getSetValues() != null && model.getSetValues().size() > 0) {
long pushedAt;
try {
pushedAt = DateUtilities.parseIso8601(serverTime);
} catch (ParseException e) {
pushedAt = 0;
}
if (pushedAt > 0)
model.setValue(RemoteModel.PUSHED_AT_PROPERTY, pushedAt);
model.putTransitory(SyncFlags.ACTFM_SUPPRESS_OUTSTANDING_ENTRIES, true);
if (dao.update(uuidCriterion, model) <= 0) { // If update doesn't update rows. create a new model
model.putTransitory(SyncFlags.ACTFM_SUPPRESS_OUTSTANDING_ENTRIES, true);
@ -85,7 +96,7 @@ public class MakeChanges<TYPE extends RemoteModel> extends ServerToClientMessage
}
@Override
public void processMessage() {
public void processMessage(String serverTime) {
JSONObject changes = json.optJSONObject("changes");
String uuid = json.optString("uuid");
if (changes != null && !TextUtils.isEmpty(uuid)) {
@ -105,7 +116,7 @@ public class MakeChanges<TYPE extends RemoteModel> extends ServerToClientMessage
if (model.getSetValues() != null && !model.getSetValues().containsKey(uuidProperty.name))
model.setValue(uuidProperty, uuid);
saveOrUpdateModelAfterChanges(dao, model, oldUuid, uuid, getMatchCriterion(model));
saveOrUpdateModelAfterChanges(dao, model, oldUuid, uuid, serverTime, getMatchCriterion(model));
afterSaveChanges(changes, model, uuid, oldUuid);
} catch (IllegalAccessException e) {

@ -127,7 +127,7 @@ public class NameMaps {
putTaskPropertyToServerName(Task.TITLE, "title", true);
putTaskPropertyToServerName(Task.IMPORTANCE, "importance", true);
putTaskPropertyToServerName(Task.DUE_DATE, "due", true);
putTaskPropertyToServerName(Task.HIDE_UNTIL, "hide_until", false);
putTaskPropertyToServerName(Task.HIDE_UNTIL, "hide_until", true);
putTaskPropertyToServerName(Task.CREATION_DATE, "created_at", true);
putTaskPropertyToServerName(Task.COMPLETION_DATE, "completed_at", true);
putTaskPropertyToServerName(Task.RECURRENCE, "repeat", true);

@ -42,7 +42,7 @@ public class NowBriefed<TYPE extends RemoteModel> extends ServerToClientMessage
}
@Override
public void processMessage() {
public void processMessage(String serverTime) {
if (pushedAt > 0) {
if (TextUtils.isEmpty(uuid)) {
if (!TextUtils.isEmpty(taskId)) {

@ -1,5 +1,6 @@
package com.todoroo.astrid.actfm.sync.messages;
import org.apache.http.entity.mime.MultipartEntity;
import org.json.JSONException;
import org.json.JSONObject;
@ -13,7 +14,7 @@ public class RequestDoubleCheck<TYPE extends RemoteModel> extends ClientToServer
}
@Override
protected boolean serializeExtrasToJSON(JSONObject serializeTo) throws JSONException {
protected boolean serializeExtrasToJSON(JSONObject serializeTo, MultipartEntity entity) throws JSONException {
// No extras
return true;
}

@ -13,7 +13,7 @@ import com.todoroo.astrid.data.UserActivity;
@SuppressWarnings("nls")
public abstract class ServerToClientMessage {
public abstract void processMessage();
public abstract void processMessage(String serverTime);
public static final String TYPE_MAKE_CHANGES = "MakeChanges";
public static final String TYPE_NOW_BRIEFED = "NowBriefed";

@ -11,11 +11,8 @@ import com.todoroo.astrid.data.TaskListMetadataOutstanding;
public class TaskListMetadataChangesHappened extends ChangesHappened<TaskListMetadata, TaskListMetadataOutstanding> {
private final Throwable throwable;
public TaskListMetadataChangesHappened(long id, Class<TaskListMetadata> modelClass, TaskListMetadataDao modelDao, TaskListMetadataOutstandingDao outstandingDao) {
super(id, modelClass, modelDao, outstandingDao);
throwable = new Throwable();
}
@Override

@ -20,7 +20,7 @@ public class UserData extends ServerToClientMessage {
@Override
@SuppressWarnings("nls")
public void processMessage() {
public void processMessage(String serverTime) {
String uuid = json.optString("uuid");
String email = json.optString("email");

@ -22,9 +22,9 @@ import com.todoroo.astrid.api.AstridFilterExposer;
import com.todoroo.astrid.api.Filter;
import com.todoroo.astrid.api.FilterListItem;
import com.todoroo.astrid.dao.MetadataDao.MetadataCriteria;
import com.todoroo.astrid.dao.TaskDao.TaskCriteria;
import com.todoroo.astrid.data.Metadata;
import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.data.TaskApiDao.TaskCriteria;
import com.todoroo.astrid.service.ThemeService;
import com.todoroo.astrid.tags.TaskToTagMetadata;

@ -54,8 +54,8 @@ import com.todoroo.astrid.api.MultipleSelectCriterion;
import com.todoroo.astrid.api.PermaSql;
import com.todoroo.astrid.api.TextInputCriterion;
import com.todoroo.astrid.dao.Database;
import com.todoroo.astrid.dao.TaskDao.TaskCriteria;
import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.data.TaskApiDao.TaskCriteria;
import com.todoroo.astrid.service.StatisticsService;
import com.todoroo.astrid.service.ThemeService;
import com.todoroo.astrid.utility.AstridPreferences;

@ -38,10 +38,10 @@ import com.todoroo.astrid.api.Filter;
import com.todoroo.astrid.api.FilterListItem;
import com.todoroo.astrid.api.PermaSql;
import com.todoroo.astrid.dao.StoreObjectDao;
import com.todoroo.astrid.dao.TaskDao.TaskCriteria;
import com.todoroo.astrid.data.Metadata;
import com.todoroo.astrid.data.StoreObject;
import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.data.TaskApiDao.TaskCriteria;
import com.todoroo.astrid.gtasks.GtasksPreferenceService;
import com.todoroo.astrid.service.TagDataService;
import com.todoroo.astrid.service.ThemeService;

@ -28,6 +28,7 @@ import com.todoroo.astrid.dao.UserActivityDao;
import com.todoroo.astrid.dao.UserActivityOutstandingDao;
import com.todoroo.astrid.dao.UserDao;
import com.todoroo.astrid.data.Metadata;
import com.todoroo.astrid.gtasks.GtasksPreferenceService;
import com.todoroo.astrid.service.AddOnService;
import com.todoroo.astrid.service.AstridDependencyInjector;
import com.todoroo.astrid.service.MetadataService;
@ -105,6 +106,9 @@ public final class PluginServices {
@Autowired
ActFmPreferenceService actFmPreferenceService;
@Autowired
GtasksPreferenceService gtasksPreferenceService;
private static volatile PluginServices instance;
static {
@ -212,6 +216,10 @@ public final class PluginServices {
return getInstance().actFmPreferenceService;
}
public static GtasksPreferenceService getGtasksPreferenceService() {
return getInstance().gtasksPreferenceService;
}
// -- helpers
/**

@ -23,9 +23,9 @@ import com.todoroo.andlib.sql.Query;
import com.todoroo.astrid.api.AstridApiConstants;
import com.todoroo.astrid.api.CustomFilterCriterion;
import com.todoroo.astrid.api.MultipleSelectCriterion;
import com.todoroo.astrid.dao.MetadataDao.MetadataCriteria;
import com.todoroo.astrid.dao.TaskDao;
import com.todoroo.astrid.data.Metadata;
import com.todoroo.astrid.data.MetadataApiDao.MetadataCriteria;
import com.todoroo.astrid.data.StoreObject;
import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.service.AstridDependencyInjector;

@ -24,6 +24,7 @@ import com.todoroo.andlib.sql.Functions;
import com.todoroo.andlib.sql.Join;
import com.todoroo.andlib.sql.Order;
import com.todoroo.andlib.sql.QueryTemplate;
import com.todoroo.astrid.actfm.sync.ActFmPreferenceService;
import com.todoroo.astrid.api.AstridApiConstants;
import com.todoroo.astrid.api.AstridFilterExposer;
import com.todoroo.astrid.api.Filter;
@ -31,9 +32,9 @@ import com.todoroo.astrid.api.FilterCategoryWithNewButton;
import com.todoroo.astrid.api.FilterListItem;
import com.todoroo.astrid.api.FilterWithCustomIntent;
import com.todoroo.astrid.api.PermaSql;
import com.todoroo.astrid.dao.MetadataDao.MetadataCriteria;
import com.todoroo.astrid.dao.TaskDao.TaskCriteria;
import com.todoroo.astrid.data.Metadata;
import com.todoroo.astrid.data.MetadataApiDao.MetadataCriteria;
import com.todoroo.astrid.data.StoreObject;
import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.service.AstridDependencyInjector;
@ -48,6 +49,7 @@ public class GtasksFilterExposer extends BroadcastReceiver implements AstridFilt
@Autowired private GtasksListService gtasksListService;
@Autowired private GtasksPreferenceService gtasksPreferenceService;
@Autowired private ActFmPreferenceService actFmPreferenceService;
static {
AstridDependencyInjector.initialize();
@ -92,8 +94,8 @@ public class GtasksFilterExposer extends BroadcastReceiver implements AstridFilt
private FilterListItem[] prepareFilters(Context context) {
DependencyInjectionService.getInstance().inject(this);
// if we aren't logged in, don't expose features
if(!gtasksPreferenceService.isLoggedIn())
// if we aren't logged in (or we are logged in to astrid.com), don't expose features
if(!gtasksPreferenceService.isLoggedIn() || actFmPreferenceService.isLoggedIn())
return null;
lists = gtasksListService.getLists();

@ -30,8 +30,8 @@ import com.todoroo.astrid.data.StoreObject;
import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.gtasks.sync.GtasksTaskContainer;
import com.todoroo.astrid.subtasks.OrderedMetadataListUpdater.OrderedListIterator;
import com.todoroo.astrid.sync.SyncMetadataService;
import com.todoroo.astrid.sync.SyncProviderUtilities;
import com.todoroo.astrid.utility.SyncMetadataService;
/**
* Service for working with GTasks metadata
@ -91,6 +91,7 @@ public final class GtasksMetadataService extends SyncMetadataService<GtasksTaskC
return;
cursor.moveToFirst();
remoteTask.task.setId(cursor.get(Metadata.TASK));
remoteTask.task.setUuid(taskDao.uuidFromLocalId(remoteTask.task.getId()));
remoteTask.gtaskMetadata = new Metadata(cursor);
} finally {
cursor.close();

@ -5,6 +5,7 @@
*/
package com.todoroo.astrid.gtasks;
import android.app.ProgressDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
@ -14,10 +15,15 @@ import com.todoroo.andlib.service.Autowired;
import com.todoroo.andlib.service.DependencyInjectionService;
import com.todoroo.andlib.utility.DialogUtilities;
import com.todoroo.astrid.actfm.sync.ActFmPreferenceService;
import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.gtasks.auth.GtasksLoginActivity;
import com.todoroo.astrid.gtasks.sync.GtasksSyncV2Provider;
import com.todoroo.astrid.gtasks.sync.GtasksSyncV2Provider.GtasksImportCallback;
import com.todoroo.astrid.gtasks.sync.GtasksSyncV2Provider.GtasksImportTuple;
import com.todoroo.astrid.sync.SyncProviderPreferences;
import com.todoroo.astrid.sync.SyncProviderUtilities;
import com.todoroo.astrid.sync.SyncResultCallbackAdapter;
import com.todoroo.astrid.tags.TagService;
/**
* Displays synchronization preferences and an action panel so users can
@ -30,6 +36,7 @@ public class GtasksPreferences extends SyncProviderPreferences {
@Autowired private GtasksPreferenceService gtasksPreferenceService;
@Autowired private ActFmPreferenceService actFmPreferenceService;
@Autowired private TagService tagService;
public GtasksPreferences() {
super();
@ -61,11 +68,67 @@ public class GtasksPreferences extends SyncProviderPreferences {
startLogin();
}
} else {
setResult(RESULT_CODE_SYNCHRONIZE);
finish();
syncOrImport();
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_LOGIN && resultCode == RESULT_OK) {
syncOrImport();
} else {
super.onActivityResult(requestCode, resultCode, data);
}
}
private void syncOrImport() {
if (actFmPreferenceService.isLoggedIn()) {
startBlockingImport();
} else {
setResultForSynchronize();
}
}
private void setResultForSynchronize() {
setResult(RESULT_CODE_SYNCHRONIZE);
finish();
}
private void startBlockingImport() {
final ProgressDialog pd = DialogUtilities.progressDialog(this, getString(R.string.gtasks_import_progress));
pd.setCancelable(false);
GtasksImportCallback callback = new GtasksImportCallback(new SyncResultCallbackAdapter() {/**/}) {
@Override
public void finished() {
super.finished();
for (GtasksImportTuple tuple : importConflicts) {
final GtasksImportTuple finalTuple = tuple;
String prompt = getString(R.string.gtasks_import_add_to_shared_list, tuple.tagName, tuple.taskName);
DialogUtilities.okCancelCustomDialog(GtasksPreferences.this,
getString(R.string.gtasks_import_dlg_title),
prompt,
R.string.gtasks_import_add_task_ok,
R.string.gtasks_import_add_task_cancel,
android.R.drawable.ic_dialog_alert,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Task task = new Task();
task.setId(finalTuple.taskId);
task.setUuid(finalTuple.taskUuid);
tagService.createLink(task, finalTuple.tagName, finalTuple.tagUuid);
}
},
null);
}
DialogUtilities.dismissDialog(GtasksPreferences.this, pd);
}
};
GtasksSyncV2Provider.getInstance().synchronizeActiveTasks(true, callback);
}
private void startLogin() {
Intent intent = new Intent(this, GtasksLoginActivity.class);
startActivityForResult(intent, REQUEST_LOGIN);

@ -20,6 +20,7 @@ import com.todoroo.andlib.service.DependencyInjectionService;
import com.todoroo.andlib.utility.AndroidUtilities;
import com.todoroo.andlib.utility.DateUtilities;
import com.todoroo.andlib.utility.Preferences;
import com.todoroo.astrid.actfm.sync.ActFmPreferenceService;
import com.todoroo.astrid.dao.MetadataDao;
import com.todoroo.astrid.dao.TaskDao;
import com.todoroo.astrid.data.Metadata;
@ -43,6 +44,7 @@ public final class GtasksSyncService {
@Autowired GtasksMetadataService gtasksMetadataService;
@Autowired TaskDao taskDao;
@Autowired GtasksPreferenceService gtasksPreferenceService;
@Autowired ActFmPreferenceService actFmPreferenceService;
public GtasksSyncService() {
DependencyInjectionService.getInstance().inject(this);
@ -104,6 +106,8 @@ public final class GtasksSyncService {
public void onModelUpdated(final Task model, boolean outstandingEntries) {
if(model.checkAndClearTransitory(SyncFlags.GTASKS_SUPPRESS_SYNC))
return;
if (actFmPreferenceService.isLoggedIn())
return;
if (gtasksPreferenceService.isOngoing() && !model.checkTransitory(TaskService.TRANS_REPEAT_COMPLETE)) //Don't try and sync changes that occur during a normal sync
return;
final ContentValues setValues = model.getSetValues();
@ -178,6 +182,8 @@ public final class GtasksSyncService {
return;
if (metadata.checkAndClearTransitory(SyncFlags.GTASKS_SUPPRESS_SYNC))
return;
if (actFmPreferenceService.isLoggedIn())
return;
if (!metadata.getValue(Metadata.KEY).equals(GtasksMetadata.METADATA_KEY)) //Don't care about non-gtasks metadata
return;
if (gtasksPreferenceService.isOngoing()) //Don't try and sync changes that occur during a normal sync
@ -192,6 +198,9 @@ public final class GtasksSyncService {
* Synchronize with server when data changes
*/
public void pushTaskOnSave(Task task, ContentValues values, GtasksInvoker invoker, boolean sleep) throws IOException {
if (actFmPreferenceService.isLoggedIn())
return;
if (sleep)
AndroidUtilities.sleepDeep(1000L); //Wait for metadata to be saved
@ -297,6 +306,8 @@ public final class GtasksSyncService {
}
public void pushMetadataOnSave(Metadata model, GtasksInvoker invoker) throws IOException {
if (actFmPreferenceService.isLoggedIn())
return;
AndroidUtilities.sleepDeep(1000L);
String taskId = model.getValue(GtasksMetadata.ID);

@ -29,12 +29,17 @@ import com.todoroo.andlib.utility.DateUtilities;
import com.todoroo.andlib.utility.Preferences;
import com.todoroo.astrid.actfm.sync.ActFmPreferenceService;
import com.todoroo.astrid.core.PluginServices;
import com.todoroo.astrid.dao.MetadataDao;
import com.todoroo.astrid.dao.MetadataDao.MetadataCriteria;
import com.todoroo.astrid.dao.StoreObjectDao;
import com.todoroo.astrid.dao.TagDataDao;
import com.todoroo.astrid.dao.TagMetadataDao;
import com.todoroo.astrid.dao.TaskDao;
import com.todoroo.astrid.data.Metadata;
import com.todoroo.astrid.data.RemoteModel;
import com.todoroo.astrid.data.StoreObject;
import com.todoroo.astrid.data.SyncFlags;
import com.todoroo.astrid.data.TagData;
import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.gtasks.GtasksList;
import com.todoroo.astrid.gtasks.GtasksListService;
@ -50,14 +55,17 @@ import com.todoroo.astrid.service.AstridDependencyInjector;
import com.todoroo.astrid.service.MetadataService;
import com.todoroo.astrid.service.StatisticsConstants;
import com.todoroo.astrid.service.StatisticsService;
import com.todoroo.astrid.service.SyncResultCallbackWrapper.WidgetUpdatingCallbackWrapper;
import com.todoroo.astrid.service.TaskService;
import com.todoroo.astrid.sync.SyncResultCallback;
import com.todoroo.astrid.sync.SyncV2Provider;
import com.todoroo.astrid.tags.TagService;
public class GtasksSyncV2Provider extends SyncV2Provider {
@Autowired TaskService taskService;
@Autowired MetadataService metadataService;
@Autowired MetadataDao metadataDao;
@Autowired StoreObjectDao storeObjectDao;
@Autowired ActFmPreferenceService actFmPreferenceService;
@Autowired GtasksPreferenceService gtasksPreferenceService;
@ -65,6 +73,9 @@ public class GtasksSyncV2Provider extends SyncV2Provider {
@Autowired GtasksListService gtasksListService;
@Autowired GtasksMetadataService gtasksMetadataService;
@Autowired GtasksTaskListUpdater gtasksTaskListUpdater;
@Autowired TagService tagService;
@Autowired TagDataDao tagDataDao;
@Autowired TagMetadataDao tagMetadataDao;
static {
AstridDependencyInjector.initialize();
@ -102,14 +113,42 @@ public class GtasksSyncV2Provider extends SyncV2Provider {
@Override
public boolean isActive() {
return gtasksPreferenceService.isLoggedIn();
return gtasksPreferenceService.isLoggedIn() && !actFmPreferenceService.isLoggedIn();
}
public static class GtasksImportTuple {
public long taskId;
public String taskName;
public String taskUuid;
public String tagUuid;
public String tagName;
}
public static class GtasksImportCallback extends WidgetUpdatingCallbackWrapper {
protected final ArrayList<GtasksImportTuple> importConflicts;
public GtasksImportCallback(SyncResultCallback wrap) {
super(wrap);
importConflicts = new ArrayList<GtasksImportTuple>();
}
public void addImportConflict(GtasksImportTuple tuple) {
importConflicts.add(tuple);
}
}
@Override
public void synchronizeActiveTasks(final boolean manual, final SyncResultCallback callback) {
// TODO: Improve this logic. Should only be able to import from settings or something.
final boolean isImport = actFmPreferenceService.isLoggedIn();
if (isImport && !manual)
return;
callback.started();
callback.incrementMax(100);
gtasksPreferenceService.recordSyncStart();
new Thread(new Runnable() {
@ -137,10 +176,13 @@ public class GtasksSyncV2Provider extends SyncV2Provider {
new Thread(new Runnable() {
@Override
public void run() {
synchronizeListHelper(list, invoker, manual, handler, callback);
synchronizeListHelper(list, invoker, manual, handler, callback, isImport);
callback.incrementProgress(25);
if (finisher.decrementAndGet() == 0) {
pushUpdated(invoker, callback);
if (!isImport)
pushUpdated(invoker, callback);
else
finishImport(callback);
finishSync(callback);
}
}
@ -186,6 +228,8 @@ public class GtasksSyncV2Provider extends SyncV2Provider {
if (!GtasksList.TYPE.equals(gtasksList.getValue(StoreObject.TYPE)))
return;
final boolean isImport = actFmPreferenceService.isLoggedIn();
callback.started();
callback.incrementMax(100);
@ -198,7 +242,7 @@ public class GtasksSyncV2Provider extends SyncV2Provider {
gtasksSyncService.waitUntilEmpty();
callback.incrementProgress(13);
final GtasksInvoker service = new GtasksInvoker(authToken);
synchronizeListHelper(gtasksList, service, manual, null, callback);
synchronizeListHelper(gtasksList, service, manual, null, callback, isImport);
} finally {
callback.incrementProgress(25);
callback.finished();
@ -221,7 +265,7 @@ public class GtasksSyncV2Provider extends SyncV2Provider {
private synchronized void synchronizeListHelper(StoreObject list, GtasksInvoker invoker,
boolean manual, SyncExceptionHandler errorHandler, SyncResultCallback callback) {
boolean manual, SyncExceptionHandler errorHandler, SyncResultCallback callback, boolean isImport) {
String listId = list.getValue(GtasksList.REMOTE_ID);
long lastSyncDate;
if (!manual && list.containsNonNullValue(GtasksList.LAST_SYNC)) {
@ -253,7 +297,7 @@ public class GtasksSyncV2Provider extends SyncV2Provider {
list.setValue(GtasksList.LAST_SYNC, DateUtilities.now());
storeObjectDao.persist(list);
if(lastSyncDate == 0) {
if(lastSyncDate == 0 && !isImport) {
Long[] localIdArray = localIds.toArray(new Long[localIds.size()]);
Criterion delete = Criterion.and(Metadata.KEY.eq(GtasksMetadata.METADATA_KEY),
GtasksMetadata.LIST_ID.eq(listId),
@ -315,6 +359,7 @@ public class GtasksSyncV2Provider extends SyncV2Provider {
Task local = PluginServices.getTaskService().fetchById(task.task.getId(), Task.DUE_DATE, Task.COMPLETION_DATE);
if (local == null) {
task.task.clearValue(Task.ID);
task.task.clearValue(Task.UUID);
} else {
mergeDates(task.task, local);
if(task.task.isCompleted() && !local.isCompleted())
@ -333,13 +378,14 @@ public class GtasksSyncV2Provider extends SyncV2Provider {
private void titleMatchWithActFm(Task task) {
String title = task.getValue(Task.TITLE);
TodorooCursor<Task> match = taskService.query(Query.select(Task.ID)
TodorooCursor<Task> match = taskService.query(Query.select(Task.ID, Task.UUID)
.join(Join.left(Metadata.TABLE, Criterion.and(Metadata.KEY.eq(GtasksMetadata.METADATA_KEY), Metadata.TASK.eq(Task.ID))))
.where(Criterion.and(Task.TITLE.eq(title), GtasksMetadata.ID.isNull())));
try {
if (match.getCount() > 0) {
match.moveToFirst();
task.setId(match.get(Task.ID));
task.setUuid(match.get(Task.UUID));
}
} finally {
match.close();
@ -358,4 +404,65 @@ public class GtasksSyncV2Provider extends SyncV2Provider {
remote.setValue(Task.DUE_DATE, setDate);
}
}
private void finishImport(SyncResultCallback callback) {
TodorooCursor<Task> tasks = taskService.query(Query.select(Task.ID, Task.UUID, Task.TITLE, GtasksList.NAME)
.join(Join.inner(Metadata.TABLE, Task.ID.eq(Metadata.TASK)))
.join(Join.left(StoreObject.TABLE, GtasksMetadata.LIST_ID.eq(GtasksList.REMOTE_ID)))
.where(MetadataCriteria.withKey(GtasksMetadata.METADATA_KEY)));
GtasksImportCallback gtCallback = null;
if (callback instanceof GtasksImportCallback)
gtCallback = (GtasksImportCallback) callback;
try {
for (tasks.moveToFirst(); !tasks.isAfterLast(); tasks.moveToNext()) {
String listName = tasks.get(GtasksList.NAME);
String tagUuid = RemoteModel.NO_UUID;
if (!TextUtils.isEmpty(listName)) {
TodorooCursor<TagData> existingTag = tagDataDao.query(Query.select(TagData.UUID).where(TagData.NAME.eq(listName)));
try {
if (existingTag.getCount() > 0) {
existingTag.moveToFirst();
tagUuid = existingTag.get(TagData.UUID);
boolean taskIsInTag = metadataDao.taskIsInTag(tasks.get(Task.UUID), tagUuid);
if (tagMetadataDao.tagHasMembers(tagUuid) && !taskIsInTag) {
GtasksImportTuple tuple = new GtasksImportTuple();
tuple.taskId = tasks.get(Task.ID);
tuple.taskName = tasks.get(Task.TITLE);
tuple.taskUuid = tasks.get(Task.UUID);
tuple.tagUuid = tagUuid;
tuple.tagName = listName;
if (gtCallback != null)
gtCallback.addImportConflict(tuple);
continue;
} else if (taskIsInTag) {
continue;
}
} else {
TagData td = new TagData();
td.setValue(TagData.NAME, listName);
tagDataDao.createNew(td);
tagUuid = td.getUuid();
}
} finally {
existingTag.close();
}
if (!RemoteModel.isUuidEmpty(tagUuid)) {
Task task = new Task();
task.setId(tasks.get(Task.ID));
task.setUuid(tasks.get(Task.UUID));
tagService.createLink(task, listName, tagUuid);
}
}
}
} finally {
tasks.close();
}
}
}

@ -12,6 +12,8 @@ import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import org.json.JSONObject;
import android.app.Activity;
import android.content.Intent;
import android.content.res.Resources;
@ -448,7 +450,9 @@ public class EditNoteActivity extends LinearLayout implements TimerActionListene
userActivity.setValue(UserActivity.TARGET_NAME, task.getValue(Task.TITLE));
userActivity.setValue(UserActivity.CREATED_AT, DateUtilities.now());
if (usePicture && pendingCommentPicture != null) {
userActivity.setValue(UserActivity.PICTURE, RemoteModel.PictureHelper.uploadPictureJson(pendingCommentPicture).toString());
JSONObject pictureJson = RemoteModel.PictureHelper.savePictureJson(activity, pendingCommentPicture);
if (pictureJson != null)
userActivity.setValue(UserActivity.PICTURE, pictureJson.toString());
}
userActivityDao.createNew(userActivity);

@ -25,10 +25,10 @@ import com.todoroo.andlib.service.Autowired;
import com.todoroo.andlib.service.DependencyInjectionService;
import com.todoroo.andlib.sql.Query;
import com.todoroo.andlib.utility.DateUtilities;
import com.todoroo.astrid.dao.MetadataDao.MetadataCriteria;
import com.todoroo.astrid.dao.StoreObjectDao;
import com.todoroo.astrid.dao.StoreObjectDao.StoreObjectCriteria;
import com.todoroo.astrid.data.Metadata;
import com.todoroo.astrid.data.MetadataApiDao.MetadataCriteria;
import com.todoroo.astrid.data.StoreObject;
import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.service.MetadataService;

@ -1,47 +0,0 @@
/**
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.producteev;
import com.todoroo.astrid.producteev.sync.ProducteevSyncProvider;
import com.todoroo.astrid.service.StatisticsService;
import com.todoroo.astrid.sync.SyncBackgroundService;
import com.todoroo.astrid.sync.SyncProvider;
import com.todoroo.astrid.sync.SyncProviderUtilities;
/**
* SynchronizationService is the service that performs Astrid's background
* synchronization with online task managers. Starting this service
* schedules a repeating alarm which handles the synchronization
*
* @author Tim Su
*
*/
public class ProducteevBackgroundService extends SyncBackgroundService {
@Override
protected SyncProvider<?> getSyncProvider() {
return new ProducteevSyncProvider();
}
@Override
protected SyncProviderUtilities getSyncUtilities() {
return ProducteevUtilities.INSTANCE;
}
@Override
public void onCreate() {
super.onCreate();
StatisticsService.sessionStart(this);
}
@Override
public void onDestroy() {
StatisticsService.sessionStop(this);
super.onDestroy();
}
}

@ -1,285 +0,0 @@
/**
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.producteev;
import java.util.ArrayList;
import org.json.JSONObject;
import android.app.Activity;
import android.app.ProgressDialog;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.util.Log;
import android.view.View;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.ArrayAdapter;
import android.widget.EditText;
import android.widget.Spinner;
import android.widget.TextView;
import com.timsu.astrid.R;
import com.todoroo.andlib.service.Autowired;
import com.todoroo.andlib.service.DependencyInjectionService;
import com.todoroo.andlib.service.ExceptionService;
import com.todoroo.andlib.utility.DateUtilities;
import com.todoroo.andlib.utility.DialogUtilities;
import com.todoroo.andlib.utility.Preferences;
import com.todoroo.astrid.data.Metadata;
import com.todoroo.astrid.data.StoreObject;
import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.producteev.sync.ProducteevDashboard;
import com.todoroo.astrid.producteev.sync.ProducteevDataService;
import com.todoroo.astrid.producteev.sync.ProducteevSyncProvider;
import com.todoroo.astrid.producteev.sync.ProducteevTask;
import com.todoroo.astrid.producteev.sync.ProducteevUser;
import com.todoroo.astrid.service.MetadataService;
import com.todoroo.astrid.ui.PopupControlSet;
/**
* Control Set for managing task/dashboard assignments in Producteev
*
* @author Arne Jans <arne.jans@gmail.com>
*
*/
public class ProducteevControlSet extends PopupControlSet {
private Spinner responsibleSelector;
private Spinner dashboardSelector;
private ArrayList<ProducteevUser> users = null;
private ArrayList<ProducteevDashboard> dashboards = null;
@Autowired MetadataService metadataService;
@Autowired ExceptionService exceptionService;
private int lastDashboardSelection = 0;
public ProducteevControlSet(final Activity activity, int layout, int displayViewLayout, int title) {
super(activity, layout, displayViewLayout, title);
DependencyInjectionService.getInstance().inject(this);
}
/**
* Refresh the content of the responsibleSelector with the given userlist.
*
* @param newUsers the new userlist to show in the responsibleSelector
*/
private void refreshResponsibleSpinner(ArrayList<ProducteevUser> newUsers) {
Metadata metadata = ProducteevDataService.getInstance().getTaskMetadata(model.getId());
long responsibleId = -1;
if(metadata != null && metadata.containsNonNullValue(ProducteevTask.RESPONSIBLE_ID))
responsibleId = metadata.getValue(ProducteevTask.RESPONSIBLE_ID);
refreshResponsibleSpinner(newUsers, responsibleId);
}
/**
* Refresh the content of the responsibleSelector with the given userlist.
*
* @param newUsers the new userlist to show in the responsibleSelector
* @param responsibleId the id of the responsible user to set in the spinner
*/
private void refreshResponsibleSpinner(ArrayList<ProducteevUser> newUsers, long responsibleId) {
// Fill the responsible-spinner and set the current responsible
this.users = (newUsers == null ? new ArrayList<ProducteevUser>() : newUsers);
ArrayAdapter<ProducteevUser> usersAdapter = new ArrayAdapter<ProducteevUser>(activity,
android.R.layout.simple_spinner_item, this.users);
usersAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
responsibleSelector.setAdapter(usersAdapter);
int visibility = newUsers == null ? View.GONE : View.VISIBLE;
getView().findViewById(R.id.producteev_TEA_task_assign_label).setVisibility(visibility);
responsibleSelector.setVisibility(visibility);
int responsibleSpinnerIndex = 0;
for (int i = 0; i < this.users.size() ; i++) {
if (this.users.get(i).getId() == responsibleId ||
(responsibleId == -1 && this.users.get(i).getId() == Preferences.getLong(ProducteevUtilities.PREF_USER_ID, -1))) {
responsibleSpinnerIndex = i;
break;
}
}
responsibleSelector.setSelection(responsibleSpinnerIndex);
}
@Override
protected void readFromTaskOnInitialize() {
Metadata metadata = ProducteevDataService.getInstance().getTaskMetadata(model.getId());
if(metadata == null)
metadata = ProducteevTask.newMetadata();
// Fill the dashboard-spinner and set the current dashboard
long dashboardId = ProducteevUtilities.INSTANCE.getDefaultDashboard();
if(metadata.containsNonNullValue(ProducteevTask.DASHBOARD_ID))
dashboardId = metadata.getValue(ProducteevTask.DASHBOARD_ID);
StoreObject[] dashboardsData = ProducteevDataService.getInstance().getDashboards();
dashboards = new ArrayList<ProducteevDashboard>(dashboardsData.length);
ProducteevDashboard ownerDashboard = null;
int dashboardSpinnerIndex = -1;
int i = 0;
for (i=0;i<dashboardsData.length;i++) {
ProducteevDashboard dashboard = new ProducteevDashboard(dashboardsData[i]);
dashboards.add(dashboard);
if(dashboard.getId() == dashboardId) {
ownerDashboard = dashboard;
dashboardSpinnerIndex = i;
}
}
//dashboard to not sync as first spinner-entry
dashboards.add(0, new ProducteevDashboard(ProducteevUtilities.DASHBOARD_NO_SYNC, activity.getString(R.string.producteev_no_dashboard),null));
// dummy entry for adding a new dashboard
dashboards.add(new ProducteevDashboard(ProducteevUtilities.DASHBOARD_CREATE, activity.getString(R.string.producteev_create_dashboard),null));
ArrayAdapter<ProducteevDashboard> dashAdapter = new ArrayAdapter<ProducteevDashboard>(activity,
android.R.layout.simple_spinner_item, dashboards);
dashAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
dashboardSelector.setAdapter(dashAdapter);
dashboardSelector.setSelection(dashboardSpinnerIndex+1);
if (ownerDashboard == null || ownerDashboard.getId() == ProducteevUtilities.DASHBOARD_NO_SYNC
|| ownerDashboard.getId() == ProducteevUtilities.DASHBOARD_CREATE) {
responsibleSelector.setEnabled(false);
responsibleSelector.setAdapter(null);
getView().findViewById(R.id.producteev_TEA_task_assign_label).setVisibility(View.GONE);
return;
}
refreshResponsibleSpinner(ownerDashboard.getUsers());
}
@Override
protected void afterInflate() {
this.displayText.setText(activity.getString(R.string.producteev_TEA_control_set_display));
//view = LayoutInflater.from(activity).inflate(R.layout.producteev_control, parent, true);
this.responsibleSelector = (Spinner) getView().findViewById(R.id.producteev_TEA_task_assign);
TextView emptyView = new TextView(activity);
emptyView.setText(activity.getText(R.string.producteev_no_dashboard));
responsibleSelector.setEmptyView(emptyView);
this.dashboardSelector = (Spinner) getView().findViewById(R.id.producteev_TEA_dashboard_assign);
this.dashboardSelector.setOnItemSelectedListener(new OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> spinnerParent, View spinnerView,
int position, long id) {
final Spinner dashSelector = (Spinner) spinnerParent;
ProducteevDashboard dashboard = (ProducteevDashboard) dashSelector.getSelectedItem();
if (dashboard.getId() == ProducteevUtilities.DASHBOARD_CREATE) {
// let the user create a new dashboard
final EditText editor = new EditText(ProducteevControlSet.this.activity);
OnClickListener okListener = new OnClickListener() {
@Override
public void onClick(DialogInterface dlg, int which) {
Activity context = ProducteevControlSet.this.activity;
String newDashboardName = editor.getText().toString();
if (newDashboardName == null || newDashboardName.length() == 0) {
dlg.cancel();
} else {
// create the real dashboard, select it in the spinner and refresh responsiblespinner
ProgressDialog progressDialog = com.todoroo.andlib.utility.DialogUtilities.progressDialog(context,
context.getString(R.string.DLG_wait));
try {
progressDialog.show();
JSONObject newDashJSON = ProducteevSyncProvider.getInvoker().dashboardsCreate(
newDashboardName).getJSONObject("dashboard"); //$NON-NLS-1$
StoreObject local = ProducteevDataService.getInstance().updateDashboards(newDashJSON, true);
if (local != null) {
ProducteevDashboard newDashboard = new ProducteevDashboard(local);
ArrayAdapter<ProducteevDashboard> adapter = (ArrayAdapter<ProducteevDashboard>) dashSelector.getAdapter();
adapter.insert(newDashboard, adapter.getCount()-1);
dashSelector.setSelection(adapter.getCount()-2);
refreshResponsibleSpinner(newDashboard.getUsers());
DialogUtilities.dismissDialog(context, progressDialog);
}
} catch (Exception e) {
DialogUtilities.dismissDialog(context, progressDialog);
DialogUtilities.okDialog(context,
context.getString(R.string.DLG_error, e.getMessage()),
null);
exceptionService.reportError("pdv-create-dashboard", e); //$NON-NLS-1$
dashSelector.setSelection(0);
}
}
}
};
OnClickListener cancelListener = new OnClickListener() {
@Override
public void onClick(DialogInterface dlg, int which) {
dlg.cancel();
dashboardSelector.setSelection(lastDashboardSelection);
}
};
DialogUtilities.viewDialog(ProducteevControlSet.this.activity,
ProducteevControlSet.this.activity.getString(R.string.producteev_create_dashboard_name),
editor,
okListener,
cancelListener);
} else {
refreshResponsibleSpinner(dashboard.getUsers());
lastDashboardSelection = position;
}
}
@Override
public void onNothingSelected(AdapterView<?> spinnerParent) {
//
}
});
}
@Override
protected String writeToModelAfterInitialized(Task task) {
Metadata metadata = ProducteevDataService.getInstance().getTaskMetadata(task.getId());
try {
if (metadata == null) {
metadata = new Metadata();
metadata.setValue(Metadata.KEY, ProducteevTask.METADATA_KEY);
metadata.setValue(Metadata.TASK, task.getId());
metadata.setValue(ProducteevTask.ID, 0L);
}
ProducteevDashboard dashboard = (ProducteevDashboard) dashboardSelector.getSelectedItem();
metadata.setValue(ProducteevTask.DASHBOARD_ID, dashboard.getId());
ProducteevUser responsibleUser = (ProducteevUser) responsibleSelector.getSelectedItem();
if(responsibleUser == null)
metadata.setValue(ProducteevTask.RESPONSIBLE_ID, 0L);
else
metadata.setValue(ProducteevTask.RESPONSIBLE_ID, responsibleUser.getId());
// Erase PDTV-repeating-info if task itself is repeating with Astrid-repeat
if (task.containsNonNullValue(Task.RECURRENCE) && task.getValue(Task.RECURRENCE).length()>0) {
metadata.setValue(ProducteevTask.REPEATING_SETTING, ""); //$NON-NLS-1$
}
if(metadata.getSetValues().size() > 0) {
metadataService.save(metadata);
task.setValue(Task.MODIFICATION_DATE, DateUtilities.now());
}
} catch (Exception e) {
Log.e("error-saving-pdv", "Error Saving Metadata", e); //$NON-NLS-1$ //$NON-NLS-2$
}
return null;
}
@Override
protected void refreshDisplayView() {
// TODO Auto-generated method stub
}
}

@ -1,124 +0,0 @@
/**
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.producteev;
import java.util.Set;
import java.util.TreeSet;
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.Join;
import com.todoroo.andlib.sql.Query;
import com.todoroo.astrid.api.AstridApiConstants;
import com.todoroo.astrid.api.CustomFilterCriterion;
import com.todoroo.astrid.api.MultipleSelectCriterion;
import com.todoroo.astrid.dao.MetadataDao;
import com.todoroo.astrid.dao.TaskDao;
import com.todoroo.astrid.data.Metadata;
import com.todoroo.astrid.data.StoreObject;
import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.producteev.sync.ProducteevDashboard;
import com.todoroo.astrid.producteev.sync.ProducteevDataService;
import com.todoroo.astrid.producteev.sync.ProducteevTask;
import com.todoroo.astrid.producteev.sync.ProducteevUser;
public class ProducteevCustomFilterCriteriaExposer extends BroadcastReceiver {
private static final String IDENTIFIER_PRODUCTEEV_WORKSPACE = "producteev_workspace"; //$NON-NLS-1$
private static final String IDENTIFIER_PRODUCTEEV_ASSIGNEE = "producteev_assignee"; //$NON-NLS-1$
@SuppressWarnings("nls")
@Override
public void onReceive(Context context, Intent intent) {
// if we aren't logged in, don't expose features
if(!ProducteevUtilities.INSTANCE.isLoggedIn())
return;
Resources r = context.getResources();
StoreObject[] objects = ProducteevDataService.getInstance().getDashboards();
ProducteevDashboard[] dashboards = new ProducteevDashboard[objects.length];
for (int i = 0; i < objects.length; i++) {
dashboards[i] = new ProducteevDashboard(objects[i]);
}
CustomFilterCriterion[] ret = new CustomFilterCriterion[2];
int j = 0;
{
String[] workspaceNames = new String[objects.length];
String[] workspaceIds = new String[objects.length];
for (int i = 0; i < dashboards.length; i++) {
workspaceNames[i] = dashboards[i].getName();
workspaceIds[i] = String.valueOf(dashboards[i].getId());
}
ContentValues values = new ContentValues();
values.put(Metadata.KEY.name, ProducteevTask.METADATA_KEY);
values.put(ProducteevTask.DASHBOARD_ID.name, "?");
CustomFilterCriterion criterion = new MultipleSelectCriterion(
IDENTIFIER_PRODUCTEEV_WORKSPACE,
context.getString(R.string.CFC_producteev_in_workspace_text),
// Todo: abstract these metadata queries
Query.select(Metadata.TASK).from(Metadata.TABLE).join(Join.inner(
Task.TABLE, Metadata.TASK.eq(Task.ID))).where(Criterion.and(
TaskDao.TaskCriteria.activeAndVisible(),
MetadataDao.MetadataCriteria.withKey(ProducteevTask.METADATA_KEY),
ProducteevTask.DASHBOARD_ID.eq("?"))).toString(),
values,
workspaceNames,
workspaceIds,
((BitmapDrawable)r.getDrawable(R.drawable.silk_folder)).getBitmap(),
context.getString(R.string.CFC_producteev_in_workspace_name));
ret[j++] = criterion;
}
{
Set<ProducteevUser> users = new TreeSet<ProducteevUser>();
for (ProducteevDashboard dashboard : dashboards) {
users.addAll(dashboard.getUsers());
}
int numUsers = users.size();
String[] userNames = new String[numUsers];
String[] userIds = new String[numUsers];
int i = 0;
for (ProducteevUser user : users) {
userNames[i] = user.toString();
userIds[i] = String.valueOf(user.getId());
i++;
}
ContentValues values = new ContentValues(2);
values.put(Metadata.KEY.name, ProducteevTask.METADATA_KEY);
values.put(ProducteevTask.RESPONSIBLE_ID.name, "?");
CustomFilterCriterion criterion = new MultipleSelectCriterion(
IDENTIFIER_PRODUCTEEV_ASSIGNEE,
context.getString(R.string.CFC_producteev_assigned_to_text),
// Todo: abstract these metadata queries, and unify this code with the CustomFilterExposers.
Query.select(Metadata.TASK).from(Metadata.TABLE).join(Join.inner(
Task.TABLE, Metadata.TASK.eq(Task.ID))).where(Criterion.and(
TaskDao.TaskCriteria.activeAndVisible(),
MetadataDao.MetadataCriteria.withKey(ProducteevTask.METADATA_KEY),
ProducteevTask.RESPONSIBLE_ID.eq("?"))).toString(),
values,
userNames,
userIds,
((BitmapDrawable)r.getDrawable(R.drawable.silk_user_gray)).getBitmap(),
context.getString(R.string.CFC_producteev_assigned_to_name));
ret[j++] = criterion;
}
// transmit filter list
Intent broadcastIntent = new Intent(AstridApiConstants.BROADCAST_SEND_CUSTOM_FILTER_CRITERIA);
broadcastIntent.putExtra(AstridApiConstants.EXTRAS_ADDON, ProducteevUtilities.IDENTIFIER);
broadcastIntent.putExtra(AstridApiConstants.EXTRAS_RESPONSE, ret);
context.sendBroadcast(broadcastIntent, AstridApiConstants.PERMISSION_READ);
}
}

@ -1,180 +0,0 @@
/**
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.producteev;
import java.text.DateFormatSymbols;
import java.util.Calendar;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import com.timsu.astrid.R;
import com.todoroo.andlib.service.ContextManager;
import com.todoroo.andlib.utility.Preferences;
import com.todoroo.astrid.adapter.TaskAdapter;
import com.todoroo.astrid.api.AstridApiConstants;
import com.todoroo.astrid.data.Metadata;
import com.todoroo.astrid.data.StoreObject;
import com.todoroo.astrid.producteev.sync.ProducteevDashboard;
import com.todoroo.astrid.producteev.sync.ProducteevDataService;
import com.todoroo.astrid.producteev.sync.ProducteevTask;
/**
* Exposes Task Details for Producteev:
* - notes
*
* @author Tim Su <tim@todoroo.com>
*
*/
public class ProducteevDetailExposer extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
ContextManager.setContext(context);
long taskId = intent.getLongExtra(AstridApiConstants.EXTRAS_TASK_ID, -1);
if(taskId == -1)
return;
String taskDetail;
try {
taskDetail = getTaskDetails(context, taskId);
} catch (Exception e) {
return;
}
if(taskDetail == null)
return;
Intent broadcastIntent = new Intent(AstridApiConstants.BROADCAST_SEND_DETAILS);
broadcastIntent.putExtra(AstridApiConstants.EXTRAS_ADDON, ProducteevUtilities.IDENTIFIER);
broadcastIntent.putExtra(AstridApiConstants.EXTRAS_TASK_ID, taskId);
broadcastIntent.putExtra(AstridApiConstants.EXTRAS_RESPONSE, taskDetail);
context.sendBroadcast(broadcastIntent, AstridApiConstants.PERMISSION_READ);
}
@SuppressWarnings("nls")
public String getTaskDetails(Context context, long id) {
Metadata metadata = ProducteevDataService.getInstance().getTaskMetadata(id);
if(metadata == null)
return null;
StringBuilder builder = new StringBuilder();
if(!ProducteevUtilities.INSTANCE.isLoggedIn())
return null;
long dashboardId = -1;
if(metadata.containsNonNullValue(ProducteevTask.DASHBOARD_ID))
dashboardId = metadata.getValue(ProducteevTask.DASHBOARD_ID);
long responsibleId = -1;
if(metadata.containsNonNullValue(ProducteevTask.RESPONSIBLE_ID))
responsibleId = metadata.getValue(ProducteevTask.RESPONSIBLE_ID);
long creatorId = -1;
if(metadata.containsNonNullValue(ProducteevTask.CREATOR_ID))
creatorId = metadata.getValue(ProducteevTask.CREATOR_ID);
String repeatSetting = null;
if(metadata.containsNonNullValue(ProducteevTask.REPEATING_SETTING))
repeatSetting = metadata.getValue(ProducteevTask.REPEATING_SETTING);
// display dashboard if not "no sync" or "default"
StoreObject ownerDashboard = null;
for(StoreObject dashboard : ProducteevDataService.getInstance().getDashboards()) {
if(dashboard == null || !dashboard.containsNonNullValue(ProducteevDashboard.REMOTE_ID))
continue;
if(dashboard.getValue(ProducteevDashboard.REMOTE_ID) == dashboardId) {
ownerDashboard = dashboard;
break;
}
}
if(dashboardId != ProducteevUtilities.DASHBOARD_NO_SYNC && dashboardId
!= Preferences.getLong(ProducteevUtilities.PREF_DEFAULT_DASHBOARD, 0L) &&
ownerDashboard != null) {
String dashboardName = ownerDashboard.getValue(ProducteevDashboard.NAME);
builder.append("<img src='silk_folder'/> ").append(dashboardName).append(TaskAdapter.DETAIL_SEPARATOR); //$NON-NLS-1$
}
// display responsible user if not current one
if(responsibleId > 0 && ownerDashboard != null && responsibleId !=
Preferences.getLong(ProducteevUtilities.PREF_USER_ID, 0L)) {
String user = ProducteevDashboard.getUserFromDashboard(ownerDashboard, responsibleId);
if(user != null)
builder.append("<img src='silk_user_gray'/> ").append(user).append(TaskAdapter.DETAIL_SEPARATOR); //$NON-NLS-1$
} else {
// display creator user if not responsible user
if(creatorId > 0 && ownerDashboard != null && creatorId != responsibleId) {
String user = ProducteevDashboard.getUserFromDashboard(ownerDashboard, creatorId);
if(user != null)
builder.append("<img src='silk_user_orange'/> ").append( //$NON-NLS-1$
context.getString(R.string.producteev_PDE_task_from, user)).
append(TaskAdapter.DETAIL_SEPARATOR);
}
}
// display repeating task information
if (repeatSetting != null && repeatSetting.length() > 0) {
String interval = null;
String[] pdvRepeating = repeatSetting.split(",");
int pdvRepeatingValue = 0;
String pdvRepeatingDay = null;
try {
pdvRepeatingValue = Integer.parseInt(pdvRepeating[0]);
} catch (Exception e) {
pdvRepeatingDay = pdvRepeating[0];
pdvRepeatingValue = 1;
}
String pdvRepeatingInterval = pdvRepeating[1];
if (pdvRepeatingInterval.startsWith("day")) {
interval = context.getResources().getQuantityString(R.plurals.DUt_days, pdvRepeatingValue,
pdvRepeatingValue);
} else if (pdvRepeatingInterval.startsWith("weekday")) {
interval = context.getResources().getQuantityString(R.plurals.DUt_weekdays, pdvRepeatingValue,
pdvRepeatingValue);
} else if (pdvRepeatingInterval.startsWith("week")) {
interval = context.getResources().getQuantityString(R.plurals.DUt_weeks, pdvRepeatingValue,
pdvRepeatingValue);
} else if (pdvRepeatingInterval.startsWith("month")) {
interval = context.getResources().getQuantityString(R.plurals.DUt_months, pdvRepeatingValue,
pdvRepeatingValue);
} else if (pdvRepeatingInterval.startsWith("year")) {
interval = context.getResources().getQuantityString(R.plurals.DUt_years, pdvRepeatingValue,
pdvRepeatingValue);
}
interval = "<b>" + interval + "</b>"; //$NON-NLS-1$//$NON-NLS-2$
if (pdvRepeatingDay != null) {
DateFormatSymbols dfs = new DateFormatSymbols();
String[] weekdays = dfs.getShortWeekdays();
if (pdvRepeatingDay.equals("monday")) {
pdvRepeatingDay = weekdays[Calendar.MONDAY];
} else if (pdvRepeatingDay.equals("tuesday")) {
pdvRepeatingDay = weekdays[Calendar.TUESDAY];
} else if (pdvRepeatingDay.equals("wednesday")) {
pdvRepeatingDay = weekdays[Calendar.WEDNESDAY];
} else if (pdvRepeatingDay.equals("thursday")) {
pdvRepeatingDay = weekdays[Calendar.THURSDAY];
} else if (pdvRepeatingDay.equals("friday")) {
pdvRepeatingDay = weekdays[Calendar.FRIDAY];
} else if (pdvRepeatingDay.equals("saturday")) {
pdvRepeatingDay = weekdays[Calendar.SATURDAY];
} else if (pdvRepeatingDay.equals("sunday")) {
pdvRepeatingDay = weekdays[Calendar.SUNDAY];
}
interval = context.getResources().getString(R.string.repeat_detail_byday).replace("$I", //$NON-NLS-1$
interval).replace("$D", pdvRepeatingDay); //$NON-NLS-1$
}
String detail = context.getString(R.string.repeat_detail_duedate, interval);
builder.append("<img src='repeating_deadline'/> ").append(detail). //$NON-NLS-1$
append(TaskAdapter.DETAIL_SEPARATOR);
}
if(builder.length() == 0)
return null;
String result = builder.toString();
return result.substring(0, result.length() - TaskAdapter.DETAIL_SEPARATOR.length());
}
}

@ -1,197 +0,0 @@
/**
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.producteev;
import java.util.TreeSet;
import android.content.BroadcastReceiver;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import com.timsu.astrid.R;
import com.todoroo.andlib.service.ContextManager;
import com.todoroo.andlib.sql.Criterion;
import com.todoroo.andlib.sql.QueryTemplate;
import com.todoroo.andlib.utility.Preferences;
import com.todoroo.astrid.api.AstridApiConstants;
import com.todoroo.astrid.api.AstridFilterExposer;
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.dao.MetadataDao.MetadataCriteria;
import com.todoroo.astrid.dao.TaskDao.TaskCriteria;
import com.todoroo.astrid.data.Metadata;
import com.todoroo.astrid.data.StoreObject;
import com.todoroo.astrid.producteev.sync.ProducteevDashboard;
import com.todoroo.astrid.producteev.sync.ProducteevDataService;
import com.todoroo.astrid.producteev.sync.ProducteevTask;
import com.todoroo.astrid.producteev.sync.ProducteevUser;
/**
* Exposes filters based on Producteev Dashboards
*
* @author Arne Jans <arne.jans@gmail.com>
*
*/
public class ProducteevFilterExposer extends BroadcastReceiver implements AstridFilterExposer {
/**
* @param context
*/
public static Filter filterFromList(Context context, ProducteevDashboard dashboard, long currentUserId) {
String dashboardTitle = dashboard.getName();
String title = dashboard.getName();
ContentValues values = new ContentValues();
values.put(Metadata.KEY.name, ProducteevTask.METADATA_KEY);
values.put(ProducteevTask.DASHBOARD_ID.name, dashboard.getId());
values.put(ProducteevTask.ID.name, 0);
values.put(ProducteevTask.CREATOR_ID.name, currentUserId);
values.put(ProducteevTask.RESPONSIBLE_ID.name, currentUserId);
Filter filter;
if (currentUserId != -1)
filter = new Filter(dashboardTitle, title, new QueryTemplate().join(
ProducteevDataService.METADATA_JOIN).where(Criterion.and(
MetadataCriteria.withKey(ProducteevTask.METADATA_KEY),
TaskCriteria.isActive(),
TaskCriteria.isVisible(),
Criterion.or(ProducteevTask.CREATOR_ID.eq(currentUserId),
ProducteevTask.RESPONSIBLE_ID.eq(currentUserId)),
ProducteevTask.DASHBOARD_ID.eq(dashboard.getId()))),
values);
else
filter = new Filter(dashboardTitle, title, new QueryTemplate().join(
ProducteevDataService.METADATA_JOIN).where(Criterion.and(
MetadataCriteria.withKey(ProducteevTask.METADATA_KEY),
TaskCriteria.isActive(),
TaskCriteria.isVisible(),
ProducteevTask.DASHBOARD_ID.eq(dashboard.getId()))),
values);
return filter;
}
private Filter filterUserAssignedByMe(Context context, ProducteevUser user, long currentUserId) {
String title = context.getString(R.string.producteev_FEx_responsible_title, user.toString());
ContentValues values = new ContentValues();
values.put(Metadata.KEY.name, ProducteevTask.METADATA_KEY);
values.put(ProducteevTask.ID.name, 0);
values.put(ProducteevTask.CREATOR_ID.name, currentUserId);
values.put(ProducteevTask.RESPONSIBLE_ID.name, user.getId());
Filter filter = new Filter(user.toString(), title, new QueryTemplate().join(
ProducteevDataService.METADATA_JOIN).where(Criterion.and(
MetadataCriteria.withKey(ProducteevTask.METADATA_KEY),
TaskCriteria.isActive(),
TaskCriteria.isVisible(),
ProducteevTask.CREATOR_ID.eq(currentUserId),
ProducteevTask.RESPONSIBLE_ID.eq(user.getId()))),
values);
return filter;
}
private Filter filterUserAssignedByOthers(Context context, ProducteevUser user, long currentUserId) {
String title = context.getString(R.string.producteev_FEx_responsible_title, user.toString());
ContentValues values = new ContentValues();
values.put(Metadata.KEY.name, ProducteevTask.METADATA_KEY);
values.put(ProducteevTask.ID.name, 0);
values.put(ProducteevTask.CREATOR_ID.name, 0);
values.put(ProducteevTask.RESPONSIBLE_ID.name, currentUserId);
Filter filter = new Filter(user.toString(), title, new QueryTemplate().join(
ProducteevDataService.METADATA_JOIN).where(Criterion.and(
MetadataCriteria.withKey(ProducteevTask.METADATA_KEY),
TaskCriteria.isActive(),
TaskCriteria.isVisible(),
Criterion.not(ProducteevTask.CREATOR_ID.eq(currentUserId)),
ProducteevTask.RESPONSIBLE_ID.eq(user.getId()))),
values);
return filter;
}
@Override
public void onReceive(Context context, Intent intent) {
ContextManager.setContext(context);
FilterListItem[] list = prepareFilters(context);
Intent broadcastIntent = new Intent(AstridApiConstants.BROADCAST_SEND_FILTERS);
broadcastIntent.putExtra(AstridApiConstants.EXTRAS_ADDON, ProducteevUtilities.IDENTIFIER);
broadcastIntent.putExtra(AstridApiConstants.EXTRAS_RESPONSE, list);
context.sendBroadcast(broadcastIntent, AstridApiConstants.PERMISSION_READ);
}
private FilterListItem[] prepareFilters(Context context) {
// if we aren't logged in, don't expose features
if(!ProducteevUtilities.INSTANCE.isLoggedIn())
return null;
StoreObject[] dashboards = ProducteevDataService.getInstance().getDashboards();
// If user does not have any dashboards, don't show this section at all
if(dashboards.length == 0)
return null;
FilterListHeader producteevHeader = new FilterListHeader(context.getString(R.string.producteev_FEx_header));
long currentUserId = Preferences.getLong(ProducteevUtilities.PREF_USER_ID, -1);
// load dashboards
Filter[] dashboardFilters = new Filter[dashboards.length];
for(int i = 0; i < dashboards.length; i++)
dashboardFilters[i] = filterFromList(context, new ProducteevDashboard(dashboards[i]), currentUserId);
FilterCategory producteevDashboards = new FilterCategory(context.getString(R.string.producteev_FEx_dashboard),
dashboardFilters);
// load responsible people, assigned by me
TreeSet<ProducteevUser> people = loadResponsiblePeople(dashboards);
Filter[] peopleByMeFilters = new Filter[people.size()];
int index = 0;
for (ProducteevUser person : people)
peopleByMeFilters[index++] = filterUserAssignedByMe(context, person, currentUserId);
FilterCategory producteevUsersByMeCategory = new FilterCategory(context.getString(R.string.producteev_FEx_responsible_byme),
peopleByMeFilters);
// load responsible people, assigned by others
Filter[] peopleByOthersFilters = new Filter[people.size()];
index = 0;
for (ProducteevUser person : people)
peopleByOthersFilters[index++] = filterUserAssignedByOthers(context, person, currentUserId);
FilterCategory producteevUsersByOthersCategory = new FilterCategory(context.getString(R.string.producteev_FEx_responsible_byothers),
peopleByOthersFilters);
// transmit filter list
FilterListItem[] list = new FilterListItem[4];
list[0] = producteevHeader;
list[1] = producteevDashboards;
list[2] = producteevUsersByMeCategory;
list[3] = producteevUsersByOthersCategory;
return list;
}
/**
* @param dashboards
* @return people in a map of name => pair(dashboard id, user id)
*/
private TreeSet<ProducteevUser> loadResponsiblePeople(StoreObject[] dashboards) {
TreeSet<ProducteevUser> users = new TreeSet<ProducteevUser>();
for(StoreObject dashboard : dashboards) {
ProducteevDashboard elDashboard = new ProducteevDashboard(dashboard);
users.addAll(elDashboard.getUsers());
}
return users;
}
@Override
public FilterListItem[] getFilters() {
if (ContextManager.getContext() == null)
return null;
return prepareFilters(ContextManager.getContext());
}
}

@ -1,240 +0,0 @@
/**
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.producteev;
import java.util.TimeZone;
import android.app.Activity;
import android.app.ProgressDialog;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.text.Editable;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Spinner;
import android.widget.TextView;
import com.timsu.astrid.R;
import com.todoroo.andlib.service.ContextManager;
import com.todoroo.andlib.service.DependencyInjectionService;
import com.todoroo.andlib.utility.DialogUtilities;
import com.todoroo.andlib.utility.Preferences;
import com.todoroo.astrid.producteev.api.ApiAuthenticationException;
import com.todoroo.astrid.producteev.api.ProducteevInvoker;
import com.todoroo.astrid.producteev.sync.ProducteevSyncProvider;
import com.todoroo.astrid.service.StatisticsConstants;
import com.todoroo.astrid.service.StatisticsService;
/**
* This activity allows users to sign in or log in to Producteev
*
* @author arne.jans
*
*/
public class ProducteevLoginActivity extends Activity {
// --- ui initialization
public ProducteevLoginActivity() {
super();
DependencyInjectionService.getInstance().inject(this);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ContextManager.setContext(this);
setContentView(R.layout.producteev_login_activity);
setTitle(R.string.producteev_PLA_title);
// terms clicking
findViewById(R.id.terms).setOnClickListener(new OnClickListener() {
@Override
public void onClick(View arg0) {
startActivity(new Intent(Intent.ACTION_VIEW,
Uri.parse("https://www.producteev.com/#terms"))); //$NON-NLS-1$
}
});
final TextView errors = (TextView) findViewById(R.id.error);
final EditText emailEditText = (EditText) findViewById(R.id.email);
final EditText passwordEditText = (EditText) findViewById(R.id.password);
final View newUserLayout = findViewById(R.id.newUserLayout);
final Spinner timezoneList = (Spinner) findViewById(R.id.timezoneList);
String[] timezoneEntries = getResources().getStringArray(R.array.PLA_timezones_list);
String defaultTimeZone = TimeZone.getDefault().getID();
int selected = 0;
for(int i = 0; i < timezoneEntries.length; i++) {
if(timezoneEntries[i].equals(defaultTimeZone))
selected = i;
}
timezoneList.setSelection(selected);
Button signIn = (Button) findViewById(R.id.signIn);
signIn.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
errors.setVisibility(View.GONE);
if(newUserLayout.getVisibility() == View.VISIBLE)
newUserLayout.setVisibility(View.GONE);
else {
Editable email = emailEditText.getText();
Editable password = passwordEditText.getText();
if(email.length() == 0 || password.length() == 0) {
errors.setVisibility(View.VISIBLE);
errors.setText(R.string.producteev_PLA_errorEmpty);
return;
}
performLogin(email.toString(), password.toString());
}
}
});
Button createNew = (Button) findViewById(R.id.createNew);
createNew.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
errors.setVisibility(View.GONE);
if(newUserLayout.getVisibility() != View.VISIBLE)
newUserLayout.setVisibility(View.VISIBLE);
else {
Editable email = emailEditText.getText();
Editable password = passwordEditText.getText();
Editable firstName = ((EditText)findViewById(R.id.firstName)).getText();
Editable lastName = ((EditText)findViewById(R.id.lastName)).getText();
String timezone = timezoneList.getSelectedItem().toString();
if(email.length() == 0 || password.length() == 0 ||
firstName.length() == 0 ||
lastName.length() == 0) {
errors.setVisibility(View.VISIBLE);
errors.setText(R.string.producteev_PLA_errorEmpty);
return;
}
performSignup(email.toString(), password.toString(),
firstName.toString(), lastName.toString(), timezone);
}
}
});
}
private void performLogin(final String email, final String password) {
final ProgressDialog dialog = DialogUtilities.progressDialog(this,
getString(R.string.DLG_wait));
final TextView errors = (TextView) findViewById(R.id.error);
dialog.show();
new Thread() {
@Override
public void run() {
ProducteevInvoker invoker = ProducteevSyncProvider.getInvoker();
final StringBuilder errorMessage = new StringBuilder();
try {
invoker.authenticate(email, password);
Preferences.setString(R.string.producteev_PPr_email, email);
Preferences.setString(R.string.producteev_PPr_password, password);
ProducteevUtilities.INSTANCE.setToken(invoker.getToken());
StatisticsService.reportEvent(StatisticsConstants.PRODUCTEEV_LOGIN);
synchronize();
} catch (ApiAuthenticationException e) {
errorMessage.append(getString(R.string.producteev_PLA_errorAuth));
} catch (Exception e) {
errorMessage.append(e.getMessage());
} finally {
runOnUiThread(new Runnable() {
public void run() {
DialogUtilities.dismissDialog(ProducteevLoginActivity.this, dialog);
if(errorMessage.length() > 0) {
errors.setVisibility(View.VISIBLE);
errors.setText(errorMessage);
}
}
});
}
}
}.start();
}
private void performSignup(final String email, final String password,
final String firstName, final String lastName, final String timezone) {
final ProgressDialog dialog = DialogUtilities.progressDialog(this,
getString(R.string.DLG_wait));
final TextView errors = (TextView) findViewById(R.id.error);
dialog.show();
new Thread() {
@Override
public void run() {
ProducteevInvoker invoker = ProducteevSyncProvider.getInvoker();
final StringBuilder errorMessage = new StringBuilder();
try {
invoker.usersSignUp(email, firstName, lastName, password, timezone, null);
invoker.authenticate(email, password);
Preferences.setString(R.string.producteev_PPr_email, email);
Preferences.setString(R.string.producteev_PPr_password, password);
ProducteevUtilities.INSTANCE.setToken(invoker.getToken());
StatisticsService.reportEvent(StatisticsConstants.PRODUCTEEV_SIGNUP);
synchronize();
} catch (Exception e) {
errorMessage.append(e.getMessage());
} finally {
runOnUiThread(new Runnable() {
public void run() {
DialogUtilities.dismissDialog(ProducteevLoginActivity.this, dialog);
if(errorMessage.length() > 0) {
errors.setVisibility(View.VISIBLE);
errors.setText(errorMessage);
}
}
});
}
}
}.start();
}
/**
* Perform synchronization
*/
protected void synchronize() {
startService(new Intent(null, null,
this, ProducteevBackgroundService.class));
finish();
}
@Override
protected void onStart() {
super.onStart();
}
@Override
protected void onResume() {
super.onResume();
StatisticsService.sessionStart(this);
}
@Override
protected void onPause() {
super.onPause();
StatisticsService.sessionPause();
}
@Override
protected void onStop() {
super.onStop();
StatisticsService.sessionStop(this);
}
}

@ -1,102 +0,0 @@
/**
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.producteev;
import android.content.res.Resources;
import android.os.Bundle;
import android.preference.ListPreference;
import android.preference.Preference;
import com.timsu.astrid.R;
import com.todoroo.andlib.utility.AndroidUtilities;
import com.todoroo.astrid.data.StoreObject;
import com.todoroo.astrid.producteev.sync.ProducteevDashboard;
import com.todoroo.astrid.producteev.sync.ProducteevDataService;
import com.todoroo.astrid.producteev.sync.ProducteevSyncProvider;
import com.todoroo.astrid.sync.SyncProviderPreferences;
import com.todoroo.astrid.sync.SyncProviderUtilities;
/**
* Displays synchronization preferences and an action panel so users can
* initiate actions from the menu.
*
* @author timsu
*
*/
public class ProducteevPreferences extends SyncProviderPreferences {
@Override
public int getPreferenceResource() {
return R.xml.preferences_producteev;
}
@Override
public void startSync() {
new ProducteevSyncProvider().synchronize(this);
finish();
}
@Override
public void logOut() {
new ProducteevSyncProvider().signOut();
}
@Override
public SyncProviderUtilities getUtilities() {
return ProducteevUtilities.INSTANCE;
}
@Override
protected void onPause() {
super.onPause();
new ProducteevBackgroundService().scheduleService();
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ListPreference defaultDash = (ListPreference)findPreference(getString(R.string.producteev_PPr_defaultdash_key));
String[] entries, entryValues;
StoreObject[] dashboards = ProducteevDataService.getInstance().getDashboards();
if(ProducteevUtilities.INSTANCE.isLoggedIn() && dashboards.length > 0) {
entries = new String[dashboards.length + 1];
entryValues = new String[dashboards.length + 1];
for(int i = 0; i < dashboards.length; i++) {
entries[i + 1] = dashboards[i].getValue(ProducteevDashboard.NAME);
entryValues[i + 1] = Long.toString(dashboards[i].getValue(ProducteevDashboard.REMOTE_ID));
}
} else {
entries = new String[2];
entries[1] = getString(R.string.producteev_default_dashboard);
entryValues = new String[2];
entryValues[1] = Integer.toString(ProducteevUtilities.DASHBOARD_DEFAULT);
}
entries[0] = getString(R.string.producteev_no_dashboard);
entryValues[0] = Integer.toString(ProducteevUtilities.DASHBOARD_NO_SYNC);
defaultDash.setEntries(entries);
defaultDash.setEntryValues(entryValues);
}
@Override
public void updatePreferences(Preference preference, Object value) {
super.updatePreferences(preference, value);
final Resources r = getResources();
if (r.getString(R.string.producteev_PPr_defaultdash_key).equals(
preference.getKey())) {
int index = AndroidUtilities.indexOf(((ListPreference)preference).getEntryValues(), (String)value);
if(index == -1)
index = 1;
if(index == 0)
preference.setSummary(R.string.producteev_PPr_defaultdash_summary_none);
else
preference.setSummary(r.getString(
R.string.producteev_PPr_defaultdash_summary,
((ListPreference)preference).getEntries()[index]));
}
}
}

@ -1,28 +0,0 @@
/**
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.producteev;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import com.todoroo.andlib.service.ContextManager;
import com.todoroo.astrid.service.AstridDependencyInjector;
public class ProducteevStartupReceiver extends BroadcastReceiver {
static {
AstridDependencyInjector.initialize();
}
@Override
/** Called when device is restarted */
public void onReceive(final Context context, Intent intent) {
ContextManager.setContext(context);
new ProducteevBackgroundService().scheduleService();
}
}

@ -1,43 +0,0 @@
/**
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.producteev;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import com.timsu.astrid.R;
import com.todoroo.andlib.service.ContextManager;
import com.todoroo.astrid.api.AstridApiConstants;
import com.todoroo.astrid.api.SyncAction;
/**
* Exposes sync action
*
*/
public class ProducteevSyncActionExposer extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
ContextManager.setContext(context);
// if we aren't logged in, don't expose sync action
if(!ProducteevUtilities.INSTANCE.isLoggedIn())
return;
Intent syncIntent = new Intent(null, null,
context, ProducteevBackgroundService.class);
PendingIntent pendingIntent = PendingIntent.getService(context, 0, syncIntent, PendingIntent.FLAG_UPDATE_CURRENT);
SyncAction syncAction = new SyncAction(context.getString(R.string.producteev_PPr_header),
pendingIntent);
Intent broadcastIntent = new Intent(AstridApiConstants.BROADCAST_SEND_SYNC_ACTIONS);
broadcastIntent.putExtra(AstridApiConstants.EXTRAS_ADDON, ProducteevUtilities.IDENTIFIER);
broadcastIntent.putExtra(AstridApiConstants.EXTRAS_RESPONSE, syncAction);
context.sendBroadcast(broadcastIntent, AstridApiConstants.PERMISSION_READ);
}
}

@ -1,84 +0,0 @@
/**
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.producteev;
import com.timsu.astrid.R;
import com.todoroo.andlib.utility.Preferences;
import com.todoroo.astrid.sync.SyncProviderUtilities;
/**
* Displays synchronization preferences and an action panel so users can
* initiate actions from the menu.
*
* @author timsu
*
*/
public class ProducteevUtilities extends SyncProviderUtilities {
/** add-on identifier */
public static final String IDENTIFIER = "pdv"; //$NON-NLS-1$
public static final ProducteevUtilities INSTANCE = new ProducteevUtilities();
/** setting for dashboard to getting created */
public static final int DASHBOARD_CREATE = -2;
/** setting for dashboard to not synchronize */
public static final int DASHBOARD_NO_SYNC = -1;
/** setting for dashboard to use default one */
public static final int DASHBOARD_DEFAULT = 0;
@Override
public String getIdentifier() {
return IDENTIFIER;
}
@Override
public int getSyncIntervalKey() {
return R.string.producteev_PPr_interval_key;
}
// --- producteev-specific preferences
public static final String PREF_SERVER_LAST_SYNC = IDENTIFIER + "_last_server"; //$NON-NLS-1$
public static final String PREF_SERVER_LAST_NOTIFICATION = IDENTIFIER + "_last_notification"; //$NON-NLS-1$
public static final String PREF_SERVER_LAST_ACTIVITY = IDENTIFIER + "_last_activity"; //$NON-NLS-1$
/** Producteev user's default dashboard. This is different from the
* preference key, which indicates where user wants to put new tasks */
public static final String PREF_DEFAULT_DASHBOARD = IDENTIFIER + "_defaultdash"; //$NON-NLS-1$
/** Producteev user's id */
public static final String PREF_USER_ID = IDENTIFIER + "_userid"; //$NON-NLS-1$
/**
* Gets default dashboard from setting
* @return DASHBOARD_NO_SYNC if should not sync, otherwise remote id
*/
public long getDefaultDashboard() {
int defaultDashboard = Preferences.getIntegerFromString(R.string.producteev_PPr_defaultdash_key,
DASHBOARD_DEFAULT);
if(defaultDashboard == DASHBOARD_NO_SYNC)
return DASHBOARD_NO_SYNC;
else if(defaultDashboard == DASHBOARD_DEFAULT)
return Preferences.getLong(PREF_DEFAULT_DASHBOARD, 0);
else
return (long) defaultDashboard;
}
private ProducteevUtilities() {
// prevent instantiation
}
@Override
public String getLoggedInUserName() {
return Preferences.getStringValue(R.string.producteev_PPr_email);
}
}

@ -1,23 +0,0 @@
/**
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.producteev.api;
/**
* Exception that is thrown when an authentication exception occurs and users
* need to sign in
*
* @author timsu
*
*/
public class ApiAuthenticationException extends ApiServiceException {
private static final long serialVersionUID = 1696103465107607150L;
public ApiAuthenticationException(String detailMessage) {
super(detailMessage);
}
}

@ -1,24 +0,0 @@
/**
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.producteev.api;
/**
* Exception that wraps an exception encountered during API invocation or
* processing.
*
* @author timsu
*
*/
public class ApiResponseParseException extends ApiServiceException {
private static final long serialVersionUID = 5421855785088364483L;
public ApiResponseParseException(Throwable throwable) {
super("Exception reading API response: " + throwable.getMessage()); //$NON-NLS-1$
initCause(throwable);
}
}

@ -1,40 +0,0 @@
/**
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.producteev.api;
import java.io.IOException;
/**
* Exception that wraps an exception encountered during API invocation or
* processing.
*
* @author timsu
*
*/
public class ApiServiceException extends IOException {
private static final long serialVersionUID = 8805573304840404684L;
public ApiServiceException(String detailMessage) {
super(detailMessage);
}
public ApiServiceException(Throwable throwable) {
super(throwable.getMessage());
initCause(throwable);
}
public ApiServiceException() {
super();
}
@Override
public String toString() {
return getClass().getSimpleName() + ": " + getMessage(); //$NON-NLS-1$
}
}

@ -1,23 +0,0 @@
/**
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.producteev.api;
/**
* Exception that wraps a 403 exception
*
* @author timsu
*
*/
public class ApiSignatureException extends ApiServiceException {
private static final long serialVersionUID = 4320984373933L;
public ApiSignatureException(String detailMessage) {
super(detailMessage);
}
}

@ -1,112 +0,0 @@
/**
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.producteev.api;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import org.json.JSONObject;
import com.todoroo.andlib.service.ContextManager;
import com.todoroo.andlib.utility.DateUtilities;
import com.todoroo.astrid.data.Metadata;
import com.todoroo.astrid.notes.NoteMetadata;
import com.todoroo.astrid.producteev.sync.ProducteevDataService;
/**
* Utilities for working with API responses and JSON objects
*
* @author timsu
*
*/
public final class ApiUtilities {
private static final SimpleDateFormat timeParser = new SimpleDateFormat(
"EEE, dd MMM yyyy HH:mm:ss Z", Locale.US); //$NON-NLS-1$
private static final SimpleDateFormat timeWriter = new SimpleDateFormat(
"yyyy/MM/dd HH:mm:ss Z", Locale.US); //$NON-NLS-1$
private static final SimpleDateFormat dateWriter = new SimpleDateFormat(
"yyyy/MM/dd", Locale.US); //$NON-NLS-1$
/**
* Utility method to convert PDV time to unix time
*
* @param date
* @param defaultValue
* @return
*/
public static long producteevToUnixTime(String value, long defaultValue) {
synchronized(timeParser) {
try {
return DateUtilities.dateToUnixtime(timeParser.parse(value));
} catch (ParseException e) {
return defaultValue;
}
}
}
/**
* Utility method to convert unix time to PDV time
* @param time
* @return
*/
public static String unixTimeToProducteev(long time) {
synchronized(timeWriter) {
return timeWriter.format(DateUtilities.unixtimeToDate(time));
}
}
/**
* Utility method to convert unix date to PDV date
* @param time
* @return
*/
public static String unixDateToProducteev(long date) {
synchronized(dateWriter) {
return dateWriter.format(DateUtilities.unixtimeToDate(date));
}
}
/**
* Un-escape a Producteev string
* @param string
* @return
*/
public static String decode(String string) {
return StringEscapeUtils.unescapeHtml(string);
}
/**
* Create metadata from json object
* @param note JSON object with params id_note and message
* @param creatorName
* @return
*/
@SuppressWarnings("nls")
public static Metadata createNoteMetadata(JSONObject note, String creatorName) {
Metadata metadata = new Metadata();
metadata.setValue(Metadata.KEY, NoteMetadata.METADATA_KEY);
metadata.setValue(NoteMetadata.EXT_ID, note.optString("id_note"));
metadata.setValue(NoteMetadata.EXT_PROVIDER, ProducteevDataService.NOTE_PROVIDER);
metadata.setValue(NoteMetadata.BODY, ApiUtilities.decode(note.optString("message")));
long created = ApiUtilities.producteevToUnixTime(note.optString("time_create"), 0);
metadata.setValue(Metadata.CREATION_DATE, created);
if(creatorName != null)
metadata.setValue(NoteMetadata.TITLE, creatorName);
else
metadata.setValue(NoteMetadata.TITLE, DateUtilities.getDateStringWithWeekday(ContextManager.getContext(),
new Date(created)));
return metadata;
}
}

@ -1,682 +0,0 @@
/**
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.producteev.api;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import org.apache.commons.codec.digest.DigestUtils;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import android.text.TextUtils;
import com.todoroo.andlib.utility.Pair;
@SuppressWarnings("nls")
public class ProducteevInvoker {
private final String URL = "https://api.producteev.com/";
private final String apiKey;
private final String apiSecret;
/** saved credentials in case we need to re-log in */
private String retryEmail;
private String retryPassword;
private String token = null;
/**
* Create new producteev service
* @param apiKey
* @param apiSecret
*/
public ProducteevInvoker(String apiKey, String apiSecret) {
this.apiKey = apiKey;
this.apiSecret = apiSecret;
}
// --- authentication and time
/**
* Authenticate the given user
*/
public void authenticate(String email, String password) throws IOException, ApiServiceException {
retryEmail = email;
retryPassword = password;
JSONObject response = invokeGet("users/login.json",
"email", email, "password", password);
try {
token = response.getJSONObject("login").getString("token");
} catch (JSONException e) {
throw new ApiServiceException(e);
}
}
public boolean hasToken() {
return token != null;
}
public void setCredentials(String token, String email, String password) {
this.token = token;
retryEmail = email;
retryPassword = password;
}
public String getToken() {
return token;
}
/**
* Gets Server time
*/
public String time() throws IOException, ApiServiceException {
JSONObject response = invokeGet("time.json");
try {
return response.getJSONObject("time").getString("value");
} catch (JSONException e) {
throw new ApiServiceException(e);
}
}
// --- users
/**
* Sign up as the given user
*/
public JSONObject usersSignUp(String email, String firstName, String lastName, String
password, String timezone, Long fbUid) throws IOException, ApiServiceException {
return invokeGet("users/signup.json",
"email", email,
"firstname", firstName,
"lastname", lastName,
"password", password,
"timezone", timezone,
"fbuid", fbUid);
}
// --- dashboards
/**
* show list
*
* @param idResponsible (optional) if null return every task for current user
* @param since (optional) if not null, the function only returns tasks modified or created since this date
*
* @return array tasks/view
*/
public JSONArray dashboardsShowList(String since) throws ApiServiceException, IOException {
return getResponse(callAuthenticated("dashboards/show_list.json",
"token", token,
"since", since), "dashboards");
}
/**
* create a dasbhoard
*
* @param name
* @return the new created dashboard as JSONObject
*/
public JSONObject dashboardsCreate(String name) throws ApiServiceException, IOException {
return callAuthenticated("dashboards/create.json",
"token", token,
"title", name);
}
/**
* return the list of users who can access a specific dashboard
*
* @param idDashboard
* @param dashboard array-information about the dashboard, if this ...
*/
public JSONArray dashboardsAccess(long idDashboard, String dashboard) throws ApiServiceException, IOException {
return getResponse(callAuthenticated("dashboards/access.json",
"token", token,
"id_dashboard", idDashboard,
"dashboard", dashboard),"dashboard");
}
// --- tasks
/**
* create a task
*
* @param title
* @param idResponsible (optional)
* @param idDashboard (optional);
* @param deadline (optional)
* @param reminder (optional) 0 = "None", 1 = "5 minutes before", 2 = "15 minutes before", 3 = "30 minutes before", 4 = "1 hour before", 5 = "2 hours before", 6 = "1 day before", 7 = "2 days before", 8 = "on date of event"
* @param status (optional) (1 = UNDONE, 2 = DONE)
* @param star (optional) (0 to 5 stars)
*
* @return task
*/
public JSONObject tasksCreate(String title, Long idResponsible, Long idDashboard,
String deadline, Integer reminder, Integer status, Integer star) throws ApiServiceException, IOException {
return callAuthenticated("tasks/create.json",
"token", token,
"title", title,
"id_responsible", idResponsible,
"id_dashboard", idDashboard,
"deadline", deadline,
"reminder", reminder,
"status", status,
"star", star);
}
/**
* show list
*
* @param idResponsible (optional) if null return every task for current user
* @param since (optional) if not null, the function only returns tasks modified or created since this date
*
* @return array tasks/view
*/
public JSONArray tasksShowList(Long idDashboard, String since) throws ApiServiceException, IOException {
return getResponse(callAuthenticated("tasks/show_list.json",
"token", token,
"id_dashboard", idDashboard,
"since", since), "tasks");
}
/**
* get a task
*
* @param idTask
*
* @return array tasks/view
*/
public JSONObject tasksView(Long idTask) throws ApiServiceException, IOException {
return callAuthenticated("tasks/view.json",
"token", token,
"id_task", idTask);
}
/**
* change title of a task
*
* @param idTask
* @param title
*
* @return array tasks/view
*/
public JSONObject tasksSetTitle(long idTask, String title) throws ApiServiceException, IOException {
return callAuthenticated("tasks/set_title.json",
"token", token,
"id_task", idTask,
"title", title);
}
/**
* set status of a task
*
* @param idTask
* @param status (1 = UNDONE, 2 = DONE)
*
* @return array tasks/view
*/
public JSONObject tasksSetStatus(long idTask, int status) throws ApiServiceException, IOException {
return callAuthenticated("tasks/set_status.json",
"token", token,
"id_task", idTask,
"status", status);
}
/**
* set star status of a task
*
* @param idTask
* @param star (0 to 5 stars)
*
* @return array tasks/view
*/
public JSONObject tasksSetStar(long idTask, int star) throws ApiServiceException, IOException {
return callAuthenticated("tasks/set_star.json",
"token", token,
"id_task", idTask,
"star", star);
}
/**
* set a deadline
*
* @param idTask
* @param deadline
* @param allDay (optional), 1: all day task (time specified in deadline will be ignored), 0: deadline with time
*
* @return array tasks/view
*/
public JSONObject tasksSetDeadline(long idTask, String deadline, Integer allDay) throws ApiServiceException, IOException {
return callAuthenticated("tasks/set_deadline.json",
"token", token,
"id_task", idTask,
"deadline", deadline,
"all_day", allDay);
}
/**
* unset a deadline
*
* @param idTask
*
* @return array tasks/view
*/
public JSONObject tasksUnsetDeadline(long idTask) throws ApiServiceException, IOException {
return callAuthenticated("tasks/unset_deadline.json",
"token", token,
"id_task", idTask);
}
/**
* set repeating
*
* @param idTask
* @param repeatInterval
* @param repeatValue
*
* @return array tasks/view
* @throws IOException
* @throws ApiServiceException
*/
public JSONObject tasksSetRepeating(long idTask, String repeatInterval, Integer repeatValue) throws ApiServiceException, IOException {
return callAuthenticated("tasks/set_repeating.json",
"token", token,
"id_task", idTask,
"repeat_interval", repeatInterval,
"repeat_value", (repeatValue == null ? 1 : repeatValue));
}
/**
* unset repeating
*
* @param idTask
*
* @return array tasks/view
* @throws IOException
* @throws ApiServiceException
*/
public JSONObject tasksUnsetRepeating(long idTask) throws ApiServiceException, IOException {
return callAuthenticated("tasks/unset_repeating.json",
"token", token,
"id_task", idTask);
}
/**
* set a workspace
*
* @param idTask
* @param id_dashboard
*
* @return array tasks/view
*/
public JSONObject tasksSetWorkspace(long idTask, long idDashboard) throws ApiServiceException, IOException {
return callAuthenticated("tasks/set_workspace.json",
"token", token,
"id_task", idTask,
"id_dashboard", idDashboard);
}
/**
* delete a task
*
* @param idTask
*
* @return array with the result = (Array("stats" => Array("result" => "TRUE|FALSE"))
*/
public JSONObject tasksDelete(long idTask) throws ApiServiceException, IOException {
return callAuthenticated("tasks/delete.json",
"token", token,
"id_task", idTask);
}
/**
* get labels assigned to a task
*
* @param idTask
*
* @return array: list of labels/view
*/
public JSONArray tasksLabels(long idTask) throws ApiServiceException, IOException {
return getResponse(callAuthenticated("tasks/labels.json",
"token", token,
"id_task", idTask), "labels");
}
/**
* change labels for a task
*
* @param idTask
* @param idLabels
*
* @return array: tasks/view
*/
public JSONObject tasksChangeLabel(long idTask, long... idLabels) throws ApiServiceException, IOException {
Object[] parameters = new Object[2 * (idLabels.length + 2)];
parameters[0] = "token"; parameters[1] = token;
parameters[2] = "id_task"; parameters[3] = idTask;
for(int i = 0; i < idLabels.length; i++) {
parameters[i * 2 + 4] = "id_label[]";
parameters[i * 2 + 5] = idLabels[i];
}
return callAuthenticated("tasks/change_labels.json", parameters);
}
/**
* set a labels to a task
*
* @param idTask
* @param idLabels
*
* @return array: tasks/view
*/
@Deprecated
public JSONObject tasksSetLabel(long idTask, long idLabel) throws ApiServiceException, IOException {
return callAuthenticated("tasks/set_label.json",
"token", token,
"id_task", idTask,
"id_label", idLabel);
}
/**
* set a labels to a task
*
* @param idTask
* @param idLabel
*
* @return array: tasks/view
*/
@Deprecated
public JSONObject tasksUnsetLabel(long idTask, long idLabel) throws ApiServiceException, IOException {
return callAuthenticated("tasks/unset_label.json",
"token", token,
"id_task", idTask,
"id_label", idLabel);
}
/**
* change responsible of a task
*
* @param idTask
* @param idResponsible
*
* @return array: tasks/view
*/
public JSONObject tasksSetResponsible(long idTask, long idResponsible) throws ApiServiceException, IOException {
return callAuthenticated("tasks/set_responsible.json",
"token", token,
"id_task", idTask,
"id_responsible", idResponsible);
}
/**
* change responsible of a task
*
* @param idTask
* @param idResponsible
*
* @return array: tasks/view
*/
public JSONObject tasksUnsetResponsible(long idTask) throws ApiServiceException, IOException {
return callAuthenticated("tasks/unset_responsible.json",
"token", token,
"id_task", idTask);
}
/**
* create a note attached to a task
*
* @param idTask
* @param message
*
* @return array tasks::note_view
*/
public JSONObject tasksNoteCreate(long idTask, String message) throws ApiServiceException, IOException {
return callAuthenticated("tasks/note_create.json",
"token", token,
"id_task", idTask,
"message", message);
}
// --- labels
/**
* get every label for a given dashboard
*
* @param idTask
* @param idLabel
*
* @return array: labels/view
*/
public JSONArray labelsShowList(long idDashboard, String since) throws ApiServiceException, IOException {
return getResponse(callAuthenticated("labels/show_list.json",
"token", token,
"id_dashboard", idDashboard,
"since", since), "labels");
}
/**
* create a label
*
* @param idDashboard
* @param title
*
* @return array: labels/view
*/
public JSONObject labelsCreate(long idDashboard, String title) throws ApiServiceException, IOException {
return callAuthenticated("labels/create.json",
"token", token,
"id_dashboard", idDashboard,
"title", title);
}
public JSONObject labelsDelete(long idLabel) throws ApiServiceException, IOException {
return callAuthenticated("labels/delete.json",
"token", token,
"id_label", idLabel);
}
// --- notifications/activities
/**
* get every activities
*
* @param dashboardId (optional) if not null, this function only returns notifications for this specific dashboard
* @param lastId (optional) this function returns only activities later than this id
*/
public JSONArray activitiesShowActivities(Long dashboardId, Long lastId) throws ApiResponseParseException, ApiServiceException, IOException {
return getResponse(callAuthenticated("activities/show_activities.json",
"token", token,
"id_dashboard", dashboardId,
"last_id", lastId), "activities");
}
/**
* get every notification for the current user
* @param dashboardId
* @param lastId
* @return
* @throws ApiResponseParseException
* @throws ApiServiceException
* @throws IOException
*/
public JSONArray activitiesShowNotifications(Long dashboardId, Long lastId) throws ApiResponseParseException, ApiServiceException, IOException {
return getResponse(callAuthenticated("activities/show_notifications.json",
"token", token,
"id_dashboard", dashboardId,
"last_id", lastId), "activities");
}
// --- users
/**
* get a user
*
* @param idColleague
*
* @return array information about the user
*/
public JSONObject usersView(Long idColleague) throws ApiServiceException, IOException {
return callAuthenticated("users/view.json",
"token", token,
"id_colleague", idColleague);
}
public JSONObject usersColleagues() throws ApiServiceException, IOException {
return callAuthenticated("users/colleagues.json",
"token", token);
}
// --- invocation
private final ProducteevRestClient restClient = new ProducteevRestClient();
/**
* Invokes authenticated method using HTTP GET. Will retry after re-authenticating if service exception encountered
*
* @param method
* API method to invoke
* @param getParameters
* Name/Value pairs. Values will be URL encoded.
* @return response object
*/
private JSONObject callAuthenticated(String method, Object... getParameters)
throws IOException, ApiServiceException {
try {
String request = createFetchUrl(method, getParameters);
String response = null;
try {
response = restClient.get(request);
} catch (ApiSignatureException e) {
// clear cookies, get new token, retry
String oldToken = token;
restClient.reset();
authenticate(retryEmail, retryPassword);
for(int i = 0; i < getParameters.length; i++)
if(oldToken.equals(getParameters[i])) {
getParameters[i] = getToken();
}
request = createFetchUrl(method, getParameters);
try {
response = restClient.get(request);
} catch (ApiSignatureException newException) {
//
}
if(response == null)
throw e;
}
if(response.startsWith("DEBUG MESSAGE")) {
System.err.println(response);
return new JSONObject();
}
try {
if(TextUtils.isEmpty(response))
return new JSONObject();
return new JSONObject(response);
} catch (JSONException e) {
System.err.println(response);
throw new ApiResponseParseException(e);
}
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
/**
* Invokes API method using HTTP GET
*
* @param method
* API method to invoke
* @param getParameters
* Name/Value pairs. Values will be URL encoded.
* @return response object
*/
JSONObject invokeGet(String method, Object... getParameters) throws IOException, ApiServiceException {
try {
String request = createFetchUrl(method, getParameters);
String response = restClient.get(request);
if(response.startsWith("DEBUG MESSAGE")) {
System.err.println(response);
return new JSONObject();
}
return new JSONObject(response);
} catch (JSONException e) {
throw new ApiResponseParseException(e);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
/**
* Creates a URL for invoking an HTTP GET/POST on the given method
* @param method
* @param getParameters
* @return
* @throws UnsupportedEncodingException
* @throws NoSuchAlgorithmException
*/
String createFetchUrl(String method, Object... getParameters) throws UnsupportedEncodingException, NoSuchAlgorithmException {
ArrayList<Pair<String, Object>> params = new ArrayList<Pair<String, Object>>();
for(int i = 0; i < getParameters.length; i += 2)
params.add(new Pair<String, Object>(getParameters[i].toString(), getParameters[i+1]));
params.add(new Pair<String, Object>("api_key", apiKey));
Collections.sort(params, new Comparator<Pair<String, Object>>() {
@Override
public int compare(Pair<String, Object> object1,
Pair<String, Object> object2) {
return object1.getLeft().compareTo(object2.getLeft());
}
});
StringBuilder requestBuilder = new StringBuilder(URL).append(method).append('?');
StringBuilder sigBuilder = new StringBuilder();
for(Pair<String, Object> entry : params) {
if(entry.getRight() == null)
continue;
String key = entry.getLeft();
String value = entry.getRight().toString();
String encoded = URLEncoder.encode(value, "UTF-8");
requestBuilder.append(key).append('=').append(encoded).append('&');
if(!key.endsWith("[]"))
sigBuilder.append(key).append(value);
}
sigBuilder.append(apiSecret);
String signature = DigestUtils.md5Hex(sigBuilder.toString());
requestBuilder.append("api_sig").append('=').append(signature);
return requestBuilder.toString();
}
/**
* Helper method to get a field out or throw an api exception
* @param response
* @param field
* @return
* @throws ApiResponseParseException
*/
private JSONArray getResponse(JSONObject response, String field) throws ApiResponseParseException {
try {
if(!response.has(field))
return new JSONArray();
return response.getJSONArray(field);
} catch (JSONException e) {
throw new ApiResponseParseException(e);
}
}
}

@ -1,188 +0,0 @@
/**
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.producteev.api;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams;
import org.json.JSONObject;
import com.todoroo.andlib.service.RestClient;
import com.todoroo.astrid.utility.Constants;
/**
* RestClient allows Android to consume web requests.
* <p>
* Portions by Praeda:
* http://senior.ceng.metu.edu.tr/2009/praeda/2009/01/11/a-simple
* -restful-client-at-android/
*
* @author Tim Su <tim@todoroo.com>
*
*/
public class ProducteevRestClient implements RestClient {
private static final int HTTP_OK = 200;
private static final int TIMEOUT_MILLIS = 30000;
private static HttpClient httpClient = null;
private static String convertStreamToString(InputStream is) {
/*
* To convert the InputStream to String we use the
* BufferedReader.readLine() method. We iterate until the BufferedReader
* return null which means there's no more data to read. Each line will
* appended to a StringBuilder and returned as String.
*/
BufferedReader reader = new BufferedReader(new InputStreamReader(is), 16384);
StringBuilder sb = new StringBuilder();
String line = null;
try {
while ((line = reader.readLine()) != null) {
sb.append(line + "\n"); //$NON-NLS-1$
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return sb.toString();
}
private synchronized static void initializeHttpClient() {
if (httpClient == null) {
HttpParams params = new BasicHttpParams();
HttpConnectionParams.setConnectionTimeout(params, TIMEOUT_MILLIS);
HttpConnectionParams.setSoTimeout(params, TIMEOUT_MILLIS);
httpClient = new DefaultHttpClient(params);
}
}
private String processHttpResponse(HttpResponse response) throws IOException, ApiServiceException {
HttpEntity entity = response.getEntity();
String body = null;
if (entity != null) {
InputStream contentStream = entity.getContent();
try {
body = convertStreamToString(contentStream);
} finally {
contentStream.close();
}
}
if(Constants.DEBUG)
System.err.println(body);
int statusCode = response.getStatusLine().getStatusCode();
if(statusCode != HTTP_OK || (body != null && body.startsWith("{\"error\":"))) { //$NON-NLS-1$
ApiServiceException error;
try {
JSONObject errorObject = new JSONObject(body).getJSONObject("error"); //$NON-NLS-1$
String errorMessage = errorObject.getString("message"); //$NON-NLS-1$
if(statusCode == 403)
error = new ApiSignatureException(errorMessage);
else if(statusCode == 401)
error = new ApiAuthenticationException(errorMessage);
else
error = new ApiServiceException(errorMessage);
} catch (Exception e) {
if(statusCode == 401)
error = new ApiAuthenticationException(response.getStatusLine().getReasonPhrase());
else
error = new ApiServiceException(response.getStatusLine() +
"\n" + body); //$NON-NLS-1$
}
throw error;
}
return body;
}
/**
* Issue an HTTP GET for the given URL, return the response
*
* @param url url with url-encoded params
* @return response, or null if there was no response
* @throws IOException
*/
public synchronized String get(String url) throws IOException {
initializeHttpClient();
if(Constants.DEBUG)
System.err.println("GET: " + url); //$NON-NLS-1$ // (debug)
try {
HttpGet httpGet = new HttpGet(url);
HttpResponse response = httpClient.execute(httpGet);
return processHttpResponse(response);
} catch (IOException e) {
throw e;
} catch (Exception e) {
IOException ioException = new IOException(e.getMessage());
ioException.initCause(e);
throw ioException;
}
}
/**
* Issue an HTTP POST for the given URL, return the response
*
* @param url
* @param data
* url-encoded data
* @param headers
* @throws IOException
*/
public synchronized String post(String url, HttpEntity data, Header... headers) throws IOException {
initializeHttpClient();
if(Constants.DEBUG)
System.err.println("POST: " + url); //$NON-NLS-1$ // (debug)
try {
HttpPost httpPost = new HttpPost(url);
httpPost.setEntity(data);
HttpResponse response = httpClient.execute(httpPost);
return processHttpResponse(response);
} catch (IOException e) {
throw e;
} catch (Exception e) {
IOException ioException = new IOException(e.getMessage());
ioException.initCause(e);
throw ioException;
}
}
/**
* Destroy and re-create http client
*/
public void reset() {
httpClient = null;
}
}

@ -1,123 +0,0 @@
/**
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.producteev.sync;
import java.util.ArrayList;
import java.util.StringTokenizer;
import com.todoroo.andlib.data.Property.LongProperty;
import com.todoroo.andlib.data.Property.StringProperty;
import com.todoroo.astrid.data.StoreObject;
/**
* {@link StoreObject} entries for a Producteev Dashboard
*
* @author Tim Su <tim@todoroo.com>
*
*/
public class ProducteevDashboard {
/** type*/
public static final String TYPE = "pdv-dash"; //$NON-NLS-1$
/** dashboard id in producteev */
public static final LongProperty REMOTE_ID = new LongProperty(StoreObject.TABLE,
StoreObject.ITEM.name);
/** dashboard name */
public static final StringProperty NAME = new StringProperty(StoreObject.TABLE,
StoreObject.VALUE1.name);
/** users (list in the format "id_user,name;id_user,name;") */
public static final StringProperty USERS = new StringProperty(StoreObject.TABLE,
StoreObject.VALUE2.name);
// data class-part
private final long id;
private final String name;
private ArrayList<ProducteevUser> users = null;
public ProducteevDashboard (StoreObject dashboardData) {
this(dashboardData.getValue(REMOTE_ID),dashboardData.getValue(NAME),(dashboardData.containsValue(USERS)?dashboardData.getValue(USERS):null));
}
/**
* Constructor for a dashboard.
*
* @param id id of the remote dashboard
* @param name name of the remote dashboard
* @param usercsv csv-userstring as returned by a StoreObject-dashboard with property ProducteevDashboard.USERS
*/
@SuppressWarnings("nls")
public ProducteevDashboard(long id, String name, String usercsv) {
this.id = id;
this.name = name;
if (usercsv == null)
return;
StringTokenizer tokenizer = new StringTokenizer(usercsv, ";");
int usercount = tokenizer.countTokens();
while (tokenizer.hasMoreTokens()) {
String userdata = tokenizer.nextToken();
int delim_index = userdata.indexOf(",");
String userid = userdata.substring(0, delim_index);
String username = userdata.substring(delim_index+1);
int name_gap = username.indexOf(" ");
String firstname = (name_gap == -1 ? username : username.substring(0,name_gap));
String lastname = (name_gap == -1 ? null : username.substring(name_gap+1));
if (users == null) {
users = new ArrayList<ProducteevUser>(usercount);
}
users.add(new ProducteevUser(Long.parseLong(userid),null,firstname,lastname));
}
}
/**
* @return the id
*/
public long getId() {
return id;
}
/**
* @return the name
*/
public String getName() {
return name;
}
/**
* return the name of this dashboard
*/
@Override
public String toString() {
return name;
}
/**
* @return the users
*/
public ArrayList<ProducteevUser> getUsers() {
if (users == null) {
return new ArrayList<ProducteevUser>(); // Don't return null
}
return users;
}
/** Try and find user in the dashboard. return null if un-findable */
public static String getUserFromDashboard(StoreObject dashboard, long userId) {
String users = ";" + dashboard.getValue(USERS); //$NON-NLS-1$
int index = users.indexOf(";" + userId + ","); //$NON-NLS-1$ //$NON-NLS-2$
if(index > -1)
return users.substring(users.indexOf(',', index) + 1,
users.indexOf(';', index + 1));
return null;
}
}

@ -1,310 +0,0 @@
/**
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.producteev.sync;
import java.util.ArrayList;
import java.util.Random;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import android.content.Context;
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.Join;
import com.todoroo.andlib.sql.Query;
import com.todoroo.astrid.core.PluginServices;
import com.todoroo.astrid.dao.MetadataDao.MetadataCriteria;
import com.todoroo.astrid.dao.StoreObjectDao;
import com.todoroo.astrid.dao.StoreObjectDao.StoreObjectCriteria;
import com.todoroo.astrid.dao.TaskDao;
import com.todoroo.astrid.dao.TaskDao.TaskCriteria;
import com.todoroo.astrid.data.Metadata;
import com.todoroo.astrid.data.StoreObject;
import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.notes.NoteMetadata;
import com.todoroo.astrid.producteev.ProducteevUtilities;
import com.todoroo.astrid.producteev.api.ApiUtilities;
import com.todoroo.astrid.service.MetadataService;
import com.todoroo.astrid.tags.TaskToTagMetadata;
public final class ProducteevDataService {
// --- constants
/** Utility for joining tasks with metadata */
public static final Join METADATA_JOIN = Join.left(Metadata.TABLE, Task.ID.eq(Metadata.TASK));
/** NoteMetadata provider string */
public static final String NOTE_PROVIDER = "producteev"; //$NON-NLS-1$
// --- singleton
private static ProducteevDataService instance = null;
public static synchronized ProducteevDataService getInstance() {
if(instance == null)
instance = new ProducteevDataService(ContextManager.getContext());
return instance;
}
// --- instance variables
protected final Context context;
@Autowired
private TaskDao taskDao;
@Autowired
private MetadataService metadataService;
@Autowired
private StoreObjectDao storeObjectDao;
private final ProducteevUtilities preferences = ProducteevUtilities.INSTANCE;
static final Random random = new Random();
private ProducteevDataService(Context context) {
this.context = context;
DependencyInjectionService.getInstance().inject(this);
}
// --- task and metadata methods
/**
* Clears metadata information. Used when user logs out of service
*/
public void clearMetadata() {
PluginServices.getTaskService().clearDetails(Task.ID.in(Query.select(Metadata.TASK).from(Metadata.TABLE).
where(MetadataCriteria.withKey(ProducteevTask.METADATA_KEY))));
metadataService.deleteWhere(Metadata.KEY.eq(ProducteevTask.METADATA_KEY));
storeObjectDao.deleteWhere(StoreObject.TYPE.eq(ProducteevDashboard.TYPE));
}
/**
* Gets tasks that were created since last sync
* @param properties
* @return
*/
public TodorooCursor<Task> getLocallyCreated(Property<?>[] properties) {
return
taskDao.query(Query.select(properties).join(ProducteevDataService.METADATA_JOIN).where(
Criterion.and(
Criterion.not(Task.ID.in(Query.select(Metadata.TASK).from(Metadata.TABLE).
where(Criterion.and(MetadataCriteria.withKey(ProducteevTask.METADATA_KEY), ProducteevTask.ID.gt(0))))),
TaskCriteria.isActive())).
groupBy(Task.ID));
}
/**
* Gets tasks that were modified since last sync
* @param properties
* @return null if never sync'd
*/
public TodorooCursor<Task> getLocallyUpdated(Property<?>[] properties) {
long lastSyncDate = preferences.getLastSyncDate();
if(lastSyncDate == 0)
return taskDao.query(Query.select(Task.ID).where(Criterion.none));
return
taskDao.query(Query.select(properties).join(ProducteevDataService.METADATA_JOIN).where(
Criterion.and(
MetadataCriteria.withKey(ProducteevTask.METADATA_KEY),
ProducteevTask.ID.gt(0),
Task.MODIFICATION_DATE.gt(lastSyncDate))).
groupBy(Task.ID));
}
/**
* Searches for a local task with same remote id, updates this task's id
* @param remoteTask
* @return true if found local match
*/
public boolean findLocalMatch(ProducteevTaskContainer remoteTask) {
if(remoteTask.task.getId() != Task.NO_ID)
return true;
TodorooCursor<Task> cursor = taskDao.query(Query.select(Task.ID).
join(ProducteevDataService.METADATA_JOIN).where(Criterion.and(MetadataCriteria.withKey(ProducteevTask.METADATA_KEY),
ProducteevTask.ID.eq(remoteTask.pdvTask.getValue(ProducteevTask.ID)))));
try {
if(cursor.getCount() == 0)
return false;
cursor.moveToFirst();
remoteTask.task.setId(cursor.get(Task.ID));
return true;
} finally {
cursor.close();
}
}
/**
* Saves a task and its metadata
* @param task
*/
public void saveTaskAndMetadata(ProducteevTaskContainer task) {
taskDao.save(task.task);
task.metadata.add(task.pdvTask);
// note we don't include note metadata, since we only receive deltas
metadataService.synchronizeMetadata(task.task.getId(), task.metadata,
Criterion.or(MetadataCriteria.withKey(ProducteevTask.METADATA_KEY),
MetadataCriteria.withKey(TaskToTagMetadata.KEY)), true);
}
/**
* Reads a task and its metadata
* @param task
* @return
*/
public ProducteevTaskContainer readTaskAndMetadata(TodorooCursor<Task> taskCursor) {
Task task = new Task(taskCursor);
// read tags, notes, etc
ArrayList<Metadata> metadata = new ArrayList<Metadata>();
TodorooCursor<Metadata> metadataCursor = metadataService.query(Query.select(Metadata.PROPERTIES).
where(Criterion.and(MetadataCriteria.byTask(task.getId()),
Criterion.or(MetadataCriteria.withKey(TaskToTagMetadata.KEY),
MetadataCriteria.withKey(ProducteevTask.METADATA_KEY),
MetadataCriteria.withKey(NoteMetadata.METADATA_KEY)))));
try {
for(metadataCursor.moveToFirst(); !metadataCursor.isAfterLast(); metadataCursor.moveToNext()) {
metadata.add(new Metadata(metadataCursor));
}
} finally {
metadataCursor.close();
}
return new ProducteevTaskContainer(task, metadata);
}
/**
* Reads metadata out of a task
* @return null if no metadata found
*/
public Metadata getTaskMetadata(long taskId) {
TodorooCursor<Metadata> cursor = metadataService.query(Query.select(
Metadata.PROPERTIES).where(
MetadataCriteria.byTaskAndwithKey(taskId, ProducteevTask.METADATA_KEY)));
try {
if(cursor.getCount() == 0)
return null;
cursor.moveToFirst();
return new Metadata(cursor);
} finally {
cursor.close();
}
}
/**
* Reads task notes out of a task
*/
public TodorooCursor<Metadata> getTaskNotesCursor(long taskId) {
TodorooCursor<Metadata> cursor = metadataService.query(Query.select(Metadata.PROPERTIES).
where(MetadataCriteria.byTaskAndwithKey(taskId, NoteMetadata.METADATA_KEY)));
return cursor;
}
private void readDashboards() {
if (dashboards == null) {
dashboards = readStoreObjects(ProducteevDashboard.TYPE);
}
}
/**
* Reads store objects.
*/
public StoreObject[] readStoreObjects(String type) {
StoreObject[] ret;
TodorooCursor<StoreObject> cursor = storeObjectDao.query(Query.select(StoreObject.PROPERTIES).
where(StoreObjectCriteria.byType(type)));
try {
ret = new StoreObject[cursor.getCount()];
for(int i = 0; i < ret.length; i++) {
cursor.moveToNext();
StoreObject dashboard = new StoreObject(cursor);
ret[i] = dashboard;
}
} finally {
cursor.close();
}
return ret;
}
// --- dashboard methods
private StoreObject[] dashboards = null;
/**
* @return a list of dashboards
*/
public StoreObject[] getDashboards() {
readDashboards();
return dashboards;
}
/**
* Reads dashboards
* @throws JSONException
*/
@SuppressWarnings("nls")
public void updateDashboards(JSONArray changedDashboards) throws JSONException {
readDashboards();
for(int i = 0; i < changedDashboards.length(); i++) {
JSONObject remote = changedDashboards.getJSONObject(i).getJSONObject("dashboard");
updateDashboards(remote, false);
}
// clear dashboard cache
dashboards = null;
}
@SuppressWarnings("nls")
public StoreObject updateDashboards(JSONObject remote, boolean reinitCache) throws JSONException {
if (reinitCache)
readDashboards();
long id = remote.getLong("id_dashboard");
StoreObject local = null;
for(StoreObject dashboard : dashboards) {
if(dashboard.getValue(ProducteevDashboard.REMOTE_ID).equals(id)) {
local = dashboard;
break;
}
}
if(remote.getInt("deleted") != 0) {
if(local != null)
storeObjectDao.delete(local.getId());
}
if(local == null)
local = new StoreObject();
local.setValue(StoreObject.TYPE, ProducteevDashboard.TYPE);
local.setValue(ProducteevDashboard.REMOTE_ID, id);
local.setValue(ProducteevDashboard.NAME, ApiUtilities.decode(remote.getString("title")));
StringBuilder users = new StringBuilder();
JSONArray accessList = remote.getJSONArray("accesslist");
for(int j = 0; j < accessList.length(); j++) {
JSONObject user = accessList.getJSONObject(j).getJSONObject("user");
users.append(user.getLong("id_user")).append(',');
String name = ApiUtilities.decode(user.optString("firstname", "") + ' ' +
user.optString("lastname", ""));
users.append(name.trim()).append(';');
}
local.setValue(ProducteevDashboard.USERS, users.toString());
storeObjectDao.persist(local);
if (reinitCache)
dashboards = null;
return local;
}
}

@ -1,811 +0,0 @@
/**
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.producteev.sync;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import android.app.Activity;
import android.app.Notification;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.text.TextUtils;
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.service.ExceptionService;
import com.todoroo.andlib.service.NotificationManager;
import com.todoroo.andlib.sql.Criterion;
import com.todoroo.andlib.utility.AndroidUtilities;
import com.todoroo.andlib.utility.DateUtilities;
import com.todoroo.andlib.utility.Preferences;
import com.todoroo.astrid.activity.ShortcutActivity;
import com.todoroo.astrid.api.AstridApiConstants;
import com.todoroo.astrid.api.Filter;
import com.todoroo.astrid.core.PluginServices;
import com.todoroo.astrid.dao.TaskDao;
import com.todoroo.astrid.data.Metadata;
import com.todoroo.astrid.data.StoreObject;
import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.notes.NoteMetadata;
import com.todoroo.astrid.producteev.ProducteevBackgroundService;
import com.todoroo.astrid.producteev.ProducteevFilterExposer;
import com.todoroo.astrid.producteev.ProducteevLoginActivity;
import com.todoroo.astrid.producteev.ProducteevPreferences;
import com.todoroo.astrid.producteev.ProducteevUtilities;
import com.todoroo.astrid.producteev.api.ApiResponseParseException;
import com.todoroo.astrid.producteev.api.ApiServiceException;
import com.todoroo.astrid.producteev.api.ApiUtilities;
import com.todoroo.astrid.producteev.api.ProducteevInvoker;
import com.todoroo.astrid.service.AstridDependencyInjector;
import com.todoroo.astrid.service.StatisticsConstants;
import com.todoroo.astrid.service.StatisticsService;
import com.todoroo.astrid.sync.SyncContainer;
import com.todoroo.astrid.sync.SyncProvider;
import com.todoroo.astrid.sync.SyncProviderUtilities;
import com.todoroo.astrid.tags.TaskToTagMetadata;
import com.todoroo.astrid.utility.Constants;
@SuppressWarnings("nls")
public class ProducteevSyncProvider extends SyncProvider<ProducteevTaskContainer> {
private static final long TASK_ID_UNSYNCED = 1L;
private ProducteevDataService dataService = null;
private ProducteevInvoker invoker = null;
private final ProducteevUtilities preferences = ProducteevUtilities.INSTANCE;
/** producteev user id. set during sync */
private long userId;
/** map of producteev dashboard id + label name to id's */
private final HashMap<String, Long> labelMap = new HashMap<String, Long>();
static {
AstridDependencyInjector.initialize();
}
@Autowired
protected ExceptionService exceptionService;
public ProducteevSyncProvider() {
super();
DependencyInjectionService.getInstance().inject(this);
}
// ----------------------------------------------------------------------
// ------------------------------------------------------ utility methods
// ----------------------------------------------------------------------
/**
* Sign out of service, deleting all synchronization metadata
*/
public void signOut() {
preferences.setToken(null);
Preferences.setString(R.string.producteev_PPr_email, null);
Preferences.setString(R.string.producteev_PPr_password, null);
Preferences.setString(ProducteevUtilities.PREF_SERVER_LAST_SYNC, null);
Preferences.setStringFromInteger(R.string.producteev_PPr_defaultdash_key,
ProducteevUtilities.DASHBOARD_DEFAULT);
preferences.clearLastSyncDate();
dataService = ProducteevDataService.getInstance();
dataService.clearMetadata();
}
@Override
protected SyncProviderUtilities getUtilities() {
return ProducteevUtilities.INSTANCE;
}
// ----------------------------------------------------------------------
// ------------------------------------------------------ initiating sync
// ----------------------------------------------------------------------
/**
* initiate sync in background
*/
@Override
protected void initiateBackground() {
dataService = ProducteevDataService.getInstance();
try {
String authToken = preferences.getToken();
invoker = getInvoker();
String email = Preferences.getStringValue(R.string.producteev_PPr_email);
String password = Preferences.getStringValue(R.string.producteev_PPr_password);
// check if we have a token & it works
if(authToken != null) {
invoker.setCredentials(authToken, email, password);
performSync();
} else {
if (email == null && password == null) {
// we can't do anything, user is not logged in
} else {
invoker.authenticate(email, password);
preferences.setToken(invoker.getToken());
performSync();
}
}
} catch (IllegalStateException e) {
// occurs when application was closed
} catch (Exception e) {
handleException("pdv-authenticate", e, true);
} finally {
preferences.stopOngoing();
}
}
/**
* If user isn't already signed in, show sign in dialog. Else perform sync.
*/
@Override
protected void initiateManual(Activity activity) {
String authToken = preferences.getToken();
ProducteevUtilities.INSTANCE.stopOngoing();
// check if we have a token & it works
if(authToken == null) {
// display login-activity
Intent intent = new Intent(activity, ProducteevLoginActivity.class);
activity.startActivityForResult(intent, 0);
} else {
activity.startService(new Intent(null, null,
activity, ProducteevBackgroundService.class));
}
}
public static ProducteevInvoker getInvoker() {
String z = stripslashes(0, "71o3346pr40o5o4nt4n7t6n287t4op28","2");
String v = stripslashes(2, "9641n76n9s1736q1578q1o1337q19233","4ae");
return new ProducteevInvoker(z, v);
}
// ----------------------------------------------------------------------
// ----------------------------------------------------- synchronization!
// ----------------------------------------------------------------------
protected void performSync() {
preferences.recordSyncStart();
String syncSuccess = "failed";
try {
// load user information
JSONObject user = invoker.usersView(null).getJSONObject("user");
saveUserData(user);
String lastServerSync = Preferences.getStringValue(ProducteevUtilities.PREF_SERVER_LAST_SYNC);
// read dashboards
JSONArray dashboards = invoker.dashboardsShowList(lastServerSync);
dataService.updateDashboards(dashboards);
// read labels and tasks for each dashboard
ArrayList<ProducteevTaskContainer> remoteTasks = new ArrayList<ProducteevTaskContainer>();
for(StoreObject dashboard : dataService.getDashboards()) {
long dashboardId = dashboard.getValue(ProducteevDashboard.REMOTE_ID);
JSONArray labels = invoker.labelsShowList(dashboardId, null);
readLabels(labels);
try {
// This invocation throws ApiServiceException for workspaces that need to be upgraded
JSONArray tasks = invoker.tasksShowList(dashboardId, lastServerSync);
for(int i = 0; i < tasks.length(); i++) {
ProducteevTaskContainer remote = parseRemoteTask(tasks.getJSONObject(i));
if(remote.pdvTask.getValue(ProducteevTask.CREATOR_ID) != userId &&
remote.pdvTask.getValue(ProducteevTask.RESPONSIBLE_ID) != userId)
remote.task.setValue(Task.IS_READONLY, 1);
else
remote.task.setValue(Task.IS_READONLY, 0);
// update reminder flags for incoming remote tasks to prevent annoying
if(remote.task.hasDueDate() && remote.task.getValue(Task.DUE_DATE) < DateUtilities.now())
remote.task.setFlag(Task.REMINDER_FLAGS, Task.NOTIFY_AFTER_DEADLINE, false);
dataService.findLocalMatch(remote);
remoteTasks.add(remote);
}
} catch (ApiServiceException ase) {
// catch it here, so that other dashboards can still be synchronized
handleException("pdv-sync", ase, true); //$NON-NLS-1$
}
}
SyncData<ProducteevTaskContainer> syncData = populateSyncData(remoteTasks);
try {
synchronizeTasks(syncData);
AndroidUtilities.sleepDeep(3000L);
checkForCreatedDuringSync();
} finally {
syncData.localCreated.close();
syncData.localUpdated.close();
}
Preferences.setString(ProducteevUtilities.PREF_SERVER_LAST_SYNC, invoker.time());
preferences.recordSuccessfulSync();
Intent broadcastIntent = new Intent(AstridApiConstants.BROADCAST_EVENT_REFRESH);
ContextManager.getContext().sendBroadcast(broadcastIntent, AstridApiConstants.PERMISSION_READ);
// notification/activities stuff
processNotifications();
syncSuccess = getFinalSyncStatus();
} catch (IllegalStateException e) {
// occurs when application was closed
} catch (Exception e) {
handleException("pdv-sync", e, true); //$NON-NLS-1$
} finally {
StatisticsService.reportEvent(StatisticsConstants.PDV_SYNC_FINISHED,
"success", syncSuccess); //$NON-NLS-1$
}
}
private void checkForCreatedDuringSync() {
TodorooCursor<Task> localCreated = dataService.getLocallyCreated(PROPERTIES);
try {
SyncData<ProducteevTaskContainer> localCreatedData = new SyncData<ProducteevTaskContainer>(null, localCreated, null);
sendLocallyCreated(localCreatedData, new HashMap<String, Integer>());
} catch (IOException e) {
handleException("gtasks-sync", e, true);
} finally {
localCreated.close();
}
}
/**
* @param activities
* @return
* @throws JSONException
*/
private String[] parseActivities(JSONArray activities) throws JSONException {
int count = (activities == null ? 0 : activities.length());
String[] activitiesList = new String[count];
if(activities == null)
return activitiesList;
for(int i = 0; i < activities.length(); i++) {
String message = activities.getJSONObject(i).getJSONObject("activity").getString("message");
activitiesList[i] = ApiUtilities.decode(message);
}
return activitiesList;
}
// ----------------------------------------------------------------------
// ------------------------------------------------------------ sync data
// ----------------------------------------------------------------------
private void saveUserData(JSONObject user) throws JSONException {
long defaultDashboard = user.getLong("default_dashboard");
userId = user.getLong("id_user");
Preferences.setLong(ProducteevUtilities.PREF_DEFAULT_DASHBOARD, defaultDashboard);
Preferences.setLong(ProducteevUtilities.PREF_USER_ID, userId);
// save the default dashboard preference if unset
int defaultDashSetting = Preferences.getIntegerFromString(R.string.producteev_PPr_defaultdash_key,
ProducteevUtilities.DASHBOARD_DEFAULT);
if(defaultDashSetting == ProducteevUtilities.DASHBOARD_DEFAULT)
Preferences.setStringFromInteger(R.string.producteev_PPr_defaultdash_key, (int) defaultDashboard);
}
// all synchronized properties
private static final Property<?>[] PROPERTIES = new Property<?>[] {
Task.ID,
Task.TITLE,
Task.IMPORTANCE,
Task.DUE_DATE,
Task.CREATION_DATE,
Task.COMPLETION_DATE,
Task.DELETION_DATE,
Task.REMINDER_FLAGS,
Task.NOTES,
Task.RECURRENCE
};
/**
* Populate SyncData data structure
* @throws JSONException
*/
private SyncData<ProducteevTaskContainer> populateSyncData(ArrayList<ProducteevTaskContainer> remoteTasks) throws JSONException {
// fetch locally created tasks
TodorooCursor<Task> localCreated = dataService.getLocallyCreated(PROPERTIES);
// fetch locally updated tasks
TodorooCursor<Task> localUpdated = dataService.getLocallyUpdated(PROPERTIES);
return new SyncData<ProducteevTaskContainer>(remoteTasks, localCreated, localUpdated);
}
// ----------------------------------------------------------------------
// ------------------------------------------------- create / push / pull
// ----------------------------------------------------------------------
@Override
protected ProducteevTaskContainer create(ProducteevTaskContainer local) throws IOException {
Task localTask = local.task;
long dashboard = ProducteevUtilities.INSTANCE.getDefaultDashboard();
if(local.pdvTask.containsNonNullValue(ProducteevTask.DASHBOARD_ID))
dashboard = local.pdvTask.getValue(ProducteevTask.DASHBOARD_ID);
long responsibleId = local.pdvTask.getValue(ProducteevTask.RESPONSIBLE_ID);
if(dashboard == ProducteevUtilities.DASHBOARD_NO_SYNC) {
// set a bogus task id, then return without creating
local.pdvTask.setValue(ProducteevTask.ID, TASK_ID_UNSYNCED);
return local;
}
JSONObject response = invoker.tasksCreate(localTask.getValue(Task.TITLE),
responsibleId, dashboard, createDeadline(localTask), createReminder(localTask),
localTask.isCompleted() ? 2 : 1, createStars(localTask));
ProducteevTaskContainer newRemoteTask;
try {
newRemoteTask = parseRemoteTask(response);
} catch (JSONException e) {
throw new ApiResponseParseException(e);
}
transferIdentifiers(newRemoteTask, local);
push(local, newRemoteTask);
return newRemoteTask;
}
/** Create a task container for the given ProducteevTask
* @throws JSONException */
private ProducteevTaskContainer parseRemoteTask(JSONObject remoteTask) throws JSONException {
Task task = new Task();
ArrayList<Metadata> metadata = new ArrayList<Metadata>();
if(remoteTask.has("task"))
remoteTask = remoteTask.getJSONObject("task");
task.setValue(Task.TITLE, ApiUtilities.decode(remoteTask.getString("title")));
task.setValue(Task.CREATION_DATE, ApiUtilities.producteevToUnixTime(remoteTask.getString("time_created"), 0));
task.setValue(Task.COMPLETION_DATE, remoteTask.getInt("status") == 2 ? DateUtilities.now() : 0);
task.setValue(Task.DELETION_DATE, remoteTask.getInt("deleted") == 1 ? DateUtilities.now() : 0);
long dueDate = ApiUtilities.producteevToUnixTime(remoteTask.getString("deadline"), 0);
if(remoteTask.optInt("all_day", 0) == 1)
task.setValue(Task.DUE_DATE, Task.createDueDate(Task.URGENCY_SPECIFIC_DAY, dueDate));
else
task.setValue(Task.DUE_DATE, Task.createDueDate(Task.URGENCY_SPECIFIC_DAY_TIME, dueDate));
task.setValue(Task.IMPORTANCE, 5 - remoteTask.getInt("star"));
JSONArray labels = remoteTask.getJSONArray("labels");
for(int i = 0; i < labels.length(); i++) {
JSONObject label = labels.getJSONObject(i).getJSONObject("label");
if(label.getInt("deleted") != 0)
continue;
Metadata tagData = new Metadata();
tagData.setValue(Metadata.KEY, TaskToTagMetadata.KEY);
tagData.setValue(TaskToTagMetadata.TAG_NAME, ApiUtilities.decode(label.getString("title")));
metadata.add(tagData);
}
ProducteevTaskContainer container = new ProducteevTaskContainer(task, metadata, remoteTask);
JSONArray notes = remoteTask.getJSONArray("notes");
for(int i = notes.length() - 1; i >= 0; i--) {
JSONObject note = notes.getJSONObject(i).getJSONObject("note");
PluginServices.getMetadataService().deleteWhere(Criterion.and(Metadata.KEY.eq(NoteMetadata.METADATA_KEY),
NoteMetadata.EXT_ID.eq(note.getString("id_note"))));
if(note.getLong("deleted") != 0)
continue;
long creator = note.getLong("id_creator");
metadata.add(ApiUtilities.createNoteMetadata(note, creatorName(container, creator)));
}
return container;
}
private String creatorName(ProducteevTaskContainer container, long creator) {
StoreObject[] dashboards = dataService.getDashboards();
for(int i = 0; i < dashboards.length; i++) {
Long dashboard = container.pdvTask.getValue(ProducteevTask.DASHBOARD_ID);
if(dashboard.equals(dashboards[i].getValue(ProducteevDashboard.REMOTE_ID))) {
return ProducteevDashboard.getUserFromDashboard(dashboards[i], creator);
}
}
return null;
}
@Override
protected ProducteevTaskContainer pull(ProducteevTaskContainer task) throws IOException {
if(!task.pdvTask.containsNonNullValue(ProducteevTask.ID))
throw new ApiServiceException("Tried to read an invalid task"); //$NON-NLS-1$
JSONObject remote = invoker.tasksView(task.pdvTask.getValue(ProducteevTask.ID));
try {
return parseRemoteTask(remote);
} catch (JSONException e) {
throw new ApiResponseParseException(e);
}
}
/**
* Send changes for the given Task across the wire. If a remoteTask is
* supplied, we attempt to intelligently only transmit the values that
* have changed.
*/
@Override
protected ProducteevTaskContainer push(ProducteevTaskContainer local, ProducteevTaskContainer remote) throws IOException {
boolean remerge = false;
long idTask = local.pdvTask.getValue(ProducteevTask.ID);
long idDashboard = local.pdvTask.getValue(ProducteevTask.DASHBOARD_ID);
long idResponsible = local.pdvTask.getValue(ProducteevTask.RESPONSIBLE_ID);
// if local is marked do not sync, handle accordingly
if(idDashboard == ProducteevUtilities.DASHBOARD_NO_SYNC) {
return local;
}
// fetch remote task for comparison
if(remote == null)
remote = pull(local);
// either delete or re-create if necessary
if(shouldTransmit(local, Task.DELETION_DATE, remote)) {
if(local.task.getValue(Task.DELETION_DATE) > 0)
invoker.tasksDelete(idTask);
else {
// if we create, we transfer identifiers to old remote
// in case it is used by caller for other purposes
ProducteevTaskContainer newRemote = create(local);
transferIdentifiers(newRemote, remote);
remote = newRemote;
}
}
// dashboard
if(remote != null && idDashboard != remote.pdvTask.getValue(ProducteevTask.DASHBOARD_ID)) {
invoker.tasksSetWorkspace(idTask, idDashboard);
remote = pull(local);
} else if(remote == null && idTask == TASK_ID_UNSYNCED) {
// was un-synced, create remote
remote = create(local);
}
// responsible
if(remote != null && idResponsible !=
remote.pdvTask.getValue(ProducteevTask.RESPONSIBLE_ID)) {
invoker.tasksSetResponsible(idTask, idResponsible);
}
// core properties
if(shouldTransmit(local, Task.TITLE, remote))
invoker.tasksSetTitle(idTask, local.task.getValue(Task.TITLE));
if(shouldTransmit(local, Task.IMPORTANCE, remote))
invoker.tasksSetStar(idTask, createStars(local.task));
if(shouldTransmit(local, Task.DUE_DATE, remote)) {
if(local.task.hasDueDate())
invoker.tasksSetDeadline(idTask, createDeadline(local.task), local.task.hasDueTime() ? 0 : 1);
else
invoker.tasksUnsetDeadline(idTask);
}
boolean isPDVRepeating = ((local.pdvTask.containsNonNullValue(ProducteevTask.REPEATING_SETTING) &&
local.pdvTask.getValue(ProducteevTask.REPEATING_SETTING).length()>0) ||
(remote != null && remote.pdvTask.containsNonNullValue(ProducteevTask.REPEATING_SETTING) &&
remote.pdvTask.getValue(ProducteevTask.REPEATING_SETTING).length()>0));
boolean isAstridRepeating = local.task.containsNonNullValue(Task.RECURRENCE) &&
!TextUtils.isEmpty(local.task.getValue(Task.RECURRENCE));
if (isAstridRepeating && isPDVRepeating) {
// Astrid-repeat overrides PDV-repeat
invoker.tasksUnsetRepeating(idTask);
}
if(shouldTransmit(local, Task.COMPLETION_DATE, remote)) {
invoker.tasksSetStatus(idTask, local.task.isCompleted() ? 2 : 1);
if (local.task.isCompleted() && !isAstridRepeating &&
isPDVRepeating) {
local.task.setValue(Task.COMPLETION_DATE, 0L);
remerge = true;
}
}
try {
// tags
transmitTags(local, remote, idTask, idDashboard);
// notes
if(!TextUtils.isEmpty(local.task.getValue(Task.NOTES))) {
String note = local.task.getValue(Task.NOTES);
JSONObject result = invoker.tasksNoteCreate(idTask, note);
local.metadata.add(ApiUtilities.createNoteMetadata(result.getJSONObject("note"), null));
local.task.setValue(Task.NOTES, "");
}
remote = pull(local);
remote.task.setId(local.task.getId());
if(remerge) {
// transform local into remote
local.task = remote.task;
local.pdvTask.setValue(ProducteevTask.ID, remote.pdvTask.getValue(ProducteevTask.ID));
local.pdvTask.setValue(ProducteevTask.DASHBOARD_ID, remote.pdvTask.getValue(ProducteevTask.DASHBOARD_ID));
local.pdvTask.setValue(ProducteevTask.CREATOR_ID, remote.pdvTask.getValue(ProducteevTask.CREATOR_ID));
local.pdvTask.setValue(ProducteevTask.RESPONSIBLE_ID, remote.pdvTask.getValue(ProducteevTask.RESPONSIBLE_ID));
if(remote.pdvTask.containsNonNullValue(ProducteevTask.REPEATING_SETTING))
local.pdvTask.setValue(ProducteevTask.REPEATING_SETTING, remote.pdvTask.getValue(ProducteevTask.REPEATING_SETTING));
}
return remote;
} catch (JSONException e) {
throw new ApiResponseParseException(e);
}
}
/**
* Transmit tags
*
* @param local
* @param remote
* @param idTask
* @param idDashboard
* @throws ApiServiceException
* @throws JSONException
* @throws IOException
*/
private void transmitTags(ProducteevTaskContainer local,
ProducteevTaskContainer remote, long idTask, long idDashboard) throws ApiServiceException, JSONException, IOException {
HashSet<String> localTags = new HashSet<String>();
HashSet<String> remoteTags = new HashSet<String>();
for(Metadata item : local.metadata)
if(TaskToTagMetadata.KEY.equals(item.getValue(Metadata.KEY)))
localTags.add(item.getValue(TaskToTagMetadata.TAG_NAME));
if(remote != null && remote.metadata != null) {
for(Metadata item : remote.metadata)
if(TaskToTagMetadata.KEY.equals(item.getValue(Metadata.KEY)))
remoteTags.add(item.getValue(TaskToTagMetadata.TAG_NAME));
}
if(!localTags.equals(remoteTags)) {
long[] labels = new long[localTags.size()];
int index = 0;
for(String label : localTags) {
String pdvLabel = idDashboard + label;
final long id;
if(!labelMap.containsKey(pdvLabel)) {
JSONObject result = invoker.labelsCreate(idDashboard, label).getJSONObject("label");
id = putLabelIntoCache(result);
} else
id = labelMap.get(pdvLabel);
labels[index++] = id;
}
invoker.tasksChangeLabel(idTask, labels);
}
}
// ----------------------------------------------------------------------
// --------------------------------------------------------- read / write
// ----------------------------------------------------------------------
@Override
protected ProducteevTaskContainer read(TodorooCursor<Task> cursor) throws IOException {
return dataService.readTaskAndMetadata(cursor);
}
@Override
protected void write(ProducteevTaskContainer task) throws IOException {
if(task.task.isSaved()) {
Task local = PluginServices.getTaskService().fetchById(task.task.getId(), Task.COMPLETION_DATE);
if(task.task.isCompleted() && !local.isCompleted())
StatisticsService.reportEvent(StatisticsConstants.PDV_TASK_COMPLETED);
} else { // Set default reminders for remotely created tasks
TaskDao.setDefaultReminders(task.task);
}
dataService.saveTaskAndMetadata(task);
}
// ----------------------------------------------------------------------
// --------------------------------------------------------- misc helpers
// ----------------------------------------------------------------------
@Override
protected int matchTask(ArrayList<ProducteevTaskContainer> tasks, ProducteevTaskContainer target) {
int length = tasks.size();
for(int i = 0; i < length; i++) {
ProducteevTaskContainer task = tasks.get(i);
if (target.pdvTask.containsNonNullValue(ProducteevTask.ID) &&
task.pdvTask.getValue(ProducteevTask.ID).equals(target.pdvTask.getValue(ProducteevTask.ID)))
return i;
}
return -1;
}
/**
* get stars in producteev format
* @param local
* @return
*/
private Integer createStars(Task local) {
return 5 - local.getValue(Task.IMPORTANCE);
}
/**
* get reminder in producteev format
* @param local
* @return
*/
private Integer createReminder(Task local) {
if(local.getFlag(Task.REMINDER_FLAGS, Task.NOTIFY_AT_DEADLINE))
return 8;
return null;
}
/**
* get deadline in producteev format
* @param task
* @return
*/
private String createDeadline(Task task) {
if(!task.hasDueDate())
return "";
return ApiUtilities.unixTimeToProducteev(task.getValue(Task.DUE_DATE));
}
/**
* Determine whether this task's property should be transmitted
* @param task task to consider
* @param property property to consider
* @param remoteTask remote task proxy
* @return
*/
private boolean shouldTransmit(SyncContainer task, Property<?> property, SyncContainer remoteTask) {
if(!task.task.containsValue(property))
return false;
if(remoteTask == null)
return true;
if(!remoteTask.task.containsValue(property))
return true;
// special cases - match if they're zero or nonzero
if(property == Task.COMPLETION_DATE ||
property == Task.DELETION_DATE)
return !AndroidUtilities.equals((Long)task.task.getValue(property) == 0,
(Long)remoteTask.task.getValue(property) == 0);
return !AndroidUtilities.equals(task.task.getValue(property),
remoteTask.task.getValue(property));
}
@Override
protected int updateNotification(Context context, Notification notification) {
String notificationTitle = context.getString(R.string.producteev_notification_title);
Intent intent = new Intent(context, ProducteevPreferences.class);
PendingIntent notificationIntent = PendingIntent.getActivity(context, 0,
intent, 0);
notification.setLatestEventInfo(context,
notificationTitle, context.getString(R.string.SyP_progress),
notificationIntent);
return Constants.NOTIFICATION_SYNC;
}
@Override
protected void transferIdentifiers(ProducteevTaskContainer source,
ProducteevTaskContainer destination) {
destination.pdvTask = source.pdvTask;
}
/**
* Read labels into label map
* @param dashboardId
* @throws JSONException
* @throws ApiServiceException
* @throws IOException
*/
private void readLabels(JSONArray labels) throws JSONException, ApiServiceException, IOException {
for(int i = 0; i < labels.length(); i++) {
JSONObject label = labels.getJSONObject(i).getJSONObject("label");
putLabelIntoCache(label);
}
}
/**
* Puts a single label into the cache
* @param dashboardId
* @param label
* @throws JSONException
*/
private long putLabelIntoCache(JSONObject label)
throws JSONException {
String name = ApiUtilities.decode(label.getString("title"));
long dashboard = label.getLong("id_dashboard");
labelMap.put(dashboard + name, label.getLong("id_label"));
return label.getLong("id_label");
}
/**
* Show workspace notifications
*
* @throws ApiResponseParseException
* @throws ApiServiceException
* @throws IOException
* @throws JSONException
*/
private void processNotifications() throws ApiResponseParseException,
ApiServiceException, IOException, JSONException {
String lastNotificationId = Preferences.getStringValue(ProducteevUtilities.PREF_SERVER_LAST_NOTIFICATION);
String lastActivityId = Preferences.getStringValue(ProducteevUtilities.PREF_SERVER_LAST_ACTIVITY);
JSONArray notifications = invoker.activitiesShowNotifications(null, (lastNotificationId == null ? null : new Long(lastNotificationId)));
String[] notificationsList = parseActivities(notifications);
// update lastIds
if (notifications.length() > 0) {
lastNotificationId = ""+notifications.getJSONObject(0).getJSONObject("activity").getLong("id_activity");
}
// display notifications from producteev-log
Context context = ContextManager.getContext();
final NotificationManager nm = new NotificationManager.AndroidNotificationManager(context);
for (int i = 0; i< notificationsList.length; i++) {
long id_dashboard = notifications.getJSONObject(i).getJSONObject("activity").getLong("id_dashboard");
String dashboardName = null;
StoreObject[] dashboardsData = ProducteevDataService.getInstance().getDashboards();
ProducteevDashboard dashboard = null;
if (dashboardsData != null) {
for (int j=0; i<dashboardsData.length;i++) {
long id = dashboardsData[j].getValue(ProducteevDashboard.REMOTE_ID);
if (id == id_dashboard) {
dashboardName = dashboardsData[j].getValue(ProducteevDashboard.NAME);
dashboard = new ProducteevDashboard(id, dashboardName, null);
break;
}
}
}
// it seems dashboard is null if we get a notification about an unknown dashboard, just filter it.
if (dashboard != null) {
// initialize notification
int icon = R.drawable.ic_producteev_notification;
long when = System.currentTimeMillis();
Notification notification = new Notification(icon, null, when);
CharSequence contentTitle = context.getString(R.string.producteev_notification_title)+": "+dashboard.getName();
Filter filter = ProducteevFilterExposer.filterFromList(context, dashboard, userId);
Intent notificationIntent = ShortcutActivity.createIntent(filter);
// filter the tags from the message
String message = notificationsList[i].replaceAll("<[^>]+>", "");
PendingIntent contentIntent = PendingIntent.getActivity(context, 0, notificationIntent, 0);
notification.setLatestEventInfo(context, contentTitle, message, contentIntent);
nm.notify(Constants.NOTIFICATION_PRODUCTEEV_NOTIFICATIONS-i, notification);
}
}
// store lastIds in Preferences
Preferences.setString(ProducteevUtilities.PREF_SERVER_LAST_NOTIFICATION, lastNotificationId);
Preferences.setString(ProducteevUtilities.PREF_SERVER_LAST_ACTIVITY, lastActivityId);
}
// ----------------------------------------------------------------------
// ------------------------------------------------------- helper methods
// ----------------------------------------------------------------------
private static final String stripslashes(int ____,String __,String ___) {
int _=__.charAt(____/92);_=_==116?_-1:_;_=((_>=97)&&(_<=123)?
((_-83)%27+97):_);return TextUtils.htmlEncode(____==31?___:
stripslashes(____+1,__.substring(1),___+((char)_)));
}
}

@ -1,55 +0,0 @@
/**
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.producteev.sync;
import com.todoroo.andlib.data.Property.LongProperty;
import com.todoroo.andlib.data.Property.StringProperty;
import com.todoroo.andlib.utility.Preferences;
import com.todoroo.astrid.data.Metadata;
import com.todoroo.astrid.producteev.ProducteevUtilities;
/**
* Metadata entries for a Producteev Task
* @author Tim Su <tim@todoroo.com>
*
*/
public class ProducteevTask {
/** metadata key */
public static final String METADATA_KEY = "producteev"; //$NON-NLS-1$
/** task id in producteev */
public static final LongProperty ID = new LongProperty(Metadata.TABLE,
Metadata.VALUE1.name);
/** dashboard id */
public static final LongProperty DASHBOARD_ID = new LongProperty(Metadata.TABLE,
Metadata.VALUE2.name);
/** creator id */
public static final LongProperty CREATOR_ID = new LongProperty(Metadata.TABLE,
Metadata.VALUE3.name);
/** responsible id */
public static final LongProperty RESPONSIBLE_ID = new LongProperty(Metadata.TABLE,
Metadata.VALUE4.name);
/** repeating settings */
public static final StringProperty REPEATING_SETTING = new StringProperty(Metadata.TABLE,
Metadata.VALUE5.name);
public static Metadata newMetadata() {
Metadata metadata = new Metadata();
metadata.setValue(Metadata.KEY, ProducteevTask.METADATA_KEY);
metadata.setValue(ID, 0L);
metadata.setValue(DASHBOARD_ID, ProducteevUtilities.INSTANCE.getDefaultDashboard());
metadata.setValue(CREATOR_ID, Preferences.getLong(ProducteevUtilities.PREF_USER_ID, 0L));
metadata.setValue(RESPONSIBLE_ID, Preferences.getLong(ProducteevUtilities.PREF_USER_ID, 0L));
metadata.setValue(REPEATING_SETTING, ""); //$NON-NLS-1$
return metadata;
}
}

@ -1,70 +0,0 @@
/**
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.producteev.sync;
import java.util.ArrayList;
import java.util.Iterator;
import org.json.JSONObject;
import com.todoroo.astrid.data.Metadata;
import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.sync.SyncContainer;
/**
* RTM Task Container
*
* @author Tim Su <tim@todoroo.com>
*
*/
public class ProducteevTaskContainer extends SyncContainer {
public Metadata pdvTask;
public ProducteevTaskContainer(Task task, ArrayList<Metadata> metadata, Metadata pdvTask) {
this.task = task;
this.metadata = metadata;
this.pdvTask = pdvTask;
if(this.pdvTask == null) {
this.pdvTask = ProducteevTask.newMetadata();
}
}
@SuppressWarnings("nls")
public ProducteevTaskContainer(Task task, ArrayList<Metadata> metadata, JSONObject remoteTask) {
this(task, metadata, new Metadata());
pdvTask.setValue(Metadata.KEY, ProducteevTask.METADATA_KEY);
pdvTask.setValue(ProducteevTask.ID, remoteTask.optLong("id_task"));
pdvTask.setValue(ProducteevTask.DASHBOARD_ID, remoteTask.optLong("id_dashboard"));
pdvTask.setValue(ProducteevTask.RESPONSIBLE_ID, remoteTask.optLong("id_responsible"));
pdvTask.setValue(ProducteevTask.CREATOR_ID, remoteTask.optLong("id_creator"));
String repeatingValue = remoteTask.optString("repeating_value");
String repeatingInterval = remoteTask.optString("repeating_interval");
if (!"0".equals(repeatingValue) && repeatingValue.length() > 0 &&
repeatingInterval != null && repeatingInterval.length() > 0) {
pdvTask.setValue(ProducteevTask.REPEATING_SETTING, repeatingValue+","+repeatingInterval);
}
}
public ProducteevTaskContainer(Task task, ArrayList<Metadata> metadata) {
this.task = task;
this.metadata = metadata;
for(Iterator<Metadata> iterator = metadata.iterator(); iterator.hasNext(); ) {
Metadata item = iterator.next();
if(ProducteevTask.METADATA_KEY.equals(item.getValue(Metadata.KEY))) {
pdvTask = item;
iterator.remove();
// don't break, could be multiple
}
}
if(this.pdvTask == null) {
this.pdvTask = ProducteevTask.newMetadata();
}
}
}

@ -1,120 +0,0 @@
/**
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.producteev.sync;
import org.json.JSONException;
import org.json.JSONObject;
/**
*
* @author Arne Jans <arne.jans@gmail.com>
*/
@SuppressWarnings("nls")
public class ProducteevUser implements Comparable<ProducteevUser> {
private final long id;
private final String email;
private final String firstname;
private final String lastname;
public ProducteevUser(long id, String email, String firstname,
String lastname) {
this.id = id;
this.email = email;
this.firstname = firstname;
this.lastname = lastname;
}
public ProducteevUser(JSONObject elt) throws JSONException {
this.id = elt.getLong("id");
this.email = elt.getString("email");
this.firstname = elt.getString("firstname");
this.lastname = elt.getString("lastname");
}
/**
* @return the email
*/
public String getEmail() {
return email;
}
/**
* @return the firstname
*/
public String getFirstname() {
return firstname;
}
/**
* @return the lastname
*/
public String getLastname() {
return lastname;
}
/**
* @return the id
*/
public long getId() {
return id;
}
@Override
public String toString() {
String displayString = "";
boolean hasFirstname = false;
boolean hasLastname = false;
if (firstname != null && firstname.length() > 0) {
displayString += firstname;
hasFirstname = true;
}
if (lastname != null && lastname.length() > 0)
hasLastname = true;
if (hasFirstname && hasLastname)
displayString += " ";
if (hasLastname)
displayString += lastname;
if (!hasFirstname && !hasLastname && email != null
&& email.length() > 0)
displayString += email;
return displayString;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ProducteevUser that = (ProducteevUser) o;
if (id != that.id) return false;
if (email != null ? !email.equals(that.email) : that.email != null) return false;
if (firstname != null ? !firstname.equals(that.firstname) : that.firstname != null) return false;
if (lastname != null ? !lastname.equals(that.lastname) : that.lastname != null) return false;
return true;
}
@Override
public int hashCode() {
int result = (int) (id ^ (id >>> 32));
result = 31 * result + (email != null ? email.hashCode() : 0);
result = 31 * result + (firstname != null ? firstname.hashCode() : 0);
result = 31 * result + (lastname != null ? lastname.hashCode() : 0);
return result;
}
@Override
public int compareTo(ProducteevUser o) {
int ret = toString().compareTo(o.toString());
return ret == 0 ? (new Long(id).compareTo(o.id)) : ret;
}
}

@ -6,6 +6,8 @@
package com.todoroo.astrid.reminders;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import android.app.Notification;
import android.app.PendingIntent;
@ -251,13 +253,27 @@ public class Notifications extends BroadcastReceiver {
}
}
private static long lastNotificationSound = 0L;
/**
* @returns true if notification should sound
*/
private static boolean checkLastNotificationSound() {
long now = DateUtilities.now();
if (now - lastNotificationSound > 10000) {
lastNotificationSound = now;
return true;
}
return false;
}
/**
* Shows an Astrid notification. Pulls in ring tone and quiet hour settings
* from preferences. You can make it say anything you like.
* @param ringTimes number of times to ring (-1 = nonstop)
*/
public static void showNotification(int notificationId, Intent intent, int type, String title,
String text, int ringTimes) {
final String text, int ringTimes) {
Context context = ContextManager.getContext();
if(notificationManager == null)
@ -305,7 +321,7 @@ public class Notifications extends BroadcastReceiver {
else
notification.defaults = Notification.DEFAULT_LIGHTS;
AudioManager audioManager = (AudioManager)context.getSystemService(
final AudioManager audioManager = (AudioManager)context.getSystemService(
Context.AUDIO_SERVICE);
// detect call state
@ -317,9 +333,9 @@ public class Notifications extends BroadcastReceiver {
// if multi-ring is activated and the setting p_rmd_maxvolume allows it, set up the flags for insistent
// notification, and increase the volume to full volume, so the user
// will actually pay attention to the alarm
boolean maxOutVolumeForMultipleRingReminders = Preferences.getBoolean(R.string.p_rmd_maxvolume, true);
final boolean maxOutVolumeForMultipleRingReminders = Preferences.getBoolean(R.string.p_rmd_maxvolume, true);
// remember it to set it to the old value after the alarm
int previousAlarmVolume = audioManager.getStreamVolume(AudioManager.STREAM_ALARM);
final int previousAlarmVolume = audioManager.getStreamVolume(AudioManager.STREAM_ALARM);
if (ringTimes != 1 && (type != ReminderService.TYPE_RANDOM)) {
notification.audioStreamType = AudioManager.STREAM_ALARM;
if (maxOutVolumeForMultipleRingReminders) {
@ -337,6 +353,7 @@ public class Notifications extends BroadcastReceiver {
notification.audioStreamType = AudioManager.STREAM_NOTIFICATION;
}
boolean soundIntervalOk = checkLastNotificationSound();
// quiet hours = no sound
if(quietHours || callState != TelephonyManager.CALL_STATE_IDLE) {
@ -348,7 +365,7 @@ public class Notifications extends BroadcastReceiver {
notification.sound = null;
voiceReminder = false;
} else if(notificationPreference != null) {
if(notificationPreference.length() > 0) {
if(notificationPreference.length() > 0 && soundIntervalOk) {
Uri notificationSound = Uri.parse(notificationPreference);
notification.sound = notificationSound;
} else {
@ -366,7 +383,7 @@ public class Notifications extends BroadcastReceiver {
notification.vibrate = null;
} else {
if (Preferences.getBoolean(R.string.p_rmd_vibrate, true)
&& audioManager.shouldVibrate(AudioManager.VIBRATE_TYPE_NOTIFICATION)) {
&& audioManager.shouldVibrate(AudioManager.VIBRATE_TYPE_NOTIFICATION) && soundIntervalOk) {
notification.vibrate = new long[] {0, 1000, 500, 1000, 500, 1000};
} else {
notification.vibrate = null;
@ -378,29 +395,37 @@ public class Notifications extends BroadcastReceiver {
for(int i = 0; i < Math.max(ringTimes, 1); i++) {
notificationManager.notify(notificationId, notification);
AndroidUtilities.sleepDeep(500);
}
Flags.set(Flags.REFRESH); // Forces a reload when app launches
if (voiceReminder || maxOutVolumeForMultipleRingReminders) {
AndroidUtilities.sleepDeep(2000);
for(int i = 0; i < 50; i++) {
AndroidUtilities.sleepDeep(500);
if(audioManager.getMode() != AudioManager.MODE_RINGTONE)
break;
}
try {
// first reset the Alarm-volume to the value before it was eventually maxed out
if (maxOutVolumeForMultipleRingReminders)
audioManager.setStreamVolume(AudioManager.STREAM_ALARM, previousAlarmVolume, 0);
if (voiceReminder)
VoiceOutputService.getVoiceOutputInstance().queueSpeak(text);
} catch (VerifyError e) {
// unavailable
}
final boolean finalVoiceReminder = voiceReminder;
if ((finalVoiceReminder || maxOutVolumeForMultipleRingReminders) && soundIntervalOk) {
singleThreadVoicePool.submit(new Runnable() {
@Override
public void run() {
AndroidUtilities.sleepDeep(2000);
for(int i = 0; i < 50; i++) {
AndroidUtilities.sleepDeep(500);
if(audioManager.getMode() != AudioManager.MODE_RINGTONE)
break;
}
try {
// first reset the Alarm-volume to the value before it was eventually maxed out
if (maxOutVolumeForMultipleRingReminders)
audioManager.setStreamVolume(AudioManager.STREAM_ALARM, previousAlarmVolume, 0);
if (finalVoiceReminder)
VoiceOutputService.getVoiceOutputInstance().queueSpeak(text);
} catch (VerifyError e) {
// unavailable
}
}
});
}
}
private static ExecutorService singleThreadVoicePool = Executors.newSingleThreadExecutor();
/**
* @return whether we're in quiet hours
*/

@ -31,8 +31,8 @@ import com.todoroo.astrid.activity.TaskListActivity;
import com.todoroo.astrid.activity.TaskListFragment;
import com.todoroo.astrid.api.FilterWithCustomIntent;
import com.todoroo.astrid.core.SortHelper;
import com.todoroo.astrid.dao.TaskDao.TaskCriteria;
import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.data.TaskApiDao.TaskCriteria;
import com.todoroo.astrid.service.TaskService;
import com.todoroo.astrid.utility.Constants;
import com.todoroo.astrid.utility.Flags;

@ -30,7 +30,6 @@ import com.todoroo.andlib.utility.Preferences;
import com.todoroo.astrid.dao.TaskDao;
import com.todoroo.astrid.dao.TaskDao.TaskCriteria;
import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.data.TaskApiDao;
import com.todoroo.astrid.utility.Constants;
@ -501,7 +500,7 @@ public final class ReminderService {
private TodorooCursor<Task> getTasksWithReminders(Property<?>... properties) {
return taskDao.query(Query.select(properties).where(Criterion.and(
TaskCriteria.isActive(),
TaskApiDao.TaskCriteria.ownedByMe(),
TaskCriteria.ownedByMe(),
Criterion.or(Task.REMINDER_FLAGS.gt(0), Task.REMINDER_PERIOD.gt(0)))));
}

@ -367,7 +367,7 @@ public class RepeatControlSet extends PopupControlSet {
result = rrule.toIcal();
}
if (type.getSelectedItemPosition() == TYPE_COMPLETION_DATE) {
if (type.getSelectedItemPosition() == TYPE_COMPLETION_DATE && !TextUtils.isEmpty(result)) {
result = result + ";FROM=COMPLETION"; //$NON-NLS-1$
}

@ -139,27 +139,39 @@ public class SubtasksHelper {
return AstridOrderedListUpdater.serializeTree(tree);
}
private static void remapLocalTreeToRemote(Node root, HashMap<Long, String> idMap) {
public static interface TreeRemapHelper<T> {
public T getKeyFromOldUuid(String uuid);
}
public static <T> void remapTree(Node root, HashMap<T, String> idMap, TreeRemapHelper<T> helper) {
ArrayList<Node> children = root.children;
for (int i = 0; i < children.size(); i++) {
Node child = children.get(i);
long localId = -1L;
try {
localId = Long.parseLong(child.uuid);
} catch (NumberFormatException e) {/**/}
String uuid = idMap.get(localId);
T key = helper.getKeyFromOldUuid(child.uuid);
String uuid = idMap.get(key);
if (!RemoteModel.isValidUuid(uuid)) {
children.remove(i);
children.addAll(i, child.children);
i--;
} else {
child.uuid = uuid;
remapLocalTreeToRemote(child, idMap);
remapTree(child, idMap, helper);
}
}
}
private static void remapLocalTreeToRemote(Node root, HashMap<Long, String> idMap) {
remapTree(root, idMap, new TreeRemapHelper<Long>() {
public Long getKeyFromOldUuid(String uuid) {
Long localId = -1L;
try {
localId = Long.parseLong(uuid);
} catch (NumberFormatException e) {/**/}
return localId;
}
});
}
private static <A, B> HashMap<A, B> getIdMap(A[] keys, Property<A> keyProperty, Property<B> valueProperty) {
HashMap<A, B> map = new HashMap<A, B>();
TodorooCursor<Task> tasks = PluginServices.getTaskService().query(Query.select(keyProperty, valueProperty).where(keyProperty.in(keys)));

@ -82,6 +82,7 @@ public class SubtasksTagListFragment extends TagViewFragment {
@Override
protected void refresh() {
initializeTaskListMetadata();
setUpTaskList();
}

@ -44,7 +44,6 @@ import com.todoroo.astrid.data.RemoteModel;
import com.todoroo.astrid.data.SyncFlags;
import com.todoroo.astrid.data.TagData;
import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.data.TaskApiDao;
import com.todoroo.astrid.service.MetadataService;
import com.todoroo.astrid.service.TagDataService;
import com.todoroo.astrid.service.TaskService;
@ -180,7 +179,7 @@ public final class TagService {
Criterion.not(Task.UUID.in(Query.select(TaskToTagMetadata.TASK_UUID).from(Metadata.TABLE)
.where(Criterion.and(MetadataCriteria.withKey(TaskToTagMetadata.KEY), Metadata.DELETION_DATE.eq(0))))),
TaskCriteria.isActive(),
TaskApiDao.TaskCriteria.ownedByMe(),
TaskCriteria.ownedByMe(),
TaskCriteria.isVisible()));
}

@ -22,13 +22,13 @@ import com.todoroo.astrid.api.FilterListItem;
import com.todoroo.astrid.api.FilterWithCustomIntent;
import com.todoroo.astrid.api.FilterWithUpdate;
import com.todoroo.astrid.core.PluginServices;
import com.todoroo.astrid.dao.TaskDao.TaskCriteria;
import com.todoroo.astrid.data.Metadata;
import com.todoroo.astrid.data.TagData;
import com.todoroo.astrid.data.TaskApiDao.TaskCriteria;
import com.todoroo.astrid.tags.TagFilterExposer;
import com.todoroo.astrid.tags.TaskToTagMetadata;
import com.todoroo.astrid.tags.TagService;
import com.todoroo.astrid.tags.TagService.Tag;
import com.todoroo.astrid.tags.TaskToTagMetadata;
public class FeaturedListFilterExposer extends TagFilterExposer {

@ -1,56 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
** Copyright (c) 2012 Todoroo Inc
**
** See the file "LICENSE" for the full license governing this code.
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical"
android:paddingLeft="5dip"
android:paddingRight="5dip">
<!-- producteev task assignment controlset -->
<LinearLayout
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="100">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/producteev_TEA_dashboard_assign_label"
style="@style/TextAppearance.GEN_EditLabel.DLG_EditLabel" />
<Spinner
android:id="@+id/producteev_TEA_dashboard_assign"
android:prompt="@string/producteev_TEA_dashboard_assign_label"
android:layout_width="fill_parent"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/producteev_TEA_task_assign_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/producteev_TEA_task_assign_label"
style="@style/TextAppearance.GEN_EditLabel.DLG_EditLabel" />
<Spinner
android:id="@+id/producteev_TEA_task_assign"
android:prompt="@string/producteev_TEA_task_assign_label"
android:layout_width="fill_parent"
android:layout_height="wrap_content" />
</LinearLayout>
<View
android:layout_width="fill_parent"
android:layout_height="1px"
android:padding="5dip"
android:background="@android:drawable/divider_horizontal_dark" />
<include layout="@layout/control_dialog_ok"/>
</LinearLayout>

@ -1,112 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
** Copyright (c) 2012 Todoroo Inc
**
** See the file "LICENSE" for the full license governing this code.
-->
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:background="@drawable/pdv_body">
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<ImageView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:paddingTop="5dip"
android:paddingBottom="5dip"
android:scaleType="fitCenter"
android:src="@drawable/pdv_logo" />
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:paddingBottom="10dip"
android:textSize="16sp"
android:textColor="#ffffff"
android:text="@string/producteev_PLA_body" />
<TextView
android:id="@+id/error"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:paddingBottom="20dip"
android:textColor="#ff0000"
android:textSize="16sp"
android:textStyle="bold"
android:visibility="gone" />
<EditText
android:id="@+id/email"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:hint="@string/producteev_PLA_email"
android:contentDescription="E-Mail with which you registered to Producteev-service"
android:inputType="textEmailAddress" />
<EditText
android:id="@+id/password"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:contentDescription="Password for your Producteev account"
android:hint="@string/producteev_PLA_password"
android:inputType="textPassword" />
<LinearLayout
android:id="@+id/newUserLayout"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingTop="20dip"
android:visibility="gone">
<EditText
android:id="@+id/firstName"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:hint="@string/producteev_PLA_firstName"
android:inputType="textPersonName" />
<EditText
android:id="@+id/lastName"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:hint="@string/producteev_PLA_lastName"
android:inputType="textPersonName" />
<Spinner
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:id="@+id/timezoneList"
android:entries="@array/PLA_timezones_list"
android:contentDescription="@string/producteev_PLA_timezone"
android:prompt="@string/producteev_PLA_timezone" />
</LinearLayout>
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingTop="5dip"
android:baselineAligned="false">
<Button
android:id="@+id/signIn"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/producteev_PLA_signIn" />
<Button
android:id="@+id/createNew"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/producteev_PLA_createNew" />
</LinearLayout>
<TextView
android:id="@+id/terms"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:paddingTop="10dip"
android:textSize="16sp"
android:textColor="#0000ff"
android:linksClickable="true"
android:text="@string/producteev_PLA_terms" />
</LinearLayout>
</ScrollView>

@ -1,45 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
** Copyright (c) 2012 Todoroo Inc
**
** See the file "LICENSE" for the full license governing this code.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/rmilk_MLA_label"/>
<LinearLayout
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:paddingTop="5dip"
android:baselineAligned="false">
<Button android:id="@+id/done"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/DLG_done"
/>
<Button android:id="@+id/cancel"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@android:string/cancel"
/>
</LinearLayout>
<WebView android:id="@+id/browser"
android:layout_weight="1"
android:layout_width="fill_parent"
android:layout_height="wrap_content"/>
</LinearLayout>

@ -436,29 +436,6 @@
<string name="opencrx_TEA_task_assign_label">Asigna aquesta tasca a aquesta persona:</string>
<string name="opencrx_TEA_task_unassigned">&lt;Sense asignar&gt;</string>
<string name="opencrx_TEA_dashboard_default">&lt;Per defecte&gt;</string>
<string name="producteev_FEx_dashboard">Espais de treball</string>
<string name="producteev_FEx_responsible_title">Assignat a \'%s\'</string>
<string name="producteev_PDE_task_from">de %s</string>
<string name="producteev_TEA_notes">Afegir un comentari</string>
<string name="producteev_default_dashboard">Espai de treball per defecte</string>
<string name="producteev_create_dashboard">Afegir un àrea de treball...</string>
<string name="producteev_create_dashboard_name">Nom de l\'àrea de treball</string>
<string name="producteev_PPr_defaultdash_title">Espai de treball per defecte</string>
<string name="producteev_PPr_defaultdash_summary">Noves tasques s\'afegiran a: %s</string>
<string name="producteev_PPr_defaultdash_summary_none">Les tasques noves no seràn sincronitzades per defecte</string>
<string name="producteev_PLA_title">Inicia sessió a Producteev</string>
<string name="producteev_PLA_body">Iniciï sessió amb el seu compte de Producteev, o crei un compte nou!</string>
<string name="producteev_PLA_terms">Termes &amp; Condicions</string>
<string name="producteev_PLA_signIn">Ingresar</string>
<string name="producteev_PLA_createNew">Crea un usuari nou</string>
<string name="producteev_PLA_email">Adreça electrònica</string>
<string name="producteev_PLA_password">Contrasenya</string>
<string name="producteev_PLA_firstName">Nom</string>
<string name="producteev_PLA_lastName">Cognoms</string>
<string name="producteev_PLA_errorEmpty">Error: ompli tots els camps</string>
<string name="producteev_PLA_errorAuth">Error: l\'adreça electrònica o contrasenya incorrectes!</string>
<string name="producteev_TEA_task_assign_label">Asigna aquesta tasca a aquesta persona:</string>
<string name="producteev_TEA_dashboard_assign_label">Asigna aquesta tasca a aquest espai de treball:</string>
<string name="TEA_reminders_group_label">Recordatoris</string>
<string name="TEA_reminder_alarm_label">Tipus de So/Vibració</string>
<string name="TEA_reminder_mode_once">Sona una vegada</string>
@ -737,17 +714,6 @@
<item>I\'m so proud of you!</item>
<item>I love when you\'re productive!</item>
</string-array>
<string name="rmilk_EOE_button">Configuració Remember the Milk</string>
<string name="rmilk_TLA_repeat">RTM Tasca Repetitiva</string>
<string name="rmilk_TLA_sync">Necessita sincronitzar-se amb RTM</string>
<string name="rmilk_FEx_list">Llistes</string>
<string name="rmilk_FEx_list_title">RTM Llista \'%s\'</string>
<string name="rmilk_MEA_list_label">RTM Llista:</string>
<string name="rmilk_MEA_repeat_label">RTM Estat de les Repeticions:</string>
<string name="rmilk_MEA_repeat_hint">p.e. cada setmana, després de 14 dies</string>
<string name="rmilk_MLA_label">Si us plau, inicia sessió i autoritza Astrid:</string>
<string name="rmilk_MLA_error">Ho sento, hi ha hagut un error verificant les credencials. Si us plau, torni-ho a intentar. \n\n Missatge d\'error: %s</string>
<string name="rmilk_ioerror">Error de connectivitat! Verifiqui la seva connexió, o potser els servidor de RTM (status.rememberthemilk.com), per possibles solucions.</string>
<string name="TEA_tags_label">Llistes</string>
<string name="TEA_tag_hint">Llista nova</string>
<string name="tag_new_list">Llista nova</string>

@ -528,40 +528,6 @@
<string name="file_err_copy">Chyba při kopírování souboru jako přílohy</string>
<string name="file_err_download">Chyba při stahování souboru</string>
<string name="file_err_show">Je nám líto, ale systém zatím tento typ souboru nepodporuje</string>
<string name="producteev_FEx_header">Producteev</string>
<string name="producteev_FEx_dashboard">Pracovní plochy</string>
<string name="producteev_FEx_responsible_byme">Přidělil jsem</string>
<string name="producteev_FEx_responsible_byothers">Ostatní přidělili</string>
<string name="producteev_FEx_responsible_title">Přiřazeno k \'%s\'</string>
<string name="producteev_PDE_task_from">od %s</string>
<string name="producteev_TEA_notes">Přidat komentář</string>
<string name="producteev_PPr_header">Producteev</string>
<string name="producteev_default_dashboard">Výchozí pracovní plocha</string>
<string name="producteev_no_dashboard">(Nesynchronizovat)</string>
<string name="producteev_create_dashboard">Přidat novou pracovní plochu...</string>
<string name="producteev_create_dashboard_name">Název nové pracovní plochy</string>
<string name="producteev_PPr_defaultdash_title">Výchozí pracovní plocha</string>
<string name="producteev_PPr_defaultdash_summary">Nové úkoly budou přidány do: %s</string>
<string name="producteev_PPr_defaultdash_summary_none">Nové úkoly nebudou standardně synchronizovány</string>
<string name="producteev_PLA_title">Přihlaste se k Producteev</string>
<string name="producteev_PLA_body">Přihlašte se pomocí existujícího účtu Producteev nebo vytvořte nový!</string>
<string name="producteev_PLA_terms">Podmínky používání</string>
<string name="producteev_PLA_signIn">Přihlásit se</string>
<string name="producteev_PLA_createNew">Vytvořit nového uživatele</string>
<string name="producteev_PLA_email">Email</string>
<string name="producteev_PLA_password">Heslo</string>
<string name="producteev_PLA_timezone">Časové pásmo</string>
<string name="producteev_PLA_firstName">Křestní jméno</string>
<string name="producteev_PLA_lastName">Příjmení</string>
<string name="producteev_PLA_errorEmpty">Chyba: vyplňte všechna pole!</string>
<string name="producteev_PLA_errorAuth">Chyba: Nesprávný e-mail, nebo heslo!</string>
<string name="producteev_notification_title">Producteev</string>
<string name="producteev_TEA_task_assign_label">Přiřadit úkol k této osobě:</string>
<string name="producteev_TEA_dashboard_assign_label">Přiřadit úkol k této pracovní ploše:</string>
<string name="CFC_producteev_in_workspace_text">Na pracovní ploše: ?</string>
<string name="CFC_producteev_in_workspace_name">Na pracovní ploše...</string>
<string name="CFC_producteev_assigned_to_text">Přiřazeno k: ?</string>
<string name="CFC_producteev_assigned_to_name">Přiřazeno k ...</string>
<string name="TEA_reminders_group_label">Upomínky</string>
<string name="TEA_reminder_label">Upozorni mě...</string>
<string name="TEA_reminder_due">Při termínu</string>
@ -838,17 +804,6 @@
<item>I\'m so proud of you!</item>
<item>I love when you\'re productive!</item>
</string-array>
<string name="rmilk_EOE_button">Nastavení Remember the Milk</string>
<string name="rmilk_TLA_repeat">RTM Opakující se úkol</string>
<string name="rmilk_TLA_sync">Je nutná synchronizace s RTM</string>
<string name="rmilk_FEx_list">Seznamy</string>
<string name="rmilk_FEx_list_title">RTM seznam \'%s\'</string>
<string name="rmilk_MEA_list_label">RTM seznam:</string>
<string name="rmilk_MEA_repeat_label">RTM Opakovací status:</string>
<string name="rmilk_MEA_repeat_hint">to je každý týden, po 14 dnech</string>
<string name="rmilk_MLA_label">Prosím přihlaš se a autorizuj Astrid:</string>
<string name="rmilk_MLA_error">Omlouvám se, nastala chyba při ověřování tvého přihlášení. Prosím, zkus to znovu. \n\n Chybová hláška: %s</string>
<string name="rmilk_ioerror">Chyba připojení! Zkontroluj Tvé Internetové připojení nebo možná RTM servery (status.rememberthemilk.com), pro možná řešení.</string>
<string name="TEA_tags_label">Seznamy</string>
<string name="TEA_tags_none">Žádný</string>
<string name="tag_FEx_header">Seznamy</string>

@ -324,28 +324,6 @@
<string name="CFC_opencrx_in_workspace_name">I arbejdsområde...</string>
<string name="EPr_statistics_desc_disabled">Ingen data om brug vil blive rapporteret</string>
<string name="EPr_statistics_desc_enabled">Hjælp os med at forbedre Astrid ved at sende anonyme data om brug</string>
<string name="producteev_FEx_dashboard">Arbejdsområder</string>
<string name="producteev_PDE_task_from">fra %s</string>
<string name="producteev_TEA_notes">Tilføj en kommentar</string>
<string name="producteev_default_dashboard">Standard-arbejdsområde</string>
<string name="producteev_no_dashboard">(Synkroniser ikke)</string>
<string name="producteev_create_dashboard">Tilføj et nyt arbejdsområde...</string>
<string name="producteev_create_dashboard_name">Navn på nyt arbejdsområde</string>
<string name="producteev_PPr_defaultdash_title">Standard-arbejdsområde</string>
<string name="producteev_PPr_defaultdash_summary">Nye opgaver vil blive føjet til: %s</string>
<string name="producteev_PPr_defaultdash_summary_none">Nye opgaver vil som udgangspunkt ikke blive synkroniseret</string>
<string name="producteev_PLA_title">Log ind til Producteev</string>
<string name="producteev_PLA_body">Log ind med din eksisterende Producteev-konto eller opret en ny konto!</string>
<string name="producteev_PLA_signIn">Log ind</string>
<string name="producteev_PLA_createNew">Opret ny bruger</string>
<string name="producteev_PLA_password">Adgangskode</string>
<string name="producteev_PLA_timezone">Tidszone</string>
<string name="producteev_PLA_firstName">Fornavn</string>
<string name="producteev_PLA_lastName">Efternavn</string>
<string name="producteev_PLA_errorEmpty">Fejl: Udfyld alle felter!</string>
<string name="producteev_PLA_errorAuth">Fejl: E-mail eller adgangskode er forkert!</string>
<string name="CFC_producteev_in_workspace_text">I arbejdsområde: ?</string>
<string name="CFC_producteev_in_workspace_name">I arbejdsområde...</string>
<string name="TEA_reminder_due">når deadline er nået</string>
<string name="TEA_reminder_overdue">når deadline er overskredet</string>
<string name="TEA_reminder_mode_once">Ring en gang</string>
@ -602,7 +580,6 @@
<item>I\'m so proud of you!</item>
<item>I love when you\'re productive!</item>
</string-array>
<string name="rmilk_EOE_button">Remember the Milk-opsætning</string>
<string name="tag_settings_title">Settings:</string>
<string name="welcome_title_1">Velkommen til Astrid!</string>
<string name="welcome_body_5">and much more!</string>

@ -671,41 +671,6 @@
<string name="premium_success">Vielen Dank für die Anmeldung bei Astrid Premium!</string>
<string name="premium_success_with_server_error">Vielen Dank für die Anmeldung bei Astrid Premium! Die neuen Funktionen werden bald für dich auf astrid.com verfügbar sein und sollten bereits jetzt auf deinem Handy funktionieren.</string>
<string name="premium_verification_error">Der Premium Account für dieses Google Play Konto ist mit einem anderen Benutzer verknüpft. Um weitere E-Mail-Adressen zu deinem Account hinzuzufügen, besuche astrid.com/users/profile</string>
<string name="producteev_FEx_header">Producteev</string>
<string name="producteev_FEx_dashboard">Arbeitsbereiche</string>
<string name="producteev_FEx_responsible_byme">Durch mich zugeordnet an</string>
<string name="producteev_FEx_responsible_byothers">Durch andere zugeordnet an</string>
<string name="producteev_FEx_responsible_title">Zugeordnet an \'%s\'</string>
<string name="producteev_PDE_task_from">von %s</string>
<string name="producteev_TEA_notes">Einen Kommentar hinzufügen</string>
<string name="producteev_PPr_header">Producteev</string>
<string name="producteev_default_dashboard">Standard-Arbeitsbereich</string>
<string name="producteev_no_dashboard">(Nicht syncronisieren)</string>
<string name="producteev_create_dashboard">Einen neuen Arbeitsbereich hinzufügen …</string>
<string name="producteev_create_dashboard_name">Name des neuen Arbeitsbereichs</string>
<string name="producteev_PPr_defaultdash_title">Standard-Arbeitsbereich</string>
<string name="producteev_PPr_defaultdash_summary">Neue Aufgaben werden hinzugefügt zu: %s</string>
<string name="producteev_PPr_defaultdash_summary_none">Neue Aufgaben werden standardmäßig nicht synchronisiert</string>
<string name="producteev_PLA_title">Bei Producteev anmelden</string>
<string name="producteev_PLA_body">Melden Sie sich mit Ihrem vorhandenen Producteev-Konto an, oder erstellen Sie ein neues Konto!</string>
<string name="producteev_PLA_terms">Allgemeine Geschäftsbedingungen</string>
<string name="producteev_PLA_signIn">Anmelden</string>
<string name="producteev_PLA_createNew">Neuen Benutzer erstellen</string>
<string name="producteev_PLA_email">E-Mail</string>
<string name="producteev_PLA_password">Passwort</string>
<string name="producteev_PLA_timezone">Zeitzone</string>
<string name="producteev_PLA_firstName">Vorname</string>
<string name="producteev_PLA_lastName">Nachname</string>
<string name="producteev_PLA_errorEmpty">Fehler: Füllen Sie alle Felder aus!</string>
<string name="producteev_PLA_errorAuth">Fehler: E-Mail oder Kennwort falsch!</string>
<string name="producteev_notification_title">Producteev</string>
<string name="producteev_TEA_control_set_display">Aufgabe dieser Person zuordnen:</string>
<string name="producteev_TEA_task_assign_label">Aufgabe dieser Person zuordnen:</string>
<string name="producteev_TEA_dashboard_assign_label">Aufgabe diesem Arbeitsbereich zuordnen:</string>
<string name="CFC_producteev_in_workspace_text">Im Arbeitsbereich: ?</string>
<string name="CFC_producteev_in_workspace_name">Im Arbeitsbereich …</string>
<string name="CFC_producteev_assigned_to_text">Zugeordnet zu: ?</string>
<string name="CFC_producteev_assigned_to_name">Zugeordnet zu …</string>
<string name="TEA_reminders_group_label">Erinnerungen</string>
<string name="TEA_reminder_label">Erinnere mich:</string>
<string name="TEA_reminder_due">Wenn Aufgabe fällig ist</string>
@ -1013,21 +978,6 @@
<item>Ich bin stolz auf Sie!</item>
<item>Es gefällt mir, wenn Sie produktiv sind!</item>
</string-array>
<string name="rmilk_EOE_button">Remember the Milk Einstellungen</string>
<string name="rmilk_TLA_repeat">RTM Wiederholende Aufgabe</string>
<string name="rmilk_TLA_sync">Synchronisierung mit RTM benötigt</string>
<string name="rmilk_FEx_header">Remember the Milk</string>
<string name="rmilk_FEx_list">Listen</string>
<string name="rmilk_FEx_list_title">RTM Liste \'%s\'</string>
<string name="rmilk_MEA_title">Remember the Milk</string>
<string name="rmilk_MEA_list_label">RTM Liste:</string>
<string name="rmilk_MEA_repeat_label">RTM Wiederholungsstatus</string>
<string name="rmilk_MEA_repeat_hint">z.B. jede Woche, nach 14 Tagen</string>
<string name="rmilk_MPr_header">Remember the Milk</string>
<string name="rmilk_MLA_label">Bitte einloggen und Astrid autorisieren</string>
<string name="rmilk_MLA_error">Entschuldigung, beim Einloggen ist ein Fehler aufgetreten. Bitte versuche es erneut. \n\n Fehlermeldung: %s</string>
<string name="rmilk_notification_title">Astrid: Remember the Milk</string>
<string name="rmilk_ioerror">Verbindungsfehler! Bitte überprüfe deine Internetverbindung oder die RTM Server (status.rememberthemilk.com) zur Lösung des Problems.</string>
<string name="subtasks_help_title">Sortieren und einrücken in Astrid</string>
<string name="subtasks_help_1">Drücken und halten, um Aufgabe zu verschieben</string>
<string name="subtasks_help_2">Sortieren ändern durch Ziehen nach oben oder unten</string>

@ -710,41 +710,6 @@
<string name="premium_verification_error">La suscripción Premium para ésta cuenta de Google Play está conectada a un usuario diferente. Para ligar direcciones de correo electrónico adicionales a tu cuenta, visita astrid.com/users/profile</string>
<string name="premium_check_for_purchases">¿Ha adquirido premium? Clic aquí para comprobar el historial de compras de Google Play.</string>
<string name="premium_checking_for_purchases">Comprando compras...</string>
<string name="producteev_FEx_header">Producteev</string>
<string name="producteev_FEx_dashboard">Espacios de trabajo</string>
<string name="producteev_FEx_responsible_byme">Asignada por mi a</string>
<string name="producteev_FEx_responsible_byothers">Asignada por otros a</string>
<string name="producteev_FEx_responsible_title">Asignado a \'%s\'</string>
<string name="producteev_PDE_task_from">de %s</string>
<string name="producteev_TEA_notes">Añadir un comentario</string>
<string name="producteev_PPr_header">Producteev</string>
<string name="producteev_default_dashboard">Espacio de trabajo por defecto</string>
<string name="producteev_no_dashboard">(No sincronizar)</string>
<string name="producteev_create_dashboard">Añadir nueva área de trabajo...</string>
<string name="producteev_create_dashboard_name">Nombre de la nueva área de trabajo</string>
<string name="producteev_PPr_defaultdash_title">Espacio de trabajo por defecto</string>
<string name="producteev_PPr_defaultdash_summary">Las tareas nuevas se añadirán a: %s</string>
<string name="producteev_PPr_defaultdash_summary_none">Las tareas nuevas no se sincronizarán por defecto</string>
<string name="producteev_PLA_title">Ingresar a Producteev</string>
<string name="producteev_PLA_body">Ingrese en Producteev con su cuenta existente, o cree una nueva!</string>
<string name="producteev_PLA_terms">Términos y Condiciones</string>
<string name="producteev_PLA_signIn">Ingresar</string>
<string name="producteev_PLA_createNew">Crear un usuario nuevo</string>
<string name="producteev_PLA_email">Correo electrónico</string>
<string name="producteev_PLA_password">Contraseña</string>
<string name="producteev_PLA_timezone">Huso horario</string>
<string name="producteev_PLA_firstName">Nombre</string>
<string name="producteev_PLA_lastName">Apellido</string>
<string name="producteev_PLA_errorEmpty">Error: rellene todos los campos!</string>
<string name="producteev_PLA_errorAuth">Error: correo o contraseña incorrectos!</string>
<string name="producteev_notification_title">Producteev</string>
<string name="producteev_TEA_control_set_display">Tareas de producteev</string>
<string name="producteev_TEA_task_assign_label">Asignar esta tarea a esta persona:</string>
<string name="producteev_TEA_dashboard_assign_label">Asignar esa tarea a este espacio de trabajo:</string>
<string name="CFC_producteev_in_workspace_text">¿En qué espacio de trabajo?</string>
<string name="CFC_producteev_in_workspace_name">En el espacio de trabajo...</string>
<string name="CFC_producteev_assigned_to_text">Asignado a: ?</string>
<string name="CFC_producteev_assigned_to_name">Asginado a...</string>
<string name="TEA_reminders_group_label">Recordatorios</string>
<string name="TEA_reminder_label">Recordarme:</string>
<string name="TEA_reminder_due">Cuando la tarea esté en debido tiempo</string>
@ -1052,21 +1017,6 @@
<item>¡Estoy muy orgulloso de usted!</item>
<item>¡Me encanta cuando usted es productivo!</item>
</string-array>
<string name="rmilk_EOE_button">Ajustes de Remember the Milk</string>
<string name="rmilk_TLA_repeat">RTM Tarea Repetitiva</string>
<string name="rmilk_TLA_sync">Se necesita sincronizar con RTM</string>
<string name="rmilk_FEx_header">Remember the Milk</string>
<string name="rmilk_FEx_list">Listas</string>
<string name="rmilk_FEx_list_title">RTM Lista \'%s\'</string>
<string name="rmilk_MEA_title">Remember the Milk</string>
<string name="rmilk_MEA_list_label">RTM Lista:</string>
<string name="rmilk_MEA_repeat_label">RTM Repita Estado:</string>
<string name="rmilk_MEA_repeat_hint">Es decir, cada semana, después de 14 días</string>
<string name="rmilk_MPr_header">Remember the Milk</string>
<string name="rmilk_MLA_label">Por favor, inicie sesión y autorice a Astrid:</string>
<string name="rmilk_MLA_error">Lo siento. Se produjo un error al verificar sus credenciales. Por favor, intente de nuevo. \n\nMensaje de error: %s</string>
<string name="rmilk_notification_title">Astrid: Remember the Milk</string>
<string name="rmilk_ioerror">Error de conexión! Compruebe su conexión a Internet, o quizá los servidores de RTM (status.rememberthemilk.com), para posibles soluciones.</string>
<string name="subtasks_help_title">Organice y Aplique Sangrías en Astrid</string>
<string name="subtasks_help_1">Mantenga presionado para mover una tarea</string>
<string name="subtasks_help_2">Arrastre verticalmente para reacomodar</string>

@ -683,41 +683,6 @@
<string name="premium_success">Merci pour votre inscription à Astrid Premium !</string>
<string name="premium_success_with_server_error">Merci pour votre inscription à Astrid Premium ! Vos nouvelles fonctionnalités vont être débloquées sur astrid.com et devrait être disponible sur votre téléphone aussitôt.</string>
<string name="premium_verification_error">L\'abonnement premium de ce compte Google Play est lié à un utilisateur différent. Pour ajouter une adresse mail à votre compte, allez sur astrid.com/users/profile</string>
<string name="producteev_FEx_header">Producteev</string>
<string name="producteev_FEx_dashboard">Espaces de travail</string>
<string name="producteev_FEx_responsible_byme">Assigné par moi à</string>
<string name="producteev_FEx_responsible_byothers">Assigné par d\'autres à</string>
<string name="producteev_FEx_responsible_title">Attribué à « %s »</string>
<string name="producteev_PDE_task_from">de %s</string>
<string name="producteev_TEA_notes">Ajouter un commentaire</string>
<string name="producteev_PPr_header">Producteev</string>
<string name="producteev_default_dashboard">Espace de travail par défaut</string>
<string name="producteev_no_dashboard">(Ne pas synchroniser)</string>
<string name="producteev_create_dashboard">Ajouter un nouvel espace de travail</string>
<string name="producteev_create_dashboard_name">Nom du nouvel espace de travail</string>
<string name="producteev_PPr_defaultdash_title">Espace de travail par défaut</string>
<string name="producteev_PPr_defaultdash_summary">Les nouvelles tâches seront ajoutées à : %s</string>
<string name="producteev_PPr_defaultdash_summary_none">Les nouvelles tâches ne seront pas synchronisées par défaut</string>
<string name="producteev_PLA_title">Se Connecter à Producteev</string>
<string name="producteev_PLA_body">Connectez-vous avec votre compte Producteev existant, ou créez un nouveau compte !</string>
<string name="producteev_PLA_terms">Termes Et Conditions</string>
<string name="producteev_PLA_signIn">S\'identifier</string>
<string name="producteev_PLA_createNew">Créer un nouvel utilisateur</string>
<string name="producteev_PLA_email">E-mail</string>
<string name="producteev_PLA_password">Mot de passe</string>
<string name="producteev_PLA_timezone">Fuseau horaire</string>
<string name="producteev_PLA_firstName">Prénom</string>
<string name="producteev_PLA_lastName">Nom</string>
<string name="producteev_PLA_errorEmpty">Erreur : remplissez tous les champs !</string>
<string name="producteev_PLA_errorAuth">Erreur: E-mail ou mot de passe incorrect!</string>
<string name="producteev_notification_title">Producteev</string>
<string name="producteev_TEA_control_set_display">Tâche attribuée Producteev</string>
<string name="producteev_TEA_task_assign_label">Attribuer cette tâche à cette personne :</string>
<string name="producteev_TEA_dashboard_assign_label">Attribuer cette tâche à cet espace de travail :</string>
<string name="CFC_producteev_in_workspace_text">Dans l\'espace de travail : ?</string>
<string name="CFC_producteev_in_workspace_name">Dans l\'espace de travail...</string>
<string name="CFC_producteev_assigned_to_text">Attribué à : ?</string>
<string name="CFC_producteev_assigned_to_name">Attribué à...</string>
<string name="TEA_reminders_group_label">Rappels</string>
<string name="TEA_reminder_label">Rappel moi :</string>
<string name="TEA_reminder_due">... lorsque la tâche est échue</string>
@ -1025,21 +990,6 @@
<item>Je suis si fière de toi !</item>
<item>J\'adore quand vous êtes productif !</item>
</string-array>
<string name="rmilk_EOE_button">Paramètres Remember the Milk</string>
<string name="rmilk_TLA_repeat">Répétition de tâche RTM</string>
<string name="rmilk_TLA_sync">Requiert une synchronisation avec RTM</string>
<string name="rmilk_FEx_header">Remember the Milk</string>
<string name="rmilk_FEx_list">Listes</string>
<string name="rmilk_FEx_list_title">Liste RTM \'%s\'</string>
<string name="rmilk_MEA_title">Remember the Milk</string>
<string name="rmilk_MEA_list_label">Liste RTM :</string>
<string name="rmilk_MEA_repeat_label">Statut de répétition RTM :</string>
<string name="rmilk_MEA_repeat_hint">ex. : chaque semaine, après 14 jours</string>
<string name="rmilk_MPr_header">Remember the Milk</string>
<string name="rmilk_MLA_label">Veuillez vous connecter et autoriser Astrid :</string>
<string name="rmilk_MLA_error">Désolé, une erreur est survenue lors de la vérification de votre identifiant. Veuillez réessayer. \n\n Message d\'erreur : %s</string>
<string name="rmilk_notification_title">Astrid : Remember the Milk</string>
<string name="rmilk_ioerror">Erreur de connexion ! Vérifiez votre connexion Internet ou les serveur RTM (status.rememberthemilk.com) pour de possibles solutions.</string>
<string name="subtasks_help_title">Trier et indenter dans Astrid</string>
<string name="subtasks_help_1">Garder appuyer pour déplacer une tâche</string>
<string name="subtasks_help_2">Déplacer verticalement pour réorganiser</string>

@ -474,34 +474,6 @@
<string name="EPr_statistics_title">Statistiche Anomime Utilizzo</string>
<string name="EPr_statistics_desc_disabled">Nessun dato verrà inviato</string>
<string name="EPr_statistics_desc_enabled">Aiutaci a migliorare Astrid inviando informazioni anonime sull\'utilizzo</string>
<string name="producteev_FEx_dashboard">Aree di lavoro</string>
<string name="producteev_FEx_responsible_byme">Assegnato a</string>
<string name="producteev_FEx_responsible_title">Assegnato a \'%s\'</string>
<string name="producteev_PDE_task_from">da %s</string>
<string name="producteev_TEA_notes">Aggiungi un Commento</string>
<string name="producteev_default_dashboard">Spazio di lavoro predefinito</string>
<string name="producteev_no_dashboard">Non Sincronizzare</string>
<string name="producteev_create_dashboard">Aggiungi un nuovo Spazio di Lavoro...</string>
<string name="producteev_create_dashboard_name">Nome per il nuovo Spazio di Lavoro</string>
<string name="producteev_PPr_defaultdash_title">Spazio di lavoro predefinito</string>
<string name="producteev_PPr_defaultdash_summary">Le nuove attività verranno aggiunte a: %s</string>
<string name="producteev_PPr_defaultdash_summary_none">Le nuove attività non verranno sincronizzate in modo predefinito</string>
<string name="producteev_PLA_title">Accedi a Producteev</string>
<string name="producteev_PLA_body">Entra con il tuo account Producteev, o registra un nuovo account!</string>
<string name="producteev_PLA_terms">Termini &amp; Condizioni</string>
<string name="producteev_PLA_signIn">Accedi</string>
<string name="producteev_PLA_createNew">Crea Nuovo Utente</string>
<string name="producteev_PLA_timezone">Fuso orario</string>
<string name="producteev_PLA_firstName">Nome</string>
<string name="producteev_PLA_lastName">Cognome</string>
<string name="producteev_PLA_errorEmpty">Errore: riempi tutti i campi!</string>
<string name="producteev_PLA_errorAuth">Errore: e-mail o password sbagliate!</string>
<string name="producteev_TEA_task_assign_label">Assegna questa attività alla persona seguente:</string>
<string name="producteev_TEA_dashboard_assign_label">Assegna questa attività a questo spazio di lavoro:</string>
<string name="CFC_producteev_in_workspace_text">Nello spazio di lavoro: ?</string>
<string name="CFC_producteev_in_workspace_name">Nello spazio di lavoro...</string>
<string name="CFC_producteev_assigned_to_text">Assegnato a: ?</string>
<string name="CFC_producteev_assigned_to_name">Assegnato a...</string>
<string name="TEA_reminder_label">Ricordami...</string>
<string name="TEA_reminder_due">... quando l\'attività deve terminare</string>
<string name="TEA_reminder_overdue">... quando l\'attività è in ritardo</string>
@ -783,21 +755,6 @@
<item>I\'m so proud of you!</item>
<item>I love when you\'re productive!</item>
</string-array>
<string name="rmilk_EOE_button">Ricorda le impostazioni di Milk</string>
<string name="rmilk_TLA_repeat">Ripetizione Attività RTM</string>
<string name="rmilk_TLA_sync">Necessita la sincronizzazione con RTM</string>
<string name="rmilk_FEx_header">Ricorda Milk</string>
<string name="rmilk_FEx_list">Liste</string>
<string name="rmilk_FEx_list_title">LIsta RTM \'%s\'</string>
<string name="rmilk_MEA_title">Ricorda Milk</string>
<string name="rmilk_MEA_list_label">Lista RTM:</string>
<string name="rmilk_MEA_repeat_label">Stato di ripetizione RTM:</string>
<string name="rmilk_MEA_repeat_hint">in altre parole ogni settimana, dopo 14 giorni</string>
<string name="rmilk_MPr_header">Ricorda Milk</string>
<string name="rmilk_MLA_label">Per favore esegui l\'accesso e autorizza Astrid:</string>
<string name="rmilk_MLA_error">Spiacenti,si è verificato un errore durante la verifica di accesso. Prova di nuovo. \n\n Messaggio di Errore: %s</string>
<string name="rmilk_notification_title">Astrid: Ricorda il Milk</string>
<string name="rmilk_ioerror">Errore di connessione! Verificare la connessione Internet, o magari i server RTM (status.rememberthemilk.com), per le possibili soluzioni.</string>
<string name="TEA_tags_label">Liste</string>
<string name="TEA_tag_hint">Nuovo elenco</string>
<string name="tag_new_list">Nuovo elenco</string>

@ -625,41 +625,6 @@
<string name="premium_description_2">סינכרון קבצים עם אתר astrid.com</string>
<string name="premium_login_prompt">יש צורך בחשבון באתר astrid.com כדי להרשם לגירסת הפרימיום. אנא התחבר או הרשם.</string>
<string name="premium_success_with_server_error">תודה על ההרשמה לאסטריד פרימיום! התכונות החדשות תיפתחנה לשימוש באתר astrid.com בקרוב, ותהיינה זמינות בטלפון שלך מיידית.</string>
<string name="producteev_FEx_header">Producteev</string>
<string name="producteev_FEx_dashboard">משטחי עבודה</string>
<string name="producteev_FEx_responsible_byme">הטלתי על</string>
<string name="producteev_FEx_responsible_byothers">הוטלו עלי</string>
<string name="producteev_FEx_responsible_title">הוטל על \'%s\'</string>
<string name="producteev_PDE_task_from">מ- %s</string>
<string name="producteev_TEA_notes">הוסף הערה</string>
<string name="producteev_PPr_header">Producteev</string>
<string name="producteev_default_dashboard">משטח עבודה ברירת מחדל</string>
<string name="producteev_no_dashboard">(אל תסנכרן)</string>
<string name="producteev_create_dashboard">הוסף משטח עבודה חדש...</string>
<string name="producteev_create_dashboard_name">שם של משטח עבודה חדש...</string>
<string name="producteev_PPr_defaultdash_title">ברירת מחדל של משטח עבודה</string>
<string name="producteev_PPr_defaultdash_summary">משימות חדשות תתווספנה ל־%s</string>
<string name="producteev_PPr_defaultdash_summary_none">משימות חדשות לא תסתנכרנה באופן אוטומטי</string>
<string name="producteev_PLA_title">Producteev התחבר ל</string>
<string name="producteev_PLA_body">התחבר באמצעות חשבון ה־Producteev שלך או צור חשבון חדש!</string>
<string name="producteev_PLA_terms">תנאים והתניות</string>
<string name="producteev_PLA_signIn">התחבר</string>
<string name="producteev_PLA_createNew">צור משתמש חדש</string>
<string name="producteev_PLA_email">דוא״ל</string>
<string name="producteev_PLA_password">סיסמא</string>
<string name="producteev_PLA_timezone">איזור זמן</string>
<string name="producteev_PLA_firstName">שם פרטי</string>
<string name="producteev_PLA_lastName">שם משפחה</string>
<string name="producteev_PLA_errorEmpty">שגיאה: אנא מלא את כל השדות!</string>
<string name="producteev_PLA_errorAuth">שגיאה: כתובת דוא״ל או סיסמא אינם נכונים!</string>
<string name="producteev_notification_title">Producteev</string>
<string name="producteev_TEA_control_set_display">הטלת משימה של Producteev</string>
<string name="producteev_TEA_task_assign_label">הטל משימה זו על אדם זה:</string>
<string name="producteev_TEA_dashboard_assign_label">הצב משימה זו במשטח עבודה:</string>
<string name="CFC_producteev_in_workspace_text">במשטח עבודה: ?</string>
<string name="CFC_producteev_in_workspace_name">במשטח עבודה...</string>
<string name="CFC_producteev_assigned_to_text">הוטל על: ?</string>
<string name="CFC_producteev_assigned_to_name">הוטל על...</string>
<string name="TEA_reminders_group_label">תזכורות</string>
<string name="TEA_reminder_label">הזכר לי:</string>
<string name="TEA_reminder_due">כאשר הגיע מועד הסף</string>
@ -964,21 +929,6 @@
<item>אני כל כך גֵּאָה בְּךָ!</item>
<item>אני אוהבת שאתה פורה!</item>
</string-array>
<string name="rmilk_EOE_button">הגדרות Remember the Milk</string>
<string name="rmilk_TLA_repeat">משימה חוזרת של RTM</string>
<string name="rmilk_TLA_sync">נדרש סינכרון עם ּRTM</string>
<string name="rmilk_FEx_header">Remember the Milk</string>
<string name="rmilk_FEx_list">רשימות</string>
<string name="rmilk_FEx_list_title">רשימת RTM \'%s\'</string>
<string name="rmilk_MEA_title">Remember the Milk</string>
<string name="rmilk_MEA_list_label">רשימה של RTM:</string>
<string name="rmilk_MEA_repeat_label">סטטוס חזרה של RTM:</string>
<string name="rmilk_MEA_repeat_hint">למשל כל שבוע, או אחרי 14 יום</string>
<string name="rmilk_MPr_header">Remember the Milk</string>
<string name="rmilk_MLA_label">אנא התחבר ואשר את אסטריד:</string>
<string name="rmilk_MLA_error">מצטערת, אירעה שגיאה בהתחברות למערכת.\n\nהודעת השגיאה הייתה: %s</string>
<string name="rmilk_notification_title">אסטריד: Remember the Milk</string>
<string name="rmilk_ioerror">שגיאת התחברות! בדוק את החיבור שלך לאינטרנט ואת השרתים של RTM (status.rememberthemilk.com) כדי לנסות לפתור את הבעיה.</string>
<string name="subtasks_help_title">מיין והזח באסטריד</string>
<string name="subtasks_help_1">גע והחזק כדי להזיז משימה</string>
<string name="subtasks_help_2">גרור אנכית כדי לארגן מחדש</string>

@ -352,14 +352,6 @@
<string name="EPr_statistics_title">匿名の使用統計データ</string>
<string name="EPr_statistics_desc_disabled">使用統計情報は送信されません</string>
<string name="EPr_statistics_desc_enabled">送られた使用統計情報は Astrid の改善に使用されます</string>
<string name="producteev_PLA_title">Producteev にログイン</string>
<string name="producteev_PLA_body">既存の Producteev アカウントにサインインするか、アカウントを作成してください</string>
<string name="producteev_PLA_signIn">サインイン</string>
<string name="producteev_PLA_email">メールアドレス</string>
<string name="producteev_PLA_password">パスワード</string>
<string name="producteev_PLA_timezone">タイムゾーン</string>
<string name="producteev_PLA_errorEmpty">エラー: すべての入力欄を入力してください</string>
<string name="producteev_PLA_errorAuth">エラー: メールアドレスかパスワードが正しくありません</string>
<string name="TEA_reminder_label">通知するのは...</string>
<string name="TEA_reminder_due">期限になったとき</string>
<string name="TEA_reminder_overdue">期限を過ぎたとき</string>
@ -635,11 +627,6 @@
<item>I\'m so proud of you!</item>
<item>I love when you\'re productive!</item>
</string-array>
<string name="rmilk_EOE_button">Remember the Milk の設定</string>
<string name="rmilk_FEx_list">リスト</string>
<string name="rmilk_FEx_list_title">RTM のリスト: %s</string>
<string name="rmilk_MLA_label">ログインしてAstridに権限を与えてください</string>
<string name="rmilk_ioerror">接続に失敗しました。インターネット接続を確認してください。RTM のサーバ運用情報 (status.rememberthemilk.com) も確かめてみてください。</string>
<string name="TEA_tags_label">リスト</string>
<string name="tag_FEx_header">リスト</string>
<string name="TEA_no_tags_modified">変更されませんでした</string>

@ -710,41 +710,6 @@
<string name="premium_verification_error">이 구글 플레이 계정으로 구매한 프리미엄 이용권은 다른 사용자에게 연결되어 있습니다. 당신의 계정에 추가적인 이메일을 연결하려면 astrid.com/users/profile 을 방문하세요.</string>
<string name="premium_check_for_purchases">프리미엄을 이미 구매하셨나요? 구글 플레이에서 구매 내역을 확인하려면 여기를 클릭하세요</string>
<string name="premium_checking_for_purchases">구매 확인 중...</string>
<string name="producteev_FEx_header">Producteev</string>
<string name="producteev_FEx_dashboard">작업공간</string>
<string name="producteev_FEx_responsible_byme">내가 할당한 사람:</string>
<string name="producteev_FEx_responsible_byothers">다른 사람이 할당한 사람:</string>
<string name="producteev_FEx_responsible_title">할당됨: \'%s\'</string>
<string name="producteev_PDE_task_from">from %s</string>
<string name="producteev_TEA_notes">댓글 달기</string>
<string name="producteev_PPr_header">Producteev</string>
<string name="producteev_default_dashboard">기본 작업공간</string>
<string name="producteev_no_dashboard">(싱크 중지)</string>
<string name="producteev_create_dashboard">새로운 작업공간 추가...</string>
<string name="producteev_create_dashboard_name">새로운 작업공간 이름</string>
<string name="producteev_PPr_defaultdash_title">기본 작업공간</string>
<string name="producteev_PPr_defaultdash_summary">새 일정 추가장소: %s</string>
<string name="producteev_PPr_defaultdash_summary_none">새 일정은 기본적으로 동기화되지 않습니다</string>
<string name="producteev_PLA_title">Producteev 로그인 하기</string>
<string name="producteev_PLA_body">Producteev 계정으로 로그인 하시거나 새로운 계정을 만드세요!</string>
<string name="producteev_PLA_terms">계약 조건</string>
<string name="producteev_PLA_signIn">로그인</string>
<string name="producteev_PLA_createNew">새 사용자 생성</string>
<string name="producteev_PLA_email">이메일</string>
<string name="producteev_PLA_password">비밀번호</string>
<string name="producteev_PLA_timezone">표준시간대</string>
<string name="producteev_PLA_firstName">이름</string>
<string name="producteev_PLA_lastName"></string>
<string name="producteev_PLA_errorEmpty">에러: 모든 항목을 입력하세요!</string>
<string name="producteev_PLA_errorAuth">에러: 이메일이나 비밀번호가 맞지 않습니다!</string>
<string name="producteev_notification_title">Producteev</string>
<string name="producteev_TEA_control_set_display">Producteev 일정</string>
<string name="producteev_TEA_task_assign_label">이 일정을 다음 사람에 할당:</string>
<string name="producteev_TEA_dashboard_assign_label">이 일정을 다음 작업공간에 할당:</string>
<string name="CFC_producteev_in_workspace_text">작업공간에: ?</string>
<string name="CFC_producteev_in_workspace_name">작업공간...</string>
<string name="CFC_producteev_assigned_to_text">할당된 사람: ?</string>
<string name="CFC_producteev_assigned_to_name">할당된 사람...</string>
<string name="TEA_reminders_group_label">알림</string>
<string name="TEA_reminder_label">알려주세요:</string>
<string name="TEA_reminder_due">완료일이 되었을 때</string>
@ -1052,21 +1017,6 @@
<item>네가 자랑스럽다!</item>
<item>네가 생산적일 때가 좋더라!</item>
</string-array>
<string name="rmilk_EOE_button">Remember the Milk 설정</string>
<string name="rmilk_TLA_repeat">RTM 반복 일정</string>
<string name="rmilk_TLA_sync">RTM 과 싱크가 필요</string>
<string name="rmilk_FEx_header">Remember the Milk</string>
<string name="rmilk_FEx_list">목록</string>
<string name="rmilk_FEx_list_title">RTM 목록 \'%s\'</string>
<string name="rmilk_MEA_title">Remember the Milk</string>
<string name="rmilk_MEA_list_label">RTM 목록:</string>
<string name="rmilk_MEA_repeat_label">RTM 반복 설정:</string>
<string name="rmilk_MEA_repeat_hint">예: 매주, 14일 후</string>
<string name="rmilk_MPr_header">Remember the Milk</string>
<string name="rmilk_MLA_label">로그인 후 Astrid 를 인증하세요:</string>
<string name="rmilk_MLA_error">로그인 정보 확인에 실패하였습니다. 다시 시도하세요. \n\n 에러 메시지: %s</string>
<string name="rmilk_notification_title">Astrid: Remember the Milk</string>
<string name="rmilk_ioerror">연결 오류! 인터넷 연결이나 RTM 서버 (status.rememberthemilk.com) 를 확인하세요.</string>
<string name="subtasks_help_title">Astrid에서 정렬하고 들여쓰기</string>
<string name="subtasks_help_1">일정을 옮기려면 길게 두드립니다</string>
<string name="subtasks_help_2">재배열하려면 수직으로 드래그합니다</string>

@ -289,35 +289,6 @@
<string name="CFC_opencrx_assigned_to_name">Tildelt...</string>
<string name="EPr_statistics_desc_disabled">Ingen bruksdata vil bli rapportert</string>
<string name="EPr_statistics_desc_enabled">Hjelp oss å forbedre Astrid ved å sende anonym bruksdata</string>
<string name="producteev_FEx_dashboard">Arbeidsområder</string>
<string name="producteev_FEx_responsible_title">Tildelt til \'%s\'</string>
<string name="producteev_PDE_task_from">fra %s</string>
<string name="producteev_TEA_notes">Legg til kommentar</string>
<string name="producteev_default_dashboard">Standart arbeidsområde</string>
<string name="producteev_no_dashboard">(Ikke synkroniser)</string>
<string name="producteev_create_dashboard">Legg til nytt arbeidsområde</string>
<string name="producteev_create_dashboard_name">Navn for nytt arbeidsområde</string>
<string name="producteev_PPr_defaultdash_title">Standart arbeidsområde</string>
<string name="producteev_PPr_defaultdash_summary">Nye oppgaver legges til: %s</string>
<string name="producteev_PPr_defaultdash_summary_none">Nye oppgaver synkroniseres ikke automatisk</string>
<string name="producteev_PLA_title">Logg inn på Producteev</string>
<string name="producteev_PLA_body">Logg inn med din Producteev-konto eller opprett en ny konto!</string>
<string name="producteev_PLA_terms">Brukervilkår</string>
<string name="producteev_PLA_signIn">Logg inn</string>
<string name="producteev_PLA_createNew">Lag ny bruker</string>
<string name="producteev_PLA_email">E-post</string>
<string name="producteev_PLA_password">Passord</string>
<string name="producteev_PLA_timezone">Tidssone</string>
<string name="producteev_PLA_firstName">Fornavn</string>
<string name="producteev_PLA_lastName">Etternavn</string>
<string name="producteev_PLA_errorEmpty">Feil: Alle felt må fylles ut!</string>
<string name="producteev_PLA_errorAuth">Feil: feil e-post eller passord!</string>
<string name="producteev_TEA_task_assign_label">Tildel følgende person denne oppgaven:</string>
<string name="producteev_TEA_dashboard_assign_label">Legg oppgaven til dette arbeidsområdet:</string>
<string name="CFC_producteev_in_workspace_text">I arbeidsområde:?</string>
<string name="CFC_producteev_in_workspace_name">I arbeidsområde...</string>
<string name="CFC_producteev_assigned_to_text">Tildelt: ?</string>
<string name="CFC_producteev_assigned_to_name">Tildelt...</string>
<string name="TEA_reminder_label">Minn meg på...</string>
<string name="TEA_reminder_due">...når oppgaven forfaller</string>
<string name="TEA_reminder_overdue">... når oppgaven har forfalt</string>
@ -593,17 +564,6 @@
<item>I\'m so proud of you!</item>
<item>I love when you\'re productive!</item>
</string-array>
<string name="rmilk_EOE_button">Remember the Milk innstillinger</string>
<string name="rmilk_TLA_repeat">RTM gjentagende oppgave</string>
<string name="rmilk_TLA_sync">Trenger synkronisering med RTM</string>
<string name="rmilk_FEx_list">Lister</string>
<string name="rmilk_FEx_list_title">RTM Liste \'%s\'</string>
<string name="rmilk_MEA_list_label">RTM Liste:</string>
<string name="rmilk_MEA_repeat_label">RTM Gjentakelsesstatus</string>
<string name="rmilk_MEA_repeat_hint">f.eks. hver uke, etter 14 dager</string>
<string name="rmilk_MLA_label">Vennligst logg inn og autoriser Astrid</string>
<string name="rmilk_MLA_error">Beklager, kunne ikke verifisere innloggingen. Vennligst prøv igjen. \n\n Feilmelding: %s</string>
<string name="rmilk_ioerror">Tilkoblings feil! Sjekk internettforbindelsen din, evt. RTM serverene (status.rememberthemilk.com), for mulig feilløsning.</string>
<string name="TEA_tags_label">Lister</string>
<string name="tag_FEx_header">Lister</string>
<string name="TEA_no_tags_modified">Ingen endringer gjort</string>

@ -620,41 +620,6 @@
<string name="file_err_copy">Fout bij kopiëren toe te voegen bestand</string>
<string name="file_err_download">Fout bij downloaden bestand</string>
<string name="file_err_show">Sorry, het systeem ondersteunt dit bestandstype niet</string>
<string name="producteev_FEx_header">Producteev</string>
<string name="producteev_FEx_dashboard">Werkbladen</string>
<string name="producteev_FEx_responsible_byme">Ook aan mij toegewezen</string>
<string name="producteev_FEx_responsible_byothers">Ook aan anderen toegewezen</string>
<string name="producteev_FEx_responsible_title">Toegewezen Aan \'%s\'</string>
<string name="producteev_PDE_task_from">van %s</string>
<string name="producteev_TEA_notes">Commentaar toevoegen</string>
<string name="producteev_PPr_header">Producteev</string>
<string name="producteev_default_dashboard">Standaard Workspace</string>
<string name="producteev_no_dashboard">(Niet synchroniseren)</string>
<string name="producteev_create_dashboard">Nieuwe Workspace toevoegen...</string>
<string name="producteev_create_dashboard_name">Naam voor nieuwe Workspace</string>
<string name="producteev_PPr_defaultdash_title">Standaard Workspace</string>
<string name="producteev_PPr_defaultdash_summary">Nieuwe taken worden toegevoegd aan: %s</string>
<string name="producteev_PPr_defaultdash_summary_none">Nieuwe taken worden standaard niet gesynchroniseerd</string>
<string name="producteev_PLA_title">Aanmelden bij Producteev</string>
<string name="producteev_PLA_body">Meld je aan bij Producteev met je bestaande gegevens of maak een nieuw account aan.</string>
<string name="producteev_PLA_terms">Algemene Voorwaarden</string>
<string name="producteev_PLA_signIn">Aanmelden</string>
<string name="producteev_PLA_createNew">Maak gebruikersaccount</string>
<string name="producteev_PLA_email">E-mail</string>
<string name="producteev_PLA_password">Wachtwoord</string>
<string name="producteev_PLA_timezone">Tijdzone</string>
<string name="producteev_PLA_firstName">Voornaam</string>
<string name="producteev_PLA_lastName">Achternaam</string>
<string name="producteev_PLA_errorEmpty">Fout: vul alle velden in!</string>
<string name="producteev_PLA_errorAuth">Fout: e-mail of wachtwoord verkeerd ingevuld!</string>
<string name="producteev_notification_title">Producteev</string>
<string name="producteev_TEA_control_set_display">Producteev Toewijzing</string>
<string name="producteev_TEA_task_assign_label">Taak aan deze persoon toewijzen:</string>
<string name="producteev_TEA_dashboard_assign_label">Taak aan deze Workspace toewijzen:</string>
<string name="CFC_producteev_in_workspace_text">In Workspace: ?</string>
<string name="CFC_producteev_in_workspace_name">In Workspace...</string>
<string name="CFC_producteev_assigned_to_text">Toegewezen aan: ?</string>
<string name="CFC_producteev_assigned_to_name">Toegewezen aan...</string>
<string name="TEA_reminders_group_label">Herinneringen</string>
<string name="TEA_reminder_label">Herinneren:</string>
<string name="TEA_reminder_due">op einddatum taak</string>
@ -960,21 +925,6 @@
<item>Ik ben zo trots op je!</item>
<item>Ik hou ervan wanneer je productief bent!</item>
</string-array>
<string name="rmilk_EOE_button">Remember the Milk instellingen</string>
<string name="rmilk_TLA_repeat">RTM herhalende taak</string>
<string name="rmilk_TLA_sync">Synchroniseren met RTM</string>
<string name="rmilk_FEx_header">Remember the Milk</string>
<string name="rmilk_FEx_list">Lijsten</string>
<string name="rmilk_FEx_list_title">RTM-lijst \'%s\'</string>
<string name="rmilk_MEA_title">Remember the Milk</string>
<string name="rmilk_MEA_list_label">RTM-lijst:</string>
<string name="rmilk_MEA_repeat_label">RTM herhaal status:</string>
<string name="rmilk_MEA_repeat_hint">bijv. wekelijks, over 14 dagen</string>
<string name="rmilk_MPr_header">Remember the Milk</string>
<string name="rmilk_MLA_label">Aanmelden en Astrid autoriseren:</string>
<string name="rmilk_MLA_error">Fout bij het aanmelden. Probeer het nogmaals. \n\n Foutmelding: %s</string>
<string name="rmilk_notification_title">Astrid: Remember the Milk</string>
<string name="rmilk_ioerror">Verbindingsfout! Controleer de internetverbinding, of de RTM servers (status.rememberthemilk.com), voor mogelijke oplossingen.</string>
<string name="subtasks_help_title">Sorteren en inspringen in Astrid</string>
<string name="subtasks_help_1">Druk en sleep om een taak te verplaatsen</string>
<string name="subtasks_help_2">Sleep verticaal om te herschikken</string>

@ -620,37 +620,6 @@
<string name="premium_description_2">Synchronizuj pliki z astrid.com</string>
<string name="premium_description_3">Dodaj załączniki przez email</string>
<string name="premium_description_4">Zapisz notatki głosowe jako pliki audio</string>
<string name="producteev_FEx_dashboard">Obszary robocze</string>
<string name="producteev_FEx_responsible_byme">Przydzielone przeze mnie</string>
<string name="producteev_FEx_responsible_byothers">Przydzielone przez innych też</string>
<string name="producteev_FEx_responsible_title">Przypisany do \'%s\'</string>
<string name="producteev_PDE_task_from">od %s</string>
<string name="producteev_TEA_notes">Dodaj komentarz</string>
<string name="producteev_default_dashboard">Domyślny obszar roboczy</string>
<string name="producteev_no_dashboard">(Nie synchronizuj)</string>
<string name="producteev_create_dashboard">Dodaj nowy obszar roboczy...</string>
<string name="producteev_create_dashboard_name">Nazwa nowego obszaru roboczego</string>
<string name="producteev_PPr_defaultdash_title">Domyślny obszar roboczy</string>
<string name="producteev_PPr_defaultdash_summary">Nowe zadania będą dodawane do %s</string>
<string name="producteev_PPr_defaultdash_summary_none">Nowe zadania nie będą domyślnie synchronizowane</string>
<string name="producteev_PLA_title">Zaloguj do Producteev</string>
<string name="producteev_PLA_body">Zaloguj się używając aktualnego konta Producteev lub utwórz nowe!</string>
<string name="producteev_PLA_terms">Zasady &amp; Warunki</string>
<string name="producteev_PLA_signIn">Zaloguj się</string>
<string name="producteev_PLA_createNew">Utwórz nowego użytkownika</string>
<string name="producteev_PLA_email">Adres e-mail</string>
<string name="producteev_PLA_password">Hasło</string>
<string name="producteev_PLA_timezone">Strefa czasowa</string>
<string name="producteev_PLA_firstName">Imię</string>
<string name="producteev_PLA_lastName">Nazwisko</string>
<string name="producteev_PLA_errorEmpty">Błąd: wypełnij wszystkie pola!</string>
<string name="producteev_PLA_errorAuth">Błąd: e-mail lub hasło niepoprawne!</string>
<string name="producteev_TEA_task_assign_label">Przypisz to zadanie do tej osoby:</string>
<string name="producteev_TEA_dashboard_assign_label">Przypisz to zadanie do tego obszaru roboczego:</string>
<string name="CFC_producteev_in_workspace_text">W grupie roboczej: ?</string>
<string name="CFC_producteev_in_workspace_name">W grupie roboczej...</string>
<string name="CFC_producteev_assigned_to_text">Przypisany do: ?</string>
<string name="CFC_producteev_assigned_to_name">Przypisany do...</string>
<string name="TEA_reminders_group_label">Przypomnienia</string>
<string name="TEA_reminder_label">Przypomnij mi:</string>
<string name="TEA_reminder_due">Jeśli zadanie jest wynikiem</string>
@ -948,21 +917,6 @@
<item>Jestem z Ciebie taka dumna!</item>
<item>Uwielbiam, gdy jesteś produktywny/a!</item>
</string-array>
<string name="rmilk_EOE_button">Zapamiętaj ustawienia aplikacji \"Remember the Milk\"</string>
<string name="rmilk_TLA_repeat">Powtarzanie zadań RTM</string>
<string name="rmilk_TLA_sync">Potrzebna synchronizacja z RTM</string>
<string name="rmilk_FEx_header">Pamiętaj o mleku</string>
<string name="rmilk_FEx_list">Listy</string>
<string name="rmilk_FEx_list_title">Lista RTM \'%s\'</string>
<string name="rmilk_MEA_title">Pamiętaj o mleku</string>
<string name="rmilk_MEA_list_label">Lista RTM:</string>
<string name="rmilk_MEA_repeat_label">Status powtarzania RTM:</string>
<string name="rmilk_MEA_repeat_hint">np. co tydzień, po 14 dniach</string>
<string name="rmilk_MPr_header">Pamiętaj o mleku</string>
<string name="rmilk_MLA_label">Zaloguj i autoryzuj Astrid:</string>
<string name="rmilk_MLA_error">Wybacz, wystąpił błąd podczas weryfikacji Twojego loginu. Spróbuj ponownie. \n\n Treść błędu: %s</string>
<string name="rmilk_notification_title">Astrid: Remeber the Milk</string>
<string name="rmilk_ioerror">Błąd połączenia! Sprawdź swoje połączenie z Internetem lub odwiedź serwer RTM (status.rememberthemilk.com) w celu możliwego rozwiązania problemu.</string>
<string name="TEA_tags_label">Listy</string>
<string name="TEA_tags_none">Brak</string>
<string name="TEA_tag_hint">Nowa lista</string>

@ -690,41 +690,6 @@
<string name="premium_success">Obrigado por se inscrever no Astrid Premium!</string>
<string name="premium_success_with_server_error">Obrigado por assinar o Astrid Premium! Os novos recursos serão desbloqueados para você em astrid.com em breve e devem estar disponíveis em seu telefone imediatamente.</string>
<string name="premium_verification_error">A assinatura premium para esta conta Google Play está conectada com um usuário diferente. Para conectar e-mails adicionais em sua conta, visite http://astrid.com/users/profile</string>
<string name="producteev_FEx_header">Producteev</string>
<string name="producteev_FEx_dashboard">Espaços de trabalho</string>
<string name="producteev_FEx_responsible_byme">Atribuído por mim com</string>
<string name="producteev_FEx_responsible_byothers">Atribuído por outras pessoas para</string>
<string name="producteev_FEx_responsible_title">Atribuído a \'%s\'</string>
<string name="producteev_PDE_task_from">de %s</string>
<string name="producteev_TEA_notes">Inserir comentário</string>
<string name="producteev_PPr_header">Producteev</string>
<string name="producteev_default_dashboard">Área de trabalho padrão</string>
<string name="producteev_no_dashboard">(Não Sincronizar)</string>
<string name="producteev_create_dashboard">Inserir novo espaço de trabalho</string>
<string name="producteev_create_dashboard_name">Nome para novo espaço de trabalho</string>
<string name="producteev_PPr_defaultdash_title">Área de trabalho padrão</string>
<string name="producteev_PPr_defaultdash_summary">Novas tarefas serão inseridas em: %s</string>
<string name="producteev_PPr_defaultdash_summary_none">Novas tarefas não serão sincronizadas por padrão</string>
<string name="producteev_PLA_title">Logar-se ao Producteev</string>
<string name="producteev_PLA_body">Entrar com uma conta Producteev existente, ou criar uma nova conta!</string>
<string name="producteev_PLA_terms">Termos &amp; Condições</string>
<string name="producteev_PLA_signIn">Entrar</string>
<string name="producteev_PLA_createNew">Criar Novo Usuário</string>
<string name="producteev_PLA_email">E-mail</string>
<string name="producteev_PLA_password">Senha</string>
<string name="producteev_PLA_timezone">Fuso horário</string>
<string name="producteev_PLA_firstName">Primeiro Nome</string>
<string name="producteev_PLA_lastName">Sobrenome</string>
<string name="producteev_PLA_errorEmpty">Erro: preencha todos os campos!</string>
<string name="producteev_PLA_errorAuth">Erro: E-mail ou senha incorreta!</string>
<string name="producteev_notification_title">Producteev</string>
<string name="producteev_TEA_control_set_display">Atribuição Producteev</string>
<string name="producteev_TEA_task_assign_label">Atribuir essa tarefa a esta pessoa:</string>
<string name="producteev_TEA_dashboard_assign_label">Atribuir essa tarefa a esta área de trabalho:</string>
<string name="CFC_producteev_in_workspace_text">Na área de trabalho: ?</string>
<string name="CFC_producteev_in_workspace_name">Na área de trabalho...</string>
<string name="CFC_producteev_assigned_to_text">Atribuído a:?</string>
<string name="CFC_producteev_assigned_to_name">Atribuído a ...</string>
<string name="TEA_reminders_group_label">Lembretes</string>
<string name="TEA_reminder_label">Lembre me:</string>
<string name="TEA_reminder_due">no dia que a tarefa vencer</string>
@ -1031,21 +996,6 @@
<item>Estou tão orgulhoso de você!</item>
<item>Eu adoro quando você é produtivo(a)!</item>
</string-array>
<string name="rmilk_EOE_button">Configurações do Remember the Milk</string>
<string name="rmilk_TLA_repeat">Tarefa recorrente do RTM</string>
<string name="rmilk_TLA_sync">Precisa ser sincronizado com RTM</string>
<string name="rmilk_FEx_header">Remember the Milk</string>
<string name="rmilk_FEx_list">Listas</string>
<string name="rmilk_FEx_list_title">Lista RTM \'%s\'</string>
<string name="rmilk_MEA_title">Remember the Milk</string>
<string name="rmilk_MEA_list_label">Lista RTM:</string>
<string name="rmilk_MEA_repeat_label">Status de recorrência do RTM:</string>
<string name="rmilk_MEA_repeat_hint">ex. semanalmente, depois de 14 dias</string>
<string name="rmilk_MPr_header">Remember the Milk</string>
<string name="rmilk_MLA_label">Por favor, conecte-se e autorize o Astrid:</string>
<string name="rmilk_MLA_error">Desculpe, houve um erro identificando seu login. Por favor tente novamente. \n\n Mensagem de Erro: %s</string>
<string name="rmilk_notification_title">Astrid: Remember the Milk</string>
<string name="rmilk_ioerror">Erro de conexão! Verifique sua conexão com a Internet, ou talvez os servidores do RTM (status.rememberthemilk.com), para possíveis soluções.</string>
<string name="subtasks_help_title">Ordenar e Recuar no Astrid</string>
<string name="subtasks_help_1">Toque e segure para mover a tarefa</string>
<string name="subtasks_help_2">Arraste verticalmente para reordenar</string>

@ -490,7 +490,6 @@
<item>I\'m so proud of you!</item>
<item>I love when you\'re productive!</item>
</string-array>
<string name="rmilk_FEx_list">Listas</string>
<string name="TEA_tags_label">Listas</string>
<string name="tag_FEx_header">Listas</string>
<string name="tag_settings_title">Settings:</string>

@ -705,41 +705,6 @@
<string name="premium_success">Спасибо за то, что подписались на Astrid Premium!</string>
<string name="premium_success_with_server_error">Спасибо за то, что оформили премиум подписку на Astrid! Новые возможности на astrid.com разблокируются в ближайшее время, на Вашем телефоне они должны быть доступны уже сейчас.</string>
<string name="premium_verification_error">Премиум подписка для этой учетной записи Google Play оформлена на другого пользователя. Привязать дополнительный email к своей учетной записи можно здесь: astrid.com/users/profile</string>
<string name="producteev_FEx_header">Producteev</string>
<string name="producteev_FEx_dashboard">Рабочие области</string>
<string name="producteev_FEx_responsible_byme">Назначено мною</string>
<string name="producteev_FEx_responsible_byothers">Назначено другими</string>
<string name="producteev_FEx_responsible_title">Ответственный \'%s\'</string>
<string name="producteev_PDE_task_from">от %s</string>
<string name="producteev_TEA_notes">Добавить комментарий</string>
<string name="producteev_PPr_header">Producteev</string>
<string name="producteev_default_dashboard">Рабочая среда по умолчанию</string>
<string name="producteev_no_dashboard">(Не синхронизировано)</string>
<string name="producteev_create_dashboard">Добавить новую рабочую среду</string>
<string name="producteev_create_dashboard_name">Имя новой рабочей среды</string>
<string name="producteev_PPr_defaultdash_title">Рабочая среда по умолчанию</string>
<string name="producteev_PPr_defaultdash_summary">Добавлять новые задачи в %s</string>
<string name="producteev_PPr_defaultdash_summary_none">Новые задачи не будут синхонизированы по умолчанию</string>
<string name="producteev_PLA_title">Войти в Producteev</string>
<string name="producteev_PLA_body">Войдите в Producteev, используя существующую учётную запись, или создайте новую учётную запись!</string>
<string name="producteev_PLA_terms">Условия использования</string>
<string name="producteev_PLA_signIn">Войти</string>
<string name="producteev_PLA_createNew">Создать нового пользователя</string>
<string name="producteev_PLA_email">Электронная почта</string>
<string name="producteev_PLA_password">Пароль</string>
<string name="producteev_PLA_timezone">Часовой пояс</string>
<string name="producteev_PLA_firstName">Имя</string>
<string name="producteev_PLA_lastName">Фамилия</string>
<string name="producteev_PLA_errorEmpty">Ошибка: заполните все поля!</string>
<string name="producteev_PLA_errorAuth">Ошибка: неправильная почта или пароль!</string>
<string name="producteev_notification_title">Producteev</string>
<string name="producteev_TEA_control_set_display">Назначение Producteev</string>
<string name="producteev_TEA_task_assign_label">Назначить эту задачу этому человеку:</string>
<string name="producteev_TEA_dashboard_assign_label">Назначить эту задачу для этой рабочей области:</string>
<string name="CFC_producteev_in_workspace_text">в среде: ?</string>
<string name="CFC_producteev_in_workspace_name">в среде...</string>
<string name="CFC_producteev_assigned_to_text">Присвоено: ?</string>
<string name="CFC_producteev_assigned_to_name">Присвоено ...</string>
<string name="TEA_reminders_group_label">Напоминания</string>
<string name="TEA_reminder_label">Напомнить мне:</string>
<string name="TEA_reminder_due">… при наступлении срока задания</string>
@ -1046,21 +1011,6 @@
<item>Я так горжусь Вами!</item>
<item>Я люблю когда Вы плодотворны!</item>
</string-array>
<string name="rmilk_EOE_button">Запомнить настройки Milk</string>
<string name="rmilk_TLA_repeat">Повторяющаяся задача RTM</string>
<string name="rmilk_TLA_sync">Необходима синхронизация с RTM</string>
<string name="rmilk_FEx_header">Помни про молоко</string>
<string name="rmilk_FEx_list">Списки</string>
<string name="rmilk_FEx_list_title">Список RTM \'%s\'</string>
<string name="rmilk_MEA_title">Помни про молоко</string>
<string name="rmilk_MEA_list_label">Список RTM:</string>
<string name="rmilk_MEA_repeat_label">Состояние повтора RTM</string>
<string name="rmilk_MEA_repeat_hint">например, каждую неделю, спустя 14 дней</string>
<string name="rmilk_MPr_header">Помни про молоко</string>
<string name="rmilk_MLA_label">Пожалуйста, войдите и авторизуйте Astrid:</string>
<string name="rmilk_MLA_error">Извините, при авторизации возникла ошибка. Пожалуйста, попробуйте ещё раз. \n\n Сообщение об ошибке: %s</string>
<string name="rmilk_notification_title">Astrid: Помни про молоко</string>
<string name="rmilk_ioerror">Ошибка соединения! Проверьте соединение с интернетом и, возможно, сервером RTM (status.rememberthemilk.com) для возможного решения.</string>
<string name="subtasks_help_title">Сортировка и отступ в Astrid</string>
<string name="subtasks_help_1">Нажмите и удерживайте, чтобы переместить задачу</string>
<string name="subtasks_help_2">Перетащите вертикально, чтобы перестроить</string>

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

Loading…
Cancel
Save