1 /* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.messaging.widget; 18 19 import android.app.PendingIntent; 20 import android.appwidget.AppWidgetManager; 21 import android.content.ComponentName; 22 import android.content.Context; 23 import android.content.Intent; 24 import android.database.Cursor; 25 import android.net.Uri; 26 import android.os.Looper; 27 import android.text.TextUtils; 28 import android.view.View; 29 import android.widget.RemoteViews; 30 31 import com.android.messaging.R; 32 import com.android.messaging.datamodel.MessagingContentProvider; 33 import com.android.messaging.datamodel.data.ConversationListItemData; 34 import com.android.messaging.ui.UIIntents; 35 import com.android.messaging.ui.WidgetPickConversationActivity; 36 import com.android.messaging.util.LogUtil; 37 import com.android.messaging.util.OsUtil; 38 import com.android.messaging.util.SafeAsyncTask; 39 import com.android.messaging.util.UiUtils; 40 41 public class WidgetConversationProvider extends BaseWidgetProvider { 42 public static final String ACTION_NOTIFY_MESSAGES_CHANGED = 43 "com.android.Bugle.intent.action.ACTION_NOTIFY_MESSAGES_CHANGED"; 44 45 public static final int WIDGET_CONVERSATION_TEMPLATE_REQUEST_CODE = 1985; 46 public static final int WIDGET_CONVERSATION_REPLY_CODE = 1987; 47 48 // Intent extras 49 public static final String UI_INTENT_EXTRA_RECIPIENT = "recipient"; 50 public static final String UI_INTENT_EXTRA_ICON = "icon"; 51 52 /** 53 * Update the widget appWidgetId 54 */ 55 @Override updateWidget(final Context context, final int appWidgetId)56 protected void updateWidget(final Context context, final int appWidgetId) { 57 if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { 58 LogUtil.v(TAG, "updateWidget appWidgetId: " + appWidgetId); 59 } 60 if (OsUtil.hasRequiredPermissions()) { 61 rebuildWidget(context, appWidgetId); 62 } else { 63 AppWidgetManager.getInstance(context).updateAppWidget(appWidgetId, 64 UiUtils.getWidgetMissingPermissionView(context)); 65 } 66 } 67 68 @Override getAction()69 protected String getAction() { 70 return ACTION_NOTIFY_MESSAGES_CHANGED; 71 } 72 73 @Override getListId()74 protected int getListId() { 75 return R.id.message_list; 76 } 77 rebuildWidget(final Context context, final int appWidgetId)78 public static void rebuildWidget(final Context context, final int appWidgetId) { 79 if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { 80 LogUtil.v(TAG, "WidgetConversationProvider.rebuildWidget appWidgetId: " + appWidgetId); 81 } 82 final RemoteViews remoteViews = new RemoteViews(context.getPackageName(), 83 R.layout.widget_conversation); 84 PendingIntent clickIntent; 85 final UIIntents uiIntents = UIIntents.get(); 86 if (!isWidgetConfigured(appWidgetId)) { 87 // Widget has not been configured yet. Hide the normal UI elements and show the 88 // configuration view instead. 89 remoteViews.setViewVisibility(R.id.widget_label, View.GONE); 90 remoteViews.setViewVisibility(R.id.message_list, View.GONE); 91 remoteViews.setViewVisibility(R.id.launcher_icon, View.VISIBLE); 92 remoteViews.setViewVisibility(R.id.widget_configuration, View.VISIBLE); 93 94 remoteViews.setOnClickPendingIntent(R.id.widget_configuration, 95 uiIntents.getWidgetPendingIntentForConfigurationActivity(context, appWidgetId)); 96 97 // On click intent for Goto Conversation List 98 clickIntent = uiIntents.getWidgetPendingIntentForConversationListActivity(context); 99 remoteViews.setOnClickPendingIntent(R.id.widget_header, clickIntent); 100 101 if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { 102 LogUtil.v(TAG, "WidgetConversationProvider.rebuildWidget appWidgetId: " + 103 appWidgetId + " going into configure state"); 104 } 105 } else { 106 remoteViews.setViewVisibility(R.id.widget_label, View.VISIBLE); 107 remoteViews.setViewVisibility(R.id.message_list, View.VISIBLE); 108 remoteViews.setViewVisibility(R.id.launcher_icon, View.GONE); 109 remoteViews.setViewVisibility(R.id.widget_configuration, View.GONE); 110 111 final String conversationId = 112 WidgetPickConversationActivity.getConversationIdPref(appWidgetId); 113 final boolean isMainThread = Looper.myLooper() == Looper.getMainLooper(); 114 // If we're running on the UI thread, we can't do the DB access needed to get the 115 // conversation data. We'll do excute this again off of the UI thread. 116 final ConversationListItemData convData = isMainThread ? 117 null : getConversationData(context, conversationId); 118 119 // Launch an intent to avoid ANRs 120 final Intent intent = new Intent(context, WidgetConversationService.class); 121 intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId); 122 intent.putExtra(UIIntents.UI_INTENT_EXTRA_CONVERSATION_ID, conversationId); 123 intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME))); 124 remoteViews.setRemoteAdapter(appWidgetId, R.id.message_list, intent); 125 126 remoteViews.setTextViewText(R.id.widget_label, convData != null ? 127 convData.getName() : context.getString(R.string.app_name)); 128 129 // On click intent for Goto Conversation List 130 clickIntent = uiIntents.getWidgetPendingIntentForConversationListActivity(context); 131 remoteViews.setOnClickPendingIntent(R.id.widget_goto_conversation_list, clickIntent); 132 133 // Open the conversation when click on header 134 clickIntent = uiIntents.getWidgetPendingIntentForConversationActivity(context, 135 conversationId, WIDGET_CONVERSATION_REQUEST_CODE); 136 remoteViews.setOnClickPendingIntent(R.id.widget_header, clickIntent); 137 138 // On click intent for Conversation 139 // Note: the template intent has to be a "naked" intent without any extras. It turns out 140 // that if the template intent does have extras, those particular extras won't get 141 // replaced by the fill-in intent on each list item. 142 clickIntent = uiIntents.getWidgetPendingIntentForConversationActivity(context, 143 conversationId, WIDGET_CONVERSATION_TEMPLATE_REQUEST_CODE); 144 remoteViews.setPendingIntentTemplate(R.id.message_list, clickIntent); 145 146 if (isMainThread) { 147 // We're running on the UI thread and we couldn't update all the parts of the 148 // widget dependent on ConversationListItemData. However, we have to update 149 // the widget regardless, even with those missing pieces. Here we update the 150 // widget again in the background. 151 SafeAsyncTask.executeOnThreadPool(new Runnable() { 152 @Override 153 public void run() { 154 rebuildWidget(context, appWidgetId); 155 } 156 }); 157 } 158 } 159 160 AppWidgetManager.getInstance(context).updateAppWidget(appWidgetId, remoteViews); 161 162 } 163 164 /* 165 * notifyMessagesChanged called when the conversation changes so the widget will 166 * update and reflect the changes 167 */ notifyMessagesChanged(final Context context, final String conversationId)168 public static void notifyMessagesChanged(final Context context, final String conversationId) { 169 if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { 170 LogUtil.v(TAG, "notifyMessagesChanged"); 171 } 172 final Intent intent = new Intent(ACTION_NOTIFY_MESSAGES_CHANGED); 173 intent.putExtra(UIIntents.UI_INTENT_EXTRA_CONVERSATION_ID, conversationId); 174 context.sendBroadcast(intent); 175 } 176 177 /* 178 * notifyConversationDeleted is called when a conversation is deleted. Look through all the 179 * widgets and if they're displaying that conversation, force the widget into its 180 * configuration state. 181 */ notifyConversationDeleted(final Context context, final String conversationId)182 public static void notifyConversationDeleted(final Context context, 183 final String conversationId) { 184 if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { 185 LogUtil.v(TAG, "notifyConversationDeleted convId: " + conversationId); 186 } 187 188 final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context); 189 for (final int appWidgetId : appWidgetManager.getAppWidgetIds(new ComponentName(context, 190 WidgetConversationProvider.class))) { 191 // Retrieve the persisted information for this widget from preferences. 192 final String widgetConvId = 193 WidgetPickConversationActivity.getConversationIdPref(appWidgetId); 194 195 if (widgetConvId == null || widgetConvId.equals(conversationId)) { 196 if (widgetConvId != null) { 197 WidgetPickConversationActivity.deleteConversationIdPref(appWidgetId); 198 } 199 rebuildWidget(context, appWidgetId); 200 } 201 } 202 } 203 204 /* 205 * notifyConversationRenamed is called when a conversation is renamed. Look through all the 206 * widgets and if they're displaying that conversation, force the widget to rebuild itself 207 */ notifyConversationRenamed(final Context context, final String conversationId)208 public static void notifyConversationRenamed(final Context context, 209 final String conversationId) { 210 if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { 211 LogUtil.v(TAG, "notifyConversationRenamed convId: " + conversationId); 212 } 213 214 final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context); 215 for (final int appWidgetId : appWidgetManager.getAppWidgetIds(new ComponentName(context, 216 WidgetConversationProvider.class))) { 217 // Retrieve the persisted information for this widget from preferences. 218 final String widgetConvId = 219 WidgetPickConversationActivity.getConversationIdPref(appWidgetId); 220 221 if (widgetConvId != null && widgetConvId.equals(conversationId)) { 222 rebuildWidget(context, appWidgetId); 223 } 224 } 225 } 226 227 @Override onReceive(final Context context, final Intent intent)228 public void onReceive(final Context context, final Intent intent) { 229 if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { 230 LogUtil.v(TAG, "WidgetConversationProvider onReceive intent: " + intent); 231 } 232 final String action = intent.getAction(); 233 234 // The base class AppWidgetProvider's onReceive handles the normal widget intents. Here 235 // we're looking for an intent sent by our app when it knows a message has 236 // been sent or received (or a conversation has been read) and is telling the widget it 237 // needs to update. 238 if (getAction().equals(action)) { 239 final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context); 240 final int[] appWidgetIds = appWidgetManager.getAppWidgetIds(new ComponentName(context, 241 this.getClass())); 242 243 if (appWidgetIds.length == 0) { 244 if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { 245 LogUtil.v(TAG, "WidgetConversationProvider onReceive no widget ids"); 246 } 247 return; 248 } 249 // Normally the conversation id points to a specific conversation and we only update 250 // widgets looking at that conversation. When the conversation id is null, that means 251 // there's been a massive change (such as the initial import) and we need to update 252 // every conversation widget. 253 final String conversationId = intent.getExtras() 254 .getString(UIIntents.UI_INTENT_EXTRA_CONVERSATION_ID); 255 256 // Only update the widgets that match the conversation id that changed. 257 for (final int widgetId : appWidgetIds) { 258 // Retrieve the persisted information for this widget from preferences. 259 final String widgetConvId = 260 WidgetPickConversationActivity.getConversationIdPref(widgetId); 261 if (conversationId == null || TextUtils.equals(conversationId, widgetConvId)) { 262 // Update the list portion (i.e. the message list) of the widget 263 appWidgetManager.notifyAppWidgetViewDataChanged(widgetId, getListId()); 264 } 265 } 266 } else { 267 super.onReceive(context, intent); 268 } 269 } 270 getConversationData(final Context context, final String conversationId)271 private static ConversationListItemData getConversationData(final Context context, 272 final String conversationId) { 273 if (TextUtils.isEmpty(conversationId)) { 274 return null; 275 } 276 final Uri uri = MessagingContentProvider.buildConversationMetadataUri(conversationId); 277 Cursor cursor = null; 278 try { 279 cursor = context.getContentResolver().query(uri, 280 ConversationListItemData.PROJECTION, 281 null, // selection 282 null, // selection args 283 null); // sort order 284 if (cursor != null && cursor.getCount() > 0) { 285 final ConversationListItemData conv = new ConversationListItemData(); 286 cursor.moveToFirst(); 287 conv.bind(cursor); 288 return conv; 289 } 290 } finally { 291 if (cursor != null) { 292 cursor.close(); 293 } 294 } 295 return null; 296 } 297 298 @Override deletePreferences(final int widgetId)299 protected void deletePreferences(final int widgetId) { 300 WidgetPickConversationActivity.deleteConversationIdPref(widgetId); 301 } 302 303 /** 304 * When this widget is created, it's created for a particular conversation and that 305 * ConversationId is stored in shared prefs. If the associated conversation is deleted, 306 * the widget doesn't get deleted. Instead, it goes into a "tap to configure" state. This 307 * function determines whether the widget has been configured and has an associated 308 * ConversationId. 309 */ isWidgetConfigured(final int appWidgetId)310 public static boolean isWidgetConfigured(final int appWidgetId) { 311 final String conversationId = 312 WidgetPickConversationActivity.getConversationIdPref(appWidgetId); 313 return !TextUtils.isEmpty(conversationId); 314 } 315 316 } 317