Move over resources and files for the widget.
This renames/ moves over styles and drawables for the widget
as well as xml files and classes.
Doesn't do much right now except display a unified email
widget in the widgets selection area and then say "app not
installed" when you drop it. But its a start!
Change-Id: Ie2759ce0adf520bd65222b50ddc8ab14c6659a37
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 3d27dbb..95fc926 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -45,6 +45,24 @@
<activity
android:name=".ui.MailActivity"
android:uiOptions="splitActionBarWhenNarrow" />
+ <activity android:name="LabelSynchronizationActivity"
+ android:theme="@android:style/Theme.Holo.Light.Dialog" />
+
+ <activity android:name="CreateShortcutActivity"
+ android:theme="@style/ShortcutWidgetTheme"
+ android:label="@string/activity_folder_selection" />
+
+ <activity android:name="FolderSelectionActivity"
+ android:theme="@style/ShortcutWidgetTheme"
+ android:label="@string/activity_folder_selection" />
+
+ <activity android:name="MailboxSelectionActivity"
+ android:theme="@style/InvisibleShortcutWidgetTheme"
+ android:label="@string/app_name" >
+ <intent-filter>
+ <action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
+ </intent-filter>
+ </activity>
<provider
android:authorities="com.android.mail.mockprovider"
@@ -87,6 +105,22 @@
<service android:name=".providers.protos.boot.GmailAccountService"/>
<service android:name=".providers.protos.boot.EmailAccountService"/>
<service android:name=".compose.EmptyService"/>
+
+ <!-- Widget -->
+ <receiver android:name=".widget.WidgetProvider" android:label="@string/app_name">
+ <intent-filter>
+ <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
+ </intent-filter>
+ <intent-filter>
+ <action android:name="com.google.android.gm.intent.ACTION_NOTIFY_DATASET_CHANGED" />
+ </intent-filter>
+ <meta-data android:name="android.appwidget.provider"
+ android:resource="@xml/widget_info" />
+ </receiver>
+ <service android:name=".widget.WidgetService"
+ android:permission="android.permission.BIND_REMOTEVIEWS"
+ android:exported="false" />
+
</application>
</manifest>
diff --git a/res/drawable-hdpi/gradient_bg_email_widget_holo.9.png b/res/drawable-hdpi/gradient_bg_widget_holo.9.png
similarity index 100%
rename from res/drawable-hdpi/gradient_bg_email_widget_holo.9.png
rename to res/drawable-hdpi/gradient_bg_widget_holo.9.png
Binary files differ
diff --git a/res/drawable-hdpi/header_bg_email_widget_holo.9.png b/res/drawable-hdpi/header_bg_widget_holo.9.png
similarity index 100%
rename from res/drawable-hdpi/header_bg_email_widget_holo.9.png
rename to res/drawable-hdpi/header_bg_widget_holo.9.png
Binary files differ
diff --git a/res/drawable-hdpi/list_div_top_btm_email_widget_holo.9.png b/res/drawable-hdpi/list_div_top_btm_widget_holo.9.png
similarity index 100%
rename from res/drawable-hdpi/list_div_top_btm_email_widget_holo.9.png
rename to res/drawable-hdpi/list_div_top_btm_widget_holo.9.png
Binary files differ
diff --git a/res/drawable-mdpi/gradient_bg_email_widget_holo.9.png b/res/drawable-mdpi/gradient_bg_widget_holo.9.png
similarity index 100%
rename from res/drawable-mdpi/gradient_bg_email_widget_holo.9.png
rename to res/drawable-mdpi/gradient_bg_widget_holo.9.png
Binary files differ
diff --git a/res/drawable-mdpi/header_bg_email_widget_holo.9.png b/res/drawable-mdpi/header_bg_widget_holo.9.png
similarity index 100%
rename from res/drawable-mdpi/header_bg_email_widget_holo.9.png
rename to res/drawable-mdpi/header_bg_widget_holo.9.png
Binary files differ
diff --git a/res/drawable-mdpi/list_div_top_btm_email_widget_holo.9.png b/res/drawable-mdpi/list_div_top_btm_widget_holo.9.png
similarity index 100%
rename from res/drawable-mdpi/list_div_top_btm_email_widget_holo.9.png
rename to res/drawable-mdpi/list_div_top_btm_widget_holo.9.png
Binary files differ
diff --git a/res/drawable-nodpi/googlemail_widget_preview.png b/res/drawable-nodpi/googlemail_widget_preview.png
deleted file mode 100644
index fac94b0..0000000
--- a/res/drawable-nodpi/googlemail_widget_preview.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-nodpi/mail_widget_preview.png b/res/drawable-nodpi/mail_widget_preview.png
new file mode 100644
index 0000000..f04b42a
--- /dev/null
+++ b/res/drawable-nodpi/mail_widget_preview.png
Binary files differ
diff --git a/res/drawable-xhdpi/gradient_bg_email_widget_holo.9.png b/res/drawable-xhdpi/gradient_bg_widget_holo.9.png
similarity index 100%
rename from res/drawable-xhdpi/gradient_bg_email_widget_holo.9.png
rename to res/drawable-xhdpi/gradient_bg_widget_holo.9.png
Binary files differ
diff --git a/res/drawable-xhdpi/header_bg_email_widget_holo.9.png b/res/drawable-xhdpi/header_bg_widget_holo.9.png
similarity index 100%
rename from res/drawable-xhdpi/header_bg_email_widget_holo.9.png
rename to res/drawable-xhdpi/header_bg_widget_holo.9.png
Binary files differ
diff --git a/res/drawable-xhdpi/list_div_top_btm_email_widget_holo.9.png b/res/drawable-xhdpi/list_div_top_btm_widget_holo.9.png
similarity index 100%
rename from res/drawable-xhdpi/list_div_top_btm_email_widget_holo.9.png
rename to res/drawable-xhdpi/list_div_top_btm_widget_holo.9.png
Binary files differ
diff --git a/res/drawable/widget_conversation_read_selector.xml b/res/drawable/widget_conversation_read_selector.xml
new file mode 100644
index 0000000..78eb9c7
--- /dev/null
+++ b/res/drawable/widget_conversation_read_selector.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 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_focused_holo" />
+ <item android:drawable="@drawable/list_read_holo" />
+</selector>
diff --git a/res/drawable/widget_conversation_unread_selector.xml b/res/drawable/widget_conversation_unread_selector.xml
new file mode 100644
index 0000000..56f4d16
--- /dev/null
+++ b/res/drawable/widget_conversation_unread_selector.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 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_focused_holo" />
+ <item android:drawable="@drawable/list_unread_holo" />
+</selector>
diff --git a/res/layout/folder_sync_for_widget_dialog.xml b/res/layout/folder_sync_for_widget_dialog.xml
new file mode 100644
index 0000000..4e17f98
--- /dev/null
+++ b/res/layout/folder_sync_for_widget_dialog.xml
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 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.
+-->
+
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:fillViewport="true"
+ android:layout_height="0dip"
+ android:layout_weight="1"
+ android:layout_width="match_parent">
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <TextView
+ android:id="@+id/folder_sync_for_widget_text"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:padding="10dip"
+ android:paddingBottom="20dip"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:singleLine="false"
+ android:text="@string/folder_sync_for_widget_description" />
+
+ <CheckBox
+ android:id="@+id/folder_sync_for_widget_confirm"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/folder_sync_for_widget_text"
+ android:text="@string/folder_sync_for_widget_checkbox"
+ android:checked="true" />
+
+ </RelativeLayout>
+
+</ScrollView>
diff --git a/res/layout/mailbox_item.xml b/res/layout/mailbox_item.xml
new file mode 100644
index 0000000..1ec2156
--- /dev/null
+++ b/res/layout/mailbox_item.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 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.
+-->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_height="wrap_content"
+ android:layout_width="match_parent"
+ android:minHeight="?android:attr/listPreferredItemHeight"
+ android:orientation="horizontal"
+ android:gravity="center_vertical">
+
+ <TextView
+ android:id="@+id/mailbox_name"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:paddingLeft="16dip"
+ android:paddingRight="16dip"
+ android:drawablePadding="10dip" />
+
+</LinearLayout>
diff --git a/res/layout/mailbox_selection_activity.xml b/res/layout/mailbox_selection_activity.xml
new file mode 100644
index 0000000..88fbf92
--- /dev/null
+++ b/res/layout/mailbox_selection_activity.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 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.
+-->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:minWidth="@dimen/folder_list_popup_width"
+ android:orientation="vertical">
+
+ <ListView
+ android:id="@android:id/list"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:cacheColorHint="#00000000"
+ android:paddingLeft="16dip"
+ android:paddingRight="16dip"
+ android:fadingEdgeLength="16dip" />
+
+ <include layout="@layout/one_button_button_bar"/>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/res/layout/one_button_button_bar.xml b/res/layout/one_button_button_bar.xml
new file mode 100644
index 0000000..018a177
--- /dev/null
+++ b/res/layout/one_button_button_bar.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 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.
+-->
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:divider="?android:attr/dividerHorizontal"
+ android:showDividers="beginning"
+ android:dividerPadding="16dip"
+ android:background="@null">
+ <LinearLayout
+ style="?android:attr/buttonBarStyle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:paddingLeft="2dip"
+ android:paddingRight="2dip"
+ android:measureWithLargestChild="true"
+ android:background="@null">
+ <Button
+ android:id="@+id/cancel"
+ style="?android:attr/buttonBarButtonStyle"
+ android:layout_weight="1"
+ android:layout_width="0dip"
+ android:layout_height="wrap_content"
+ android:text="@string/cancel" />
+ </LinearLayout>
+</LinearLayout>
\ No newline at end of file
diff --git a/res/layout/shortcut_name_activity.xml b/res/layout/shortcut_name_activity.xml
new file mode 100644
index 0000000..c9e05c5
--- /dev/null
+++ b/res/layout/shortcut_name_activity.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 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.
+-->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ style="@style/ShortcutNameWrapper"
+ android:minWidth="@dimen/folder_list_popup_width"
+ android:orientation="vertical">
+
+ <!-- Shortcut name -->
+ <EditText
+ android:id="@+id/folder_text"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:inputType="textAutoCorrect|textCapSentences|textImeMultiLine"
+ android:imeOptions="actionDone"
+ android:layout_marginRight="16dip"
+ android:layout_marginLeft="16dip"
+ android:layout_gravity="center"
+ style="@style/ShortcutNameStyle"/>
+
+ <include layout="@layout/two_button_button_bar"/>
+
+</LinearLayout>
diff --git a/res/layout/two_button_button_bar.xml b/res/layout/two_button_button_bar.xml
new file mode 100644
index 0000000..a98ef19
--- /dev/null
+++ b/res/layout/two_button_button_bar.xml
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 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.
+-->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:divider="?android:attr/dividerHorizontal"
+ android:showDividers="beginning"
+ android:dividerPadding="16dip"
+ android:background="@null">
+ <LinearLayout
+ style="?android:attr/buttonBarStyle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:paddingLeft="2dip"
+ android:paddingRight="2dip"
+ android:measureWithLargestChild="true"
+ android:background="@null">
+ <Button
+ android:id="@+id/cancel"
+ style="?android:attr/buttonBarButtonStyle"
+ android:layout_weight="1"
+ android:layout_width="0dip"
+ android:layout_height="wrap_content"
+ android:text="@string/cancel" />
+ <Button
+ android:id="@+id/done"
+ style="?android:attr/buttonBarButtonStyle"
+ android:layout_weight="1"
+ android:layout_width="0dip"
+ android:layout_height="wrap_content"
+ android:text="@string/done" />
+ </LinearLayout>
+</LinearLayout>
\ No newline at end of file
diff --git a/res/layout/widget.xml b/res/layout/widget.xml
new file mode 100644
index 0000000..d0da897
--- /dev/null
+++ b/res/layout/widget.xml
@@ -0,0 +1,140 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 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.
+-->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_marginTop="@dimen/widget_margin_top"
+ android:layout_marginLeft="@dimen/widget_margin_left"
+ android:layout_marginRight="@dimen/widget_margin_right"
+ android:layout_marginBottom="@dimen/widget_margin_bottom"
+ android:orientation="vertical">
+ <LinearLayout
+ android:id="@+id/widget_header"
+ android:layout_width="match_parent"
+ android:layout_height="48dp"
+ android:orientation="horizontal"
+ android:paddingLeft="8dip"
+ android:paddingRight="8dip"
+ android:background="@drawable/header_bg_widget_holo"
+ android:gravity="center_vertical">
+ <ImageView
+ android:id="@+id/widget_icon"
+ android:layout_width="32dip"
+ android:layout_height="32dip"
+ android:layout_marginRight="8dip"
+ android:src="@mipmap/ic_launcher_mail" />
+ <LinearLayout
+ android:layout_width="0dip"
+ android:layout_height="wrap_content"
+ android:layout_weight = "1"
+ android:layout_marginRight="8dip"
+ android:orientation="vertical">
+ <TextView
+ android:id="@+id/widget_folder"
+ style="@style/WidgetTitle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:freezesText="true" />
+ <TextView
+ android:id="@+id/widget_account"
+ style="@style/WidgetSubtitle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:freezesText="true" />
+ </LinearLayout>
+ <!-- Note: We can't set "freezesText" on the widget_unread_count view, as the launcher can
+ restore stale data over updated data. -->
+ <TextView
+ android:id="@+id/widget_unread_count"
+ style="@style/UnreadCount"
+ android:layout_marginRight="12dip"
+ android:textColor="#777777" />
+ <ImageButton
+ android:id="@+id/widget_compose"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:src="@drawable/ic_menu_compose_normal_holo_light"
+ android:background="?android:attr/selectableItemBackground"
+ android:paddingLeft="4dip"
+ android:paddingBottom="4dip" />
+ </LinearLayout>
+ <ListView
+ android:id="@+id/conversation_list"
+ android:layout_width="match_parent"
+ android:layout_height="0dip"
+ android:layout_weight="1"
+ android:cacheColorHint="#00000000"
+ android:background="@drawable/gradient_bg_widget_holo" />
+ <LinearLayout
+ android:id="@+id/widget_configuration"
+ android:layout_width="match_parent"
+ android:layout_height="0dip"
+ android:layout_weight="1"
+ android:orientation="horizontal"
+ android:gravity="center"
+ android:background="@drawable/gradient_bg_widget_holo"
+ android:visibility="gone">
+ <ImageView
+ android:src="@mipmap/ic_launcher_mail"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:paddingRight="8dip" />
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/tap_to_configure"
+ android:textColor="@android:color/black"
+ android:textSize="16sp"
+ android:textStyle="bold"/>
+ </LinearLayout>
+ <LinearLayout android:id="@+id/widget_folder_not_synced"
+ android:layout_width="match_parent"
+ android:layout_height="0dip"
+ android:layout_weight="1"
+ android:paddingLeft="24dip"
+ android:paddingRight="24dip"
+ android:orientation="vertical"
+ android:gravity="center"
+ android:background="@drawable/gradient_bg_widget_holo"
+ android:visibility="gone" >
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="16dip"
+ android:text="@string/non_synced_folder_description"
+ android:textColor="@android:color/black"
+ android:textSize="16sp" />
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="8dip"
+ android:paddingBottom="8dip"
+ android:text="@string/tap_to_configure_folder_sync"
+ android:textColor="@android:color/black"
+ android:background="@android:color/darker_gray"
+ android:gravity="center"
+ android:textSize="16sp"
+ android:textStyle="bold"/>
+ </LinearLayout>
+ <ImageView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@drawable/list_div_top_btm_widget_holo" />
+</LinearLayout>
diff --git a/res/layout/widget_conversation.xml b/res/layout/widget_conversation.xml
new file mode 100644
index 0000000..423403b
--- /dev/null
+++ b/res/layout/widget_conversation.xml
@@ -0,0 +1,105 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 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.
+-->
+
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/widget_conversation"
+ android:layout_width="match_parent"
+ android:layout_height="70sp">
+ <!--
+ Remote view doesn't allow changing background so we have to work around
+ by having 2 image views here.
+ -->
+ <ImageView
+ android:id="@+id/widget_unread_background"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@drawable/widget_conversation_unread_selector" />
+ <ImageView
+ android:id="@+id/widget_read_background"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@drawable/widget_conversation_read_selector" />
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:paddingLeft="16dip"
+ android:paddingRight="8dip"
+ android:orientation="vertical">
+ <RelativeLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentRight="true"
+ android:gravity="right" >
+ <ImageView
+ android:id="@+id/widget_folder_0"
+ android:layout_width="32dip"
+ android:layout_height="6sp"
+ android:scaleType="fitXY" />
+ <ImageView
+ android:id="@+id/widget_folder_1"
+ android:layout_width="32dip"
+ android:layout_height="6sp"
+ android:layout_toRightOf="@id/widget_folder_0"
+ android:scaleType="fitXY" />
+ <ImageView
+ android:id="@+id/widget_folder_2"
+ android:layout_width="32dip"
+ android:layout_height="6sp"
+ android:layout_toRightOf="@id/widget_folder_1"
+ android:scaleType="fitXY" />
+ </RelativeLayout>
+ <RelativeLayout
+ android:id="@+id/widget_attachment_and_date"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentTop="true"
+ android:layout_alignParentRight="true"
+ android:layout_marginTop="10sp"
+ android:orientation="horizontal" >
+ <!-- Use negative margins to align attachment icon with text -->
+ <ImageView
+ android:id="@+id/widget_attachment"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="-4sp" />
+ <TextView
+ android:id="@+id/widget_date"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_toRightOf="@id/widget_attachment" />
+ </RelativeLayout>
+ <TextView
+ android:id="@+id/widget_senders"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentTop="true"
+ android:layout_alignParentLeft="true"
+ android:layout_toLeftOf="@id/widget_attachment_and_date"
+ android:layout_marginTop="6sp"
+ android:paddingRight="16dip"
+ android:singleLine="true" />
+ <TextView
+ android:id="@+id/widget_subject"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/widget_senders"
+ android:layout_marginTop="-2sp"
+ android:maxLines="2" />
+ </RelativeLayout>
+</FrameLayout>
\ No newline at end of file
diff --git a/res/layout/widget_loading.xml b/res/layout/widget_loading.xml
new file mode 100644
index 0000000..c0f03e3
--- /dev/null
+++ b/res/layout/widget_loading.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2013 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.
+-->
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/widget_loading"
+ android:layout_width="match_parent"
+ android:layout_height="64sp"
+ android:background="@drawable/conversation_read_selector">
+ <TextView
+ android:id="@+id/loading_text"
+ android:layout_height="match_parent"
+ android:layout_width="match_parent"
+ android:layout_marginLeft="16dip"
+ android:gravity="center"
+ android:textColor="@android:color/black"
+ android:textSize="14sp"
+ android:textStyle="bold" />
+</FrameLayout>
\ No newline at end of file
diff --git a/res/values/constants.xml b/res/values/constants.xml
index 4229c70..5fadbda 100644
--- a/res/values/constants.xml
+++ b/res/values/constants.xml
@@ -41,4 +41,6 @@
<integer name="conversation_view_weight">1</integer>
<!-- Max unread count to show for a folder -->
<integer name="maxUnreadCount">999</integer>
+ <!-- <integer name="widget_refresh_delay_ms">4000</integer>-->
+ <integer name="widget_folder_refresh_delay_ms">500</integer>
</resources>
diff --git a/res/values/dimen.xml b/res/values/dimen.xml
index 9f350a3..540af6f 100644
--- a/res/values/dimen.xml
+++ b/res/values/dimen.xml
@@ -17,7 +17,6 @@
-->
<resources>
-
<dimen name="account_dropdown_item_height">48dip</dimen>
<dimen name="wide_senders_font_size">16sp</dimen>
<dimen name="wide_subject_font_size">16sp</dimen>
@@ -67,4 +66,11 @@
<dimen name="folder_list_popup_width">200dip</dimen>
<dimen name="folder_list_folder_color_width">36dip</dimen>
<dimen name="folder_list_minimum_height">48dip</dimen>
+ <dimen name="widget_senders_font_size">18sp</dimen>
+ <dimen name="widget_subject_font_size">14sp</dimen>
+ <dimen name="widget_date_font_size">12sp</dimen>
+ <dimen name="widget_margin_top">11dip</dimen>
+ <dimen name="widget_margin_left">17dip</dimen>
+ <dimen name="widget_margin_right">17dip</dimen>
+ <dimen name="widget_margin_bottom">22dip</dimen>
</resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index a81e9b3..3496bf1 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -441,8 +441,6 @@
<!-- Folders -->
<!-- Displayed in the middle of the Folders screen when no folders have been defined -->
<string name="folders_activity_instructions">No folders have been set for this account.</string>
- <!-- Displayed in widget when unread count > 100. [CHAR LIMIT=4] -->
- <string name="widget_large_unread_count">%d+</string>
<!-- Sync status errors translate="false" [CHAR LIMIT=100] -->
<string-array name="sync_status">
@@ -453,4 +451,51 @@
<item>Storage error</item>
<item>Internal error</item>
</string-array>
+
+ <!-- Widget strings -->
+ <!-- Displayed when user adds a new widget. Tapping on the widget in this
+ mode will bring user to the account selection screen [CHAR LIMIT=35] -->
+ <string name="tap_to_configure">Touch to set up</string>
+
+ <!-- Displayed within a widget that was setup for a non-synced label [CHAR LIMIT=35] -->
+ <string name="non_synced_folder_description">To view conversations, sync this folder.</string>
+
+ <!-- Displayed within a widget that was setup for a non-synced folder. This is the text of the
+ button that will allow the user to configure sync for the folder [CHAR LIMIT=35] -->
+ <string name="tap_to_configure_folder_sync">Sync Folder</string>
+
+ <!-- Displayed in widget when unread count > 100. [CHAR LIMIT=4] -->
+ <string name="widget_large_unread_count">%d+</string>
+
+ <!-- Displayed at the end of the conversation list in the widget. Tapping on this will open the default Inbox. [CHAR LIMIT=35] -->
+ <string name="view_more_conversations">View more conversations</string>
+
+ <!-- Displayed while we load a conversation. [CHAR LIMIT=100] -->
+ <string name="loading_conversation">Loading\u2026</string>
+
+ <!-- Name of the Mailboxes activity [CHAR LIMIT=30] -->
+ <string name="activity_mailbox_selection">Choose account</string>
+
+ <!-- Name of the Folders activity [CHAR LIMIT=30] -->
+ <string name="activity_folder_selection">Choose folder</string>
+
+ <!-- Manange folders mode, sync option: synchronize all conversations with this folder. [CHAR LIMIT=100] -->
+ <string name="sync_all">Sync: All</string>
+ <plurals name="sync_recent">
+ <!-- Manage folders mode, sync option: synchronize conversations in the last day with this folder [CHAR LIMIT=100] -->
+ <item quantity="one">Sync: Last <xliff:g id="count" example="1">%1$d</xliff:g> day</item>
+ <!-- Manage folders mode, sync option: synchronize conversations in the last 2+ day with this folder [CHAR LIMIT=100] -->
+ <item quantity="other">Sync: Last <xliff:g id="count" example="4">%1$d</xliff:g> days</item>
+ </plurals>
+ <!-- Manage folders mode, sync option: don't synchronize any conversation with this folder [CHAR LIMIT=100] -->
+ <string name="sync_none">Sync: None</string>
+
+ <!-- Title for dialog encouraging users enable sync for a currently non-synced folder [CHAR LIMIT=30] -->
+ <string name="folder_sync_for_widget_title">Sync folder</string>
+
+ <!-- Dialog text encouraging users to enable sync for a newly created widget [CHAR LIMIT=400] -->
+ <string name="folder_sync_for_widget_description">To view conversations in the widget, sync this folder.</string>
+
+ <!-- Checkbox text to enable sync for a folder where a widget is in the processes of being added, and sync had been not been enabled [CHAR LIMIT=30] -->
+ <string name="folder_sync_for_widget_checkbox">Sync last <xliff:g id="numSyncDays" example="30">%d</xliff:g> days</string>
</resources>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index 290dbb7..2d51989 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -313,4 +313,30 @@
<item name="android:layout_height">@dimen/folder_swatch_height</item>
<item name="android:layout_width">@dimen/folder_list_folder_color_width</item>
</style>
+
+ <!-- Widget styles -->
+ <style name="WidgetTitle" parent="@android:style/TextAppearance.Holo.Widget.ActionBar.Title">
+ <item name="android:textColor">#333333</item>
+ <item name="android:singleLine">true</item>
+ </style>
+
+ <style name="WidgetSubtitle" parent="@android:style/TextAppearance.Holo.Widget.ActionBar.Subtitle">
+ <item name="android:textColor">#777777</item>
+ <item name="android:singleLine">true</item>
+ </style>
+
+ <style name="ShortcutNameWrapper">
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">match_parent</item>
+ </style>
+
+ <style name="ShortcutNameStyle">
+ <item name="android:layout_marginTop">16dip</item>
+ </style>
+
+ <style name="ShortcutWidgetTheme" parent="@android:style/Theme.Holo.Light.DialogWhenLarge"/>
+
+ <style name="InvisibleShortcutWidgetTheme" parent="@style/ShortcutWidgetTheme">
+ <item name="android:windowNoDisplay">true</item>
+ </style>
</resources>
diff --git a/res/xml-sw600dp/widget_info.xml b/res/xml-sw600dp/widget_info.xml
new file mode 100644
index 0000000..a650e59
--- /dev/null
+++ b/res/xml-sw600dp/widget_info.xml
@@ -0,0 +1,27 @@
+<?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.
+-->
+
+<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
+ android:minWidth="220dp"
+ android:minHeight="220dp"
+ android:updatePeriodMillis="0"
+ android:previewImage="@drawable/mail_widget_preview"
+ android:initialLayout="@layout/widget"
+ android:configure="com.android.mail.ui.MailboxSelectionActivity"
+ android:resizeMode="horizontal|vertical">
+</appwidget-provider>
diff --git a/res/xml/widget_info.xml b/res/xml/widget_info.xml
new file mode 100644
index 0000000..819dc5e
--- /dev/null
+++ b/res/xml/widget_info.xml
@@ -0,0 +1,29 @@
+<?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.
+-->
+
+<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
+ android:minWidth="180dp"
+ android:minHeight="180dp"
+ android:minResizeWidth="180dp"
+ android:minResizeHeight="110dp"
+ android:updatePeriodMillis="0"
+ android:previewImage="@drawable/mail_widget_preview"
+ android:initialLayout="@layout/widget"
+ android:configure="com.android.mail.ui.MailboxSelectionActivity"
+ android:resizeMode="horizontal|vertical">
+</appwidget-provider>
diff --git a/src/com/android/mail/providers/Account.java b/src/com/android/mail/providers/Account.java
index f0573cd..3287632 100644
--- a/src/com/android/mail/providers/Account.java
+++ b/src/com/android/mail/providers/Account.java
@@ -146,6 +146,20 @@
dest.writeInt(syncStatus);
}
+ /**
+ * Get the settings associated with this account.
+ * TODO: this method is just a stand-in.
+ */
+ public Cursor getSettings() {
+ return null;
+ }
+
+ public Folder getAccountInbox() {
+ // TODO: (mindyp) fill in with call to settings or reading of account settings
+ // to get the default inbox for this account.
+ return null;
+ }
+
@SuppressWarnings("hiding")
public static final Creator<Account> CREATOR = new Creator<Account>() {
@Override
diff --git a/src/com/android/mail/ui/FolderListFragment.java b/src/com/android/mail/ui/FolderListFragment.java
index 0a4f5ab..83ccbb2 100644
--- a/src/com/android/mail/ui/FolderListFragment.java
+++ b/src/com/android/mail/ui/FolderListFragment.java
@@ -59,6 +59,9 @@
private FolderChangeListener mListener;
private static final int FOLDER_LOADER_ID = 0;
+
+ public static final int MODE_DEFAULT = 0;
+ public static final int MODE_PICK = 1;
/**
* Hidden constructor.
*/
@@ -73,6 +76,15 @@
* to display conversation list context.
*/
public static FolderListFragment newInstance(FolderChangeListener listener, String uri) {
+ return newInstance(listener, uri, MODE_DEFAULT);
+ }
+
+ /**
+ * Creates a new instance of {@link ConversationListFragment}, initialized
+ * to display conversation list context.
+ */
+ public static FolderListFragment newInstance(FolderChangeListener listener, String uri,
+ int mode) {
FolderListFragment fragment = new FolderListFragment(listener, uri);
return fragment;
}
diff --git a/src/com/android/mail/ui/FolderSelectionActivity.java b/src/com/android/mail/ui/FolderSelectionActivity.java
new file mode 100644
index 0000000..4621201
--- /dev/null
+++ b/src/com/android/mail/ui/FolderSelectionActivity.java
@@ -0,0 +1,212 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+
+package com.android.mail.ui;
+
+import android.app.ActionBar;
+import android.app.Activity;
+import android.app.Dialog;
+import android.app.Fragment;
+import android.app.FragmentTransaction;
+import android.appwidget.AppWidgetManager;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.view.DragEvent;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+
+import com.android.mail.R;
+import com.android.mail.providers.Account;
+import com.android.mail.providers.Folder;
+import com.android.mail.providers.UIProvider;
+import com.android.mail.utils.LogUtils;
+import com.android.mail.utils.Utils;
+import com.android.mail.widget.WidgetProvider;
+import com.google.common.collect.Sets;
+
+import java.util.Set;
+
+/**
+ * This activity displays the list of available folders for the current account.
+ */
+public class FolderSelectionActivity extends Activity implements OnClickListener,
+ DialogInterface.OnClickListener, FolderChangeListener {
+ public static final String EXTRA_ACCOUNT_SHORTCUT = "account-shortcut";
+
+ private static final String LOG_TAG = new LogUtils().getLogTag();
+
+ private Account mAccount;
+ private Folder mSelectedFolder;
+ private boolean mConfigureShortcut;
+ private boolean mConfigureWidget;
+ private int mAppWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID;
+
+ @Override
+ public void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ setContentView(R.layout.folders_activity);
+
+ final Intent intent = getIntent();
+ final String action = intent.getAction();
+ mConfigureShortcut = Intent.ACTION_CREATE_SHORTCUT.equals(action);
+ mConfigureWidget = AppWidgetManager.ACTION_APPWIDGET_CONFIGURE.equals(action);
+ if (!mConfigureShortcut && !mConfigureWidget) {
+ LogUtils.wtf(LOG_TAG, "unexpected intent: %s", intent);
+ }
+ if (mConfigureShortcut || mConfigureWidget) {
+ ActionBar actionBar = getActionBar();
+ if (actionBar != null) {
+ actionBar.setIcon(R.mipmap.ic_launcher_shortcut_folder);
+ }
+ }
+
+ if (mConfigureWidget) {
+ mAppWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
+ AppWidgetManager.INVALID_APPWIDGET_ID);
+ if (mAppWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {
+ LogUtils.wtf(LOG_TAG, "invalid widgetId");
+ }
+ }
+
+ mAccount = intent.getParcelableExtra(EXTRA_ACCOUNT_SHORTCUT);
+ Button cancelButton = (Button) findViewById(R.id.cancel);
+ cancelButton.setVisibility(View.VISIBLE);
+ cancelButton.setOnClickListener(this);
+
+ FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
+ Fragment fragment = FolderListFragment.newInstance(this, mAccount.folderListUri,
+ FolderListFragment.MODE_PICK);
+ fragmentTransaction.replace(R.id.folders_pane, fragment);
+ fragmentTransaction.commitAllowingStateLoss();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+
+ // TODO: (mindyp) Make sure we're operating on the same account as
+ // before. If the user switched accounts, switch back.
+ }
+
+ @Override
+ public void onClick(View v) {
+ switch (v.getId()) {
+ case R.id.cancel:
+ doCancel();
+ break;
+ }
+ }
+
+ private void doCancel() {
+ setResult(RESULT_CANCELED);
+ finish();
+ }
+
+ @Override
+ public Dialog onCreateDialog(int id, Bundle bundle) {
+ Dialog dialog = null;
+ switch (id) {
+ case R.layout.folder_sync_for_widget_dialog:
+ dialog = new SyncForWidgetDialog(this, mAccount, mSelectedFolder, this);
+ break;
+ }
+ return dialog == null ? super.onCreateDialog(id, bundle) : dialog;
+ }
+
+ /**
+ * Create a widget for the specified account and label
+ */
+ private void createWidget() {
+ WidgetProvider.updateWidget(this, mAppWidgetId, mAccount, mSelectedFolder);
+ Intent result = new Intent();
+ result.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId);
+ setResult(RESULT_OK, result);
+ finish();
+ }
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ if (which == DialogInterface.BUTTON_POSITIVE) {
+ // The only dialog that is
+ createWidget();
+ } else {
+ doCancel();
+ }
+ }
+
+ @Override
+ public void onFolderChanged(Folder folder) {
+ if (!folder.equals(mSelectedFolder)) {
+ mSelectedFolder = folder;
+ Intent resultIntent = new Intent();
+
+ if (mConfigureShortcut) {
+ /*
+ * Create the shortcut Intent based on it with the additional
+ * information that we have in this activity: name of the
+ * account, calculate the human readable name of the label and
+ * use it as the shortcut name, etc...
+ */
+ final Intent clickIntent = Utils.createViewConversationIntent(this, mAccount,
+ mSelectedFolder, UIProvider.INVALID_CONVERSATION_ID);
+ resultIntent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, clickIntent);
+ resultIntent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE,
+ Intent.ShortcutIconResource.fromContext(this,
+ R.mipmap.ic_launcher_shortcut_folder));
+
+ CharSequence humanLabelName = mSelectedFolder.name;
+
+ resultIntent.putExtra(Intent.EXTRA_SHORTCUT_NAME, humanLabelName);
+
+ // Now ask the user what name they want for this shortcut. Pass
+ // the
+ // shortcut intent that we just created, the user can modify the
+ // label in
+ // ShortcutNameActivity.
+ final Intent shortcutNameIntent = new Intent(this, ShortcutNameActivity.class);
+ shortcutNameIntent.setFlags(Intent.FLAG_ACTIVITY_NO_HISTORY
+ | Intent.FLAG_ACTIVITY_FORWARD_RESULT);
+ shortcutNameIntent.putExtra(ShortcutNameActivity.EXTRA_LABEL_CLICK_INTENT,
+ resultIntent);
+ shortcutNameIntent.putExtra(ShortcutNameActivity.EXTRA_SHORTCUT_NAME,
+ humanLabelName);
+
+ startActivity(shortcutNameIntent);
+ finish();
+ } else if (mConfigureWidget) {
+ // Check to see if the widget is set to be synchronized
+ final Cursor settings = mAccount.getSettings();
+ final Set<String> synchronizedLabelsSet = Sets.newHashSet();
+
+ // Add all of the synchronized labels to the set
+ // TODO: (mindyp) deal with labels.
+ // synchronizedLabelsSet.addAll(settings.getLabelsIncluded());
+ // synchronizedLabelsSet.addAll(settings.getLabelsPartial());
+
+ if (!synchronizedLabelsSet.contains(mSelectedFolder.name)) {
+ // Display a dialog offering to enable sync for this label
+ showDialog(R.layout.folder_sync_for_widget_dialog);
+ } else {
+ createWidget();
+ }
+ }
+ }
+ }
+}
diff --git a/src/com/android/mail/ui/MailboxSelectionActivity.java b/src/com/android/mail/ui/MailboxSelectionActivity.java
new file mode 100644
index 0000000..09174d8
--- /dev/null
+++ b/src/com/android/mail/ui/MailboxSelectionActivity.java
@@ -0,0 +1,310 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+package com.android.mail.ui;
+
+import com.android.mail.R;
+import com.android.mail.providers.Account;
+import com.android.mail.providers.AccountCacheProvider;
+import com.android.mail.providers.UIProvider;
+import com.android.mail.utils.LogUtils;
+import com.android.mail.utils.Utils;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+
+import android.app.ActionBar;
+import android.app.ListActivity;
+import android.appwidget.AppWidgetManager;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.ListView;
+import android.widget.SimpleAdapter;
+import android.widget.TextView;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * An activity that shows the list of all the available accounts and return the
+ * one selected in onResult().
+ */
+public class MailboxSelectionActivity extends ListActivity implements OnClickListener {
+
+ // Used to save our instance state
+ private static final String CREATE_SHORTCUT_KEY = "createShortcut";
+ private static final String CREATE_WIDGET_KEY = "createWidget";
+ private static final String WIDGET_ID_KEY = "widgetId";
+ private static final String WAITING_FOR_ADD_ACCOUNT_RESULT_KEY = "waitingForAddAccountResult";
+
+ private static final String ACCOUNT = "name";
+ private static final String[] COLUMN_NAMES = { ACCOUNT };
+ protected static final String LOG_TAG = new LogUtils().getLogTag();
+ private final int[] VIEW_IDS = { R.id.mailbox_name };
+ private boolean mCreateShortcut = false;
+ private boolean mConfigureWidget = false;
+ private SimpleAdapter mAdapter;
+ private int mAppWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID;
+
+ // Boolean to indicate that we are waiting for the result from an add account
+ // operation. This boolean is necessary, as there is no guarantee on whether the
+ // AccountManager callback or onResume will be called first.
+ boolean mWaitingForAddAccountResult = false;
+
+ // Can only do certain actions if the Activity is resumed (e.g. setVisible)
+ private boolean mResumed = false;
+
+ @Override
+ public void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ setContentView(R.layout.mailbox_selection_activity);
+ if (icicle != null) {
+ restoreState(icicle);
+ } else {
+ if (Intent.ACTION_CREATE_SHORTCUT.equals(getIntent().getAction())) {
+ mCreateShortcut = true;
+ }
+ mAppWidgetId = getIntent().getIntExtra(
+ AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID);
+ if (mAppWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID) {
+ mConfigureWidget = true;
+ }
+ }
+ // We set the default title to "Gmail" or "Google Mail" for consistency
+ // in Task Switcher. If this is for create shortcut or configure widget,
+ // we should set the title to "Select account".
+ if (mCreateShortcut || mConfigureWidget) {
+ setTitle(getResources().getString(R.string.activity_mailbox_selection));
+ ActionBar actionBar = getActionBar();
+ if (actionBar != null) {
+ actionBar.setIcon(R.mipmap.ic_launcher_shortcut_folder);
+ }
+ }
+ ((Button) findViewById(R.id.cancel)).setOnClickListener(this);
+
+ // Initially, assume that the main view is invisible. It will be made visible,
+ // if we display the account list
+ setVisible(false);
+ setResult(RESULT_CANCELED);
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle icicle) {
+ super.onSaveInstanceState(icicle);
+
+ icicle.putBoolean(CREATE_SHORTCUT_KEY, mCreateShortcut);
+ icicle.putBoolean(CREATE_WIDGET_KEY, mConfigureWidget);
+ if (mAppWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID) {
+ icicle.putInt(WIDGET_ID_KEY, mAppWidgetId);
+ }
+ icicle.putBoolean(WAITING_FOR_ADD_ACCOUNT_RESULT_KEY, mWaitingForAddAccountResult);
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+
+ // Initially set up the state, with cached data, unless we are configuring
+ // a widget, where we want to wait for the real data
+ if (!mConfigureWidget) {
+ setupWithCachedAccounts();
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ mResumed = true;
+ // Only fetch the accounts, if we are not handling a response from the
+ // launched child activity.
+ if (!mWaitingForAddAccountResult) {
+ setupWithAccounts();
+ }
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ mResumed = false;
+ }
+
+ @Override
+ public void onNewIntent(Intent intent) {
+ super.onNewIntent(intent);
+ setIntent(intent);
+ }
+
+ /**
+ * Restores the activity state from a bundle
+ */
+ private void restoreState(Bundle icicle) {
+ if (icicle.containsKey(CREATE_SHORTCUT_KEY)) {
+ mCreateShortcut = icicle.getBoolean(CREATE_SHORTCUT_KEY);
+ }
+ if (icicle.containsKey(CREATE_WIDGET_KEY)) {
+ mConfigureWidget = icicle.getBoolean(CREATE_WIDGET_KEY);
+ }
+ if (icicle.containsKey(WIDGET_ID_KEY)) {
+ mAppWidgetId = icicle.getInt(WIDGET_ID_KEY);
+ }
+ if (icicle.containsKey(WAITING_FOR_ADD_ACCOUNT_RESULT_KEY)) {
+ mWaitingForAddAccountResult = icicle.getBoolean(WAITING_FOR_ADD_ACCOUNT_RESULT_KEY);
+ }
+ }
+
+ private void setupWithAccounts() {
+ final ContentResolver resolver = getContentResolver();
+ AsyncTask<Void, Void, Void> getAccounts = new AsyncTask<Void, Void, Void>() {
+ @Override
+ protected Void doInBackground(Void... params) {
+ Cursor cursor = resolver.query(AccountCacheProvider.getAccountsUri(),
+ UIProvider.ACCOUNTS_PROJECTION, null, null, null);
+ Account[] accounts = new Account[0];
+ if (cursor != null) {
+ accounts = new Account[cursor.getCount()];
+ int i = 0;
+ while (cursor.moveToNext()) {
+ accounts[i] = new Account(cursor);
+ i++;
+ }
+ }
+ completeSetupWithAccounts(accounts);
+ return null;
+ }
+
+ }.execute();
+ }
+
+ private void setupWithCachedAccounts() {
+ // TODO: (mindyp) have the AccountCacheProvider cache accounts.
+ }
+
+ private void completeSetupWithAccounts(Account[] accounts) {
+ // TODO: Cache the latest set of accounts
+ // AccountCacheProvider.cacheAccountList(this, false /* synced */,
+ // accounts);
+
+ // Populate the map
+ final List<Map<String, Object>> listData = Lists.newArrayList();
+ for (int i = 0; i < accounts.length; i++) {
+ Map<String, Object> m = Maps.newHashMap();
+ final String account = accounts[i].name;
+ m.put(ACCOUNT, account);
+
+ listData.add(m);
+ }
+ updateAccountList(listData);
+ }
+
+ private void updateAccountList(final List<Map<String, Object>> accountData) {
+ boolean displayAccountList = true;
+ // Configuring a widget or shortcut.
+ if (mConfigureWidget || mCreateShortcut) {
+ if (accountData.size() == 0) {
+ // No account found, show Add Account screen, for both the widget or
+ // shortcut creation process
+ // TODO: (mindyp) allow for adding of account.
+ // No reason to display the account list
+ displayAccountList = false;
+
+ // Indicate that we need to handle the response from the add account action
+ // This allows us to process the results that we get in the AddAccountCallback
+ mWaitingForAddAccountResult = true;
+ } else if (mConfigureWidget && accountData.size() == 1) {
+ // When configuring a widget, if there is only one account, automatically
+ // choose that account.
+ selectAccount((String) accountData.get(0).get(ACCOUNT));
+ // No reason to display the account list
+ displayAccountList = false;
+ }
+ }
+
+ if (displayAccountList) {
+ // We are about to display the list, make this activity visible
+ // But only if the Activity is not paused!
+ if (mResumed) {
+ setVisible(true);
+ }
+
+ mAdapter = new SimpleAdapter(
+ this, accountData, R.layout.mailbox_item, COLUMN_NAMES, VIEW_IDS) {
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ View v = super.getView(position, convertView, parent);
+ TextView accountView = (TextView) v.findViewById(R.id.mailbox_name);
+
+ Map<String, Object> data = accountData.get(position);
+ String account = (String) data.get(ACCOUNT);
+ accountView.setText(account);
+
+ // Set the tag here so we can get the account name in
+ // OnListItemClick()
+ v.setTag(account);
+
+ return v;
+ }
+ };
+ setListAdapter(mAdapter);
+ }
+ }
+
+ @Override
+ protected void onListItemClick(ListView l, View v, int position, long id) {
+ if (v != null) {
+ String selectedAccount = (String) v.getTag();
+ selectAccount(selectedAccount);
+ }
+ }
+
+ private void selectAccount(String account) {
+ if (mCreateShortcut || mConfigureWidget) {
+ // Invoked for a shortcut creation
+ final Intent intent = new Intent(this, FolderSelectionActivity.class);
+ intent.setFlags(
+ Intent.FLAG_ACTIVITY_NO_HISTORY | Intent.FLAG_ACTIVITY_FORWARD_RESULT);
+ intent.setAction(mCreateShortcut ?
+ Intent.ACTION_CREATE_SHORTCUT : AppWidgetManager.ACTION_APPWIDGET_CONFIGURE);
+ if (mConfigureWidget) {
+ intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId);
+ }
+ intent.putExtra(FolderSelectionActivity.EXTRA_ACCOUNT_SHORTCUT, account);
+ startActivity(intent);
+ finish();
+ } else {
+ // TODO: (mindyp) handle changing the account for this shortcut.
+ finish();
+ }
+ }
+
+ @Override
+ public void onClick(View v) {
+ switch (v.getId()) {
+ case R.id.cancel:
+ setResult(RESULT_CANCELED);
+ finish();
+ break;
+ }
+ }
+}
diff --git a/src/com/android/mail/ui/ShortcutNameActivity.java b/src/com/android/mail/ui/ShortcutNameActivity.java
new file mode 100644
index 0000000..d17570d
--- /dev/null
+++ b/src/com/android/mail/ui/ShortcutNameActivity.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+package com.android.mail.ui;
+
+import android.app.ActionBar;
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.text.Editable;
+import android.text.Selection;
+import android.text.TextUtils;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.inputmethod.EditorInfo;
+import android.widget.EditText;
+import android.widget.TextView;
+
+import com.android.mail.R;
+
+/**
+ * This activity prompts the user for a name for the shortcut to their label
+ * This activity could, in the future, be used to allow the user to specify an
+ * icon for the shortcut
+ */
+public class ShortcutNameActivity extends Activity implements OnClickListener,
+ TextView.OnEditorActionListener {
+ /*package*/static final String EXTRA_LABEL_CLICK_INTENT = "extra_label_click_intent";
+
+ /*package*/static final String EXTRA_SHORTCUT_NAME = "extra_shortcut_name";
+
+ private EditText mLabelText;
+ private String mLabelName;
+
+ private Intent mShortcutClickIntent;
+
+ @Override
+ public void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+ setContentView(R.layout.shortcut_name_activity);
+
+ mShortcutClickIntent = (Intent)getIntent().getParcelableExtra(EXTRA_LABEL_CLICK_INTENT);
+ mLabelName = getIntent().getStringExtra(EXTRA_SHORTCUT_NAME);
+
+ mLabelText = (EditText) findViewById(R.id.folder_text);
+ mLabelText.setText(mLabelName);
+ mLabelText.setOnEditorActionListener(this);
+
+ // Set focus to end of input line
+ mLabelText.requestFocus();
+ Editable editableText = mLabelText.getText();
+ Selection.setSelection(editableText, editableText.length());
+
+ findViewById(R.id.done).setOnClickListener(this);
+ findViewById(R.id.cancel).setOnClickListener(this);
+ ActionBar actionBar = getActionBar();
+ if (actionBar != null) {
+ actionBar.setIcon(R.mipmap.ic_launcher_shortcut_folder);
+ }
+ }
+
+ public void onClick(View v) {
+ final int id = v.getId();
+ if (R.id.done == id) {
+ doCreateShortcut();
+ } else if (R.id.cancel == id) {
+ doCancel();
+ }
+ }
+
+ public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+ if (actionId == EditorInfo.IME_ACTION_DONE) {
+ doCreateShortcut();
+ return true;
+ }
+ return false;
+ }
+
+ private void doCreateShortcut() {
+ // Get the name that the user entered
+ CharSequence userShortcutName = mLabelText.getText();
+
+ Intent resultIntent = new Intent();
+ resultIntent.putExtra(EXTRA_LABEL_CLICK_INTENT, mShortcutClickIntent);
+ // Initially set the name of the shortcut to the name of the label.
+ // If the user sets a valid name, the user specified one will be used.
+ resultIntent.putExtra(Intent.EXTRA_SHORTCUT_NAME, mLabelName);
+
+ final String shortcutName = userShortcutName.toString();
+ if (TextUtils.getTrimmedLength(shortcutName) > 0) {
+ mShortcutClickIntent.putExtra(Intent.EXTRA_SHORTCUT_NAME, shortcutName);
+ }
+
+ setResult(RESULT_OK, mShortcutClickIntent);
+ finish();
+ }
+
+ private void doCancel() {
+ setResult(RESULT_CANCELED);
+ finish();
+ }
+
+}
diff --git a/src/com/android/mail/ui/SyncForWidgetDialog.java b/src/com/android/mail/ui/SyncForWidgetDialog.java
new file mode 100644
index 0000000..f355970
--- /dev/null
+++ b/src/com/android/mail/ui/SyncForWidgetDialog.java
@@ -0,0 +1,95 @@
+// Copyright 2012 Google Inc. All Rights Reserved.
+
+package com.android.mail.ui;
+
+import android.content.res.Resources;
+import android.view.View;
+
+import android.app.AlertDialog;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.database.Cursor;
+import android.os.AsyncTask;
+import android.view.LayoutInflater;
+import android.widget.CheckBox;
+
+import com.android.mail.R;
+import com.android.mail.providers.Account;
+import com.android.mail.providers.Folder;
+import com.google.common.collect.Sets;
+
+import java.util.Set;
+
+/**
+ * A dialog for users attempting to create a widget for a non-synced label.
+ */
+public class SyncForWidgetDialog extends AlertDialog
+ implements DialogInterface.OnClickListener {
+
+ private static final long SYNC_DAYS = 30;
+ private final Account mAccount;
+ private final Folder mFolder;
+
+ private final DialogInterface.OnClickListener mConfirmClickListener;
+
+ public SyncForWidgetDialog(Context context, Account account, Folder folder,
+ DialogInterface.OnClickListener confirmWidgetCreationListener) {
+ super(context);
+
+ mAccount = account;
+ mFolder = folder;
+ mConfirmClickListener = confirmWidgetCreationListener;
+ final ContentResolver resolver = context.getContentResolver();
+
+ // Get the current sync window for the specified account
+ final Cursor settings = mAccount.getSettings();
+ // TODO: get sync days from settings.
+ final long syncDays = SYNC_DAYS;
+
+ final Resources res = context.getResources();
+
+ setTitle(R.string.folder_sync_for_widget_title);
+ setIcon(res.getDrawable(R.mipmap.ic_launcher_mail));
+ setButton(DialogInterface.BUTTON_POSITIVE, context.getString(android.R.string.ok), this);
+ setButton(DialogInterface.BUTTON_NEGATIVE,
+ context.getString(android.R.string.cancel), this);
+
+ LayoutInflater inflater = (LayoutInflater) context.getSystemService(
+ Context.LAYOUT_INFLATER_SERVICE);
+ final View dialogContents = inflater.inflate(R.layout.folder_sync_for_widget_dialog, null);
+ final CheckBox checkboxView =
+ (CheckBox) dialogContents.findViewById(R.id.folder_sync_for_widget_confirm);
+ checkboxView.setText(res.getString(R.string.folder_sync_for_widget_checkbox, syncDays));
+ setView(dialogContents);
+ }
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ if (which == DialogInterface.BUTTON_POSITIVE) {
+ CheckBox confirmView = (CheckBox) findViewById(R.id.folder_sync_for_widget_confirm);
+
+ if (confirmView.isChecked()) {
+
+ AsyncTask<Context, Void, Void> enableLabelSyncTask =
+ new AsyncTask<Context, Void, Void>() {
+
+ @Override
+ protected Void doInBackground(Context... params) {
+ final Context context = params[0];
+ final Cursor settings = mAccount.getSettings();
+ // TODO: (mindyp) enable syncing of folder.
+
+ return null;
+ }
+
+ };
+ enableLabelSyncTask.execute(getContext());
+ }
+ }
+
+ // Call the activity the created this widget, to let them know that the user pressed OK
+ mConfirmClickListener.onClick(dialog, which);
+ }
+
+}
diff --git a/src/com/android/mail/utils/DelayedTaskHandler.java b/src/com/android/mail/utils/DelayedTaskHandler.java
new file mode 100644
index 0000000..67c438c
--- /dev/null
+++ b/src/com/android/mail/utils/DelayedTaskHandler.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+
+package com.android.mail.utils;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.SystemClock;
+
+/**
+ * Class to queue tasks. This delay the specified task by the specified time, up to the maximum
+ * duration from the time that the first request was received
+ */
+public abstract class DelayedTaskHandler extends Handler {
+ private final int mDelayMs;
+
+ private static final long UNSET_TIME = -1;
+
+ /**
+ * Tracks the time when a task was queued, when there had been no pending task.
+ */
+ private long mLastTaskExecuteTime = UNSET_TIME;
+
+ public DelayedTaskHandler(Looper looper, int defaultDelayMs) {
+ super(looper);
+ mDelayMs = defaultDelayMs;
+ }
+
+ /**
+ * Schedule the task to be run after the default delay.
+ */
+ public void scheduleTask() {
+ final long currentTime = SystemClock.elapsedRealtime();
+ removeMessages(0);
+ if (mLastTaskExecuteTime == UNSET_TIME ||
+ ((mLastTaskExecuteTime + mDelayMs) < currentTime)) {
+ // If this is the first task that has been queued or if the last task ran more than
+ // long enough ago that we don't want to delay this task, run it now
+ sendEmptyMessage(0);
+ } else {
+ // Otherwise delay this task for the specify delay duration
+ sendEmptyMessageDelayed(0, mDelayMs);
+ }
+ }
+
+ @Override
+ public void dispatchMessage(Message msg) {
+ onTaskExecution();
+ performTask();
+ }
+
+ /**
+ * Called when the task managed by this handler is executed. This method can also be called
+ * to indicate that the task has been started externally.
+ *
+ * This updates the handler's internal timestamp.
+ */
+ public void onTaskExecution() {
+ mLastTaskExecuteTime = SystemClock.elapsedRealtime();
+ }
+
+ /**
+ * Method to perform the needed task.
+ */
+ protected abstract void performTask();
+}
diff --git a/src/com/android/mail/utils/Utils.java b/src/com/android/mail/utils/Utils.java
index 70a6dee..168d6cd 100644
--- a/src/com/android/mail/utils/Utils.java
+++ b/src/com/android/mail/utils/Utils.java
@@ -17,8 +17,10 @@
package com.android.mail.utils;
import android.content.Context;
+import android.content.Intent;
import android.content.res.Resources;
import android.graphics.Typeface;
+import android.net.Uri;
import android.text.Html;
import android.text.Spannable;
import android.text.SpannableString;
@@ -36,10 +38,13 @@
import android.webkit.WebView;
import com.android.mail.R;
+import com.android.mail.browse.ConversationCursor;
+import com.android.mail.providers.Account;
+import com.android.mail.providers.Folder;
+import com.android.mail.providers.UIProvider;
import com.google.common.collect.Maps;
import java.util.Map;
-import java.util.regex.Pattern;
public class Utils {
/**
@@ -59,6 +64,14 @@
public static String[] sSenderFragments = new String[8];
public static final String EXTRA_ACCOUNT = "account";
+ /*
+ * Notifies that changes happened. Certain UI components, e.g., widgets, can
+ * register for this {@link Intent} and update accordingly. However, this
+ * can be very broad and is NOT the preferred way of getting notification.
+ */
+ // TODO: UI Provider has this notification URI?
+ public static final String ACTION_NOTIFY_DATASET_CHANGED =
+ "com.android.mail.ACTION_NOTIFY_DATASET_CHANGED";
/**
* Sets WebView in a restricted mode suitable for email use.
@@ -324,8 +337,6 @@
}
maxChars -= fixedFragmentLength;
- final boolean normalMessagesExist = numMessagesFragment.length() != 0
- || maxFoundPriority != Integer.MIN_VALUE;
int maxPriorityToInclude = -1; // inclusive
int numCharsUsed = numMessagesFragment.length();
int numSendersUsed = 0;
@@ -535,4 +546,28 @@
}
return errors[status];
}
+
+ /**
+ * @return an intent which, if launched, will display the corresponding conversation
+ */
+ public static Intent createViewConversationIntent(Context context,
+ Account account, Folder folder, long conversationId) {
+ final Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ Uri.Builder builder = new Uri.Builder();
+ builder.scheme("content");
+ builder.authority(UIProvider.AUTHORITY);
+ builder.appendEncodedPath("account/" + account);
+ final String intentLabel = folder.name != null ? folder.name
+ : account.getAccountInbox().name;
+ builder.appendPath("label");
+ builder.appendPath(intentLabel);
+
+ if (conversationId != UIProvider.INVALID_CONVERSATION_ID) {
+ builder.appendEncodedPath("conversationId/" + conversationId);
+ }
+ intent.setDataAndType(builder.build(), "application/" + UIProvider.AUTHORITY);
+
+ return intent;
+ }
}
diff --git a/src/com/android/mail/widget/BaseWidgetProvider.java b/src/com/android/mail/widget/BaseWidgetProvider.java
new file mode 100644
index 0000000..3bd9d0c
--- /dev/null
+++ b/src/com/android/mail/widget/BaseWidgetProvider.java
@@ -0,0 +1,336 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+
+package com.android.mail.widget;
+
+import com.android.mail.ConversationListContext;
+import com.android.mail.R;
+import com.android.mail.providers.Account;
+import com.android.mail.providers.AccountCacheProvider;
+import com.android.mail.providers.Folder;
+import com.android.mail.providers.UIProvider;
+import com.android.mail.ui.MailboxSelectionActivity;
+import com.android.mail.utils.Utils;
+import com.google.common.collect.Sets;
+import com.google.common.primitives.Ints;
+
+import android.accounts.AccountManager;
+import android.app.PendingIntent;
+import android.appwidget.AppWidgetManager;
+import android.appwidget.AppWidgetProvider;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.CursorLoader;
+import android.content.Intent;
+import android.content.Loader;
+import android.content.SharedPreferences.Editor;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.text.TextUtils;
+import android.view.View;
+import android.widget.RemoteViews;
+
+import java.util.Set;
+
+public class BaseWidgetProvider extends AppWidgetProvider {
+ public static final String EXTRA_ACCOUNT = "account";
+ public static final String EXTRA_FOLDER = "folder";
+ public static final String EXTRA_UNREAD = "unread";
+ public static final String WIDGET_ACCOUNT_PREFIX = "widget-account-";
+
+ private static final String ACCOUNT_FOLDER_PREFERENCE_SEPARATOR = " ";
+
+ private static String createWidgetPreferenceValue(Account account, Folder folder) {
+ return account.name + ACCOUNT_FOLDER_PREFERENCE_SEPARATOR + folder.name;
+
+ }
+
+ /**
+ * Persists the information about the specified widget.
+ */
+ static void saveWidgetInformation(Context context, int appWidgetId, Account account,
+ Folder folder) {
+ // TODO: (mindyp) save widget information.
+ /*
+ * Editor editor = Persistence.getPreferences(context).edit();
+ * editor.putString(WidgetProvider.WIDGET_ACCOUNT_PREFIX + appWidgetId,
+ * BaseWidgetProvider.createWidgetPreferenceValue(account, folder));
+ * editor.apply();
+ */
+ }
+
+ /**
+ * Remove preferences when deleting widget
+ */
+ @Override
+ public void onDeleted(Context context, int[] appWidgetIds) {
+ super.onDeleted(context, appWidgetIds);
+
+ // TODO: (mindyp) save widget information.
+ /*
+ * Editor editor = Persistence.getPreferences(context).edit(); for (int
+ * i = 0; i < appWidgetIds.length; ++i) { // Remove the account in the
+ * preference editor.remove(WIDGET_ACCOUNT_PREFIX + appWidgetIds[i]); }
+ * editor.apply();
+ */
+ }
+
+ /**
+ * @return the list ids for the currently configured widgets.
+ */
+ private static int[] getCurrentWidgetIds(Context context) {
+ final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
+ final ComponentName mailComponent = new ComponentName(context, WidgetProvider.PROVIDER_NAME);
+ return appWidgetManager.getAppWidgetIds(mailComponent);
+ }
+
+ /**
+ * Catches ACTION_NOTIFY_DATASET_CHANGED intent and update the corresponding
+ * widgets.
+ */
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ super.onReceive(context, intent);
+
+ // Receive notification for a certain account.
+ if (Utils.ACTION_NOTIFY_DATASET_CHANGED.equals(intent.getAction())) {
+ final String accountToBeUpdated = intent.getExtras().getString(Utils.EXTRA_ACCOUNT);
+ if (accountToBeUpdated == null) {
+ return;
+ }
+
+ final Set<Integer> widgetsToUpdate = Sets.newHashSet();
+
+ for (int id : getCurrentWidgetIds(context)) {
+ // Retrieve the persisted information for this widget from
+ // preferences.
+ // TODO: (mindyp) get widget preferences.
+ /*
+ * final String accountFolder =
+ * Persistence.getPreferences(context)
+ * .getString(WIDGET_ACCOUNT_PREFIX + id, null); // If the
+ * account matched, update the widget. if (accountFolder !=
+ * null) { final String[] parsedInfo = TextUtils.split(
+ * accountFolder, ACCOUNT_FOLDER_PREFERENCE_SEPARATOR); if
+ * (accountToBeUpdated.equals(parsedInfo[0])) {
+ * widgetsToUpdate.add(id); } }
+ */
+ }
+ if (widgetsToUpdate.size() > 0) {
+ final int[] widgets = Ints.toArray(widgetsToUpdate);
+ AppWidgetManager.getInstance(context).notifyAppWidgetViewDataChanged(widgets,
+ R.id.conversation_list);
+ }
+ }
+ }
+
+ /**
+ * Update all widgets in the list
+ */
+ @Override
+ public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
+ super.onUpdate(context, appWidgetManager, appWidgetIds);
+
+ // Update each of the widgets with a remote adapter
+ for (int i = 0; i < appWidgetIds.length; ++i) {
+ // Get the account for this widget from preference
+ // TODO: (mindyp) get widget preferences.
+ /*
+ * String accountFolder =
+ * Persistence.getPreferences(context).getString(
+ * WIDGET_ACCOUNT_PREFIX + appWidgetIds[i], null);
+ */
+ String accountFolder = null;
+ String account = null;
+ String folder = null;
+ if (!TextUtils.isEmpty(accountFolder)) {
+ final String[] parsedInfo = TextUtils.split(accountFolder,
+ ACCOUNT_FOLDER_PREFERENCE_SEPARATOR);
+ if (parsedInfo.length == 2) {
+ account = parsedInfo[0];
+ folder = parsedInfo[1];
+ } else {
+ // TODO: (mindyp) how can we lookup the associated account?
+ // AccountCacheProvider?
+ account = accountFolder;
+ folder = "inbox"; // account.getAccountInbox(context,
+ // account);
+ }
+ }
+ // account will be null the first time a widget is created. This is
+ // OK, as
+ // isAccountValid will return false, allowing the widget to be
+ // configured
+ updateWidget(context, appWidgetIds[i], new Account(Parcel.obtain()),
+ new Folder(Parcel.obtain()));
+ }
+ }
+
+ private static boolean isAccountValid(Context context, Account account) {
+ if (account != null) {
+ Cursor accountCursor = context.getContentResolver().query(
+ AccountCacheProvider.getAccountsUri(), UIProvider.ACCOUNTS_PROJECTION, null,
+ null, null);
+ accountCursor.moveToFirst();
+ do {
+ if (account != null && account.equals(new Account(accountCursor)))
+ return true;
+ } while (accountCursor.moveToNext());
+ return false;
+ }
+ return false;
+ }
+
+ /**
+ * Returns if a widget is configured with the given account.
+ */
+ public static boolean isWidgetConfigured(Context context, int appWidgetId, Account account,
+ Folder folder) {
+ if (isAccountValid(context, account)) {
+ // TODO: (mindyp) get widget preferences.
+ /*
+ * final String accountFolder =
+ * Persistence.getPreferences(context).getString(
+ * WIDGET_ACCOUNT_PREFIX + appWidgetId, null);
+ */
+ // Return true if this widget id has been configured and saved.
+ final String accountFolder = null;
+ return accountFolder != null;
+ }
+ return false;
+ }
+
+ /**
+ * Update the widget appWidgetId with the given account and folder
+ */
+ public static void updateWidget(Context context, int appWidgetId, Account account, Folder folder) {
+ RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.widget);
+ PendingIntent clickIntent;
+ final boolean isAccountValid = isAccountValid(context, account);
+
+ if (!isAccountValid) {
+ // Widget has not been configured yet
+ remoteViews.setViewVisibility(R.id.widget_folder, View.GONE);
+ remoteViews.setViewVisibility(R.id.widget_account, View.GONE);
+ remoteViews.setViewVisibility(R.id.widget_unread_count, View.GONE);
+ remoteViews.setViewVisibility(R.id.widget_compose, View.GONE);
+ remoteViews.setViewVisibility(R.id.conversation_list, View.GONE);
+ remoteViews.setViewVisibility(R.id.widget_folder_not_synced, View.GONE);
+ remoteViews.setViewVisibility(R.id.widget_configuration, View.VISIBLE);
+
+ final Intent configureIntent = new Intent(context, MailboxSelectionActivity.class);
+ configureIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
+ configureIntent.setData(Uri.parse(configureIntent.toUri(Intent.URI_INTENT_SCHEME)));
+ configureIntent.setFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
+ clickIntent = PendingIntent.getActivity(context, 0, configureIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT);
+ remoteViews.setOnClickPendingIntent(R.id.widget_configuration, clickIntent);
+ } else {
+ // Set folder to a space here to avoid flicker.
+ configureValidAccountWidget(context, remoteViews, appWidgetId, account, folder, " ");
+
+ }
+ AppWidgetManager.getInstance(context).updateAppWidget(appWidgetId, remoteViews);
+ }
+
+ private static boolean isFolderSynchronized(Context context, Account account, Folder folder) {
+ // Determine if the folder has been configured to be synced
+ final ContentResolver resolver = context.getContentResolver();
+
+ // TODO: (mindyp) Get the current sync window for the specified account
+ final Cursor settings = account.getSettings();
+ final Set<String> synchronizedFoldersSet = Sets.newHashSet();
+
+ // TODO: Add all of the synchronized folders to the set
+ // synchronizedFoldersSet.addAll(settings.getFoldersIncluded());
+ // synchronizedFoldersSet.addAll(settings.getFoldersPartial());
+
+ return synchronizedFoldersSet.contains(folder);
+ }
+
+ /**
+ * Modifies the remoteView for the given account and folder.
+ */
+ static void configureValidAccountWidget(Context context, RemoteViews remoteViews,
+ int appWidgetId, Account account, Folder folder, String folderDisplayName) {
+ PendingIntent clickIntent;// Widget is configured, now display the
+ // chosen account.
+
+ final boolean isFolderSynchronized = isFolderSynchronized(context, account, folder);
+
+ remoteViews.setViewVisibility(R.id.widget_folder, View.VISIBLE);
+ remoteViews.setTextViewText(R.id.widget_folder, folderDisplayName);
+ remoteViews.setViewVisibility(R.id.widget_account, View.VISIBLE);
+ remoteViews.setTextViewText(R.id.widget_account, account.name);
+ remoteViews.setViewVisibility(R.id.widget_unread_count, View.VISIBLE);
+ remoteViews.setViewVisibility(R.id.widget_compose, View.VISIBLE);
+ if (isFolderSynchronized) {
+ remoteViews.setViewVisibility(R.id.conversation_list, View.VISIBLE);
+ remoteViews.setViewVisibility(R.id.widget_folder_not_synced, View.GONE);
+ } else {
+ remoteViews.setViewVisibility(R.id.conversation_list, View.GONE);
+ remoteViews.setViewVisibility(R.id.widget_folder_not_synced, View.VISIBLE);
+
+ // TODO: (mindyp) Configure the remote view so clicks will launch an
+ // activity that
+ // will allow the user to enable sync for this folder.
+ // remoteViews.setOnClickPendingIntent(R.id.widget_folder_not_synced,
+ // clickIntent);
+ }
+ remoteViews.setViewVisibility(R.id.widget_configuration, View.GONE);
+
+ // Launch an intent to avoid ANRs
+ final Intent intent = new Intent(context, WidgetService.class);
+ intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
+ intent.putExtra(EXTRA_ACCOUNT, account);
+ intent.putExtra(EXTRA_FOLDER, folder);
+ intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));
+ remoteViews.setRemoteAdapter(R.id.conversation_list, intent);
+
+ // Open mail app when click on header
+ final Intent mailIntent = Utils.createViewConversationIntent(context, account, folder,
+ UIProvider.INVALID_CONVERSATION_ID);
+ clickIntent = PendingIntent.getActivity(context, 0, mailIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT);
+ remoteViews.setOnClickPendingIntent(R.id.widget_header, clickIntent);
+
+ // On click intent for Compose
+ final Intent composeIntent = new Intent();
+ composeIntent.setAction(Intent.ACTION_SEND);
+ composeIntent.setData(Uri.parse("from://" + UIProvider.AUTHORITY + "/account/" + account));
+ clickIntent = PendingIntent.getActivity(context, 0, composeIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT);
+ remoteViews.setOnClickPendingIntent(R.id.widget_compose, clickIntent);
+
+ // On click intent for Conversation
+ final Intent conversationIntent = new Intent();
+ conversationIntent.setAction(Intent.ACTION_VIEW);
+ clickIntent = PendingIntent.getActivity(context, 0, conversationIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT);
+ remoteViews.setPendingIntentTemplate(R.id.conversation_list, clickIntent);
+ }
+
+ /**
+ * Updates all of the configured widgets.
+ */
+ public static void updateAllWidgets(Context context) {
+ AppWidgetManager.getInstance(context).notifyAppWidgetViewDataChanged(
+ getCurrentWidgetIds(context), R.id.conversation_list);
+ }
+}
diff --git a/src/com/android/mail/widget/README b/src/com/android/mail/widget/README
deleted file mode 100644
index a3fcb75..0000000
--- a/src/com/android/mail/widget/README
+++ /dev/null
@@ -1 +0,0 @@
-Unified email directories
\ No newline at end of file
diff --git a/src/com/android/mail/widget/WidgetConversationViewBuilder.java b/src/com/android/mail/widget/WidgetConversationViewBuilder.java
new file mode 100644
index 0000000..eefca0c
--- /dev/null
+++ b/src/com/android/mail/widget/WidgetConversationViewBuilder.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+
+package com.android.mail.widget;
+
+import com.android.mail.R;
+import com.android.mail.providers.Account;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Typeface;
+import android.text.Spannable;
+import android.text.SpannableStringBuilder;
+import android.text.style.AbsoluteSizeSpan;
+import android.text.style.ForegroundColorSpan;
+import android.text.style.StyleSpan;
+import android.view.View;
+import android.widget.RemoteViews;
+
+import java.util.Map;
+
+public class WidgetConversationViewBuilder {
+ // Static font sizes
+ private static int SENDERS_FONT_SIZE;
+ private static int DATE_FONT_SIZE;
+ private static int SUBJECT_FONT_SIZE;
+
+ // Static colors
+ private static int SUBJECT_TEXT_COLOR_READ;
+ private static int SUBJECT_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 DRAFT_TEXT_COLOR;
+
+ // Static bitmap
+ private static Bitmap ATTACHMENT;
+
+ private final Context mContext;
+
+ /*
+ * Get font sizes and bitmaps from Resources
+ */
+ public WidgetConversationViewBuilder(Context context, Account account) {
+ mContext = context;
+ Resources res = context.getResources();
+
+ // Initialize font sizes
+ SENDERS_FONT_SIZE = res.getDimensionPixelSize(R.dimen.widget_senders_font_size);
+ DATE_FONT_SIZE = res.getDimensionPixelSize(R.dimen.widget_date_font_size);
+ SUBJECT_FONT_SIZE = res.getDimensionPixelSize(R.dimen.widget_subject_font_size);
+
+ // Initialize colors
+ SUBJECT_TEXT_COLOR_READ = res.getColor(R.color.subject_text_color_read);
+ SUBJECT_TEXT_COLOR_UNREAD = res.getColor(R.color.subject_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);
+ DRAFT_TEXT_COLOR = res.getColor(R.color.drafts);
+
+ // Initialize Bitmap
+ ATTACHMENT = BitmapFactory.decodeResource(res, R.drawable.ic_attachment_holo_light);
+ }
+
+ /*
+ * Add size, color and style to a given text
+ */
+ private CharSequence addStyle(CharSequence text, int size, int color) {
+ SpannableStringBuilder builder = new SpannableStringBuilder(text);
+ builder.setSpan(
+ new AbsoluteSizeSpan(size), 0, text.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ if (color != 0) {
+ builder.setSpan(new ForegroundColorSpan(color), 0, text.length(),
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ return builder;
+ }
+
+ /*
+ * Return the full View
+ */
+ public RemoteViews getStyledView(
+ CharSequence senders, CharSequence status, CharSequence date, CharSequence subject,
+ CharSequence snippet, String folders, boolean hasAttachments) {
+
+ // TODO: (mindyp) determine unread state.
+ final boolean isUnread = true;
+
+ // Add style to senders
+ CharSequence styledSenders = addStyle(senders, SENDERS_FONT_SIZE,
+ isUnread ? SENDERS_TEXT_COLOR_UNREAD : SENDERS_TEXT_COLOR_READ);
+
+ // Add the status indicator
+ if (status.length() > 0) {
+ final SpannableStringBuilder builder = new SpannableStringBuilder(styledSenders);
+
+ if (senders.length() > 0) {
+ // TODO(pwestbro) sender formatting should use resources. Bug 5354473
+ builder.append(addStyle(", ", SENDERS_FONT_SIZE,
+ isUnread ? SENDERS_TEXT_COLOR_UNREAD : SENDERS_TEXT_COLOR_READ));
+ }
+
+ final CharSequence styledStatus = addStyle(status, SENDERS_FONT_SIZE, DRAFT_TEXT_COLOR);
+ styledSenders = builder.append(styledStatus);
+ }
+
+ // Add style to date
+ CharSequence styledDate = addStyle(date, DATE_FONT_SIZE, isUnread ? DATE_TEXT_COLOR_UNREAD
+ : DATE_TEXT_COLOR_READ);
+
+ // Add style to subject
+ int subjectColor = isUnread ? SUBJECT_TEXT_COLOR_UNREAD : SUBJECT_TEXT_COLOR_READ;
+ SpannableStringBuilder subjectAndSnippet = new SpannableStringBuilder(mContext.getString(
+ R.string.subject_and_snippet, subject, snippet));
+ if (isUnread) {
+ subjectAndSnippet.setSpan(new StyleSpan(Typeface.BOLD), 0, subject.length(),
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ subjectAndSnippet.setSpan(new ForegroundColorSpan(subjectColor), 0, subjectAndSnippet
+ .length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ CharSequence styledSubject = addStyle(subjectAndSnippet, SUBJECT_FONT_SIZE, 0);
+
+ // Paper clip for attachment
+ Bitmap paperclipBitmap = null;
+ if (hasAttachments) {
+ paperclipBitmap = ATTACHMENT;
+ }
+
+ // Inflate and fill out the remote view
+ RemoteViews remoteViews = new RemoteViews(
+ mContext.getPackageName(), R.layout.widget_conversation);
+ remoteViews.setTextViewText(R.id.widget_senders, styledSenders);
+ remoteViews.setTextViewText(R.id.widget_date, styledDate);
+ remoteViews.setTextViewText(R.id.widget_subject, styledSubject);
+ if (paperclipBitmap != null) {
+ remoteViews.setViewVisibility(R.id.widget_attachment, View.VISIBLE);
+ remoteViews.setImageViewBitmap(R.id.widget_attachment, paperclipBitmap);
+ } else {
+ remoteViews.setViewVisibility(R.id.widget_attachment, View.GONE);
+ }
+ if (isUnread) {
+ remoteViews.setViewVisibility(R.id.widget_unread_background, View.VISIBLE);
+ remoteViews.setViewVisibility(R.id.widget_read_background, View.GONE);
+ } else {
+ remoteViews.setViewVisibility(R.id.widget_unread_background, View.GONE);
+ remoteViews.setViewVisibility(R.id.widget_read_background, View.VISIBLE);
+ }
+
+ return remoteViews;
+ }
+}
diff --git a/src/com/android/mail/widget/WidgetProvider.java b/src/com/android/mail/widget/WidgetProvider.java
new file mode 100644
index 0000000..a035545
--- /dev/null
+++ b/src/com/android/mail/widget/WidgetProvider.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+
+package com.android.mail.widget;
+
+public class WidgetProvider extends BaseWidgetProvider {
+ public static final String PROVIDER_NAME = "com.android.mail.widget.WidgetProvider";
+}
diff --git a/src/com/android/mail/widget/WidgetService.java b/src/com/android/mail/widget/WidgetService.java
new file mode 100644
index 0000000..ac462ec
--- /dev/null
+++ b/src/com/android/mail/widget/WidgetService.java
@@ -0,0 +1,313 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+package com.android.mail.widget;
+
+import com.android.mail.R;
+import com.android.mail.browse.ConversationCursor;
+import com.android.mail.providers.Account;
+import com.android.mail.providers.Conversation;
+import com.android.mail.providers.Folder;
+import com.android.mail.providers.UIProvider;
+import com.android.mail.utils.DelayedTaskHandler;
+import com.android.mail.utils.LogUtils;
+import com.android.mail.utils.Utils;
+
+import android.appwidget.AppWidgetManager;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.CursorLoader;
+import android.content.Intent;
+import android.content.Loader;
+import android.content.Loader.OnLoadCompleteListener;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Looper;
+import android.text.SpannableStringBuilder;
+import android.text.TextUtils;
+import android.text.format.DateUtils;
+import android.view.View;
+import android.widget.RemoteViews;
+import android.widget.RemoteViewsService;
+
+import java.util.Map;
+
+public class WidgetService extends RemoteViewsService {
+ /**
+ * Lock to avoid race condition between widgets.
+ */
+ private static Object sWidgetLock = new Object();
+
+ @Override
+ public RemoteViewsFactory onGetViewFactory(Intent intent) {
+ return new MailFactory(getApplicationContext(), intent);
+ }
+
+ /**
+ * Remote Views Factory for Mail Widget.
+ */
+ private static class MailFactory
+ implements RemoteViewsService.RemoteViewsFactory, OnLoadCompleteListener<Cursor> {
+ private static final int MAX_CONVERSATIONS_COUNT = 25;
+ private static final int MAX_SENDERS_LENGTH = 25;
+ private static final String LOG_TAG = new LogUtils().getLogTag();
+
+ private final Context mContext;
+ private final int mAppWidgetId;
+ private final Account mAccount;
+ private final Folder mFolder;
+ private final WidgetConversationViewBuilder mWidgetConversationViewBuilder;
+ private Cursor mConversationCursor;
+ private CursorLoader mFolderLoader;
+ private FolderUpdateHandler mFolderUpdateHandler;
+ private int mFolderCount;
+ private boolean mShouldShowViewMore;
+ private boolean mFolderInformationShown = false;
+ private ContentResolver mResolver;
+
+ public MailFactory(Context context, Intent intent) {
+ mContext = context;
+ mAppWidgetId = intent.getIntExtra(
+ AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID);
+ mAccount = intent.getParcelableExtra(WidgetProvider.EXTRA_ACCOUNT);
+ final Folder folder = intent.getParcelableExtra(WidgetProvider.EXTRA_FOLDER);
+ mFolder = folder != null ? folder : mAccount.getAccountInbox();
+ mWidgetConversationViewBuilder = new WidgetConversationViewBuilder(mContext, mAccount);
+ mResolver = context.getContentResolver();
+ }
+
+ @Override
+ public void onCreate() {
+ // Save the map between widgetId and account to preference
+ BaseWidgetProvider.saveWidgetInformation(mContext, mAppWidgetId, mAccount, mFolder);
+
+ // If the account of this widget has been removed, we want to update the widget to
+ // "Tap to configure" mode.
+ if (!BaseWidgetProvider.isWidgetConfigured(mContext, mAppWidgetId, mAccount, mFolder)) {
+ BaseWidgetProvider.updateWidget(mContext, mAppWidgetId, mAccount, mFolder);
+ }
+
+ mConversationCursor = mResolver.query(Uri.parse(mFolder.conversationListUri),
+ UIProvider.CONVERSATION_PROJECTION, null, null, null);
+
+ mFolderLoader = new CursorLoader(mContext, Uri.parse(mFolder.uri),
+ UIProvider.FOLDERS_PROJECTION, null, null, null);
+ mFolderLoader.registerListener(0, this);
+ mFolderUpdateHandler = new FolderUpdateHandler(mContext.getResources().getInteger(
+ R.integer.widget_folder_refresh_delay_ms));
+ mFolderUpdateHandler.scheduleTask();
+
+ }
+
+ @Override
+ public void onDestroy() {
+ synchronized (sWidgetLock) {
+ if (mConversationCursor != null && !mConversationCursor.isClosed()) {
+ mConversationCursor.close();
+ mConversationCursor = null;
+ }
+ }
+
+ if (mFolderLoader != null) {
+ mFolderLoader.reset();
+ mFolderLoader = null;
+ }
+ }
+
+ @Override
+ public void onDataSetChanged() {
+ synchronized (sWidgetLock) {
+ // TODO: use loader manager.
+ mConversationCursor.requery();
+ }
+ mFolderUpdateHandler.scheduleTask();
+ }
+
+ /**
+ * Returns the number of items should be shown in the widget list. This method also updates
+ * the boolean that indicates whether the "show more" item should be shown.
+ * @return the number of items to be displayed in the list.
+ */
+ @Override
+ public int getCount() {
+ synchronized (sWidgetLock) {
+ final int count = getConversationCount();
+ mShouldShowViewMore = count < mConversationCursor.getCount()
+ || count < mFolderCount;
+ return count + (mShouldShowViewMore ? 1 : 0);
+ }
+ }
+
+ /**
+ * Returns the number of conversations that should be shown in the widget. This method
+ * doesn't update the boolean that indicates that the "show more" item should be included
+ * in the list.
+ * @return
+ */
+ private int getConversationCount() {
+ synchronized (sWidgetLock) {
+ return Math.min(mConversationCursor.getCount(), MAX_CONVERSATIONS_COUNT);
+ }
+ }
+
+ /**
+ * @return the {@link RemoteViews} for a specific position in the list.
+ */
+ @Override
+ public RemoteViews getViewAt(int position) {
+ synchronized (sWidgetLock) {
+ // "View more conversations" view.
+ if (mConversationCursor == null
+ || (mShouldShowViewMore && position >= getConversationCount())) {
+ return getViewMoreConversationsView();
+ }
+
+ if (!mConversationCursor.moveToPosition(position)) {
+ // If we ever fail to move to a position, return the "View More conversations"
+ // view.
+ LogUtils.e(LOG_TAG,
+ "Failed to move to position %d in the cursor.", position);
+ return getViewMoreConversationsView();
+ }
+
+ Conversation conversation = Conversation.from(mConversationCursor);
+ // Split the senders and status from the instructions.
+ SpannableStringBuilder senderBuilder = new SpannableStringBuilder();
+ SpannableStringBuilder statusBuilder = new SpannableStringBuilder();
+ Utils.getStyledSenderSnippet(mContext, conversation.senders, senderBuilder,
+ statusBuilder, MAX_SENDERS_LENGTH, false, false, false);
+
+ // Get styled date.
+ CharSequence date = DateUtils.getRelativeTimeSpanString(
+ mContext, conversation.dateMs);
+
+ // Load up our remote view.
+ RemoteViews remoteViews = mWidgetConversationViewBuilder.getStyledView(
+ senderBuilder, statusBuilder, date, filterTag(conversation.subject),
+ conversation.snippet, conversation.folderList, conversation.hasAttachments);
+
+ // On click intent.
+ remoteViews.setOnClickFillInIntent(R.id.widget_conversation,
+ Utils.createViewConversationIntent(mContext, mAccount, mFolder,
+ conversation.id));
+
+ return remoteViews;
+ }
+ }
+
+ /**
+ * @return the "View more conversations" view.
+ */
+ private RemoteViews getViewMoreConversationsView() {
+ RemoteViews view = new RemoteViews(mContext.getPackageName(), R.layout.widget_loading);
+ view.setTextViewText(
+ R.id.loading_text, mContext.getText(R.string.view_more_conversations));
+ view.setOnClickFillInIntent(R.id.widget_loading,
+ Utils.createViewConversationIntent(mContext, mAccount, mFolder,
+ UIProvider.INVALID_CONVERSATION_ID));
+ return view;
+ }
+
+ @Override
+ public RemoteViews getLoadingView() {
+ RemoteViews view = new RemoteViews(mContext.getPackageName(), R.layout.widget_loading);
+ view.setTextViewText(
+ R.id.loading_text, mContext.getText(R.string.loading_conversation));
+ return view;
+ }
+
+ @Override
+ public int getViewTypeCount() {
+ return 2;
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public boolean hasStableIds() {
+ return false;
+ }
+
+ @Override
+ public void onLoadComplete(Loader<Cursor> loader, Cursor data) {
+ if (!data.moveToFirst()) {
+ return;
+ }
+
+ final int unreadCount = data.getInt(UIProvider.FOLDER_UNREAD_COUNT_COLUMN);
+ final String folderName = data.getString(UIProvider.FOLDER_NAME_COLUMN);
+ mFolderCount = data.getInt(UIProvider.FOLDER_TOTAL_COUNT_COLUMN);
+
+ RemoteViews remoteViews = new RemoteViews(mContext.getPackageName(), R.layout.widget);
+ AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(mContext);
+
+ if (!mFolderInformationShown && !TextUtils.isEmpty(folderName)) {
+ // We want to do a full update to the widget at least once, as the widget
+ // manager doesn't cache the state of the remote views when doing a partial
+ // widget update. This causes the label name to be shown as blank if the state
+ // of the widget is restored.
+ BaseWidgetProvider.configureValidAccountWidget(
+ mContext, remoteViews, mAppWidgetId, mAccount, mFolder, folderName);
+ appWidgetManager.updateAppWidget(mAppWidgetId, remoteViews);
+ mFolderInformationShown = true;
+ }
+
+ remoteViews.setViewVisibility(R.id.widget_folder, View.VISIBLE);
+ remoteViews.setTextViewText(R.id.widget_folder, folderName);
+ remoteViews.setViewVisibility(R.id.widget_unread_count, View.VISIBLE);
+ remoteViews.setTextViewText(
+ R.id.widget_unread_count, Utils.getUnreadCountString(mContext, unreadCount));
+
+ appWidgetManager.partiallyUpdateAppWidget(mAppWidgetId, remoteViews);
+ }
+
+ /**
+ * 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 static String filterTag(String subject) {
+ String result = subject;
+ if (subject.length() > 0 && subject.charAt(0) == '[') {
+ int end = subject.indexOf(']');
+ if (end > 0) {
+ String tag = subject.substring(1, end);
+ result = "[" + Utils.ellipsize(tag, 7) + "]" + subject.substring(end + 1);
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * A {@link DelayedTaskHandler} to throttle label update to a reasonable rate.
+ */
+ private class FolderUpdateHandler extends DelayedTaskHandler {
+ public FolderUpdateHandler(int refreshDelay) {
+ super(Looper.myLooper(), refreshDelay);
+ }
+
+ @Override
+ protected void performTask() {
+ // Start the loader. The cached data will be returned if present.
+ if (mFolderLoader != null) {
+ mFolderLoader.startLoading();
+ }
+ }
+ }
+ }
+}