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