• Home
  • History
  • Annotate
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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