summaryrefslogtreecommitdiff
path: root/src/com/android/messaging/widget/WidgetConversationProvider.java
blob: 6ae561417ba9004d9c9317a42c5c8fb190fa281c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
/*
 * Copyright (C) 2015 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.messaging.widget;

import android.app.PendingIntent;
import android.appwidget.AppWidgetManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Looper;
import android.text.TextUtils;
import android.view.View;
import android.widget.RemoteViews;

import com.android.messaging.R;
import com.android.messaging.datamodel.MessagingContentProvider;
import com.android.messaging.datamodel.data.ConversationListItemData;
import com.android.messaging.ui.UIIntents;
import com.android.messaging.ui.WidgetPickConversationActivity;
import com.android.messaging.util.LogUtil;
import com.android.messaging.util.OsUtil;
import com.android.messaging.util.SafeAsyncTask;
import com.android.messaging.util.UiUtils;

public class WidgetConversationProvider extends BaseWidgetProvider {
    public static final String ACTION_NOTIFY_MESSAGES_CHANGED =
            "com.android.Bugle.intent.action.ACTION_NOTIFY_MESSAGES_CHANGED";

    public static final int WIDGET_CONVERSATION_TEMPLATE_REQUEST_CODE = 1985;
    public static final int WIDGET_CONVERSATION_REPLY_CODE = 1987;

    // Intent extras
    public static final String UI_INTENT_EXTRA_RECIPIENT = "recipient";
    public static final String UI_INTENT_EXTRA_ICON = "icon";

    /**
     * Update the widget appWidgetId
     */
    @Override
    protected void updateWidget(final Context context, final int appWidgetId) {
        if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
            LogUtil.v(TAG, "updateWidget appWidgetId: " + appWidgetId);
        }
        if (OsUtil.hasRequiredPermissions()) {
            rebuildWidget(context, appWidgetId);
        } else {
            AppWidgetManager.getInstance(context).updateAppWidget(appWidgetId,
                    UiUtils.getWidgetMissingPermissionView(context));
        }
    }

    @Override
    protected String getAction() {
        return ACTION_NOTIFY_MESSAGES_CHANGED;
    }

    @Override
    protected int getListId() {
        return R.id.message_list;
    }

    public static void rebuildWidget(final Context context, final int appWidgetId) {
        if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
            LogUtil.v(TAG, "WidgetConversationProvider.rebuildWidget appWidgetId: " + appWidgetId);
        }
        final RemoteViews remoteViews = new RemoteViews(context.getPackageName(),
                R.layout.widget_conversation);
        PendingIntent clickIntent;
        final UIIntents uiIntents = UIIntents.get();
        if (!isWidgetConfigured(appWidgetId)) {
            // Widget has not been configured yet. Hide the normal UI elements and show the
            // configuration view instead.
            remoteViews.setViewVisibility(R.id.widget_label, View.GONE);
            remoteViews.setViewVisibility(R.id.message_list, View.GONE);
            remoteViews.setViewVisibility(R.id.launcher_icon, View.VISIBLE);
            remoteViews.setViewVisibility(R.id.widget_configuration, View.VISIBLE);

            remoteViews.setOnClickPendingIntent(R.id.widget_configuration,
                    uiIntents.getWidgetPendingIntentForConfigurationActivity(context, appWidgetId));

            // On click intent for Goto Conversation List
            clickIntent = uiIntents.getWidgetPendingIntentForConversationListActivity(context);
            remoteViews.setOnClickPendingIntent(R.id.widget_header, clickIntent);

            if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
                LogUtil.v(TAG, "WidgetConversationProvider.rebuildWidget appWidgetId: " +
                        appWidgetId + " going into configure state");
            }
        } else {
            remoteViews.setViewVisibility(R.id.widget_label, View.VISIBLE);
            remoteViews.setViewVisibility(R.id.message_list, View.VISIBLE);
            remoteViews.setViewVisibility(R.id.launcher_icon, View.GONE);
            remoteViews.setViewVisibility(R.id.widget_configuration, View.GONE);

            final String conversationId =
                    WidgetPickConversationActivity.getConversationIdPref(appWidgetId);
            final boolean isMainThread =  Looper.myLooper() == Looper.getMainLooper();
            // If we're running on the UI thread, we can't do the DB access needed to get the
            // conversation data. We'll do excute this again off of the UI thread.
            final ConversationListItemData convData = isMainThread ?
                    null : getConversationData(context, conversationId);

            // Launch an intent to avoid ANRs
            final Intent intent = new Intent(context, WidgetConversationService.class);
            intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
            intent.putExtra(UIIntents.UI_INTENT_EXTRA_CONVERSATION_ID, conversationId);
            intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));
            remoteViews.setRemoteAdapter(appWidgetId, R.id.message_list, intent);

            remoteViews.setTextViewText(R.id.widget_label, convData != null ?
                    convData.getName() : context.getString(R.string.app_name));

            // On click intent for Goto Conversation List
            clickIntent = uiIntents.getWidgetPendingIntentForConversationListActivity(context);
            remoteViews.setOnClickPendingIntent(R.id.widget_goto_conversation_list, clickIntent);

            // Open the conversation when click on header
            clickIntent = uiIntents.getWidgetPendingIntentForConversationActivity(context,
                    conversationId, WIDGET_CONVERSATION_REQUEST_CODE);
            remoteViews.setOnClickPendingIntent(R.id.widget_header, clickIntent);

            // On click intent for Conversation
            // Note: the template intent has to be a "naked" intent without any extras. It turns out
            // that if the template intent does have extras, those particular extras won't get
            // replaced by the fill-in intent on each list item.
            clickIntent = uiIntents.getWidgetPendingIntentForConversationActivity(context,
                    conversationId, WIDGET_CONVERSATION_TEMPLATE_REQUEST_CODE);
            remoteViews.setPendingIntentTemplate(R.id.message_list, clickIntent);

            if (isMainThread) {
                // We're running on the UI thread and we couldn't update all the parts of the
                // widget dependent on ConversationListItemData. However, we have to update
                // the widget regardless, even with those missing pieces. Here we update the
                // widget again in the background.
                SafeAsyncTask.executeOnThreadPool(new Runnable() {
                    @Override
                    public void run() {
                        rebuildWidget(context, appWidgetId);
                    }
                });
            }
        }

        AppWidgetManager.getInstance(context).updateAppWidget(appWidgetId, remoteViews);

    }

    /*
     * notifyMessagesChanged called when the conversation changes so the widget will
     * update and reflect the changes
     */
    public static void notifyMessagesChanged(final Context context, final String conversationId) {
        if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
            LogUtil.v(TAG, "notifyMessagesChanged");
        }
        final Intent intent = new Intent(ACTION_NOTIFY_MESSAGES_CHANGED);
        intent.putExtra(UIIntents.UI_INTENT_EXTRA_CONVERSATION_ID, conversationId);
        context.sendBroadcast(intent);
    }

    /*
     * notifyConversationDeleted is called when a conversation is deleted. Look through all the
     * widgets and if they're displaying that conversation, force the widget into its
     * configuration state.
     */
    public static void notifyConversationDeleted(final Context context,
            final String conversationId) {
        if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
            LogUtil.v(TAG, "notifyConversationDeleted convId: " + conversationId);
        }

        final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
        for (final int appWidgetId : appWidgetManager.getAppWidgetIds(new ComponentName(context,
                WidgetConversationProvider.class))) {
            // Retrieve the persisted information for this widget from preferences.
            final String widgetConvId =
                    WidgetPickConversationActivity.getConversationIdPref(appWidgetId);

            if (widgetConvId == null || widgetConvId.equals(conversationId)) {
                if (widgetConvId != null) {
                    WidgetPickConversationActivity.deleteConversationIdPref(appWidgetId);
                }
                rebuildWidget(context, appWidgetId);
            }
        }
    }

    /*
     * notifyConversationRenamed is called when a conversation is renamed. Look through all the
     * widgets and if they're displaying that conversation, force the widget to rebuild itself
     */
    public static void notifyConversationRenamed(final Context context,
            final String conversationId) {
        if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
            LogUtil.v(TAG, "notifyConversationRenamed convId: " + conversationId);
        }

        final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
        for (final int appWidgetId : appWidgetManager.getAppWidgetIds(new ComponentName(context,
                WidgetConversationProvider.class))) {
            // Retrieve the persisted information for this widget from preferences.
            final String widgetConvId =
                    WidgetPickConversationActivity.getConversationIdPref(appWidgetId);

            if (widgetConvId != null && widgetConvId.equals(conversationId)) {
                rebuildWidget(context, appWidgetId);
            }
        }
    }

    @Override
    public void onReceive(final Context context, final Intent intent) {
        if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
            LogUtil.v(TAG, "WidgetConversationProvider onReceive intent: " + intent);
        }
        final String action = intent.getAction();

        // The base class AppWidgetProvider's onReceive handles the normal widget intents. Here
        // we're looking for an intent sent by our app when it knows a message has
        // been sent or received (or a conversation has been read) and is telling the widget it
        // needs to update.
        if (getAction().equals(action)) {
            final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
            final int[] appWidgetIds = appWidgetManager.getAppWidgetIds(new ComponentName(context,
                    this.getClass()));

            if (appWidgetIds.length == 0) {
                if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
                    LogUtil.v(TAG, "WidgetConversationProvider onReceive no widget ids");
                }
                return;
            }
            // Normally the conversation id points to a specific conversation and we only update
            // widgets looking at that conversation. When the conversation id is null, that means
            // there's been a massive change (such as the initial import) and we need to update
            // every conversation widget.
            final String conversationId = intent.getExtras()
                    .getString(UIIntents.UI_INTENT_EXTRA_CONVERSATION_ID);

            // Only update the widgets that match the conversation id that changed.
            for (final int widgetId : appWidgetIds) {
                // Retrieve the persisted information for this widget from preferences.
                final String widgetConvId =
                        WidgetPickConversationActivity.getConversationIdPref(widgetId);
                if (conversationId == null || TextUtils.equals(conversationId, widgetConvId)) {
                    // Update the list portion (i.e. the message list) of the widget
                    appWidgetManager.notifyAppWidgetViewDataChanged(widgetId, getListId());
                }
            }
        } else {
            super.onReceive(context, intent);
        }
    }

    private static ConversationListItemData getConversationData(final Context context,
            final String conversationId) {
        if (TextUtils.isEmpty(conversationId)) {
            return null;
        }
        final Uri uri = MessagingContentProvider.buildConversationMetadataUri(conversationId);
        Cursor cursor = null;
        try {
            cursor = context.getContentResolver().query(uri,
                    ConversationListItemData.PROJECTION,
                    null,       // selection
                    null,       // selection args
                    null);      // sort order
            if (cursor != null && cursor.getCount() > 0) {
                final ConversationListItemData conv = new ConversationListItemData();
                cursor.moveToFirst();
                conv.bind(cursor);
                return conv;
            }
        } finally {
            if (cursor != null) {
                cursor.close();
            }
        }
        return null;
    }

    @Override
    protected void deletePreferences(final int widgetId) {
        WidgetPickConversationActivity.deleteConversationIdPref(widgetId);
    }

    /**
     * When this widget is created, it's created for a particular conversation and that
     * ConversationId is stored in shared prefs. If the associated conversation is deleted,
     * the widget doesn't get deleted. Instead, it goes into a "tap to configure" state. This
     * function determines whether the widget has been configured and has an associated
     * ConversationId.
     */
    public static boolean isWidgetConfigured(final int appWidgetId) {
        final String conversationId =
                WidgetPickConversationActivity.getConversationIdPref(appWidgetId);
        return !TextUtils.isEmpty(conversationId);
    }

}