/** * Copyright (c) 2012 Todoroo Inc * * See the file "LICENSE" for the full license governing this code. */ 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.DisplayMetrics; import android.util.Log; import android.view.Display; import android.view.View; import android.view.WindowManager; import android.widget.RemoteViews; 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.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; public class TasksWidget extends AppWidgetProvider { public static final int THEME_LEGACY = -1; 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) { try { ContextManager.setContext(context); super.onUpdate(context, appWidgetManager, appWidgetIds); // Start in service to prevent Application Not Responding timeout updateWidgets(context); } catch (Exception e) { Log.e("astrid-update-widget", "widget update error", e); //$NON-NLS-1$ //$NON-NLS-2$ } } /** * Update all widgets * @param id */ 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)); } /** * Update widget with the given id * @param id */ public static void updateWidget(Context context, int id) { Intent intent = new Intent(ContextManager.getContext(), TasksWidget.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 { int id = extrasId; RemoteViews updateViews = buildUpdate(this, id); manager.updateAppWidget(id, updateViews); } stopSelf(); } @Override public IBinder onBind(Intent intent) { return null; } @SuppressWarnings("nls") public RemoteViews buildUpdate(Context context, int widgetId) { DependencyInjectionService.getInstance().inject(this); RemoteViews views = getThemedRemoteViews(context); int numberOfTasks = getNumberOfTasks(); TodorooCursor<Task> 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 = 0; 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); if (isLegacyTheme()) textColor = r.getColor(android.R.color.white); 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.Theme || theme == R.style.Theme_Transparent); } private boolean isLegacyTheme() { int theme = ThemeService.getWidgetTheme(); return theme == THEME_LEGACY; } /** * 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. * @param context * @return */ @SuppressWarnings("nls") private RemoteViews getThemedRemoteViews(Context context) { int theme = ThemeService.getWidgetTheme(); String packageName = context.getPackageName(); Resources r = context.getResources(); int layout; RemoteViews views = null; int titleColor; int buttonDrawable; if (isLegacyTheme()) { views = new RemoteViews(packageName, R.layout.widget_initialized_legacy); views.setTextColor(R.id.widget_title, r.getColor(android.R.color.white)); views.setInt(R.id.widget_button, "setImageResource", R.drawable.button_plus); views.setViewVisibility(R.id.widget_header_separator, View.GONE); return views; } else if(isDarkTheme()) { layout = (theme == R.style.Theme_Transparent ? R.layout.widget_initialized_dark_transparent : R.layout.widget_initialized_dark); titleColor = r.getColor(R.color.widget_text_color_dark); buttonDrawable = R.drawable.plus_button_blue; } else if (theme == R.style.Theme_White) { layout = R.layout.widget_initialized_red; titleColor = r.getColor(R.color.widget_text_color_light); buttonDrawable = R.drawable.plus_button_red; } else if (theme == R.style.Theme_White_Alt) { layout = R.layout.widget_initialized; titleColor = r.getColor(R.color.widget_text_color_light); buttonDrawable = R.drawable.plus_button_blue; } else { layout = (theme == R.style.Theme_TransparentWhite ? R.layout.widget_initialized_transparent : R.layout.widget_initialized); titleColor = r.getColor(R.color.widget_text_color_light); buttonDrawable = R.drawable.plus_button_dark_blue; } views = new RemoteViews(packageName, layout); views.setTextColor(R.id.widget_title, titleColor); views.setInt(R.id.widget_button, "setImageResource", buttonDrawable); return views; } private int getNumberOfTasks() { Display display = ((WindowManager) this.getSystemService( Context.WINDOW_SERVICE)).getDefaultDisplay(); DisplayMetrics metrics = new DisplayMetrics(); display.getMetrics(metrics); if(metrics.density <= 0.75) return 4; else return 5; } 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); Bundle extras = AndroidUtilities.bundleFromSerializedString(serializedExtras); ((FilterWithCustomIntent) filter).customExtras = extras; } // Validate tagData long id = Preferences.getLong(WidgetConfigActivity.PREF_TAG_ID + widgetId, 0); TagData tagData = null; 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; } } }