| /* |
| * 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); |
| } |
| |
| } |