Home | History | Annotate | Download | only in widget
      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
     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