diff options
60 files changed, 1779 insertions, 102 deletions
diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 5ab5910..b58533c 100644..100755 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -58,7 +58,7 @@ <application android:name="com.android.messaging.BugleApplication" android:allowBackup="false" - android:icon="@drawable/ic_launcher" + android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:theme="@style/BugleTheme" android:supportsRtl="true"> @@ -424,7 +424,7 @@ </activity> <activity android:name=".ui.SmsStorageLowWarningActivity" - android:theme="@style/Invisible" + android:theme="@style/InvisibleNoDisplay" android:configChanges="orientation|screenSize|keyboardHidden" /> <activity android:name=".ui.appsettings.ApnSettingsActivity" diff --git a/res/drawable/ic_library_music_black_24px.xml b/res/drawable/ic_library_music_black_24px.xml new file mode 100755 index 0000000..761d839 --- /dev/null +++ b/res/drawable/ic_library_music_black_24px.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="utf-8"?> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + + <path + android:pathData="M0 0h24v24H0z" /> + <path + android:fillColor="#000000" + android:pathData="M20 2H8c-1.1 0-2 .9-2 2v12c0 1.1 .9 2 2 2h12c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-2 +5h-3v5.5c0 1.38-1.12 2.5-2.5 2.5S10 13.88 10 12.5s1.12-2.5 2.5-2.5c.57 0 1.08 +.19 1.5 .51 V5h4v2zM4 6H2v14c0 1.1 .9 2 2 2h14v-2H4V6z" /> +</vector>
\ No newline at end of file diff --git a/res/drawable/ic_library_music_white_24px.xml b/res/drawable/ic_library_music_white_24px.xml new file mode 100755 index 0000000..28ac927 --- /dev/null +++ b/res/drawable/ic_library_music_white_24px.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="utf-8"?> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + + <path + android:pathData="M0 0h24v24H0z" /> + <path + android:fillColor="#FFFFFF" + android:pathData="M20 2H8c-1.1 0-2 .9-2 2v12c0 1.1 .9 2 2 2h12c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-2 +5h-3v5.5c0 1.38-1.12 2.5-2.5 2.5S10 13.88 10 12.5s1.12-2.5 2.5-2.5c.57 0 1.08 +.19 1.5 .51 V5h4v2zM4 6H2v14c0 1.1 .9 2 2 2h14v-2H4V6z" /> +</vector>
\ No newline at end of file diff --git a/res/drawable/ic_play_circle_filled_white_24px.xml b/res/drawable/ic_play_circle_filled_white_24px.xml new file mode 100755 index 0000000..2f3b1b9 --- /dev/null +++ b/res/drawable/ic_play_circle_filled_white_24px.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="utf-8"?> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + + <path + android:pathData="M0 0h24v24H0z" /> + <path + android:fillColor="#FFFFFF" + android:pathData="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 14.5v-9l6 +4.5-6 4.5z" /> +</vector>
\ No newline at end of file diff --git a/res/layout/audio_list_item_view.xml b/res/layout/audio_list_item_view.xml new file mode 100644 index 0000000..e22acc1 --- /dev/null +++ b/res/layout/audio_list_item_view.xml @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2016 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. +--> +<com.android.messaging.ui.mediapicker.AudioListItemView + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="@dimen/audio_list_item_height" + android:background="@color/gallery_image_default_background" + android:clickable="true"> + + <FrameLayout + android:layout_gravity="center_vertical" + android:layout_marginLeft="@dimen/audio_list_item_icon_margin" + android:layout_width="@dimen/audio_list_item_icon_size" + android:layout_height="@dimen/audio_list_item_icon_size"> + + <CheckBox + android:id="@+id/audio_checkbox" + style="@style/GalleryGridItemViewCheckBoxStyle" + android:button="@drawable/gallery_checkbox_selector" + android:layout_width="match_parent" + android:layout_height="match_parent"/> + + <ImageView + android:id="@+id/audio_button" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="@drawable/transparent_button_background" + android:scaleType="fitCenter" + android:src="@drawable/ic_library_music_black_24px" + android:contentDescription="@string/video_thumbnail_view_play_button_content_description"/> + + </FrameLayout> + + <TextView + android:id="@+id/audio_filename" + android:layout_width="match_parent" + android:layout_height="@dimen/audio_list_item_text_height" + android:layout_marginLeft="@dimen/audio_list_item_text_margin" + android:layout_gravity="center_vertical" + android:textColor="@color/button_text"/> + +</com.android.messaging.ui.mediapicker.AudioListItemView> diff --git a/res/layout/conversation_list_item_view.xml b/res/layout/conversation_list_item_view.xml index 636616b..43cb3ee 100644 --- a/res/layout/conversation_list_item_view.xml +++ b/res/layout/conversation_list_item_view.xml @@ -37,7 +37,6 @@ android:layout_height="wrap_content" android:gravity="center_vertical|left" android:visibility="gone" - android:src="@drawable/ic_archive_small_dark" android:importantForAccessibility="no" android:contentDescription="@null"/> <FrameLayout @@ -50,7 +49,6 @@ android:layout_height="wrap_content" android:layout_gravity="center_vertical|right" android:visibility="gone" - android:src="@drawable/ic_archive_small_dark" android:importantForAccessibility="no" android:contentDescription="@null"/> </LinearLayout> diff --git a/res/layout/gallery_grid_item_view.xml b/res/layout/gallery_grid_item_view.xml index 8b7ee58..c5e32df 100644 --- a/res/layout/gallery_grid_item_view.xml +++ b/res/layout/gallery_grid_item_view.xml @@ -27,6 +27,22 @@ android:layout_height="match_parent" android:scaleType="centerCrop" /> + <FrameLayout + android:id="@+id/video_button" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_gravity="center" + android:visibility="invisible"> + <ImageView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:background="@drawable/transparent_button_background" + android:scaleType="fitCenter" + android:src="@drawable/ic_play_circle_filled_white_24px" + android:contentDescription="@string/video_thumbnail_view_play_button_content_description"/> + </FrameLayout> + <View android:layout_width="match_parent" android:layout_height="match_parent" diff --git a/res/layout/mediapicker_audio_list_chooser.xml b/res/layout/mediapicker_audio_list_chooser.xml new file mode 100644 index 0000000..fbab0bc --- /dev/null +++ b/res/layout/mediapicker_audio_list_chooser.xml @@ -0,0 +1,39 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2016 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:layout_width="match_parent" + android:layout_height="match_parent"> + + <com.android.messaging.ui.mediapicker.AudioListView + android:id="@+id/audio_list_view" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:padding="6dp" + android:background="@android:color/white" /> + + <!-- This view will hide all other views if the required permission is not granted --> + <TextView + android:id="@+id/missing_permission_view" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:text="@string/enable_permission_procedure" + android:contentDescription="@string/enable_permission_procedure_description" + android:background="@android:color/white" + android:gravity="center" + android:visibility="gone" /> +</FrameLayout>
\ No newline at end of file diff --git a/res/layout/widget_conversation.xml b/res/layout/widget_conversation.xml index a9cda98..eaf1dcf 100644 --- a/res/layout/widget_conversation.xml +++ b/res/layout/widget_conversation.xml @@ -39,7 +39,7 @@ android:freezesText="true" /> <ImageView android:id="@+id/launcher_icon" - android:src="@drawable/ic_launcher" + android:src="@mipmap/ic_launcher" android:layout_height="wrap_content" android:layout_width="0dip" android:layout_weight = "1" diff --git a/res/drawable-hdpi/ic_launcher.png b/res/mipmap-hdpi/ic_launcher.png Binary files differindex 5e4e62f..5e4e62f 100644 --- a/res/drawable-hdpi/ic_launcher.png +++ b/res/mipmap-hdpi/ic_launcher.png diff --git a/res/drawable-mdpi/ic_launcher.png b/res/mipmap-mdpi/ic_launcher.png Binary files differindex ef6a3c5..ef6a3c5 100644 --- a/res/drawable-mdpi/ic_launcher.png +++ b/res/mipmap-mdpi/ic_launcher.png diff --git a/res/drawable-xhdpi/ic_launcher.png b/res/mipmap-xhdpi/ic_launcher.png Binary files differindex ad1acb4..ad1acb4 100644 --- a/res/drawable-xhdpi/ic_launcher.png +++ b/res/mipmap-xhdpi/ic_launcher.png diff --git a/res/drawable-xxhdpi/ic_launcher.png b/res/mipmap-xxhdpi/ic_launcher.png Binary files differindex de56e98..de56e98 100644 --- a/res/drawable-xxhdpi/ic_launcher.png +++ b/res/mipmap-xxhdpi/ic_launcher.png diff --git a/res/drawable-xxxhdpi/ic_launcher.png b/res/mipmap-xxxhdpi/ic_launcher.png Binary files differindex 9ac88b4..9ac88b4 100644 --- a/res/drawable-xxxhdpi/ic_launcher.png +++ b/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/res/values-ldrtl/styles.xml b/res/values-ldrtl/styles.xml index bd270c7..abfd16c 100644 --- a/res/values-ldrtl/styles.xml +++ b/res/values-ldrtl/styles.xml @@ -38,7 +38,7 @@ <item name="android:background">@null</item> <item name="android:scrollHorizontally">false</item> <item name="android:textCursorDrawable">@null</item> - <item name="android:inputType">textShortMessage|textAutoCorrect|textCapSentences|textMultiLine</item> + <item name="android:inputType">textAutoCorrect|textCapSentences|textMultiLine</item> </style> <style name="ConversationComposeSubjectText" parent="ConversationComposeSendText"> diff --git a/res/values/cm_colors.xml b/res/values/cm_colors.xml new file mode 100755 index 0000000..05e91e0 --- /dev/null +++ b/res/values/cm_colors.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2016 The CyanogenMod Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--><resources> + <color name="cm_accent">#FF9800</color> + <color name="button_text">#000000</color> +</resources> diff --git a/res/values/cm_constants.xml b/res/values/cm_constants.xml new file mode 100644 index 0000000..5dcac5d --- /dev/null +++ b/res/values/cm_constants.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2016 The CyanogenMod Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<resources> + + <!-- Preference keys for user-visible settings --> + <!-- Application-wide settings --> + <string name="swipe_right_deletes_conversation_key" translatable="false">swipe_right_deletes_conversation</string> + + <!-- This should really go into a config xml, but whoever wrote this app is an idiot, so follow their pattern --> + <bool name="swipe_right_deletes_conversation_default" translatable="false">false</bool> + + <!-- Preference keys for user-visible settings --> + <!-- Application-wide settings --> + <bool name="show_emoticons_pref_default" translatable="false">true</bool> +</resources> diff --git a/res/values/cm_strings.xml b/res/values/cm_strings.xml new file mode 100644 index 0000000..1914da1 --- /dev/null +++ b/res/values/cm_strings.xml @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2015 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <!-- Swipe to delete conversation --> + <string name="swipe_to_delete_conversation_pref_title">Swipe to delete</string> + <string name="swipe_to_delete_conversation_pref_summary">Swipe to the right to delete a conversation</string> + + <!-- Show emoticons --> + <string name="show_emoticons_pref_title">Emoticons access</string> + <string name="show_emoticons_pref_summary">Show the emoticons key on the keyboard</string> + + <!-- Audio Library Tab for MMS attachments --> + <string name="mediapicker_galleryChooserDescription_cm">Choose images or videos from this + device</string> + <string name="mediapicker_audioListChooserDescription">Choose audio files from this + device</string> + <string name="mediapicker_gallery_image_item_attachment_too_large">Can\'t attach video. + Max message size exceeded.</string> + <string name="mediapicker_audio_list_title">Choose audio</string> + <string name="mediapicker_audio_list_item_selected_content_description">Selected audio</string> + <string name="mediapicker_audio_list_item_unselected_content_description">Tap to select</string> + <string name="mediapicker_audio_list_title_selection"><xliff:g id="count">%d</xliff:g> selected + </string> +</resources> diff --git a/res/values/colors.xml b/res/values/colors.xml index f33e105..b425a51 100644 --- a/res/values/colors.xml +++ b/res/values/colors.xml @@ -62,13 +62,13 @@ <color name="message_text_color_outgoing">#ff323232</color> <color name="conversation_background">#eeeeee</color> <color name="conversation_edge_effect">#9d9d9d</color> - <color name="compose_message_send_color">@color/primary_color</color> + <color name="compose_message_send_color">@color/cm_accent</color> <color name="compose_message_send_color_pressed">#999999</color> <color name="message_bubble_color_outgoing">#ffffffff</color> <color name="message_error_bubble_color_incoming">#e2e2e2</color> <color name="message_audio_button_color_incoming">#ffffffff</color> - <color name="message_bubble_color_selected">#8BC34A</color> - <color name="message_image_selected_tint">#80689F38</color> + <color name="message_bubble_color_selected">@color/cm_accent</color> + <color name="message_image_selected_tint">#80FF9800</color> <color name="generic_video_icon">#ff808080</color> <!-- Base color used for color filtering. --> @@ -87,7 +87,7 @@ <color name="compose_contact_divider">#44000000</color> <color name="contact_picker_tab_pressed">#ddffffff</color> <color name="contact_picker_tab_underline">@android:color/white</color> - <color name="contact_list_alphabet_header">@color/primary_color</color> + <color name="contact_list_alphabet_header">@color/cm_accent</color> <color name="contact_picker_background">#ffffff</color> <color name="chips_dropdown_background_activated">#4285f4</color> <color name="chips_dropdown_background_pressed">#ededed</color> @@ -109,7 +109,7 @@ <color name="audio_picker_hint_text_color">#40000000</color> <color name="audio_picker_timer_text_color">#323232</color> <color name="audio_attachment_timer_text_color">#323232</color> - <color name="audio_progress_bar_color">@color/primary_color</color> + <color name="audio_progress_bar_color">@color/cm_accent</color> <color name="notification_sender_text">#9A9A9A</color> <color name="notification_secondary_text">#FFFFFF</color> @@ -124,15 +124,15 @@ <color name="people_and_options_header_text">#6d6d6d</color> <color name="people_and_options_list_divider">#cccccc</color> - <color name="fab_color">@color/primary_color</color> + <color name="fab_color">@color/cm_accent</color> <color name="fab_pressed_color">#3ea4dc</color> <color name="fab_ripple">#40ffffff</color> <color name="message_text_counter_color">#555555</color> - <color name="mms_indicator_color">#8BC34A</color> + <color name="mms_indicator_color">#681faf</color> <color name="list_empty_text">#6d6d6d</color> <color name="low_storage_action_item_color">#ff000000</color> - <color name="unblock_item_text_color">@color/primary_color</color> + <color name="unblock_item_text_color">@color/cm_accent</color> <color name="open_conversation_animation_background_shadow">#40000000</color> <color name="compose_notification_bar_background">@color/primary_color</color> diff --git a/res/values/dimens.xml b/res/values/dimens.xml index 5ff0eb7..6f82d72 100644 --- a/res/values/dimens.xml +++ b/res/values/dimens.xml @@ -92,6 +92,13 @@ <dimen name="multiple_attachment_preview_padding">1dp</dimen> <dimen name="attachment_preview_more_items_text_size">22sp</dimen> + <dimen name="audio_list_item_height">48dp</dimen> + <dimen name="audio_list_item_icon_size">20dp</dimen> + <dimen name="audio_list_item_icon_margin">26dp</dimen> + <dimen name="audio_list_item_text_margin">26dp</dimen> + <dimen name="audio_list_item_text_height">24dp</dimen> + + <item name="letter_to_tile_ratio" type="dimen">67%</item> <item name="sim_identifier_to_tile_ratio" type="dimen">28%</item> <item name="small_sim_identifier_to_tile_ratio" type="dimen">75%</item> diff --git a/res/values/strings.xml b/res/values/strings.xml index 012d87f..3d21f3a 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -51,7 +51,8 @@ <string name="contact_list_send_to_text">Send to <xliff:g id="destination">%s</xliff:g></string> <string name="mediapicker_cameraChooserDescription">Capture pictures or video</string> - <string name="mediapicker_galleryChooserDescription">Choose images from this device</string> + <string name="mediapicker_galleryChooserDescription">Choose images or videos from this + device</string> <string name="mediapicker_audioChooserDescription">Record audio</string> <string name="mediapicker_gallery_title">Choose photo</string> <string name="mediapicker_gallery_item_selected_content_description">The media is selected.</string> @@ -61,9 +62,7 @@ <string name="mediapicker_gallery_image_item_description">image <xliff:g id="date">%1$tB %1$te %1$tY %1$tl %1$tM %1$tp</xliff:g></string> <string name="mediapicker_gallery_image_item_description_no_date">image</string> <string name="mediapicker_audio_title">Record audio</string> - <string name="action_share">Share</string> - <string name="posted_just_now">"Just now"</string> <string name="posted_now">"Now"</string> diff --git a/res/values/styles.xml b/res/values/styles.xml index 582c755..5f473ad 100644..100755 --- a/res/values/styles.xml +++ b/res/values/styles.xml @@ -34,7 +34,7 @@ <item name="android:dropDownListViewStyle">@style/DropDownListViewStyle</item> <item name="colorPrimary">@color/action_bar_background_color</item> <item name="colorPrimaryDark">@color/action_bar_background_color_dark</item> - <item name="colorAccent">@color/action_bar_background_color</item> + <item name="colorAccent">@color/cm_accent</item> <item name="android:textColorHighlight">@color/text_highlight_color</item> <item name="actionBarStyle">@style/BugleActionBar</item> <item name="apnPreferenceStyle">@style/ApnPreference</item> @@ -47,6 +47,7 @@ <style name="BugleTheme.ConversationActivityBase" parent="BugleTheme"> <item name="android:windowBackground">@color/conversation_background</item> <item name="windowActionBarOverlay">true</item> + <item name="colorAccent">@color/cm_accent</item> <item name="android:fastScrollPreviewBackgroundLeft">@drawable/contacts_fastscroll_label_left</item> <item name="android:fastScrollPreviewBackgroundRight">@drawable/contacts_fastscroll_label_right</item> </style> @@ -76,6 +77,14 @@ <item name="android:windowNoDisplay">true</item> </style> + <style name="InvisibleNoDisplay" parent="BugleBaseTheme"> + <item name="android:windowBackground">@null</item> + <item name="android:windowContentOverlay">@null</item> + <item name="android:windowIsTranslucent">true</item> + <item name="android:windowAnimationStyle">@null</item> + <item name="android:windowDisablePreview">true</item> + </style> + <style name="BugleActionBar" parent="@style/Widget.AppCompat.Light.ActionBar.Solid"> <item name="height">@dimen/action_bar_height</item> <item name="displayOptions">showTitle</item> @@ -133,7 +142,8 @@ <item name="android:background">@null</item> <item name="android:scrollHorizontally">false</item> <item name="android:textCursorDrawable">@null</item> - <item name="android:inputType">textShortMessage|textAutoCorrect|textCapSentences|textMultiLine</item> + <item name="android:inputType">textAutoCorrect|textCapSentences|textMultiLine</item> + <item name="android:colorAccent">@color/cm_accent</item> </style> <style name="ConversationComposeSubjectText" parent="ConversationComposeSendText"> @@ -352,7 +362,7 @@ <item name="android:background">@null</item> </style> - <style name="DropDownListViewStyle"> + <style name="DropDownListViewStyle" parent="Widget.AppCompat.ListView.DropDown"> <item name="android:dividerHeight">0dp</item> </style> diff --git a/res/xml-v21/preferences_application.xml b/res/xml-v21/preferences_application.xml index 5d8ee4c..c403c06 100644 --- a/res/xml-v21/preferences_application.xml +++ b/res/xml-v21/preferences_application.xml @@ -66,6 +66,20 @@ android:persistent="true" android:dependency="@string/notifications_enabled_pref_key" /> + <SwitchPreference + android:key="pref_show_emoticons" + android:title="@string/show_emoticons_pref_title" + android:persistent="true" + android:defaultValue="@bool/show_emoticons_pref_default" + android:summary="@string/show_emoticons_pref_summary" /> + + <SwitchPreference + android:key="@string/swipe_right_deletes_conversation_key" + android:title="@string/swipe_to_delete_conversation_pref_title" + android:summary="@string/swipe_to_delete_conversation_pref_summary" + android:defaultValue="false" + android:persistent="true" /> + <PreferenceScreen android:key="@string/advanced_pref_key" android:title="@string/advanced_settings" /> diff --git a/res/xml-v23/preferences_application.xml b/res/xml-v23/preferences_application.xml index 8fbadc4..d9cc151 100644 --- a/res/xml-v23/preferences_application.xml +++ b/res/xml-v23/preferences_application.xml @@ -67,6 +67,20 @@ android:persistent="true" android:dependency="@string/notifications_enabled_pref_key" /> + <SwitchPreference + android:key="pref_show_emoticons" + android:title="@string/show_emoticons_pref_title" + android:persistent="true" + android:defaultValue="@bool/show_emoticons_pref_default" + android:summary="@string/show_emoticons_pref_summary" /> + + <SwitchPreference + android:key="@string/swipe_right_deletes_conversation_key" + android:title="@string/swipe_to_delete_conversation_pref_title" + android:summary="@string/swipe_to_delete_conversation_pref_summary" + android:defaultValue="false" + android:persistent="true" /> + <PreferenceScreen android:key="@string/advanced_pref_key" android:title="@string/advanced_settings" /> diff --git a/res/xml/preferences_application.xml b/res/xml/preferences_application.xml index 7a18d09..03b868b 100644 --- a/res/xml/preferences_application.xml +++ b/res/xml/preferences_application.xml @@ -66,6 +66,20 @@ android:persistent="true" android:dependency="@string/notifications_enabled_pref_key" /> + <CheckBoxPreference + android:key="pref_show_emoticons" + android:title="@string/show_emoticons_pref_title" + android:persistent="true" + android:defaultValue="@bool/show_emoticons_pref_default" + android:summary="@string/show_emoticons_pref_summary" /> + + <SwitchPreference + android:key="@string/swipe_right_deletes_conversation_key" + android:title="@string/swipe_to_delete_conversation_pref_title" + android:summary="@string/swipe_to_delete_conversation_pref_summary" + android:defaultValue="false" + android:persistent="true" /> + <PreferenceScreen android:key="@string/advanced_pref_key" android:title="@string/advanced_settings" /> diff --git a/src/com/android/messaging/datamodel/AudioBoundCursorLoader.java b/src/com/android/messaging/datamodel/AudioBoundCursorLoader.java new file mode 100644 index 0000000..406432b --- /dev/null +++ b/src/com/android/messaging/datamodel/AudioBoundCursorLoader.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2016 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.messaging.datamodel; + +import android.content.Context; +import android.net.Uri; +import android.provider.MediaStore.Files; +import android.provider.MediaStore.Files.FileColumns; +import android.provider.MediaStore.Images.Media; +import com.android.messaging.datamodel.data.AudioListItemData; +import com.android.messaging.datamodel.data.MessagePartData; +import com.google.common.base.Joiner; + +/** + * A BoundCursorLoader that reads local media on the device. + */ +public class AudioBoundCursorLoader extends BoundCursorLoader { + public static final String MEDIA_SCANNER_VOLUME_EXTERNAL = "external"; + private static final Uri STORAGE_URI = Files.getContentUri(MEDIA_SCANNER_VOLUME_EXTERNAL); + private static final String SORT_ORDER = Media.DATE_MODIFIED + " DESC"; + private static final String TAG = AudioBoundCursorLoader.class.getSimpleName(); + private static final String AUDIO_SELECTION = createSelection( + MessagePartData.ACCEPTABLE_AUDIO_TYPES, + new Integer[] {FileColumns.MEDIA_TYPE_AUDIO}); + + public AudioBoundCursorLoader(final String bindingId, final Context context) { + super(bindingId, context, STORAGE_URI, AudioListItemData.AUDIO_PROJECTION, + AUDIO_SELECTION, null, SORT_ORDER); + } + + private static String createSelection(final String[] mimeTypes, Integer[] mediaTypes) { + return Media.MIME_TYPE + " IN ('" + Joiner.on("','").join(mimeTypes) + "') AND " + + FileColumns.MEDIA_TYPE + " IN (" + Joiner.on(',').join(mediaTypes) + ")"; + } +} diff --git a/src/com/android/messaging/datamodel/BugleNotifications.java b/src/com/android/messaging/datamodel/BugleNotifications.java index b796e73..ffe9105 100644 --- a/src/com/android/messaging/datamodel/BugleNotifications.java +++ b/src/com/android/messaging/datamodel/BugleNotifications.java @@ -191,6 +191,15 @@ public class BugleNotifications { } /** + * Play a sound to notify arrival of a class 0 message + * + */ + public static void playClassZeroNotification() { + final Uri ringtoneUri = RingtoneUtil.getNotificationRingtoneUri(null); + playObservableConversationNotificationSound(ringtoneUri); + } + + /** * Cancel all notifications of a certain type. * * @param type Message or error notifications from Constants. diff --git a/src/com/android/messaging/datamodel/DataModel.java b/src/com/android/messaging/datamodel/DataModel.java index 936b51c..b53a217 100644 --- a/src/com/android/messaging/datamodel/DataModel.java +++ b/src/com/android/messaging/datamodel/DataModel.java @@ -25,6 +25,7 @@ import com.android.messaging.Factory; import com.android.messaging.datamodel.action.Action; import com.android.messaging.datamodel.action.ActionService; import com.android.messaging.datamodel.action.BackgroundWorker; +import com.android.messaging.datamodel.data.AudioListItemData; import com.android.messaging.datamodel.data.BlockedParticipantsData; import com.android.messaging.datamodel.data.BlockedParticipantsData.BlockedParticipantsDataListener; import com.android.messaging.datamodel.data.ContactListItemData; @@ -84,6 +85,8 @@ public abstract class DataModel { public abstract GalleryGridItemData createGalleryGridItemData(); + public abstract AudioListItemData createAudioListItemData(); + public abstract LaunchConversationData createLaunchConversationData( LaunchConversationDataListener listener); diff --git a/src/com/android/messaging/datamodel/DataModelImpl.java b/src/com/android/messaging/datamodel/DataModelImpl.java index 6ab3f00..2f5b377 100644 --- a/src/com/android/messaging/datamodel/DataModelImpl.java +++ b/src/com/android/messaging/datamodel/DataModelImpl.java @@ -25,6 +25,7 @@ import com.android.messaging.datamodel.action.ActionService; import com.android.messaging.datamodel.action.BackgroundWorker; import com.android.messaging.datamodel.action.FixupMessageStatusOnStartupAction; import com.android.messaging.datamodel.action.ProcessPendingMessagesAction; +import com.android.messaging.datamodel.data.AudioListItemData; import com.android.messaging.datamodel.data.BlockedParticipantsData; import com.android.messaging.datamodel.data.BlockedParticipantsData.BlockedParticipantsDataListener; import com.android.messaging.datamodel.data.ContactListItemData; @@ -115,6 +116,11 @@ public class DataModelImpl extends DataModel { } @Override + public AudioListItemData createAudioListItemData() { + return new AudioListItemData(); + } + + @Override public LaunchConversationData createLaunchConversationData( final LaunchConversationDataListener listener) { return new LaunchConversationData(listener); diff --git a/src/com/android/messaging/datamodel/DatabaseHelper.java b/src/com/android/messaging/datamodel/DatabaseHelper.java index 2025e2c..5bfca06 100644 --- a/src/com/android/messaging/datamodel/DatabaseHelper.java +++ b/src/com/android/messaging/datamodel/DatabaseHelper.java @@ -181,9 +181,9 @@ public class DatabaseHelper extends SQLiteOpenHelper { + ConversationColumns.OTHER_PARTICIPANT_NORMALIZED_DESTINATION + " TEXT, " + ConversationColumns.CURRENT_SELF_ID + " TEXT, " + ConversationColumns.PARTICIPANT_COUNT + " INT DEFAULT(0), " - + ConversationColumns.NOTIFICATION_ENABLED + " INT DEFAULT(1), " + + ConversationColumns.NOTIFICATION_ENABLED + " INT DEFAULT(-1), " + ConversationColumns.NOTIFICATION_SOUND_URI + " TEXT, " - + ConversationColumns.NOTIFICATION_VIBRATION + " INT DEFAULT(1), " + + ConversationColumns.NOTIFICATION_VIBRATION + " INT DEFAULT(-1), " + ConversationColumns.INCLUDE_EMAIL_ADDRESS + " INT DEFAULT(0), " + ConversationColumns.SMS_SERVICE_CENTER + " TEXT ," + ConversationColumns.IS_ENTERPRISE + " INT DEFAULT(0)" diff --git a/src/com/android/messaging/datamodel/GalleryBoundCursorLoader.java b/src/com/android/messaging/datamodel/GalleryBoundCursorLoader.java index 28ec303..2816bab 100644 --- a/src/com/android/messaging/datamodel/GalleryBoundCursorLoader.java +++ b/src/com/android/messaging/datamodel/GalleryBoundCursorLoader.java @@ -33,9 +33,10 @@ public class GalleryBoundCursorLoader extends BoundCursorLoader { public static final String MEDIA_SCANNER_VOLUME_EXTERNAL = "external"; private static final Uri STORAGE_URI = Files.getContentUri(MEDIA_SCANNER_VOLUME_EXTERNAL); private static final String SORT_ORDER = Media.DATE_MODIFIED + " DESC"; + private static final String TAG = GalleryBoundCursorLoader.class.getSimpleName(); private static final String IMAGE_SELECTION = createSelection( MessagePartData.ACCEPTABLE_IMAGE_TYPES, - new Integer[] { FileColumns.MEDIA_TYPE_IMAGE }); + new Integer[] { FileColumns.MEDIA_TYPE_IMAGE, FileColumns.MEDIA_TYPE_VIDEO}); public GalleryBoundCursorLoader(final String bindingId, final Context context) { super(bindingId, context, STORAGE_URI, GalleryGridItemData.IMAGE_PROJECTION, diff --git a/src/com/android/messaging/datamodel/action/BugleActionToasts.java b/src/com/android/messaging/datamodel/action/BugleActionToasts.java index f60facd..17d15f2 100644 --- a/src/com/android/messaging/datamodel/action/BugleActionToasts.java +++ b/src/com/android/messaging/datamodel/action/BugleActionToasts.java @@ -114,7 +114,6 @@ public class BugleActionToasts { } public static void onConversationDeleted() { - showToast(R.string.conversation_deleted); } private static void showToast(final int messageResId) { diff --git a/src/com/android/messaging/datamodel/action/ProcessPendingMessagesAction.java b/src/com/android/messaging/datamodel/action/ProcessPendingMessagesAction.java index 8a41f4a..ecbec10 100644 --- a/src/com/android/messaging/datamodel/action/ProcessPendingMessagesAction.java +++ b/src/com/android/messaging/datamodel/action/ProcessPendingMessagesAction.java @@ -24,12 +24,14 @@ import android.net.ConnectivityManager; import android.os.Parcel; import android.os.Parcelable; import android.telephony.ServiceState; +import android.telephony.SubscriptionInfo; import com.android.messaging.Factory; import com.android.messaging.datamodel.BugleDatabaseOperations; import com.android.messaging.datamodel.DataModel; import com.android.messaging.datamodel.DatabaseHelper; import com.android.messaging.datamodel.DatabaseHelper.MessageColumns; +import com.android.messaging.datamodel.DatabaseHelper.ParticipantColumns; import com.android.messaging.datamodel.DatabaseWrapper; import com.android.messaging.datamodel.MessagingContentProvider; import com.android.messaging.datamodel.data.MessageData; @@ -45,6 +47,7 @@ import com.android.messaging.util.OsUtil; import com.android.messaging.util.PhoneUtils; import java.util.HashSet; +import java.util.List; import java.util.Set; /** @@ -218,13 +221,15 @@ public class ProcessPendingMessagesAction extends Action implements Parcelable { final DatabaseWrapper db = DataModel.get().getDatabase(); final long now = System.currentTimeMillis(); - final String toSendMessageId = findNextMessageToSend(db, now); - if (toSendMessageId != null) { - return true; - } else { - final String toDownloadMessageId = findNextMessageToDownload(db, now); - if (toDownloadMessageId != null) { + for (int subId : getActiveSubscriptionIds()) { + final String toSendMessageId = findNextMessageToSend(db, now, subId); + if (toSendMessageId != null) { return true; + } else { + final String toDownloadMessageId = findNextMessageToDownload(db, now, subId); + if (toDownloadMessageId != null) { + return true; + } } } // Messages may be in the process of sending/downloading even when there are no pending @@ -232,6 +237,21 @@ public class ProcessPendingMessagesAction extends Action implements Parcelable { return false; } + private static int[] getActiveSubscriptionIds() { + if (!OsUtil.isAtLeastL_MR1()) { + return new int[] { ParticipantData.DEFAULT_SELF_SUB_ID }; + } + List<SubscriptionInfo> subscriptions = PhoneUtils.getDefault().toLMr1() + .getActiveSubscriptionInfoList(); + + int numSubs = subscriptions.size(); + int[] result = new int[numSubs]; + for (int i = 0; i < numSubs; i++) { + result[i] = subscriptions.get(i).getSubscriptionId(); + } + return result; + } + /** * Queue any pending actions * @param actionState @@ -240,37 +260,44 @@ public class ProcessPendingMessagesAction extends Action implements Parcelable { private boolean queueActions(final Action processingAction) { final DatabaseWrapper db = DataModel.get().getDatabase(); final long now = System.currentTimeMillis(); - boolean succeeded = true; + boolean succeeded = false; - // Will queue no more than one message to send plus one message to download + // Will queue no more than one message per subscription to send plus one message to download // This keeps outgoing messages "in order" but allow downloads to happen even if sending // gets blocked until messages time out. Manual resend bumps messages to head of queue. - final String toSendMessageId = findNextMessageToSend(db, now); - final String toDownloadMessageId = findNextMessageToDownload(db, now); - if (toSendMessageId != null) { - LogUtil.i(TAG, "ProcessPendingMessagesAction: Queueing message " + toSendMessageId - + " for sending"); - // This could queue nothing - if (!SendMessageAction.queueForSendInBackground(toSendMessageId, processingAction)) { - LogUtil.w(TAG, "ProcessPendingMessagesAction: Failed to queue message " - + toSendMessageId + " for sending"); - succeeded = false; + for (int subId : getActiveSubscriptionIds()) { + final String toSendMessageId = findNextMessageToSend(db, now, subId); + final String toDownloadMessageId = findNextMessageToDownload(db, now, subId); + if (toSendMessageId != null) { + LogUtil.i(TAG, "ProcessPendingMessagesAction: Queueing message " + toSendMessageId + + " for sending"); + // This could queue nothing + if (!SendMessageAction.queueForSendInBackground(toSendMessageId, + processingAction)) { + LogUtil.w(TAG, "ProcessPendingMessagesAction: Failed to queue message " + + toSendMessageId + " for sending"); + } else { + succeeded = true; + } } - } - if (toDownloadMessageId != null) { - LogUtil.i(TAG, "ProcessPendingMessagesAction: Queueing message " + toDownloadMessageId - + " for download"); - // This could queue nothing - if (!DownloadMmsAction.queueMmsForDownloadInBackground(toDownloadMessageId, - processingAction)) { - LogUtil.w(TAG, "ProcessPendingMessagesAction: Failed to queue message " + if (toDownloadMessageId != null) { + LogUtil.i(TAG, "ProcessPendingMessagesAction: Queueing message " + toDownloadMessageId + " for download"); - succeeded = false; + // This could queue nothing + if (!DownloadMmsAction.queueMmsForDownloadInBackground(toDownloadMessageId, + processingAction)) { + LogUtil.w(TAG, "ProcessPendingMessagesAction: Failed to queue message " + + toDownloadMessageId + " for download"); + } else { + succeeded = true; + } + } - } - if (toSendMessageId == null && toDownloadMessageId == null) { - if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) { - LogUtil.d(TAG, "ProcessPendingMessagesAction: No messages to send or download"); + if (toSendMessageId == null && toDownloadMessageId == null) { + if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) { + LogUtil.d(TAG, "ProcessPendingMessagesAction: No messages to send or download"); + } + succeeded = true; } } return succeeded; @@ -293,7 +320,21 @@ public class ProcessPendingMessagesAction extends Action implements Parcelable { return null; } - private static String findNextMessageToSend(final DatabaseWrapper db, final long now) { + private static String prefixColumnWithTable(final String tableName, final String column) { + return tableName + "." + column; + } + + private static String[] prefixProjectionWithTable(final String tableName, + final String[] projection) { + String[] result = new String[projection.length]; + for (int i = 0; i < projection.length; i++) { + result[i] = prefixColumnWithTable(tableName, projection[i]); + } + return result; + } + + private static String findNextMessageToSend(final DatabaseWrapper db, final long now, + final int subId) { String toSendMessageId = null; db.beginTransaction(); Cursor sending = null; @@ -302,12 +343,25 @@ public class ProcessPendingMessagesAction extends Action implements Parcelable { int pendingCnt = 0; int failedCnt = 0; try { + String[] projection = prefixProjectionWithTable(DatabaseHelper.MESSAGES_TABLE, + MessageData.getProjection()); + String subIdClause = + prefixColumnWithTable(DatabaseHelper.MESSAGES_TABLE, + MessageColumns.SELF_PARTICIPANT_ID) + + " = " + + prefixColumnWithTable(DatabaseHelper.PARTICIPANTS_TABLE, + ParticipantColumns._ID) + + " AND " + ParticipantColumns.SUB_ID + " =?"; + // First check to see if we have any messages already sending - sending = db.query(DatabaseHelper.MESSAGES_TABLE, - MessageData.getProjection(), - DatabaseHelper.MessageColumns.STATUS + " IN (?, ?)", + sending = db.query(DatabaseHelper.MESSAGES_TABLE + "," + + DatabaseHelper.PARTICIPANTS_TABLE, + projection, + DatabaseHelper.MessageColumns.STATUS + " IN (?, ?)" + + " AND " + subIdClause, new String[]{Integer.toString(MessageData.BUGLE_STATUS_OUTGOING_SENDING), - Integer.toString(MessageData.BUGLE_STATUS_OUTGOING_RESENDING)}, + Integer.toString(MessageData.BUGLE_STATUS_OUTGOING_RESENDING), + Integer.toString(subId)}, null, null, DatabaseHelper.MessageColumns.RECEIVED_TIMESTAMP + " ASC"); @@ -317,12 +371,14 @@ public class ProcessPendingMessagesAction extends Action implements Parcelable { final ContentValues values = new ContentValues(); values.put(DatabaseHelper.MessageColumns.STATUS, MessageData.BUGLE_STATUS_OUTGOING_FAILED); - cursor = db.query(DatabaseHelper.MESSAGES_TABLE, - MessageData.getProjection(), + cursor = db.query(DatabaseHelper.MESSAGES_TABLE + "," + + DatabaseHelper.PARTICIPANTS_TABLE, + projection, DatabaseHelper.MessageColumns.STATUS + " IN (" + MessageData.BUGLE_STATUS_OUTGOING_YET_TO_SEND + "," - + MessageData.BUGLE_STATUS_OUTGOING_AWAITING_RETRY + ")", - null, + + MessageData.BUGLE_STATUS_OUTGOING_AWAITING_RETRY + ")" + + " AND " + subIdClause, + new String[]{Integer.toString(subId)}, null, null, DatabaseHelper.MessageColumns.RECEIVED_TIMESTAMP + " ASC"); @@ -388,31 +444,49 @@ public class ProcessPendingMessagesAction extends Action implements Parcelable { return toSendMessageId; } - private static String findNextMessageToDownload(final DatabaseWrapper db, final long now) { + private static String findNextMessageToDownload(final DatabaseWrapper db, final long now, + final int subId) { String toDownloadMessageId = null; db.beginTransaction(); Cursor cursor = null; int downloadingCnt = 0; int pendingCnt = 0; try { + String[] projection = prefixProjectionWithTable(DatabaseHelper.MESSAGES_TABLE, + MessageData.getProjection()); + String subIdClause = + prefixColumnWithTable(DatabaseHelper.MESSAGES_TABLE, + MessageColumns.SELF_PARTICIPANT_ID) + + " = " + + prefixColumnWithTable(DatabaseHelper.PARTICIPANTS_TABLE, + ParticipantColumns._ID) + + " AND " + ParticipantColumns.SUB_ID + " =?"; + // First check if we have any messages already downloading - downloadingCnt = (int) db.queryNumEntries(DatabaseHelper.MESSAGES_TABLE, - DatabaseHelper.MessageColumns.STATUS + " IN (?, ?)", + downloadingCnt = (int) db.queryNumEntries(DatabaseHelper.MESSAGES_TABLE + + "," + DatabaseHelper.PARTICIPANTS_TABLE, + DatabaseHelper.MessageColumns.STATUS + " IN (?, ?)" + + " AND " + subIdClause, new String[] { Integer.toString(MessageData.BUGLE_STATUS_INCOMING_AUTO_DOWNLOADING), - Integer.toString(MessageData.BUGLE_STATUS_INCOMING_MANUAL_DOWNLOADING) + Integer.toString(MessageData.BUGLE_STATUS_INCOMING_MANUAL_DOWNLOADING), + Integer.toString(subId) }); // TODO: This query is not actually needed if downloadingCnt == 0. - cursor = db.query(DatabaseHelper.MESSAGES_TABLE, - MessageData.getProjection(), + cursor = db.query(DatabaseHelper.MESSAGES_TABLE + "," + + DatabaseHelper.PARTICIPANTS_TABLE, + projection, DatabaseHelper.MessageColumns.STATUS + " =? OR " - + DatabaseHelper.MessageColumns.STATUS + " =?", + + DatabaseHelper.MessageColumns.STATUS + " =?" + + " AND " + subIdClause, new String[]{ Integer.toString( MessageData.BUGLE_STATUS_INCOMING_RETRYING_AUTO_DOWNLOAD), Integer.toString( - MessageData.BUGLE_STATUS_INCOMING_RETRYING_MANUAL_DOWNLOAD) + MessageData.BUGLE_STATUS_INCOMING_RETRYING_MANUAL_DOWNLOAD), + Integer.toString( + subId) }, null, null, diff --git a/src/com/android/messaging/datamodel/data/AudioListItemData.java b/src/com/android/messaging/datamodel/data/AudioListItemData.java new file mode 100644 index 0000000..e069c80 --- /dev/null +++ b/src/com/android/messaging/datamodel/data/AudioListItemData.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2016 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.messaging.datamodel.data; + +import android.database.Cursor; +import android.graphics.Rect; +import android.net.Uri; +import android.provider.BaseColumns; +import android.provider.MediaStore.Images.Media; +import android.text.TextUtils; +import com.android.messaging.util.UriUtil; + +/** + * Provides data for GalleryGridItemView + */ +public class AudioListItemData { + private static final String TAG = AudioListItemData.class.getSimpleName(); + public static final String[] AUDIO_PROJECTION = new String[] { + Media._ID, + Media.DATA, + Media.MIME_TYPE, + Media.DATE_MODIFIED}; + + public static final String[] SPECIAL_ITEM_COLUMNS = new String[] { + BaseColumns._ID + }; + + private static final int INDEX_ID = 0; + + // For local image gallery. + private static final int INDEX_DATA_PATH = 1; + private static final int INDEX_MIME_TYPE = 2; + private static final int INDEX_DATE_MODIFIED = 3; + + private Uri mAudioUri; + private String mContentType; + private long mDateSeconds; + + public AudioListItemData() { + } + + public void bind(final Cursor cursor) { + mContentType = cursor.getString(INDEX_MIME_TYPE); + final String dateModified = cursor.getString(INDEX_DATE_MODIFIED); + mDateSeconds = !TextUtils.isEmpty(dateModified) ? Long.parseLong(dateModified) : -1; + mAudioUri = UriUtil.getUriForResourceFile(cursor.getString(INDEX_DATA_PATH)); + } + + public Uri getAudioUri() { + return mAudioUri; + } + + public String getAudioFilename() { + if (mAudioUri != null) { + return mAudioUri.getLastPathSegment(); + } else { + return ""; + } + } + + public MessagePartData constructMessagePartData(final Rect startRect) { + return new MediaPickerMessagePartData(startRect, mContentType, + mAudioUri, 0, 0); + } + + /** + * @return The date in seconds. This can be negative if we could not retreive date info + */ + public long getDateSeconds() { + return mDateSeconds; + } + + public String getContentType() { + return mContentType; + } +} diff --git a/src/com/android/messaging/datamodel/data/ConversationListItemData.java b/src/com/android/messaging/datamodel/data/ConversationListItemData.java index f627a09..13cfc74 100644 --- a/src/com/android/messaging/datamodel/data/ConversationListItemData.java +++ b/src/com/android/messaging/datamodel/data/ConversationListItemData.java @@ -30,6 +30,7 @@ import com.android.messaging.datamodel.action.DeleteConversationAction; import com.android.messaging.util.Assert; import com.android.messaging.util.ContactUtil; import com.android.messaging.util.Dates; +import com.android.messaging.util.NotificationUtil; import com.google.common.base.Joiner; import java.util.ArrayList; @@ -92,9 +93,11 @@ public class ConversationListItemData { INDEX_OTHER_PARTICIPANT_NORMALIZED_DESTINATION); mSelfId = cursor.getString(INDEX_SELF_ID); mParticipantCount = cursor.getInt(INDEX_PARTICIPANT_COUNT); - mNotificationEnabled = cursor.getInt(INDEX_NOTIFICATION_ENABLED) == 1; + mNotificationEnabled = NotificationUtil.getConversationNotificationEnabled + (cursor.getInt(INDEX_NOTIFICATION_ENABLED)); mNotificationSoundUri = cursor.getString(INDEX_NOTIFICATION_SOUND_URI); - mNotificationVibrate = cursor.getInt(INDEX_NOTIFICATION_VIBRATION) == 1; + mNotificationVibrate = NotificationUtil.getConversationNotificationVibrateEnabled( + cursor.getInt(INDEX_NOTIFICATION_VIBRATION)); mIncludeEmailAddress = cursor.getInt(INDEX_INCLUDE_EMAIL_ADDRESS) == 1; mMessageStatus = cursor.getInt(INDEX_MESSAGE_STATUS); mMessageRawTelephonyStatus = cursor.getInt(INDEX_MESSAGE_RAW_TELEPHONY_STATUS); diff --git a/src/com/android/messaging/datamodel/data/GalleryGridItemData.java b/src/com/android/messaging/datamodel/data/GalleryGridItemData.java index 6649757..a83189e 100644 --- a/src/com/android/messaging/datamodel/data/GalleryGridItemData.java +++ b/src/com/android/messaging/datamodel/data/GalleryGridItemData.java @@ -19,6 +19,8 @@ package com.android.messaging.datamodel.data; import android.database.Cursor; import android.graphics.Rect; import android.net.Uri; +import android.os.AsyncTask; +import android.os.Handler; import android.provider.BaseColumns; import android.provider.MediaStore.Images.Media; import android.text.TextUtils; @@ -26,7 +28,9 @@ import android.text.TextUtils; import com.android.messaging.datamodel.media.FileImageRequestDescriptor; import com.android.messaging.datamodel.media.ImageRequest; import com.android.messaging.datamodel.media.UriImageRequestDescriptor; +import com.android.messaging.datamodel.media.VideoThumbnailRequestDescriptor; import com.android.messaging.util.Assert; +import com.android.messaging.util.UriUtil; /** * Provides data for GalleryGridItemView @@ -59,7 +63,9 @@ public class GalleryGridItemData { private UriImageRequestDescriptor mImageData; private String mContentType; private boolean mIsDocumentPickerItem; + private boolean mIsVideoItem; private long mDateSeconds; + private long mContentSize = 0; public GalleryGridItemData() { } @@ -67,10 +73,15 @@ public class GalleryGridItemData { public void bind(final Cursor cursor, final int desiredWidth, final int desiredHeight) { mIsDocumentPickerItem = TextUtils.equals(cursor.getString(INDEX_ID), ID_DOCUMENT_PICKER_ITEM); + if (mIsDocumentPickerItem) { mImageData = null; mContentType = null; } else { + + String mimeType = (cursor.getString(INDEX_MIME_TYPE)); + mIsVideoItem = (mimeType != null && mimeType.toLowerCase().contains("video/")); + int sourceWidth = cursor.getInt(INDEX_WIDTH); int sourceHeight = cursor.getInt(INDEX_HEIGHT); @@ -85,22 +96,60 @@ public class GalleryGridItemData { mContentType = cursor.getString(INDEX_MIME_TYPE); final String dateModified = cursor.getString(INDEX_DATE_MODIFIED); mDateSeconds = !TextUtils.isEmpty(dateModified) ? Long.parseLong(dateModified) : -1; - mImageData = new FileImageRequestDescriptor( - cursor.getString(INDEX_DATA_PATH), - desiredWidth, - desiredHeight, - sourceWidth, - sourceHeight, - true /* canUseThumbnail */, - true /* allowCompression */, - true /* isStatic */); + if (mIsVideoItem) { + mImageData = new VideoThumbnailRequestDescriptor( + cursor.getLong(INDEX_ID), + cursor.getString(INDEX_DATA_PATH), + desiredWidth, + desiredHeight, + sourceWidth, + sourceHeight); + } else { + mImageData = new FileImageRequestDescriptor( + cursor.getString(INDEX_DATA_PATH), + desiredWidth, + desiredHeight, + sourceWidth, + sourceHeight, + true /* canUseThumbnail */, + true /* allowCompression */, + true /* isStatic */); + } + + // the the size of the image in the background, needed for selection size checking + // preload here in the background, so that it's ready when the thumb is clicked on + // TODO - remove when video compression is added, as no size check will be performed + // TODO - at selection time + if (mIsVideoItem) { + new AsyncTask<Void, Void, Long>() { + @Override + protected Long doInBackground(Void... params) { + Long size = UriUtil.getContentSize(getImageUri()); + return size; + } + + @Override + protected void onPostExecute(Long result) { + mContentSize = result; + } + }.execute(); + } + } } + public long getContentSize() { + return mContentSize; + } + public boolean isDocumentPickerItem() { return mIsDocumentPickerItem; } + public boolean isVideoItem() { + return mIsVideoItem; + } + public Uri getImageUri() { return mImageData.uri; } diff --git a/src/com/android/messaging/datamodel/data/MediaPickerData.java b/src/com/android/messaging/datamodel/data/MediaPickerData.java index b0c8bf7..8de663a 100644 --- a/src/com/android/messaging/datamodel/data/MediaPickerData.java +++ b/src/com/android/messaging/datamodel/data/MediaPickerData.java @@ -23,10 +23,12 @@ import android.database.Cursor; import android.os.Bundle; import android.support.annotation.Nullable; +import com.android.messaging.datamodel.AudioBoundCursorLoader; import com.android.messaging.datamodel.BoundCursorLoader; import com.android.messaging.datamodel.GalleryBoundCursorLoader; import com.android.messaging.datamodel.binding.BindableData; import com.android.messaging.datamodel.binding.BindingBase; +import com.android.messaging.ui.mediapicker.MediaPicker; import com.android.messaging.util.Assert; import com.android.messaging.util.BuglePrefs; import com.android.messaging.util.BuglePrefsKeys; @@ -44,7 +46,9 @@ public class MediaPickerData extends BindableData { private final Context mContext; private LoaderManager mLoaderManager; private final GalleryLoaderCallbacks mGalleryLoaderCallbacks; - private MediaPickerDataListener mListener; + private MediaPickerDataListener mImageListener; + private MediaPickerDataListener mAudioListener; + private static final String TAG = MediaPickerData.class.getSimpleName(); public MediaPickerData(final Context context) { mContext = context; @@ -52,6 +56,7 @@ public class MediaPickerData extends BindableData { } public static final int GALLERY_IMAGE_LOADER = 1; + public static final int GALLERY_AUDIO_LOADER = 2; /** * A trampoline class so that we can inherit from LoaderManager.LoaderCallbacks multiple times. @@ -66,6 +71,9 @@ public class MediaPickerData extends BindableData { case GALLERY_IMAGE_LOADER: return new GalleryBoundCursorLoader(bindingId, mContext); + case GALLERY_AUDIO_LOADER: + return new AudioBoundCursorLoader(bindingId, mContext); + default: Assert.fail("Unknown loader id for gallery picker!"); break; @@ -85,10 +93,15 @@ public class MediaPickerData extends BindableData { if (isBound(cursorLoader.getBindingId())) { switch (loader.getId()) { case GALLERY_IMAGE_LOADER: - mListener.onMediaPickerDataUpdated(MediaPickerData.this, data, + mImageListener.onMediaPickerDataUpdated(MediaPickerData.this, data, GALLERY_IMAGE_LOADER); break; + case GALLERY_AUDIO_LOADER: + mAudioListener.onMediaPickerDataUpdated(MediaPickerData.this, data, + GALLERY_AUDIO_LOADER); + break; + default: Assert.fail("Unknown loader id for gallery picker!"); break; @@ -107,10 +120,15 @@ public class MediaPickerData extends BindableData { if (isBound(cursorLoader.getBindingId())) { switch (loader.getId()) { case GALLERY_IMAGE_LOADER: - mListener.onMediaPickerDataUpdated(MediaPickerData.this, null, + mImageListener.onMediaPickerDataUpdated(MediaPickerData.this, null, GALLERY_IMAGE_LOADER); break; + case GALLERY_AUDIO_LOADER: + mAudioListener.onMediaPickerDataUpdated(MediaPickerData.this, null, + GALLERY_AUDIO_LOADER); + break; + default: Assert.fail("Unknown loader id for media picker!"); break; @@ -131,10 +149,13 @@ public class MediaPickerData extends BindableData { args.putString(BINDING_ID, binding.getBindingId()); if (loaderId == GALLERY_IMAGE_LOADER) { mLoaderManager.initLoader(loaderId, args, mGalleryLoaderCallbacks).forceLoad(); - } else { + mImageListener = listener; + } else if (loaderId == GALLERY_AUDIO_LOADER) { + mLoaderManager.initLoader(loaderId, args, mGalleryLoaderCallbacks).forceLoad(); + mAudioListener = listener; + }else { Assert.fail("Unsupported loader id for media picker!"); } - mListener = listener; } public void destroyLoader(final int loaderId) { @@ -150,6 +171,7 @@ public class MediaPickerData extends BindableData { // This could be null if we bind but the caller doesn't init the BindableData if (mLoaderManager != null) { mLoaderManager.destroyLoader(GALLERY_IMAGE_LOADER); + mLoaderManager.destroyLoader(GALLERY_AUDIO_LOADER); mLoaderManager = null; } } diff --git a/src/com/android/messaging/datamodel/data/MessagePartData.java b/src/com/android/messaging/datamodel/data/MessagePartData.java index fffaca8..b4b74f8 100644 --- a/src/com/android/messaging/datamodel/data/MessagePartData.java +++ b/src/com/android/messaging/datamodel/data/MessagePartData.java @@ -53,8 +53,33 @@ import java.util.concurrent.TimeUnit; public class MessagePartData implements Parcelable { public static final int UNSPECIFIED_SIZE = MessagingContentProvider.UNSPECIFIED_SIZE; public static final String[] ACCEPTABLE_IMAGE_TYPES = - new String[] { ContentType.IMAGE_JPEG, ContentType.IMAGE_JPG, ContentType.IMAGE_PNG, - ContentType.IMAGE_GIF }; + new String[] { + // Images + ContentType.IMAGE_JPEG, + ContentType.IMAGE_JPG, + ContentType.IMAGE_PNG, + ContentType.IMAGE_GIF, + + // Videos + ContentType.VIDEO_MP4, + ContentType.VIDEO_MPEG, + ContentType.VIDEO_MPEG4, + ContentType.VIDEO_3GP, + ContentType.VIDEO_3GPP, + ContentType.VIDEO_WEBM + }; + + public static final String[] ACCEPTABLE_AUDIO_TYPES = + new String[] { + // Audio + ContentType.AUDIO_AMR, + ContentType.AUDIO_3GPP, + ContentType.AUDIO_AAC, + ContentType.AUDIO_MP3, + ContentType.AUDIO_MP4, + ContentType.AUDIO_MPEG, + ContentType.AUDIO_MPEG3 + }; private static final String[] sProjection = { PartColumns._ID, diff --git a/src/com/android/messaging/datamodel/data/PeopleOptionsItemData.java b/src/com/android/messaging/datamodel/data/PeopleOptionsItemData.java index 5af6a30..b24ca55 100644 --- a/src/com/android/messaging/datamodel/data/PeopleOptionsItemData.java +++ b/src/com/android/messaging/datamodel/data/PeopleOptionsItemData.java @@ -24,6 +24,7 @@ import android.net.Uri; import com.android.messaging.R; import com.android.messaging.datamodel.data.ConversationListItemData.ConversationListViewColumns; import com.android.messaging.util.Assert; +import com.android.messaging.util.NotificationUtil; import com.android.messaging.util.RingtoneUtil; public class PeopleOptionsItemData { @@ -78,7 +79,8 @@ public class PeopleOptionsItemData { mItemId = settingType; mOtherParticipant = otherParticipant; - final boolean notificationEnabled = cursor.getInt(INDEX_NOTIFICATION_ENABLED) == 1; + final boolean notificationEnabled = NotificationUtil + .getConversationNotificationEnabled(cursor.getInt(INDEX_NOTIFICATION_ENABLED)); switch (settingType) { case SETTING_NOTIFICATION_ENABLED: mTitle = mContext.getString(R.string.notifications_enabled_conversation_pref_title); @@ -104,7 +106,8 @@ public class PeopleOptionsItemData { case SETTING_NOTIFICATION_VIBRATION: mTitle = mContext.getString(R.string.notification_vibrate_pref_title); - mChecked = cursor.getInt(INDEX_NOTIFICATION_VIBRATION) == 1; + mChecked = NotificationUtil.getConversationNotificationVibrateEnabled( + cursor.getInt(INDEX_NOTIFICATION_VIBRATION)); mEnabled = notificationEnabled; break; diff --git a/src/com/android/messaging/datamodel/media/AvatarRequest.java b/src/com/android/messaging/datamodel/media/AvatarRequest.java index 22d5ccc..6a738c7 100644 --- a/src/com/android/messaging/datamodel/media/AvatarRequest.java +++ b/src/com/android/messaging/datamodel/media/AvatarRequest.java @@ -161,7 +161,7 @@ public class AvatarRequest extends UriImageRequest<AvatarRequestDescriptor> { getBackgroundColor()); final Resources resources = mContext.getResources(); final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); - paint.setTypeface(Typeface.create("sans-serif-thin", Typeface.NORMAL)); + paint.setTypeface(Typeface.create("sans-serif-medium", Typeface.NORMAL)); paint.setColor(resources.getColor(R.color.letter_tile_font_color)); final float letterToTileRatio = resources.getFraction(R.dimen.letter_to_tile_ratio, 1, 1); paint.setTextSize(letterToTileRatio * minOfWidthAndHeight); diff --git a/src/com/android/messaging/ui/ClassZeroActivity.java b/src/com/android/messaging/ui/ClassZeroActivity.java index 129ec19..ccb15a0 100644 --- a/src/com/android/messaging/ui/ClassZeroActivity.java +++ b/src/com/android/messaging/ui/ClassZeroActivity.java @@ -33,6 +33,7 @@ import android.view.Window; import com.android.messaging.R; import com.android.messaging.datamodel.action.ReceiveSmsMessageAction; +import com.android.messaging.datamodel.BugleNotifications; import com.android.messaging.util.Assert; import java.util.ArrayList; @@ -88,6 +89,8 @@ public class ClassZeroActivity extends Activity { return false; } mMessageQueue.add(messageValues); + // Show a notification to let the user know a new message has arrived + BugleNotifications.playClassZeroNotification(); return true; } diff --git a/src/com/android/messaging/ui/appsettings/ApplicationSettingsActivity.java b/src/com/android/messaging/ui/appsettings/ApplicationSettingsActivity.java index 906009f..20ffebf 100644 --- a/src/com/android/messaging/ui/appsettings/ApplicationSettingsActivity.java +++ b/src/com/android/messaging/ui/appsettings/ApplicationSettingsActivity.java @@ -28,6 +28,7 @@ import android.preference.Preference; import android.preference.PreferenceFragment; import android.preference.PreferenceScreen; import android.preference.RingtonePreference; +import android.preference.SwitchPreference; import android.preference.TwoStatePreference; import android.provider.Settings; import android.support.v4.app.NavUtils; @@ -97,6 +98,8 @@ public class ApplicationSettingsActivity extends BugleActionBarActivity { private String mSmsEnabledPrefKey; private Preference mSmsEnabledPreference; private boolean mIsSmsPreferenceClicked; + private String mSwipeRightToDeleteConversationkey; + private SwitchPreference mSwipeRightToDeleteConversationPreference; public ApplicationSettingsFragment() { // Required empty constructor @@ -121,6 +124,10 @@ public class ApplicationSettingsActivity extends BugleActionBarActivity { mSmsDisabledPreference = findPreference(mSmsDisabledPrefKey); mSmsEnabledPrefKey = getString(R.string.sms_enabled_pref_key); mSmsEnabledPreference = findPreference(mSmsEnabledPrefKey); + mSwipeRightToDeleteConversationkey = getString( + R.string.swipe_right_deletes_conversation_key); + mSwipeRightToDeleteConversationPreference = + (SwitchPreference) findPreference(mSwipeRightToDeleteConversationkey); mIsSmsPreferenceClicked = false; final SharedPreferences prefs = getPreferenceScreen().getSharedPreferences(); diff --git a/src/com/android/messaging/ui/conversation/ComposeMessageView.java b/src/com/android/messaging/ui/conversation/ComposeMessageView.java index 17f8f74..a2e9a95 100644 --- a/src/com/android/messaging/ui/conversation/ComposeMessageView.java +++ b/src/com/android/messaging/ui/conversation/ComposeMessageView.java @@ -25,6 +25,7 @@ import android.text.Editable; import android.text.Html; import android.text.InputFilter; import android.text.InputFilter.LengthFilter; +import android.text.InputType; import android.text.TextUtils; import android.text.TextWatcher; import android.util.AttributeSet; @@ -69,6 +70,8 @@ import com.android.messaging.util.MediaUtil; import com.android.messaging.util.OsUtil; import com.android.messaging.util.UiUtils; +import com.cyanogenmod.messaging.util.PrefsUtils; + import java.util.Collection; import java.util.List; @@ -213,6 +216,14 @@ public class ComposeMessageView extends LinearLayout new LengthFilter(MmsConfig.get(ParticipantData.DEFAULT_SELF_SUB_ID) .getMaxTextLimit()) }); + if (PrefsUtils.isShowEmoticonsEnabled()) { + mComposeEditText.setInputType(mComposeEditText.getInputType() + | InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE); + } else { + mComposeEditText.setInputType(mComposeEditText.getInputType() + & ~InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE); + } + mSelfSendIcon = (SimIconView) findViewById(R.id.self_send_icon); mSelfSendIcon.setOnClickListener(new OnClickListener() { @Override diff --git a/src/com/android/messaging/ui/conversation/ConversationFragment.java b/src/com/android/messaging/ui/conversation/ConversationFragment.java index a6a191a..2631118 100644 --- a/src/com/android/messaging/ui/conversation/ConversationFragment.java +++ b/src/com/android/messaging/ui/conversation/ConversationFragment.java @@ -191,7 +191,8 @@ public class ConversationFragment extends Fragment implements ConversationDataLi intent.getStringExtra(UIIntents.UI_INTENT_EXTRA_CONVERSATION_SELF_ID); Assert.notNull(conversationId); Assert.notNull(selfId); - if (TextUtils.equals(mBinding.getData().getConversationId(), conversationId)) { + if (isBound() && TextUtils + .equals(mBinding.getData().getConversationId(), conversationId)) { mComposeMessageView.updateConversationSelfIdOnExternalChange(selfId); } } @@ -564,6 +565,7 @@ public class ConversationFragment extends Fragment implements ConversationDataLi @Override public void run() { view.setAlpha(1); + dispatchAddFinished(holder); } }); mPopupTransitionAnimation.startAfterLayoutComplete(); diff --git a/src/com/android/messaging/ui/conversationlist/ConversationListItemView.java b/src/com/android/messaging/ui/conversationlist/ConversationListItemView.java index 6b02eb3..f8920e0 100644 --- a/src/com/android/messaging/ui/conversationlist/ConversationListItemView.java +++ b/src/com/android/messaging/ui/conversationlist/ConversationListItemView.java @@ -58,6 +58,7 @@ import com.android.messaging.util.PhoneUtils; import com.android.messaging.util.Typefaces; import com.android.messaging.util.UiUtils; import com.android.messaging.util.UriUtil; +import com.cyanogenmod.messaging.util.PrefsUtils; import java.util.List; @@ -75,6 +76,8 @@ public class ConversationListItemView extends FrameLayout implements OnClickList private static String sPlusOneString; private static String sPlusNString; + private static final int SWIPE_DIRECTION_RIGHT = 2; + public interface HostInterface { boolean isConversationSelected(final String conversationId); void onConversationClicked(final ConversationListItemData conversationListItemData, @@ -502,6 +505,18 @@ public class ConversationListItemView extends FrameLayout implements OnClickList final int notificationBellVisiblity = mData.getNotificationEnabled() ? GONE : VISIBLE; mNotificationBellView.setVisibility(notificationBellVisiblity); + + if (PrefsUtils.isSwipeRightToDeleteEnabled()) { + mCrossSwipeArchiveLeftImageView.setImageDrawable(getResources() + .getDrawable(R.drawable.ic_delete_small_dark)); + mCrossSwipeArchiveRightImageView.setImageDrawable(getResources() + .getDrawable(R.drawable.ic_archive_small_dark)); + } else { + mCrossSwipeArchiveLeftImageView.setImageDrawable(getResources() + .getDrawable(R.drawable.ic_archive_small_dark)); + mCrossSwipeArchiveRightImageView.setImageDrawable(getResources() + .getDrawable(R.drawable.ic_archive_small_dark)); + } } public boolean isSwipeAnimatable() { @@ -535,10 +550,16 @@ public class ConversationListItemView extends FrameLayout implements OnClickList } } - public void onSwipeComplete() { + public void onSwipeComplete(int swipeDirection) { final String conversationId = mData.getConversationId(); + if (PrefsUtils.isSwipeRightToDeleteEnabled() + && swipeDirection == ConversationListSwipeHelper.SWIPE_DIRECTION_RIGHT) { + mData.deleteConversation(); + UiUtils.showSnackBar(getContext(), getRootView(), + getResources().getString(R.string.conversation_deleted)); + return; + } UpdateConversationArchiveStatusAction.archiveConversation(conversationId); - final Runnable undoRunnable = new Runnable() { @Override public void run() { diff --git a/src/com/android/messaging/ui/conversationlist/ConversationListSwipeHelper.java b/src/com/android/messaging/ui/conversationlist/ConversationListSwipeHelper.java index 4988259..ac2aeb0 100644 --- a/src/com/android/messaging/ui/conversationlist/ConversationListSwipeHelper.java +++ b/src/com/android/messaging/ui/conversationlist/ConversationListSwipeHelper.java @@ -44,9 +44,9 @@ public class ConversationListSwipeHelper implements OnItemTouchListener { private static final float PERCENTAGE_OF_WIDTH_TO_DISMISS = 0.4f; private static final float FLING_PERCENTAGE_OF_WIDTH_TO_DISMISS = 0.05f; - private static final int SWIPE_DIRECTION_NONE = 0; - private static final int SWIPE_DIRECTION_LEFT = 1; - private static final int SWIPE_DIRECTION_RIGHT = 2; + public static final int SWIPE_DIRECTION_NONE = 0; + public static final int SWIPE_DIRECTION_LEFT = 1; + public static final int SWIPE_DIRECTION_RIGHT = 2; private final RecyclerView mRecyclerView; private final long mDefaultRestoreAnimationDuration; @@ -269,7 +269,7 @@ public class ConversationListSwipeHelper implements OnItemTouchListener { private void onSwipeGestureEnd(final ConversationListItemView itemView, final int swipeDirection) { if (swipeDirection == SWIPE_DIRECTION_RIGHT || swipeDirection == SWIPE_DIRECTION_LEFT) { - itemView.onSwipeComplete(); + itemView.onSwipeComplete(swipeDirection); } // Balances out onSwipeGestureStart. diff --git a/src/com/android/messaging/ui/mediapicker/AudioListAdapter.java b/src/com/android/messaging/ui/mediapicker/AudioListAdapter.java new file mode 100644 index 0000000..2520d6b --- /dev/null +++ b/src/com/android/messaging/ui/mediapicker/AudioListAdapter.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2016 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.messaging.ui.mediapicker; + +import android.content.Context; +import android.database.Cursor; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CursorAdapter; +import com.android.messaging.R; +import com.android.messaging.ui.mediapicker.AudioListItemView.HostInterface; +import com.android.messaging.util.Assert; + +/** + * Bridges between the image cursor loaded by GalleryBoundCursorLoader and the GalleryGridView. + */ +public class AudioListAdapter extends CursorAdapter { + private HostInterface mGgivHostInterface; + + public AudioListAdapter(final Context context, final Cursor cursor) { + super(context, cursor, 0); + } + + public void setHostInterface(final HostInterface ggivHostInterface) { + mGgivHostInterface = ggivHostInterface; + } + + /** + * {@inheritDoc} + */ + @Override + public void bindView(final View view, final Context context, final Cursor cursor) { + Assert.isTrue(view instanceof AudioListItemView); + final AudioListItemView audioListItemView = (AudioListItemView) view; + audioListItemView.bind(cursor, mGgivHostInterface); + } + + /** + * {@inheritDoc} + */ + @Override + public View newView(final Context context, final Cursor cursor, final ViewGroup parent) { + final LayoutInflater layoutInflater = LayoutInflater.from(context); + return layoutInflater.inflate(R.layout.audio_list_item_view, parent, false); + } +} diff --git a/src/com/android/messaging/ui/mediapicker/AudioListChooser.java b/src/com/android/messaging/ui/mediapicker/AudioListChooser.java new file mode 100644 index 0000000..de0445c --- /dev/null +++ b/src/com/android/messaging/ui/mediapicker/AudioListChooser.java @@ -0,0 +1,219 @@ +/* + * Copyright (C) 2016 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.messaging.ui.mediapicker; + +import android.Manifest; +import android.content.pm.PackageManager; +import android.database.Cursor; +import android.support.v7.app.ActionBar; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import com.android.messaging.Factory; +import com.android.messaging.R; +import com.android.messaging.datamodel.data.MediaPickerData; +import com.android.messaging.datamodel.data.MediaPickerData.MediaPickerDataListener; +import com.android.messaging.datamodel.data.MessagePartData; +import com.android.messaging.util.Assert; +import com.android.messaging.util.OsUtil; + +/** + * Chooser which allows the user to select one or more existing audios + */ +class AudioListChooser extends MediaChooser implements + AudioListView.AudioListViewListener, MediaPickerDataListener { + private final AudioListAdapter mAdapter; + private AudioListView mAudioListView; + private View mMissingPermissionView; + private static final String TAG = AudioListChooser.class.getSimpleName(); + + AudioListChooser(final MediaPicker mediaPicker) { + super(mediaPicker); + mAdapter = new AudioListAdapter(Factory.get().getApplicationContext(), null); + } + + @Override + public int getSupportedMediaTypes() { + return MediaPicker.MEDIA_TYPE_AUDIO; + } + + @Override + public View destroyView() { + mAudioListView.setAdapter(null); + mAdapter.setHostInterface(null); + // The loader is started only if startMediaPickerDataLoader() is called + if (OsUtil.hasStoragePermission()) { + mBindingRef.getData().destroyLoader(MediaPickerData.GALLERY_AUDIO_LOADER); + } + return super.destroyView(); + } + + @Override + public int getIconResource() { + return R.drawable.ic_library_music_white_24px; + } + + @Override + public int getIconDescriptionResource() { + return R.string.mediapicker_audioChooserDescription; + } + + @Override + public boolean canSwipeDown() { + return mAudioListView.canSwipeDown(); + } + + @Override + public void onItemSelected(final MessagePartData item) { + mMediaPicker.dispatchItemsSelected(item, !mAudioListView.isMultiSelectEnabled()); + } + + @Override + public void onItemUnselected(final MessagePartData item) { + mMediaPicker.dispatchItemUnselected(item); + } + + @Override + public void onConfirmSelection() { + // The user may only confirm if multiselect is enabled. + Assert.isTrue(mAudioListView.isMultiSelectEnabled()); + mMediaPicker.dispatchConfirmItemSelection(); + } + + @Override + public void onUpdate() { + mMediaPicker.invalidateOptionsMenu(); + } + + @Override + public void onCreateOptionsMenu(final MenuInflater inflater, final Menu menu) { + if (mView != null) { + mAudioListView.onCreateOptionsMenu(inflater, menu); + } + } + + @Override + public boolean onOptionsItemSelected(final MenuItem item) { + return (mView != null) ? mAudioListView.onOptionsItemSelected(item) : false; + } + + @Override + protected View createView(final ViewGroup container) { + final LayoutInflater inflater = getLayoutInflater(); + final View view = inflater.inflate( + R.layout.mediapicker_audio_list_chooser, + container /* root */, + false /* attachToRoot */); + + mAudioListView = (AudioListView) view.findViewById(R.id.audio_list_view); + mAdapter.setHostInterface(mAudioListView); + mAudioListView.setAdapter(mAdapter); + mAudioListView.setHostInterface(this); + mAudioListView.setDraftMessageDataModel(mMediaPicker.getDraftMessageDataModel()); + if (OsUtil.hasStoragePermission()) { + startMediaPickerDataLoader(); + } + + mMissingPermissionView = view.findViewById(R.id.missing_permission_view); + updateForPermissionState(OsUtil.hasStoragePermission()); + return view; + } + + @Override + int getActionBarTitleResId() { + return R.string.mediapicker_audio_list_title; + } + + @Override + void updateActionBar(final ActionBar actionBar) { + super.updateActionBar(actionBar); + if (mAudioListView == null) { + return; + } + final int selectionCount = mAudioListView.getSelectionCount(); + if (selectionCount > 0 && mAudioListView.isMultiSelectEnabled()) { + actionBar.setTitle(getContext().getResources().getString( + R.string.mediapicker_audio_list_title_selection, + selectionCount)); + } + } + + @Override + public void onMediaPickerDataUpdated(final MediaPickerData mediaPickerData, final Object data, + final int loaderId) { + mBindingRef.ensureBound(mediaPickerData); + Assert.equals(MediaPickerData.GALLERY_AUDIO_LOADER, loaderId); + Cursor rawCursor = null; + if (data instanceof Cursor) { + rawCursor = (Cursor) data; + } + + mAdapter.swapCursor(rawCursor); + } + + @Override + public void onResume() { + if (OsUtil.hasStoragePermission()) { + // Work around a bug in MediaStore where cursors querying the Files provider don't get + // updated for changes to Images.Media or Video.Media. + startMediaPickerDataLoader(); + updateForPermissionState(true); + } else { + updateForPermissionState(false); + } + } + + @Override + protected void setSelected(final boolean selected) { + super.setSelected(selected); + if (selected && !OsUtil.hasStoragePermission()) { + mMediaPicker.requestPermissions( + new String[] { Manifest.permission.READ_EXTERNAL_STORAGE }, + MediaPicker.AUDIO_LIBRARY_PERMISSION_REQUEST_CODE); + } + } + + private void startMediaPickerDataLoader() { + mBindingRef.getData().startLoader(MediaPickerData.GALLERY_AUDIO_LOADER, mBindingRef, null, + this); + } + + @Override + protected void onRequestPermissionsResult( + final int requestCode, final String permissions[], final int[] grantResults) { + if (requestCode == MediaPicker.AUDIO_LIBRARY_PERMISSION_REQUEST_CODE) { + final boolean permissionGranted = grantResults[0] == PackageManager.PERMISSION_GRANTED; + if (permissionGranted) { + startMediaPickerDataLoader(); + } + updateForPermissionState(permissionGranted); + } + } + + private void updateForPermissionState(final boolean granted) { + // onRequestPermissionsResult can sometimes get called before createView(). + if (mAudioListView == null) { + return; + } + + mAudioListView.setVisibility(granted ? View.VISIBLE : View.GONE); + mMissingPermissionView.setVisibility(granted ? View.GONE : View.VISIBLE); + } +} diff --git a/src/com/android/messaging/ui/mediapicker/AudioListItemView.java b/src/com/android/messaging/ui/mediapicker/AudioListItemView.java new file mode 100644 index 0000000..d95f1fd --- /dev/null +++ b/src/com/android/messaging/ui/mediapicker/AudioListItemView.java @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2016 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.messaging.ui.mediapicker; + +import android.content.Context; +import android.database.Cursor; +import android.util.AttributeSet; +import android.view.View; +import android.widget.CheckBox; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; +import com.android.messaging.R; +import com.android.messaging.datamodel.DataModel; +import com.android.messaging.datamodel.data.AudioListItemData; +import com.google.common.annotations.VisibleForTesting; + +/** + * Shows an item in the audio picker list view. Hosts an FileImageView with a checkbox. + */ +public class AudioListItemView extends LinearLayout { + private static final String TAG = AudioListItemView.class.getSimpleName(); + /** + * Implemented by the owner of this ListItemView instance to communicate on media + * picking and selection events. + */ + public interface HostInterface { + void onItemClicked(View view, AudioListItemData data, boolean longClick); + boolean isItemSelected(AudioListItemData data); + boolean isMultiSelectEnabled(); + } + + @VisibleForTesting + AudioListItemData mData; + TextView mAudioFilename; + CheckBox mCheckBox; + ImageView mImageIcon; + + private HostInterface mHostInterface; + private final OnClickListener mOnClickListener = new OnClickListener() { + @Override + public void onClick(final View v) { + mHostInterface.onItemClicked(AudioListItemView.this, mData, false /*longClick*/); + } + }; + + public AudioListItemView(final Context context, final AttributeSet attrs) { + super(context, attrs); + mData = DataModel.get().createAudioListItemData(); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mAudioFilename = (TextView)findViewById(R.id.audio_filename); + mImageIcon = (ImageView) findViewById(R.id.audio_button); + + mCheckBox = (CheckBox)findViewById(R.id.audio_checkbox); + mCheckBox.setOnClickListener(mOnClickListener); + setOnClickListener(mOnClickListener); + + final OnLongClickListener longClickListener = new OnLongClickListener() { + @Override + public boolean onLongClick(final View v) { + mHostInterface.onItemClicked(v, mData, true /* longClick */); + return true; + } + }; + setOnLongClickListener(longClickListener); + mCheckBox.setOnLongClickListener(longClickListener); + } + + + public void bind(final Cursor cursor, final HostInterface hostInterface) { + mData.bind(cursor); + mHostInterface = hostInterface; + updateViewState(); + } + + private void updateViewState() { + updateListItemView(); + if (mHostInterface.isMultiSelectEnabled()) { + mCheckBox.setVisibility(VISIBLE); + mCheckBox.setClickable(true); + mCheckBox.setChecked(mHostInterface.isItemSelected(mData)); + mImageIcon.setVisibility(GONE); + } else { + mCheckBox.setVisibility(GONE); + mCheckBox.setClickable(false); + mImageIcon.setVisibility(VISIBLE); + } + } + + private void updateListItemView() { + mAudioFilename.setText(mData.getAudioFilename()); + } + +} diff --git a/src/com/android/messaging/ui/mediapicker/AudioListView.java b/src/com/android/messaging/ui/mediapicker/AudioListView.java new file mode 100644 index 0000000..e097b9e --- /dev/null +++ b/src/com/android/messaging/ui/mediapicker/AudioListView.java @@ -0,0 +1,311 @@ +/* + * Copyright (C) 2016 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.messaging.ui.mediapicker; + +import android.content.Context; +import android.graphics.Rect; +import android.net.Uri; +import android.os.Parcel; +import android.os.Parcelable; +import android.support.v4.util.ArrayMap; +import android.util.AttributeSet; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import com.android.messaging.R; +import com.android.messaging.datamodel.binding.BindingBase; +import com.android.messaging.datamodel.binding.ImmutableBindingRef; +import com.android.messaging.datamodel.data.AudioListItemData; +import com.android.messaging.datamodel.data.DraftMessageData; +import com.android.messaging.datamodel.data.DraftMessageData.DraftMessageDataListener; +import com.android.messaging.datamodel.data.MessagePartData; +import com.android.messaging.ui.PersistentInstanceState; +import com.android.messaging.util.Assert; +import com.android.messaging.util.ContentType; +import com.android.messaging.util.LogUtil; + +import java.util.Iterator; +import java.util.Map; + +/** + * Shows a list of audio filenames from external storage in a ListView with multi-select + * capabilities + */ +public class AudioListView extends MediaPickerListView implements + AudioListItemView.HostInterface, + PersistentInstanceState, + DraftMessageDataListener { + /** + * Implemented by the owner of this GalleryGridView instance to communicate on image + * picking and multi-image selection events. + */ + public interface AudioListViewListener { + void onItemSelected(MessagePartData item); + void onItemUnselected(MessagePartData item); + void onConfirmSelection(); + void onUpdate(); + } + + private AudioListViewListener mListener; + + // TODO: Consider putting this into the data model object if we add more states. + private final ArrayMap<Uri, MessagePartData> mSelectedAudios; + private boolean mIsMultiSelectMode = false; + private ImmutableBindingRef<DraftMessageData> mDraftMessageDataModel; + + public AudioListView(final Context context, final AttributeSet attrs) { + super(context, attrs); + mSelectedAudios = new ArrayMap<Uri, MessagePartData>(); + } + + public void setHostInterface(final AudioListViewListener hostInterface) { + mListener = hostInterface; + } + + public void setDraftMessageDataModel(final BindingBase<DraftMessageData> dataModel) { + mDraftMessageDataModel = BindingBase.createBindingReference(dataModel); + mDraftMessageDataModel.getData().addListener(this); + } + + @Override + public void onItemClicked(final View view, final AudioListItemData data, + final boolean longClick) { + if (ContentType.isMediaType(data.getContentType())) { + if (longClick) { + // Turn on multi-select mode when an item is long-pressed. + setMultiSelectEnabled(true); + } + + final Rect startRect = new Rect(); + view.getGlobalVisibleRect(startRect); + if (isMultiSelectEnabled()) { + toggleItemSelection(startRect, data); + } else { + mListener.onItemSelected(data.constructMessagePartData(startRect)); + } + } else { + LogUtil.w(LogUtil.BUGLE_TAG, + "Selected item has invalid contentType " + data.getContentType()); + } + } + + @Override + public boolean isItemSelected(final AudioListItemData data) { + return mSelectedAudios.containsKey(data.getAudioUri()); + } + + int getSelectionCount() { + return mSelectedAudios.size(); + } + + @Override + public boolean isMultiSelectEnabled() { + return mIsMultiSelectMode; + } + + private void toggleItemSelection(final Rect startRect, final AudioListItemData data) { + Assert.isTrue(isMultiSelectEnabled()); + if (isItemSelected(data)) { + final MessagePartData item = mSelectedAudios.remove(data.getAudioUri()); + mListener.onItemUnselected(item); + if (mSelectedAudios.size() == 0) { + // No image is selected any more, turn off multi-select mode. + setMultiSelectEnabled(false); + } + } else { + final MessagePartData item = data.constructMessagePartData(startRect); + mSelectedAudios.put(data.getAudioUri(), item); + mListener.onItemSelected(item); + } + invalidateViews(); + } + + private void toggleMultiSelect() { + mIsMultiSelectMode = !mIsMultiSelectMode; + invalidateViews(); + } + + private void setMultiSelectEnabled(final boolean enabled) { + if (mIsMultiSelectMode != enabled) { + toggleMultiSelect(); + } + } + + private boolean canToggleMultiSelect() { + // We allow the user to toggle multi-select mode only when nothing has selected. If + // something has been selected, we show a confirm button instead. + return mSelectedAudios.size() == 0; + } + + public void onCreateOptionsMenu(final MenuInflater inflater, final Menu menu) { + inflater.inflate(R.menu.gallery_picker_menu, menu); + final MenuItem toggleMultiSelect = menu.findItem(R.id.action_multiselect); + final MenuItem confirmMultiSelect = menu.findItem(R.id.action_confirm_multiselect); + final boolean canToggleMultiSelect = canToggleMultiSelect(); + toggleMultiSelect.setVisible(canToggleMultiSelect); + confirmMultiSelect.setVisible(!canToggleMultiSelect); + } + + public boolean onOptionsItemSelected(final MenuItem item) { + switch (item.getItemId()) { + case R.id.action_multiselect: + Assert.isTrue(canToggleMultiSelect()); + toggleMultiSelect(); + return true; + + case R.id.action_confirm_multiselect: + Assert.isTrue(!canToggleMultiSelect()); + mListener.onConfirmSelection(); + return true; + } + return false; + } + + + @Override + public void onDraftChanged(final DraftMessageData data, final int changeFlags) { + mDraftMessageDataModel.ensureBound(data); + // Whenever attachment changed, refresh selection state to remove those that are not + // selected. + if ((changeFlags & DraftMessageData.ATTACHMENTS_CHANGED) == + DraftMessageData.ATTACHMENTS_CHANGED) { + refreshImageSelectionStateOnAttachmentChange(); + } + } + + @Override + public void onDraftAttachmentLimitReached(final DraftMessageData data) { + mDraftMessageDataModel.ensureBound(data); + // Whenever draft attachment limit is reach, refresh selection state to remove those + // not actually added to draft. + refreshImageSelectionStateOnAttachmentChange(); + } + + @Override + public void onDraftAttachmentLoadFailed() { + // Nothing to do since the failed attachment gets removed automatically. + } + + private void refreshImageSelectionStateOnAttachmentChange() { + boolean changed = false; + final Iterator<Map.Entry<Uri, MessagePartData>> iterator = + mSelectedAudios.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry<Uri, MessagePartData> entry = iterator.next(); + if (!mDraftMessageDataModel.getData().containsAttachment(entry.getKey())) { + iterator.remove(); + changed = true; + } + } + + if (changed) { + mListener.onUpdate(); + invalidateViews(); + } + } + + @Override // PersistentInstanceState + public Parcelable saveState() { + return onSaveInstanceState(); + } + + @Override // PersistentInstanceState + public void restoreState(final Parcelable restoredState) { + onRestoreInstanceState(restoredState); + invalidateViews(); + } + + @Override + public Parcelable onSaveInstanceState() { + final Parcelable superState = super.onSaveInstanceState(); + final SavedState savedState = new SavedState(superState); + savedState.isMultiSelectMode = mIsMultiSelectMode; + savedState.selectedAudios = mSelectedAudios.values() + .toArray(new MessagePartData[mSelectedAudios.size()]); + return savedState; + } + + @Override + public void onRestoreInstanceState(final Parcelable state) { + if (!(state instanceof SavedState)) { + super.onRestoreInstanceState(state); + return; + } + + final SavedState savedState = (SavedState) state; + super.onRestoreInstanceState(savedState.getSuperState()); + mIsMultiSelectMode = savedState.isMultiSelectMode; + mSelectedAudios.clear(); + for (int i = 0; i < savedState.selectedAudios.length; i++) { + final MessagePartData selectedAudio = savedState.selectedAudios[i]; + mSelectedAudios.put(selectedAudio.getContentUri(), selectedAudio); + } + } + + @Override // PersistentInstanceState + public void resetState() { + mSelectedAudios.clear(); + mIsMultiSelectMode = false; + invalidateViews(); + } + + public static class SavedState extends BaseSavedState { + boolean isMultiSelectMode; + MessagePartData[] selectedAudios; + + SavedState(final Parcelable superState) { + super(superState); + } + + private SavedState(final Parcel in) { + super(in); + isMultiSelectMode = in.readInt() == 1 ? true : false; + + // Read parts + final int partCount = in.readInt(); + selectedAudios = new MessagePartData[partCount]; + for (int i = 0; i < partCount; i++) { + selectedAudios[i] = ((MessagePartData) in.readParcelable( + MessagePartData.class.getClassLoader())); + } + } + + @Override + public void writeToParcel(final Parcel out, final int flags) { + super.writeToParcel(out, flags); + out.writeInt(isMultiSelectMode ? 1 : 0); + + // Write parts + out.writeInt(selectedAudios.length); + for (final MessagePartData image : selectedAudios) { + out.writeParcelable(image, flags); + } + } + + public static final Creator<SavedState> CREATOR = + new Creator<SavedState>() { + @Override + public SavedState createFromParcel(final Parcel in) { + return new SavedState(in); + } + @Override + public SavedState[] newArray(final int size) { + return new SavedState[size]; + } + }; + } +} diff --git a/src/com/android/messaging/ui/mediapicker/AudioMediaChooser.java b/src/com/android/messaging/ui/mediapicker/AudioMediaChooser.java index 5d79293..3553f10 100644 --- a/src/com/android/messaging/ui/mediapicker/AudioMediaChooser.java +++ b/src/com/android/messaging/ui/mediapicker/AudioMediaChooser.java @@ -123,6 +123,10 @@ class AudioMediaChooser extends MediaChooser implements final int requestCode, final String permissions[], final int[] grantResults) { if (requestCode == MediaPicker.RECORD_AUDIO_PERMISSION_REQUEST_CODE) { final boolean permissionGranted = grantResults[0] == PackageManager.PERMISSION_GRANTED; + // onRequestPermissionsResult can sometimes get called before createView(). + if (mEnabledView == null) { + return; + } mEnabledView.setVisibility(permissionGranted ? View.VISIBLE : View.GONE); mMissingPermissionView.setVisibility(permissionGranted ? View.GONE : View.VISIBLE); } diff --git a/src/com/android/messaging/ui/mediapicker/GalleryGridItemView.java b/src/com/android/messaging/ui/mediapicker/GalleryGridItemView.java index 3d71fe6..034ca8b 100644 --- a/src/com/android/messaging/ui/mediapicker/GalleryGridItemView.java +++ b/src/com/android/messaging/ui/mediapicker/GalleryGridItemView.java @@ -18,7 +18,9 @@ package com.android.messaging.ui.mediapicker; import android.content.Context; import android.database.Cursor; import android.graphics.Rect; +import android.support.v7.mms.CarrierConfigValuesLoader; import android.util.AttributeSet; +import android.util.Log; import android.view.MotionEvent; import android.view.TouchDelegate; import android.view.View; @@ -26,11 +28,16 @@ import android.widget.CheckBox; import android.widget.FrameLayout; import android.widget.ImageView.ScaleType; +import android.widget.Toast; import com.android.messaging.R; import com.android.messaging.datamodel.DataModel; +import com.android.messaging.datamodel.data.DraftMessageData; import com.android.messaging.datamodel.data.GalleryGridItemData; +import com.android.messaging.datamodel.data.ParticipantData; +import com.android.messaging.sms.MmsConfig; import com.android.messaging.ui.AsyncImageView; import com.android.messaging.ui.ConversationDrawables; +import com.android.messaging.util.UriUtil; import com.google.common.annotations.VisibleForTesting; import java.util.concurrent.TimeUnit; @@ -39,6 +46,7 @@ import java.util.concurrent.TimeUnit; * Shows an item in the gallery picker grid view. Hosts an FileImageView with a checkbox. */ public class GalleryGridItemView extends FrameLayout { + private static final String TAG = GalleryGridItemView.class.getSimpleName(); /** * Implemented by the owner of this GalleryGridItemView instance to communicate on media * picking and selection events. @@ -47,17 +55,74 @@ public class GalleryGridItemView extends FrameLayout { void onItemClicked(View view, GalleryGridItemData data, boolean longClick); boolean isItemSelected(GalleryGridItemData data); boolean isMultiSelectEnabled(); + int getSubscriptionProviderSubId(); } @VisibleForTesting GalleryGridItemData mData; + private View mVideoButtonOverlayView; private AsyncImageView mImageView; private CheckBox mCheckBox; private HostInterface mHostInterface; + private static long mTotalContentSize = 0; + private static long mMaxMessageSize = 0; + + private boolean checkSize() { + if (mData.isDocumentPickerItem()) { + return true; + } + // only perform the check for videos, since they are not being compressed + // images will be compressed, so exclude them from this check + + // determine the maximum message size, this will be computed only once per this class + if (mMaxMessageSize == 0) { + int subId = mHostInterface.getSubscriptionProviderSubId(); + mMaxMessageSize = MmsConfig.get(subId).getMaxMessageSize(); + } + + long contentSize = mData.getContentSize(); + if (mHostInterface.isMultiSelectEnabled()) { + if (mData.isVideoItem()) { + if (mHostInterface.isItemSelected(mData)) { + // un-selecting + mTotalContentSize -= contentSize; + if (mTotalContentSize < 0) { + mTotalContentSize = 0; + } + } else { + // selecting + mTotalContentSize += contentSize; + } + } + } else { + // short click or first long click + if (mData.isVideoItem()) { + mTotalContentSize = contentSize; + } else { + mTotalContentSize = 0; + } + } + + if (mTotalContentSize > mMaxMessageSize) { + mTotalContentSize -= contentSize; + if (mTotalContentSize < 0) { + mTotalContentSize = 0; + } + + Toast.makeText(getContext(), getContext(). + getString(R.string.mediapicker_gallery_image_item_attachment_too_large), + Toast.LENGTH_LONG).show(); + return false; + } + + return true; + } private final OnClickListener mOnClickListener = new OnClickListener() { @Override public void onClick(final View v) { - mHostInterface.onItemClicked(GalleryGridItemView.this, mData, false /*longClick*/); + if (checkSize()) { + mHostInterface.onItemClicked(GalleryGridItemView.this, mData, false /*longClick*/); + } } }; @@ -69,6 +134,7 @@ public class GalleryGridItemView extends FrameLayout { @Override protected void onFinishInflate() { super.onFinishInflate(); + mVideoButtonOverlayView = findViewById(R.id.video_button); mImageView = (AsyncImageView) findViewById(R.id.image); mCheckBox = (CheckBox) findViewById(R.id.checkbox); mCheckBox.setOnClickListener(mOnClickListener); @@ -76,7 +142,9 @@ public class GalleryGridItemView extends FrameLayout { final OnLongClickListener longClickListener = new OnLongClickListener() { @Override public boolean onLongClick(final View v) { - mHostInterface.onItemClicked(v, mData, true /* longClick */); + if (checkSize()) { + mHostInterface.onItemClicked(v, mData, true /* longClick */); + } return true; } }; @@ -136,12 +204,26 @@ public class GalleryGridItemView extends FrameLayout { private void updateImageView() { if (mData.isDocumentPickerItem()) { + hideVideoPlayButtonOverlay(); mImageView.setScaleType(ScaleType.CENTER); setBackgroundColor(ConversationDrawables.get().getConversationThemeColor()); mImageView.setImageResourceId(null); mImageView.setImageResource(R.drawable.ic_photo_library_light); mImageView.setContentDescription(getResources().getString( R.string.pick_image_from_document_library_content_description)); + } else if (mData.isVideoItem()) { + showVideoPlayButtonOverlay(); + mImageView.setScaleType(ScaleType.CENTER_CROP); + setBackgroundColor(getResources().getColor(R.color.gallery_image_default_background)); + mImageView.setImageResourceId(mData.getImageRequestDescriptor()); + final long dateSeconds = mData.getDateSeconds(); + final boolean isValidDate = (dateSeconds > 0); + final int templateId = isValidDate ? + R.string.mediapicker_gallery_image_item_description : + R.string.mediapicker_gallery_image_item_description_no_date; + String contentDescription = String.format(getResources().getString(templateId), + dateSeconds * TimeUnit.SECONDS.toMillis(1)); + mImageView.setContentDescription(contentDescription); } else { mImageView.setScaleType(ScaleType.CENTER_CROP); setBackgroundColor(getResources().getColor(R.color.gallery_image_default_background)); @@ -156,4 +238,17 @@ public class GalleryGridItemView extends FrameLayout { mImageView.setContentDescription(contentDescription); } } + + private void showVideoPlayButtonOverlay() { + if (mVideoButtonOverlayView != null) { + mVideoButtonOverlayView.setVisibility(View.VISIBLE); + } + } + + private void hideVideoPlayButtonOverlay() { + if (mVideoButtonOverlayView != null) { + mVideoButtonOverlayView.setVisibility(View.INVISIBLE); + } + } + } diff --git a/src/com/android/messaging/ui/mediapicker/GalleryGridView.java b/src/com/android/messaging/ui/mediapicker/GalleryGridView.java index a5a7dad..62f7f6f 100644 --- a/src/com/android/messaging/ui/mediapicker/GalleryGridView.java +++ b/src/com/android/messaging/ui/mediapicker/GalleryGridView.java @@ -34,6 +34,7 @@ import com.android.messaging.datamodel.data.DraftMessageData; import com.android.messaging.datamodel.data.GalleryGridItemData; import com.android.messaging.datamodel.data.MessagePartData; import com.android.messaging.datamodel.data.DraftMessageData.DraftMessageDataListener; +import com.android.messaging.datamodel.data.ParticipantData; import com.android.messaging.ui.PersistentInstanceState; import com.android.messaging.util.Assert; import com.android.messaging.util.ContentType; @@ -69,11 +70,20 @@ public class GalleryGridView extends MediaPickerGridView implements private boolean mIsMultiSelectMode = false; private ImmutableBindingRef<DraftMessageData> mDraftMessageDataModel; + /** Provides subscription-related data to access per-subscription configurations. */ + private DraftMessageData.DraftMessageSubscriptionDataProvider mSubscriptionDataProvider; + + public GalleryGridView(final Context context, final AttributeSet attrs) { super(context, attrs); mSelectedImages = new ArrayMap<Uri, MessagePartData>(); } + public void setSubscriptionProvider(DraftMessageData.DraftMessageSubscriptionDataProvider + provider) { + mSubscriptionDataProvider = provider; + } + public void setHostInterface(final GalleryGridViewListener hostInterface) { mListener = hostInterface; } @@ -121,6 +131,16 @@ public class GalleryGridView extends MediaPickerGridView implements return mIsMultiSelectMode; } + @Override + public int getSubscriptionProviderSubId() { + if (mSubscriptionDataProvider != null) { + return mSubscriptionDataProvider.getConversationSelfSubId(); + } else { + return ParticipantData.DEFAULT_SELF_SUB_ID; + } + } + + private void toggleItemSelection(final Rect startRect, final GalleryGridItemData data) { Assert.isTrue(isMultiSelectEnabled()); if (isItemSelected(data)) { diff --git a/src/com/android/messaging/ui/mediapicker/GalleryMediaChooser.java b/src/com/android/messaging/ui/mediapicker/GalleryMediaChooser.java index 9422386..af360bc 100644 --- a/src/com/android/messaging/ui/mediapicker/GalleryMediaChooser.java +++ b/src/com/android/messaging/ui/mediapicker/GalleryMediaChooser.java @@ -21,7 +21,9 @@ import android.content.pm.PackageManager; import android.database.Cursor; import android.database.MatrixCursor; import android.database.MergeCursor; +import android.provider.Telephony; import android.support.v7.app.ActionBar; +import android.support.v7.mms.CarrierConfigValuesLoader; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; @@ -29,6 +31,7 @@ import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; +import android.widget.Toast; import com.android.messaging.Factory; import com.android.messaging.R; import com.android.messaging.datamodel.data.GalleryGridItemData; @@ -37,6 +40,7 @@ import com.android.messaging.datamodel.data.MessagePartData; import com.android.messaging.datamodel.data.MediaPickerData.MediaPickerDataListener; import com.android.messaging.util.Assert; import com.android.messaging.util.OsUtil; +import com.android.messaging.util.UriUtil; /** * Chooser which allows the user to select one or more existing images or videos @@ -46,6 +50,7 @@ class GalleryMediaChooser extends MediaChooser implements private final GalleryGridAdapter mAdapter; private GalleryGridView mGalleryGridView; private View mMissingPermissionView; + private static final String TAG = GalleryMediaChooser.class.getSimpleName(); GalleryMediaChooser(final MediaPicker mediaPicker) { super(mediaPicker); @@ -127,6 +132,7 @@ class GalleryMediaChooser extends MediaChooser implements mGalleryGridView = (GalleryGridView) view.findViewById(R.id.gallery_grid_view); mAdapter.setHostInterface(mGalleryGridView); + mGalleryGridView.setSubscriptionProvider(this); mGalleryGridView.setAdapter(mAdapter); mGalleryGridView.setHostInterface(this); mGalleryGridView.setDraftMessageDataModel(mMediaPicker.getDraftMessageDataModel()); @@ -172,6 +178,7 @@ class GalleryMediaChooser extends MediaChooser implements if (data instanceof Cursor) { rawCursor = (Cursor) data; } + // Before delivering the cursor, wrap around the local gallery cursor // with an extra item for document picker integration in the front. final MatrixCursor specialItemsCursor = @@ -188,6 +195,9 @@ class GalleryMediaChooser extends MediaChooser implements // Work around a bug in MediaStore where cursors querying the Files provider don't get // updated for changes to Images.Media or Video.Media. startMediaPickerDataLoader(); + updateForPermissionState(true); + } else { + updateForPermissionState(false); } } diff --git a/src/com/android/messaging/ui/mediapicker/MediaPicker.java b/src/com/android/messaging/ui/mediapicker/MediaPicker.java index f441d09..6692620 100644 --- a/src/com/android/messaging/ui/mediapicker/MediaPicker.java +++ b/src/com/android/messaging/ui/mediapicker/MediaPicker.java @@ -178,6 +178,7 @@ public class MediaPicker extends Fragment implements DraftMessageSubscriptionDat mChoosers = new MediaChooser[] { new CameraMediaChooser(this), new GalleryMediaChooser(this), + new AudioListChooser(this), new AudioMediaChooser(this), }; @@ -725,6 +726,7 @@ public class MediaPicker extends Fragment implements DraftMessageSubscriptionDat protected static final int LOCATION_PERMISSION_REQUEST_CODE = 2; protected static final int RECORD_AUDIO_PERMISSION_REQUEST_CODE = 3; protected static final int GALLERY_PERMISSION_REQUEST_CODE = 4; + protected static final int AUDIO_LIBRARY_PERMISSION_REQUEST_CODE = 5; @Override public void onRequestPermissionsResult( diff --git a/src/com/android/messaging/ui/mediapicker/MediaPickerListView.java b/src/com/android/messaging/ui/mediapicker/MediaPickerListView.java new file mode 100644 index 0000000..f448b87 --- /dev/null +++ b/src/com/android/messaging/ui/mediapicker/MediaPickerListView.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2015 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.messaging.ui.mediapicker; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.GridView; +import android.widget.ListView; + +public class MediaPickerListView extends ListView { + + public MediaPickerListView(final Context context, final AttributeSet attrs) { + super(context, attrs); + } + + /** + * Returns if the list view can be swiped down further. It cannot be swiped down + * if there's no item or if we are already at the top. + */ + public boolean canSwipeDown() { + if (getAdapter() == null || getAdapter().getCount() == 0 || getChildCount() == 0) { + return false; + } + + final int firstVisiblePosition = getFirstVisiblePosition(); + if (firstVisiblePosition == 0 && getChildAt(0).getTop() >= 0) { + return false; + } + return true; + } +} diff --git a/src/com/android/messaging/util/NotificationUtil.java b/src/com/android/messaging/util/NotificationUtil.java new file mode 100644 index 0000000..6afa961 --- /dev/null +++ b/src/com/android/messaging/util/NotificationUtil.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2016 The CyanogenMod 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.messaging.util; + +import android.content.Context; + +import com.android.messaging.Factory; +import com.android.messaging.R; + +public class NotificationUtil { + /** + * Get the final enabled status for notifications in this conversation, based on the given + * value, and the default (application-wide) value. + * + * @param conversationVal the custom value for this conversation, or -1 if it does not exist. + * @return whether notifications should be enabled for this conversation. + */ + public static boolean getConversationNotificationEnabled(int conversationVal) { + return getEnabledCustomOrDefault(conversationVal, R.string.notifications_enabled_pref_key); + } + + /** + * Get the final enabled status for notification vibration in this conversation, based on the + * given value and the default (application-wide) value. + * + * @param conversationVal the custom value for this conversation, or -1 if it does not exist. + * @return whether notification vibration should be enabled for this conversation. + */ + public static boolean getConversationNotificationVibrateEnabled(int conversationVal) { + return getEnabledCustomOrDefault(conversationVal, R.string.notification_vibration_pref_key); + } + + private static boolean getEnabledCustomOrDefault(int customVal, int keyRes) { + // Load default if we do not have a custom value set. + if (customVal == -1) { + final BuglePrefs prefs = BuglePrefs.getApplicationPrefs(); + final Context context = Factory.get().getApplicationContext(); + final String prefKey = context.getString(keyRes); + return prefs.getBoolean(prefKey, true); + } else { + return customVal == 1; + } + } +} diff --git a/src/com/android/messaging/util/UiUtils.java b/src/com/android/messaging/util/UiUtils.java index 84fe353..a9a1dc4 100644 --- a/src/com/android/messaging/util/UiUtils.java +++ b/src/com/android/messaging/util/UiUtils.java @@ -132,6 +132,16 @@ public class UiUtils { null /* placement */); } + public static void showSnackBar(final Context context, @NonNull final View parentView, + final String message) { + Assert.notNull(context); + Assert.isTrue(!TextUtils.isEmpty(message)); + SnackBarManager.get() + .newBuilder(parentView) + .setText(message) + .show(); + } + public static void showSnackBarWithCustomAction(final Context context, @NonNull final View parentView, @NonNull final String message, diff --git a/src/com/cyanogenmod/messaging/util/PrefsUtils.java b/src/com/cyanogenmod/messaging/util/PrefsUtils.java new file mode 100644 index 0000000..7247bdb --- /dev/null +++ b/src/com/cyanogenmod/messaging/util/PrefsUtils.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2016 The CyanogenMod 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.cyanogenmod.messaging.util; + +import android.content.Context; +import com.android.messaging.Factory; +import com.android.messaging.R; +import com.android.messaging.util.BuglePrefs; + +public class PrefsUtils { + public static final String SHOW_EMOTICONS_ENABLED = "pref_show_emoticons"; + + private PrefsUtils() { + //Don't instantiate + } + + /** + * Returns whether or not swipe to dismiss in the ConversationListFragment deletes + * the conversation rather than archiving it. + * @return hopefully true + */ + public static boolean isSwipeRightToDeleteEnabled() { + final BuglePrefs prefs = BuglePrefs.getApplicationPrefs(); + final Context context = Factory.get().getApplicationContext(); + final String prefKey = context.getString(R.string.swipe_right_deletes_conversation_key); + final boolean defaultValue = context.getResources().getBoolean( + R.bool.swipe_right_deletes_conversation_default); + return prefs.getBoolean(prefKey, defaultValue); + } + + public static boolean isShowEmoticonsEnabled() { + final BuglePrefs prefs = BuglePrefs.getApplicationPrefs(); + final Context context = Factory.get().getApplicationContext(); + final boolean defaultValue = context.getResources().getBoolean( + R.bool.show_emoticons_pref_default); + return prefs.getBoolean(SHOW_EMOTICONS_ENABLED, defaultValue); + } +} diff --git a/tests/src/com/android/messaging/datamodel/FakeDataModel.java b/tests/src/com/android/messaging/datamodel/FakeDataModel.java index 5e80eab..c411fb2 100644 --- a/tests/src/com/android/messaging/datamodel/FakeDataModel.java +++ b/tests/src/com/android/messaging/datamodel/FakeDataModel.java @@ -23,6 +23,7 @@ import android.test.RenamingDelegatingContext; import com.android.messaging.datamodel.action.ActionService; import com.android.messaging.datamodel.action.BackgroundWorker; +import com.android.messaging.datamodel.data.AudioListItemData; import com.android.messaging.datamodel.data.BlockedParticipantsData; import com.android.messaging.datamodel.data.BlockedParticipantsData.BlockedParticipantsDataListener; import com.android.messaging.datamodel.data.ContactListItemData; @@ -165,6 +166,11 @@ public class FakeDataModel extends DataModel { } @Override + public AudioListItemData createAudioListItemData() { + return new AudioListItemData(); + } + + @Override public LaunchConversationData createLaunchConversationData( final LaunchConversationDataListener listener) { return new LaunchConversationData(listener); |