Home | History | Annotate | Download | only in widget
      1 /*
      2  * Copyright (C) 2012 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.mms.widget;
     18 
     19 import android.app.PendingIntent;
     20 import android.appwidget.AppWidgetManager;
     21 import android.content.Context;
     22 import android.content.Intent;
     23 import android.content.res.Resources;
     24 import android.database.Cursor;
     25 import android.provider.Telephony.Threads;
     26 import android.text.Spannable;
     27 import android.text.SpannableStringBuilder;
     28 import android.text.style.ForegroundColorSpan;
     29 import android.text.style.TextAppearanceSpan;
     30 import android.util.Log;
     31 import android.view.View;
     32 import android.widget.RemoteViews;
     33 import android.widget.RemoteViewsService;
     34 
     35 import com.android.mms.LogTag;
     36 import com.android.mms.R;
     37 import com.android.mms.data.Contact;
     38 import com.android.mms.data.Conversation;
     39 import com.android.mms.ui.ConversationList;
     40 import com.android.mms.ui.ConversationListItem;
     41 import com.android.mms.ui.MessageUtils;
     42 import com.android.mms.util.SmileyParser;
     43 
     44 public class MmsWidgetService extends RemoteViewsService {
     45     private static final String TAG = "MmsWidgetService";
     46 
     47     /**
     48      * Lock to avoid race condition between widgets.
     49      */
     50     private static final Object sWidgetLock = new Object();
     51 
     52     @Override
     53     public RemoteViewsFactory onGetViewFactory(Intent intent) {
     54         if (Log.isLoggable(LogTag.WIDGET, Log.VERBOSE)) {
     55             Log.v(TAG, "onGetViewFactory intent: " + intent);
     56         }
     57         return new MmsFactory(getApplicationContext(), intent);
     58     }
     59 
     60     /**
     61      * Remote Views Factory for Mms Widget.
     62      */
     63     private static class MmsFactory
     64             implements RemoteViewsService.RemoteViewsFactory, Contact.UpdateListener {
     65         private static final int MAX_CONVERSATIONS_COUNT = 25;
     66         private final Context mContext;
     67         private final int mAppWidgetId;
     68         private boolean mShouldShowViewMore;
     69         private Cursor mConversationCursor;
     70         private int mUnreadConvCount;
     71         private final AppWidgetManager mAppWidgetManager;
     72 
     73         // Static colors
     74         private static int SUBJECT_TEXT_COLOR_READ;
     75         private static int SUBJECT_TEXT_COLOR_UNREAD;
     76         private static int SENDERS_TEXT_COLOR_READ;
     77         private static int SENDERS_TEXT_COLOR_UNREAD;
     78 
     79         public MmsFactory(Context context, Intent intent) {
     80             mContext = context;
     81             mAppWidgetId = intent.getIntExtra(
     82                     AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID);
     83             mAppWidgetManager = AppWidgetManager.getInstance(context);
     84             if (Log.isLoggable(LogTag.WIDGET, Log.VERBOSE)) {
     85                 Log.v(TAG, "MmsFactory intent: " + intent + "widget id: " + mAppWidgetId);
     86             }
     87             // Initialize colors
     88             Resources res = context.getResources();
     89             SENDERS_TEXT_COLOR_READ = res.getColor(R.color.widget_sender_text_color_read);
     90             SENDERS_TEXT_COLOR_UNREAD = res.getColor(R.color.widget_sender_text_color_unread);
     91             SUBJECT_TEXT_COLOR_READ = res.getColor(R.color.widget_subject_text_color_read);
     92             SUBJECT_TEXT_COLOR_UNREAD = res.getColor(R.color.widget_subject_text_color_unread);
     93         }
     94 
     95         @Override
     96         public void onCreate() {
     97             if (Log.isLoggable(LogTag.WIDGET, Log.VERBOSE)) {
     98                 Log.v(TAG, "onCreate");
     99             }
    100             Contact.addListener(this);
    101         }
    102 
    103         @Override
    104         public void onDestroy() {
    105             if (Log.isLoggable(LogTag.WIDGET, Log.VERBOSE)) {
    106                 Log.v(TAG, "onDestroy");
    107             }
    108             synchronized (sWidgetLock) {
    109                 if (mConversationCursor != null && !mConversationCursor.isClosed()) {
    110                     mConversationCursor.close();
    111                     mConversationCursor = null;
    112                 }
    113                 Contact.removeListener(this);
    114             }
    115         }
    116 
    117         @Override
    118         public void onDataSetChanged() {
    119             if (Log.isLoggable(LogTag.WIDGET, Log.VERBOSE)) {
    120                 Log.v(TAG, "onDataSetChanged");
    121             }
    122             synchronized (sWidgetLock) {
    123                 if (mConversationCursor != null) {
    124                     mConversationCursor.close();
    125                     mConversationCursor = null;
    126                 }
    127                 mConversationCursor = queryAllConversations();
    128                 mUnreadConvCount = queryUnreadCount();
    129                 onLoadComplete();
    130             }
    131         }
    132 
    133         private Cursor queryAllConversations() {
    134             return mContext.getContentResolver().query(
    135                     Conversation.sAllThreadsUri, Conversation.ALL_THREADS_PROJECTION,
    136                     null, null, null);
    137         }
    138 
    139         private int queryUnreadCount() {
    140             Cursor cursor = null;
    141             int unreadCount = 0;
    142             try {
    143                 cursor = mContext.getContentResolver().query(
    144                     Conversation.sAllThreadsUri, Conversation.ALL_THREADS_PROJECTION,
    145                     Threads.READ + "=0", null, null);
    146                 if (cursor != null) {
    147                     unreadCount = cursor.getCount();
    148                 }
    149             } finally {
    150                 if (cursor != null) {
    151                     cursor.close();
    152                 }
    153             }
    154             return unreadCount;
    155         }
    156 
    157         /**
    158          * Returns the number of items should be shown in the widget list.  This method also updates
    159          * the boolean that indicates whether the "show more" item should be shown.
    160          * @return the number of items to be displayed in the list.
    161          */
    162         @Override
    163         public int getCount() {
    164             if (Log.isLoggable(LogTag.WIDGET, Log.VERBOSE)) {
    165                 Log.v(TAG, "getCount");
    166             }
    167             synchronized (sWidgetLock) {
    168                 if (mConversationCursor == null) {
    169                     return 0;
    170                 }
    171                 final int count = getConversationCount();
    172                 mShouldShowViewMore = count < mConversationCursor.getCount();
    173                 return count + (mShouldShowViewMore ? 1 : 0);
    174             }
    175         }
    176 
    177         /**
    178          * Returns the number of conversations that should be shown in the widget.  This method
    179          * doesn't update the boolean that indicates that the "show more" item should be included
    180          * in the list.
    181          * @return
    182          */
    183         private int getConversationCount() {
    184             if (Log.isLoggable(LogTag.WIDGET, Log.VERBOSE)) {
    185                 Log.v(TAG, "getConversationCount");
    186             }
    187 
    188             return Math.min(mConversationCursor.getCount(), MAX_CONVERSATIONS_COUNT);
    189         }
    190 
    191         /*
    192          * Add color to a given text
    193          */
    194         private SpannableStringBuilder addColor(CharSequence text, int color) {
    195             SpannableStringBuilder builder = new SpannableStringBuilder(text);
    196             if (color != 0) {
    197                 builder.setSpan(new ForegroundColorSpan(color), 0, text.length(),
    198                         Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
    199             }
    200             return builder;
    201         }
    202 
    203         /**
    204          * @return the {@link RemoteViews} for a specific position in the list.
    205          */
    206         @Override
    207         public RemoteViews getViewAt(int position) {
    208             if (Log.isLoggable(LogTag.WIDGET, Log.VERBOSE)) {
    209                 Log.v(TAG, "getViewAt position: " + position);
    210             }
    211             synchronized (sWidgetLock) {
    212                 // "View more conversations" view.
    213                 if (mConversationCursor == null
    214                         || (mShouldShowViewMore && position >= getConversationCount())) {
    215                     return getViewMoreConversationsView();
    216                 }
    217 
    218                 if (!mConversationCursor.moveToPosition(position)) {
    219                     // If we ever fail to move to a position, return the "View More conversations"
    220                     // view.
    221                     Log.w(TAG, "Failed to move to position: " + position);
    222                     return getViewMoreConversationsView();
    223                 }
    224 
    225                 Conversation conv = Conversation.from(mContext, mConversationCursor);
    226 
    227                 // Inflate and fill out the remote view
    228                 RemoteViews remoteViews = new RemoteViews(
    229                         mContext.getPackageName(), R.layout.widget_conversation);
    230 
    231                 if (conv.hasUnreadMessages()) {
    232                     remoteViews.setViewVisibility(R.id.widget_unread_background, View.VISIBLE);
    233                     remoteViews.setViewVisibility(R.id.widget_read_background, View.GONE);
    234                 } else {
    235                     remoteViews.setViewVisibility(R.id.widget_unread_background, View.GONE);
    236                     remoteViews.setViewVisibility(R.id.widget_read_background, View.VISIBLE);
    237                 }
    238                 boolean hasAttachment = conv.hasAttachment();
    239                 remoteViews.setViewVisibility(R.id.attachment, hasAttachment ? View.VISIBLE :
    240                     View.GONE);
    241 
    242                 // Date
    243                 remoteViews.setTextViewText(R.id.date,
    244                         addColor(MessageUtils.formatTimeStampString(mContext, conv.getDate()),
    245                                 conv.hasUnreadMessages() ? SUBJECT_TEXT_COLOR_UNREAD :
    246                                     SUBJECT_TEXT_COLOR_READ));
    247 
    248                 // From
    249                 int color = conv.hasUnreadMessages() ? SENDERS_TEXT_COLOR_UNREAD :
    250                         SENDERS_TEXT_COLOR_READ;
    251                 SpannableStringBuilder from = addColor(conv.getRecipients().formatNames(", "),
    252                         color);
    253 
    254                 if (conv.hasDraft()) {
    255                     from.append(mContext.getResources().getString(R.string.draft_separator));
    256                     int before = from.length();
    257                     from.append(mContext.getResources().getString(R.string.has_draft));
    258                     from.setSpan(new TextAppearanceSpan(mContext,
    259                             android.R.style.TextAppearance_Small, color), before,
    260                             from.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
    261                     from.setSpan(new ForegroundColorSpan(
    262                             mContext.getResources().getColor(R.drawable.text_color_red)),
    263                             before, from.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
    264                 }
    265 
    266                 // Unread messages are shown in bold
    267                 if (conv.hasUnreadMessages()) {
    268                     from.setSpan(ConversationListItem.STYLE_BOLD, 0, from.length(),
    269                             Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
    270                 }
    271                 remoteViews.setTextViewText(R.id.from, from);
    272 
    273                 // Subject
    274                 // TODO: the SmileyParser inserts image spans but they don't seem to make it
    275                 // into the remote view.
    276                 SmileyParser parser = SmileyParser.getInstance();
    277                 remoteViews.setTextViewText(R.id.subject,
    278                         addColor(parser.addSmileySpans(conv.getSnippet()),
    279                                 conv.hasUnreadMessages() ? SUBJECT_TEXT_COLOR_UNREAD :
    280                                     SUBJECT_TEXT_COLOR_READ));
    281 
    282                 // On click intent.
    283                 Intent clickIntent = new Intent(Intent.ACTION_VIEW);
    284                 clickIntent.setType("vnd.android-dir/mms-sms");
    285                 clickIntent.putExtra("thread_id", conv.getThreadId());
    286 
    287                 remoteViews.setOnClickFillInIntent(R.id.widget_conversation, clickIntent);
    288 
    289                 return remoteViews;
    290             }
    291         }
    292 
    293         /**
    294          * @return the "View more conversations" view. When the user taps this item, they're
    295          * taken to the messaging app's conversation list.
    296          */
    297         private RemoteViews getViewMoreConversationsView() {
    298             if (Log.isLoggable(LogTag.WIDGET, Log.VERBOSE)) {
    299                 Log.v(TAG, "getViewMoreConversationsView");
    300             }
    301             RemoteViews view = new RemoteViews(mContext.getPackageName(), R.layout.widget_loading);
    302             view.setTextViewText(
    303                     R.id.loading_text, mContext.getText(R.string.view_more_conversations));
    304             PendingIntent pendingIntent =
    305                     PendingIntent.getActivity(mContext, 0, new Intent(mContext,
    306                             ConversationList.class),
    307                             PendingIntent.FLAG_UPDATE_CURRENT);
    308             view.setOnClickPendingIntent(R.id.widget_loading, pendingIntent);
    309             return view;
    310         }
    311 
    312         @Override
    313         public RemoteViews getLoadingView() {
    314             RemoteViews view = new RemoteViews(mContext.getPackageName(), R.layout.widget_loading);
    315             view.setTextViewText(
    316                     R.id.loading_text, mContext.getText(R.string.loading_conversations));
    317             return view;
    318         }
    319 
    320         @Override
    321         public int getViewTypeCount() {
    322             return 2;
    323         }
    324 
    325         @Override
    326         public long getItemId(int position) {
    327             return position;
    328         }
    329 
    330         @Override
    331         public boolean hasStableIds() {
    332             return true;
    333         }
    334 
    335         private void onLoadComplete() {
    336             if (Log.isLoggable(LogTag.WIDGET, Log.VERBOSE)) {
    337                 Log.v(TAG, "onLoadComplete");
    338             }
    339             RemoteViews remoteViews = new RemoteViews(mContext.getPackageName(), R.layout.widget);
    340 
    341             remoteViews.setViewVisibility(R.id.widget_unread_count, mUnreadConvCount > 0 ?
    342                     View.VISIBLE : View.GONE);
    343             if (mUnreadConvCount > 0) {
    344                 remoteViews.setTextViewText(
    345                         R.id.widget_unread_count, Integer.toString(mUnreadConvCount));
    346             }
    347 
    348             mAppWidgetManager.partiallyUpdateAppWidget(mAppWidgetId, remoteViews);
    349         }
    350 
    351         public void onUpdate(Contact updated) {
    352             if (Log.isLoggable(LogTag.WIDGET, Log.VERBOSE)) {
    353                 Log.v(TAG, "onUpdate from Contact: " + updated);
    354             }
    355             mAppWidgetManager.notifyAppWidgetViewDataChanged(mAppWidgetId, R.id.conversation_list);
    356         }
    357 
    358     }
    359 }
    360