diff options
31 files changed, 3535 insertions, 8 deletions
diff --git a/Android.mk b/Android.mk index 0e8cb5a7b..d68735ca8 100644 --- a/Android.mk +++ b/Android.mk @@ -22,13 +22,16 @@ LOCAL_SRC_FILES += ../../providers/ContactsProvider/src/com/android/providers/co ../../providers/ContactsProvider/src/com/android/providers/contacts/HanziToPinyin.java \ ../../providers/ContactsProvider/src/com/android/providers/contacts/util/NeededForTesting.java LOCAL_RESOURCE_DIR := $(addprefix $(LOCAL_PATH)/, $(res_dirs)) \ - frameworks/support/v7/cardview/res frameworks/support/v7/recyclerview/res + frameworks/support/v7/cardview/res frameworks/support/v7/recyclerview/res \ + frameworks/support/v7/appcompat/res frameworks/support/design/res LOCAL_ASSET_DIR += $(LOCAL_PATH)/assets LOCAL_AAPT_FLAGS := \ --auto-add-overlay \ --extra-packages android.support.v7.cardview \ --extra-packages android.support.v7.recyclerview \ + --extra-packages android.support.v7.appcompat \ + --extra-packages android.support.design \ --extra-packages com.android.incallui \ --extra-packages com.android.contacts.common \ --extra-packages com.android.phone.common @@ -39,8 +42,10 @@ LOCAL_STATIC_JAVA_LIBRARIES := \ android-common \ android-support-v13 \ android-support-v4 \ + android-support-v7-appcompat \ android-support-v7-cardview \ android-support-v7-recyclerview \ + android-support-design \ com.android.services.telephony.common \ com.android.vcard \ guava \ diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 882122bfe..6e09c8c26 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -173,6 +173,17 @@ </intent-filter> </activity> + <activity android:name=".callstats.CallStatsDetailActivity" + android:label="@string/callStatsDetailTitle" + android:theme="@style/CallDetailActivityTheme" + android:screenOrientation="portrait" + android:icon="@mipmap/ic_launcher_phone" > + <intent-filter> + <action android:name="android.intent.action.VIEW" /> + <category android:name="android.intent.category.DEFAULT" /> + </intent-filter> + </activity> + <activity android:name="com.android.contacts.common.test.FragmentTestActivity"> <intent-filter> <category android:name="android.intent.category.TEST"/> diff --git a/proguard.flags b/proguard.flags index 2bf5215c9..4f38ffd31 100644 --- a/proguard.flags +++ b/proguard.flags @@ -15,4 +15,7 @@ # Keep ExtendedLookupDirectories for assets/contacts_extensions.properties -keep class com.android.dialer.lookup.ExtendedLookupDirectories { *; } +# Keep behavior class needed by design library +-keep class android.support.design.widget.AppBarLayout$ScrollingViewBehavior { *; } + -verbose diff --git a/res/layout/call_log_spinner_item.xml b/res/layout/call_log_spinner_item.xml new file mode 100644 index 000000000..045deb737 --- /dev/null +++ b/res/layout/call_log_spinner_item.xml @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +Copyright (c) 2013-2014, The Linux Foundation. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of The Linux Foundation, Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED +WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS +BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR +BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE +OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN +IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +--> +<TextView xmlns:android="http://schemas.android.com/apk/res/android" + style="@style/CallLogSpinnerStyle" + android:layout_width="match_parent" + android:layout_height="40dip" + android:paddingLeft="8dip" + android:paddingRight="8dip" /> diff --git a/res/layout/call_spinner.xml b/res/layout/call_spinner.xml new file mode 100644 index 000000000..708d06919 --- /dev/null +++ b/res/layout/call_spinner.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:paddingStart="@dimen/call_log_outer_margin" + android:paddingEnd="@dimen/call_log_outer_margin" + android:paddingBottom="@dimen/call_log_inner_margin"> + <Spinner + android:id="@+id/filter_sub_spinner" + android:layout_width="0dip" + android:layout_height="@dimen/list_section_divider_min_height" + android:layout_weight="1.2" + android:layout_marginTop="5dip" /> + <Spinner + android:id="@+id/filter_status_spinner" + android:layout_width="0dip" + android:layout_height="@dimen/list_section_divider_min_height" + android:layout_weight="2" + android:layout_marginTop="5dip" /> +</LinearLayout> diff --git a/res/layout/call_stats_detail.xml b/res/layout/call_stats_detail.xml new file mode 100644 index 000000000..4df88d7de --- /dev/null +++ b/res/layout/call_stats_detail.xml @@ -0,0 +1,109 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2009 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/call_stats_detail" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" + android:layout_alignParentStart="true" + android:layout_alignParentTop="true" + android:background="@color/background_dialer_call_log" > + + <!-- Caller information "card" --> + <LinearLayout + android:id="@+id/caller_information" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingStart="@dimen/call_detail_horizontal_margin" + android:paddingTop="@dimen/call_detail_top_margin" + android:paddingBottom="@dimen/call_detail_bottom_margin" + android:baselineAligned="false" + android:orientation="horizontal" + android:translationZ="@dimen/call_detail_translation_z" + android:focusable="true" + android:background="@color/background_dialer_white" > + + <QuickContactBadge + android:id="@+id/quick_contact_photo" + android:layout_width="@dimen/contact_photo_size" + android:layout_height="@dimen/contact_photo_size" + android:layout_alignParentStart="true" + android:layout_gravity="top" + android:layout_marginTop="3dp" + android:focusable="true" /> + + <LinearLayout + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:orientation="vertical" + android:gravity="center_vertical" + android:layout_marginStart="@dimen/call_detail_horizontal_margin"> + + <TextView + android:id="@+id/caller_name" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textColor="?attr/call_log_primary_text_color" + android:textSize="@dimen/call_log_primary_text_size" + android:includeFontPadding="false" + android:layout_marginBottom="5dp" + android:singleLine="true" /> + + <TextView + android:id="@+id/caller_number" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textColor="?attr/call_log_secondary_text_color" + android:textSize="@dimen/call_log_secondary_text_size" + android:layout_marginBottom="1dp" + android:singleLine="true" /> + + <TextView + android:id="@+id/phone_account_label" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textColor="?attr/call_log_secondary_text_color" + android:textSize="@dimen/call_log_secondary_text_size" + android:singleLine="true" + android:visibility="gone" + /> + </LinearLayout> + + <ImageView + android:id="@+id/call_back_button" + android:layout_width="@dimen/call_log_list_item_primary_action_dimen" + android:layout_height="@dimen/call_log_list_item_primary_action_dimen" + android:layout_marginEnd="16dp" + android:background="?android:attr/selectableItemBackgroundBorderless" + android:src="@drawable/ic_call_24dp" + android:scaleType="center" + android:tint="@color/call_log_list_item_primary_action_icon_tint" + android:contentDescription="@string/description_call_log_call_action" + android:visibility="gone" /> + + </LinearLayout> + + <ScrollView + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <!-- The actual details --> + <include layout="@layout/call_stats_detail_info" /> + + </ScrollView> + +</LinearLayout> diff --git a/res/layout/call_stats_detail_info.xml b/res/layout/call_stats_detail_info.xml new file mode 100644 index 000000000..c636c1ab1 --- /dev/null +++ b/res/layout/call_stats_detail_info.xml @@ -0,0 +1,239 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2011 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:ex="http://schemas.android.com/apk/res/com.android.dialer" + android:id="@+id/call_stats_info" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:paddingStart="@dimen/call_detail_horizontal_margin" + android:paddingEnd="@dimen/call_detail_horizontal_margin" + android:paddingTop="@dimen/call_log_inner_margin" + android:paddingBottom="@dimen/call_log_inner_margin"> + + <TextView + android:id="@+id/date_filter" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:textAppearance="?android:attr/textAppearanceSmall" + android:textColor="@color/secondary_text_color" /> + + <RelativeLayout + android:id="@+id/duration_container" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_below="@id/date_filter"> + + <TextView + android:id="@+id/durations_header" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="@string/call_stats_title_durations" + android:layout_marginBottom="@dimen/call_log_vertical_padding" + android:textColor="?android:attr/colorAccent" + style="@android:style/TextAppearance.Material.Subhead" /> + + <TextView + android:id="@+id/total_duration_total" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentEnd="true" + android:layout_below="@id/durations_header" + android:textColor="?attr/call_log_primary_text_color" + android:textSize="@dimen/call_log_primary_text_size" /> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentStart="true" + android:layout_toStartOf="@id/total_duration_total" + android:layout_alignBaseline="@id/total_duration_total" + android:text="@string/call_stats_title_of_total" + android:textColor="?attr/call_log_primary_text_color" + android:textSize="@dimen/call_log_primary_text_size" /> + + <com.android.dialer.widget.LinearColorBar + android:id="@+id/duration_total_percent_bar" + android:layout_width="match_parent" + android:layout_below="@id/total_duration_total" + style="@style/CallStatsBarStyle" /> + + <TextView + android:id="@+id/total_duration_number" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentEnd="true" + android:layout_below="@id/duration_total_percent_bar" + android:textColor="?attr/call_log_primary_text_color" + android:textSize="@dimen/call_log_primary_text_size" /> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentStart="true" + android:layout_toStartOf="@id/total_duration_number" + android:layout_alignBaseline="@id/total_duration_number" + android:text="@string/call_stats_title_for_number" + android:textColor="?attr/call_log_primary_text_color" + android:textSize="@dimen/call_log_primary_text_size" /> + + <com.android.dialer.widget.LinearColorBar + android:id="@+id/duration_number_percent_bar" + android:layout_width="match_parent" + android:layout_below="@id/total_duration_number" + style="@style/CallStatsBarStyle" /> + + <include + android:id="@+id/in_duration" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_below="@id/duration_number_percent_bar" + android:layout_marginBottom="@dimen/call_log_icon_margin" + layout="@layout/call_stats_detail_line" /> + + <include + android:id="@+id/out_duration" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_below="@id/in_duration" + android:layout_marginBottom="@dimen/call_log_icon_margin" + layout="@layout/call_stats_detail_line" /> + + </RelativeLayout> + + <TextView + android:id="@+id/count_header" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_below="@id/duration_container" + android:layout_marginTop="@dimen/call_log_vertical_padding" + android:layout_marginBottom="@dimen/call_log_vertical_padding" + android:text="@string/call_stats_title_count" + android:textColor="?android:attr/colorAccent" + style="@android:style/TextAppearance.Material.Subhead" /> + + <TextView + android:id="@+id/total_count_total" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentEnd="true" + android:layout_below="@id/count_header" + android:textColor="?attr/call_log_primary_text_color" + android:textSize="@dimen/call_log_primary_text_size" /> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentStart="true" + android:layout_toStartOf="@id/total_count_total" + android:layout_alignBaseline="@id/total_count_total" + android:text="@string/call_stats_title_of_total" + android:textColor="?attr/call_log_primary_text_color" + android:textSize="@dimen/call_log_primary_text_size" /> + + <com.android.dialer.widget.LinearColorBar + android:id="@+id/count_total_percent_bar" + android:layout_width="match_parent" + android:layout_below="@id/total_count_total" + style="@style/CallStatsBarStyle" /> + + <TextView + android:id="@+id/total_count_number" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentEnd="true" + android:layout_below="@id/count_total_percent_bar" + android:textColor="?attr/call_log_primary_text_color" + android:textSize="@dimen/call_log_primary_text_size" /> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentStart="true" + android:layout_toStartOf="@id/total_count_number" + android:layout_alignBaseline="@id/total_count_number" + android:text="@string/call_stats_title_for_number" + android:textColor="?attr/call_log_primary_text_color" + android:textSize="@dimen/call_log_primary_text_size" /> + + <com.android.dialer.widget.LinearColorBar + android:id="@+id/count_number_percent_bar" + android:layout_width="match_parent" + android:layout_below="@id/total_count_number" + style="@style/CallStatsBarStyle" /> + + <include + android:id="@+id/in_count" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_below="@id/count_number_percent_bar" + android:layout_marginBottom="@dimen/call_log_icon_margin" + layout="@layout/call_stats_detail_line" /> + + <include + android:id="@+id/out_count" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_below="@id/in_count" + android:layout_marginBottom="@dimen/call_log_icon_margin" + layout="@layout/call_stats_detail_line" /> + + <include + android:id="@+id/missed_count" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_below="@id/out_count" + android:layout_marginBottom="@dimen/call_log_icon_margin" + layout="@layout/call_stats_detail_line" /> + + <include + android:id="@+id/blacklisted_count" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_below="@id/missed_count" + android:layout_marginBottom="@dimen/call_log_icon_margin" + layout="@layout/call_stats_detail_line" /> + + <TextView + android:id="@+id/average_header" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_below="@id/blacklisted_count" + android:layout_marginTop="@dimen/call_log_vertical_padding" + android:layout_marginBottom="@dimen/call_log_vertical_padding" + android:text="@string/call_stats_title_average_duration" + android:textColor="?android:attr/colorAccent" + style="@android:style/TextAppearance.Material.Subhead" /> + + <include + android:id="@+id/in_average" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_below="@id/average_header" + android:layout_marginBottom="@dimen/call_log_icon_margin" + layout="@layout/call_stats_detail_line" /> + + <include + android:id="@+id/out_average" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_below="@id/in_average" + android:layout_marginBottom="@dimen/call_log_icon_margin" + layout="@layout/call_stats_detail_line" /> + +</RelativeLayout> diff --git a/res/layout/call_stats_detail_line.xml b/res/layout/call_stats_detail_line.xml new file mode 100644 index 000000000..579caa1ef --- /dev/null +++ b/res/layout/call_stats_detail_line.xml @@ -0,0 +1,45 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2011 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <com.android.dialer.calllog.CallTypeIconsView + android:id="@+id/icon" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center_vertical" /> + + <TextView + android:id="@+id/value" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:layout_marginStart="@dimen/call_log_icon_margin" + android:textColor="?attr/call_log_primary_text_color" + android:textSize="@dimen/call_log_primary_text_size" /> + + <TextView + android:id="@+id/percent" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/call_log_icon_margin" + android:textColor="?attr/call_log_secondary_text_color" + android:textSize="@dimen/call_log_secondary_text_size" /> + +</LinearLayout> diff --git a/res/layout/call_stats_fragment.xml b/res/layout/call_stats_fragment.xml new file mode 100644 index 000000000..140e2baa3 --- /dev/null +++ b/res/layout/call_stats_fragment.xml @@ -0,0 +1,80 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2011 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<!-- Layout parameters are set programmatically. --> +<android.support.design.widget.CoordinatorLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:design="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="@color/background_dialer_call_log" + design:statusBarBackground="@null" > + + <android.support.design.widget.AppBarLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="@color/background_dialer_call_log" + design:elevation="2dp"> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + design:layout_scrollFlags="scroll|enterAlways"> + + <include layout="@layout/call_spinner" /> + + <TextView + android:id="@+id/date_filter" + style="@style/ContactListSeparatorTextViewStyle" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/call_log_outer_margin" + android:layout_marginEnd="@dimen/call_log_outer_margin" + android:visibility="gone" + android:background="@null" /> + + <TextView + android:id="@+id/sum_header" + style="@style/ContactListSeparatorTextViewStyle" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/call_log_outer_margin" + android:layout_marginEnd="@dimen/call_log_outer_margin" + android:paddingBottom="@dimen/call_log_inner_margin" + android:visibility="gone" + android:background="@null" /> + </LinearLayout> + + </android.support.design.widget.AppBarLayout> + + <android.support.v7.widget.RecyclerView + android:id="@+id/recycler_view" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:paddingStart="@dimen/call_log_horizontal_margin" + android:paddingEnd="@dimen/call_log_horizontal_margin" + design:layout_behavior="@string/appbar_scrolling_view_behavior" /> + + <com.android.dialer.widget.EmptyContentView + android:id="@+id/empty_list_view" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:visibility="gone" /> + +</android.support.design.widget.CoordinatorLayout> diff --git a/res/layout/call_stats_list_item.xml b/res/layout/call_stats_list_item.xml new file mode 100644 index 000000000..ea9cc0776 --- /dev/null +++ b/res/layout/call_stats_list_item.xml @@ -0,0 +1,88 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2007 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. +--> + +<RelativeLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:ex="http://schemas.android.com/apk/res/com.android.dialer" + android:id="@+id/primary_action_view" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingStart="@dimen/call_log_start_margin" + android:paddingEnd="@dimen/call_log_outer_margin" + android:paddingTop="@dimen/call_log_vertical_padding" + android:paddingBottom="@dimen/call_log_vertical_padding" + android:background="?android:attr/selectableItemBackground"> + + <QuickContactBadge + android:id="@+id/quick_contact_photo" + android:layout_width="@dimen/contact_photo_size" + android:layout_height="@dimen/contact_photo_size" + android:layout_marginEnd="@dimen/call_log_start_margin" + android:paddingTop="2dp" /> + + <TextView + android:id="@+id/name" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_toEndOf="@id/quick_contact_photo" + android:ellipsize="marquee" + android:singleLine="true" + android:textColor="?attr/call_log_primary_text_color" + android:textSize="@dimen/call_log_primary_text_size" /> + + <TextView + android:id="@+id/percent" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_toEndOf="@id/name" + android:layout_alignBaseline="@id/name" + android:layout_alignParentEnd="true" + android:gravity="end" + android:textColor="?attr/call_log_secondary_text_color" + android:textSize="@dimen/call_log_secondary_text_size" /> + + <com.android.dialer.widget.LinearColorBar + android:id="@+id/percent_bar" + android:layout_width="wrap_content" + android:layout_below="@id/name" + android:layout_toEndOf="@id/quick_contact_photo" + android:layout_alignParentEnd="true" + style="@style/CallStatsBarStyle" /> + + <TextView + android:id="@+id/label" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_below="@id/percent_bar" + android:layout_toEndOf="@id/quick_contact_photo" + android:layout_marginEnd="8dp" + android:singleLine="true" + android:textColor="?attr/call_log_secondary_text_color" + android:textSize="@dimen/call_log_secondary_text_size" /> + + <TextView + android:id="@+id/number" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_below="@id/percent_bar" + android:layout_toEndOf="@id/label" + android:layout_alignBaseline="@id/label" + android:singleLine="true" + android:textColor="?attr/call_log_secondary_text_color" + android:textSize="12sp" /> + +</RelativeLayout> diff --git a/res/layout/double_date_picker_dialog.xml b/res/layout/double_date_picker_dialog.xml new file mode 100644 index 000000000..ec4a1f25b --- /dev/null +++ b/res/layout/double_date_picker_dialog.xml @@ -0,0 +1,67 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +** +** Copyright 2007, The Android Open Source Project +** +** Licensed under the Apache License, Version 2.0 (the "License"); +** you may not use this file except in compliance with the License. +** You may obtain a copy of the License at +** +** http://www.apache.org/licenses/LICENSE-2.0 +** +** Unless required by applicable law or agreed to in writing, software +** distributed under the License is distributed on an "AS IS" BASIS, +** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +** See the License for the specific language governing permissions and +** limitations under the License. +*/ +--> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:gravity="center" + android:orientation="vertical" > + + <Spinner + android:id="@+id/date_quick_selection" + android:layout_marginTop="3dp" + android:layout_marginBottom="3dp" + android:layout_width="wrap_content" + android:layout_height="wrap_content" /> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginBottom="-12dp" + android:layout_marginRight="3dp" + android:layout_marginTop="3dp" + android:text="@string/call_stats_filter_from" + android:textAppearance="?android:attr/textAppearanceMedium" /> + + <DatePicker + android:id="@+id/date_picker_from" + android:datePickerMode="spinner" + android:layout_width="wrap_content" + android:layout_height="0dp" + android:layout_weight="1" + android:calendarViewShown="false" + android:spinnersShown="true" /> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginBottom="-12dp" + android:layout_marginRight="4dp" + android:text="@string/call_stats_filter_to" + android:textAppearance="?android:attr/textAppearanceMedium" /> + + <DatePicker + android:id="@+id/date_picker_to" + android:datePickerMode="spinner" + android:layout_width="wrap_content" + android:layout_height="0dp" + android:layout_weight="1" + android:calendarViewShown="false" + android:spinnersShown="true" /> + +</LinearLayout> diff --git a/res/menu/call_stats_details_options.xml b/res/menu/call_stats_details_options.xml new file mode 100644 index 000000000..c16db3315 --- /dev/null +++ b/res/menu/call_stats_details_options.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2011 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + <item + android:id="@+id/menu_edit_number_before_call" + android:title="@string/recentCalls_editNumberBeforeCall" + android:onClick="onMenuEditNumberBeforeCall" + /> + <item + android:id="@+id/menu_add_to_blacklist" + android:title="@string/menu_add_to_blacklist" + android:onClick="onMenuAddToBlacklist" + /> +</menu> diff --git a/res/menu/call_stats_options.xml b/res/menu/call_stats_options.xml new file mode 100644 index 000000000..f4cdb93d8 --- /dev/null +++ b/res/menu/call_stats_options.xml @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2011 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<menu xmlns:android="http://schemas.android.com/apk/res/android" > + <item + android:id="@+id/date_filter" + android:showAsAction="never" + android:title="@string/call_stats_date_filter"/> + + <item + android:id="@+id/reset_date_filter" + android:showAsAction="never" + android:visible="false" + android:title="@string/call_stats_reset_filter"/> + + <item + android:id="@+id/sort_by_duration" + android:showAsAction="never" + android:visible="false" + android:title="@string/call_stats_sort_by_duration"/> + + <item + android:id="@+id/sort_by_count" + android:showAsAction="never" + android:title="@string/call_stats_sort_by_count"/> + +</menu> diff --git a/res/values/attrs.xml b/res/values/attrs.xml index 23f639fd2..90f8070a8 100644 --- a/res/values/attrs.xml +++ b/res/values/attrs.xml @@ -33,4 +33,12 @@ <declare-styleable name="SearchEditTextLayout" /> + <declare-styleable name="LinearColorBar"> + <attr name="redColor" format="color" /> + <attr name="greenColor" format="color" /> + <attr name="blueColor" format="color" /> + <attr name="orangeColor" format="color" /> + <attr name="backgroundColor" format="color" /> + </declare-styleable> + </resources> diff --git a/res/values/cm_arrays.xml b/res/values/cm_arrays.xml index 881a4e055..087ec89b7 100644 --- a/res/values/cm_arrays.xml +++ b/res/values/cm_arrays.xml @@ -15,6 +15,23 @@ limitations under the License. --> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <!-- 0: sec + 1: min + 2: min sec + 3: hour + 4: hour sec + 5: hour min + 6: hour min sec --> + + <string-array name="call_stats_duration"> + <item><xliff:g id="seconds" example="2 secs">%3$s</xliff:g></item> + <item><xliff:g id="minutes" example="2 mins">%2$s</xliff:g></item> + <item><xliff:g id="minutes" example="2 mins">%2$s</xliff:g> <xliff:g id="seconds" example="2 secs">%3$s</xliff:g></item> + <item><xliff:g id="hours" example="2 hrs">%1$s</xliff:g></item> + <item><xliff:g id="hours" example="2 hrs">%1$s</xliff:g> <xliff:g id="seconds" example="2 secs">%3$s</xliff:g></item> + <item><xliff:g id="hours" example="2 hrs">%1$s</xliff:g> <xliff:g id="minutes" example="2 mins">%2$s</xliff:g></item> + <item><xliff:g id="hours" example="2 hrs">%1$s</xliff:g> <xliff:g id="minutes" example="2 mins">%2$s</xliff:g> <xliff:g id="seconds" example="2 secs">%3$s</xliff:g></item> + </string-array> <string-array name="forward_lookup_providers" translatable="false"> <item>Google</item> diff --git a/res/values/cm_plurals.xml b/res/values/cm_plurals.xml new file mode 100644 index 000000000..0c4b4d21a --- /dev/null +++ b/res/values/cm_plurals.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2013 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 xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <plurals name="hour"> + <item quantity="one">1 hr</item> + <item quantity="other">%d hrs</item> + </plurals> + <plurals name="minute"> + <item quantity="one">1 min</item> + <item quantity="other">%d mins</item> + </plurals> + <plurals name="second"> + <item quantity="one">1 sec</item> + <item quantity="other">%d secs</item> + </plurals> + + <plurals name="call"> + <item quantity="one">1 call</item> + <item quantity="other">%d calls</item> + </plurals> +</resources> diff --git a/res/values/cm_strings.xml b/res/values/cm_strings.xml index a6e5f5c5c..8cbcf50ca 100644 --- a/res/values/cm_strings.xml +++ b/res/values/cm_strings.xml @@ -68,4 +68,46 @@ <string name="incall_vibrate_hangup_title">Vibrate on hang up</string> <string name="incall_vibrate_45_title">Vibrate every minute</string> <string name="incall_vibrate_45_summary">Vibrates at the 45 second mark of every minute during outgoing calls</string> + + <string name="call_log_show_all_slots">All SIMs</string> + <string name="call_log_all_calls_header">All calls</string> + <string name="call_log_blacklist_header">Blocked calls only</string> + + <!-- Call statistics --> + <string name="call_log_stats_title">Statistics</string> + <string name="callStatsDetailTitle">Call stat details</string> + + <string name="call_stats_incoming">Incoming: <xliff:g id="value">%s</xliff:g></string> + <string name="call_stats_outgoing">Outgoing: <xliff:g id="value">%s</xliff:g></string> + <string name="call_stats_missed">Missed: <xliff:g id="value">%s</xliff:g></string> + <string name="call_stats_blacklisted">Blocked: <xliff:g id="value">%s</xliff:g></string> + <string name="call_stats_percent"><xliff:g id="percent">%d</xliff:g>%%</string> + <string name="call_stats_header_total">Total: <xliff:g id="call_count">%s</xliff:g>, <xliff:g id="duration">%s</xliff:g></string> + <string name="call_stats_header_total_callsonly">Total: <xliff:g id="call_count">%s</xliff:g></string> + <string name="call_stats_filter_from">Start date</string> + <string name="call_stats_filter_to">End date</string> + <string name="call_stats_filter_picker_title">Filter range</string> + + <string name="date_quick_selection">Quick selection</string> + <string name="date_qs_currentmonth">Current month</string> + <string name="date_qs_currentquarter">Current quarter</string> + <string name="date_qs_currentyear">Current year</string> + <string name="date_qs_lastweek">Last week</string> + <string name="date_qs_lastmonth">Last month</string> + <string name="date_qs_lastquarter">Last quarter</string> + <string name="date_qs_lastyear">Last year</string> + + <string name="call_stats_date_filter">Adjust time range</string> + <string name="call_stats_reset_filter">Reset time range</string> + <string name="call_stats_sort_by_duration">Sort by call duration</string> + <string name="call_stats_sort_by_count">Sort by call count</string> + + <string name="call_stats_title_for_number">This number</string> + <string name="call_stats_title_of_total">Of total</string> + <string name="call_stats_title_durations">Call durations</string> + <string name="call_stats_title_count">Call count</string> + <string name="call_stats_title_average_duration">Average call duration</string> + + <!-- Text displayed when there are no call log entries in the selected time range. --> + <string name="recent_calls_no_items_in_range">Your call log does not contain any calls in the selected time range.</string> </resources> diff --git a/res/values/colors.xml b/res/values/colors.xml index 5a481b639..451210404 100644 --- a/res/values/colors.xml +++ b/res/values/colors.xml @@ -111,4 +111,7 @@ <!-- Color for call log blacklist item --> <color name="call_log_action_block_red">#F44336</color> <color name="call_log_action_block_gray">#616161</color> + + <!-- Colors for incoming and outgoing calls in the call statistics --> + <color name="call_stats_bar_background">#88888888</color> </resources> diff --git a/res/values/styles.xml b/res/values/styles.xml index e3a2f9974..816e1ab33 100644 --- a/res/values/styles.xml +++ b/res/values/styles.xml @@ -30,6 +30,7 @@ <item name="android:listViewStyle">@style/ListViewStyle</item> <item name="android:overlapAnchor">true</item> <item name="android:alertDialogTheme">@style/AlertDialogTheme</item> + <item name="android:colorAccent">@color/dialtacts_theme_color</item> <item name="activated_background">@drawable/list_item_activated_background</item> <item name="section_header_background">@drawable/list_title_holo</item> <item name="list_section_header_height">32dip</item> @@ -199,7 +200,6 @@ <!-- Setting description. --> <item name="android:textColorSecondary">@color/settings_text_color_secondary</item> <item name="android:windowBackground">@color/setting_background_color</item> - <item name="android:colorAccent">@color/dialtacts_theme_color</item> <item name="android:textColorLink">@color/dialtacts_theme_color</item> </style> @@ -218,6 +218,16 @@ <item name="cardBackgroundColor">@color/background_dialer_call_log_list_item</item> </style> + <style name="CallLogSpinnerStyle"> + <item name="android:textAppearance">?android:attr/textAppearanceSmall</item> + <item name="android:textStyle">bold</item> + <item name="android:textColor">@android:color/black</item> + <item name="android:gravity">center_vertical</item> + <item name="android:ellipsize">marquee</item> + <item name="android:singleLine">true</item> + <item name="android:textAllCaps">true</item> + </style> + <style name="TextActionStyle"> <item name="android:layout_width">wrap_content</item> <item name="android:layout_height">@dimen/call_log_action_height</item> @@ -246,4 +256,17 @@ <item name="android:background">@drawable/oval_ripple</item> <item name="android:padding">8dp</item> </style> + + <style name="CallStatsBarStyle"> + <item name="android:layout_height">4dp</item> + <item name="android:layout_marginTop">6dp</item> + <item name="android:layout_marginBottom">6dp</item> + <item name="android:orientation">horizontal</item> + <item name="backgroundColor">@color/call_stats_bar_background</item> + <item name="blueColor">@color/answered_incoming_call</item> + <item name="greenColor">@color/answered_outgoing_call</item> + <item name="redColor">@color/missed_call</item> + <item name="orangeColor">@color/blacklisted_call</item> + </style> + </resources> diff --git a/src/com/android/dialer/calllog/CallLogActivity.java b/src/com/android/dialer/calllog/CallLogActivity.java index 97e601630..1d2ea475c 100644 --- a/src/com/android/dialer/calllog/CallLogActivity.java +++ b/src/com/android/dialer/calllog/CallLogActivity.java @@ -39,23 +39,28 @@ import com.android.contacts.common.util.PermissionsUtil; import com.android.contacts.commonbind.analytics.AnalyticsUtil; import com.android.dialer.DialtactsActivity; import com.android.dialer.R; +import com.android.dialer.callstats.CallStatsFragment; import com.android.dialer.util.DialerUtils; import com.android.dialer.voicemail.VoicemailStatusHelper; import com.android.dialer.voicemail.VoicemailStatusHelperImpl; +import com.android.dialer.widget.DoubleDatePickerDialog; -public class CallLogActivity extends Activity implements ViewPager.OnPageChangeListener { +public class CallLogActivity extends Activity implements + ViewPager.OnPageChangeListener, DoubleDatePickerDialog.OnDateSetListener { private ViewPager mViewPager; private ViewPagerTabs mViewPagerTabs; private ViewPagerAdapter mViewPagerAdapter; private CallLogFragment mAllCallsFragment; private CallLogFragment mMissedCallsFragment; + private CallStatsFragment mStatsFragment; private String[] mTabTitles; private static final int TAB_INDEX_ALL = 0; private static final int TAB_INDEX_MISSED = 1; + private static final int TAB_INDEX_STATS = 2; - private static final int TAB_INDEX_COUNT = 2; + private static final int TAB_INDEX_COUNT = 3; private boolean mIsResumed; @@ -76,20 +81,24 @@ public class CallLogActivity extends Activity implements ViewPager.OnPageChangeL return new CallLogFragment(CallLogQueryHandler.CALL_TYPE_ALL); case TAB_INDEX_MISSED: return new CallLogFragment(Calls.MISSED_TYPE); + case TAB_INDEX_STATS: + return new CallStatsFragment(); } throw new IllegalStateException("No fragment at position " + position); } @Override public Object instantiateItem(ViewGroup container, int position) { - final CallLogFragment fragment = - (CallLogFragment) super.instantiateItem(container, position); + final Object fragment = super.instantiateItem(container, position); switch (position) { case TAB_INDEX_ALL: - mAllCallsFragment = fragment; + mAllCallsFragment = (CallLogFragment) fragment; break; case TAB_INDEX_MISSED: - mMissedCallsFragment = fragment; + mMissedCallsFragment = (CallLogFragment) fragment; + break; + case TAB_INDEX_STATS: + mStatsFragment = (CallStatsFragment) fragment; break; } return fragment; @@ -139,6 +148,7 @@ public class CallLogActivity extends Activity implements ViewPager.OnPageChangeL mTabTitles = new String[TAB_INDEX_COUNT]; mTabTitles[0] = getString(R.string.call_log_all_title); mTabTitles[1] = getString(R.string.call_log_missed_title); + mTabTitles[2] = getString(R.string.call_log_stats_title); mViewPager = (ViewPager) findViewById(R.id.call_log_pager); @@ -200,6 +210,15 @@ public class CallLogActivity extends Activity implements ViewPager.OnPageChangeL } @Override + public void onDateSet(long from, long to) { + switch (mViewPager.getCurrentItem()) { + case TAB_INDEX_STATS: + mStatsFragment.onDateSet(from, to); + break; + } + } + + @Override public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { mViewPagerTabs.onPageScrolled(position, positionOffset, positionOffsetPixels); } diff --git a/src/com/android/dialer/calllog/CallLogQueryHandler.java b/src/com/android/dialer/calllog/CallLogQueryHandler.java index aceca62aa..7da9b1dbe 100644 --- a/src/com/android/dialer/calllog/CallLogQueryHandler.java +++ b/src/com/android/dialer/calllog/CallLogQueryHandler.java @@ -68,6 +68,11 @@ public class CallLogQueryHandler extends NoNullCursorAsyncQueryHandler { */ public static final int CALL_TYPE_ALL = -1; + /** + * To specify all slots. + */ + public static final int CALL_SIM_ALL = -1; + private final WeakReference<Listener> mListener; private final Context mContext; diff --git a/src/com/android/dialer/calllog/SpinnerContent.java b/src/com/android/dialer/calllog/SpinnerContent.java new file mode 100644 index 000000000..721e3d208 --- /dev/null +++ b/src/com/android/dialer/calllog/SpinnerContent.java @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2014 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.dialer.calllog; + +import android.content.Context; +import android.provider.CallLog; +import android.telephony.TelephonyManager; +import android.text.TextUtils; +import android.util.Log; +import android.widget.Spinner; + +import com.android.contacts.common.MoreContactUtils; +import com.android.dialer.R; + +import java.util.ArrayList; +import java.util.List; + +/** + * To save the spinner content. + */ +public class SpinnerContent { + private static String TAG = SpinnerContent.class.getSimpleName(); + + public final int value; + public final String label; + + // The index for call type spinner. + private static final int INDEX_CALL_TYPE_ALL = 0; + private static final int INDEX_CALL_TYPE_INCOMING = 1; + private static final int INDEX_CALL_TYPE_OUTGOING = 2; + private static final int INDEX_CALL_TYPE_MISSED = 3; + private static final int INDEX_CALL_TYPE_BLACKLIST = 4; + private static final int INDEX_CALL_TYPE_VOICEMAIL = 5; + + public static void setSpinnerContentValue(Spinner spinner, int value) { + for (int i = 0, count = spinner.getCount(); i < count; i++) { + SpinnerContent sc = (SpinnerContent) spinner.getItemAtPosition(i); + if (sc.value == value) { + spinner.setSelection(i, true); + Log.i(TAG, "Set selection for spinner(" + sc + ") with the value: " + value); + return; + } + } + } + + public SpinnerContent(int value, String label) { + this.value = value; + this.label = label; + } + + @Override + public String toString() { + return label; + } + + /** + * @return the spinner contents for the different sims (all, sim0, sim1 etc) + */ + public static List<SpinnerContent> setupSubFilterContent(Context context) { + TelephonyManager telephonyManager = + (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); + int count = telephonyManager.getPhoneCount(); + // Update the filter sub content. + ArrayList<SpinnerContent> values = new ArrayList<SpinnerContent>(count + 1); + values.add(new SpinnerContent(CallLogQueryHandler.CALL_SIM_ALL, + context.getString(R.string.call_log_show_all_slots))); + for (int i = 0; i < count; i++) { + String subDisplayName = PhoneAccountUtils.getAccountLabel(context, + MoreContactUtils.getAccount(i)); + if (!TextUtils.isEmpty(subDisplayName)) { + values.add(new SpinnerContent(i, subDisplayName)); + } + } + return values; + } + + /** + * @param voicemailAvailable true if voicemail should be included in the return values + * @return the spinner contents for the different call types (incoming, outgoing etc) + */ + public static List<SpinnerContent> setupStatusFilterContent(Context context, + boolean voicemailAvailable) { + // Didn't show the voice mail item if not available. + int statusCount = voicemailAvailable ? 6 : 5; + ArrayList<SpinnerContent> values = new ArrayList<SpinnerContent>(statusCount); + for (int i = 0; i < statusCount; i++) { + int value = CallLogQueryHandler.CALL_TYPE_ALL; + String label = null; + switch (i) { + case INDEX_CALL_TYPE_ALL: + value = CallLogQueryHandler.CALL_TYPE_ALL; + label = context.getString(R.string.call_log_all_calls_header); + break; + case INDEX_CALL_TYPE_INCOMING: + value = CallLog.Calls.INCOMING_TYPE; + label = context.getString(R.string.call_log_incoming_header); + break; + case INDEX_CALL_TYPE_OUTGOING: + value = CallLog.Calls.OUTGOING_TYPE; + label = context.getString(R.string.call_log_outgoing_header); + break; + case INDEX_CALL_TYPE_MISSED: + value = CallLog.Calls.MISSED_TYPE; + label = context.getString(R.string.call_log_missed_header); + break; + case INDEX_CALL_TYPE_BLACKLIST: + value = CallLog.Calls.BLACKLIST_TYPE; + label = context.getString(R.string.call_log_blacklist_header); + break; + case INDEX_CALL_TYPE_VOICEMAIL: + value = CallLog.Calls.VOICEMAIL_TYPE; + label = context.getString(R.string.call_log_voicemail_header); + break; + } + values.add(new SpinnerContent(value, label)); + } + return values; + } +} diff --git a/src/com/android/dialer/callstats/CallStatsAdapter.java b/src/com/android/dialer/callstats/CallStatsAdapter.java new file mode 100644 index 000000000..672fa6f83 --- /dev/null +++ b/src/com/android/dialer/callstats/CallStatsAdapter.java @@ -0,0 +1,270 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * Copyright (C) 2013 Android Open Kang 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.dialer.callstats; + +import android.content.Context; +import android.content.Intent; +import android.support.v7.widget.RecyclerView; +import android.text.TextUtils; +import android.view.ContextMenu; +import android.view.LayoutInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; + +import com.android.contacts.common.CallUtil; +import com.android.contacts.common.ClipboardUtils; +import com.android.contacts.common.GeoUtil; +import com.android.contacts.common.util.PermissionsUtil; +import com.android.dialer.DialtactsActivity; +import com.android.dialer.R; +import com.android.dialer.calllog.CallLogQueryHandler; +import com.android.dialer.calllog.ContactInfo; +import com.android.dialer.calllog.ContactInfoHelper; +import com.android.dialer.contactinfo.ContactInfoCache; +import com.android.dialer.util.DialerUtils; +import com.android.dialer.util.PhoneNumberUtil; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Adapter class to hold and handle call stat entries + */ +class CallStatsAdapter extends RecyclerView.Adapter { + private final Context mContext; + private final ContactInfoHelper mContactInfoHelper; + private final ContactInfoCache mContactInfoCache; + + private ArrayList<CallStatsDetails> mAllItems; + private ArrayList<CallStatsDetails> mShownItems; + private CallStatsDetails mTotalItem; + private Map<ContactInfo, CallStatsDetails> mInfoLookup; + + private int mType = CallLogQueryHandler.CALL_TYPE_ALL; + private long mFilterFrom; + private long mFilterTo; + private boolean mSortByDuration; + + /** + * Listener that is triggered to populate the context menu with actions to perform on the call's + * number, when the call log entry is long pressed. + */ + private final View.OnCreateContextMenuListener mOnCreateContextMenuListener = + new View.OnCreateContextMenuListener() { + @Override + public void onCreateContextMenu(ContextMenu menu, View v, + ContextMenu.ContextMenuInfo menuInfo) { + final CallStatsListItemViewHolder vh = + (CallStatsListItemViewHolder) v.getTag(); + if (TextUtils.isEmpty(vh.details.number)) { + return; + } + + menu.setHeaderTitle(vh.details.number); + + final MenuItem copyItem = menu.add( + ContextMenu.NONE, + R.id.context_menu_copy_to_clipboard, + ContextMenu.NONE, + R.string.copy_text); + + copyItem.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { + @Override + public boolean onMenuItemClick(MenuItem item) { + ClipboardUtils.copyText(CallStatsAdapter.this.mContext, null, + vh.details.number, true); + return true; + } + }); + + // The edit number before call does not show up if any of the conditions apply: + // 1) Number cannot be called + // 2) Number is the voicemail number + // 3) Number is a SIP address + + boolean canPlaceCallsTo = PhoneNumberUtil.canPlaceCallsTo(vh.details.number, + vh.details.numberPresentation); + if (!canPlaceCallsTo || PhoneNumberUtil.isSipNumber(vh.details.number)) { + return; + } + + final MenuItem editItem = menu.add( + ContextMenu.NONE, + R.id.context_menu_edit_before_call, + ContextMenu.NONE, + R.string.recentCalls_editNumberBeforeCall); + + editItem.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { + @Override + public boolean onMenuItemClick(MenuItem item) { + final Intent intent = new Intent(Intent.ACTION_DIAL, + CallUtil.getCallUri(vh.details.number)); + intent.setClass(mContext, DialtactsActivity.class); + DialerUtils.startActivityWithErrorToast(mContext, intent); + return true; + } + }); + } + }; + + protected final ContactInfoCache.OnContactInfoChangedListener mOnContactInfoChangedListener = + new ContactInfoCache.OnContactInfoChangedListener() { + @Override + public void onContactInfoChanged() { + notifyDataSetChanged(); + } + }; + + private final Comparator<CallStatsDetails> mDurationComparator = + new Comparator<CallStatsDetails>() { + @Override + public int compare(CallStatsDetails o1, CallStatsDetails o2) { + Long duration1 = o1.getRequestedDuration(mType); + Long duration2 = o2.getRequestedDuration(mType); + // sort descending + return duration2.compareTo(duration1); + } + }; + private final Comparator<CallStatsDetails> mCountComparator = + new Comparator<CallStatsDetails>() { + @Override + public int compare(CallStatsDetails o1, CallStatsDetails o2) { + Integer count1 = o1.getRequestedCount(mType); + Integer count2 = o2.getRequestedCount(mType); + // sort descending + return count2.compareTo(count1); + } + }; + + CallStatsAdapter(Context context) { + mContext = context; + + final String currentCountryIso = GeoUtil.getCurrentCountryIso(mContext); + mContactInfoHelper = new ContactInfoHelper(mContext, currentCountryIso); + + mAllItems = new ArrayList<CallStatsDetails>(); + mShownItems = new ArrayList<CallStatsDetails>(); + mTotalItem = new CallStatsDetails(null, 0, null, null, null, null, 0); + mInfoLookup = new ConcurrentHashMap<ContactInfo, CallStatsDetails>(); + + mContactInfoCache = new ContactInfoCache( + mContactInfoHelper, mOnContactInfoChangedListener); + if (!PermissionsUtil.hasContactsPermissions(context)) { + mContactInfoCache.disableRequestProcessing(); + } + } + + public void updateData(Map<ContactInfo, CallStatsDetails> calls, long from, long to) { + mInfoLookup.clear(); + mInfoLookup.putAll(calls); + mFilterFrom = from; + mFilterTo = to; + + mAllItems.clear(); + mTotalItem.reset(); + + for (Map.Entry<ContactInfo, CallStatsDetails> entry : calls.entrySet()) { + final CallStatsDetails call = entry.getValue(); + mAllItems.add(call); + mTotalItem.mergeWith(call); + } + } + + public void updateDisplayedData(int type, boolean sortByDuration) { + mType = type; + mSortByDuration = sortByDuration; + + mShownItems.clear(); + + for (CallStatsDetails call : mAllItems) { + if (sortByDuration && call.getRequestedDuration(type) > 0) { + mShownItems.add(call); + } else if (!sortByDuration && call.getRequestedCount(type) > 0) { + mShownItems.add(call); + } + } + + Collections.sort(mShownItems, sortByDuration ? mDurationComparator : mCountComparator); + notifyDataSetChanged(); + } + + public void invalidateCache() { + mContactInfoCache.invalidate(); + } + + public void startCache() { + if (PermissionsUtil.hasPermission(mContext, android.Manifest.permission.READ_CONTACTS)) { + mContactInfoCache.start(); + } + } + + public void pauseCache() { + mContactInfoCache.stop(); + } + + @Override + public int getItemCount() { + return mShownItems.size(); + } + + @Override + public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + LayoutInflater inflater = LayoutInflater.from(mContext); + View view = inflater.inflate(R.layout.call_stats_list_item, parent, false); + CallStatsListItemViewHolder viewHolder = CallStatsListItemViewHolder.create(view); + + viewHolder.mPrimaryActionView.setOnCreateContextMenuListener(mOnCreateContextMenuListener); + viewHolder.mPrimaryActionView.setTag(viewHolder); + + return viewHolder; + } + + @Override + public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) { + CallStatsDetails details = mShownItems.get(position); + CallStatsDetails first = mShownItems.get(0); + CallStatsListItemViewHolder views = (CallStatsListItemViewHolder) viewHolder; + + views.setDetails(details, first, mTotalItem, mType, mSortByDuration); + views.clickIntent = getItemClickIntent(details); + } + + private Intent getItemClickIntent(CallStatsDetails details) { + Intent intent = new Intent(mContext, CallStatsDetailActivity.class); + intent.putExtra(CallStatsDetailActivity.EXTRA_DETAILS, details); + intent.putExtra(CallStatsDetailActivity.EXTRA_TOTAL, mTotalItem); + intent.putExtra(CallStatsDetailActivity.EXTRA_FROM, mFilterFrom); + intent.putExtra(CallStatsDetailActivity.EXTRA_TO, mFilterTo); + return intent; + } + + public String getTotalCallCountString() { + return CallStatsListItemViewHolder.getCallCountString( + mContext, mTotalItem.getRequestedCount(mType)); + } + + public String getFullDurationString(boolean withSeconds) { + final long duration = mTotalItem.getRequestedDuration(mType); + return CallStatsListItemViewHolder.getDurationString( + mContext, duration, withSeconds); + } +} diff --git a/src/com/android/dialer/callstats/CallStatsDetailActivity.java b/src/com/android/dialer/callstats/CallStatsDetailActivity.java new file mode 100644 index 000000000..3f4d6c98a --- /dev/null +++ b/src/com/android/dialer/callstats/CallStatsDetailActivity.java @@ -0,0 +1,380 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * Copyright (C) 2013 Android Open Kang 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.dialer.callstats; + +import android.app.ActionBar; +import android.app.Activity; +import android.content.Intent; +import android.content.res.Resources; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Bundle; +import android.provider.CallLog.Calls; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.text.TextUtils; +import android.text.format.DateUtils; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.QuickContactBadge; +import android.widget.TextView; + +import com.android.contacts.common.CallUtil; +import com.android.contacts.common.ContactPhotoManager; +import com.android.contacts.common.GeoUtil; +import com.android.contacts.common.util.UriUtils; +import com.android.dialer.R; +import com.android.dialer.calllog.CallTypeIconsView; +import com.android.dialer.calllog.ContactInfo; +import com.android.dialer.calllog.ContactInfoHelper; +import com.android.dialer.util.IntentUtil; +import com.android.dialer.util.PhoneNumberUtil; +import com.android.dialer.widget.LinearColorBar; + +/** + * Activity to display detailed information about a callstat item + */ +public class CallStatsDetailActivity extends Activity { + private static final String TAG = "CallStatsDetailActivity"; + + public static final String EXTRA_DETAILS = "details"; + public static final String EXTRA_TOTAL = "total"; + public static final String EXTRA_FROM = "from"; + public static final String EXTRA_TO = "to"; + + private ContactInfoHelper mContactInfoHelper; + private Resources mResources; + + private QuickContactBadge mQuickContactBadge; + private TextView mCallerName; + private TextView mCallerNumber; + private View mCallButton; + + private TextView mTotalDuration, mTotalCount; + private TextView mTotalTotalDuration, mTotalTotalCount; + + private DetailLine mInDuration, mOutDuration; + private DetailLine mInCount, mOutCount; + private DetailLine mMissedCount, mBlacklistedCount; + private DetailLine mInAverage, mOutAverage; + + private LinearColorBar mDurationBar, mCountBar; + private LinearColorBar mTotalDurationBar, mTotalCountBar; + + private CallStatsDetails mData; + private CallStatsDetails mTotalData; + private String mNumber = null; + private boolean mHasEditNumberBeforeCallOption; + + private class UpdateContactTask extends AsyncTask<String, Void, ContactInfo> { + @Override + protected ContactInfo doInBackground(String... strings) { + ContactInfo info = mContactInfoHelper.lookupNumber(strings[0], strings[1]); + return info; + } + + @Override + protected void onPostExecute(ContactInfo info) { + mData.updateFromInfo(info); + updateData(); + } + } + + @Override + protected void onCreate(Bundle icicle) { + super.onCreate(icicle); + + setContentView(R.layout.call_stats_detail); + + mResources = getResources(); + mContactInfoHelper = new ContactInfoHelper(this, GeoUtil.getCurrentCountryIso(this)); + + mQuickContactBadge = (QuickContactBadge) findViewById(R.id.quick_contact_photo); + mQuickContactBadge.setOverlay(null); + mQuickContactBadge.setPrioritizedMimeType(Phone.CONTENT_ITEM_TYPE); + mCallerName = (TextView) findViewById(R.id.caller_name); + mCallerNumber = (TextView) findViewById(R.id.caller_number); + + mCallButton = (View) findViewById(R.id.call_back_button); + mCallButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + startActivity(IntentUtil.getCallIntent(mNumber)); + } + }); + + mDurationBar = (LinearColorBar) findViewById(R.id.duration_number_percent_bar); + mTotalDurationBar = (LinearColorBar) findViewById(R.id.duration_total_percent_bar); + mTotalDuration = (TextView) findViewById(R.id.total_duration_number); + mTotalTotalDuration = (TextView) findViewById(R.id.total_duration_total); + mInDuration = new DetailLine(R.id.in_duration, + R.string.call_stats_incoming, Calls.INCOMING_TYPE); + mOutDuration = new DetailLine(R.id.out_duration, + R.string.call_stats_outgoing, Calls.OUTGOING_TYPE); + + mCountBar = (LinearColorBar) findViewById(R.id.count_number_percent_bar); + mTotalCountBar = (LinearColorBar) findViewById(R.id.count_total_percent_bar); + mTotalCount = (TextView) findViewById(R.id.total_count_number); + mTotalTotalCount = (TextView) findViewById(R.id.total_count_total); + mInCount = new DetailLine(R.id.in_count, + R.string.call_stats_incoming, Calls.INCOMING_TYPE); + mOutCount = new DetailLine(R.id.out_count, + R.string.call_stats_outgoing, Calls.OUTGOING_TYPE); + mMissedCount = new DetailLine(R.id.missed_count, + R.string.call_stats_missed, Calls.MISSED_TYPE); + mBlacklistedCount = new DetailLine(R.id.blacklisted_count, + R.string.call_stats_blacklisted, Calls.BLACKLIST_TYPE); + + mInAverage = new DetailLine(R.id.in_average, + R.string.call_stats_incoming, Calls.INCOMING_TYPE); + mOutAverage = new DetailLine(R.id.out_average, + R.string.call_stats_outgoing, Calls.OUTGOING_TYPE); + + ActionBar actionBar = getActionBar(); + if (actionBar != null) { + actionBar.setDisplayHomeAsUpEnabled(true); + } + + Intent launchIntent = getIntent(); + mData = (CallStatsDetails) launchIntent.getParcelableExtra(EXTRA_DETAILS); + mTotalData = (CallStatsDetails) launchIntent.getParcelableExtra(EXTRA_TOTAL); + updateData(); + + TextView dateFilterView = (TextView) findViewById(R.id.date_filter); + long filterFrom = launchIntent.getLongExtra(EXTRA_FROM, -1); + if (filterFrom == -1) { + dateFilterView.setVisibility(View.GONE); + } else { + long filterTo = launchIntent.getLongExtra(EXTRA_TO, -1); + dateFilterView.setText(DateUtils.formatDateRange( + this, filterFrom, filterTo, 0)); + } + } + + @Override + public void onResume() { + super.onResume(); + new UpdateContactTask().execute(mData.number.toString(), mData.countryIso); + } + + private void updateData() { + mNumber = mData.number.toString(); + + // Cache the details about the phone number. + final boolean canPlaceCallsTo = + PhoneNumberUtil.canPlaceCallsTo(mNumber, mData.numberPresentation); + final CharSequence callLocationOrType = !TextUtils.isEmpty(mData.name) + ? Phone.getTypeLabel(mResources, mData.numberType, mData.numberLabel) + : mData.geocode; + + mData.updateDisplayPropertiesIfNeeded(this); + + final boolean isSipNumber = PhoneNumberUtil.isSipNumber(mNumber); + mHasEditNumberBeforeCallOption = + canPlaceCallsTo && !isSipNumber && !mData.isVoicemailNumber; + + if (!TextUtils.isEmpty(mData.name)) { + mCallerName.setText(mData.name); + mCallerNumber.setText(callLocationOrType + " " + mData.displayNumber); + } else { + mCallerName.setText(mData.displayNumber); + if (!TextUtils.isEmpty(callLocationOrType)) { + mCallerNumber.setText(callLocationOrType); + mCallerNumber.setVisibility(View.VISIBLE); + } else { + mCallerNumber.setVisibility(View.GONE); + } + } + + mCallButton.setVisibility(canPlaceCallsTo ? View.VISIBLE : View.GONE); + + String lookupKey = mData.contactUri == null + ? null : UriUtils.getLookupKeyFromUri(mData.contactUri); + final boolean isBusiness = mContactInfoHelper.isBusiness(mData.sourceType); + final int contactType = + mData.isVoicemailNumber ? ContactPhotoManager.TYPE_VOICEMAIL : + isBusiness ? ContactPhotoManager.TYPE_BUSINESS : + ContactPhotoManager.TYPE_DEFAULT; + final String nameForDefaultImage = TextUtils.isEmpty(mData.name) + ? mData.displayNumber : mData.name; + + loadContactPhoto( + mData.contactUri, mData.photoUri, nameForDefaultImage, lookupKey, contactType); + + invalidateOptionsMenu(); + + long totalDuration = mData.getFullDuration(); + mInDuration.updateFromDurations(mData.inDuration, totalDuration); + mOutDuration.updateFromDurations(mData.outDuration, totalDuration); + if (totalDuration != 0) { + mTotalDuration.setText(CallStatsListItemViewHolder.getDurationString(this, + totalDuration, true)); + mTotalTotalDuration.setText(CallStatsListItemViewHolder.getDurationString(this, + mTotalData.getFullDuration(), true)); + updateBar(mDurationBar, mData.inDuration, mData.outDuration, 0, 0, totalDuration); + updateBar(mTotalDurationBar, mData.inDuration, mData.outDuration, + 0, 0, mTotalData.getFullDuration()); + findViewById(R.id.duration_container).setVisibility(View.VISIBLE); + } else { + findViewById(R.id.duration_container).setVisibility(View.GONE); + } + + mInAverage.updateAsAverage(mData.inDuration, mData.incomingCount); + mOutAverage.updateAsAverage(mData.outDuration, mData.outgoingCount); + + int totalCount = mData.getTotalCount(); + mTotalCount.setText(CallStatsListItemViewHolder.getCallCountString(this, totalCount)); + mTotalTotalCount.setText( + CallStatsListItemViewHolder.getCallCountString(this, mTotalData.getTotalCount())); + mInCount.updateFromCounts(mData.incomingCount, totalCount); + mOutCount.updateFromCounts(mData.outgoingCount, totalCount); + mMissedCount.updateFromCounts(mData.missedCount, totalCount); + mBlacklistedCount.updateFromCounts(mData.blacklistCount, totalCount); + updateBar(mCountBar, mData.incomingCount, mData.outgoingCount, + mData.missedCount, mData.blacklistCount, totalCount); + updateBar(mTotalCountBar, mData.incomingCount, mData.outgoingCount, + mData.missedCount, mData.blacklistCount, mTotalData.getTotalCount()); + } + + private void loadContactPhoto(Uri contactUri, Uri photoUri, String displayName, + String lookupKey, int contactType) { + + final ContactPhotoManager.DefaultImageRequest request = + new ContactPhotoManager.DefaultImageRequest(displayName, lookupKey, + contactType, true /* isCircular */); + + mQuickContactBadge.assignContactUri(contactUri); + mQuickContactBadge.setContentDescription( + mResources.getString(R.string.description_contact_details, displayName)); + + ContactPhotoManager.getInstance(this).loadDirectoryPhoto(mQuickContactBadge, photoUri, + false /* darkTheme */, true /* isCircular */, request); + } + + private void updateBar(LinearColorBar bar, + long value1, long value2, long value3, long value4, long total) { + bar.setRatios((float) value1 / total, (float) value2 / total, + (float) value3 / total, (float) value4 / total); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.call_stats_details_options, menu); + return super.onCreateOptionsMenu(menu); + } + + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + menu.findItem(R.id.menu_edit_number_before_call).setVisible( + mHasEditNumberBeforeCallOption); + return super.onPrepareOptionsMenu(menu); + } + + @Override + public boolean onMenuItemSelected(int featureId, MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: { + onHomeSelected(); + return true; + } + // All the options menu items are handled by onMenu... methods. + default: + throw new IllegalArgumentException(); + } + } + + public void onMenuEditNumberBeforeCall(MenuItem menuItem) { + startActivity(new Intent(Intent.ACTION_DIAL, CallUtil.getCallUri(mNumber))); + } + + public void onMenuAddToBlacklist(MenuItem menuItem) { + mContactInfoHelper.addNumberToBlacklist(mNumber); + } + + private void onHomeSelected() { + Intent intent = new Intent(Intent.ACTION_VIEW, Calls.CONTENT_URI); + // This will open the call log even if the detail view has been opened directly. + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + startActivity(intent); + finish(); + } + + private class DetailLine { + private int mValueTemplateResId; + private View mRootView; + private TextView mTextView; + private TextView mPercentView; + + public DetailLine(int rootViewId, int valueTemplateResId, int iconType) { + mValueTemplateResId = valueTemplateResId; + mRootView = findViewById(rootViewId); + mTextView = (TextView) mRootView.findViewById(R.id.value); + mPercentView = (TextView) mRootView.findViewById(R.id.percent); + + CallTypeIconsView icon = (CallTypeIconsView) mRootView.findViewById(R.id.icon); + icon.add(iconType); + } + + public void updateFromCounts(int count, int totalCount) { + if (count == 0 && totalCount > 0) { + mRootView.setVisibility(View.GONE); + return; + } + + mRootView.setVisibility(View.VISIBLE); + String value = CallStatsListItemViewHolder.getCallCountString( + mTextView.getContext(), count); + mTextView.setText(getString(mValueTemplateResId, value)); + updatePercent(count, totalCount); + } + + public void updateFromDurations(long duration, long totalDuration) { + if (duration == 0 && totalDuration >= 0) { + mRootView.setVisibility(View.GONE); + return; + } + + mRootView.setVisibility(View.VISIBLE); + String value = CallStatsListItemViewHolder.getDurationString( + mTextView.getContext(), duration, true); + mTextView.setText(getString(mValueTemplateResId, value)); + updatePercent(duration, totalDuration); + } + + public void updateAsAverage(long duration, int count) { + if (count == 0) { + mRootView.setVisibility(View.GONE); + return; + } + + mRootView.setVisibility(View.VISIBLE); + mPercentView.setVisibility(View.GONE); + + long averageDuration = (long) Math.round( + (float) duration / (float) count); + String value = CallStatsListItemViewHolder.getDurationString( + mTextView.getContext(), averageDuration, true); + mTextView.setText(getString(mValueTemplateResId, value)); + } + + private void updatePercent(long value, long total) { + int percent = (int) Math.round(100F * value / total); + mPercentView.setText(getString(R.string.call_stats_percent, percent)); + } + } +} diff --git a/src/com/android/dialer/callstats/CallStatsDetails.java b/src/com/android/dialer/callstats/CallStatsDetails.java new file mode 100644 index 000000000..c07209fb5 --- /dev/null +++ b/src/com/android/dialer/callstats/CallStatsDetails.java @@ -0,0 +1,244 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * Copyright (C) 2013 Android Open Kang 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.dialer.callstats; + +import android.content.Context; +import android.net.Uri; +import android.os.Parcel; +import android.os.Parcelable; +import android.provider.CallLog.Calls; +import android.telecom.PhoneAccountHandle; +import android.text.BidiFormatter; +import android.text.TextDirectionHeuristics; + +import com.android.dialer.calllog.ContactInfo; +import com.android.dialer.calllog.PhoneNumberDisplayUtil; +import com.android.dialer.util.PhoneNumberUtil; + +/** + * Class to store statistical details for a given contact/number. + */ +public class CallStatsDetails implements Parcelable { + public final String number; + public final int numberPresentation; + public String formattedNumber; + public final String countryIso; + public final String geocode; + public final long date; + public String name; + public int numberType; + public String numberLabel; + public Uri contactUri; + public Uri photoUri; + public long photoId; + public long inDuration; + public long outDuration; + public int incomingCount; + public int outgoingCount; + public int missedCount; + public int blacklistCount; + public PhoneAccountHandle accountHandle; + public int sourceType = 0; + + public boolean isVoicemailNumber; + public String displayNumber; + + public CallStatsDetails(CharSequence number, int numberPresentation, + PhoneAccountHandle accountHandle, ContactInfo info, + String countryIso, String geocode, long date) { + this.number = number != null ? number.toString() : null; + this.numberPresentation = numberPresentation; + this.countryIso = countryIso; + this.geocode = geocode; + this.date = date; + + reset(); + + if (info != null) { + updateFromInfo(info); + } + } + + public void updateFromInfo(ContactInfo info) { + this.name = info.name; + this.numberType = info.type; + this.numberLabel = info.label; + this.photoId = info.photoId; + this.photoUri = info.photoUri; + this.formattedNumber = info.formattedNumber; + this.contactUri = info.lookupUri; + this.photoUri = info.photoUri; + this.photoId = info.photoId; + this.sourceType = info.sourceType; + this.displayNumber = null; + } + + public void updateDisplayPropertiesIfNeeded(Context context) { + if (displayNumber != null) { + return; + } + isVoicemailNumber = + PhoneNumberUtil.isVoicemailNumber(context, accountHandle, number); + final CharSequence displayNumber = PhoneNumberDisplayUtil.getDisplayNumber(context, + number, numberPresentation, formattedNumber, isVoicemailNumber); + this.displayNumber = BidiFormatter.getInstance().unicodeWrap( + displayNumber.toString(), TextDirectionHeuristics.LTR); + } + + public long getFullDuration() { + return inDuration + outDuration; + } + + public int getTotalCount() { + return incomingCount + outgoingCount + missedCount + blacklistCount; + } + + public void addTimeOrMissed(int type, long time) { + switch (type) { + case Calls.INCOMING_TYPE: + incomingCount++; + inDuration += time; + break; + case Calls.OUTGOING_TYPE: + outgoingCount++; + outDuration += time; + break; + case Calls.MISSED_TYPE: + missedCount++; + break; + case Calls.BLACKLIST_TYPE: + blacklistCount++; + break; + } + } + + public int getDurationPercentage(int type) { + long duration = getRequestedDuration(type); + return Math.round((float) duration * 100F / getFullDuration()); + } + + public int getCountPercentage(int type) { + int count = getRequestedCount(type); + return Math.round((float) count * 100F / getTotalCount()); + } + + public long getRequestedDuration(int type) { + switch (type) { + case Calls.INCOMING_TYPE: + return inDuration; + case Calls.OUTGOING_TYPE: + return outDuration; + case Calls.MISSED_TYPE: + return (long) missedCount; + case Calls.BLACKLIST_TYPE: + return (long) blacklistCount; + default: + return getFullDuration(); + } + } + + public int getRequestedCount(int type) { + switch (type) { + case Calls.INCOMING_TYPE: + return incomingCount; + case Calls.OUTGOING_TYPE: + return outgoingCount; + case Calls.MISSED_TYPE: + return missedCount; + case Calls.BLACKLIST_TYPE: + return blacklistCount; + default: + return getTotalCount(); + } + } + + public void mergeWith(CallStatsDetails other) { + this.inDuration += other.inDuration; + this.outDuration += other.outDuration; + this.incomingCount += other.incomingCount; + this.outgoingCount += other.outgoingCount; + this.missedCount += other.missedCount; + this.blacklistCount += other.blacklistCount; + } + + public void reset() { + this.inDuration = this.outDuration = 0; + this.incomingCount = this.outgoingCount = this.missedCount = this.blacklistCount = 0; + } + + /* Parcelable interface */ + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeString(number); + out.writeInt(numberPresentation); + out.writeString(formattedNumber); + out.writeString(countryIso); + out.writeString(geocode); + out.writeLong(date); + out.writeString(name); + out.writeInt(numberType); + out.writeString(numberLabel); + out.writeParcelable(contactUri, flags); + out.writeParcelable(photoUri, flags); + out.writeLong(photoId); + out.writeLong(inDuration); + out.writeLong(outDuration); + out.writeInt(incomingCount); + out.writeInt(outgoingCount); + out.writeInt(missedCount); + out.writeInt(blacklistCount); + } + + public static final Parcelable.Creator<CallStatsDetails> CREATOR = + new Parcelable.Creator<CallStatsDetails>() { + public CallStatsDetails createFromParcel(Parcel in) { + return new CallStatsDetails(in); + } + + public CallStatsDetails[] newArray(int size) { + return new CallStatsDetails[size]; + } + }; + + private CallStatsDetails (Parcel in) { + number = in.readString(); + numberPresentation = in.readInt(); + formattedNumber = in.readString(); + countryIso = in.readString(); + geocode = in.readString(); + date = in.readLong(); + name = in.readString(); + numberType = in.readInt(); + numberLabel = in.readString(); + contactUri = in.readParcelable(null); + photoUri = in.readParcelable(null); + photoId = in.readLong(); + inDuration = in.readLong(); + outDuration = in.readLong(); + incomingCount = in.readInt(); + outgoingCount = in.readInt(); + missedCount = in.readInt(); + blacklistCount = in.readInt(); + } +} diff --git a/src/com/android/dialer/callstats/CallStatsFragment.java b/src/com/android/dialer/callstats/CallStatsFragment.java new file mode 100644 index 000000000..ede2543c4 --- /dev/null +++ b/src/com/android/dialer/callstats/CallStatsFragment.java @@ -0,0 +1,387 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * Copyright (C) 2013 Android Open Kang 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.dialer.callstats; + +import android.app.Fragment; +import android.content.ContentResolver; +import android.content.Context; +import android.content.pm.PackageManager; +import android.database.ContentObserver; +import android.os.Bundle; +import android.os.Handler; +import android.provider.CallLog; +import android.provider.ContactsContract; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.LinearLayoutManager; +import android.telephony.TelephonyManager; +import android.text.format.DateUtils; +import android.util.Log; +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 android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.Spinner; +import android.widget.TextView; + +import com.android.contacts.common.util.PermissionsUtil; +import com.android.dialer.R; +import com.android.dialer.calllog.CallLogQueryHandler; +import com.android.dialer.calllog.ContactInfo; +import com.android.dialer.calllog.SpinnerContent; +import com.android.dialer.widget.DoubleDatePickerDialog; +import com.android.dialer.widget.EmptyContentView; + +import java.util.Map; + +import static android.Manifest.permission.READ_CALL_LOG; + +public class CallStatsFragment extends Fragment implements + CallStatsQueryHandler.Listener, AdapterView.OnItemSelectedListener, + EmptyContentView.OnEmptyViewActionButtonClickedListener, + DoubleDatePickerDialog.OnDateSetListener { + private static final String TAG = "CallStatsFragment"; + + private static final int READ_CALL_LOG_PERMISSION_REQUEST_CODE = 1; + + private int mCallTypeFilter = CallLogQueryHandler.CALL_TYPE_ALL; + private long mFilterFrom = -1; + private long mFilterTo = -1; + private boolean mSortByDuration = true; + private boolean mDataLoaded = false; + + private RecyclerView mRecyclerView; + private EmptyContentView mEmptyListView; + private LinearLayoutManager mLayoutManager; + private CallStatsAdapter mAdapter; + private CallStatsQueryHandler mCallStatsQueryHandler; + + // Add and change for filter call stats. + private Spinner mFilterSubSpinnerView; + private Spinner mFilterStatusSpinnerView; + + // Default to all slots. + private int mCallSubFilter = CallStatsQueryHandler.CALL_SIM_ALL; + + private TextView mSumHeaderView; + private TextView mDateFilterView; + + private boolean mHasReadCallLogPermission = false; + + private boolean mRefreshDataRequired = true; + private final ContentObserver mObserver = new ContentObserver(new Handler()) { + @Override + public void onChange(boolean selfChange) { + mRefreshDataRequired = true; + } + }; + + @Override + public void onCreate(Bundle state) { + super.onCreate(state); + + final ContentResolver cr = getActivity().getContentResolver(); + mCallStatsQueryHandler = new CallStatsQueryHandler(cr, this); + cr.registerContentObserver(CallLog.CONTENT_URI, true, mObserver); + cr.registerContentObserver(ContactsContract.Contacts.CONTENT_URI, true, mObserver); + + setHasOptionsMenu(true); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) { + View view = inflater.inflate(R.layout.call_stats_fragment, container, false); + + mRecyclerView = (RecyclerView) view.findViewById(R.id.recycler_view); + mRecyclerView.setHasFixedSize(true); + mLayoutManager = new LinearLayoutManager(getActivity()); + mRecyclerView.setLayoutManager(mLayoutManager); + mEmptyListView = (EmptyContentView) view.findViewById(R.id.empty_list_view); + mEmptyListView.setImage(R.drawable.empty_call_log); + mEmptyListView.setActionClickedListener(this); + + mSumHeaderView = (TextView) view.findViewById(R.id.sum_header); + mDateFilterView = (TextView) view.findViewById(R.id.date_filter); + mFilterSubSpinnerView = (Spinner) view.findViewById(R.id.filter_sub_spinner); + mFilterStatusSpinnerView = (Spinner) view.findViewById(R.id.filter_status_spinner); + + // Update the filter views. + updateFilterSpinnerViews(); + + return view; + } + + /** + * Initialize the filter views content. + */ + private void updateFilterSpinnerViews() { + if (mFilterSubSpinnerView == null + || mFilterStatusSpinnerView == null) { + Log.w(TAG, "The filter spinner view is null!"); + return; + } + + final TelephonyManager telephony = (TelephonyManager) getActivity().getSystemService( + Context.TELEPHONY_SERVICE); + if (!telephony.isMultiSimEnabled()) { + mFilterSubSpinnerView.setVisibility(View.GONE); + } else { + // Update the sub filter's content. + ArrayAdapter<SpinnerContent> filterSubAdapter = new ArrayAdapter<SpinnerContent>( + this.getActivity(), R.layout.call_log_spinner_item, + SpinnerContent.setupSubFilterContent(getActivity())); + + if (filterSubAdapter.getCount() <= 1) { + mFilterSubSpinnerView.setVisibility(View.GONE); + } else { + mFilterSubSpinnerView.setAdapter(filterSubAdapter); + mFilterSubSpinnerView.setOnItemSelectedListener(this); + SpinnerContent.setSpinnerContentValue(mFilterSubSpinnerView, mCallSubFilter); + } + } + + + // Update the status filter's content. + ArrayAdapter<SpinnerContent> filterStatusAdapter = new ArrayAdapter<SpinnerContent>( + this.getActivity(), R.layout.call_log_spinner_item, + SpinnerContent.setupStatusFilterContent(getActivity(), false)); + mFilterStatusSpinnerView.setAdapter(filterStatusAdapter); + mFilterStatusSpinnerView.setOnItemSelectedListener(this); + SpinnerContent.setSpinnerContentValue(mFilterStatusSpinnerView, mCallTypeFilter); + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + mAdapter = new CallStatsAdapter(getActivity()); + mRecyclerView.setAdapter(mAdapter); + updateEmptyVisibilityAndMessage(); + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + if (getUserVisibleHint() && PermissionsUtil.hasPermission(getActivity(), READ_CALL_LOG)) { + inflater.inflate(R.menu.call_stats_options, menu); + + final MenuItem resetItem = menu.findItem(R.id.reset_date_filter); + final MenuItem sortDurationItem = menu.findItem(R.id.sort_by_duration); + final MenuItem sortCountItem = menu.findItem(R.id.sort_by_count); + + resetItem.setVisible(mFilterFrom != -1); + sortDurationItem.setVisible(!mSortByDuration); + sortCountItem.setVisible(mSortByDuration); + } + + super.onCreateOptionsMenu(menu, inflater); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + final int itemId = item.getItemId(); + switch (itemId) { + case R.id.date_filter: { + final DoubleDatePickerDialog.Fragment fragment = + new DoubleDatePickerDialog.Fragment(); + fragment.setArguments(DoubleDatePickerDialog.Fragment.createArguments( + mFilterFrom, mFilterTo)); + fragment.show(getFragmentManager(), "filter"); + break; + } + case R.id.reset_date_filter: { + mFilterFrom = -1; + mFilterTo = -1; + fetchCalls(); + updateEmptyVisibilityAndMessage(); + getActivity().invalidateOptionsMenu(); + break; + } + case R.id.sort_by_duration: + case R.id.sort_by_count: { + mSortByDuration = itemId == R.id.sort_by_duration; + mAdapter.updateDisplayedData(mCallTypeFilter, mSortByDuration); + getActivity().invalidateOptionsMenu(); + break; + } + } + return true; + } + + @Override + public void onItemSelected(AdapterView<?> parent, View view, int pos, long id) { + if (parent == mFilterSubSpinnerView) { + mCallSubFilter = pos - 1; + mCallStatsQueryHandler.fetchCalls(mFilterFrom, mFilterTo, mCallSubFilter); + } else { + mCallTypeFilter = ((SpinnerContent)parent.getItemAtPosition(pos)).value; + mAdapter.updateDisplayedData(mCallTypeFilter, mSortByDuration); + if (mDataLoaded) { + updateHeader(); + updateEmptyVisibilityAndMessage(); + } + } + } + + @Override + public void onNothingSelected(AdapterView<?> parent) { + } + + @Override + public void onEmptyViewActionButtonClicked() { + if (!PermissionsUtil.hasPermission(getActivity(), READ_CALL_LOG)) { + requestPermissions(new String[] { READ_CALL_LOG }, + READ_CALL_LOG_PERMISSION_REQUEST_CODE); + } + } + + @Override + public void onRequestPermissionsResult(int requestCode, String[] permissions, + int[] grantResults) { + if (requestCode == READ_CALL_LOG_PERMISSION_REQUEST_CODE) { + if (grantResults.length >= 1 && PackageManager.PERMISSION_GRANTED == grantResults[0]) { + // Force a refresh of the data since we were missing the permission before this. + mRefreshDataRequired = true; + getActivity().invalidateOptionsMenu(); + } + } + } + + @Override + public void onDateSet(long from, long to) { + mFilterFrom = from; + mFilterTo = to; + getActivity().invalidateOptionsMenu(); + fetchCalls(); + updateEmptyVisibilityAndMessage(); + } + + /** + * Called by the CallStatsQueryHandler when the list of calls has been + * fetched or updated. + */ + @Override + public void onCallsFetched(Map<ContactInfo, CallStatsDetails> calls) { + if (getActivity() == null || getActivity().isFinishing()) { + return; + } + + mDataLoaded = true; + mAdapter.updateData(calls, mFilterFrom, mFilterTo); + mAdapter.updateDisplayedData(mCallTypeFilter, mSortByDuration); + updateHeader(); + updateEmptyVisibilityAndMessage(); + } + + @Override + public void onResume() { + super.onResume(); + final boolean hasReadCallLogPermission = + PermissionsUtil.hasPermission(getActivity(), READ_CALL_LOG); + if (!mHasReadCallLogPermission && hasReadCallLogPermission) { + // We didn't have the permission before, and now we do. Force a refresh of the call log. + // Note that this code path always happens on a fresh start, but mRefreshDataRequired + // is already true in that case anyway. + mRefreshDataRequired = true; + mDataLoaded = false; + updateEmptyVisibilityAndMessage(); + getActivity().invalidateOptionsMenu(); + } + mHasReadCallLogPermission = hasReadCallLogPermission; + refreshData(); + mAdapter.startCache(); + } + + @Override + public void onPause() { + super.onPause(); + mAdapter.pauseCache(); + } + + @Override + public void onDestroy() { + super.onDestroy(); + mAdapter.pauseCache(); + getActivity().getContentResolver().unregisterContentObserver(mObserver); + } + + private void fetchCalls() { + mCallStatsQueryHandler.fetchCalls(mFilterFrom, mFilterTo, mCallSubFilter); + } + + private void updateHeader() { + final String callCount = mAdapter.getTotalCallCountString(); + final String duration = mAdapter.getFullDurationString(false); + + if (duration != null) { + mSumHeaderView.setText(getString(R.string.call_stats_header_total, callCount, duration)); + } else { + mSumHeaderView.setText(getString(R.string.call_stats_header_total_callsonly, callCount)); + } + mSumHeaderView.setVisibility(isListEmpty() ? View.GONE : View.VISIBLE); + + if (mFilterFrom == -1) { + mDateFilterView.setVisibility(View.GONE); + } else { + mDateFilterView.setText(DateUtils.formatDateRange(getActivity(), + mFilterFrom, mFilterTo, 0)); + mDateFilterView.setVisibility(View.VISIBLE); + } + } + + /** Requests updates to the data to be shown. */ + private void refreshData() { + // Prevent unnecessary refresh. + if (mRefreshDataRequired) { + // Mark all entries in the contact info cache as out of date, so + // they will be looked up again once being shown. + mAdapter.invalidateCache(); + fetchCalls(); + mRefreshDataRequired = false; + } + } + + private boolean isListEmpty() { + return mDataLoaded && mAdapter.getItemCount() == 0; + } + + private void updateEmptyVisibilityAndMessage() { + final Context context = getActivity(); + if (context == null) { + return; + } + + boolean showListView = !isListEmpty(); + + if (!PermissionsUtil.hasPermission(context, READ_CALL_LOG)) { + mEmptyListView.setDescription(R.string.permission_no_calllog); + mEmptyListView.setActionLabel(R.string.permission_single_turn_on); + showListView = false; + } else if (mFilterFrom > 0 || mFilterTo > 0) { + mEmptyListView.setDescription(R.string.recent_calls_no_items_in_range); + mEmptyListView.setActionLabel(EmptyContentView.NO_LABEL); + } else { + mEmptyListView.setDescription(R.string.recentCalls_empty); + mEmptyListView.setActionLabel(EmptyContentView.NO_LABEL); + } + + mRecyclerView.setVisibility(showListView ? View.VISIBLE : View.GONE); + mEmptyListView.setVisibility(!showListView ? View.VISIBLE : View.GONE); + } +} diff --git a/src/com/android/dialer/callstats/CallStatsListItemViewHolder.java b/src/com/android/dialer/callstats/CallStatsListItemViewHolder.java new file mode 100644 index 000000000..97d5084f0 --- /dev/null +++ b/src/com/android/dialer/callstats/CallStatsListItemViewHolder.java @@ -0,0 +1,264 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.callstats; + +import android.content.Context; +import android.content.res.Resources; +import android.content.Intent; +import android.net.Uri; +import android.provider.CallLog.Calls; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.support.v7.widget.RecyclerView; +import android.text.TextUtils; +import android.view.View; +import android.widget.QuickContactBadge; +import android.widget.TextView; + +import com.android.contacts.common.ContactPhotoManager; +import com.android.contacts.common.GeoUtil; +import com.android.contacts.common.util.PhoneNumberHelper; +import com.android.contacts.common.util.UriUtils; +import com.android.dialer.R; +import com.android.dialer.calllog.CallLogQueryHandler; +import com.android.dialer.calllog.ContactInfoHelper; +import com.android.dialer.util.DialerUtils; +import com.android.dialer.widget.LinearColorBar; + +/** + * This is an object containing references to views contained by the call log list item. This + * improves performance by reducing the frequency with which we need to find views by IDs. + * + * This object also contains UI logic pertaining to the view, to isolate it from the CallLogAdapter. + */ +public final class CallStatsListItemViewHolder extends RecyclerView.ViewHolder + implements View.OnClickListener { + + public CallStatsDetails details; + public Intent clickIntent; + + public final View mRootView; + public final QuickContactBadge mQuickContactView; + public final View mPrimaryActionView; + public final TextView mNameView; + public final TextView mNumberView; + public final TextView mLabelView; + public final TextView mPercentView; + public final LinearColorBar mBarView; + + private Context mContext; + private ContactInfoHelper mContactInfoHelper; + private final int mPhotoSize; + + private CallStatsListItemViewHolder(View rootView, + QuickContactBadge quickContactView, + View primaryActionView, + TextView nameView, + TextView numberView, + TextView labelView, + TextView percentView, + LinearColorBar barView) { + super(rootView); + + mRootView = rootView; + mQuickContactView = quickContactView; + mPrimaryActionView = primaryActionView; + mNameView = nameView; + mNumberView = numberView; + mLabelView = labelView; + mPercentView = percentView; + mBarView = barView; + + mPrimaryActionView.setOnClickListener(this); + + quickContactView.setPrioritizedMimeType(Phone.CONTENT_ITEM_TYPE); + + mContext = rootView.getContext(); + mPhotoSize = mContext.getResources().getDimensionPixelSize(R.dimen.contact_photo_size); + mContactInfoHelper = new ContactInfoHelper(mContext, + GeoUtil.getCurrentCountryIso(mContext)); + } + + public static CallStatsListItemViewHolder create(View view) { + return new CallStatsListItemViewHolder(view, + (QuickContactBadge) view.findViewById(R.id.quick_contact_photo), + view.findViewById(R.id.primary_action_view), + (TextView) view.findViewById(R.id.name), + (TextView) view.findViewById(R.id.number), + (TextView) view.findViewById(R.id.label), + (TextView) view.findViewById(R.id.percent), + (LinearColorBar) view.findViewById(R.id.percent_bar)); + } + + @Override + public void onClick(View v) { + if (clickIntent != null) { + DialerUtils.startActivityWithErrorToast(mContext, clickIntent); + } + } + + public void setDetails(CallStatsDetails details, CallStatsDetails first, + CallStatsDetails total, int type, boolean byDuration) { + this.details = details; + details.updateDisplayPropertiesIfNeeded(mContext); + + CharSequence numberFormattedLabel = null; + // Only show a label if the number is shown and it is not a SIP address. + if (!TextUtils.isEmpty(details.number) + && !PhoneNumberHelper.isUriNumber(details.number.toString())) { + numberFormattedLabel = Phone.getTypeLabel(mContext.getResources(), + details.numberType, details.numberLabel); + } + + final CharSequence nameText; + final CharSequence numberText; + final CharSequence labelText; + + if (TextUtils.isEmpty(details.name)) { + nameText = details.displayNumber; + if (TextUtils.isEmpty(details.geocode) || details.isVoicemailNumber) { + numberText = null; + } else { + numberText = details.geocode; + } + labelText = null; + } else { + nameText = details.name; + numberText = details.displayNumber; + labelText = numberFormattedLabel; + } + + float in = 0, out = 0, missed = 0, blacklist = 0; + float ratio = getDetailValue(details, type, byDuration) / + getDetailValue(first, type, byDuration); + + if (type == CallLogQueryHandler.CALL_TYPE_ALL) { + float full = getDetailValue(details, type, byDuration); + in = getDetailValue(details, Calls.INCOMING_TYPE, byDuration) * ratio / full; + out = getDetailValue(details, Calls.OUTGOING_TYPE, byDuration) * ratio / full; + if (!byDuration) { + missed = getDetailValue(details, Calls.MISSED_TYPE, byDuration) * ratio / full; + blacklist = getDetailValue(details, Calls.BLACKLIST_TYPE, byDuration) * ratio / full; + } + } else if (type == Calls.INCOMING_TYPE) { + in = ratio; + } else if (type == Calls.OUTGOING_TYPE) { + out = ratio; + } else if (type == Calls.MISSED_TYPE) { + missed = ratio; + } else if (type == Calls.BLACKLIST_TYPE) { + blacklist = ratio; + } + + mBarView.setRatios(in, out, missed, blacklist); + mNameView.setText(nameText); + mNumberView.setText(numberText); + mLabelView.setText(labelText); + mLabelView.setVisibility(TextUtils.isEmpty(labelText) ? View.GONE : View.VISIBLE); + + if (byDuration && type == Calls.MISSED_TYPE) { + mPercentView.setText(getCallCountString(mContext, details.missedCount)); + } else if (byDuration && type == Calls.BLACKLIST_TYPE) { + mPercentView.setText(getCallCountString(mContext, details.blacklistCount)); + } else { + float percent = getDetailValue(details, type, byDuration) * 100F / + getDetailValue(total, type, byDuration); + mPercentView.setText(String.format("%.1f%%", percent)); + } + + final String nameForDefaultImage = TextUtils.isEmpty(details.name) + ? details.displayNumber : details.name; + setPhoto(details.photoId, details.photoUri, details.contactUri, nameForDefaultImage, + details.isVoicemailNumber, mContactInfoHelper.isBusiness(details.sourceType)); + } + + private float getDetailValue(CallStatsDetails details, int type, boolean byDuration) { + if (byDuration) { + return (float) details.getRequestedDuration(type); + } else { + return (float) details.getRequestedCount(type); + } + } + + public static String getCallCountString(Context context, long count) { + return context.getResources().getQuantityString(R.plurals.call, (int) count, (int) count); + } + + public static String getDurationString(Context context, long duration, boolean includeSeconds) { + int hours, minutes, seconds; + + hours = (int) (duration / 3600); + duration -= (long) hours * 3600; + minutes = (int) (duration / 60); + duration -= (long) minutes * 60; + seconds = (int) duration; + + if (!includeSeconds) { + if (seconds >= 30) { + minutes++; + } + if (minutes >= 60) { + hours++; + } + } + + boolean dispHours = hours > 0; + boolean dispMinutes = minutes > 0 || (!includeSeconds && hours == 0); + boolean dispSeconds = includeSeconds && (seconds > 0 || (hours == 0 && minutes == 0)); + + final Resources res = context.getResources(); + final String hourString = dispHours ? + res.getQuantityString(R.plurals.hour, hours, hours) : null; + final String minuteString = dispMinutes ? + res.getQuantityString(R.plurals.minute, minutes, minutes) : null; + final String secondString = dispSeconds ? + res.getQuantityString(R.plurals.second, seconds, seconds) : null; + + int index = ((dispHours ? 4 : 0) | (dispMinutes ? 2 : 0) | (dispSeconds ? 1 : 0)) - 1; + String[] formats = res.getStringArray(R.array.call_stats_duration); + return String.format(formats[index], hourString, minuteString, secondString); + } + + private void setPhoto(long photoId, Uri photoUri, Uri contactUri, String displayName, + boolean isVoicemail, boolean isBusiness) { + mQuickContactView.assignContactUri(contactUri); + mQuickContactView.setOverlay(null); + + int contactType = ContactPhotoManager.TYPE_DEFAULT; + if (isVoicemail) { + contactType = ContactPhotoManager.TYPE_VOICEMAIL; + } else if (isBusiness) { + contactType = ContactPhotoManager.TYPE_BUSINESS; + } + + String lookupKey = null; + if (contactUri != null) { + lookupKey = UriUtils.getLookupKeyFromUri(contactUri); + } + + ContactPhotoManager.DefaultImageRequest request = + new ContactPhotoManager.DefaultImageRequest(displayName, lookupKey, + contactType, true /* isCircular */); + + if (photoId == 0 && photoUri != null) { + ContactPhotoManager.getInstance(mContext).loadPhoto(mQuickContactView, photoUri, + mPhotoSize, false /* darkTheme */, true /* isCircular */, request); + } else { + ContactPhotoManager.getInstance(mContext).loadThumbnail(mQuickContactView, photoId, + false /* darkTheme */, true /* isCircular */, request); + } + } +} diff --git a/src/com/android/dialer/callstats/CallStatsQuery.java b/src/com/android/dialer/callstats/CallStatsQuery.java new file mode 100644 index 000000000..2b0f6c7d0 --- /dev/null +++ b/src/com/android/dialer/callstats/CallStatsQuery.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * Copyright (C) 2013 Android Open Kang 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.dialer.callstats; + +import android.provider.CallLog.Calls; + +public class CallStatsQuery { + + public static final String[] _PROJECTION = new String[] { + Calls._ID, // 0 + Calls.NUMBER, // 1 + Calls.DATE, // 2 + Calls.DURATION, // 3 + Calls.TYPE, // 4 + Calls.COUNTRY_ISO, // 5 + Calls.GEOCODED_LOCATION, // 6 + Calls.CACHED_NAME, // 7 + Calls.CACHED_NUMBER_TYPE, // 8 + Calls.CACHED_NUMBER_LABEL, // 9 + Calls.CACHED_LOOKUP_URI, // 10 + Calls.CACHED_MATCHED_NUMBER, // 11 + Calls.CACHED_NORMALIZED_NUMBER, // 12 + Calls.CACHED_PHOTO_ID, // 13 + Calls.CACHED_FORMATTED_NUMBER, // 14 + Calls.NUMBER_PRESENTATION, // 15 + Calls.PHONE_ACCOUNT_COMPONENT_NAME, // 16 + Calls.PHONE_ACCOUNT_ID, // 17 + }; + + public static final int ID = 0; + public static final int NUMBER = 1; + public static final int DATE = 2; + public static final int DURATION = 3; + public static final int CALL_TYPE = 4; + public static final int COUNTRY_ISO = 5; + public static final int GEOCODED_LOCATION = 6; + public static final int CACHED_NAME = 7; + public static final int CACHED_NUMBER_TYPE = 8; + public static final int CACHED_NUMBER_LABEL = 9; + public static final int CACHED_LOOKUP_URI = 10; + public static final int CACHED_MATCHED_NUMBER = 11; + public static final int CACHED_NORMALIZED_NUMBER = 12; + public static final int CACHED_PHOTO_ID = 13; + public static final int CACHED_FORMATTED_NUMBER = 14; + public static final int NUMBER_PRESENTATION = 15; + public static final int ACCOUNT_COMPONENT_NAME = 16; + public static final int ACCOUNT_ID = 17; +} diff --git a/src/com/android/dialer/callstats/CallStatsQueryHandler.java b/src/com/android/dialer/callstats/CallStatsQueryHandler.java new file mode 100644 index 000000000..4d63ce123 --- /dev/null +++ b/src/com/android/dialer/callstats/CallStatsQueryHandler.java @@ -0,0 +1,302 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * Copyright (C) 2013 Android Open Kang 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.dialer.callstats; + +import android.content.AsyncQueryHandler; +import android.content.ContentResolver; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabaseCorruptException; +import android.database.sqlite.SQLiteDiskIOException; +import android.database.sqlite.SQLiteFullException; +import android.net.Uri; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.provider.CallLog.Calls; +import android.telecom.PhoneAccountHandle; +import android.telephony.PhoneNumberUtils; +import android.telephony.SubscriptionManager; +import android.util.Log; + +import com.android.contacts.common.util.UriUtils; +import com.android.dialer.calllog.ContactInfo; +import com.android.dialer.calllog.PhoneAccountUtils; +import com.google.common.collect.Lists; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Class to handle call-log queries, optionally with a date-range filter + */ +public class CallStatsQueryHandler extends AsyncQueryHandler { + private static final String[] EMPTY_STRING_ARRAY = new String[0]; + + private static final int EVENT_PROCESS_DATA = 10; + + private static final int QUERY_CALLS_TOKEN = 100; + + /** + * To specify all slots. + */ + public static final int CALL_SIM_ALL = -1; + + private static final String TAG = "CallStatsQueryHandler"; + + private final WeakReference<Listener> mListener; + private Handler mWorkerThreadHandler; + + /** + * Simple handler that wraps background calls to catch + * {@link SQLiteException}, such as when the disk is full. + */ + protected class CatchingWorkerHandler extends AsyncQueryHandler.WorkerHandler { + public CatchingWorkerHandler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + if (msg.arg1 == EVENT_PROCESS_DATA) { + Cursor cursor = (Cursor) msg.obj; + Message reply = CallStatsQueryHandler.this.obtainMessage(msg.what); + reply.obj = processData(cursor); + reply.arg1 = msg.arg1; + reply.sendToTarget(); + return; + } + + try { + // Perform same query while catching any exceptions + super.handleMessage(msg); + } catch (SQLiteDiskIOException e) { + Log.w(TAG, "Exception on background worker thread", e); + } catch (SQLiteFullException e) { + Log.w(TAG, "Exception on background worker thread", e); + } catch (SQLiteDatabaseCorruptException e) { + Log.w(TAG, "Exception on background worker thread", e); + } + } + } + + @Override + protected Handler createHandler(Looper looper) { + // Provide our special handler that catches exceptions + mWorkerThreadHandler = new CatchingWorkerHandler(looper); + return mWorkerThreadHandler; + } + + public CallStatsQueryHandler(ContentResolver contentResolver, Listener listener) { + super(contentResolver); + mListener = new WeakReference<Listener>(listener); + } + + public void fetchCalls(long from, long to, int slotId) { + cancelOperation(QUERY_CALLS_TOKEN); + + StringBuilder selection = new StringBuilder(); + List<String> selectionArgs = Lists.newArrayList(); + + if (from != -1) { + selection.append(String.format("(%s > ?)", Calls.DATE)); + selectionArgs.add(String.valueOf(from)); + } + if (to != -1) { + if (selection.length() > 0) { + selection.append(" AND "); + } + selection.append(String.format("(%s < ?)", Calls.DATE)); + selectionArgs.add(String.valueOf(to)); + } + if (slotId > CALL_SIM_ALL) { + int[] subId = SubscriptionManager.getSubId(slotId); + if (subId != null && subId.length >= 1) { + if (selection.length() > 0) { + selection.append(" AND "); + } + selection.append(String.format("(%s = ?)", Calls.PHONE_ACCOUNT_ID)); + selectionArgs.add(Integer.toString(subId[0])); + } + } + + startQuery(QUERY_CALLS_TOKEN, null, Calls.CONTENT_URI, CallStatsQuery._PROJECTION, + selection.toString(), selectionArgs.toArray(EMPTY_STRING_ARRAY), + Calls.NUMBER + " ASC"); + } + + @Override + protected synchronized void onQueryComplete(int token, Object cookie, Cursor cursor) { + if (token == QUERY_CALLS_TOKEN) { + Message msg = mWorkerThreadHandler.obtainMessage(token); + msg.arg1 = EVENT_PROCESS_DATA; + msg.obj = cursor; + + mWorkerThreadHandler.sendMessage(msg); + } + } + + @Override + public void handleMessage(Message msg) { + if (msg.arg1 == EVENT_PROCESS_DATA) { + final Map<ContactInfo, CallStatsDetails> calls = + (Map<ContactInfo, CallStatsDetails>) msg.obj; + final Listener listener = mListener.get(); + if (listener != null) { + listener.onCallsFetched(calls); + } + } else { + super.handleMessage(msg); + } + } + + private Map<ContactInfo, CallStatsDetails> processData(Cursor cursor) { + final Map<ContactInfo, CallStatsDetails> result = new HashMap<ContactInfo, CallStatsDetails>(); + final ArrayList<ContactInfo> infos = new ArrayList<ContactInfo>(); + final ArrayList<CallStatsDetails> calls = new ArrayList<CallStatsDetails>(); + CallStatsDetails pending = null; + + cursor.moveToFirst(); + + while (!cursor.isAfterLast()) { + final String number = cursor.getString(CallStatsQuery.NUMBER); + final long duration = cursor.getLong(CallStatsQuery.DURATION); + final int callType = cursor.getInt(CallStatsQuery.CALL_TYPE); + + if (pending == null || !phoneNumbersEqual(pending.number.toString(), number)) { + final long date = cursor.getLong(CallStatsQuery.DATE); + final int numberPresentation = cursor.getInt(CallStatsQuery.NUMBER_PRESENTATION); + final String countryIso = cursor.getString(CallStatsQuery.COUNTRY_ISO); + final String geocode = cursor.getString(CallStatsQuery.GEOCODED_LOCATION); + final ContactInfo info = getContactInfoFromCallStats(cursor); + final PhoneAccountHandle accountHandle = PhoneAccountUtils.getAccount( + cursor.getString(CallStatsQuery.ACCOUNT_COMPONENT_NAME), + cursor.getString(CallStatsQuery.ACCOUNT_ID)); + + pending = new CallStatsDetails(number, numberPresentation, + accountHandle, info, countryIso, geocode, date); + infos.add(info); + calls.add(pending); + } + + pending.addTimeOrMissed(callType, duration); + cursor.moveToNext(); + } + + cursor.close(); + mergeItemsByNumber(calls, infos); + + for (int i = 0; i < calls.size(); i++) { + result.put(infos.get(i), calls.get(i)); + } + + return result; + } + + private void mergeItemsByNumber(List<CallStatsDetails> calls, List<ContactInfo> infos) { + // temporarily store items marked for removal + final ArrayList<CallStatsDetails> callsToRemove = new ArrayList<CallStatsDetails>(); + final ArrayList<ContactInfo> infosToRemove = new ArrayList<ContactInfo>(); + + for (int i = 0; i < calls.size(); i++) { + final CallStatsDetails outerItem = calls.get(i); + final String currentFormattedNumber = outerItem.number.toString(); + + for (int j = calls.size() - 1; j > i; j--) { + final CallStatsDetails innerItem = calls.get(j); + final String innerNumber = innerItem.number.toString(); + + if (phoneNumbersEqual(currentFormattedNumber, innerNumber)) { + outerItem.mergeWith(innerItem); + //make sure we're not counting twice in case we're dealing with + //multiple different formats + innerItem.reset(); + callsToRemove.add(innerItem); + infosToRemove.add(infos.get(j)); + } + } + } + + for (CallStatsDetails call : callsToRemove) { + calls.remove(call); + } + for (ContactInfo info : infosToRemove) { + infos.remove(info); + } + } + + private ContactInfo getContactInfoFromCallStats(Cursor c) { + ContactInfo info = new ContactInfo(); + info.lookupUri = UriUtils.parseUriOrNull(c.getString(CallStatsQuery.CACHED_LOOKUP_URI)); + info.name = c.getString(CallStatsQuery.CACHED_NAME); + info.type = c.getInt(CallStatsQuery.CACHED_NUMBER_TYPE); + info.label = c.getString(CallStatsQuery.CACHED_NUMBER_LABEL); + + final String matchedNumber = c.getString(CallStatsQuery.CACHED_MATCHED_NUMBER); + info.number = matchedNumber == null ? c.getString(CallStatsQuery.NUMBER) : matchedNumber; + info.normalizedNumber = c.getString(CallStatsQuery.CACHED_NORMALIZED_NUMBER); + info.formattedNumber = c.getString(CallStatsQuery.CACHED_FORMATTED_NUMBER); + + info.photoId = c.getLong(CallStatsQuery.CACHED_PHOTO_ID); + info.photoUri = null; // We do not cache the photo URI. + + return info; + } + + private static boolean phoneNumbersEqual(String number1, String number2) { + if (PhoneNumberUtils.isUriNumber(number1) || PhoneNumberUtils.isUriNumber(number2)) { + return sipAddressesEqual(number1, number2); + } else { + return PhoneNumberUtils.compare(number1, number2); + } + } + + private static boolean sipAddressesEqual(String number1, String number2) { + if (number1 == null || number2 == null) return number1 == number2; + + int index1 = number1.indexOf('@'); + final String userinfo1; + final String rest1; + if (index1 != -1) { + userinfo1 = number1.substring(0, index1); + rest1 = number1.substring(index1); + } else { + userinfo1 = number1; + rest1 = ""; + } + + int index2 = number2.indexOf('@'); + final String userinfo2; + final String rest2; + if (index2 != -1) { + userinfo2 = number2.substring(0, index2); + rest2 = number2.substring(index2); + } else { + userinfo2 = number2; + rest2 = ""; + } + + return userinfo1.equals(userinfo2) && rest1.equalsIgnoreCase(rest2); + } + + public interface Listener { + void onCallsFetched(Map<ContactInfo, CallStatsDetails> calls); + } +} diff --git a/src/com/android/dialer/widget/DoubleDatePickerDialog.java b/src/com/android/dialer/widget/DoubleDatePickerDialog.java new file mode 100644 index 000000000..1d75e1bab --- /dev/null +++ b/src/com/android/dialer/widget/DoubleDatePickerDialog.java @@ -0,0 +1,336 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * Copyright (C) 2013 Android Open Kang 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.dialer.widget; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.DialogFragment; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.os.Bundle; +import android.text.format.DateUtils; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemSelectedListener; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.DatePicker; +import android.widget.DatePicker.OnDateChangedListener; +import android.widget.Spinner; +import android.widget.TextView; + +import java.util.ArrayList; +import java.util.Calendar; + +import com.android.dialer.R; + +/** + * Alertdialog with two date pickers - one for a start and one for an end date. + * Used to filter the callstats query. + */ +public class DoubleDatePickerDialog extends AlertDialog + implements OnClickListener, OnDateChangedListener, OnItemSelectedListener { + + private static final String TAG = "DoubleDatePickerDialog"; + + public interface OnDateSetListener { + void onDateSet(long from, long to); + } + + public static class Fragment extends DialogFragment implements OnDateSetListener { + private DoubleDatePickerDialog mDialog; + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + mDialog = new DoubleDatePickerDialog(getActivity(), this); + return mDialog; + } + + @Override + public void onStart() { + final Bundle args = getArguments(); + final long from = args.getLong("from", -1); + final long to = args.getLong("to", -1); + + if (from != -1) { + mDialog.setValues(from, to); + } else { + mDialog.resetPickers(); + } + super.onStart(); + } + + @Override + public void onDateSet(long from, long to) { + ((DoubleDatePickerDialog.OnDateSetListener) getActivity()).onDateSet(from, to); + } + + public static Bundle createArguments(long from, long to) { + final Bundle args = new Bundle(); + args.putLong("from", from); + args.putLong("to", to); + return args; + } + } + + private interface QuickSelection { + void adjustStartDate(Calendar date); + } + + private static final int[] QUICKSELECTION_ENTRIES = new int[] { + R.string.date_qs_currentmonth, + R.string.date_qs_currentquarter, + R.string.date_qs_currentyear, + R.string.date_qs_lastweek, + R.string.date_qs_lastmonth, + R.string.date_qs_lastquarter, + R.string.date_qs_lastyear + }; + + private static final QuickSelection[] QUICKSELECTIONS = new QuickSelection[] { + new QuickSelection() { + @Override + public void adjustStartDate(Calendar date) { + date.set(Calendar.DAY_OF_MONTH, 1); + } + }, + new QuickSelection() { + @Override + public void adjustStartDate(Calendar date) { + final int currentMonth = date.get(Calendar.MONTH); + date.set(Calendar.MONTH, currentMonth - (currentMonth % 3)); + date.set(Calendar.DAY_OF_MONTH, 1); + } + }, + new QuickSelection() { + @Override + public void adjustStartDate(Calendar date) { + date.set(Calendar.MONTH, 0); + date.set(Calendar.DAY_OF_MONTH, 1); + } + }, + new QuickSelection() { + @Override + public void adjustStartDate(Calendar date) { + date.add(Calendar.WEEK_OF_YEAR, -1); + } + }, + new QuickSelection() { + @Override + public void adjustStartDate(Calendar date) { + date.add(Calendar.MONTH, -1); + } + }, + new QuickSelection() { + @Override + public void adjustStartDate(Calendar date) { + date.add(Calendar.MONTH, -3); + } + }, + new QuickSelection() { + @Override + public void adjustStartDate(Calendar date) { + date.add(Calendar.YEAR, -1); + } + }, + }; + + private static final String YEAR = "year"; + private static final String MONTH = "month"; + private static final String DAY = "day"; + + private final Spinner mQuickSelSpinner; + private final DatePicker mDatePickerFrom; + private final DatePicker mDatePickerTo; + private final OnDateSetListener mCallBack; + private Button mOkButton; + private int mQuickSelSelection = -1; + + public DoubleDatePickerDialog(final Context context, OnDateSetListener callBack) { + super(context); + + mCallBack = callBack; + + setTitle(R.string.call_stats_filter_picker_title); + setButton(BUTTON_NEGATIVE, context.getString(android.R.string.cancel), this); + setButton(BUTTON_POSITIVE, context.getString(android.R.string.ok), this); + setIcon(0); + + LayoutInflater inflater = + (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + View view = inflater.inflate(R.layout.double_date_picker_dialog, null); + setView(view); + + mDatePickerFrom = (DatePicker) view.findViewById(R.id.date_picker_from); + mDatePickerTo = (DatePicker) view.findViewById(R.id.date_picker_to); + + ArrayList<CharSequence> quickSelEntries = new ArrayList<CharSequence>(); + for (int entryId : QUICKSELECTION_ENTRIES) { + quickSelEntries.add(context.getString(entryId)); + } + ArrayAdapter<CharSequence> quickSelAdapter = new ArrayAdapter<CharSequence>( + context, android.R.layout.simple_spinner_item, + android.R.id.text1, quickSelEntries) { + @Override + public View getView(int position, View convertView, android.view.ViewGroup parent) { + final TextView v = (TextView) super.getView(position, convertView, parent); + v.setText(context.getString(R.string.date_quick_selection)); + return v; + } + }; + quickSelAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + + mQuickSelSpinner = (Spinner) view.findViewById(R.id.date_quick_selection); + mQuickSelSpinner.setOnItemSelectedListener(this); + mQuickSelSpinner.setAdapter(quickSelAdapter); + + resetPickers(); + } + + @Override + protected void onStart() { + super.onStart(); + mOkButton = getButton(DialogInterface.BUTTON_POSITIVE); + updateOkButtonState(); + } + + @Override + public void onClick(DialogInterface dialog, int which) { + switch (which) { + case BUTTON_POSITIVE: + tryNotifyDateSet(); + break; + case BUTTON_NEGATIVE: + break; + } + } + + @Override + public void onItemSelected(AdapterView<?> parent, View view, int pos, long id) { + if (mQuickSelSelection >= 0) { + QuickSelection sel = QUICKSELECTIONS[pos]; + Calendar from = Calendar.getInstance(); + long millisTo = from.getTimeInMillis(); + sel.adjustStartDate(from); + long millisFrom = from.getTimeInMillis(); + + setValues(millisFrom, millisTo); + } + mQuickSelSelection = pos; + } + + @Override + public void onNothingSelected(AdapterView<?> parent) { + } + + public void onDateChanged(DatePicker view, int year, + int month, int day) { + view.init(year, month, day, this); + updateOkButtonState(); + } + + public void setValues(long millisFrom, long millisTo) { + setPicker(mDatePickerFrom, millisFrom); + setPicker(mDatePickerTo, millisTo); + updateOkButtonState(); + } + + public void resetPickers() { + long millis = System.currentTimeMillis(); + setPicker(mDatePickerFrom, millis); + setPicker(mDatePickerTo, millis); + updateOkButtonState(); + } + + private void setPicker(DatePicker picker, long millis) { + Calendar c = Calendar.getInstance(); + c.setTimeInMillis(millis); + + int year = c.get(Calendar.YEAR); + int month = c.get(Calendar.MONTH); + int day = c.get(Calendar.DAY_OF_MONTH); + + picker.init(year, month, day, this); + } + + private long getMillisForPicker(DatePicker picker, boolean endOfDay) { + Calendar c = Calendar.getInstance(); + c.set(Calendar.YEAR, picker.getYear()); + c.set(Calendar.MONTH, picker.getMonth()); + c.set(Calendar.DAY_OF_MONTH, picker.getDayOfMonth()); + c.set(Calendar.HOUR_OF_DAY, 0); + c.set(Calendar.MINUTE, 0); + c.set(Calendar.SECOND, 0); + + long millis = c.getTimeInMillis(); + if (endOfDay) { + millis += 24L * 60L * 60L * 1000L - 1L; + } + + return millis; + } + + private void updateOkButtonState() { + if (mOkButton != null) { + long millisFrom = getMillisForPicker(mDatePickerFrom, false); + long millisTo = getMillisForPicker(mDatePickerTo, true); + mOkButton.setEnabled(millisFrom < millisTo); + } + } + + private void tryNotifyDateSet() { + if (mCallBack != null) { + mDatePickerFrom.clearFocus(); + mDatePickerTo.clearFocus(); + + long millisFrom = getMillisForPicker(mDatePickerFrom, false); + long millisTo = getMillisForPicker(mDatePickerTo, true); + + mCallBack.onDateSet(millisFrom, millisTo); + } + } + + // users like to play with it, so save the state and don't reset each time + @Override + public Bundle onSaveInstanceState() { + Bundle state = super.onSaveInstanceState(); + state.putInt("F_" + YEAR, mDatePickerFrom.getYear()); + state.putInt("F_" + MONTH, mDatePickerFrom.getMonth()); + state.putInt("F_" + DAY, mDatePickerFrom.getDayOfMonth()); + state.putInt("T_" + YEAR, mDatePickerTo.getYear()); + state.putInt("T_" + MONTH, mDatePickerTo.getMonth()); + state.putInt("T_" + DAY, mDatePickerTo.getDayOfMonth()); + return state; + } + + @Override + public void onRestoreInstanceState(Bundle savedInstanceState) { + super.onRestoreInstanceState(savedInstanceState); + int fyear = savedInstanceState.getInt("F_" + YEAR); + int fmonth = savedInstanceState.getInt("F_" + MONTH); + int fday = savedInstanceState.getInt("F_" + DAY); + int tyear = savedInstanceState.getInt("T_" + YEAR); + int tmonth = savedInstanceState.getInt("T_" + MONTH); + int tday = savedInstanceState.getInt("T_" + DAY); + mDatePickerFrom.init(fyear, fmonth, fday, this); + mDatePickerTo.init(tyear, tmonth, tday, this); + } +} diff --git a/src/com/android/dialer/widget/LinearColorBar.java b/src/com/android/dialer/widget/LinearColorBar.java new file mode 100644 index 000000000..5ccfccef8 --- /dev/null +++ b/src/com/android/dialer/widget/LinearColorBar.java @@ -0,0 +1,228 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * Copyright (C) 2013 Android Open Kang 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.dialer.widget; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.LinearGradient; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.Rect; +import android.graphics.Shader; +import android.util.AttributeSet; +import android.util.DisplayMetrics; +import android.widget.LinearLayout; + +import com.android.dialer.R; + +public class LinearColorBar extends LinearLayout { + private float mFirstRatio; + private float mSecondRatio; + private float mThirdRatio; + private float mFourthRatio; + + private int mBackgroundColor; + private int mBlueColor; + private int mGreenColor; + private int mRedColor; + private int mOrangeColor; + + final Rect mRect = new Rect(); + final Paint mPaint = new Paint(); + + int mLastInterestingLeft, mLastInterestingRight; + int mLineWidth; + + final Path mColorPath = new Path(); + final Path mEdgePath = new Path(); + final Paint mColorGradientPaint = new Paint(); + final Paint mEdgeGradientPaint = new Paint(); + + public LinearColorBar(Context context, AttributeSet attrs) { + super(context, attrs); + setWillNotDraw(false); + + TypedArray a = context.obtainStyledAttributes( + attrs, R.styleable.LinearColorBar, 0, 0); + int n = a.getIndexCount(); + + for (int i = 0; i < n; i++) { + int attr = a.getIndex(i); + + switch (attr) { + case R.styleable.LinearColorBar_backgroundColor: + mBackgroundColor = a.getInt(attr, 0); + break; + case R.styleable.LinearColorBar_redColor: + mRedColor = a.getInt(attr, 0); + break; + case R.styleable.LinearColorBar_greenColor: + mGreenColor = a.getInt(attr, 0); + break; + case R.styleable.LinearColorBar_blueColor: + mBlueColor = a.getInt(attr, 0); + break; + case R.styleable.LinearColorBar_orangeColor: + mOrangeColor = a.getInt(attr, 0); + break; + } + } + + a.recycle(); + + mPaint.setStyle(Paint.Style.FILL); + mColorGradientPaint.setStyle(Paint.Style.FILL); + mColorGradientPaint.setAntiAlias(true); + mEdgeGradientPaint.setStyle(Paint.Style.STROKE); + mLineWidth = getResources().getDisplayMetrics().densityDpi >= DisplayMetrics.DENSITY_HIGH + ? 2 : 1; + mEdgeGradientPaint.setStrokeWidth(mLineWidth); + mEdgeGradientPaint.setAntiAlias(true); + } + + public void setRatios(float blue, float green, float red, float orange) { + mFirstRatio = blue; + mSecondRatio = green; + mThirdRatio = red; + mFourthRatio = orange; + invalidate(); + } + + private void updateIndicator() { + int off = getPaddingTop() - getPaddingBottom(); + if (off < 0) + off = 0; + mRect.top = off; + mRect.bottom = getHeight(); + + mColorGradientPaint.setShader(new LinearGradient( + 0, 0, 0, off - 2, mBackgroundColor & 0xffffff, + mBackgroundColor, Shader.TileMode.CLAMP)); + mEdgeGradientPaint.setShader(new LinearGradient( + 0, 0, 0, off / 2, 0x00a0a0a0, 0xffa0a0a0, Shader.TileMode.CLAMP)); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + updateIndicator(); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + int width = getWidth(); + + int left = 0; + + int right = left + (int) (width * mFirstRatio); + int right2 = right + (int) (width * mSecondRatio); + int right3 = right2 + (int) (width * mThirdRatio); + int right4 = right3 + (int) (width * mFourthRatio); + + int indicatorLeft = right4; + int indicatorRight = width; + + if (mLastInterestingLeft != indicatorLeft || mLastInterestingRight != indicatorRight) { + mColorPath.reset(); + mEdgePath.reset(); + if (indicatorLeft < indicatorRight) { + final int midTopY = mRect.top; + final int midBottomY = 0; + final int xoff = 2; + mColorPath.moveTo(indicatorLeft, mRect.top); + mColorPath.cubicTo(indicatorLeft, midBottomY, + -xoff, midTopY, + -xoff, 0); + mColorPath.lineTo(width + xoff - 1, 0); + mColorPath.cubicTo(width + xoff - 1, midTopY, + indicatorRight, midBottomY, + indicatorRight, mRect.top); + mColorPath.close(); + final float lineOffset = mLineWidth + .5f; + mEdgePath.moveTo(-xoff + lineOffset, 0); + mEdgePath.cubicTo(-xoff + lineOffset, midTopY, + indicatorLeft + lineOffset, midBottomY, + indicatorLeft + lineOffset, mRect.top); + mEdgePath.moveTo(width + xoff - 1 - lineOffset, 0); + mEdgePath.cubicTo(width + xoff - 1 - lineOffset, midTopY, + indicatorRight - lineOffset, midBottomY, + indicatorRight - lineOffset, mRect.top); + } + mLastInterestingLeft = indicatorLeft; + mLastInterestingRight = indicatorRight; + } + + if (!mEdgePath.isEmpty()) { + canvas.drawPath(mEdgePath, mEdgeGradientPaint); + canvas.drawPath(mColorPath, mColorGradientPaint); + } + + if (left < right) { + mRect.left = left; + mRect.right = right; + mPaint.setColor(mBlueColor); + canvas.drawRect(mRect, mPaint); + width -= (right - left); + left = right; + } + + right = right2; + + if (left < right) { + mRect.left = left; + mRect.right = right; + mPaint.setColor(mGreenColor); + canvas.drawRect(mRect, mPaint); + width -= (right - left); + left = right; + } + + right = right3; + + if (left < right) { + mRect.left = left; + mRect.right = right; + mPaint.setColor(mRedColor); + canvas.drawRect(mRect, mPaint); + width -= (right - left); + left = right; + } + + right = right4; + + if (left < right) { + mRect.left = left; + mRect.right = right; + mPaint.setColor(mOrangeColor); + canvas.drawRect(mRect, mPaint); + width -= (right - left); + left = right; + } + + right = left + width; + if (left < right) { + mRect.left = left; + mRect.right = right; + mPaint.setColor(mBackgroundColor); + canvas.drawRect(mRect, mPaint); + } + } +} |
