Pixel perfect Email widget

* Functionally correct and pixel perfect
* All current assets in place

Bug: 3292507
Bug: 3284201
Bug: 3135118
Bug: 3255036
Bug: 3252913

Change-Id: I8fa6752748c74b3a9789a3675da12f6c0c11975f
This commit is contained in:
Marc Blank 2010-12-17 09:54:20 -08:00
parent 13c615314f
commit 0fd8ae8808
13 changed files with 293 additions and 142 deletions

View File

@ -477,7 +477,7 @@
it exposes user passwords and other confidential information. -->
<provider
android:name=".provider.EmailProvider"
android:authorities="com.android.email.provider; com.android.email.notifier"
android:authorities="com.android.email.provider;com.android.email.notifier"
android:multiprocess="true"
android:permission="com.android.email.permission.ACCESS_PROVIDER"
android:label="@string/app_name"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 854 B

After

Width:  |  Height:  |  Size: 696 B

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (c) 2010, Google Inc. -->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_focused="true"
android:drawable="@drawable/header_row_focused_email_widget_holo" />
<item android:state_pressed="true"
android:drawable="@drawable/header_row_press_email_widget_holo" />
<item android:drawable="@drawable/bg_row_email_widget_holo" />
</selector>

View File

@ -14,56 +14,95 @@
limitations under the License.
-->
<RelativeLayout
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/widget_foo"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_margin="5dip"
android:background="@drawable/widget_background"
android:orientation="vertical">
<RelativeLayout
android:id="@+id/widget_header"
android:layout_width="fill_parent"
android:layout_height="60dip"
android:background="@drawable/widget_bg_top"
android:orientation="horizontal">
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="4dip"
android:layout_marginBottom="20dip"
android:layout_marginLeft="12dip"
android:layout_marginRight="12dip"
android:orientation="vertical"
android:background="@drawable/gradient_bg_email_widget_holo">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="42sp"
android:layout_marginTop="5dip"
android:layout_marginLeft="5dip"
android:layout_marginRight="5dip"
android:orientation="horizontal"
android:paddingTop="4dip"
android:paddingLeft="16dip"
android:paddingRight="8dip"
android:background="@drawable/header_bg_email_widget_holo">
<ImageView
android:id="@+id/widget_logo"
android:layout_width="60dip"
android:layout_height="60dip"
android:layout_marginLeft="5dip"
android:layout_marginRight="2dip"
android:layout_marginBottom="5dip"
android:layout_alignParentTop="true"
android:layout_alignParentLeft="true"
android:src="@mipmap/icon"/>
<ImageButton
android:id="@+id/widget_compose"
android:layout_width="56dip"
android:layout_height="56dip"
android:layout_marginRight="5dip"
android:layout_marginTop="3dip"
android:layout_alignParentTop="true"
android:layout_alignParentRight="true"
android:src="@drawable/ic_menu_compose"/>
<TextView
android:id="@+id/widget_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toRightOf="@id/widget_logo"
android:layout_marginTop="10dip"
android:textSize="18sp"
android:textColor="#F000"
android:textStyle="bold"
android:text="@string/widget_unread"
android:paddingRight="8dip"
android:src="@drawable/ic_email_email_widget_holo" />
<LinearLayout
android:id="@+id/widget_header"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight = "1"
android:orientation="vertical">
<TextView
android:id="@+id/widget_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="-2sp"
android:text="@string/widget_all_mail"
android:textSize="14sp"
android:textStyle="bold"
android:textColor="@android:color/white"
android:shadowColor="@color/widget_label_shadow_color"
android:shadowRadius="3"
android:freezesText="true" />
<TextView
android:id="@+id/widget_tap"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="-2sp"
android:singleLine="true"
android:text="@string/widget_other_views"
android:textSize="14sp"
android:textColor="@color/widget_account_color" />
</LinearLayout>
<TextView
android:id="@+id/widget_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="-16sp"
android:paddingRight="8dip"
android:textSize="48sp"
android:textColor="@color/widget_unread_count_color"
android:freezesText="true" />
</RelativeLayout>
<ImageView
android:id="@+id/widget_compose"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="2dip"
android:src="@drawable/btn_ic_new_email_email_widget_holo_dark" />
</LinearLayout>
<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/list_div_top_btm_email_widget_holo" />
<ListView
android:id="@+id/message_list"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_below="@id/widget_header"
android:cacheColorHint="#00000000"
/>
</RelativeLayout>
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginLeft="5dip"
android:layout_marginRight="5dip"
android:paddingTop="6dip"
android:clipToPadding="false"
android:fadingEdge="none"
android:divider="@drawable/list_div_email_widget_holo"
android:cacheColorHint="#00000000" />
<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/list_div_top_btm_email_widget_holo" />
</LinearLayout>

View File

@ -14,43 +14,46 @@
limitations under the License.
-->
<RelativeLayout
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/widget_message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/widget_date"
android:layout_width="64dip"
android:layout_height="64sp"
android:paddingLeft="16dip"
android:paddingRight="8dip"
android:orientation="vertical"
android:background="@drawable/widget_conversation_selector"
>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:textColor="#F000"
android:textSize="14sp"
android:paddingLeft="10dip"
android:drawablePadding="4dip"
/>
<TextView
android:id="@+id/widget_from"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_toLeftOf="@id/widget_date"
android:textColor="#F000"
android:ellipsize="end"
android:singleLine="true"
android:textSize="16sp"
android:paddingLeft="10dip"
android:drawablePadding="4dip"
/>
<TextView
android:id="@+id/widget_subject"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/widget_from"
android:minLines="2"
android:textSize="14sp"
android:textColor="#F000"
android:paddingLeft="10dip"
/>
</RelativeLayout>
android:layout_marginTop="8sp"
android:orientation="horizontal"
>
<TextView
android:id="@+id/widget_from"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:paddingRight="16dip"
android:singleLine="true" />
<!-- Use negative margins to align attachment icon with text -->
<ImageView
android:id="@+id/widget_attachment"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="-10sp"
android:layout_marginRight="-4dip"
android:src="@drawable/ic_attachment_holo_light" />
<TextView
android:id="@+id/widget_date"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
<TextView
android:id="@+id/widget_subject"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="-4sp"
android:maxLines="2"/>
</LinearLayout>

View File

@ -26,4 +26,8 @@
<dimen name="mailbox_list_width">304dip</dimen>
<!-- width of the message list, on the message list + message view mode. -->
<dimen name="message_list_width">466dip</dimen>
<dimen name="widget_senders_font_size">14sp</dimen>
<dimen name="widget_subject_font_size">14sp</dimen>
<dimen name="widget_date_font_size">14sp</dimen>
</resources>

View File

@ -29,4 +29,8 @@
(i.e. on portrait, it's the "expanded" message list.)
-->
<dimen name="message_list_width">440dip</dimen>
<dimen name="widget_senders_font_size">14sp</dimen>
<dimen name="widget_subject_font_size">14sp</dimen>
<dimen name="widget_date_font_size">14sp</dimen>
</resources>

View File

@ -22,4 +22,11 @@
<color name="account_setup_divider_color">#b6d650</color>
<color name="message_view_fogged_glass_color">#00000000</color>
<!-- Widget colors -->
<color name="widget_label_shadow_color">#0d0d0d</color>
<color name="widget_account_color">#666666</color>
<color name="widget_unread_count_color">#4d4d4d</color>
<color name="widget_default_text_color">#000000</color>
<color name="widget_light_text_color">#666666</color>
</resources>

View File

@ -37,4 +37,11 @@
<color name="combined_view_account_color_3">#ff009b8b</color>
<color name="combined_view_account_color_4">#fff4fd04</color>
<color name="combined_view_account_color_5">#ffe65020</color>
<!-- Widget colors -->
<color name="widget_label_shadow_color">#0d0d0d</color>
<color name="widget_account_color">#666666</color>
<color name="widget_unread_count_color">#4d4d4d</color>
<color name="widget_default_text_color">#000000</color>
<color name="widget_light_text_color">#666666</color>
</resources>

View File

@ -37,4 +37,8 @@
<dimen name="message_list_item_color_tip_right_margin_on_wide">65dip</dimen>
<dimen name="error_message_height">48dip</dimen>
<dimen name="widget_senders_font_size">14sp</dimen>
<dimen name="widget_subject_font_size">14sp</dimen>
<dimen name="widget_date_font_size">14sp</dimen>
</resources>

View File

@ -997,7 +997,7 @@ save attachment.</string>
<!-- Widget -->
<!-- Instruction for how to move to different widget views [CHAR LIMIT=50] -->
<string name="widget_other_views">Tap email icon for other views</string>
<string name="widget_other_views">Tap icon to configure</string>
<!-- Header for the "All Mail" widget view (showing all of the user's mail) [CHAR LIMIT=20] -->
<string name="widget_all_mail">All Mail</string>
<!-- Header for the "Unread" widget view (showing all unread mail) [CHAR LIMIT=20] -->

View File

@ -15,9 +15,9 @@
-->
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:minWidth="220dp"
android:minHeight="220dp"
android:minWidth="258dip"
android:minHeight="258dip"
android:updatePeriodMillis="0"
android:initialLayout="@layout/widget"
android:previewImage="@drawable/email_widget_preview"
android:initialLayout="@layout/widget"
/>

View File

@ -36,20 +36,22 @@ import android.content.ContentUris;
import android.content.Context;
import android.content.Intent;
import android.content.Loader;
import android.content.res.Resources;
import android.database.Cursor;
import android.graphics.Typeface;
import android.graphics.Paint.Align;
import android.net.Uri;
import android.net.Uri.Builder;
import android.os.Bundle;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.TextPaint;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.text.TextUtils.TruncateAt;
import android.text.format.DateUtils;
import android.text.style.AbsoluteSizeSpan;
import android.text.style.ForegroundColorSpan;
import android.text.style.StyleSpan;
import android.util.Log;
import android.view.View;
import android.widget.RemoteViews;
import android.widget.RemoteViewsService;
@ -98,7 +100,14 @@ public class WidgetProvider extends AppWidgetProvider {
private static AppWidgetManager sWidgetManager;
private static Context sContext;
private static ContentResolver sResolver;
private static TextPaint sDatePaint = new TextPaint();
private static int sSenderFontSize;
private static int sSubjectFontSize;
private static int sDateFontSize;
private static int sDefaultTextColor;
private static int sLightTextColor;
private static String sSubjectSnippetDivider;
private static String sConfigureText;
/**
* Types of views that we're prepared to show in the widget - all mail, unread mail, and starred
@ -169,12 +178,19 @@ public class WidgetProvider extends AppWidgetProvider {
}
mWidgetId = _widgetId;
mLoader = new WidgetLoader();
if (sDatePaint == null) {
sDatePaint = new TextPaint();
sDatePaint.setTypeface(Typeface.DEFAULT);
sDatePaint.setTextSize(14);
sDatePaint.setAntiAlias(true);
sDatePaint.setTextAlign(Align.RIGHT);
if (sSubjectSnippetDivider == null) {
// Initialize string, color, dimension resources
Resources res = sContext.getResources();
sSubjectSnippetDivider =
res.getString(R.string.message_list_subject_snippet_divider);
sSenderFontSize = res.getDimensionPixelSize(R.dimen.widget_senders_font_size);
sSubjectFontSize = res.getDimensionPixelSize(R.dimen.widget_subject_font_size);
sDateFontSize = res.getDimensionPixelSize(R.dimen.widget_date_font_size);
sDefaultTextColor = res.getColor(R.color.widget_default_text_color);
sDefaultTextColor = res.getColor(R.color.widget_default_text_color);
sLightTextColor = res.getColor(R.color.widget_light_text_color);
sConfigureText = res.getString(R.string.widget_other_views);
}
}
@ -203,8 +219,7 @@ public class WidgetProvider extends AppWidgetProvider {
}
RemoteViews views =
new RemoteViews(sContext.getPackageName(), R.layout.widget);
views.setTextViewText(R.id.widget_title,
mViewType.getTitle(sContext) + " (" + mCursorCount + ")");
setupTitleAndCount(views);
sWidgetManager.partiallyUpdateAppWidget(mWidgetId, views);
sWidgetManager.notifyAppWidgetViewDataChanged(mWidgetId, R.id.message_list);
}
@ -219,7 +234,7 @@ public class WidgetProvider extends AppWidgetProvider {
* @param selection a valid query selection argument
*/
void startLoadingWithSelection(String selection) {
stopLoading();
reset();
setSelection(selection);
startLoading();
}
@ -256,25 +271,6 @@ public class WidgetProvider extends AppWidgetProvider {
sWidgetManager.notifyAppWidgetViewDataChanged(mWidgetId, R.id.message_list);
}
private void setStyleSpan(SpannableString str, int typeface) {
int length = str.length();
str.setSpan(new StyleSpan(typeface), 0, length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
private CharSequence formattedText(String str, int typeface) {
if (str == null) {
return "";
}
SpannableString ss = new SpannableString(str);
setStyleSpan(ss, typeface);
return ss;
}
private CharSequence formattedTextFromCursor(Cursor c, int column, int typeface) {
return formattedText(mCursor.getString(column), typeface);
}
/**
* Convenience method for creating an onClickPendingIntent that executes a command via
* our command Uri. Used for the "next view" command; appends the widget id to the command
@ -327,6 +323,16 @@ public class WidgetProvider extends AppWidgetProvider {
views.setOnClickFillInIntent(viewId, intent);
}
private void setupTitleAndCount(RemoteViews views) {
// Set up the title (view type + count of messages)
views.setTextViewText(R.id.widget_title, mViewType.getTitle(sContext));
views.setTextViewText(R.id.widget_tap, sConfigureText);
String count = "";
if (mCursorCount != TOTAL_COUNT_UNKNOWN) {
count = Integer.toString(mCursor.getCount());
}
views.setTextViewText(R.id.widget_count, count);
}
/**
* Update the "header" of the widget (i.e. everything that doesn't include the scrolling
* message list)
@ -341,13 +347,11 @@ public class WidgetProvider extends AppWidgetProvider {
// Set up the list with an adapter
Intent intent = new Intent(sContext, WidgetService.class);
intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mWidgetId);
intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));
views.setRemoteAdapter(R.id.message_list, intent);
// Set up the title (view type + count of messages)
views.setTextViewText(R.id.widget_title,
mViewType.getTitle(sContext) + " (" + mCursorCount + ")");
setupTitleAndCount(views);
// Set up "new" button (compose new message) and "next view" button
setActivityIntent(views, R.id.widget_compose, MessageCompose.class);
@ -356,39 +360,107 @@ public class WidgetProvider extends AppWidgetProvider {
// Use a bare intent for our template; we need to fill everything in
intent = new Intent(sContext, WidgetService.class);
PendingIntent pendingIntent =
PendingIntent.getService(sContext, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
PendingIntent.getService(sContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
views.setPendingIntentTemplate(R.id.message_list, pendingIntent);
// And finally update the widget
sWidgetManager.updateAppWidget(mWidgetId, views);
}
/**
* Add size and color styling to text
*
* @param text the text to style
* @param size the font size for this text
* @param color the color for this text
* @return a CharSequence quitable for use in RemoteViews.setTextViewText()
*/
private CharSequence addStyle(CharSequence text, int size, int color) {
SpannableStringBuilder builder = new SpannableStringBuilder(text);
builder.setSpan(
new AbsoluteSizeSpan(size), 0, text.length(),
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
if (color != 0) {
builder.setSpan(new ForegroundColorSpan(color), 0, text.length(),
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
return builder;
}
/**
* Create styled text for our combination subject and snippet
*
* @param subject the message's subject (or null)
* @param snippet the message's snippet (or null)
* @param read whether or not the message is read
* @return a CharSequence suitable for use in RemoteViews.setTextViewText()
*/
private CharSequence getStyledSubjectSnippet (String subject, String snippet,
boolean read) {
SpannableStringBuilder ssb = new SpannableStringBuilder();
boolean hasSubject = false;
if (!TextUtils.isEmpty(subject)) {
SpannableString ss = new SpannableString(subject);
ss.setSpan(new StyleSpan(read ? Typeface.NORMAL : Typeface.BOLD), 0, ss.length(),
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
ss.setSpan(new ForegroundColorSpan(sDefaultTextColor), 0, ss.length(),
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
ssb.append(ss);
hasSubject = true;
}
if (!TextUtils.isEmpty(snippet)) {
if (hasSubject) {
ssb.append(sSubjectSnippetDivider);
}
SpannableString ss = new SpannableString(snippet);
ss.setSpan(new ForegroundColorSpan(sLightTextColor), 0, snippet.length(),
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
ssb.append(ss);
}
return addStyle(ssb, sSubjectFontSize, 0);
}
/* (non-Javadoc)
* @see android.widget.RemoteViewsService.RemoteViewsFactory#getViewAt(int)
*/
public RemoteViews getViewAt(int position) {
// Use the cursor to set up the widget
synchronized (mCursorLock) {
if (mCursor == null || !mCursor.moveToPosition(position)) {
if (mCursor == null || mCursor.isClosed() || !mCursor.moveToPosition(position)) {
return getLoadingView();
}
RemoteViews views =
new RemoteViews(sContext.getPackageName(), R.layout.widget_list_item);
boolean isUnread = mCursor.getInt(WIDGET_COLUMN_FLAG_READ) != 1;
// Typeface for from, subject, and date (normal/bold) depends on whether the message
// is read/unread
int typeface = (mCursor.getInt(WIDGET_COLUMN_FLAG_READ) == 0) ? Typeface.BOLD
: Typeface.NORMAL;
views.setTextViewText(R.id.widget_from,
formattedTextFromCursor(mCursor, WIDGET_COLUMN_DISPLAY_NAME, typeface));
views.setTextViewText(R.id.widget_subject,
formattedTextFromCursor(mCursor, WIDGET_COLUMN_SUBJECT, typeface));
// Add style to sender
SpannableStringBuilder from =
new SpannableStringBuilder(mCursor.getString(WIDGET_COLUMN_DISPLAY_NAME));
from.setSpan(
isUnread ? new StyleSpan(Typeface.BOLD) : new StyleSpan(Typeface.NORMAL), 0,
from.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
CharSequence styledFrom = addStyle(from, sSenderFontSize, sDefaultTextColor);
views.setTextViewText(R.id.widget_from, styledFrom);
long timestamp = mCursor.getLong(WIDGET_COLUMN_TIMESTAMP);
// Get a nicely formatted date string (relative to today)
String date = DateUtils.getRelativeTimeSpanString(sContext, timestamp).toString();
views.setTextViewText(R.id.widget_date, TextUtils.ellipsize(date, sDatePaint, 64,
TruncateAt.END));
// Add style to date
CharSequence styledDate = addStyle(date, sDateFontSize, sDefaultTextColor);
views.setTextViewText(R.id.widget_date, styledDate);
// Add style to subject/snippet
String subject = mCursor.getString(WIDGET_COLUMN_SUBJECT);
String snippet = mCursor.getString(WIDGET_COLUMN_SNIPPET);
CharSequence subjectAndSnippet =
getStyledSubjectSnippet(subject, snippet, !isUnread);
views.setTextViewText(R.id.widget_subject, subjectAndSnippet);
if (mCursor.getInt(WIDGET_COLUMN_FLAG_ATTACHMENT) != 0) {
views.setViewVisibility(R.id.widget_attachment, View.VISIBLE);
} else {
views.setViewVisibility(R.id.widget_attachment, View.GONE);
}
// Set button intents for view, reply, and delete
String messageId = mCursor.getString(WIDGET_COLUMN_ID);
@ -433,6 +505,13 @@ public class WidgetProvider extends AppWidgetProvider {
public void onDataSetChanged() {
}
private void onDeleted() {
if (mLoader != null) {
mLoader.stopLoading();
}
sWidgetMap.remove(mWidgetId);
}
@Override
public void onDestroy() {
if (mLoader != null) {
@ -514,7 +593,7 @@ public class WidgetProvider extends AppWidgetProvider {
EmailWidget widget = sWidgetMap.get(widgetId);
if (widget != null) {
// Stop loading and remove the widget from the map
widget.onDestroy();
widget.onDeleted();
}
}
}
@ -545,9 +624,6 @@ public class WidgetProvider extends AppWidgetProvider {
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Uri data = intent.getData();
if (Email.DEBUG) {
Log.d(TAG, "Executing: " + data);
}
if (data == null) return Service.START_NOT_STICKY;
List<String> pathSegments = data.getPathSegments();
// Our path segments are <command>, <arg1> [, <arg2>]
@ -583,11 +659,8 @@ public class WidgetProvider extends AppWidgetProvider {
}
private void openMessage(long mailboxId, long messageId) {
// TODO Use narrower projection.
Mailbox mailbox = Mailbox.restoreMailboxWithId(this, mailboxId);
if (mailbox == null) {
return;
}
if (mailbox == null) return;
startActivity(Welcome.createOpenMessageIntent(this, mailbox.mAccountKey, mailboxId,
messageId));
}