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