From 2ad175c876f4280ec7e727d0c5ad2d4dba7a3117 Mon Sep 17 00:00:00 2001 From: Alex Baker Date: Sun, 17 Nov 2013 02:47:02 -0600 Subject: [PATCH] Add scrollable widget Closes #21 --- .../service/DependencyInjectionService.java | 3 + astrid/src/main/AndroidManifest.xml | 27 +- .../astrid/activity/EditPreferences.java | 4 +- .../astrid/activity/TaskListFragment.java | 6 +- .../java/com/todoroo/astrid/dao/Database.java | 5 +- .../astrid/service/StartupService.java | 14 +- .../service/SyncResultCallbackWrapper.java | 9 +- .../todoroo/astrid/service/ThemeService.java | 10 +- .../todoroo/astrid/widget/TasksWidget.java | 368 +----------------- .../astrid/widget/WidgetConfigActivity.java | 10 +- .../astrid/widget/WidgetUpdateService.java | 218 +++++++++++ .../tasks/widget/ScrollableTasksWidget.java | 46 +++ .../tasks/widget/ScrollableViewsFactory.java | 131 +++++++ .../ScrollableWidgetConfigActivity.java | 157 ++++++++ .../widget/ScrollableWidgetUpdateService.java | 30 ++ .../java/org/tasks/widget/WidgetHelper.java | 259 ++++++++++++ .../res/layout/scrollable_widget_dark.xml | 72 ++++ .../res/layout/scrollable_widget_light.xml | 72 ++++ .../main/res/layout/widget_initialized.xml | 12 +- .../res/layout/widget_initialized_dark.xml | 14 +- astrid/src/main/res/layout/widget_loading.xml | 14 +- astrid/src/main/res/values-v14/attrs.xml | 5 + astrid/src/main/res/values/attrs.xml | 5 +- .../xml/scrollable_widget_provider_info.xml | 16 + .../src/main/res/xml/widget_provider_info.xml | 2 +- 25 files changed, 1086 insertions(+), 423 deletions(-) create mode 100644 astrid/src/main/java/com/todoroo/astrid/widget/WidgetUpdateService.java create mode 100644 astrid/src/main/java/org/tasks/widget/ScrollableTasksWidget.java create mode 100644 astrid/src/main/java/org/tasks/widget/ScrollableViewsFactory.java create mode 100644 astrid/src/main/java/org/tasks/widget/ScrollableWidgetConfigActivity.java create mode 100644 astrid/src/main/java/org/tasks/widget/ScrollableWidgetUpdateService.java create mode 100644 astrid/src/main/java/org/tasks/widget/WidgetHelper.java create mode 100644 astrid/src/main/res/layout/scrollable_widget_dark.xml create mode 100644 astrid/src/main/res/layout/scrollable_widget_light.xml create mode 100644 astrid/src/main/res/values-v14/attrs.xml create mode 100644 astrid/src/main/res/xml/scrollable_widget_provider_info.xml diff --git a/api/src/main/java/com/todoroo/andlib/service/DependencyInjectionService.java b/api/src/main/java/com/todoroo/andlib/service/DependencyInjectionService.java index 3909ff824..5ee35fedd 100644 --- a/api/src/main/java/com/todoroo/andlib/service/DependencyInjectionService.java +++ b/api/src/main/java/com/todoroo/andlib/service/DependencyInjectionService.java @@ -79,6 +79,9 @@ public class DependencyInjectionService { if(packageName.startsWith("org.weloveastrid")) { return true; } + if(packageName.startsWith("org.tasks")) { + return true; + } return false; } diff --git a/astrid/src/main/AndroidManifest.xml b/astrid/src/main/AndroidManifest.xml index 5bafd640d..58f42f3cf 100644 --- a/astrid/src/main/AndroidManifest.xml +++ b/astrid/src/main/AndroidManifest.xml @@ -168,7 +168,13 @@ android:theme="@android:style/Theme" /> - + + + + + + + @@ -196,6 +202,7 @@ @@ -205,6 +212,18 @@ android:resource="@xml/widget_provider_info" /> + + + + + + + @@ -225,7 +244,11 @@ - + + + diff --git a/astrid/src/main/java/com/todoroo/astrid/activity/EditPreferences.java b/astrid/src/main/java/com/todoroo/astrid/activity/EditPreferences.java index 595d28487..623bcb6c0 100644 --- a/astrid/src/main/java/com/todoroo/astrid/activity/EditPreferences.java +++ b/astrid/src/main/java/com/todoroo/astrid/activity/EditPreferences.java @@ -44,9 +44,9 @@ import com.todoroo.astrid.utility.Flags; import com.todoroo.astrid.voice.VoiceInputAssistant; import com.todoroo.astrid.voice.VoiceOutputService; import com.todoroo.astrid.voice.VoiceRecognizer; -import com.todoroo.astrid.widget.TasksWidget; import org.tasks.R; +import org.tasks.widget.WidgetHelper; import java.util.ArrayList; import java.util.HashMap; @@ -406,7 +406,7 @@ public class EditPreferences extends TodorooPreferenceActivity { findPreference(getString(R.string.p_use_dark_theme_widget)).setOnPreferenceChangeListener(new OnPreferenceChangeListener() { @Override public boolean onPreferenceChange(Preference preference, Object newValue) { - TasksWidget.updateWidgets(EditPreferences.this); + WidgetHelper.triggerUpdate(EditPreferences.this); updatePreferences(preference, newValue); return true; } diff --git a/astrid/src/main/java/com/todoroo/astrid/activity/TaskListFragment.java b/astrid/src/main/java/com/todoroo/astrid/activity/TaskListFragment.java index 145622392..87e13a8e2 100644 --- a/astrid/src/main/java/com/todoroo/astrid/activity/TaskListFragment.java +++ b/astrid/src/main/java/com/todoroo/astrid/activity/TaskListFragment.java @@ -89,9 +89,9 @@ import com.todoroo.astrid.timers.TimerPlugin; import com.todoroo.astrid.ui.QuickAddBar; import com.todoroo.astrid.utility.AstridPreferences; import com.todoroo.astrid.utility.Flags; -import com.todoroo.astrid.widget.TasksWidget; import org.tasks.R; +import org.tasks.widget.WidgetHelper; import java.util.List; import java.util.Timer; @@ -719,7 +719,7 @@ public class TaskListFragment extends ListFragment implements OnSortSelectedList if (resultCode == EditPreferences.RESULT_CODE_THEME_CHANGED || resultCode == EditPreferences.RESULT_CODE_PERFORMANCE_PREF_CHANGED) { getActivity().finish(); getActivity().startActivity(getActivity().getIntent()); - TasksWidget.updateWidgets(getActivity()); + WidgetHelper.updateWidgets(getActivity()); return; } else if (resultCode == SyncProviderPreferences.RESULT_CODE_SYNCHRONIZE) { Preferences.setLong(SyncActionHelper.PREF_LAST_AUTO_SYNC, 0); // Forces autosync to occur after login @@ -1166,7 +1166,7 @@ public class TaskListFragment extends ListFragment implements OnSortSelectedList editor.putInt(SortHelper.PREF_SORT_FLAGS, flags); editor.putInt(SortHelper.PREF_SORT_SORT, sort); editor.commit(); - TasksWidget.updateWidgets(ContextManager.getContext()); + WidgetHelper.updateWidgets(ContextManager.getContext()); } } } diff --git a/astrid/src/main/java/com/todoroo/astrid/dao/Database.java b/astrid/src/main/java/com/todoroo/astrid/dao/Database.java index 241883d6f..97f9548bf 100644 --- a/astrid/src/main/java/com/todoroo/astrid/dao/Database.java +++ b/astrid/src/main/java/com/todoroo/astrid/dao/Database.java @@ -25,7 +25,8 @@ import com.todoroo.astrid.data.Update; import com.todoroo.astrid.data.UserActivity; import com.todoroo.astrid.provider.Astrid2TaskProvider; import com.todoroo.astrid.provider.Astrid3ContentProvider; -import com.todoroo.astrid.widget.TasksWidget; + +import org.tasks.widget.WidgetHelper; /** * Database wrapper @@ -73,7 +74,7 @@ public class Database extends AbstractDatabase { public void onDatabaseUpdated() { Astrid2TaskProvider.notifyDatabaseModification(); Astrid3ContentProvider.notifyDatabaseModification(); - TasksWidget.updateWidgets(ContextManager.getContext()); + WidgetHelper.updateWidgets(ContextManager.getContext()); } }); } diff --git a/astrid/src/main/java/com/todoroo/astrid/service/StartupService.java b/astrid/src/main/java/com/todoroo/astrid/service/StartupService.java index bd8d591fb..c45aa6318 100644 --- a/astrid/src/main/java/com/todoroo/astrid/service/StartupService.java +++ b/astrid/src/main/java/com/todoroo/astrid/service/StartupService.java @@ -7,14 +7,11 @@ package com.todoroo.astrid.service; import android.Manifest; import android.app.Activity; -import android.app.AlarmManager; import android.app.AlertDialog; -import android.app.PendingIntent; import android.content.ContentValues; import android.content.Context; import android.content.DialogInterface; import android.content.DialogInterface.OnClickListener; -import android.content.Intent; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.database.sqlite.SQLiteException; @@ -53,13 +50,14 @@ import com.todoroo.astrid.reminders.ReminderStartupReceiver; import com.todoroo.astrid.tags.TaskToTagMetadata; import com.todoroo.astrid.utility.AstridPreferences; import com.todoroo.astrid.utility.Constants; -import com.todoroo.astrid.widget.TasksWidget.WidgetUpdateService; import org.tasks.R; import java.io.File; import java.util.List; +import static org.tasks.widget.WidgetHelper.startWidgetService; + /** * Service which handles jobs that need to be run when Astrid starts up. * @@ -178,13 +176,7 @@ public class StartupService { new Thread(new Runnable() { @Override public void run() { - // start widget updating alarm - AlarmManager am = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE); - Intent intent = new Intent(context, WidgetUpdateService.class); - PendingIntent pendingIntent = PendingIntent.getService(context, - 0, intent, PendingIntent.FLAG_CANCEL_CURRENT); - am.setInexactRepeating(AlarmManager.RTC, 0, - Constants.WIDGET_UPDATE_INTERVAL, pendingIntent); + startWidgetService(context); ReengagementService.scheduleReengagementAlarm(context); taskService.cleanup(); diff --git a/astrid/src/main/java/com/todoroo/astrid/service/SyncResultCallbackWrapper.java b/astrid/src/main/java/com/todoroo/astrid/service/SyncResultCallbackWrapper.java index f783a1a1d..cc2311341 100644 --- a/astrid/src/main/java/com/todoroo/astrid/service/SyncResultCallbackWrapper.java +++ b/astrid/src/main/java/com/todoroo/astrid/service/SyncResultCallbackWrapper.java @@ -3,7 +3,8 @@ package com.todoroo.astrid.service; import com.todoroo.andlib.service.ContextManager; import com.todoroo.andlib.utility.DateUtilities; import com.todoroo.astrid.sync.SyncResultCallback; -import com.todoroo.astrid.widget.TasksWidget; + +import org.tasks.widget.WidgetHelper; public class SyncResultCallbackWrapper implements SyncResultCallback { private final SyncResultCallback wrapped; @@ -36,14 +37,14 @@ public class SyncResultCallbackWrapper implements SyncResultCallback { @Override public void started() { super.started(); - TasksWidget.suppressUpdateFlag = DateUtilities.now(); + WidgetHelper.suppressUpdateFlag = DateUtilities.now(); } @Override public void finished() { super.finished(); - TasksWidget.suppressUpdateFlag = 0L; - TasksWidget.updateWidgets(ContextManager.getContext()); + WidgetHelper.suppressUpdateFlag = 0L; + WidgetHelper.updateWidgets(ContextManager.getContext()); } } diff --git a/astrid/src/main/java/com/todoroo/astrid/service/ThemeService.java b/astrid/src/main/java/com/todoroo/astrid/service/ThemeService.java index 883f43081..a522c1ecf 100644 --- a/astrid/src/main/java/com/todoroo/astrid/service/ThemeService.java +++ b/astrid/src/main/java/com/todoroo/astrid/service/ThemeService.java @@ -7,7 +7,6 @@ package com.todoroo.astrid.service; import android.app.Activity; import android.graphics.PixelFormat; -import android.text.TextUtils; import android.util.Log; import android.view.WindowManager; @@ -43,13 +42,8 @@ public class ThemeService { return getStyleForSetting(preference); } - public static int getWidgetTheme() { - String preference = Preferences.getBoolean(R.string.p_use_dark_theme_widget, false) ? THEME_BLACK : THEME_WHITE; - if (TextUtils.isEmpty(preference)) { - return getTheme(); - } else { - return getStyleForSetting(preference); - } + public static boolean isDarkWidgetTheme() { + return Preferences.getBoolean(R.string.p_use_dark_theme_widget, false); } private static int getStyleForSetting(String setting) { diff --git a/astrid/src/main/java/com/todoroo/astrid/widget/TasksWidget.java b/astrid/src/main/java/com/todoroo/astrid/widget/TasksWidget.java index 4e7dc5fc6..701aadf5e 100644 --- a/astrid/src/main/java/com/todoroo/astrid/widget/TasksWidget.java +++ b/astrid/src/main/java/com/todoroo/astrid/widget/TasksWidget.java @@ -5,68 +5,18 @@ */ package com.todoroo.astrid.widget; -import android.app.PendingIntent; -import android.app.Service; import android.appwidget.AppWidgetManager; import android.appwidget.AppWidgetProvider; -import android.content.ComponentName; -import android.content.ContentValues; import android.content.Context; import android.content.Intent; -import android.content.SharedPreferences; -import android.content.res.Resources; -import android.os.Bundle; -import android.os.IBinder; import android.util.Log; -import android.view.View; -import android.widget.RemoteViews; -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.utility.AndroidUtilities; -import com.todoroo.andlib.utility.DateUtilities; -import com.todoroo.andlib.utility.Preferences; -import com.todoroo.astrid.actfm.TagViewFragment; -import com.todoroo.astrid.activity.TaskEditActivity; -import com.todoroo.astrid.activity.TaskEditFragment; -import com.todoroo.astrid.activity.TaskListActivity; -import com.todoroo.astrid.activity.TaskListFragment; -import com.todoroo.astrid.api.Filter; -import com.todoroo.astrid.api.FilterWithCustomIntent; -import com.todoroo.astrid.api.PermaSql; -import com.todoroo.astrid.core.CoreFilterExposer; -import com.todoroo.astrid.core.SortHelper; -import com.todoroo.astrid.dao.Database; -import com.todoroo.astrid.data.TagData; -import com.todoroo.astrid.data.Task; -import com.todoroo.astrid.service.AstridDependencyInjector; -import com.todoroo.astrid.service.TagDataService; -import com.todoroo.astrid.service.TaskService; -import com.todoroo.astrid.service.ThemeService; -import com.todoroo.astrid.subtasks.SubtasksHelper; -import com.todoroo.astrid.tags.TagFilterExposer; -import com.todoroo.astrid.utility.AstridPreferences; -import com.todoroo.astrid.utility.Constants; - -import org.tasks.R; public class TasksWidget extends AppWidgetProvider { - private static final int NUM_VISIBLE_TASKS = 25; - - public static long suppressUpdateFlag = 0; // Timestamp--don't update widgets if this flag is non-zero and now() is within 5 minutes - private static final long SUPPRESS_TIME = DateUtilities.ONE_MINUTE * 5; - - static { - AstridDependencyInjector.initialize(); - } - @Override - public void onUpdate(Context context, AppWidgetManager appWidgetManager, - int[] appWidgetIds) { - + public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { try { ContextManager.setContext(context); super.onUpdate(context, appWidgetManager, appWidgetIds); @@ -82,327 +32,15 @@ public class TasksWidget extends AppWidgetProvider { * Update all widgets */ public static void updateWidgets(Context context) { - if (suppressUpdateFlag > 0 && DateUtilities.now() - suppressUpdateFlag < SUPPRESS_TIME) { - return; - } - suppressUpdateFlag = 0; - context.startService(new Intent(context, - TasksWidget.WidgetUpdateService.class)); + context.startService(new Intent(context, WidgetUpdateService.class)); } /** * Update widget with the given id */ public static void updateWidget(Context context, int id) { - Intent intent = new Intent(ContextManager.getContext(), - TasksWidget.WidgetUpdateService.class); + Intent intent = new Intent(ContextManager.getContext(), WidgetUpdateService.class); intent.putExtra(WidgetUpdateService.EXTRA_WIDGET_ID, id); context.startService(intent); } - - public static class ConfigActivity extends WidgetConfigActivity { - @Override - public void updateWidget() { - TasksWidget.updateWidget(this, mAppWidgetId); - } - } - - public static class WidgetUpdateService extends Service { - - public static final String EXTRA_WIDGET_ID = "widget_id"; //$NON-NLS-1$ - - @Autowired - Database database; - - @Autowired - TaskService taskService; - - @Autowired - TagDataService tagDataService; - - @Override - public void onStart(final Intent intent, int startId) { - ContextManager.setContext(this); - new Thread(new Runnable() { - @Override - public void run() { - startServiceInBackgroundThread(intent); - } - }).start(); - } - - public void startServiceInBackgroundThread(Intent intent) { - ComponentName thisWidget = new ComponentName(this, - TasksWidget.class); - AppWidgetManager manager = AppWidgetManager.getInstance(this); - - int extrasId = AppWidgetManager.INVALID_APPWIDGET_ID; - if(intent != null) { - extrasId = intent.getIntExtra(EXTRA_WIDGET_ID, extrasId); - } - if(extrasId == AppWidgetManager.INVALID_APPWIDGET_ID) { - int[] ids; - try { - ids = manager.getAppWidgetIds(thisWidget); - for(int id : ids) { - RemoteViews updateViews = buildUpdate(this, id); - manager.updateAppWidget(id, updateViews); - } - } catch (RuntimeException e) { - // "System server dead" was sometimes thrown here by the OS. Abort if that happens - } - } else { - RemoteViews updateViews = buildUpdate(this, extrasId); - manager.updateAppWidget(extrasId, updateViews); - } - - stopSelf(); - } - - @Override - public IBinder onBind(Intent intent) { - return null; - } - - public RemoteViews buildUpdate(Context context, int widgetId) { - DependencyInjectionService.getInstance().inject(this); - - RemoteViews views = getThemedRemoteViews(context); - - int numberOfTasks = NUM_VISIBLE_TASKS; - - TodorooCursor cursor = null; - Filter filter = null; - try { - filter = getFilter(context, widgetId); - if (SubtasksHelper.isTagFilter(filter)) { - ((FilterWithCustomIntent) filter).customTaskList = new ComponentName(context, TagViewFragment.class); // In case legacy widget was created with subtasks fragment - } - views.setTextViewText(R.id.widget_title, filter.title); - views.removeAllViews(R.id.taskbody); - - SharedPreferences publicPrefs = AstridPreferences.getPublicPrefs(this); - int flags = publicPrefs.getInt(SortHelper.PREF_SORT_FLAGS, 0); - int sort = publicPrefs.getInt(SortHelper.PREF_SORT_SORT, 0); - String query = SortHelper.adjustQueryForFlagsAndSort( - filter.getSqlQuery(), flags, sort).replaceAll("LIMIT \\d+", "") + " LIMIT " + numberOfTasks; - - String tagName = Preferences.getStringValue(WidgetConfigActivity.PREF_TITLE + widgetId); - query = SubtasksHelper.applySubtasksToWidgetFilter(filter, query, tagName, numberOfTasks); - - database.openForReading(); - cursor = taskService.fetchFiltered(query, null, Task.ID, Task.TITLE, Task.DUE_DATE, Task.COMPLETION_DATE); - Task task = new Task(); - int i; - for (i = 0; i < cursor.getCount() && i < numberOfTasks; i++) { - cursor.moveToPosition(i); - task.readFromCursor(cursor); - - String textContent; - Resources r = context.getResources(); - int textColor = r - .getColor(isDarkTheme() ? R.color.widget_text_color_dark : R.color.widget_text_color_light); - - textContent = task.getValue(Task.TITLE); - - if(task.isCompleted()) { - textColor = r.getColor(R.color.task_list_done); - } else if(task.hasDueDate() && task.isOverdue()) { - textColor = r.getColor(R.color.task_list_overdue); - } - - RemoteViews row = new RemoteViews(Constants.PACKAGE, R.layout.widget_row); - - row.setTextViewText(R.id.text, textContent); - row.setTextColor(R.id.text, textColor); - - views.addView(R.id.taskbody, row); - - RemoteViews separator = new RemoteViews(Constants.PACKAGE, R.layout.widget_separator); - boolean isLastRow = (i == cursor.getCount() - 1) || (i == numberOfTasks - 1); - if (!isLastRow) { - views.addView(R.id.taskbody, separator); - } - } - for (; i < numberOfTasks; i++) { - RemoteViews row = new RemoteViews(Constants.PACKAGE, R.layout.widget_row); - row.setViewVisibility(R.id.text, View.INVISIBLE); - views.addView(R.id.taskbody, row); - } - - } catch (Exception e) { - // can happen if database is not ready - Log.e("WIDGET-UPDATE", "Error updating widget", e); - } finally { - if(cursor != null) { - cursor.close(); - } - } - - Intent listIntent = new Intent(context, TaskListActivity.class); - String customIntent = Preferences.getStringValue(WidgetConfigActivity.PREF_CUSTOM_INTENT - + widgetId); - if(customIntent != null) { - String serializedExtras = Preferences.getStringValue(WidgetConfigActivity.PREF_CUSTOM_EXTRAS - + widgetId); - Bundle extras = AndroidUtilities.bundleFromSerializedString(serializedExtras); - listIntent.putExtras(extras); - } - listIntent.putExtra(TaskListActivity.TOKEN_SOURCE, Constants.SOURCE_WIDGET); - listIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_MULTIPLE_TASK); - if(filter != null) { - listIntent.putExtra(TaskListFragment.TOKEN_FILTER, filter); - listIntent.setAction("L" + widgetId + filter.getSqlQuery()); - } else { - listIntent.setAction("L" + widgetId); - } - if (filter instanceof FilterWithCustomIntent) { - listIntent.putExtras(((FilterWithCustomIntent) filter).customExtras); - } - - PendingIntent pListIntent = PendingIntent.getActivity(context, widgetId, - listIntent, PendingIntent.FLAG_CANCEL_CURRENT); - if (pListIntent != null) { - views.setOnClickPendingIntent(R.id.taskbody, pListIntent); - } - - - Intent editIntent; - boolean tablet = AstridPreferences.useTabletLayout(context); - if (tablet) { - editIntent = new Intent(context, TaskListActivity.class); - editIntent.putExtra(TaskListActivity.OPEN_TASK, 0L); - } - else { - editIntent = new Intent(context, TaskEditActivity.class); - } - - editIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_MULTIPLE_TASK); - editIntent.putExtra(TaskEditFragment.OVERRIDE_FINISH_ANIM, false); - if(filter != null) { - editIntent.putExtra(TaskListFragment.TOKEN_FILTER, filter); - if (filter.valuesForNewTasks != null) { - String values = AndroidUtilities.contentValuesToSerializedString(filter.valuesForNewTasks); - values = PermaSql.replacePlaceholders(values); - editIntent.putExtra(TaskEditFragment.TOKEN_VALUES, values); - editIntent.setAction("E" + widgetId + values); - } - if (tablet) { - if (filter instanceof FilterWithCustomIntent) { - Bundle customExtras = ((FilterWithCustomIntent) filter).customExtras; - editIntent.putExtras(customExtras); - } - } - } else { - editIntent.setAction("E" + widgetId); - } - - PendingIntent pEditIntent = PendingIntent.getActivity(context, -widgetId, - editIntent, 0); - if (pEditIntent != null) { - views.setOnClickPendingIntent(R.id.widget_button, pEditIntent); - views.setOnClickPendingIntent(R.id.widget_title, pEditIntent); - } - - return views; - } - - private boolean isDarkTheme() { - int theme = ThemeService.getWidgetTheme(); - return theme == R.style.Tasks; - } - - /** - * The reason we use a bunch of different but almost identical layouts is that there is a bug with - * Android 2.1 (level 7) that doesn't allow setting backgrounds on remote views. I know it's lame, - * but I didn't see a better solution. Alternatively, we could disallow theming widgets on - * Android 2.1. - */ - private RemoteViews getThemedRemoteViews(Context context) { - String packageName = context.getPackageName(); - Resources r = context.getResources(); - int layout; - RemoteViews views; - - int titleColor; - int buttonDrawable; - - if (isDarkTheme()) { - layout = R.layout.widget_initialized_dark; - titleColor = r.getColor(R.color.widget_text_color_dark); - buttonDrawable = R.drawable.ic_action_new_light; - } else { - layout = R.layout.widget_initialized; - titleColor = r.getColor(R.color.widget_text_color_light); - buttonDrawable = R.drawable.ic_action_new; - } - - views = new RemoteViews(packageName, layout); - views.setTextColor(R.id.widget_title, titleColor); - views.setInt(R.id.widget_button, "setImageResource", buttonDrawable); - return views; - } - - private Filter getFilter(Context context, int widgetId) { - - // base our filter off the inbox filter, replace stuff if we have it - Filter filter = CoreFilterExposer.buildInboxFilter(getResources()); - String sql = Preferences.getStringValue(WidgetConfigActivity.PREF_SQL + widgetId); - if(sql != null) { - filter.setSqlQuery(sql); - } - String title = Preferences.getStringValue(WidgetConfigActivity.PREF_TITLE + widgetId); - if(title != null) { - filter.title = title; - } - String contentValues = Preferences.getStringValue(WidgetConfigActivity.PREF_VALUES + widgetId); - if(contentValues != null) { - filter.valuesForNewTasks = AndroidUtilities.contentValuesFromSerializedString(contentValues); - } - - String customComponent = Preferences.getStringValue(WidgetConfigActivity.PREF_CUSTOM_INTENT - + widgetId); - if (customComponent != null) { - ComponentName component = ComponentName.unflattenFromString(customComponent); - filter = new FilterWithCustomIntent(filter.title, filter.title, filter.getSqlQuery(), filter.valuesForNewTasks); - ((FilterWithCustomIntent) filter).customTaskList = component; - String serializedExtras = Preferences.getStringValue(WidgetConfigActivity.PREF_CUSTOM_EXTRAS - + widgetId); - ((FilterWithCustomIntent) filter).customExtras = AndroidUtilities.bundleFromSerializedString(serializedExtras); - } - - // Validate tagData - long id = Preferences.getLong(WidgetConfigActivity.PREF_TAG_ID + widgetId, 0); - TagData tagData; - if (id > 0) { - tagData = tagDataService.fetchById(id, TagData.ID, TagData.NAME, TagData.TASK_COUNT, TagData.UUID, TagData.PICTURE, TagData.USER_ID, TagData.MEMBER_COUNT); - if (tagData != null && !tagData.getValue(TagData.NAME).equals(filter.title)) { // Tag has been renamed; rebuild filter - filter = TagFilterExposer.filterFromTagData(context, tagData); - Preferences.setString(WidgetConfigActivity.PREF_SQL + widgetId, filter.getSqlQuery()); - Preferences.setString(WidgetConfigActivity.PREF_TITLE + widgetId, filter.title); - ContentValues newTaskValues = filter.valuesForNewTasks; - String contentValuesString = null; - if(newTaskValues != null) { - contentValuesString = AndroidUtilities.contentValuesToSerializedString(newTaskValues); - } - Preferences.setString(WidgetConfigActivity.PREF_VALUES + widgetId, contentValuesString); - if (filter instanceof FilterWithCustomIntent) { - String flattenedExtras = AndroidUtilities.bundleToSerializedString(((FilterWithCustomIntent) filter).customExtras); - if (flattenedExtras != null) { - Preferences.setString(WidgetConfigActivity.PREF_CUSTOM_EXTRAS + widgetId, - flattenedExtras); - } - } - } - } else { - tagData = tagDataService.getTagByName(filter.title, TagData.ID); - if (tagData != null) { - Preferences.setLong(WidgetConfigActivity.PREF_TAG_ID + widgetId, tagData.getId()); - } - } - - return filter; - } - - } } diff --git a/astrid/src/main/java/com/todoroo/astrid/widget/WidgetConfigActivity.java b/astrid/src/main/java/com/todoroo/astrid/widget/WidgetConfigActivity.java index a0d770742..0fd91be39 100644 --- a/astrid/src/main/java/com/todoroo/astrid/widget/WidgetConfigActivity.java +++ b/astrid/src/main/java/com/todoroo/astrid/widget/WidgetConfigActivity.java @@ -25,13 +25,13 @@ import com.todoroo.astrid.service.ThemeService; import org.tasks.R; -abstract public class WidgetConfigActivity extends ListActivity { +public class WidgetConfigActivity extends ListActivity { static final String PREF_TITLE = "widget-title-"; static final String PREF_SQL = "widget-sql-"; static final String PREF_VALUES = "widget-values-"; - static final String PREF_CUSTOM_INTENT = "widget-intent-"; - static final String PREF_CUSTOM_EXTRAS = "widget-extras-"; + public static final String PREF_CUSTOM_INTENT = "widget-intent-"; + public static final String PREF_CUSTOM_EXTRAS = "widget-extras-"; static final String PREF_TAG_ID = "widget-tag-id-"; int mAppWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID; @@ -42,7 +42,9 @@ abstract public class WidgetConfigActivity extends ListActivity { super(); } - abstract public void updateWidget(); + public void updateWidget() { + TasksWidget.updateWidget(this, mAppWidgetId); + } @Override public void onCreate(Bundle icicle) { diff --git a/astrid/src/main/java/com/todoroo/astrid/widget/WidgetUpdateService.java b/astrid/src/main/java/com/todoroo/astrid/widget/WidgetUpdateService.java new file mode 100644 index 000000000..5ce87c51e --- /dev/null +++ b/astrid/src/main/java/com/todoroo/astrid/widget/WidgetUpdateService.java @@ -0,0 +1,218 @@ +package com.todoroo.astrid.widget; + +import android.app.PendingIntent; +import android.app.Service; +import android.appwidget.AppWidgetManager; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.res.Resources; +import android.os.IBinder; +import android.util.Log; +import android.view.View; +import android.widget.RemoteViews; + +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.utility.Preferences; +import com.todoroo.astrid.actfm.TagViewFragment; +import com.todoroo.astrid.api.Filter; +import com.todoroo.astrid.api.FilterWithCustomIntent; +import com.todoroo.astrid.core.SortHelper; +import com.todoroo.astrid.dao.Database; +import com.todoroo.astrid.data.Task; +import com.todoroo.astrid.service.AstridDependencyInjector; +import com.todoroo.astrid.service.TaskService; +import com.todoroo.astrid.service.ThemeService; +import com.todoroo.astrid.subtasks.SubtasksHelper; +import com.todoroo.astrid.utility.AstridPreferences; +import com.todoroo.astrid.utility.Constants; + +import org.tasks.R; +import org.tasks.widget.WidgetHelper; + +public class WidgetUpdateService extends Service { + + static { + AstridDependencyInjector.initialize(); + } + + private static final int NUM_VISIBLE_TASKS = 25; + + public static final String EXTRA_WIDGET_ID = "widget_id"; //$NON-NLS-1$ + + @Autowired + Database database; + + @Autowired + TaskService taskService; + + private final WidgetHelper widgetHelper = new WidgetHelper(); + + @Override + public void onStart(final Intent intent, int startId) { + ContextManager.setContext(this); + new Thread(new Runnable() { + @Override + public void run() { + startServiceInBackgroundThread(intent); + } + }).start(); + } + + public void startServiceInBackgroundThread(Intent intent) { + ComponentName thisWidget = new ComponentName(this, + TasksWidget.class); + AppWidgetManager manager = AppWidgetManager.getInstance(this); + + int extrasId = AppWidgetManager.INVALID_APPWIDGET_ID; + if(intent != null) { + extrasId = intent.getIntExtra(EXTRA_WIDGET_ID, extrasId); + } + if(extrasId == AppWidgetManager.INVALID_APPWIDGET_ID) { + int[] ids; + try { + ids = manager.getAppWidgetIds(thisWidget); + for(int id : ids) { + RemoteViews updateViews = buildUpdate(this, id); + manager.updateAppWidget(id, updateViews); + } + } catch (RuntimeException e) { + // "System server dead" was sometimes thrown here by the OS. Abort if that happens + } + } else { + RemoteViews updateViews = buildUpdate(this, extrasId); + manager.updateAppWidget(extrasId, updateViews); + } + + stopSelf(); + } + + @Override + public IBinder onBind(Intent intent) { + return null; + } + + public RemoteViews buildUpdate(Context context, int widgetId) { + DependencyInjectionService.getInstance().inject(this); + + RemoteViews views = getThemedRemoteViews(context); + + int numberOfTasks = NUM_VISIBLE_TASKS; + + TodorooCursor cursor = null; + Filter filter = null; + try { + filter = widgetHelper.getFilter(context, widgetId); + if (SubtasksHelper.isTagFilter(filter)) { + ((FilterWithCustomIntent) filter).customTaskList = new ComponentName(context, TagViewFragment.class); // In case legacy widget was created with subtasks fragment + } + views.setTextViewText(R.id.widget_title, filter.title); + views.removeAllViews(R.id.taskbody); + + SharedPreferences publicPrefs = AstridPreferences.getPublicPrefs(this); + int flags = publicPrefs.getInt(SortHelper.PREF_SORT_FLAGS, 0); + int sort = publicPrefs.getInt(SortHelper.PREF_SORT_SORT, 0); + String query = SortHelper.adjustQueryForFlagsAndSort( + filter.getSqlQuery(), flags, sort).replaceAll("LIMIT \\d+", "") + " LIMIT " + numberOfTasks; + + String tagName = Preferences.getStringValue(WidgetConfigActivity.PREF_TITLE + widgetId); + query = SubtasksHelper.applySubtasksToWidgetFilter(filter, query, tagName, numberOfTasks); + + database.openForReading(); + cursor = taskService.fetchFiltered(query, null, Task.ID, Task.TITLE, Task.DUE_DATE, Task.COMPLETION_DATE); + Task task = new Task(); + int i; + for (i = 0; i < cursor.getCount() && i < numberOfTasks; i++) { + cursor.moveToPosition(i); + task.readFromCursor(cursor); + + String textContent; + Resources r = context.getResources(); + int textColor = r + .getColor(ThemeService.isDarkWidgetTheme() ? R.color.widget_text_color_dark : R.color.widget_text_color_light); + + textContent = task.getValue(Task.TITLE); + + if(task.isCompleted()) { + textColor = r.getColor(R.color.task_list_done); + } else if(task.hasDueDate() && task.isOverdue()) { + textColor = r.getColor(R.color.task_list_overdue); + } + + RemoteViews row = new RemoteViews(Constants.PACKAGE, R.layout.widget_row); + + row.setTextViewText(R.id.text, textContent); + row.setTextColor(R.id.text, textColor); + + views.addView(R.id.taskbody, row); + + RemoteViews separator = new RemoteViews(Constants.PACKAGE, R.layout.widget_separator); + boolean isLastRow = (i == cursor.getCount() - 1) || (i == numberOfTasks - 1); + if (!isLastRow) { + views.addView(R.id.taskbody, separator); + } + } + for (; i < numberOfTasks; i++) { + RemoteViews row = new RemoteViews(Constants.PACKAGE, R.layout.widget_row); + row.setViewVisibility(R.id.text, View.INVISIBLE); + views.addView(R.id.taskbody, row); + } + + } catch (Exception e) { + // can happen if database is not ready + Log.e("WIDGET-UPDATE", "Error updating widget", e); + } finally { + if(cursor != null) { + cursor.close(); + } + } + + PendingIntent pListIntent = widgetHelper.getListIntent(context, filter, widgetId); + if (pListIntent != null) { + views.setOnClickPendingIntent(R.id.taskbody, pListIntent); + } + + PendingIntent pEditIntent = widgetHelper.getEditIntent(context, filter, widgetId); + if (pEditIntent != null) { + views.setOnClickPendingIntent(R.id.widget_button, pEditIntent); + views.setOnClickPendingIntent(R.id.widget_title, pEditIntent); + } + + return views; + } + + /** + * The reason we use a bunch of different but almost identical layouts is that there is a bug with + * Android 2.1 (level 7) that doesn't allow setting backgrounds on remote views. I know it's lame, + * but I didn't see a better solution. Alternatively, we could disallow theming widgets on + * Android 2.1. + */ + private RemoteViews getThemedRemoteViews(Context context) { + String packageName = context.getPackageName(); + Resources r = context.getResources(); + int layout; + RemoteViews views; + + int titleColor; + int buttonDrawable; + + if (ThemeService.isDarkWidgetTheme()) { + layout = R.layout.widget_initialized_dark; + titleColor = r.getColor(R.color.widget_text_color_dark); + buttonDrawable = R.drawable.ic_action_new_light; + } else { + layout = R.layout.widget_initialized; + titleColor = r.getColor(R.color.widget_text_color_light); + buttonDrawable = R.drawable.ic_action_new; + } + + views = new RemoteViews(packageName, layout); + views.setTextColor(R.id.widget_title, titleColor); + views.setInt(R.id.widget_button, "setImageResource", buttonDrawable); + return views; + } +} \ No newline at end of file diff --git a/astrid/src/main/java/org/tasks/widget/ScrollableTasksWidget.java b/astrid/src/main/java/org/tasks/widget/ScrollableTasksWidget.java new file mode 100644 index 000000000..6e4c7f54a --- /dev/null +++ b/astrid/src/main/java/org/tasks/widget/ScrollableTasksWidget.java @@ -0,0 +1,46 @@ +package org.tasks.widget; + +import android.annotation.TargetApi; +import android.appwidget.AppWidgetManager; +import android.appwidget.AppWidgetProvider; +import android.content.ComponentName; +import android.content.Context; +import android.os.Build; + +import com.todoroo.andlib.service.ContextManager; + +import org.tasks.R; + +@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) +public class ScrollableTasksWidget extends AppWidgetProvider { + + private final WidgetHelper widgetHelper = new WidgetHelper(); + + @Override + public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { + ContextManager.setContext(context); + super.onUpdate(context, appWidgetManager, appWidgetIds); + + ComponentName thisWidget = new ComponentName(context, ScrollableTasksWidget.class); + int[] ids = appWidgetManager.getAppWidgetIds(thisWidget); + for (int id : ids) { + appWidgetManager.updateAppWidget(id, widgetHelper.createScrollableWidget(context, id)); + } + } + + public static void updateWidgets(Context context) { + updateWidgets(context, null); + } + + public static void updateWidget(Context context, int id) { + updateWidgets(context, new int[]{id}); + } + + private static void updateWidgets(Context context, int[] widgetIds) { + AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context); + if (widgetIds == null) { + widgetIds = appWidgetManager.getAppWidgetIds(new ComponentName(context, ScrollableTasksWidget.class)); + } + appWidgetManager.notifyAppWidgetViewDataChanged(widgetIds, R.id.list_view); + } +} diff --git a/astrid/src/main/java/org/tasks/widget/ScrollableViewsFactory.java b/astrid/src/main/java/org/tasks/widget/ScrollableViewsFactory.java new file mode 100644 index 000000000..a2aaf0c8f --- /dev/null +++ b/astrid/src/main/java/org/tasks/widget/ScrollableViewsFactory.java @@ -0,0 +1,131 @@ +package org.tasks.widget; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.Intent; +import android.content.res.Resources; +import android.os.Build; +import android.util.Log; +import android.widget.RemoteViews; +import android.widget.RemoteViewsService; + +import com.todoroo.andlib.data.TodorooCursor; +import com.todoroo.andlib.service.Autowired; +import com.todoroo.andlib.service.DependencyInjectionService; +import com.todoroo.astrid.dao.Database; +import com.todoroo.astrid.data.Task; +import com.todoroo.astrid.service.TaskService; +import com.todoroo.astrid.utility.Constants; + +import org.tasks.R; + +@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) +public class ScrollableViewsFactory implements RemoteViewsService.RemoteViewsFactory { + + @Autowired + Database database; + + @Autowired + TaskService taskService; + + private final Context context; + private String query; + private boolean dark; + + private TodorooCursor cursor; + + public ScrollableViewsFactory(Context context, String query, boolean dark) { + this.context = context; + this.query = query; + this.dark = dark; + } + + @Override + public void onCreate() { + DependencyInjectionService.getInstance().inject(this); + + database.openForReading(); + cursor = getCursor(); + } + + @Override + public void onDataSetChanged() { + cursor = getCursor(); + } + + @Override + public void onDestroy() { + cursor.close(); + } + + @Override + public int getCount() { + return cursor.getCount(); + } + + @Override + public RemoteViews getViewAt(int position) { + return buildUpdate(position); + } + + @Override + public RemoteViews getLoadingView() { + return null; + } + + @Override + public int getViewTypeCount() { + return 1; + } + + @Override + public long getItemId(int position) { + return getTask(position).getId(); + } + + @Override + public boolean hasStableIds() { + return true; + } + + public RemoteViews buildUpdate(int position) { + try { + Task task = getTask(position); + + String textContent; + Resources r = context.getResources(); + int textColor = r.getColor(dark ? R.color.widget_text_color_dark : R.color.widget_text_color_light); + + textContent = task.getValue(Task.TITLE); + + if (task.isCompleted()) { + textColor = r.getColor(R.color.task_list_done); + } else if (task.hasDueDate() && task.isOverdue()) { + textColor = r.getColor(R.color.task_list_overdue); + } + + RemoteViews row = new RemoteViews(Constants.PACKAGE, R.layout.widget_row); + + row.setTextViewText(R.id.text, textContent); + row.setTextColor(R.id.text, textColor); + + row.setOnClickFillInIntent(R.id.text, new Intent()); + + return row; + } catch (Exception e) { + // can happen if database is not ready + Log.e("WIDGET-UPDATE", "Error updating widget", e); + } + + return null; + } + + private TodorooCursor getCursor() { + return taskService.fetchFiltered(query, null, Task.ID, Task.TITLE, Task.DUE_DATE, Task.COMPLETION_DATE); + } + + private Task getTask(int position) { + cursor.moveToPosition(position); + return new Task(cursor); + } +} diff --git a/astrid/src/main/java/org/tasks/widget/ScrollableWidgetConfigActivity.java b/astrid/src/main/java/org/tasks/widget/ScrollableWidgetConfigActivity.java new file mode 100644 index 000000000..7cd70d687 --- /dev/null +++ b/astrid/src/main/java/org/tasks/widget/ScrollableWidgetConfigActivity.java @@ -0,0 +1,157 @@ +package org.tasks.widget; + +import android.app.ListActivity; +import android.appwidget.AppWidgetManager; +import android.content.ContentValues; +import android.content.Intent; +import android.os.Bundle; +import android.util.DisplayMetrics; +import android.view.View; +import android.widget.Button; +import android.widget.ListView; + +import com.todoroo.andlib.utility.AndroidUtilities; +import com.todoroo.andlib.utility.Preferences; +import com.todoroo.astrid.adapter.FilterAdapter; +import com.todoroo.astrid.api.Filter; +import com.todoroo.astrid.api.FilterListItem; +import com.todoroo.astrid.api.FilterWithCustomIntent; +import com.todoroo.astrid.service.ThemeService; + +import org.tasks.R; + +public class ScrollableWidgetConfigActivity extends ListActivity { + + static final String PREF_TITLE = "widget-title-"; + static final String PREF_SQL = "widget-sql-"; + static final String PREF_VALUES = "widget-values-"; + static final String PREF_CUSTOM_INTENT = "widget-intent-"; + static final String PREF_CUSTOM_EXTRAS = "widget-extras-"; + static final String PREF_TAG_ID = "widget-tag-id-"; + + int mAppWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID; + + FilterAdapter adapter = null; + + private final WidgetHelper widgetHelper = new WidgetHelper(); + + public void updateWidget() { + // setup view for new widget + AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(getApplicationContext()); + appWidgetManager.updateAppWidget(mAppWidgetId, widgetHelper.createScrollableWidget(getApplicationContext(), mAppWidgetId)); + ScrollableTasksWidget.updateWidget(this, mAppWidgetId); + } + + @Override + public void onCreate(Bundle icicle) { + ThemeService.applyTheme(this); + ThemeService.setForceFilterInvert(true); + super.onCreate(icicle); + + // Set the result to CANCELED. This will cause the widget host to cancel + // out of the widget placement if they press the back button. + setResult(RESULT_CANCELED); + + // Set the view layout resource to use. + setContentView(R.layout.widget_config_activity); + + setTitle(R.string.WCA_title); + + // Find the widget id from the intent. + Intent intent = getIntent(); + Bundle extras = intent.getExtras(); + if (extras != null) { + mAppWidgetId = extras.getInt( + AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID); + } + + // If they gave us an intent without the widget id, just bail. + if (mAppWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) { + finish(); + } + + // set up ui + adapter = new FilterAdapter(this, getListView(), + R.layout.filter_adapter_row, true, true); + adapter.filterStyle = R.style.TextAppearance_FLA_Filter_Widget; + setListAdapter(adapter); + + Button button = (Button)findViewById(R.id.ok); + button.setOnClickListener(mOnClickListener); + } + + View.OnClickListener mOnClickListener = new View.OnClickListener() { + @Override + public void onClick(View v) { + // Save configuration options + saveConfiguration(adapter.getSelection()); + + updateWidget(); + + // Make sure we pass back the original appWidgetId + Intent resultValue = new Intent(); + resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId); + setResult(RESULT_OK, resultValue); + finish(); + } + }; + + + + @Override + protected void onListItemClick(ListView l, View v, int position, long id) { + super.onListItemClick(l, v, position, id); + Filter item = adapter.getItem(position); + adapter.setSelection(item); + } + + @Override + protected void onResume() { + super.onResume(); + adapter.registerRecevier(); + } + + @Override + protected void onPause() { + super.onPause(); + adapter.unregisterRecevier(); + } + + @Override + protected void onStop() { + super.onStop(); + ThemeService.setForceFilterInvert(false); + } + + private void saveConfiguration(FilterListItem filterListItem){ + DisplayMetrics metrics = new DisplayMetrics(); + getWindowManager().getDefaultDisplay().getMetrics(metrics); + + String sql = null, contentValuesString = null, title = null; + + if(filterListItem != null && filterListItem instanceof Filter) { + sql = ((Filter)filterListItem).getSqlQuery(); + ContentValues values = ((Filter)filterListItem).valuesForNewTasks; + if(values != null) { + contentValuesString = AndroidUtilities.contentValuesToSerializedString(values); + } + title = ((Filter)filterListItem).title; + } + + Preferences.setString(ScrollableWidgetConfigActivity.PREF_TITLE + mAppWidgetId, title); + Preferences.setString(ScrollableWidgetConfigActivity.PREF_SQL + mAppWidgetId, sql); + Preferences.setString(ScrollableWidgetConfigActivity.PREF_VALUES + mAppWidgetId, contentValuesString); + + if(filterListItem instanceof FilterWithCustomIntent) { + String flattenedName = ((FilterWithCustomIntent)filterListItem).customTaskList.flattenToString(); + Preferences.setString(ScrollableWidgetConfigActivity.PREF_CUSTOM_INTENT + mAppWidgetId, + flattenedName); + String flattenedExtras = AndroidUtilities.bundleToSerializedString(((FilterWithCustomIntent)filterListItem).customExtras); + if (flattenedExtras != null) { + Preferences.setString(ScrollableWidgetConfigActivity.PREF_CUSTOM_EXTRAS + mAppWidgetId, + flattenedExtras); + } + } + } + +} diff --git a/astrid/src/main/java/org/tasks/widget/ScrollableWidgetUpdateService.java b/astrid/src/main/java/org/tasks/widget/ScrollableWidgetUpdateService.java new file mode 100644 index 000000000..506f938e1 --- /dev/null +++ b/astrid/src/main/java/org/tasks/widget/ScrollableWidgetUpdateService.java @@ -0,0 +1,30 @@ +package org.tasks.widget; + +import android.annotation.TargetApi; +import android.content.Intent; +import android.os.Build; +import android.os.Bundle; +import android.widget.RemoteViewsService; + +@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) +public class ScrollableWidgetUpdateService extends RemoteViewsService { + + public static final String QUERY_ID = "org.tasks.widget.query_id"; + public static final String IS_DARK_THEME = "org.tasks.widget.is_dark_theme"; + + @Override + public RemoteViewsFactory onGetViewFactory(Intent intent) { + if (intent == null) { + return null; + } + + Bundle extras = intent.getExtras(); + if (extras == null) { + return null; + } + + String queryId = extras.getString(QUERY_ID); + boolean isDarkTheme = extras.getBoolean(IS_DARK_THEME); + return new ScrollableViewsFactory(this, queryId, isDarkTheme); + } +} diff --git a/astrid/src/main/java/org/tasks/widget/WidgetHelper.java b/astrid/src/main/java/org/tasks/widget/WidgetHelper.java new file mode 100644 index 000000000..69a4bb64a --- /dev/null +++ b/astrid/src/main/java/org/tasks/widget/WidgetHelper.java @@ -0,0 +1,259 @@ +package org.tasks.widget; + +import android.annotation.TargetApi; +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.appwidget.AppWidgetManager; +import android.content.ComponentName; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.widget.RemoteViews; + +import com.todoroo.andlib.service.Autowired; +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.TagViewFragment; +import com.todoroo.astrid.activity.TaskEditActivity; +import com.todoroo.astrid.activity.TaskEditFragment; +import com.todoroo.astrid.activity.TaskListActivity; +import com.todoroo.astrid.activity.TaskListFragment; +import com.todoroo.astrid.api.Filter; +import com.todoroo.astrid.api.FilterWithCustomIntent; +import com.todoroo.astrid.api.PermaSql; +import com.todoroo.astrid.core.CoreFilterExposer; +import com.todoroo.astrid.core.SortHelper; +import com.todoroo.astrid.data.TagData; +import com.todoroo.astrid.service.AstridDependencyInjector; +import com.todoroo.astrid.service.TagDataService; +import com.todoroo.astrid.service.ThemeService; +import com.todoroo.astrid.subtasks.SubtasksHelper; +import com.todoroo.astrid.tags.TagFilterExposer; +import com.todoroo.astrid.utility.AstridPreferences; +import com.todoroo.astrid.utility.Constants; +import com.todoroo.astrid.widget.TasksWidget; +import com.todoroo.astrid.widget.WidgetConfigActivity; +import com.todoroo.astrid.widget.WidgetUpdateService; + +import org.tasks.R; + +public class WidgetHelper { + + static { + AstridDependencyInjector.initialize(); + } + + public static long suppressUpdateFlag = 0; // Timestamp--don't update widgets if this flag is non-zero and now() is within 5 minutes + private static final long SUPPRESS_TIME = DateUtilities.ONE_MINUTE * 5; + + public static void updateWidgets(Context context) { + if (suppressUpdateFlag > 0 && DateUtilities.now() - suppressUpdateFlag < SUPPRESS_TIME) { + return; + } + suppressUpdateFlag = 0; + if(Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + TasksWidget.updateWidgets(context); + } else { + ScrollableTasksWidget.updateWidgets(context); + } + } + + public static void startWidgetService(Context context) { + Class widgetServiceClass = android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH + ? WidgetUpdateService.class + : ScrollableWidgetUpdateService.class; + AlarmManager am = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE); + Intent intent = new Intent(context, widgetServiceClass); + PendingIntent pendingIntent = PendingIntent.getService(context, + 0, intent, PendingIntent.FLAG_CANCEL_CURRENT); + am.setInexactRepeating(AlarmManager.RTC, 0, + Constants.WIDGET_UPDATE_INTERVAL, pendingIntent); + } + + public static void triggerUpdate(Context context) { + Class widgetClass = android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH + ? TasksWidget.class + : ScrollableTasksWidget.class; + AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context); + ComponentName thisWidget = new ComponentName(context, widgetClass); + Intent intent = new Intent(context, widgetClass); + intent.setAction("android.appwidget.action.APPWIDGET_UPDATE"); + intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, appWidgetManager.getAppWidgetIds(thisWidget)); + context.sendBroadcast(intent); + } + + @Autowired + TagDataService tagDataService; + + public WidgetHelper() { + DependencyInjectionService.getInstance().inject(this); + } + + @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) + public RemoteViews createScrollableWidget(Context context, int id) { + Filter filter = getFilter(context, id); + Intent rvIntent = new Intent(context, ScrollableWidgetUpdateService.class); + rvIntent.putExtra(ScrollableWidgetUpdateService.QUERY_ID, getQuery(context, filter, id)); + rvIntent.putExtra(ScrollableWidgetUpdateService.IS_DARK_THEME, ThemeService.isDarkWidgetTheme()); + rvIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, id); + rvIntent.setData(Uri.parse(rvIntent.toUri(Intent.URI_INTENT_SCHEME))); + RemoteViews remoteViews = new RemoteViews(context.getPackageName(), ThemeService.isDarkWidgetTheme() ? R.layout.scrollable_widget_dark : R.layout.scrollable_widget_light); + remoteViews.setTextViewText(R.id.widget_title, filter.title); + remoteViews.setRemoteAdapter(R.id.list_view, rvIntent); + remoteViews.setEmptyView(R.id.list_view, R.id.empty_view); + PendingIntent listIntent = getListIntent(context, filter, id); + if(listIntent != null) { + remoteViews.setOnClickPendingIntent(R.id.widget_title, listIntent); + remoteViews.setPendingIntentTemplate(R.id.list_view, listIntent); + } + PendingIntent editIntent = getEditIntent(context, filter, id); + if (editIntent != null) { + remoteViews.setOnClickPendingIntent(R.id.widget_button, editIntent); + } + return remoteViews; + } + + public PendingIntent getListIntent(Context context, Filter filter, int widgetId) { + Intent listIntent = new Intent(context, TaskListActivity.class); + String customIntent = Preferences.getStringValue(WidgetConfigActivity.PREF_CUSTOM_INTENT + + widgetId); + if(customIntent != null) { + String serializedExtras = Preferences.getStringValue(WidgetConfigActivity.PREF_CUSTOM_EXTRAS + + widgetId); + Bundle extras = AndroidUtilities.bundleFromSerializedString(serializedExtras); + listIntent.putExtras(extras); + } + listIntent.putExtra(TaskListActivity.TOKEN_SOURCE, Constants.SOURCE_WIDGET); + listIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_MULTIPLE_TASK); + if(filter != null) { + listIntent.putExtra(TaskListFragment.TOKEN_FILTER, filter); + listIntent.setAction("L" + widgetId + filter.getSqlQuery()); + } else { + listIntent.setAction("L" + widgetId); + } + if (filter instanceof FilterWithCustomIntent) { + listIntent.putExtras(((FilterWithCustomIntent) filter).customExtras); + } + + return PendingIntent.getActivity(context, widgetId, + listIntent, PendingIntent.FLAG_CANCEL_CURRENT); + } + + public PendingIntent getEditIntent(Context context, Filter filter, int id) { + Intent editIntent; + boolean tablet = AstridPreferences.useTabletLayout(context); + if (tablet) { + editIntent = new Intent(context, TaskListActivity.class); + editIntent.putExtra(TaskListActivity.OPEN_TASK, 0L); + } + else { + editIntent = new Intent(context, TaskEditActivity.class); + } + + editIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_MULTIPLE_TASK); + editIntent.putExtra(TaskEditFragment.OVERRIDE_FINISH_ANIM, false); + if(filter != null) { + editIntent.putExtra(TaskListFragment.TOKEN_FILTER, filter); + if (filter.valuesForNewTasks != null) { + String values = AndroidUtilities.contentValuesToSerializedString(filter.valuesForNewTasks); + values = PermaSql.replacePlaceholders(values); + editIntent.putExtra(TaskEditFragment.TOKEN_VALUES, values); + editIntent.setAction("E" + id + values); + } + if (tablet) { + if (filter instanceof FilterWithCustomIntent) { + Bundle customExtras = ((FilterWithCustomIntent) filter).customExtras; + editIntent.putExtras(customExtras); + } + } + } else { + editIntent.setAction("E" + id); + } + + return PendingIntent.getActivity(context, -id, editIntent, 0); + } + + public Filter getFilter(Context context, int widgetId) { + + // base our filter off the inbox filter, replace stuff if we have it + Filter filter = CoreFilterExposer.buildInboxFilter(context.getResources()); + String sql = Preferences.getStringValue(ScrollableWidgetConfigActivity.PREF_SQL + widgetId); + if(sql != null) { + filter.setSqlQuery(sql); + } + String title = Preferences.getStringValue(ScrollableWidgetConfigActivity.PREF_TITLE + widgetId); + if(title != null) { + filter.title = title; + } + String contentValues = Preferences.getStringValue(ScrollableWidgetConfigActivity.PREF_VALUES + widgetId); + if(contentValues != null) { + filter.valuesForNewTasks = AndroidUtilities.contentValuesFromSerializedString(contentValues); + } + + String customComponent = Preferences.getStringValue(ScrollableWidgetConfigActivity.PREF_CUSTOM_INTENT + + widgetId); + if (customComponent != null) { + ComponentName component = ComponentName.unflattenFromString(customComponent); + filter = new FilterWithCustomIntent(filter.title, filter.title, filter.getSqlQuery(), filter.valuesForNewTasks); + ((FilterWithCustomIntent) filter).customTaskList = component; + String serializedExtras = Preferences.getStringValue(ScrollableWidgetConfigActivity.PREF_CUSTOM_EXTRAS + + widgetId); + ((FilterWithCustomIntent) filter).customExtras = AndroidUtilities.bundleFromSerializedString(serializedExtras); + } + + // Validate tagData + long id = Preferences.getLong(ScrollableWidgetConfigActivity.PREF_TAG_ID + widgetId, 0); + TagData tagData; + if (id > 0) { + tagData = tagDataService.fetchById(id, TagData.ID, TagData.NAME, TagData.TASK_COUNT, TagData.UUID, TagData.PICTURE, TagData.USER_ID, TagData.MEMBER_COUNT); + if (tagData != null && !tagData.getValue(TagData.NAME).equals(filter.title)) { // Tag has been renamed; rebuild filter + filter = TagFilterExposer.filterFromTagData(context, tagData); + Preferences.setString(ScrollableWidgetConfigActivity.PREF_SQL + widgetId, filter.getSqlQuery()); + Preferences.setString(ScrollableWidgetConfigActivity.PREF_TITLE + widgetId, filter.title); + ContentValues newTaskValues = filter.valuesForNewTasks; + String contentValuesString = null; + if(newTaskValues != null) { + contentValuesString = AndroidUtilities.contentValuesToSerializedString(newTaskValues); + } + Preferences.setString(ScrollableWidgetConfigActivity.PREF_VALUES + widgetId, contentValuesString); + if (filter instanceof FilterWithCustomIntent) { + String flattenedExtras = AndroidUtilities.bundleToSerializedString(((FilterWithCustomIntent) filter).customExtras); + if (flattenedExtras != null) { + Preferences.setString(ScrollableWidgetConfigActivity.PREF_CUSTOM_EXTRAS + widgetId, + flattenedExtras); + } + } + } + } else { + tagData = tagDataService.getTagByName(filter.title, TagData.ID); + if (tagData != null) { + Preferences.setLong(ScrollableWidgetConfigActivity.PREF_TAG_ID + widgetId, tagData.getId()); + } + } + + return filter; + } + + private String getQuery(Context context, Filter filter, int widgetId) { + if (SubtasksHelper.isTagFilter(filter)) { + ((FilterWithCustomIntent) filter).customTaskList = new ComponentName(context, TagViewFragment.class); // In case legacy widget was created with subtasks fragment + } + + SharedPreferences publicPrefs = AstridPreferences.getPublicPrefs(context); + int flags = publicPrefs.getInt(SortHelper.PREF_SORT_FLAGS, 0); + int sort = publicPrefs.getInt(SortHelper.PREF_SORT_SORT, 0); + + String query = SortHelper.adjustQueryForFlagsAndSort( + filter.getSqlQuery(), flags, sort).replaceAll("LIMIT \\d+", ""); + + String tagName = Preferences.getStringValue(ScrollableWidgetConfigActivity.PREF_TITLE + widgetId); + + return SubtasksHelper.applySubtasksToWidgetFilter(filter, query, tagName, 0); + } +} diff --git a/astrid/src/main/res/layout/scrollable_widget_dark.xml b/astrid/src/main/res/layout/scrollable_widget_dark.xml new file mode 100644 index 000000000..9e5a72ff2 --- /dev/null +++ b/astrid/src/main/res/layout/scrollable_widget_dark.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + diff --git a/astrid/src/main/res/layout/scrollable_widget_light.xml b/astrid/src/main/res/layout/scrollable_widget_light.xml new file mode 100644 index 000000000..3555957d1 --- /dev/null +++ b/astrid/src/main/res/layout/scrollable_widget_light.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + diff --git a/astrid/src/main/res/layout/widget_initialized.xml b/astrid/src/main/res/layout/widget_initialized.xml index dc5bd9a29..17a3b5f33 100644 --- a/astrid/src/main/res/layout/widget_initialized.xml +++ b/astrid/src/main/res/layout/widget_initialized.xml @@ -19,21 +19,21 @@ android:orientation="horizontal" android:padding="3dp" android:background="@color/widget_header_light"> - + - + - + - + - + - + - + - + - + - + - + + + false + true + \ No newline at end of file diff --git a/astrid/src/main/res/values/attrs.xml b/astrid/src/main/res/values/attrs.xml index 009327596..3496b1e31 100644 --- a/astrid/src/main/res/values/attrs.xml +++ b/astrid/src/main/res/values/attrs.xml @@ -42,5 +42,8 @@ - + + true + false + diff --git a/astrid/src/main/res/xml/scrollable_widget_provider_info.xml b/astrid/src/main/res/xml/scrollable_widget_provider_info.xml new file mode 100644 index 000000000..f45b15b16 --- /dev/null +++ b/astrid/src/main/res/xml/scrollable_widget_provider_info.xml @@ -0,0 +1,16 @@ + + + + diff --git a/astrid/src/main/res/xml/widget_provider_info.xml b/astrid/src/main/res/xml/widget_provider_info.xml index 53b86ec7b..4d598d55e 100644 --- a/astrid/src/main/res/xml/widget_provider_info.xml +++ b/astrid/src/main/res/xml/widget_provider_info.xml @@ -6,7 +6,7 @@ -->