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
17package com.android.messaging.widget;
18
19import android.app.PendingIntent;
20import android.appwidget.AppWidgetManager;
21import android.content.ComponentName;
22import android.content.Context;
23import android.content.Intent;
24import android.database.Cursor;
25import android.net.Uri;
26import android.os.Looper;
27import android.text.TextUtils;
28import android.view.View;
29import android.widget.RemoteViews;
30
31import com.android.messaging.R;
32import com.android.messaging.datamodel.MessagingContentProvider;
33import com.android.messaging.datamodel.data.ConversationListItemData;
34import com.android.messaging.ui.UIIntents;
35import com.android.messaging.ui.WidgetPickConversationActivity;
36import com.android.messaging.util.LogUtil;
37import com.android.messaging.util.OsUtil;
38import com.android.messaging.util.SafeAsyncTask;
39import com.android.messaging.util.UiUtils;
40
41public 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
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
69    protected String getAction() {
70        return ACTION_NOTIFY_MESSAGES_CHANGED;
71    }
72
73    @Override
74    protected int getListId() {
75        return R.id.message_list;
76    }
77
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     */
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     */
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     */
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
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
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
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     */
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