summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanny Baumann <dannybaumann@web.de>2016-01-07 13:00:27 +0100
committerDanny Baumann <dannybaumann@web.de>2016-01-21 08:27:45 +0100
commita1739cf58ca6685c9d198ed0f3361c144d48c105 (patch)
treed4b4638a5e5f92f42d3e7059cf19cd743de4209c
parentbd62aa81d16fd5832afa0412b3f1204314dfa4a3 (diff)
downloadpackages_apps_Dialer-a1739cf58ca6685c9d198ed0f3361c144d48c105.tar.gz
packages_apps_Dialer-a1739cf58ca6685c9d198ed0f3361c144d48c105.tar.bz2
packages_apps_Dialer-a1739cf58ca6685c9d198ed0f3361c144d48c105.zip
Add back call stats feature.
Change-Id: If6deee65dc30854f4454a8cdd28fdbf3d529da04
-rw-r--r--Android.mk7
-rw-r--r--AndroidManifest.xml11
-rw-r--r--proguard.flags3
-rw-r--r--res/layout/call_log_spinner_item.xml34
-rw-r--r--res/layout/call_spinner.xml21
-rw-r--r--res/layout/call_stats_detail.xml109
-rw-r--r--res/layout/call_stats_detail_info.xml239
-rw-r--r--res/layout/call_stats_detail_line.xml45
-rw-r--r--res/layout/call_stats_fragment.xml80
-rw-r--r--res/layout/call_stats_list_item.xml88
-rw-r--r--res/layout/double_date_picker_dialog.xml67
-rw-r--r--res/menu/call_stats_details_options.xml27
-rw-r--r--res/menu/call_stats_options.xml40
-rw-r--r--res/values/attrs.xml8
-rw-r--r--res/values/cm_arrays.xml17
-rw-r--r--res/values/cm_plurals.xml35
-rw-r--r--res/values/cm_strings.xml42
-rw-r--r--res/values/colors.xml3
-rw-r--r--res/values/styles.xml25
-rw-r--r--src/com/android/dialer/calllog/CallLogActivity.java31
-rw-r--r--src/com/android/dialer/calllog/CallLogQueryHandler.java5
-rw-r--r--src/com/android/dialer/calllog/SpinnerContent.java132
-rw-r--r--src/com/android/dialer/callstats/CallStatsAdapter.java270
-rw-r--r--src/com/android/dialer/callstats/CallStatsDetailActivity.java380
-rw-r--r--src/com/android/dialer/callstats/CallStatsDetails.java244
-rw-r--r--src/com/android/dialer/callstats/CallStatsFragment.java387
-rw-r--r--src/com/android/dialer/callstats/CallStatsListItemViewHolder.java264
-rw-r--r--src/com/android/dialer/callstats/CallStatsQuery.java63
-rw-r--r--src/com/android/dialer/callstats/CallStatsQueryHandler.java302
-rw-r--r--src/com/android/dialer/widget/DoubleDatePickerDialog.java336
-rw-r--r--src/com/android/dialer/widget/LinearColorBar.java228
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);
+ }
+ }
+}