Move over conversation list items.
Note:
Will have to work out the placement of fwd/reply indicators
and important exchange indicator
This doesn't do anything with labels
This doesn't handle checkboxes or selections yet
This is basically just a port of the coordinates system, xml, and assets
Change-Id: I9b55fab81e7b27ede602d2776ce16537c13bcefb
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index a57d8cf..3cd090f 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -5,19 +5,18 @@
android:versionName="1.0">
<application android:icon="@mipmap/ic_launcher_mail"
- android:label="@string/app_name">
+ android:label="@string/app_name"
+ android:theme="@android:style/Theme.Holo.Light">
<activity android:name=".UnifiedEmail"
- android:label="@string/app_name"
- android:theme="@android:style/Theme.Holo.Light" >
+ android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
- <activity android:name=".compose.ComposeActivity"
- android:theme="@android:style/Theme.Holo.Light" />
- <activity android:name=".browse.LabelItem"
- android:theme="@android:style/Theme.Holo.Light" />
+ <activity android:name=".compose.ComposeActivity" />
+ <activity android:name=".browse.LabelItem" />
+ <activity android:name=".browse.BrowseListActivity" />
</application>
</manifest>
diff --git a/res/drawable/conversation_read_selector.xml b/res/drawable/conversation_read_selector.xml
new file mode 100644
index 0000000..705ea09
--- /dev/null
+++ b/res/drawable/conversation_read_selector.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2011 Google Inc.
+ * Licensed to 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.
+ */
+-->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_pressed="true"
+ android:drawable="@drawable/list_pressed_holo" />
+ <item android:state_focused="true"
+ android:drawable="@drawable/list_focused_holo" />
+ <item android:state_selected="true"
+ android:drawable="@drawable/list_selected_holo" />
+ <item android:drawable="@drawable/list_read_holo" />
+</selector>
diff --git a/res/drawable/conversation_unread_selector.xml b/res/drawable/conversation_unread_selector.xml
new file mode 100644
index 0000000..32f2994
--- /dev/null
+++ b/res/drawable/conversation_unread_selector.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2011 Google Inc.
+ * Licensed to 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.
+ */
+-->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_pressed="true"
+ android:drawable="@drawable/list_pressed_holo" />
+ <item android:state_focused="true"
+ android:drawable="@drawable/list_focused_holo" />
+ <item android:state_selected="true"
+ android:drawable="@drawable/list_selected_holo" />
+ <item android:drawable="@drawable/list_unread_holo" />
+</selector>
diff --git a/res/drawable/conversation_wide_read_selector.xml b/res/drawable/conversation_wide_read_selector.xml
new file mode 100644
index 0000000..d9ed255
--- /dev/null
+++ b/res/drawable/conversation_wide_read_selector.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2011 Google Inc.
+ * Licensed to 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.
+ */
+-->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_focused="true"
+ android:drawable="@drawable/list_conversation_wide_read_focused_holo" />
+ <item android:state_activated="true"
+ android:drawable="@drawable/list_conversation_wide_read_pressed_holo" />
+ <item android:state_pressed="true"
+ android:drawable="@drawable/list_conversation_wide_read_pressed_holo" />
+ <item android:state_selected="true"
+ android:drawable="@drawable/list_conversation_wide_read_selected_holo" />
+ <item android:state_selected="true"
+ android:state_activated="true"
+ android:drawable="@drawable/list_conversation_wide_read_selected_holo" />
+ <item android:drawable="@drawable/list_conversation_wide_read_normal_holo" />
+</selector>
diff --git a/res/drawable/conversation_wide_unread_selector.xml b/res/drawable/conversation_wide_unread_selector.xml
new file mode 100644
index 0000000..8322465
--- /dev/null
+++ b/res/drawable/conversation_wide_unread_selector.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2011 Google Inc.
+ * Licensed to 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.
+ */
+-->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_focused="true"
+ android:drawable="@drawable/list_conversation_wide_unread_focused_holo" />
+ <item android:state_activated="true"
+ android:drawable="@drawable/list_conversation_wide_unread_pressed_holo" />
+ <item android:state_pressed="true"
+ android:drawable="@drawable/list_conversation_wide_unread_pressed_holo" />
+ <item android:state_selected="true"
+ android:drawable="@drawable/list_conversation_wide_unread_selected_holo" />
+ <item android:state_selected="true"
+ android:state_activated="true"
+ android:drawable="@drawable/list_conversation_wide_unread_selected_holo" />
+ <item android:drawable="@drawable/list_conversation_wide_unread_normal_holo" />
+</selector>
diff --git a/res/layout/browse_item_view_normal.xml b/res/layout/browse_item_view_normal.xml
new file mode 100644
index 0000000..94119d0
--- /dev/null
+++ b/res/layout/browse_item_view_normal.xml
@@ -0,0 +1,80 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (c) 2011 Google Inc. -->
+
+<!-- This layout is used as a template to create custom view CanvasConversationHeaderView
+ in normal mode. To be able to get the correct measurements, every source field should
+ be populated with data here. E.g:
+ - Text View should set text to a random long string (android:text="@string/long_string")
+ - Image View should set source to a specific asset -->
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="70sp"
+ android:orientation="vertical">
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_vertical">
+ <ImageView
+ android:id="@+id/personal_level"
+ style="@style/PersonalIndicatorStyle"
+ android:src="@drawable/ic_email_caret_single" />
+ <TextView
+ android:id="@+id/senders"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ style="@style/SendersStyle"
+ android:text="@string/long_string"
+ android:textSize="@dimen/senders_font_size"
+ android:lines="1"
+ android:layout_toRightOf="@id/personal_level" />
+ <LinearLayout
+ android:layout_height="wrap_content"
+ android:layout_width="@dimen/total_conversationbar_width"
+ android:layout_alignParentRight="true">
+ <TextView
+ android:id="@+id/labels"
+ android:layout_width="@dimen/max_total_label_width"
+ android:layout_height="wrap_content"
+ android:text="@string/long_string"
+ android:textSize="@dimen/labels_font_size"
+ android:lines="1"
+ android:paddingTop="1dip"
+ android:layout_marginTop="10sp" />
+ <ImageView
+ android:id="@+id/paperclip"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:src="@drawable/ic_attachment_holo_light"
+ android:layout_marginTop="6sp" />
+ <TextView
+ android:id="@+id/date"
+ android:layout_width="0dip"
+ android:layout_weight="1"
+ android:layout_height="wrap_content"
+ android:text="@string/long_string"
+ android:textSize="@dimen/date_font_size"
+ android:lines="1"
+ android:layout_marginRight="16dip"
+ android:layout_marginTop="10sp" />
+ </LinearLayout>
+ </RelativeLayout>
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+ <ImageView
+ style="@style/CheckmarkStyle"
+ android:id="@+id/checkmark"
+ android:src="@drawable/btn_check_on_normal_holo_light"/>
+ <TextView
+ android:id="@+id/subject"
+ android:layout_width="@dimen/subject_width"
+ style="@style/SubjectStyle"
+ android:text="@string/long_string"
+ android:lines="2"/>
+ <ImageView
+ android:id="@+id/star"
+ style="@style/StarStyle"
+ android:src="@drawable/btn_star_off_normal_email_holo_light" />
+ </LinearLayout>
+</LinearLayout>
\ No newline at end of file
diff --git a/res/layout/browse_item_view_wide.xml b/res/layout/browse_item_view_wide.xml
new file mode 100644
index 0000000..d5e3a85
--- /dev/null
+++ b/res/layout/browse_item_view_wide.xml
@@ -0,0 +1,94 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (c) 2011 Google Inc. -->
+
+<!-- This layout is used as a template to create custom view CanvasConversationHeaderView
+ in wide mode. To be able to get the correct measurements, every source field should
+ be populated with data here. E.g:
+ - Text View should set text to a random long string (android:text="@string/long_string")
+ - Image View should set source to a specific asset -->
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="64sp"
+ android:orientation="horizontal">
+ <ImageView
+ android:id="@+id/checkmark"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="16dip"
+ android:layout_marginRight="16dip"
+ android:layout_gravity="center_vertical"
+ android:src="@drawable/btn_check_on_normal_holo_light" />
+ <TextView
+ android:id="@+id/senders"
+ android:layout_width="224dip"
+ android:layout_height="wrap_content"
+ android:text="@string/long_string"
+ android:textSize="@dimen/wide_senders_font_size"
+ android:layout_gravity="center_vertical"
+ android:maxLines="2"
+ android:layout_marginTop="@dimen/wide_senders_margin_top" />
+ <ImageView
+ android:id="@+id/personal_level"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="16dip"
+ android:layout_marginRight="16dip"
+ android:layout_gravity="center_vertical"
+ android:src="@drawable/ic_email_caret_single" />
+ <TextView
+ android:id="@+id/subject"
+ android:layout_width="0dip"
+ android:layout_weight="0.7"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:text="@string/long_string"
+ android:lines="2"
+ android:textColor="@color/subject_text_color_unread"
+ android:textSize="@dimen/wide_subject_font_size"
+ android:layout_marginTop="2sp"
+ android:layout_marginRight="@dimen/wide_subject_margin_right"/>
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+ <TextView
+ android:id="@+id/labels"
+ android:layout_width="@dimen/max_total_label_width_wide"
+ android:layout_height="wrap_content"
+ android:paddingTop="2sp"
+ android:text="@string/long_string"
+ android:textSize="@dimen/labels_font_size"
+ android:lines="1"
+ android:layout_gravity="top" />
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:layout_gravity="center_vertical">
+ <ImageView
+ android:id="@+id/paperclip"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:src="@drawable/ic_attachment_holo_light"
+ android:layout_gravity="center_vertical"
+ android:layout_marginTop="@dimen/wide_attachment_margin_top"/>
+ <TextView
+ android:id="@+id/date"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:text="@string/date"
+ android:layout_marginTop="@dimen/wide_date_margin_top" />
+ </LinearLayout>
+ </LinearLayout>
+ <ImageView
+ android:id="@+id/star"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="16dip"
+ android:layout_marginRight="16dip"
+ android:layout_gravity="center_vertical"
+ android:src="@drawable/btn_star_off_normal_email_holo_light" />
+</LinearLayout>
diff --git a/res/layout/browse_list_activity.xml b/res/layout/browse_list_activity.xml
new file mode 100644
index 0000000..5ec198a
--- /dev/null
+++ b/res/layout/browse_list_activity.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (c) 2011 Google Inc. -->
+
+<!-- This layout is used as a template to create custom view CanvasConversationHeaderView
+ in normal mode. To be able to get the correct measurements, every source field should
+ be populated with data here. E.g:
+ - Text View should set text to a random long string (android:text="@string/long_string")
+ - Image View should set source to a specific asset -->
+<ListView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/browse_list"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+</ListView>
\ No newline at end of file
diff --git a/res/layout/layout_tests.xml b/res/layout/layout_tests.xml
index a5f8c8c..7fb91a0 100644
--- a/res/layout/layout_tests.xml
+++ b/res/layout/layout_tests.xml
@@ -38,6 +38,11 @@
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:onClick="labelSpinnerTest"/>
+ <Button android:id="@+id/label_browseitem"
+ android:text="@string/test_browseitems"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:onClick="browseListItemTest"/>
</LinearLayout>
</ScrollView>
\ No newline at end of file
diff --git a/res/values-land/styles.xml b/res/values-land/styles.xml
index 0832d99..2db11d6 100644
--- a/res/values-land/styles.xml
+++ b/res/values-land/styles.xml
@@ -18,7 +18,7 @@
*/
-->
<resources>
-
+ <!-- Compose styles -->
<style name="ComposeEditTextView">
<item name="android:minHeight">40dip</item>
<item name="android:layout_gravity">center_vertical</item>
@@ -37,6 +37,7 @@
<item name="android:minHeight">38dip</item>
<item name="android:paddingTop">0dip</item>
</style>
+ <!-- End compose styles -->
<!-- Spinner primary text is smaller than usual due to extra vertical padding in spinner asset -->
<style name="AccountSpinnerAnchorTextPrimary" parent="@android:style/TextAppearance.Holo.Widget.ActionBar.Title">
diff --git a/res/values-sw600dp/arrays.xml b/res/values-sw600dp/arrays.xml
new file mode 100644
index 0000000..7646733
--- /dev/null
+++ b/res/values-sw600dp/arrays.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (c) 2011, Google Inc. -->
+
+<resources>
+ <integer-array name="senders_with_labels_and_attachment_lengths">
+ <item>50</item>
+ <item>25</item>
+ </integer-array>
+ <integer-array name="senders_with_labels_lengths">
+ <item>50</item>
+ <item>27</item>
+ </integer-array>
+ <integer-array name="senders_with_attachment_lengths">
+ <item>50</item>
+ <item>30</item>
+ </integer-array>
+ <integer-array name="senders_lengths">
+ <item>50</item>
+ <item>35</item>
+ </integer-array>
+ <!-- When no recent labels exist, pre-populate with these as defaults -->
+ <string-array name="default_recent_labels">
+ <!-- (empty) -->
+ </string-array>
+</resources>
\ No newline at end of file
diff --git a/res/values-sw600dp/constants.xml b/res/values-sw600dp/constants.xml
new file mode 100644
index 0000000..76140bd
--- /dev/null
+++ b/res/values-sw600dp/constants.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2011 Google Inc.
+ * Licensed to 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.
+ */
+-->
+<resources>
+ <!-- Boolean value indicating whether the table UI should be used. -->
+ <integer name="use_tablet_ui">1</integer>
+ <integer name="conversation_header_mode">1</integer>
+</resources>
\ No newline at end of file
diff --git a/res/values-sw600dp/dimen.xml b/res/values-sw600dp/dimen.xml
index daeb68b..b58eb01 100644
--- a/res/values-sw600dp/dimen.xml
+++ b/res/values-sw600dp/dimen.xml
@@ -20,4 +20,10 @@
<resources>
<dimen name="senders_font_size">18sp</dimen>
<dimen name="account_dropdown_dropdownwidth">496dip</dimen>
+ <dimen name="wide_senders_margin_top">3sp</dimen>
+ <dimen name="wide_attachment_margin_top">2sp</dimen>
+ <dimen name="wide_date_margin_top">4sp</dimen>
+ <dimen name="max_total_label_width_wide">128dip</dimen>
+ <dimen name="wide_subject_margin_right">28dip</dimen>
+ <dimen name="subject_width">238dip</dimen>
</resources>
diff --git a/res/values-sw600dp/styles.xml b/res/values-sw600dp/styles.xml
index 05baa27..b75a638 100644
--- a/res/values-sw600dp/styles.xml
+++ b/res/values-sw600dp/styles.xml
@@ -19,6 +19,7 @@
-->
<resources>
+ <!-- Compose styles -->
<style name="RecipientEditTextViewStyle" parent="@style/RecipientEditTextView">
<item name="android:minHeight">42dip</item>
<item name="android:gravity">bottom</item>
@@ -77,6 +78,7 @@
<style name="RecipientComposeFieldLayout" parent="@style/ComposeFieldLayout">
<item name="android:orientation">vertical</item>
</style>
+ <!-- End compose styles -->
<style name="AccountSwitchSpinnerItem">
<item name="android:layout_width">332dip</item>
@@ -84,4 +86,43 @@
<style name="AccountSpinnerAnchorTextPrimary" parent="@android:style/TextAppearance.Holo.Widget.ActionBar.Title">
</style>
+
+ <!-- Browse list item styles -->
+ <style name="PersonalIndicatorStyle">
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:layout_marginLeft">8dip</item>
+ <item name="android:layout_marginTop">1sp</item>
+ </style>
+
+ <style name="SendersStyle">
+ <item name="android:layout_marginLeft">8dip</item>
+ <item name="android:layout_marginTop">10sp</item>
+ </style>
+
+ <style name="SubjectStyle">
+ <item name="android:layout_marginLeft">8dip</item>
+ <item name="android:layout_marginTop">-6sp</item>
+ <item name="android:layout_weight">1</item>
+ <item name="android:textColor">@color/subject_text_color</item>
+ <item name="android:textSize">14sp</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:lines">2</item>
+ <item name="android:layout_width">0dip</item>
+ </style>
+
+ <style name="CheckmarkStyle">
+ <item name="android:layout_marginTop">-6sp</item>
+ <item name="android:layout_marginLeft">8dip</item>
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:layout_height">wrap_content</item>
+ </style>
+
+ <style name="StarStyle">
+ <item name="android:layout_marginRight">12dip</item>
+ <item name="android:layout_marginTop">-6sp</item>
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:layout_height">wrap_content</item>
+ </style>
+ <!-- End browse list item styles -->
</resources>
diff --git a/res/values/arrays.xml b/res/values/arrays.xml
new file mode 100644
index 0000000..b48769c
--- /dev/null
+++ b/res/values/arrays.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 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.
+-->
+<resources>
+ <integer-array name="conversation_heights">
+ <item>64</item>
+ <item>70</item>
+ </integer-array>
+ <integer-array name="senders_with_labels_and_attachment_lengths">
+ <item>20</item>
+ <item>20</item>
+ </integer-array>
+ <integer-array name="senders_with_labels_lengths">
+ <item>22</item>
+ <item>22</item>
+ </integer-array>
+ <integer-array name="senders_with_attachment_lengths">
+ <item>25</item>
+ <item>25</item>
+ </integer-array>
+ <integer-array name="senders_lengths">
+ <item>27</item>
+ <item>27</item>
+ </integer-array>
+ <!-- When no recent labels exist, pre-populate with these as defaults. Do not translate. -->
+ <string-array translatable="false" name="default_recent_labels">
+ <!-- Starred -->
+ <item>^t</item>
+ <!-- Sent -->
+ <item>^f</item>
+ <!-- Drafts -->
+ <item>^r</item>
+ </string-array>
+</resources>
\ No newline at end of file
diff --git a/res/values/colors.xml b/res/values/colors.xml
index 7c2bd78..f17a1fa 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -30,6 +30,11 @@
<color name="date_text_color_read">@color/senders_text_color_read</color>
<color name="label_list_heading_text_color">#777777</color>
<color name="actionbar_secondary">#777777</color>
+ <color name="subject_text_color">#333333</color>
+ <color name="faded_conversation_header">#7f99d6eb</color>
+ <color name="faded_activated_conversation_header">#7faccbf9</color>
+ <color name="drafts">#ff990000</color>
+ <color name="light_text_color">#ff666666</color>
<!-- Compose colors -->
<color name="compose_label_text">#aaaaaa</color>
diff --git a/res/values/constants.xml b/res/values/constants.xml
new file mode 100644
index 0000000..d6fc4c8
--- /dev/null
+++ b/res/values/constants.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2011 Google Inc.
+ * Licensed to 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.
+ */
+-->
+<resources>
+ <!-- Boolean value indicating whether the table UI should be used. -->
+ <integer name="use_tablet_ui">0</integer>
+ <integer name="conversation_list_header_mode">1</integer>
+ <integer name="conversation_header_mode">1</integer>
+</resources>
\ No newline at end of file
diff --git a/res/values/dimen.xml b/res/values/dimen.xml
index 17ecf65..09d0a89 100644
--- a/res/values/dimen.xml
+++ b/res/values/dimen.xml
@@ -28,4 +28,19 @@
<dimen name="account_dropdown_dropdownwidth">274dip</dimen>
<dimen name="actionbar_subject_padding_right">8dp</dimen>
<dimen name="compose_scrollview_width">700dp</dimen>
+ <dimen name="total_conversationbar_width">150dip</dimen>
+ <dimen name="max_total_label_width">64dip</dimen>
+ <dimen name="max_total_label_width_wide">64dip</dimen>
+ <dimen name="subject_width">0dip</dimen>
+ <dimen name="labels_font_size">12sp</dimen>
+ <dimen name="wide_senders_margin_top">0sp</dimen>
+ <dimen name="wide_attachment_margin_top">2sp</dimen>
+ <dimen name="wide_subject_margin_right">0dip</dimen>
+ <dimen name="wide_date_margin_top">2sp</dimen>
+ <dimen name="date_background_height">17sp</dimen>
+ <dimen name="date_background_padding_left">4dip</dimen>
+ <dimen name="touch_slop">16dip</dimen>
+ <dimen name="standard_scaled_dimen">100sp</dimen>
+ <dimen name="label_cell_width">8dip</dimen>
+ <dimen name="triangle_width">15dip</dimen>
</resources>
diff --git a/res/values/donottranslate.xml b/res/values/donottranslate.xml
new file mode 100644
index 0000000..71066f2
--- /dev/null
+++ b/res/values/donottranslate.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2011 Google Inc.
+ * Licensed to 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.
+ */
+-->
+
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- A random long string to be used in template XML files browse_item_view_*.xml -->
+ <string name="long_string">looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong</string>
+ <!-- A random date to be used in template XML files browse_item_view_*.xml -->
+ <string name="date">Aug 8, 2011</string>
+</resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 1e5b56c..3fb5ee8 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -115,8 +115,39 @@
<!-- Solicit feedback string in about screen [CHAR LIMIT=50]-->
<string name="contextmenu_feedback">Send feedback</string>
+ <!-- Browse list item strings -->
+ <!-- Displayed when drag and drop conversations "Move ? conversations" [CHAR LIMIT=50] -->
+ <plurals name="move_conversation">
+ <!-- Move 1 conversation -->
+ <item quantity="one">Move conversation</item>
+ <!-- Move several conversations -->
+ <item quantity="other">Move <xliff:g>%1$d</xliff:g> conversations</item>
+ </plurals>
+ <!-- Formatting string for the content description field of a conversation list item when device is in accessibility mode. [CHAR LIMI=250] -->
+ <string name="content_description">Subject: <xliff:g id="subject">%1$s</xliff:g> Snippet:<xliff:g id="snippet">%2$s</xliff:g></string>
+ <!-- Formatting string. If the subject contains the tag of a mailing-list (text surrounded with
+ return the subject with that tag ellipsized, e.g. "[android-gmail-team] Hello" -> "[andr...] Hello" [CHAR LIMIT=100] -->
+ <string name="filtered_tag"> [<xliff:g id="tag">%1$s</xliff:g>]<xliff:g id="subject">%2$s</xliff:g></string>
+ <!-- Displayed in Conversation Header View and Widget in the form of "subject - snippet"
+ [CHAR LIMIT=5] -->
+ <string name="subject_and_snippet"><xliff:g>%s</xliff:g> \u2014 <xliff:g>%s</xliff:g></string>
+ <!-- Displayed in browse list item when the list item is a draft message instead of showing the subject [CHAR LIMIT=100] -->
+ <plurals name="draft">
+ <!-- Title of the screen when there is exactly one draft -->
+ <item quantity="one">Draft</item>
+ <!-- Title of the screen when there are more than one draft -->
+ <item quantity="other">Drafts</item>
+ </plurals>
+ <!-- Message displayed in a browse list item for one second when message is being sent [CHAR LIMIT=20]-->
+ <string name="sending">Sending\u2026</string>
+ <!-- Message displayed in a browse list item for one second after a send failed [CHAR LIMIT=20]-->
+ <string name="send_failed">Message wasn\'t sent.</string>
+ <!-- Strings used to show myself in a To/Cc list. [CHAR LIMIT=15] -->
+ <string name="me">me</string>
+
<!-- Layout tests strings -->
<string name="test_compose" translate="false">Test Compose Layout</string>
<string name="test_accountspinner" translate="false">Test Account Spinner Layout</string>
<string name="test_labelspinner" translate="false">Test Label Spinner Layout</string>
+ <string name="test_browseitems" translate="false">Test Browse Items</string>
</resources>
\ No newline at end of file
diff --git a/res/values/styles.xml b/res/values/styles.xml
index 5a56e03..ed13de1 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -172,4 +172,43 @@
<style name="AccountSpinnerAnchorTextSecondary" parent="@android:style/TextAppearance.Holo.Widget.ActionBar.Subtitle">
<item name="android:textColor">@color/actionbar_secondary</item>
</style>
+
+ <!-- Browse list item styles -->
+ <style name="PersonalIndicatorStyle">
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:layout_marginLeft">8dip</item>
+ <item name="android:layout_marginTop">10sp</item>
+ </style>
+
+ <style name="SendersStyle">
+ <item name="android:layout_marginLeft">8dip</item>
+ <item name="android:layout_marginTop">8sp</item>
+ </style>
+
+ <style name="CheckmarkStyle">
+ <item name="android:layout_marginTop">-4sp</item>
+ <item name="android:layout_marginLeft">8dip</item>
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:layout_height">wrap_content</item>
+ </style>
+
+ <style name="StarStyle">
+ <item name="android:layout_marginRight">16dip</item>
+ <item name="android:layout_marginTop">-4sp</item>
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:layout_height">wrap_content</item>
+ </style>
+
+ <style name="SubjectStyle">
+ <item name="android:layout_marginLeft">8dip</item>
+ <item name="android:layout_marginTop">-4sp</item>
+ <item name="android:layout_weight">1</item>
+ <item name="android:textColor">@color/subject_text_color</item>
+ <item name="android:textSize">14sp</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:lines">2</item>
+ <item name="android:layout_width">0dip</item>
+ </style>
+ <!-- End browser list item styles -->
</resources>
diff --git a/src/com/android/email/UnifiedEmail.java b/src/com/android/email/UnifiedEmail.java
index a1fc200..316cab9 100644
--- a/src/com/android/email/UnifiedEmail.java
+++ b/src/com/android/email/UnifiedEmail.java
@@ -16,6 +16,7 @@
package com.android.email;
+import com.android.email.browse.BrowseListActivity;
import com.android.email.browse.LabelItem;
import com.android.email.compose.ComposeActivity;
@@ -49,4 +50,7 @@
public void composeTest(View v){
startActivityWithClass(ComposeActivity.class);
}
+ public void browseListItemTest(View v){
+ startActivityWithClass(BrowseListActivity.class);
+ }
}
\ No newline at end of file
diff --git a/src/com/android/email/ViewMode.java b/src/com/android/email/ViewMode.java
new file mode 100644
index 0000000..ca5bbb2
--- /dev/null
+++ b/src/com/android/email/ViewMode.java
@@ -0,0 +1,159 @@
+/**
+ * Copyright (c) 2011, Google Inc.
+ *
+ * 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.email;
+
+import com.android.email.utils.Utils;
+import com.google.common.collect.Lists;
+
+import android.content.Context;
+import android.os.Bundle;
+
+import java.util.ArrayList;
+
+
+/**
+ * Represents the view mode for the tablet Gmail activity.
+ * Transitions between modes should be done through this central object, and UI components that are
+ * dependent on the mode should listen to changes on this object.
+ */
+public class ViewMode {
+ // Key used to save this {@link ViewMode}.
+ private static final String VIEW_MODE_KEY = "view-mode";
+
+ public static final int MODE_UNKNOWN = 0;
+ public static final int MODE_LABEL_LIST = 1;
+ public static final int MODE_CONVERSATION_LIST = 2;
+ public static final int MODE_CONVERSATION = 3;
+
+ private int mMode = MODE_UNKNOWN;
+ private final boolean mTwoPane;
+ private final ArrayList<ModeChangeListener> mListeners = Lists.newArrayList();
+
+ public ViewMode(Context context) {
+ mTwoPane = Utils.useTabletUI(context);
+ }
+
+ /**
+ * Requests a transition of the mode to show a conversation as the prominent view.
+ * @return Whether or not a change occured.
+ */
+ public boolean transitionToConversationMode() {
+ return setModeInternal(MODE_CONVERSATION);
+ }
+
+ /**
+ * Requests a transition of the mode to show the conversation list as the prominent view.
+ * @return Whether or not a change occured.
+ */
+ public boolean transitionToConversationListMode() {
+ return setModeInternal(MODE_CONVERSATION_LIST);
+ }
+
+ /**
+ * Requests a transition of the mode to show the label list as the prominent view.
+ * @return Whether or not a change occured.
+ */
+ public boolean transitionToLabelListMode() {
+ return setModeInternal(MODE_LABEL_LIST);
+ }
+
+ /**
+ * Sets the internal mode.
+ * @return Whether or not a change occured.
+ */
+ private boolean setModeInternal(int mode) {
+ if (mMode == mode) {
+ return false;
+ }
+
+ mMode = mode;
+ dispatchModeChange();
+ return true;
+ }
+
+ /**
+ * @return The current mode.
+ */
+ public int getMode() {
+ return mMode;
+ }
+
+ /**
+ * @return Whether or not to display 2 pane.
+ */
+ public boolean isTwoPane() {
+ return mTwoPane;
+ }
+
+ public boolean isConversationMode() {
+ return mMode == MODE_CONVERSATION;
+ }
+
+ public boolean isConversationListMode() {
+ return mMode == MODE_CONVERSATION_LIST;
+ }
+
+ public boolean isLabelListMode() {
+ return mMode == MODE_LABEL_LIST;
+ }
+
+ public void handleSaveInstanceState(Bundle outState) {
+ outState.putInt(VIEW_MODE_KEY, mMode);
+ }
+
+ public void handleRestore(Bundle inState) {
+ int mode = inState.getInt(VIEW_MODE_KEY, MODE_UNKNOWN);
+ setModeInternal(mode);
+ }
+
+ /**
+ * A listener for changes on a ViewMode.
+ */
+ public interface ModeChangeListener {
+ /**
+ * Handles a mode change.
+ */
+ void onViewModeChanged(ViewMode mode);
+ }
+
+ /**
+ * Adds a listener from this view mode.
+ * Must happen in the UI thread.
+ */
+ public void addListener(ModeChangeListener listener) {
+ mListeners.add(listener);
+ }
+
+ /**
+ * Removes a listener from this view mode.
+ * Must happen in the UI thread.
+ */
+ public void removeListener(ModeChangeListener listener) {
+ mListeners.remove(listener);
+ }
+
+ /**
+ * Dispatches a change event for the mode.
+ * Always happens in the UI thread.
+ */
+ private void dispatchModeChange() {
+ ArrayList<ModeChangeListener> list = new ArrayList<ModeChangeListener>(mListeners);
+ for (ModeChangeListener listener : list) {
+ listener.onViewModeChanged(this);
+ }
+ }
+}
diff --git a/src/com/android/email/browse/BrowseItemView.java b/src/com/android/email/browse/BrowseItemView.java
new file mode 100644
index 0000000..2bc9bb6
--- /dev/null
+++ b/src/com/android/email/browse/BrowseItemView.java
@@ -0,0 +1,776 @@
+/**
+ * Copyright (c) 2011, Google Inc.
+ *
+ * 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.email.browse;
+
+import com.android.email.browse.BrowseItemViewModel.SenderFragment;
+import com.android.email.perf.Timer;
+import com.android.email.providers.UIProvider;
+import com.android.email.R;
+import com.android.email.ViewMode;
+import com.android.email.utils.Utils;
+import com.google.common.annotations.VisibleForTesting;
+
+import android.content.ClipData;
+import android.content.ClipData.Item;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.LinearGradient;
+import android.graphics.Paint;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.graphics.Shader;
+import android.graphics.Typeface;
+import android.graphics.drawable.Drawable;
+import android.text.Layout.Alignment;
+import android.text.Spannable;
+import android.text.SpannableStringBuilder;
+import android.text.StaticLayout;
+import android.text.TextPaint;
+import android.text.TextUtils;
+import android.text.TextUtils.TruncateAt;
+import android.text.format.DateUtils;
+import android.text.style.CharacterStyle;
+import android.text.style.ForegroundColorSpan;
+import android.text.style.StyleSpan;
+import android.util.SparseArray;
+import android.view.DragEvent;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.accessibility.AccessibilityEvent;
+
+import java.util.Map;
+
+public class BrowseItemView extends View {
+ // Timer.
+ private static int sLayoutCount = 0;
+ private static Timer sTimer; // Create the sTimer here if you need to do perf analysis.
+ private static final int PERF_LAYOUT_ITERATIONS = 50;
+ private static final String PERF_TAG_LAYOUT = "CCHV.layout";
+ private static final String PERF_TAG_CALCULATE_TEXTS_BITMAPS = "CCHV.txtsbmps";
+ private static final String PERF_TAG_CALCULATE_SENDER_SUBJECT = "CCHV.sendersubj";
+ private static final String PERF_TAG_CALCULATE_LABELS = "CCHV.labels";
+ private static final String PERF_TAG_CALCULATE_COORDINATES = "CCHV.coordinates";
+
+ // Static bitmaps.
+ private static Bitmap CHECKMARK_OFF;
+ private static Bitmap CHECKMARK_ON;
+ private static Bitmap STAR_OFF;
+ private static Bitmap STAR_ON;
+ private static Bitmap ATTACHMENT;
+ private static Bitmap ONLY_TO_ME;
+ private static Bitmap TO_ME_AND_OTHERS;
+ private static Bitmap IMPORTANT_ONLY_TO_ME;
+ private static Bitmap IMPORTANT_TO_ME_AND_OTHERS;
+ private static Bitmap IMPORTANT_TO_OTHERS;
+ private static Bitmap DATE_BACKGROUND;
+
+ // Static colors.
+ private static int DEFAULT_TEXT_COLOR;
+ private static int ACTIVATED_TEXT_COLOR;
+ private static int LIGHT_TEXT_COLOR;
+ private static int DRAFT_TEXT_COLOR;
+ private static int SUBJECT_TEXT_COLOR_READ;
+ private static int SUBJECT_TEXT_COLOR_UNREAD;
+ private static int SNIPPET_TEXT_COLOR_READ;
+ private static int SNIPPET_TEXT_COLOR_UNREAD;
+ private static int SENDERS_TEXT_COLOR_READ;
+ private static int SENDERS_TEXT_COLOR_UNREAD;
+ private static int DATE_TEXT_COLOR_READ;
+ private static int DATE_TEXT_COLOR_UNREAD;
+ private static int DATE_BACKGROUND_PADDING_LEFT;
+ private static int TOUCH_SLOP;
+ private static int sDateBackgroundHeight;
+ private static int sStandardScaledDimen;
+ private static CharacterStyle sLightTextStyle;
+ private static CharacterStyle sNormalTextStyle;
+
+ // Static paints.
+ private static TextPaint sPaint = new TextPaint();
+ private static TextPaint sLabelsPaint = new TextPaint();
+
+ // Backgrounds for different states.
+ private final SparseArray<Drawable> mBackgrounds = new SparseArray<Drawable>();
+
+ // Dimensions and coordinates.
+ private int mViewWidth = -1;
+ private int mMode = -1;
+ private int mDateX;
+ private int mPaperclipX;
+ private int mLabelsXEnd;
+ private int mSendersWidth;
+
+ /** Whether we're running under test mode. */
+ private boolean mTesting = false;
+
+ @VisibleForTesting
+ BrowseItemViewCoordinates mCoordinates;
+
+ // Current displayed label.
+ private CharSequence mDisplayedLabel;
+
+ private final Context mContext;
+ private static UIProvider sProvider;
+
+ private StarHandler mStarHandler;
+ private String mAccount;
+ private BrowseItemViewModel mHeader;
+ private ViewMode mViewMode;
+ private static int sFadedColor = -1;
+ private static int sFadedActivatedColor = -1;
+
+ static {
+ sPaint.setAntiAlias(true);
+ sLabelsPaint.setAntiAlias(true);
+ }
+
+ /**
+ * This handler will be called when user toggle a star in a conversation
+ * header view. It can be used to update the state of other views to ensure
+ * UI consistency.
+ */
+ public static interface StarHandler {
+ public void toggleStar(boolean toggleOn, long conversationId, long maxMessageId);
+ }
+
+ public BrowseItemView(Context context, String account) {
+ super(context);
+ mContext = context.getApplicationContext();
+ mAccount = account;
+ Resources res = mContext.getResources();
+
+ if (CHECKMARK_OFF == null) {
+ // Initialize static bitmaps.
+ CHECKMARK_OFF = BitmapFactory.decodeResource(res,
+ R.drawable.btn_check_off_normal_holo_light);
+ CHECKMARK_ON = BitmapFactory.decodeResource(res,
+ R.drawable.btn_check_on_normal_holo_light);
+ STAR_OFF = BitmapFactory.decodeResource(res,
+ R.drawable.btn_star_off_normal_email_holo_light);
+ STAR_ON = BitmapFactory.decodeResource(res,
+ R.drawable.btn_star_on_normal_email_holo_light);
+ ONLY_TO_ME = BitmapFactory.decodeResource(res, R.drawable.ic_email_caret_double);
+ TO_ME_AND_OTHERS = BitmapFactory.decodeResource(res, R.drawable.ic_email_caret_single);
+ IMPORTANT_ONLY_TO_ME = BitmapFactory.decodeResource(res,
+ R.drawable.ic_email_caret_double_important_unread);
+ IMPORTANT_TO_ME_AND_OTHERS = BitmapFactory.decodeResource(res,
+ R.drawable.ic_email_caret_single_important_unread);
+ IMPORTANT_TO_OTHERS = BitmapFactory.decodeResource(res,
+ R.drawable.ic_email_caret_none_important_unread);
+ ATTACHMENT = BitmapFactory.decodeResource(res, R.drawable.ic_attachment_holo_light);
+ DATE_BACKGROUND = BitmapFactory.decodeResource(res, R.drawable.label_bg_holo_light);
+
+ // Initialize colors.
+ DEFAULT_TEXT_COLOR = res.getColor(R.color.default_text_color);
+ ACTIVATED_TEXT_COLOR = res.getColor(android.R.color.white);
+ LIGHT_TEXT_COLOR = res.getColor(R.color.light_text_color);
+ DRAFT_TEXT_COLOR = res.getColor(R.color.drafts);
+ SUBJECT_TEXT_COLOR_READ = res.getColor(R.color.subject_text_color_read);
+ SUBJECT_TEXT_COLOR_UNREAD = res.getColor(R.color.subject_text_color_unread);
+ SNIPPET_TEXT_COLOR_READ = res.getColor(R.color.snippet_text_color_read);
+ SNIPPET_TEXT_COLOR_UNREAD = res.getColor(R.color.snippet_text_color_unread);
+ SENDERS_TEXT_COLOR_READ = res.getColor(R.color.senders_text_color_read);
+ SENDERS_TEXT_COLOR_UNREAD = res.getColor(R.color.senders_text_color_unread);
+ DATE_TEXT_COLOR_READ = res.getColor(R.color.date_text_color_read);
+ DATE_TEXT_COLOR_UNREAD = res.getColor(R.color.date_text_color_unread);
+ DATE_BACKGROUND_PADDING_LEFT = res
+ .getDimensionPixelSize(R.dimen.date_background_padding_left);
+ TOUCH_SLOP = res.getDimensionPixelSize(R.dimen.touch_slop);
+ sDateBackgroundHeight = res.getDimensionPixelSize(R.dimen.date_background_height);
+ sStandardScaledDimen = res.getDimensionPixelSize(R.dimen.standard_scaled_dimen);
+
+ // Initialize static color.
+ sNormalTextStyle = new StyleSpan(Typeface.NORMAL);
+ sLightTextStyle = new ForegroundColorSpan(LIGHT_TEXT_COLOR);
+
+ // Get a reference to the Gmail content provider mail access
+ sProvider = new UIProvider();
+ }
+ }
+
+ /**
+ * Bind this view to the content of the cursor and request layout.
+ */
+ public void bind(BrowseItemViewModel model, StarHandler starHandler, String account,
+ CharSequence displayedLabel, ViewMode viewMode) {
+ mStarHandler = starHandler;
+ mAccount = account;
+ mDisplayedLabel = displayedLabel;
+ mViewMode = viewMode;
+ mHeader = model;
+ setContentDescription(mHeader.getContentDescription(mContext));
+ requestLayout();
+ }
+
+ /**
+ * Sets the mode. Only used for testing.
+ */
+ @VisibleForTesting
+ void setMode(int mode) {
+ mMode = mode;
+ mTesting = true;
+ }
+
+ private static void startTimer(String tag) {
+ if (sTimer != null) {
+ sTimer.start(tag);
+ }
+ }
+
+ private static void pauseTimer(String tag) {
+ if (sTimer != null) {
+ sTimer.pause(tag);
+ }
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ startTimer(PERF_TAG_LAYOUT);
+
+ super.onLayout(changed, left, top, right, bottom);
+
+ int width = right - left;
+ if (width != mViewWidth) {
+ mViewWidth = width;
+ if (!mTesting) {
+ mMode = BrowseItemViewCoordinates.getMode(mContext, mViewMode);
+ }
+ }
+ mHeader.viewWidth = mViewWidth;
+ Resources res = getResources();
+ mHeader.standardScaledDimen = res.getDimensionPixelOffset(R.dimen.standard_scaled_dimen);
+ if (mHeader.standardScaledDimen != sStandardScaledDimen) {
+ // Large Text has been toggle on/off. Update the static dimens.
+ sStandardScaledDimen = mHeader.standardScaledDimen;
+ BrowseItemViewCoordinates.refreshConversationHeights(mContext);
+ sDateBackgroundHeight = res.getDimensionPixelSize(R.dimen.date_background_height);
+ }
+ mCoordinates = BrowseItemViewCoordinates.forWidth(mContext, mViewWidth, mMode,
+ mHeader.standardScaledDimen);
+ calculateTextsAndBitmaps();
+ calculateCoordinates();
+ mHeader.validate(mContext);
+
+ pauseTimer(PERF_TAG_LAYOUT);
+ if (sTimer != null && ++sLayoutCount >= PERF_LAYOUT_ITERATIONS) {
+ sTimer.dumpResults();
+ sTimer = new Timer();
+ sLayoutCount = 0;
+ }
+ }
+
+ @Override
+ public void setBackgroundResource(int resourceId) {
+ Drawable drawable = mBackgrounds.get(resourceId);
+ if (drawable == null) {
+ drawable = getResources().getDrawable(resourceId);
+ mBackgrounds.put(resourceId, drawable);
+ }
+ if (getBackground() != drawable) {
+ super.setBackgroundDrawable(drawable);
+ }
+ }
+
+ private void calculateTextsAndBitmaps() {
+ startTimer(PERF_TAG_CALCULATE_TEXTS_BITMAPS);
+
+ // Update font color.
+ int fontColor = getFontColor(DEFAULT_TEXT_COLOR);
+ boolean fontChanged = false;
+ if (mHeader.fontColor != fontColor) {
+ fontChanged = true;
+ mHeader.fontColor = fontColor;
+ }
+
+ boolean isUnread = true;
+
+ final boolean checkboxEnabled = true;
+ if (mHeader.checkboxVisible != checkboxEnabled) {
+ mHeader.checkboxVisible = checkboxEnabled;
+ }
+
+ // Update background.
+ updateBackground(isUnread);
+
+ if (mHeader.isLayoutValid(mContext)) {
+ // Relayout subject if font color has changed.
+ if (fontChanged) {
+ createSubjectSpans(isUnread);
+ layoutSubject();
+ }
+ pauseTimer(PERF_TAG_CALCULATE_TEXTS_BITMAPS);
+ return;
+ }
+
+ // Initialize label displayer.
+ startTimer(PERF_TAG_CALCULATE_LABELS);
+
+ pauseTimer(PERF_TAG_CALCULATE_LABELS);
+
+ // Star.
+ mHeader.starBitmap = mHeader.starred ? STAR_ON : STAR_OFF;
+
+ // Date.
+ mHeader.dateText = DateUtils.getRelativeTimeSpanString(mContext, mHeader.dateMs).toString();
+
+ // Paper clip icon.
+ mHeader.paperclip = null;
+ if (mHeader.hasAttachments) {
+ mHeader.paperclip = ATTACHMENT;
+ }
+
+ // Personal level.
+ mHeader.personalLevelBitmap = null;
+
+ startTimer(PERF_TAG_CALCULATE_SENDER_SUBJECT);
+
+ // Subject.
+ createSubjectSpans(isUnread);
+
+ // Parse senders fragments.
+ parseSendersFragments(isUnread);
+
+ pauseTimer(PERF_TAG_CALCULATE_SENDER_SUBJECT);
+ pauseTimer(PERF_TAG_CALCULATE_TEXTS_BITMAPS);
+ }
+
+ private void createSubjectSpans(boolean isUnread) {
+ String subject = filterTag(mHeader.subject);
+ String snippet = mHeader.snippet;
+ int subjectColor = isUnread ? SUBJECT_TEXT_COLOR_UNREAD : SUBJECT_TEXT_COLOR_READ;
+ int snippetColor = isUnread ? SNIPPET_TEXT_COLOR_UNREAD : SNIPPET_TEXT_COLOR_READ;
+ mHeader.subjectText = new SpannableStringBuilder(mContext.getString(
+ R.string.subject_and_snippet, subject, snippet));
+ if (isUnread) {
+ mHeader.subjectText.setSpan(new StyleSpan(Typeface.BOLD), 0, subject.length(),
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ int fontColor = getFontColor(subjectColor);
+ mHeader.subjectText.setSpan(new ForegroundColorSpan(fontColor), 0,
+ subject.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ fontColor = getFontColor(snippetColor);
+ mHeader.subjectText.setSpan(new ForegroundColorSpan(fontColor), subject.length() + 1,
+ mHeader.subjectText.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+
+ private int getFontColor(int defaultColor) {
+ return isActivated() && mViewMode.isTwoPane() ? ACTIVATED_TEXT_COLOR
+ : defaultColor;
+ }
+
+ private void layoutSubject() {
+ sPaint.setTextSize(mCoordinates.subjectFontSize);
+ sPaint.setColor(mHeader.fontColor);
+ mHeader.subjectLayout = new StaticLayout(mHeader.subjectText, sPaint,
+ mCoordinates.subjectWidth, Alignment.ALIGN_NORMAL, 1, 0, true);
+ if (mCoordinates.subjectLineCount < mHeader.subjectLayout.getLineCount()) {
+ int end = mHeader.subjectLayout.getLineEnd(mCoordinates.subjectLineCount - 1);
+ mHeader.subjectLayout = new StaticLayout(mHeader.subjectText.subSequence(0, end),
+ sPaint, mCoordinates.subjectWidth, Alignment.ALIGN_NORMAL, 1, 0, true);
+ }
+ }
+
+ /**
+ * Parses senders text into small fragments.
+ */
+ private void parseSendersFragments(boolean isUnread) {
+ SpannableStringBuilder sendersBuilder = new SpannableStringBuilder();
+ SpannableStringBuilder statusBuilder = new SpannableStringBuilder();
+ Utils.getStyledSenderSnippet(mContext, mHeader.fromSnippetInstructions, sendersBuilder,
+ statusBuilder, BrowseItemViewCoordinates.getSubjectLength(mContext, mMode,
+ false, mHeader.hasAttachments), false,
+ false, mHeader.hasDraftMessage);
+ mHeader.sendersText = sendersBuilder.toString();
+
+ CharacterStyle[] spans = sendersBuilder.getSpans(0, sendersBuilder.length(),
+ CharacterStyle.class);
+ mHeader.clearSenderFragments();
+ int lastPosition = 0;
+ CharacterStyle style = sNormalTextStyle;
+ if (spans != null) {
+ for (CharacterStyle span : spans) {
+ style = span;
+ int start = sendersBuilder.getSpanStart(style);
+ int end = sendersBuilder.getSpanEnd(style);
+ if (start > lastPosition) {
+ mHeader.addSenderFragment(lastPosition, start, sNormalTextStyle, false);
+ }
+ // From instructions won't be updated until the next sync. So we
+ // have to override the text style here to be consistent with
+ // the background color.
+ if (isUnread) {
+ mHeader.addSenderFragment(start, end, style, false);
+ } else {
+ mHeader.addSenderFragment(start, end, sNormalTextStyle, false);
+ }
+ lastPosition = end;
+ }
+ }
+ if (lastPosition < sendersBuilder.length()) {
+ style = sLightTextStyle;
+ mHeader.addSenderFragment(lastPosition, sendersBuilder.length(), style, true);
+ }
+ if (statusBuilder.length() > 0) {
+ if (mHeader.sendersText.length() > 0) {
+ mHeader.sendersText = mHeader.sendersText.concat(", ");
+
+ // Extend the last fragment to include the comma.
+ int lastIndex = mHeader.senderFragments.size() - 1;
+ int start = mHeader.senderFragments.get(lastIndex).start;
+ int end = mHeader.senderFragments.get(lastIndex).end + 2;
+ style = mHeader.senderFragments.get(lastIndex).style;
+
+ // The new fragment is only fixed if the previous fragment
+ // is fixed.
+ boolean isFixed = mHeader.senderFragments.get(lastIndex).isFixed;
+
+ // Remove the old fragment.
+ mHeader.senderFragments.remove(lastIndex);
+
+ // Add new fragment.
+ mHeader.addSenderFragment(start, end, style, isFixed);
+ }
+ int pos = mHeader.sendersText.length();
+ mHeader.sendersText = mHeader.sendersText.concat(statusBuilder.toString());
+ mHeader.addSenderFragment(pos, mHeader.sendersText.length(), new ForegroundColorSpan(
+ DRAFT_TEXT_COLOR), true);
+ }
+ }
+
+ private boolean canFitFragment(int width, int line, int fixedWidth) {
+ if (line == mCoordinates.sendersLineCount) {
+ return width + fixedWidth <= mSendersWidth;
+ } else {
+ return width <= mSendersWidth;
+ }
+ }
+
+ private void calculateCoordinates() {
+ startTimer(PERF_TAG_CALCULATE_COORDINATES);
+
+ sPaint.setTextSize(mCoordinates.dateFontSize);
+ sPaint.setTypeface(Typeface.DEFAULT);
+ mDateX = mCoordinates.dateXEnd - (int) sPaint.measureText(mHeader.dateText);
+
+ mPaperclipX = mDateX - ATTACHMENT.getWidth();
+
+ int cellWidth = mContext.getResources().getDimensionPixelSize(R.dimen.label_cell_width);
+
+ if (BrowseItemViewCoordinates.displayLabelsAboveDate(mMode)) {
+ mLabelsXEnd = mCoordinates.dateXEnd;
+ mSendersWidth = mCoordinates.sendersWidth;
+ } else {
+ if (mHeader.paperclip != null) {
+ mLabelsXEnd = mPaperclipX;
+ } else {
+ mLabelsXEnd = mDateX - cellWidth / 2;
+ }
+ mSendersWidth = mLabelsXEnd - mCoordinates.sendersX - 2 * cellWidth;
+ }
+
+ if (mHeader.isLayoutValid(mContext)) {
+ pauseTimer(PERF_TAG_CALCULATE_COORDINATES);
+ return;
+ }
+
+ // Layout subject.
+ layoutSubject();
+
+ // First pass to calculate width of each fragment.
+ int totalWidth = 0;
+ int fixedWidth = 0;
+ sPaint.setTextSize(mCoordinates.sendersFontSize);
+ sPaint.setTypeface(Typeface.DEFAULT);
+ for (SenderFragment senderFragment : mHeader.senderFragments) {
+ CharacterStyle style = senderFragment.style;
+ int start = senderFragment.start;
+ int end = senderFragment.end;
+ style.updateDrawState(sPaint);
+ senderFragment.width = (int) sPaint.measureText(mHeader.sendersText, start, end);
+ boolean isFixed = senderFragment.isFixed;
+ if (isFixed) {
+ fixedWidth += senderFragment.width;
+ }
+ totalWidth += senderFragment.width;
+ }
+
+ // Second pass to layout each fragment.
+ int sendersY = mCoordinates.sendersY - mCoordinates.sendersAscent;
+ if (!BrowseItemViewCoordinates.displaySendersInline(mMode)) {
+ sendersY += totalWidth <= mSendersWidth ? mCoordinates.sendersLineHeight / 2 : 0;
+ }
+ totalWidth = 0;
+ int currentLine = 1;
+ boolean ellipsize = false;
+ for (SenderFragment senderFragment : mHeader.senderFragments) {
+ CharacterStyle style = senderFragment.style;
+ int start = senderFragment.start;
+ int end = senderFragment.end;
+ int width = senderFragment.width;
+ boolean isFixed = senderFragment.isFixed;
+ style.updateDrawState(sPaint);
+
+ // No more width available, we'll only show fixed fragments.
+ if (ellipsize && !isFixed) {
+ senderFragment.shouldDisplay = false;
+ continue;
+ }
+
+ // New line and ellipsize text if needed.
+ senderFragment.ellipsizedText = null;
+ if (isFixed) {
+ fixedWidth -= width;
+ }
+ if (!canFitFragment(totalWidth + width, currentLine, fixedWidth)) {
+ // The text is too long, new line won't help. We have to
+ // ellipsize text.
+ if (totalWidth == 0) {
+ ellipsize = true;
+ } else {
+ // New line.
+ if (currentLine < mCoordinates.sendersLineCount) {
+ currentLine++;
+ sendersY += mCoordinates.sendersLineHeight;
+ totalWidth = 0;
+ // The text is still too long, we have to ellipsize
+ // text.
+ if (totalWidth + width > mSendersWidth) {
+ ellipsize = true;
+ }
+ } else {
+ ellipsize = true;
+ }
+ }
+
+ if (ellipsize) {
+ width = mSendersWidth - totalWidth;
+ // No more new line, we have to reserve width for fixed
+ // fragments.
+ if (currentLine == mCoordinates.sendersLineCount) {
+ width -= fixedWidth;
+ }
+ senderFragment.ellipsizedText = TextUtils.ellipsize(
+ mHeader.sendersText.substring(start, end), sPaint, width,
+ TruncateAt.END).toString();
+ width = (int) sPaint.measureText(senderFragment.ellipsizedText);
+ }
+ }
+ senderFragment.x = mCoordinates.sendersX + totalWidth;
+ senderFragment.y = sendersY;
+ senderFragment.shouldDisplay = true;
+ totalWidth += width;
+ }
+
+ pauseTimer(PERF_TAG_CALCULATE_COORDINATES);
+ }
+
+ /**
+ * If the subject contains the tag of a mailing-list (text surrounded with
+ * []), return the subject with that tag ellipsized, e.g.
+ * "[android-gmail-team] Hello" -> "[andr...] Hello"
+ */
+ private String filterTag(String subject) {
+ String result = subject;
+ String formatString = getContext().getResources().getString(R.string.filtered_tag);
+ if (!TextUtils.isEmpty(subject) && subject.charAt(0) == '[') {
+ int end = subject.indexOf(']');
+ if (end > 0) {
+ String tag = subject.substring(1, end);
+ result = String.format(formatString, Utils.ellipsize(tag, 7),
+ subject.substring(end + 1));
+ }
+ }
+ return result;
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int width = measureWidth(widthMeasureSpec);
+ int height = measureHeight(heightMeasureSpec,
+ BrowseItemViewCoordinates.getMode(mContext, mViewMode));
+ setMeasuredDimension(width, height);
+ }
+
+ /**
+ * Determine the width of this view.
+ *
+ * @param measureSpec A measureSpec packed into an int
+ * @return The width of the view, honoring constraints from measureSpec
+ */
+ private int measureWidth(int measureSpec) {
+ int result = 0;
+ int specMode = MeasureSpec.getMode(measureSpec);
+ int specSize = MeasureSpec.getSize(measureSpec);
+
+ if (specMode == MeasureSpec.EXACTLY) {
+ // We were told how big to be
+ result = specSize;
+ } else {
+ // Measure the text
+ result = mViewWidth;
+ if (specMode == MeasureSpec.AT_MOST) {
+ // Respect AT_MOST value if that was what is called for by
+ // measureSpec
+ result = Math.min(result, specSize);
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Determine the height of this view.
+ *
+ * @param measureSpec A measureSpec packed into an int
+ * @param mode The current mode of this view
+ * @return The height of the view, honoring constraints from measureSpec
+ */
+ private int measureHeight(int measureSpec, int mode) {
+ int result = 0;
+ int specMode = MeasureSpec.getMode(measureSpec);
+ int specSize = MeasureSpec.getSize(measureSpec);
+
+ if (specMode == MeasureSpec.EXACTLY) {
+ // We were told how big to be
+ result = specSize;
+ } else {
+ // Measure the text
+ result = BrowseItemViewCoordinates.getHeight(mContext, mode);
+ if (specMode == MeasureSpec.AT_MOST) {
+ // Respect AT_MOST value if that was what is called for by
+ // measureSpec
+ result = Math.min(result, specSize);
+ }
+ }
+ return result;
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ // Check mark.
+ if (mHeader.checkboxVisible) {
+ Bitmap checkmark = CHECKMARK_OFF;
+ canvas.drawBitmap(checkmark, mCoordinates.checkmarkX, mCoordinates.checkmarkY, sPaint);
+ }
+
+ // Personal Level.
+ if (mHeader.personalLevelBitmap != null) {
+ canvas.drawBitmap(mHeader.personalLevelBitmap, mCoordinates.personalLevelX,
+ mCoordinates.personalLevelY, sPaint);
+ }
+
+ // Senders.
+ sPaint.setTextSize(mCoordinates.sendersFontSize);
+ sPaint.setTypeface(Typeface.DEFAULT);
+ boolean isUnread = true;
+ int sendersColor = getFontColor(isUnread ? SENDERS_TEXT_COLOR_UNREAD
+ : SENDERS_TEXT_COLOR_READ);
+ sPaint.setColor(sendersColor);
+ for (SenderFragment fragment : mHeader.senderFragments) {
+ if (fragment.shouldDisplay) {
+ sPaint.setTypeface(Typeface.DEFAULT);
+ fragment.style.updateDrawState(sPaint);
+ if (fragment.ellipsizedText != null) {
+ canvas.drawText(fragment.ellipsizedText, fragment.x, fragment.y, sPaint);
+ } else {
+ canvas.drawText(mHeader.sendersText, fragment.start, fragment.end, fragment.x,
+ fragment.y, sPaint);
+ }
+ }
+ }
+
+ // Subject.
+ sPaint.setTextSize(mCoordinates.subjectFontSize);
+ sPaint.setTypeface(Typeface.DEFAULT);
+ sPaint.setColor(mHeader.fontColor);
+ canvas.save();
+ canvas.translate(mCoordinates.subjectX,
+ mCoordinates.subjectY + mHeader.subjectLayout.getTopPadding());
+ mHeader.subjectLayout.draw(canvas);
+ canvas.restore();
+
+ // Date background: shown when there is an attachment or a visible
+ // label.
+ if (!isActivated()
+ && mHeader.hasAttachments
+ && BrowseItemViewCoordinates.showAttachmentBackground(mMode)) {
+ mHeader.dateBackground = DATE_BACKGROUND;
+ int leftOffset = (mHeader.hasAttachments ? mPaperclipX : mDateX)
+ - DATE_BACKGROUND_PADDING_LEFT;
+ int top = mCoordinates.labelsY;
+ Rect src = new Rect(0, 0, mHeader.dateBackground.getWidth(), mHeader.dateBackground
+ .getHeight());
+ Rect dst = new Rect(leftOffset, top, mViewWidth, top + sDateBackgroundHeight);
+ canvas.drawBitmap(mHeader.dateBackground, src, dst, sPaint);
+ } else {
+ mHeader.dateBackground = null;
+ }
+
+ // Date.
+ sPaint.setTextSize(mCoordinates.dateFontSize);
+ sPaint.setTypeface(Typeface.DEFAULT);
+ sPaint.setColor(isUnread ? DATE_TEXT_COLOR_UNREAD : DATE_TEXT_COLOR_READ);
+ drawText(canvas, mHeader.dateText, mDateX, mCoordinates.dateY - mCoordinates.dateAscent,
+ sPaint);
+
+ // Paper clip icon.
+ if (mHeader.paperclip != null) {
+ canvas.drawBitmap(mHeader.paperclip, mPaperclipX, mCoordinates.paperclipY, sPaint);
+ }
+
+ if (mHeader.faded) {
+ int fadedColor = -1;
+ if (sFadedActivatedColor == -1) {
+ sFadedActivatedColor = mContext.getResources().getColor(
+ R.color.faded_activated_conversation_header);
+ }
+ fadedColor = sFadedActivatedColor;
+ int restoreState = canvas.save();
+ Rect bounds = canvas.getClipBounds();
+ canvas.clipRect(bounds.left, bounds.top, bounds.right
+ - mContext.getResources().getDimensionPixelSize(R.dimen.triangle_width),
+ bounds.bottom);
+ canvas.drawARGB(Color.alpha(fadedColor), Color.red(fadedColor),
+ Color.green(fadedColor), Color.blue(fadedColor));
+ canvas.restoreToCount(restoreState);
+ }
+
+ // Star.
+ canvas.drawBitmap(mHeader.starBitmap, mCoordinates.starX, mCoordinates.starY, sPaint);
+ }
+
+ private void drawText(Canvas canvas, CharSequence s, int x, int y, TextPaint paint) {
+ canvas.drawText(s, 0, s.length(), x, y, paint);
+ }
+
+ private void updateBackground(boolean isUnread) {
+ if (isUnread) {
+ if (mViewMode.isTwoPane() && mViewMode.isConversationListMode()) {
+ setBackgroundResource(R.drawable.conversation_wide_unread_selector);
+ } else {
+ setBackgroundResource(R.drawable.conversation_unread_selector);
+ }
+ } else {
+ if (mViewMode.isTwoPane() && mViewMode.isConversationListMode()) {
+ setBackgroundResource(R.drawable.conversation_wide_read_selector);
+ } else {
+ setBackgroundResource(R.drawable.conversation_read_selector);
+ }
+ }
+ }
+}
diff --git a/src/com/android/email/browse/BrowseItemViewCoordinates.java b/src/com/android/email/browse/BrowseItemViewCoordinates.java
new file mode 100644
index 0000000..3619bba
--- /dev/null
+++ b/src/com/android/email/browse/BrowseItemViewCoordinates.java
@@ -0,0 +1,341 @@
+// Copyright 2011 Google Inc. All Rights Reserved.
+
+package com.android.email.browse;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.text.TextPaint;
+import android.util.SparseArray;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.MeasureSpec;
+import android.view.ViewParent;
+import android.widget.TextView;
+import com.android.email.R;
+import com.android.email.ViewMode;
+import com.android.email.utils.Utils;
+
+/**
+ * Represents the coordinates of elements inside a CanvasConversationHeaderView
+ * (eg, checkmark, star, subject, sender, labels, etc.) It will inflate a view,
+ * and record the coordinates of each element after layout. This will allows us
+ * to easily improve performance by creating custom view while still defining
+ * layout in XML files.
+ *
+ * @author phamm
+ */
+public class BrowseItemViewCoordinates {
+ // Modes.
+ private static final int WIDE_MODE = 0;
+ private static final int NORMAL_MODE = 1;
+
+ // Static threshold.
+ private static int TOTAL_LABEL_WIDTH = -1;
+ private static int TOTAL_LABEL_WIDTH_WIDE = -1;
+ private static int LABEL_CELL_WIDTH = -1;
+ private static int sConversationHeights[];
+
+ // Checkmark.
+ int checkmarkX;
+ int checkmarkY;
+
+ // Star.
+ int starX;
+ int starY;
+
+ // Personal level.
+ int personalLevelX;
+ int personalLevelY;
+
+ // Senders.
+ int sendersX;
+ int sendersY;
+ int sendersWidth;
+ int sendersLineCount;
+ int sendersLineHeight;
+ int sendersFontSize;
+ int sendersAscent;
+
+ // Subject.
+ int subjectX;
+ int subjectY;
+ int subjectWidth;
+ int subjectLineCount;
+ int subjectFontSize;
+ int subjectAscent;
+
+ // Labels.
+ int labelsXEnd;
+ int labelsY;
+ int labelsHeight;
+ int labelsTopPadding;
+ int labelsFontSize;
+ int labelsAscent;
+
+ // Date.
+ int dateXEnd;
+ int dateY;
+ int dateFontSize;
+ int dateAscent;
+
+ // Paperclip.
+ int paperclipY;
+
+ // Cache to save Coordinates based on view width.
+ private static SparseArray<BrowseItemViewCoordinates> mCache =
+ new SparseArray<BrowseItemViewCoordinates>();
+
+ private static TextPaint sPaint = new TextPaint();
+
+ static {
+ sPaint.setAntiAlias(true);
+ }
+
+ /**
+ * Returns whether to show a background on the attachment icon.
+ * Currently, we don't show a background in wide mode.
+ */
+ public static boolean showAttachmentBackground(int mode) {
+ return mode != WIDE_MODE;
+ }
+
+ /**
+ * Returns the mode of the header view (Wide/Normal/Narrow).
+ */
+ public static int getMode(Context context, ViewMode viewMode) {
+ Resources res = context.getResources();
+ return viewMode.isConversationListMode() ? res
+ .getInteger(R.integer.conversation_list_header_mode) : res
+ .getInteger(R.integer.conversation_header_mode);
+ }
+
+ /**
+ * Returns the layout id to be inflated in this mode.
+ */
+ private static int getLayoutId(int mode) {
+ switch (mode) {
+ case WIDE_MODE:
+ return R.layout.browse_item_view_wide;
+ case NORMAL_MODE:
+ return R.layout.browse_item_view_normal;
+ default:
+ throw new IllegalArgumentException("Unknown conversation header view mode " + mode);
+ }
+ }
+
+ /**
+ * Returns a value array multiplied by the specified density.
+ */
+ public static int[] getDensityDependentArray(int[] values, float density) {
+ int result[] = new int[values.length];
+ for (int i = 0; i < values.length; ++i) {
+ result[i] = (int) (values[i] * density);
+ }
+ return result;
+ }
+
+ /**
+ * Returns the height of the view in this mode.
+ */
+ public static int getHeight(Context context, int mode) {
+ Resources res = context.getResources();
+ float density = res.getDisplayMetrics().scaledDensity;
+ if (sConversationHeights == null) {
+ sConversationHeights = getDensityDependentArray(
+ res.getIntArray(R.array.conversation_heights), density);
+ }
+ return sConversationHeights[mode];
+ }
+
+ /**
+ * Refreshes the conversation heights array.
+ */
+ public static void refreshConversationHeights(Context context) {
+ Resources res = context.getResources();
+ float density = res.getDisplayMetrics().scaledDensity;
+ sConversationHeights = getDensityDependentArray(
+ res.getIntArray(R.array.conversation_heights), density);
+ }
+
+ /**
+ * Returns the x coordinates of a view by tracing up its hierarchy.
+ */
+ private static int getX(View view) {
+ int x = 0;
+ while (view != null) {
+ x += (int) view.getX();
+ ViewParent parent = view.getParent();
+ view = parent != null ? (View) parent : null;
+ }
+ return x;
+ }
+
+ /**
+ * Returns the y coordinates of a view by tracing up its hierarchy.
+ */
+ private static int getY(View view) {
+ int y = 0;
+ while (view != null) {
+ y += (int) view.getY();
+ ViewParent parent = view.getParent();
+ view = parent != null ? (View) parent : null;
+ }
+ return y;
+ }
+
+ /**
+ * Returns the number of lines of this text view.
+ */
+ private static int getLineCount(TextView textView) {
+ return Math.round(((float) textView.getHeight()) / textView.getLineHeight());
+ }
+
+ /**
+ * Returns the length (maximum of characters) of subject in this mode.
+ */
+ public static int getSubjectLength(Context context, int mode, boolean hasVisibleLabels,
+ boolean hasAttachments) {
+ if (hasVisibleLabels) {
+ if (hasAttachments) {
+ return context.getResources().getIntArray(
+ R.array.senders_with_labels_and_attachment_lengths)[mode];
+ } else {
+ return context.getResources().getIntArray(
+ R.array.senders_with_labels_lengths)[mode];
+ }
+ } else {
+ if (hasAttachments) {
+ return context.getResources().getIntArray(
+ R.array.senders_with_attachment_lengths)[mode];
+ } else {
+ return context.getResources().getIntArray(R.array.senders_lengths)[mode];
+ }
+ }
+ }
+
+ /**
+ * Returns the width available to draw labels in this mode.
+ */
+ public static int getLabelsWidth(Context context, int mode) {
+ Resources res = context.getResources();
+ if (TOTAL_LABEL_WIDTH <= 0) {
+ TOTAL_LABEL_WIDTH = res.getDimensionPixelSize(R.dimen.max_total_label_width);
+ TOTAL_LABEL_WIDTH_WIDE = res.getDimensionPixelSize(R.dimen.max_total_label_width_wide);
+ }
+ switch (mode) {
+ case WIDE_MODE:
+ return TOTAL_LABEL_WIDTH_WIDE;
+ case NORMAL_MODE:
+ return TOTAL_LABEL_WIDTH;
+ default:
+ throw new IllegalArgumentException("Unknown conversation header view mode " + mode);
+ }
+ }
+
+ /**
+ * Returns the width of a cell to draw labels.
+ */
+ public static int getLabelCellWidth(Context context, int mode, int labelsCount) {
+ Resources res = context.getResources();
+ if (LABEL_CELL_WIDTH <= 0) {
+ LABEL_CELL_WIDTH = res.getDimensionPixelSize(R.dimen.label_cell_width);
+ }
+ switch (mode) {
+ case WIDE_MODE:
+ case NORMAL_MODE:
+ return LABEL_CELL_WIDTH;
+ default:
+ throw new IllegalArgumentException("Unknown conversation header view mode " + mode);
+ }
+ }
+
+ public static boolean displaySendersInline(int mode) {
+ switch (mode) {
+ case WIDE_MODE:
+ return false;
+ case NORMAL_MODE:
+ return true;
+ default:
+ throw new IllegalArgumentException("Unknown conversation header view mode " + mode);
+ }
+ }
+
+ /**
+ * Returns coordinates for elements inside a conversation header view given
+ * the view width.
+ */
+ public static BrowseItemViewCoordinates forWidth(Context context, int width, int mode,
+ int standardScaledDimen) {
+ BrowseItemViewCoordinates coordinates = mCache.get(width ^ standardScaledDimen);
+ if (coordinates == null) {
+ coordinates = new BrowseItemViewCoordinates();
+ mCache.put(width ^ standardScaledDimen, coordinates);
+
+ // Layout the appropriate view.
+ int height = getHeight(context, mode);
+ View view = LayoutInflater.from(context).inflate(getLayoutId(mode), null);
+ int widthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);
+ int heightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
+ view.measure(widthSpec, heightSpec);
+ view.layout(0, 0, width, height);
+
+ // Records coordinates.
+ View checkmark = view.findViewById(R.id.checkmark);
+ coordinates.checkmarkX = getX(checkmark);
+ coordinates.checkmarkY = getY(checkmark);
+
+ View star = view.findViewById(R.id.star);
+ coordinates.starX = getX(star);
+ coordinates.starY = getY(star);
+
+ View personalLevel = view.findViewById(R.id.personal_level);
+ coordinates.personalLevelX = getX(personalLevel);
+ coordinates.personalLevelY = getY(personalLevel);
+
+ TextView senders = (TextView) view.findViewById(R.id.senders);
+ coordinates.sendersX = getX(senders);
+ coordinates.sendersY = getY(senders);
+ coordinates.sendersWidth = senders.getWidth();
+ coordinates.sendersLineCount = getLineCount(senders);
+ coordinates.sendersLineHeight = senders.getLineHeight();
+ coordinates.sendersFontSize = (int) senders.getTextSize();
+ sPaint.setTextSize(coordinates.sendersFontSize);
+ coordinates.sendersAscent = (int) sPaint.ascent();
+
+ TextView subject = (TextView) view.findViewById(R.id.subject);
+ coordinates.subjectX = getX(subject);
+ coordinates.subjectY = getY(subject);
+ coordinates.subjectWidth = subject.getWidth();
+ coordinates.subjectLineCount = getLineCount(subject);
+ coordinates.subjectFontSize = (int) subject.getTextSize();
+ sPaint.setTextSize(coordinates.subjectFontSize);
+ coordinates.subjectAscent = (int) sPaint.ascent();
+
+ View labels = view.findViewById(R.id.labels);
+ coordinates.labelsXEnd = getX(labels) + labels.getWidth();
+ coordinates.labelsY = getY(labels);
+ coordinates.labelsHeight = labels.getHeight();
+ coordinates.labelsTopPadding = labels.getPaddingTop();
+ if (labels instanceof TextView) {
+ coordinates.labelsFontSize = (int) ((TextView) labels).getTextSize();
+ sPaint.setTextSize(coordinates.labelsFontSize);
+ coordinates.labelsAscent = (int) sPaint.ascent();
+ }
+
+ TextView date = (TextView) view.findViewById(R.id.date);
+ coordinates.dateXEnd = getX(date) + date.getWidth();
+ coordinates.dateY = getY(date);
+ coordinates.dateFontSize = (int) date.getTextSize();
+ sPaint.setTextSize(coordinates.dateFontSize);
+ coordinates.dateAscent = (int) sPaint.ascent();
+
+ View paperclip = view.findViewById(R.id.paperclip);
+ coordinates.paperclipY = getY(paperclip);
+ }
+ return coordinates;
+ }
+
+ public static boolean displayLabelsAboveDate(int mode) {
+ return mode == WIDE_MODE;
+ }
+}
diff --git a/src/com/android/email/browse/BrowseItemViewModel.java b/src/com/android/email/browse/BrowseItemViewModel.java
new file mode 100644
index 0000000..cfc86f8
--- /dev/null
+++ b/src/com/android/email/browse/BrowseItemViewModel.java
@@ -0,0 +1,250 @@
+/**
+ * Copyright (c) 2011, Google Inc.
+ *
+ * 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.email.browse;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.Lists;
+import com.android.email.R;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.text.SpannableStringBuilder;
+import android.text.StaticLayout;
+import android.text.style.CharacterStyle;
+import android.util.LruCache;
+import android.util.Pair;
+
+import java.util.ArrayList;
+import java.util.Map;
+
+/**
+ * This is the view model for the conversation header. It includes all the
+ * information needed to layout a conversation header view. Each view model is
+ * associated with a conversation and is cached to improve the relayout time.
+ */
+public class BrowseItemViewModel {
+ private static final int MAX_CACHE_SIZE = 100;
+
+ boolean faded = false;
+ int fontColor;
+ @VisibleForTesting
+ static LruCache<Pair<String, Long>, BrowseItemViewModel> sConversationHeaderMap
+ = new LruCache<Pair<String, Long>, BrowseItemViewModel>(MAX_CACHE_SIZE);
+
+ // The hashcode used to detect if the conversation has changed.
+ private int mDataHashCode;
+ private int mLayoutHashCode;
+
+ // Star
+ boolean starred;
+
+ Bitmap starBitmap;
+
+ // Date
+ String dateText;
+ Bitmap dateBackground;
+
+ // Personal level
+ Bitmap personalLevelBitmap;
+
+ // Paperclip
+ Bitmap paperclip;
+
+ // Senders
+ String sendersText;
+
+ // A list of all the fragments that cover sendersText
+ final ArrayList<SenderFragment> senderFragments;
+
+ boolean hasAttachments;
+
+ boolean hasDraftMessage;
+
+ // Subject
+ SpannableStringBuilder subjectText;
+
+ StaticLayout subjectLayout;
+
+ // View Width
+ public int viewWidth;
+
+ // Standard scaled dimen used to detect if the scale of text has changed.
+ public int standardScaledDimen;
+
+ public long dateMs;;
+
+ public String subject;
+
+ public String snippet;
+
+ public String fromSnippetInstructions;
+
+ public long conversationId;
+
+ public long maxMessageId;
+
+ public boolean checkboxVisible;
+
+
+ /**
+ * Returns the view model for a conversation. If the model doesn't exist for this conversation
+ * null is returned. Note: this should only be called from the UI thread.
+ *
+ * @param account the account contains this conversation
+ * @param conversationId the Id of this conversation
+ * @return the view model for this conversation, or null
+ */
+ @VisibleForTesting
+ static BrowseItemViewModel forConversationIdOrNull(
+ String account, long conversationId) {
+ final Pair<String, Long> key = new Pair<String, Long>(account, conversationId);
+ synchronized(sConversationHeaderMap) {
+ return sConversationHeaderMap.get(key);
+ }
+ }
+
+ /**
+ * Returns the view model for a conversation. If this is the first time
+ * call, a new view model will be returned. Note: this should only be called
+ * from the UI thread.
+ *
+ * @param account the account contains this conversation
+ * @param conversationId the Id of this conversation
+ * @param cursor the cursor to use in populating/ updating the model.
+ * @return the view model for this conversation
+ */
+ static BrowseItemViewModel forConversationId(String account, long conversationId) {
+ synchronized(sConversationHeaderMap) {
+ BrowseItemViewModel header =
+ forConversationIdOrNull(account, conversationId);
+ if (header == null) {
+ final Pair<String, Long> key = new Pair<String, Long>(account, conversationId);
+ header = new BrowseItemViewModel();
+ sConversationHeaderMap.put(key, header);
+ }
+ return header;
+ }
+ }
+
+ public BrowseItemViewModel() {
+ senderFragments = Lists.newArrayList();
+ }
+
+ /**
+ * Adds a sender fragment.
+ *
+ * @param start the start position of this fragment
+ * @param end the start position of this fragment
+ * @param style the style of this fragment
+ * @param isFixed whether this fragment is fixed or not
+ */
+ void addSenderFragment(int start, int end, CharacterStyle style, boolean isFixed) {
+ SenderFragment senderFragment = new SenderFragment(start, end, sendersText, style, isFixed);
+ senderFragments.add(senderFragment);
+ }
+
+ /**
+ * Clears all the current sender fragments.
+ */
+ void clearSenderFragments() {
+ senderFragments.clear();
+ }
+
+ /**
+ * Returns the hashcode to compare if the data in the header is valid.
+ */
+ private static int getHashCode(Context context, String dateText, String fromSnippetInstructions) {
+ if (dateText == null) {
+ return -1;
+ }
+ return fromSnippetInstructions.hashCode() ^ dateText.hashCode();
+ }
+
+ /**
+ * Returns the layout hashcode to compare to see if thet layout state has changed.
+ */
+ private int getLayoutHashCode() {
+ return mDataHashCode ^ viewWidth ^ standardScaledDimen ^
+ Boolean.valueOf(checkboxVisible).hashCode();
+ }
+
+ /**
+ * Marks this header as having valid data and layout.
+ */
+ void validate(Context context) {
+ mDataHashCode = getHashCode(context, dateText, fromSnippetInstructions);
+ mLayoutHashCode = getLayoutHashCode();
+ }
+
+ /**
+ * Returns if the data in this model is valid.
+ */
+ boolean isDataValid(Context context) {
+ return mDataHashCode == getHashCode(context, dateText, fromSnippetInstructions);
+ }
+
+ /**
+ * Returns if the layout in this model is valid.
+ */
+ boolean isLayoutValid(Context context) {
+ return isDataValid(context) && mLayoutHashCode == getLayoutHashCode();
+ }
+
+ /**
+ * Describes the style of a Senders fragment.
+ */
+ static class SenderFragment {
+ // Coordinate where the text to be drawn.
+ int x;
+ int y;
+
+ // Indices that determine which substring of mSendersText we are
+ // displaying.
+ int start;
+ int end;
+
+ // The style to apply to the TextPaint object.
+ CharacterStyle style;
+
+ // Width of the fragment.
+ int width;
+
+ // Ellipsized text.
+ String ellipsizedText;
+
+ // Whether the fragment is fixed or not.
+ boolean isFixed;
+
+ // Should the fragment be displayed or not.
+ boolean shouldDisplay;
+
+ SenderFragment(int start, int end, CharSequence sendersText, CharacterStyle style,
+ boolean isFixed) {
+ this.start = start;
+ this.end = end;
+ this.style = style;
+ this.isFixed = isFixed;
+ }
+ }
+
+ /**
+ * Get conversation information to use for accessibility.
+ */
+ public CharSequence getContentDescription(Context context) {
+ return context.getString(R.string.content_description, subject, snippet);
+ }
+}
diff --git a/src/com/android/email/browse/BrowseListActivity.java b/src/com/android/email/browse/BrowseListActivity.java
new file mode 100644
index 0000000..5e0a11b
--- /dev/null
+++ b/src/com/android/email/browse/BrowseListActivity.java
@@ -0,0 +1,77 @@
+package com.android.email.browse;
+
+import android.app.Activity;
+import android.content.Context;
+import android.os.Bundle;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.ListView;
+
+import com.android.email.R;
+import com.android.email.ViewMode;
+
+import java.util.ArrayList;
+
+public class BrowseListActivity extends Activity {
+
+ private ListView mListView;
+ private BrowseItemAdapter mAdapter;
+ private ArrayList<BrowseItemViewModel> mTestBrowseItems = new ArrayList<BrowseItemViewModel>();
+
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.browse_list_activity);
+ mListView = (ListView) findViewById(R.id.browse_list);
+ mAdapter = new BrowseItemAdapter(this, R.layout.browse_item_view_normal);
+ BrowseItemViewModel itemOne = new BrowseItemViewModel();
+ itemOne.subject = "First";
+ itemOne.sendersText = "Mindy, Andy, Paul, Minh";
+ itemOne.conversationId = 1;
+ itemOne.snippet = "first snippet";
+ itemOne.fromSnippetInstructions = "schmindor@gmail.com";
+ mTestBrowseItems.add(itemOne);
+ mTestBrowseItems.add(itemOne);
+ BrowseItemViewModel itemTwo = new BrowseItemViewModel();
+ itemTwo.subject = "Second";
+ itemTwo.sendersText = "Mindy, Andy, Paul, Minh";
+ itemTwo.conversationId = 2;
+ itemTwo.snippet = "second snippet";
+ itemTwo.fromSnippetInstructions = "schmindor@gmail.com";
+ mTestBrowseItems.add(itemTwo);
+ mTestBrowseItems.add(itemTwo);
+ BrowseItemViewModel itemThree = new BrowseItemViewModel();
+ itemThree.subject = "Third";
+ itemThree.sendersText = "Mindy, Andy, Paul, Minh";
+ itemThree.conversationId = 3;
+ itemThree.snippet = "third snippet";
+ itemThree.fromSnippetInstructions = "schmindor@gmail.com";
+ mTestBrowseItems.add(itemThree);
+ mTestBrowseItems.add(itemThree);
+ BrowseItemViewModel itemFour = new BrowseItemViewModel();
+ itemFour.subject = "Fourth";
+ itemFour.sendersText = "Mindy, Andy, Paul, Minh";
+ itemFour.conversationId = 4;
+ itemFour.fromSnippetInstructions = "schmindor@gmail.com";
+ itemFour.snippet = "fourth snippet";
+ mTestBrowseItems.add(itemFour);
+ mTestBrowseItems.add(itemFour);
+ mAdapter.addAll(mTestBrowseItems);
+
+ mListView.setAdapter(mAdapter);
+ }
+
+ class BrowseItemAdapter extends ArrayAdapter<BrowseItemViewModel> {
+
+ public BrowseItemAdapter(Context context, int textViewResourceId) {
+ super(context, textViewResourceId);
+ }
+
+ public View getView(int position, View convertView, ViewGroup parent) {
+ BrowseItemView view = new BrowseItemView(getContext(), "test@testaccount.com");
+ view.bind(mAdapter.getItem(position), null, "test@testaccount.com", null,
+ new ViewMode(getContext()));
+ return view;
+ }
+ }
+}
diff --git a/src/com/android/email/compose/AttachmentComposeView.java b/src/com/android/email/compose/AttachmentComposeView.java
index a376cd9..87dc4da 100644
--- a/src/com/android/email/compose/AttachmentComposeView.java
+++ b/src/com/android/email/compose/AttachmentComposeView.java
@@ -35,16 +35,17 @@
class AttachmentComposeView extends LinearLayout {
private final long mSize;
private final String mFilename;
+ private final static String LOG_TAG = new LogUtils().getLogTag();
public AttachmentComposeView(Context c, Attachment attachment) {
super(c);
mFilename = attachment.getName();
mSize = attachment.getSize();
- LogUtils.d(Utils.LOG_TAG, ">>>>> Attachment uri: %s", attachment.getOriginExtras());
- LogUtils.d(Utils.LOG_TAG, ">>>>> type: %s", attachment.getContentType());
- LogUtils.d(Utils.LOG_TAG, ">>>>> name: %s", mFilename);
- LogUtils.d(Utils.LOG_TAG, ">>>>> size: %d", mSize);
+ LogUtils.d(LOG_TAG, ">>>>> Attachment uri: %s", attachment.getOriginExtras());
+ LogUtils.d(LOG_TAG, ">>>>> type: %s", attachment.getContentType());
+ LogUtils.d(LOG_TAG, ">>>>> name: %s", mFilename);
+ LogUtils.d(LOG_TAG, ">>>>> size: %d", mSize);
LayoutInflater factory = LayoutInflater.from(getContext());
diff --git a/src/com/android/email/perf/EventLogTags.logtags b/src/com/android/email/perf/EventLogTags.logtags
new file mode 100644
index 0000000..0e5efbc
--- /dev/null
+++ b/src/com/android/email/perf/EventLogTags.logtags
@@ -0,0 +1,9 @@
+# See system/core/logcat/event.logtags for a description of the format of this file.
+
+option java_package com.google.android.gm.perf
+
+# Gmail performance
+# @param action the gmail action being timed
+# @param cpu_time the number of cpu ms for this action
+# @param wall_time the SystemClock.uptimeMillis delta for this action
+206002 gmail_perf_end (action|3),(cpu_time|1|3),(wall_time|1|3),(num_sub_iteration|1)
diff --git a/src/com/android/email/perf/SimpleTimer.java b/src/com/android/email/perf/SimpleTimer.java
new file mode 100644
index 0000000..d53c302
--- /dev/null
+++ b/src/com/android/email/perf/SimpleTimer.java
@@ -0,0 +1,62 @@
+// Copyright 2011 Google Inc. All Rights Reserved.
+
+package com.android.email.perf;
+
+import android.os.SystemClock;
+
+import com.android.email.utils.LogUtils;
+import com.android.email.utils.Utils;
+
+/**
+ * A simple perf timer class that supports lap-time-style measurements. Once a timer is started,
+ * any number of laps can be marked, but they are all relative to the original start time.
+ *
+ */
+public class SimpleTimer {
+
+ private static final boolean ENABLE_SIMPLE_TIMER = true;
+ private static final String LOG_TAG = new LogUtils().getLogTag();
+
+ private final boolean mEnabled;
+ private long mStartTime;
+ private String mSessionName;
+
+ public SimpleTimer() {
+ this(false);
+ }
+
+ public SimpleTimer(boolean enabled) {
+ mEnabled = enabled;
+ }
+
+ public boolean isEnabled() {
+ return ENABLE_SIMPLE_TIMER && LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)
+ && mEnabled;
+ }
+
+ public void start() {
+ start(null);
+ }
+
+ public void start(String sessionName) {
+ mStartTime = SystemClock.uptimeMillis();
+ mSessionName = sessionName;
+ }
+
+ public void mark(String msg) {
+ if (isEnabled()) {
+ StringBuilder sb = new StringBuilder();
+ if (mSessionName != null) {
+ sb.append("(");
+ sb.append(mSessionName);
+ sb.append(") ");
+ }
+ sb.append(msg);
+ sb.append(": ");
+ sb.append(SystemClock.uptimeMillis() - mStartTime);
+ sb.append("ms elapsed");
+ LogUtils.d(LOG_TAG, sb.toString());
+ }
+ }
+
+}
diff --git a/src/com/android/email/perf/Timer.java b/src/com/android/email/perf/Timer.java
new file mode 100644
index 0000000..1cdd31b
--- /dev/null
+++ b/src/com/android/email/perf/Timer.java
@@ -0,0 +1,231 @@
+// Copyright 2010 Google Inc. All Rights Reserved.
+
+package com.android.email.perf;
+
+import com.google.common.collect.Maps;
+import com.android.email.utils.LogUtils;
+import com.android.email.utils.Utils;
+
+import android.os.Debug;
+import android.os.SystemClock;
+
+import java.util.ArrayList;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Performance timing utilities for Gmail.
+ *
+ * There are two main ways to log performance. For simple, one off events, the static methods
+ * {@link #startTiming} and {@link #stopTiming} are sufficient:
+ *
+ * <pre>
+ * Timer.startTiming("myEvent");
+ * ... code for myEvent ...
+ * Timer.stopTiming("myEvent");
+ * </pre>
+ *
+ * The other way is to instantiate a timer that can be passed around, and started and paused. The
+ * timer will accumulate the results for each tag, and dump the results when asked.
+ *
+ * <pre>
+ * Timer timer = new Timer();
+ *
+ * for (int i = 0; i < lots; i++) {
+ * timer.start("tagA");
+ * ... code for tagA ...
+ * timer.pause("tagA");
+ * ... code that isn't relevant to timing ...
+ * }
+ *
+ * timer.dumpResults();
+ * </pre>
+ *
+ */
+public class Timer {
+ private static final String LOG_TAG = new LogUtils().getLogTag();
+
+ // set this to false to compile out all timer calls
+ public static final boolean ENABLE_TIMER = false;
+ // Set to true to enable logging of performance stats.
+ private static final boolean LOG_PERFORMANCE_STATS = true;
+
+ /** An internal structure used for performance markers. */
+ private static class PerformancePoint {
+ public final long mCpuTimeNanos;
+ public final long mWallTimeMillis;
+
+ public PerformancePoint() {
+ mCpuTimeNanos = Debug.threadCpuTimeNanos();
+ mWallTimeMillis = SystemClock.uptimeMillis();
+ }
+ }
+
+ private final Map<String, ArrayList<PerformancePoint>> mPoints = Maps.newHashMap();
+ private final Map<String, Integer> mCounts = Maps.newHashMap();
+ private final boolean mEnabled;
+
+ public Timer() {
+ this(false);
+ }
+
+ public Timer(boolean enable) {
+ mEnabled = enable;
+ }
+
+ @SuppressWarnings("unused")
+ public boolean isEnabled() {
+ return ENABLE_TIMER && mEnabled;
+ }
+
+ /**
+ * Starts timing an event indicated by the {@code tag}.
+ */
+ @SuppressWarnings("unused")
+ public void start(String tag) {
+ if (ENABLE_TIMER && mEnabled) {
+ ArrayList<PerformancePoint> values = mPoints.get(tag);
+ if (values == null) {
+ values = new ArrayList<PerformancePoint>();
+ mPoints.put(tag, values);
+ }
+ if (values.size() % 2 == 0) {
+ values.add(new PerformancePoint());
+ }
+ }
+ }
+
+ /**
+ * Stops timing an event indicated by the {@code tag}
+ */
+ @SuppressWarnings("unused")
+ public void pause(String tag) {
+ if (ENABLE_TIMER && mEnabled) {
+ ArrayList<PerformancePoint> values = mPoints.get(tag);
+ if (values != null && values.size() % 2 == 1) {
+ values.add(new PerformancePoint());
+ }
+ }
+ }
+
+ @SuppressWarnings("unused")
+ public void count(String tag) {
+ if (ENABLE_TIMER && mEnabled) {
+ Integer counts = mCounts.get(tag);
+ if (counts == null) {
+ counts = 0;
+ }
+ mCounts.put(tag, counts + 1);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ public void clear() {
+ if (ENABLE_TIMER && mEnabled) {
+ mPoints.clear();
+ mCounts.clear();
+ }
+ }
+
+ /**
+ * Dumps cumulative timing results for all tags recognized by this timer.
+ */
+ @SuppressWarnings("unused")
+ public void dumpResults() {
+ if (ENABLE_TIMER && mEnabled) {
+ for (Map.Entry<String, ArrayList<PerformancePoint>> entry : mPoints.entrySet()) {
+ String tag = entry.getKey();
+ ArrayList<PerformancePoint> values = entry.getValue();
+ long cpuDurationNanos = 0;
+ long wallDurationMillis = 0;
+
+ for (int i = 0; i < values.size() - 1; i += 2) {
+ PerformancePoint startPoint = values.get(i);
+ PerformancePoint endPoint = values.get(i + 1);
+
+ cpuDurationNanos += endPoint.mCpuTimeNanos - startPoint.mCpuTimeNanos;
+ wallDurationMillis += endPoint.mWallTimeMillis - startPoint.mWallTimeMillis;
+ }
+
+ if (cpuDurationNanos == 0) {
+ cpuDurationNanos = 1;
+ }
+
+ dumpTimings(tag, 1, cpuDurationNanos, wallDurationMillis);
+ }
+
+ if (LOG_PERFORMANCE_STATS) {
+ for (Map.Entry<String, Integer> entry : mCounts.entrySet()) {
+ LogUtils.d(LOG_TAG, "Perf %s count: %d", entry.getKey(), entry.getValue());
+ }
+ }
+ }
+ }
+
+ /**
+ * Used for timing one off events.
+ */
+ private static Map<String, PerformancePoint> sPerformanceCollector =
+ new ConcurrentHashMap<String, PerformancePoint>();
+
+ /**
+ * Starts a one-off timer for an event. The event is denoted by {@code tag} and only one event
+ * of that tag may be timed at a given time.
+ */
+ public static void startTiming(String tag) {
+ if (ENABLE_TIMER) {
+ sPerformanceCollector.put(tag, new PerformancePoint());
+ }
+ }
+
+ /**
+ * Stops a one-off timer for an event indicated by {@code tag}.
+ */
+ public static void stopTiming(String tag) {
+ if (ENABLE_TIMER) {
+ stopTiming(tag, 1 /* one subiteration */);
+ }
+ }
+
+ /**
+ * Stops a one-off timer for an event indicated by {@code tag}, and indicates that the event
+ * consisted of {@code numSubIterations} sub-events, so that performance output will be denoted
+ * as such.
+ */
+ public static void stopTiming(String tag, int numSubIterations) {
+ if (ENABLE_TIMER) {
+ PerformancePoint endPoint = new PerformancePoint();
+ PerformancePoint startPoint = sPerformanceCollector.get(tag);
+ if (startPoint == null) {
+ return;
+ }
+ long cpuDurationNanos = endPoint.mCpuTimeNanos - startPoint.mCpuTimeNanos;
+ long wallDurationMillis = endPoint.mWallTimeMillis - startPoint.mWallTimeMillis;
+ // Make sure cpu Duration is non 0
+ if (cpuDurationNanos == 0) {
+ cpuDurationNanos = 1;
+ }
+
+ dumpTimings(tag, numSubIterations, cpuDurationNanos, wallDurationMillis);
+ }
+ }
+
+ private static void dumpTimings(String tag, int numSubIterations,
+ long cpuDurationNanos, long wallDurationMillis) {
+ // TODO(phamm): EventLogTags doens't work when building with SDK.
+// EventLog.writeEvent(EventLogTags.GMAIL_PERF_END, tag,
+// cpuDurationNanos / 1000000, wallDurationMillis, numSubIterations);
+
+ if (LOG_PERFORMANCE_STATS) {
+ LogUtils.d(LOG_TAG, "Perf %s wall: %d cpu: %d",
+ tag, wallDurationMillis, (cpuDurationNanos / 1000000));
+ // print out the average time for each sub iteration
+ if (numSubIterations > 1) {
+ LogUtils.d(LOG_TAG, "Perf/operation %s wall: %d cpu: %d", tag,
+ (wallDurationMillis / numSubIterations),
+ ((cpuDurationNanos / 1000000) / numSubIterations));
+ }
+ }
+ }
+
+}
diff --git a/src/com/android/email/providers/UIProvider.java b/src/com/android/email/providers/UIProvider.java
new file mode 100644
index 0000000..c68f9d2
--- /dev/null
+++ b/src/com/android/email/providers/UIProvider.java
@@ -0,0 +1,111 @@
+/**
+ * Copyright (c) 2011, Google Inc.
+ *
+ * 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.email.providers;
+
+import android.net.Uri;
+import android.provider.BaseColumns;
+
+
+public class UIProvider {
+ // This authority is only needed to get to the account list
+ // NOTE: Overlay applications may want to override this authority
+ public static final String AUTHORITY = "com.android.email.providers";
+
+ static final String BASE_URI_STRING = "content://" + AUTHORITY;
+
+ public static final String ACCOUNT_LIST_TYPE =
+ "vnd.android.cursor.dir/vnd.com.android.email.account";
+ public static final String ACCOUNT_TYPE =
+ "vnd.android.cursor.item/vnd.com.android.email.account";
+
+ public static final String[] ACCOUNTS_PROJECTION = {
+ BaseColumns._ID,
+ AccountColumns.NAME,
+ AccountColumns.PROVIDER_VERSION,
+ AccountColumns.URI,
+ AccountColumns.CAPABILITIES,
+ AccountColumns.FOLDER_LIST_URI,
+ AccountColumns.SEARCH_URI,
+ AccountColumns.ACCOUNT_FROM_ADDRESSES_URI,
+ };
+
+ public static final class AccountCapabilities {
+ public static final int SUPPORTS_SYNCABLE_FOLDERS = 0x0001;
+ public static final int SUPPORTS_REPORT_SPAM = 0x0002;
+ public static final int SUPPORTS_ARCHIVE = 0x0004;
+ public static final int SUPPORTS_SERVER_SEARCH = 0x0008;
+ public static final int SUPPORTS_FOLDER_SERVER_SEARCH = 0x00018;
+ public static final int RETURNS_SANITIZED_HTML = 0x0020;
+ public static final int SUPPORTS_DRAFT_SYNCHRONIZATION = 0x0040;
+ public static final int SUPPORTS_MULTIPLE_FROM_ADDRESSES = 0x0080;
+ public static final int SUPPORTS_SMART_REPLY = 0x0100;
+ public static final int SUPPORTS_LOCAL_SEARCH = 0x0200;
+ public static final int SUPPORTS_THREADED_CONVERSATIONS = 0x0400;
+ }
+
+ public static final class AccountColumns {
+ public static final String NAME = "name";
+ public static final String PROVIDER_VERSION = "providerVersion";
+ public static final String URI = "uri";
+ public static final String CAPABILITIES = "capabilities";
+ public static final String FOLDER_LIST_URI = "folderListUri";
+ public static final String SEARCH_URI = "searchUri";
+ public static final String ACCOUNT_FROM_ADDRESSES_URI = "accountFromAddressesUri";
+ public static final String SAVE_NEW_DRAFT_URI = "saveNewDraftUri";
+
+ private AccountColumns() {};
+ }
+
+ /**
+ * Returns a uri that, when queried, will return a cursor with a list of information for the
+ * list of configured accounts.
+ * @return
+ */
+ public static Uri getAccountsUri() {
+ return Uri.parse(BASE_URI_STRING + "/");
+ }
+
+
+ public static final class MessageColumns {
+ public static final String ID = "_id";
+ public static final String URI = "uri";
+ public static final String MESSAGE_ID = "messageId";
+ public static final String CONVERSATION_ID = "conversation";
+ public static final String SUBJECT = "subject";
+ public static final String SNIPPET = "snippet";
+ public static final String FROM = "fromAddress";
+ public static final String TO = "toAddresses";
+ public static final String CC = "ccAddresses";
+ public static final String BCC = "bccAddresses";
+ public static final String REPLY_TO = "replyToAddresses";
+ public static final String DATE_SENT_MS = "dateSentMs";
+ public static final String DATE_RECEIVED_MS = "dateReceivedMs";
+ public static final String LIST_INFO = "listInfo";
+ public static final String BODY = "body";
+ public static final String EMBEDS_EXTERNAL_RESOURCES = "bodyEmbedsExternalResources";
+ public static final String REF_MESSAGE_ID = "refMessageId";
+ public static final String FORWARD = "forward";
+ public static final String INCLUDE_QUOTED_TEXT = "includeQuotedText";
+ public static final String QUOTE_START_POS = "quoteStartPos";
+ public static final String CLIENT_CREATED = "clientCreated";
+ public static final String CUSTOM_FROM_ADDRESS = "customFromAddress";
+
+ // TODO: Add attachments, flags
+
+ private MessageColumns() {}
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/email/utils/LogUtils.java b/src/com/android/email/utils/LogUtils.java
index 8c065a0..3ca7961 100644
--- a/src/com/android/email/utils/LogUtils.java
+++ b/src/com/android/email/utils/LogUtils.java
@@ -22,7 +22,14 @@
import java.util.List;
public class LogUtils {
- private static final String TAG = "Gmail";
+ private static String LOG_TAG = "Email";
+
+ /**
+ * Get the log tag to apply to logging.
+ */
+ public String getLogTag() {
+ return LOG_TAG;
+ }
/**
* Priority constant for the println method; use LogUtils.v.
@@ -84,7 +91,7 @@
if (sDebugLoggingEnabledForTests != null) {
return sDebugLoggingEnabledForTests.booleanValue();
}
- return Log.isLoggable(TAG, Log.DEBUG);
+ return Log.isLoggable(LOG_TAG, Log.DEBUG);
}
/**
diff --git a/src/com/android/email/utils/Utils.java b/src/com/android/email/utils/Utils.java
index e12587b..f0bd69a 100644
--- a/src/com/android/email/utils/Utils.java
+++ b/src/com/android/email/utils/Utils.java
@@ -15,15 +15,24 @@
*/
package com.android.email.utils;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Typeface;
+import android.text.SpannableString;
+import android.text.SpannableStringBuilder;
+import android.text.style.CharacterStyle;
+import android.text.style.ForegroundColorSpan;
+import android.text.style.StyleSpan;
import android.webkit.WebSettings;
import android.webkit.WebView;
-public class Utils {
- public static final String LOG_TAG = "Email";
+import com.android.email.R;
- public String getLogTag() {
- return LOG_TAG;
- }
+public class Utils {
+ /**
+ * longest extension we recognize is 4 characters (e.g. "html", "docx")
+ */
+ private static final int FILE_EXTENSION_MAX_CHARS = 4;
/**
* Sets WebView in a restricted mode suitable for email use.
@@ -36,4 +45,86 @@
webSettings.setJavaScriptEnabled(true);
webSettings.setSupportZoom(false);
}
+
+ /**
+ * Format a plural string.
+ * @param resource The identity of the resource, which must be a R.plurals
+ * @param count The number of items.
+ */
+ public static String formatPlural(Context context, int resource, int count) {
+ CharSequence formatString = context.getResources().getQuantityText(resource, count);
+ return String.format(formatString.toString(), count);
+ }
+
+ /**
+ * @return an ellipsized String that's at most maxCharacters long. If the text passed is
+ * longer, it will be abbreviated. If it contains a suffix, the ellipses will be inserted
+ * in the middle and the suffix will be preserved.
+ */
+ public static String ellipsize(String text, int maxCharacters) {
+ int length = text.length();
+ if (length < maxCharacters) return text;
+
+ int realMax = Math.min(maxCharacters, length);
+ // Preserve the suffix if any
+ int index = text.lastIndexOf(".");
+ String extension = "\u2026"; // "...";
+ if (index >= 0) {
+ // Limit the suffix to dot + four characters
+ if (length - index <= FILE_EXTENSION_MAX_CHARS + 1) {
+ extension = extension + text.substring(index + 1);
+ }
+ }
+ realMax -= extension.length();
+ if (realMax < 0) realMax = 0;
+ return text.substring(0, realMax) + extension;
+ }
+
+ private static CharacterStyle sUnreadStyleSpan = null;
+ private static CharacterStyle sReadStyleSpan;
+ private static CharacterStyle sDraftsStyleSpan;
+ private static CharSequence sMeString;
+ private static CharSequence sDraftSingularString;
+ private static CharSequence sDraftPluralString;
+ private static CharSequence sSendingString;
+ private static CharSequence sSendFailedString;
+
+ public static void getStyledSenderSnippet(
+ Context context, String senderInstructions,
+ SpannableStringBuilder senderBuilder,
+ SpannableStringBuilder statusBuilder,
+ int maxChars, boolean forceAllUnread, boolean forceAllRead, boolean allowDraft) {
+ Resources res = context.getResources();
+ if (sUnreadStyleSpan == null) {
+ sUnreadStyleSpan = new StyleSpan(Typeface.BOLD);
+ sReadStyleSpan = new StyleSpan(Typeface.NORMAL);
+ sDraftsStyleSpan = new ForegroundColorSpan(res.getColor(R.color.drafts));
+
+ sMeString = context.getText(R.string.me);
+ sDraftSingularString = res.getQuantityText(R.plurals.draft, 1);
+ sDraftPluralString = res.getQuantityText(R.plurals.draft, 2);
+ SpannableString sendingString = new SpannableString(context.getText(R.string.sending));
+ sendingString.setSpan(
+ CharacterStyle.wrap(sDraftsStyleSpan), 0, sendingString.length(), 0);
+ sSendingString = sendingString;
+ sSendFailedString = context.getText(R.string.send_failed);
+ }
+
+ /* Gmail.getSenderSnippet(
+ senderInstructions, senderBuilder, statusBuilder, maxChars,
+ sUnreadStyleSpan,
+ sReadStyleSpan,
+ sDraftsStyleSpan,
+ sMeString,
+ sDraftSingularString, sDraftPluralString,
+ sSendingString, sSendFailedString,
+ forceAllUnread, forceAllRead, allowDraft);*/
+ }
+
+ /**
+ * Returns a boolean indicating whether the table UI should be shown.
+ */
+ public static boolean useTabletUI(Context context) {
+ return context.getResources().getInteger(R.integer.use_tablet_ui) != 0;
+ }
}