summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanny Baumann <dannybaumann@web.de>2014-11-12 17:01:47 -0800
committerMatt Garnes <matt@cyngn.com>2014-11-13 17:48:12 -0800
commit5acc180074556da9885c8282901f4331afd27348 (patch)
treec59f92dc9ede0f78f451310a46a069639c4f46a3
parent571a9b41a93bad68fabfb3b659730d1d3b6d1e5a (diff)
downloadandroid_packages_apps_Dialer-5acc180074556da9885c8282901f4331afd27348.tar.gz
android_packages_apps_Dialer-5acc180074556da9885c8282901f4331afd27348.tar.bz2
android_packages_apps_Dialer-5acc180074556da9885c8282901f4331afd27348.zip
Add back call stats feature.
Conflicts: src/com/android/dialer/CallDetailActivity.java src/com/android/dialer/calllog/CallLogAdapter.java src/com/android/dialer/calllog/ContactInfoHelper.java Change-Id: Id10bc12cacaee3523b7614bce8493d8b423b3f40
-rw-r--r--AndroidManifest.xml11
-rw-r--r--res/drawable-hdpi/ic_call_inout_holo_dark.pngbin0 -> 550 bytes
-rw-r--r--res/drawable-mdpi/ic_call_inout_holo_dark.pngbin0 -> 385 bytes
-rw-r--r--res/drawable-xhdpi/ic_call_inout_holo_dark.pngbin0 -> 751 bytes
-rw-r--r--res/layout/call_stats_detail.xml225
-rw-r--r--res/layout/call_stats_detail_info.xml215
-rw-r--r--res/layout/call_stats_fragment.xml85
-rw-r--r--res/layout/call_stats_list_item.xml115
-rw-r--r--res/layout/call_stats_nav_item.xml39
-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.xml45
-rw-r--r--res/values/attrs.xml17
-rw-r--r--res/values/cm_arrays.xml42
-rw-r--r--res/values/cm_plurals.xml35
-rw-r--r--res/values/cm_strings.xml37
-rw-r--r--res/values/colors.xml8
-rw-r--r--res/values/dimens.xml5
-rwxr-xr-xsrc/com/android/dialer/CallDetailActivity.java158
-rw-r--r--src/com/android/dialer/CallDetailHeader.java426
-rwxr-xr-xsrc/com/android/dialer/PhoneCallDetails.java31
-rwxr-xr-xsrc/com/android/dialer/calllog/CallLogActivity.java30
-rwxr-xr-xsrc/com/android/dialer/calllog/CallLogAdapter.java434
-rw-r--r--src/com/android/dialer/calllog/CallLogAdapterHelper.java474
-rw-r--r--src/com/android/dialer/calllog/ContactInfoHelper.java32
-rw-r--r--src/com/android/dialer/callstats/CallStatsAdapter.java249
-rw-r--r--src/com/android/dialer/callstats/CallStatsDetailActivity.java274
-rw-r--r--src/com/android/dialer/callstats/CallStatsDetailHelper.java174
-rw-r--r--src/com/android/dialer/callstats/CallStatsDetailViews.java50
-rw-r--r--src/com/android/dialer/callstats/CallStatsDetails.java238
-rw-r--r--src/com/android/dialer/callstats/CallStatsFragment.java340
-rw-r--r--src/com/android/dialer/callstats/CallStatsListItemViews.java55
-rw-r--r--src/com/android/dialer/callstats/CallStatsQuery.java59
-rw-r--r--src/com/android/dialer/callstats/CallStatsQueryHandler.java247
-rw-r--r--src/com/android/dialer/callstats/IntentProvider.java54
-rw-r--r--src/com/android/dialer/widget/AnchoredScrollView.java101
-rw-r--r--src/com/android/dialer/widget/DoubleDatePickerDialog.java336
-rw-r--r--src/com/android/dialer/widget/LinearColorBar.java210
-rw-r--r--src/com/android/dialer/widget/PieChartView.java261
39 files changed, 4644 insertions, 562 deletions
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 3b6484eb9..d7708b475 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -163,6 +163,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/res/drawable-hdpi/ic_call_inout_holo_dark.png b/res/drawable-hdpi/ic_call_inout_holo_dark.png
new file mode 100644
index 000000000..1dbf4b485
--- /dev/null
+++ b/res/drawable-hdpi/ic_call_inout_holo_dark.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_call_inout_holo_dark.png b/res/drawable-mdpi/ic_call_inout_holo_dark.png
new file mode 100644
index 000000000..88b92a195
--- /dev/null
+++ b/res/drawable-mdpi/ic_call_inout_holo_dark.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_call_inout_holo_dark.png b/res/drawable-xhdpi/ic_call_inout_holo_dark.png
new file mode 100644
index 000000000..f133f164c
--- /dev/null
+++ b/res/drawable-xhdpi/ic_call_inout_holo_dark.png
Binary files differ
diff --git a/res/layout/call_stats_detail.xml b/res/layout/call_stats_detail.xml
new file mode 100644
index 000000000..656218ff2
--- /dev/null
+++ b/res/layout/call_stats_detail.xml
@@ -0,0 +1,225 @@
+<?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.
+-->
+
+<com.android.dialer.widget.AnchoredScrollView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:ex="http://schemas.android.com/apk/res/com.android.dialer"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ ex:anchorView="@+id/photo_text_bar_dummy"
+ ex:anchoredView="@+id/controls" >
+
+ <RelativeLayout
+ android:id="@+id/call_stats_detail"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:visibility="gone" >
+
+ <!-- This layout defines the position of the scroll anchor.
+ Sizes are supposed to match their counterpart in controls -->
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:layout_alignParentTop="true" >
+
+ <!-- Contact photo placeholder -->
+ <com.android.contacts.common.widget.ProportionalLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentStart="true"
+ android:id="@+id/photo_dummy"
+ ex:direction="widthToHeight"
+ ex:ratio="0.5" >
+
+ <!-- Proportional layout requires a view in it. -->
+ <View
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" />
+
+ </com.android.contacts.common.widget.ProportionalLayout>
+
+ <!-- Contact name placeholder -->
+ <View
+ android:id="@+id/photo_text_bar_dummy"
+ android:layout_width="match_parent"
+ android:layout_height="42dip"
+ android:layout_alignBottom="@id/photo_dummy"
+ android:layout_alignParentStart="true" />
+
+ </RelativeLayout>
+
+ <!-- The actual details -->
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/controls" >
+
+ <include layout="@layout/call_stats_detail_info" />
+ </LinearLayout>
+
+ <!-- The contents of the title block -->
+
+ <RelativeLayout
+ android:id="@+id/controls"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentStart="true"
+ android:layout_alignParentTop="true" >
+
+ <com.android.contacts.common.widget.ProportionalLayout
+ android:id="@+id/contact_background_sizer"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentStart="true"
+ ex:direction="widthToHeight"
+ ex:ratio="0.5" >
+
+ <ImageView
+ android:id="@+id/contact_background"
+ android:layout_width="match_parent"
+ android:layout_height="0dip"
+ android:adjustViewBounds="true"
+ android:scaleType="centerCrop" />
+
+ </com.android.contacts.common.widget.ProportionalLayout>
+
+ <LinearLayout
+ android:id="@+id/separator"
+ android:layout_width="match_parent"
+ android:layout_height="1dip"
+ android:layout_below="@+id/contact_background_sizer"
+ android:background="@color/background_dialer_light" />
+
+ <View
+ android:id="@+id/photo_text_bar"
+ android:layout_width="match_parent"
+ android:layout_height="42dip"
+ android:layout_alignBottom="@id/contact_background_sizer"
+ android:layout_alignParentStart="true"
+ android:background="#7F000000" />
+
+ <ImageView
+ android:id="@+id/main_action"
+ android:layout_width="wrap_content"
+ android:layout_height="0dip"
+ android:layout_alignBottom="@id/photo_text_bar"
+ android:layout_alignEnd="@id/photo_text_bar"
+ android:layout_alignTop="@id/photo_text_bar"
+ android:layout_marginEnd="@dimen/call_log_outer_margin"
+ android:scaleType="center" />
+
+ <TextView
+ android:id="@+id/header_text"
+ android:layout_width="wrap_content"
+ android:layout_height="0dip"
+ android:layout_alignBottom="@id/photo_text_bar"
+ android:layout_alignStart="@id/photo_text_bar"
+ android:layout_alignTop="@id/photo_text_bar"
+ android:layout_marginStart="@dimen/call_detail_contact_name_margin"
+ android:layout_marginEnd="@dimen/call_log_inner_margin"
+ android:layout_toStartOf="@id/main_action"
+ android:gravity="center_vertical"
+ android:singleLine="true"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:textColor="?attr/call_log_header_color" />
+
+ <ImageButton
+ android:id="@+id/main_action_push_layer"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_alignBottom="@id/contact_background_sizer"
+ android:layout_alignStart="@id/contact_background_sizer"
+ android:layout_alignEnd="@id/contact_background_sizer"
+ android:layout_alignTop="@id/contact_background_sizer"
+ android:background="?android:attr/selectableItemBackground" />
+
+ <FrameLayout
+ android:id="@+id/call_and_sms"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/call_log_list_item_height"
+ android:layout_below="@id/main_action_push_layer"
+ android:layout_marginBottom="@dimen/call_detail_button_spacing"
+ android:layout_marginTop="@dimen/call_detail_button_spacing"
+ android:background="@color/background_dialer_list_items"
+ android:gravity="center_vertical" >
+
+ <LinearLayout
+ android:id="@+id/call_and_sms_main_action"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="?android:attr/selectableItemBackground"
+ android:focusable="true"
+ android:orientation="horizontal" >
+
+ <LinearLayout
+ android:layout_width="0dip"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:gravity="center_vertical"
+ android:orientation="vertical"
+ android:paddingStart="@dimen/call_log_indent_margin" >
+
+ <TextView
+ android:id="@+id/call_and_sms_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:ellipsize="end"
+ android:paddingEnd="@dimen/call_log_icon_margin"
+ android:singleLine="true"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:textColor="?attr/call_log_primary_text_color" />
+
+ <TextView
+ android:id="@+id/call_and_sms_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:ellipsize="end"
+ android:paddingEnd="@dimen/call_log_icon_margin"
+ android:singleLine="true"
+ android:textAllCaps="true"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textColor="?attr/call_log_primary_text_color" />
+ </LinearLayout>
+
+ <View
+ android:id="@+id/call_and_sms_divider"
+ android:layout_width="1px"
+ android:layout_height="32dip"
+ android:layout_gravity="center_vertical"
+ android:background="@color/background_dialer_light" />
+
+ <ImageView
+ android:id="@+id/call_and_sms_icon"
+ android:layout_width="@color/call_log_voicemail_highlight_color"
+ android:layout_height="match_parent"
+ android:background="?android:attr/selectableItemBackground"
+ android:focusable="true"
+ android:gravity="center"
+ android:paddingStart="@dimen/call_log_inner_margin"
+ android:paddingEnd="@dimen/call_log_outer_margin"
+ android:scaleType="centerInside" />
+
+ </LinearLayout>
+
+ </FrameLayout>
+
+ </RelativeLayout>
+
+ </RelativeLayout>
+
+</com.android.dialer.widget.AnchoredScrollView>
diff --git a/res/layout/call_stats_detail_info.xml b/res/layout/call_stats_detail_info.xml
new file mode 100644
index 000000000..70716700a
--- /dev/null
+++ b/res/layout/call_stats_detail_info.xml
@@ -0,0 +1,215 @@
+<?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="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="@dimen/call_detail_contact_name_margin"
+ android:layout_marginTop="@dimen/call_log_outer_margin" >
+
+ <TextView
+ android:id="@+id/date_filter"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:layout_marginBottom="@dimen/call_log_icon_margin"
+ android:textColor="@color/secondary_text_color" />
+
+ <LinearLayout
+ android:id="@+id/total_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/call_log_icon_margin"
+ android:layout_below="@id/date_filter" >
+
+ <ImageView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:src="@drawable/ic_call_inout_holo_dark" />
+
+ <TextView
+ android:id="@+id/total_summary"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_below="@id/date_filter"
+ android:layout_marginStart="@dimen/call_log_icon_margin"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textColor="@color/secondary_text_color" />
+
+ </LinearLayout>
+
+ <TextView
+ android:id="@+id/total_duration"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/total_container"
+ android:layout_marginBottom="@dimen/call_log_inner_margin"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textColor="@color/secondary_text_color" />
+
+ <com.android.dialer.widget.PieChartView
+ android:id="@+id/pie_chart"
+ android:layout_width="@dimen/call_stats_details_chart_size"
+ android:layout_height="@dimen/call_stats_details_chart_size"
+ android:layout_alignParentEnd="true"
+ android:layout_centerVertical="true"
+ android:layout_below="@id/total_duration"
+ ex:outlineColor="@color/call_stats_bar_background" />
+
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_alignParentStart="true"
+ android:layout_toStartOf="@id/pie_chart"
+ android:layout_below="@id/total_duration"
+ android:divider="?android:attr/dividerHorizontal"
+ android:orientation="vertical"
+ android:showDividers="middle" >
+
+ <LinearLayout
+ android:id="@+id/in_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/call_log_icon_margin"
+ android:layout_marginTop="@dimen/call_log_icon_margin"
+ android:orientation="vertical" >
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" >
+
+ <ImageView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:src="@drawable/ic_call_incoming_holo_dark" />
+
+ <TextView
+ android:id="@+id/in_summary"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_marginStart="@dimen/call_log_icon_margin"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textColor="@color/secondary_text_color" />
+
+ </LinearLayout>
+
+ <TextView
+ android:id="@+id/in_count"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textColor="@color/secondary_text_color" />
+
+ <TextView
+ android:id="@+id/in_duration"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textColor="@color/secondary_text_color" />
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/out_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/call_log_icon_margin"
+ android:layout_marginTop="@dimen/call_log_icon_margin"
+ android:orientation="vertical" >
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" >
+
+ <ImageView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:src="@drawable/ic_call_outgoing_holo_dark" />
+
+ <TextView
+ android:id="@+id/out_summary"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_marginStart="@dimen/call_log_icon_margin"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textColor="@color/secondary_text_color" />
+
+ </LinearLayout>
+
+ <TextView
+ android:id="@+id/out_count"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textColor="@color/secondary_text_color" />
+
+ <TextView
+ android:id="@+id/out_duration"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textColor="@color/secondary_text_color" />
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/missed_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/call_log_icon_margin"
+ android:orientation="vertical" >
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" >
+
+ <ImageView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:src="@drawable/ic_call_missed_holo_dark" />
+
+ <TextView
+ android:id="@+id/missed_summary"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="@dimen/call_log_icon_margin"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textColor="@color/secondary_text_color" />
+
+ </LinearLayout>
+
+ <TextView
+ android:id="@+id/missed_count"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textColor="@color/secondary_text_color" />
+
+ </LinearLayout>
+
+ </LinearLayout>
+
+</RelativeLayout>
diff --git a/res/layout/call_stats_fragment.xml b/res/layout/call_stats_fragment.xml
new file mode 100644
index 000000000..df121335b
--- /dev/null
+++ b/res/layout/call_stats_fragment.xml
@@ -0,0 +1,85 @@
+<?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. -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:divider="?android:attr/dividerHorizontal"
+ android:orientation="vertical"
+ android:showDividers="end" >
+
+ <LinearLayout
+ android:id="@+id/call_stats_header"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:paddingTop="@dimen/call_log_inner_margin"
+ android:visibility="gone" >
+
+ <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: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" />
+
+ <View
+ android:id="@+id/call_stats_divider"
+ android:layout_width="match_parent"
+ android:layout_height="1px"
+ android:layout_gravity="bottom"
+ android:layout_marginStart="@dimen/call_log_outer_margin"
+ android:layout_marginEnd="@dimen/call_log_outer_margin"
+ android:background="#55ffffff" />
+ </LinearLayout>
+
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" >
+
+ <ListView
+ android:id="@android:id/list"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:divider="@null"
+ android:fadingEdge="none"
+ android:scrollbarStyle="outsideOverlay" />
+
+ <TextView
+ android:id="@android:id/empty"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_marginTop="@dimen/empty_message_top_margin"
+ android:gravity="center"
+ android:text="@string/recentCalls_empty"
+ android:textAppearance="?android:attr/textAppearanceLarge"
+ android:textColor="?android:attr/textColorSecondary" />
+ </FrameLayout>
+
+</LinearLayout>
diff --git a/res/layout/call_stats_list_item.xml b/res/layout/call_stats_list_item.xml
new file mode 100644
index 000000000..29d584daf
--- /dev/null
+++ b/res/layout/call_stats_list_item.xml
@@ -0,0 +1,115 @@
+<?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.
+-->
+
+<view
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:ex="http://schemas.android.com/apk/res/com.android.dialer"
+ class="com.android.dialer.calllog.CallLogListItemView"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:id="@+id/call_log_list_item"
+ android:orientation="vertical"
+>
+
+ <GridLayout
+ android:id="@+id/primary_action_view"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_centerVertical="true"
+ android:layout_marginStart="@dimen/call_log_outer_margin"
+ android:layout_marginEnd="@dimen/call_log_outer_margin"
+ android:background="?android:attr/selectableItemBackground"
+ android:columnCount="3"
+ android:focusable="true"
+ android:nextFocusLeft="@+id/quick_contact_photo" >
+
+ <QuickContactBadge
+ android:id="@+id/quick_contact_photo"
+ android:layout_width="@dimen/call_log_list_contact_photo_size"
+ android:layout_height="@dimen/call_log_list_contact_photo_size"
+ android:layout_marginBottom="@dimen/call_log_icon_margin"
+ android:layout_marginTop="@dimen/call_log_icon_margin"
+ android:layout_rowSpan="3"
+ android:focusable="true"
+ android:nextFocusRight="@id/primary_action_view" />
+
+ <TextView
+ android:id="@+id/name"
+ android:layout_width="wrap_content"
+ android:layout_marginStart="@dimen/call_log_inner_margin"
+ android:layout_marginTop="@dimen/call_log_icon_margin"
+ android:ellipsize="marquee"
+ android:singleLine="true"
+ android:textColor="?attr/call_log_primary_text_color"
+ android:textSize="18sp" />
+
+ <TextView
+ android:id="@+id/percent"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="fill_horizontal|bottom"
+ android:gravity="end"
+ android:textColor="?attr/call_log_secondary_text_color"
+ android:textSize="14sp" />
+
+ <com.android.dialer.widget.LinearColorBar
+ android:id="@+id/percent_bar"
+ android:layout_height="10dp"
+ android:layout_columnSpan="2"
+ android:layout_gravity="fill_horizontal"
+ android:layout_marginBottom="@dimen/call_log_icon_margin"
+ android:layout_marginStart="@dimen/call_log_inner_margin"
+ android:layout_marginTop="@dimen/call_log_icon_margin"
+ android:orientation="horizontal"
+ android:paddingEnd="4dp"
+ android:paddingStart="4dp"
+ ex:backgroundColor="@color/call_stats_bar_background"
+ ex:blueColor="@color/call_stats_incoming"
+ ex:greenColor="@color/call_stats_outgoing"
+ ex:redColor="@color/call_stats_missed" />
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_columnSpan="2"
+ android:layout_marginStart="@dimen/call_log_inner_margin"
+ android:orientation="horizontal" >
+
+ <TextView
+ android:id="@+id/number"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="@dimen/call_log_icon_margin"
+ android:ellipsize="marquee"
+ android:singleLine="true"
+ android:textColor="?attr/call_log_secondary_text_color"
+ android:textSize="14sp" />
+
+ <TextView
+ android:id="@+id/label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="@dimen/call_log_icon_margin"
+ android:ellipsize="marquee"
+ android:singleLine="true"
+ android:textColor="?attr/call_log_secondary_text_color"
+ android:textSize="14sp"
+ android:textStyle="bold" />
+ </LinearLayout>
+ </GridLayout>
+
+</view>
diff --git a/res/layout/call_stats_nav_item.xml b/res/layout/call_stats_nav_item.xml
new file mode 100644
index 000000000..c6852b888
--- /dev/null
+++ b/res/layout/call_stats_nav_item.xml
@@ -0,0 +1,39 @@
+<?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="fill_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal" >
+
+ <ImageView
+ android:id="@+id/call_stats_nav_icon"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:paddingStart="8dp" />
+
+ <TextView
+ android:id="@+id/call_stats_nav_text"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_vertical"
+ android:minHeight="40dp"
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+ android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+ android:textAppearance="?android:attr/textAppearanceListItemSmall" />
+
+</LinearLayout>
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..6cc9ec0d0
--- /dev/null
+++ b/res/menu/call_stats_options.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.
+-->
+<menu xmlns:android="http://schemas.android.com/apk/res/android" >
+ <item
+ android:id="@+id/filter"
+ android:showAsAction="always"
+ android:actionViewClass="android.widget.Spinner" />
+
+ <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..5ea16f98b 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -33,4 +33,21 @@
<declare-styleable name="SearchEditTextLayout" />
+ <declare-styleable name="AnchoredScrollView">
+ <attr name="anchorView" format="reference" />
+ <attr name="anchorAtBottom" format="boolean" />
+ <attr name="anchoredView" format="reference" />
+ </declare-styleable>
+
+ <declare-styleable name="LinearColorBar">
+ <attr name="redColor" format="color" />
+ <attr name="greenColor" format="color" />
+ <attr name="blueColor" format="color" />
+ <attr name="backgroundColor" format="color" />
+ </declare-styleable>
+
+ <declare-styleable name="PieChartView">
+ <attr name="outlineColor" format="color" />
+ </declare-styleable>
+
</resources>
diff --git a/res/values/cm_arrays.xml b/res/values/cm_arrays.xml
new file mode 100644
index 000000000..4a58561dd
--- /dev/null
+++ b/res/values/cm_arrays.xml
@@ -0,0 +1,42 @@
+<?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">
+ <string-array name="call_stats_nav_items" translatable="false">
+ <item>@string/call_stats_nav_all</item>
+ <item>@string/call_stats_nav_incoming</item>
+ <item>@string/call_stats_nav_outgoing</item>
+ <item>@string/call_stats_nav_missed</item>
+ </string-array>
+
+ <!-- 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>
+</resources>
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 6c0a5596a..ef1068ef6 100644
--- a/res/values/cm_strings.xml
+++ b/res/values/cm_strings.xml
@@ -18,4 +18,41 @@
<!-- Forward lookup -->
<string name="nearby_places">Nearby places</string>
<string name="people">People</string>
+ <string name="call_log_stats_title">Statistics</string>
+ <string name="callStatsDetailTitle">Call stat details</string>
+
+ <string name="call_stats">Call statistics</string>
+ <string name="call_stats_refresh">Refresh</string>
+ <string name="activity_title_call_stats">Call statistics</string>
+
+ <string name="call_stats_nav_all">All</string>
+ <string name="call_stats_nav_incoming">Incoming</string>
+ <string name="call_stats_nav_outgoing">Outgoing</string>
+ <string name="call_stats_nav_missed">Missed</string>
+ <string name="call_stats_incoming">Incoming: <xliff:g id="percent">%d</xliff:g>%%</string>
+ <string name="call_stats_outgoing">Outgoing: <xliff:g id="percent">%d</xliff:g>%%</string>
+ <string name="call_stats_missed">Missed</string>
+ <string name="call_stats_missed_percent">Missed: <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="menu_add_to_blacklist">Add to blacklist</string>
+ <string name="toast_added_to_blacklist">%s added to blacklist.</string>
</resources>
diff --git a/res/values/colors.xml b/res/values/colors.xml
index 863bfe9d4..83b2b8f85 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -101,4 +101,12 @@
<color name="floating_action_button_touch_tint">#80ffffff</color>
+ <!-- Text color for no favorites message -->
+ <color name="nofavorite_text_color">#777777</color>
+
+ <!-- Colors for incoming and outgoing calls in the call statistics -->
+ <color name="call_stats_incoming">#33b5e5</color>
+ <color name="call_stats_outgoing">#99cc00</color>
+ <color name="call_stats_missed">#eb1313</color>
+ <color name="call_stats_bar_background">#88888888</color>
</resources>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 383a8faa6..3d50ed728 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -31,6 +31,8 @@
<dimen name="call_log_indent_margin">24dip</dimen>
<dimen name="call_log_name_margin_bottom">2dp</dimen>
<dimen name="call_log_list_item_height">56dip</dimen>
+ <dimen name="call_log_list_contact_photo_size">64dip</dimen>
+ <dimen name="call_detail_contact_name_margin">24dip</dimen>
<!-- Size of contact photos in the call log and call details. -->
<dimen name="contact_photo_size">40dp</dimen>
@@ -141,4 +143,7 @@
<dimen name="preference_padding_bottom">16dp</dimen>
<dimen name="preference_side_margin">16dp</dimen>
<dimen name="preference_summary_line_spacing_extra">4dp</dimen>
+
+ <!-- Size of the pie chart in the call stats detail activity -->
+ <dimen name="call_stats_details_chart_size">140dip</dimen>
</resources>
diff --git a/src/com/android/dialer/CallDetailActivity.java b/src/com/android/dialer/CallDetailActivity.java
index eb4af634a..2d332851c 100755
--- a/src/com/android/dialer/CallDetailActivity.java
+++ b/src/com/android/dialer/CallDetailActivity.java
@@ -92,9 +92,6 @@ public class CallDetailActivity extends AnalyticsActivity implements ProximitySe
private static final int LOADER_ID = 0;
private static final String BUNDLE_CONTACT_URI_EXTRA = "contact_uri_extra";
- private static final char LEFT_TO_RIGHT_EMBEDDING = '\u202A';
- private static final char POP_DIRECTIONAL_FORMATTING = '\u202C';
-
/** The time to wait before enabling the blank the screen due to the proximity sensor. */
private static final long PROXIMITY_BLANK_DELAY_MILLIS = 100;
/** The time to wait before disabling the blank the screen due to the proximity sensor. */
@@ -119,6 +116,7 @@ public class CallDetailActivity extends AnalyticsActivity implements ProximitySe
public static final String VOICEMAIL_FRAGMENT_TAG = "voicemail_fragment";
+ private CallDetailHeader mCallDetailHeader;
private CallTypeHelper mCallTypeHelper;
private PhoneNumberDisplayHelper mPhoneNumberHelper;
private QuickContactBadge mQuickContactBadge;
@@ -133,8 +131,6 @@ public class CallDetailActivity extends AnalyticsActivity implements ProximitySe
/* package */ LayoutInflater mInflater;
/* package */ Resources mResources;
- /** Helper to load contact photos. */
- private ContactPhotoManager mContactPhotoManager;
/** Helper to make async queries to content resolver. */
private CallDetailActivityQueryHandler mAsyncQueryHandler;
/** Helper to get voicemail status messages. */
@@ -259,6 +255,7 @@ public class CallDetailActivity extends AnalyticsActivity implements ProximitySe
mCallTypeHelper = new CallTypeHelper(getResources());
mPhoneNumberHelper = new PhoneNumberDisplayHelper(mResources);
+ mCallDetailHeader = new CallDetailHeader(this, mPhoneNumberHelper);
mVoicemailStatusHelper = new VoicemailStatusHelperImpl();
mAsyncQueryHandler = new CallDetailActivityQueryHandler(this);
@@ -270,7 +267,6 @@ public class CallDetailActivity extends AnalyticsActivity implements ProximitySe
mCallerNumber = (TextView) findViewById(R.id.caller_number);
mAccountLabel = (TextView) findViewById(R.id.phone_account_label);
mDefaultCountryIso = GeoUtil.getCurrentCountryIso(this);
- mContactPhotoManager = ContactPhotoManager.getInstance(this);
mProximitySensorManager = new ProximitySensorManager(this, mProximitySensorListener);
mContactInfoHelper = new ContactInfoHelper(this, GeoUtil.getCurrentCountryIso(this));
getActionBar().setDisplayHomeAsUpEnabled(true);
@@ -393,18 +389,8 @@ public class CallDetailActivity extends AnalyticsActivity implements ProximitySe
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
- switch (keyCode) {
- case KeyEvent.KEYCODE_CALL: {
- // Make sure phone isn't already busy before starting direct call
- TelephonyManager tm = (TelephonyManager)
- getSystemService(Context.TELEPHONY_SERVICE);
- if (tm.getCallState() == TelephonyManager.CALL_STATE_IDLE) {
- DialerUtils.startActivityWithErrorToast(this,
- CallUtil.getCallIntent(Uri.fromParts(PhoneAccount.SCHEME_TEL, mNumber,
- null)), R.string.call_not_available);
- return true;
- }
- }
+ if (mCallDetailHeader.handleKeyDown(keyCode, event)) {
+ return true;
}
return super.onKeyDown(keyCode, event);
@@ -450,8 +436,8 @@ public class CallDetailActivity extends AnalyticsActivity implements ProximitySe
PhoneCallDetails firstDetails = details[0];
mNumber = firstDetails.number.toString();
final int numberPresentation = firstDetails.numberPresentation;
- final Uri contactUri = firstDetails.contactUri;
- final Uri photoUri = firstDetails.photoUri;
+ // Set the details header, based on the first phone call.
+ mCallDetailHeader.updateViews(mNumber, numberPresentation, firstDetails);
// Cache the details about the phone number.
final boolean canPlaceCallsTo =
@@ -460,38 +446,7 @@ public class CallDetailActivity extends AnalyticsActivity implements ProximitySe
final boolean isVoicemailNumber = phoneUtils.isVoicemailNumber(mNumber);
final boolean isSipNumber = phoneUtils.isSipNumber(mNumber);
- final CharSequence callLocationOrType = getNumberTypeOrLocation(firstDetails);
-
- final CharSequence displayNumber = mPhoneNumberHelper.getDisplayNumber(
- firstDetails.number,
- firstDetails.numberPresentation,
- firstDetails.formattedNumber);
- final String displayNumberStr = mBidiFormatter.unicodeWrap(
- displayNumber.toString(), TextDirectionHeuristics.LTR);
-
-
- if (!TextUtils.isEmpty(firstDetails.name)) {
- mCallerName.setText(firstDetails.name);
- mCallerNumber.setText(callLocationOrType + " " + displayNumberStr);
- } else {
- mCallerName.setText(displayNumberStr);
- if (!TextUtils.isEmpty(callLocationOrType)) {
- mCallerNumber.setText(callLocationOrType);
- mCallerNumber.setVisibility(View.VISIBLE);
- } else {
- mCallerNumber.setVisibility(View.GONE);
- }
- }
-
- if (!TextUtils.isEmpty(firstDetails.accountLabel)) {
- mAccountLabel.setText(firstDetails.accountLabel);
- mAccountLabel.setVisibility(View.VISIBLE);
- } else {
- mAccountLabel.setVisibility(View.GONE);
- }
-
- mHasEditNumberBeforeCallOption =
- canPlaceCallsTo && !isSipNumber && !isVoicemailNumber;
+ mHasEditNumberBeforeCallOption = mCallDetailHeader.canEditNumberBeforeCall();
mHasTrashOption = hasVoicemail();
mHasRemoveFromCallLogOption = !hasVoicemail();
invalidateOptionsMenu();
@@ -500,33 +455,7 @@ public class CallDetailActivity extends AnalyticsActivity implements ProximitySe
historyList.setAdapter(
new CallDetailHistoryAdapter(CallDetailActivity.this, mInflater,
mCallTypeHelper, details));
-
- String lookupKey = contactUri == null ? null
- : ContactInfoHelper.getLookupKeyFromUri(contactUri);
-
- final boolean isBusiness = mContactInfoHelper.isBusiness(firstDetails.sourceType);
-
- final int contactType =
- isVoicemailNumber? ContactPhotoManager.TYPE_VOICEMAIL :
- isBusiness ? ContactPhotoManager.TYPE_BUSINESS :
- ContactPhotoManager.TYPE_DEFAULT;
-
- String nameForDefaultImage;
- if (TextUtils.isEmpty(firstDetails.name)) {
- nameForDefaultImage = mPhoneNumberHelper.getDisplayNumber(firstDetails.number,
- firstDetails.numberPresentation,
- firstDetails.formattedNumber).toString();
- } else {
- nameForDefaultImage = firstDetails.name.toString();
- }
-
- if (hasVoicemail() && !TextUtils.isEmpty(firstDetails.transcription)) {
- mVoicemailTranscription.setText(firstDetails.transcription);
- mVoicemailTranscription.setVisibility(View.VISIBLE);
- }
-
- loadContactPhotos(
- contactUri, photoUri, nameForDefaultImage, lookupKey, contactType);
+ mCallDetailHeader.loadContactPhotos(firstDetails.photoUri);
findViewById(R.id.call_detail).setVisibility(View.VISIBLE);
}
@@ -629,68 +558,6 @@ public class CallDetailActivity extends AnalyticsActivity implements ProximitySe
}
}
- /** Load the contact photos and places them in the corresponding views. */
- private void loadContactPhotos(Uri contactUri, Uri photoUri, String displayName,
- String lookupKey, int contactType) {
-
- Account contactAccount = null;
- if (contactUri != null) {
- ContentResolver resolver = getContentResolver();
- Uri uri = Contacts.lookupContact(resolver, contactUri);
- if (uri != null) {
- Cursor cursor = resolver.query(
- uri,
- new String[] { RawContacts.ACCOUNT_TYPE, RawContacts.ACCOUNT_NAME },
- null, null, null);
- if (cursor != null && cursor.moveToFirst()) {
- String accountType = cursor.getString(0);
- String accountName = cursor.getString(1);
- if (!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(accountType)) {
- contactAccount = new Account(accountName, accountType);
- }
- cursor.close();
- }
- }
- }
-
- final DefaultImageRequest request = new DefaultImageRequest(displayName, lookupKey,
- contactType, true /* isCircular */);
-
- mQuickContactBadge.assignContactUri(contactUri);
- mQuickContactBadge.setContentDescription(
- mResources.getString(R.string.description_contact_details, displayName));
-
- mContactPhotoManager.loadDirectoryPhoto(mQuickContactBadge, photoUri,
- contactAccount, false /* darkTheme */, true /* isCircular */, request);
- }
-
- static final class ViewEntry {
- public final String text;
- public final Intent primaryIntent;
- /** The description for accessibility of the primary action. */
- public final String primaryDescription;
-
- public CharSequence label = null;
- /** Icon for the secondary action. */
- public int secondaryIcon = 0;
- /** Intent for the secondary action. If not null, an icon must be defined. */
- public Intent secondaryIntent = null;
- /** The description for accessibility of the secondary action. */
- public String secondaryDescription = null;
-
- public ViewEntry(String text, Intent intent, String description) {
- this.text = text;
- primaryIntent = intent;
- primaryDescription = description;
- }
-
- public void setSecondaryAction(int icon, Intent intent, String description) {
- secondaryIcon = icon;
- secondaryIntent = intent;
- secondaryDescription = description;
- }
- }
-
protected void updateVoicemailStatusMessage(Cursor statusCursor) {
if (statusCursor == null) {
mStatusMessageView.setVisibility(View.GONE);
@@ -856,13 +723,4 @@ public class CallDetailActivity extends AnalyticsActivity implements ProximitySe
private void closeSystemDialogs() {
sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
}
-
- /** Returns the given text, forced to be left-to-right. */
- private static CharSequence forceLeftToRight(CharSequence text) {
- StringBuilder sb = new StringBuilder();
- sb.append(LEFT_TO_RIGHT_EMBEDDING);
- sb.append(text);
- sb.append(POP_DIRECTIONAL_FORMATTING);
- return sb.toString();
- }
}
diff --git a/src/com/android/dialer/CallDetailHeader.java b/src/com/android/dialer/CallDetailHeader.java
new file mode 100644
index 000000000..70ef60ae2
--- /dev/null
+++ b/src/com/android/dialer/CallDetailHeader.java
@@ -0,0 +1,426 @@
+/*
+ * 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.
+ */
+
+package com.android.dialer;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.provider.Contacts.Intents.Insert;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.Contacts;
+import android.telecom.PhoneAccount;
+import android.telephony.PhoneNumberUtils;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import android.view.ActionMode;
+import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.android.contacts.common.CallUtil;
+import com.android.contacts.common.ClipboardUtils;
+import com.android.contacts.common.ContactPhotoManager;
+import com.android.contacts.common.format.FormatUtils;
+import com.android.contacts.common.util.Constants;
+import com.android.dialer.calllog.PhoneNumberDisplayHelper;
+import com.android.dialer.calllog.PhoneNumberUtilsWrapper;
+
+public class CallDetailHeader {
+ private static final char LEFT_TO_RIGHT_EMBEDDING = '\u202A';
+ private static final char POP_DIRECTIONAL_FORMATTING = '\u202C';
+
+ private Activity mActivity;
+ private Resources mResources;
+ private PhoneNumberDisplayHelper mPhoneNumberHelper;
+ private ContactPhotoManager mContactPhotoManager;
+
+ private String mNumber;
+
+ private TextView mHeaderTextView;
+ private View mHeaderOverlayView;
+ private ImageView mMainActionView;
+ private ImageButton mMainActionPushLayerView;
+ private ImageView mContactBackgroundView;
+
+ private ActionMode mPhoneNumberActionMode;
+ private boolean mHasEditNumberBeforeCallOption;
+ private boolean mCanPlaceCallsTo;
+
+ private CharSequence mPhoneNumberLabelToCopy;
+ private CharSequence mPhoneNumberToCopy;
+
+ public interface Data {
+ CharSequence getName();
+ CharSequence getNumber();
+ int getNumberPresentation();
+ int getNumberType();
+ CharSequence getNumberLabel();
+ CharSequence getFormattedNumber();
+ Uri getContactUri();
+ }
+
+ private final View.OnClickListener mPrimaryActionListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ if (finishPhoneNumerSelectedActionModeIfShown()) {
+ return;
+ }
+ mActivity.startActivity(((ViewEntry) view.getTag()).primaryIntent);
+ }
+ };
+
+ private final View.OnClickListener mSecondaryActionListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ if (finishPhoneNumerSelectedActionModeIfShown()) {
+ return;
+ }
+ mActivity.startActivity(((ViewEntry) view.getTag()).secondaryIntent);
+ }
+ };
+
+ private final View.OnLongClickListener mPrimaryLongClickListener =
+ new View.OnLongClickListener() {
+ @Override
+ public boolean onLongClick(View v) {
+ if (finishPhoneNumerSelectedActionModeIfShown()) {
+ return true;
+ }
+ startPhoneNumberSelectedActionMode(v);
+ return true;
+ }
+ };
+
+ public CallDetailHeader(Activity activity, PhoneNumberDisplayHelper phoneNumberHelper) {
+ mActivity = activity;
+ mResources = activity.getResources();
+ mPhoneNumberHelper = phoneNumberHelper;
+ mContactPhotoManager = ContactPhotoManager.getInstance(activity);
+
+ mHeaderTextView = (TextView) activity.findViewById(R.id.header_text);
+ mHeaderOverlayView = activity.findViewById(R.id.photo_text_bar);
+ mMainActionView = (ImageView) activity.findViewById(R.id.main_action);
+ mMainActionPushLayerView = (ImageButton) activity.findViewById(R.id.main_action_push_layer);
+ mContactBackgroundView = (ImageView) activity.findViewById(R.id.contact_background);
+ }
+
+ /**
+ * If the phone number is selected, unselect it and return {@code true}.
+ * Otherwise, just {@code false}.
+ */
+ private boolean finishPhoneNumerSelectedActionModeIfShown() {
+ if (mPhoneNumberActionMode == null) return false;
+ mPhoneNumberActionMode.finish();
+ return true;
+ }
+
+ private void startPhoneNumberSelectedActionMode(View targetView) {
+ mPhoneNumberActionMode =
+ mActivity.startActionMode(new PhoneNumberActionModeCallback(targetView));
+ }
+
+ private class PhoneNumberActionModeCallback implements ActionMode.Callback {
+ private final View mTargetView;
+ private final Drawable mOriginalViewBackground;
+
+ public PhoneNumberActionModeCallback(View targetView) {
+ mTargetView = targetView;
+
+ // Highlight the phone number view. Remember the old background, and put a new one.
+ mOriginalViewBackground = mTargetView.getBackground();
+ mTargetView.setBackgroundColor(mResources.getColor(R.color.item_selected));
+ }
+
+ @Override
+ public boolean onCreateActionMode(ActionMode mode, Menu menu) {
+ if (TextUtils.isEmpty(mPhoneNumberToCopy)) return false;
+
+ mActivity.getMenuInflater().inflate(R.menu.call_details_cab, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
+ return true;
+ }
+
+ @Override
+ public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.copy_phone_number:
+ ClipboardUtils.copyText(mActivity, mPhoneNumberLabelToCopy,
+ mPhoneNumberToCopy, true);
+ mode.finish(); // Close the CAB
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void onDestroyActionMode(ActionMode mode) {
+ mPhoneNumberActionMode = null;
+
+ // Restore the view background.
+ mTargetView.setBackground(mOriginalViewBackground);
+ }
+ }
+
+ public void updateViews(String number, int numberPresentation, Data data) {
+ // Cache the details about the phone number.
+ final PhoneNumberUtilsWrapper phoneUtils = new PhoneNumberUtilsWrapper();
+ final boolean isVoicemailNumber = phoneUtils.isVoicemailNumber(number);
+ final boolean isSipNumber = phoneUtils.isSipNumber(number);
+
+ final CharSequence dataName = data.getName();
+ final CharSequence dataNumber = data.getNumber();
+ final Uri contactUri = data.getContactUri();
+
+ mNumber = number;
+ mCanPlaceCallsTo = PhoneNumberUtilsWrapper.canPlaceCallsTo(number, numberPresentation);
+
+ // Let user view contact details if they exist, otherwise add option to create new
+ // contact from this number.
+ final Intent mainActionIntent;
+ final int mainActionIcon;
+ final String mainActionDescription;
+
+ final CharSequence nameOrNumber;
+ if (!TextUtils.isEmpty(dataName)) {
+ nameOrNumber = dataName;
+ } else {
+ nameOrNumber = dataNumber;
+ }
+
+ if (contactUri != null) {
+ mainActionIntent = new Intent(Intent.ACTION_VIEW, contactUri);
+ // This will launch People's detail contact screen, so we probably want to
+ // treat it as a separate People task.
+ mainActionIntent.setFlags(
+ Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ mainActionIcon = R.drawable.ic_contacts_holo_dark;
+ mainActionDescription =
+ mResources.getString(R.string.description_view_contact, nameOrNumber);
+ } else if (isVoicemailNumber) {
+ mainActionIntent = null;
+ mainActionIcon = 0;
+ mainActionDescription = null;
+ } else if (isSipNumber) {
+ // TODO: This item is currently disabled for SIP addresses, because
+ // the Insert.PHONE extra only works correctly for PSTN numbers.
+ //
+ // To fix this for SIP addresses, we need to:
+ // - define ContactsContract.Intents.Insert.SIP_ADDRESS, and use it here if
+ // the current number is a SIP address
+ // - update the contacts UI code to handle Insert.SIP_ADDRESS by
+ // updating the SipAddress field
+ // and then we can remove the "!isSipNumber" check above.
+ mainActionIntent = null;
+ mainActionIcon = 0;
+ mainActionDescription = null;
+ } else if (mCanPlaceCallsTo) {
+ mainActionIntent = new Intent(Intent.ACTION_INSERT_OR_EDIT);
+ mainActionIntent.setType(Contacts.CONTENT_ITEM_TYPE);
+ mainActionIntent.putExtra(Insert.PHONE, number);
+ mainActionIcon = R.drawable.ic_add_contact_holo_dark;
+ mainActionDescription = mResources.getString(R.string.description_add_contact);
+ } else {
+ // If we cannot call the number, when we probably cannot add it as a contact either.
+ // This is usually the case of private, unknown, or payphone numbers.
+ mainActionIntent = null;
+ mainActionIcon = 0;
+ mainActionDescription = null;
+ }
+
+ if (mainActionIntent == null) {
+ mMainActionView.setVisibility(View.INVISIBLE);
+ mMainActionPushLayerView.setVisibility(View.GONE);
+ mHeaderTextView.setVisibility(View.INVISIBLE);
+ mHeaderOverlayView.setVisibility(View.INVISIBLE);
+ } else {
+ mMainActionView.setVisibility(View.VISIBLE);
+ mMainActionView.setImageResource(mainActionIcon);
+ mMainActionPushLayerView.setVisibility(View.VISIBLE);
+ mMainActionPushLayerView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mActivity.startActivity(mainActionIntent);
+ }
+ });
+ mMainActionPushLayerView.setContentDescription(mainActionDescription);
+ mHeaderTextView.setVisibility(View.VISIBLE);
+ mHeaderOverlayView.setVisibility(View.VISIBLE);
+ }
+
+ // This action allows to call the number that places the call.
+ if (mCanPlaceCallsTo) {
+ final CharSequence displayNumber =
+ mPhoneNumberHelper.getDisplayNumber(
+ dataNumber, data.getNumberPresentation(), data.getFormattedNumber());
+
+ ViewEntry entry = new ViewEntry(
+ mResources.getString(R.string.menu_callNumber,
+ forceLeftToRight(displayNumber)),
+ CallUtil.getCallIntent(number),
+ mResources.getString(R.string.description_call, nameOrNumber));
+
+ // Only show a label if the number is shown and it is not a SIP address.
+ if (!TextUtils.isEmpty(dataName)
+ && !TextUtils.isEmpty(dataNumber)
+ && !PhoneNumberUtils.isUriNumber(dataNumber.toString())) {
+ entry.label = Phone.getTypeLabel(mResources, data.getNumberType(),
+ data.getNumberLabel());
+ }
+
+ // The secondary action allows to send an SMS to the number that placed the
+ // call.
+ if (phoneUtils.canSendSmsTo(number, numberPresentation)) {
+ entry.setSecondaryAction(
+ R.drawable.ic_text_holo_light,
+ new Intent(Intent.ACTION_SENDTO,
+ Uri.fromParts("sms", number, null)),
+ mResources.getString(R.string.description_send_text_message, nameOrNumber));
+ }
+
+ configureCallButton(entry);
+ mPhoneNumberToCopy = displayNumber;
+ mPhoneNumberLabelToCopy = entry.label;
+ } else {
+ disableCallButton();
+ mPhoneNumberToCopy = null;
+ mPhoneNumberLabelToCopy = null;
+ }
+
+ mHasEditNumberBeforeCallOption =
+ mCanPlaceCallsTo && !isSipNumber && !isVoicemailNumber;
+ }
+
+ /** Load the contact photos and places them in the corresponding views. */
+ public void loadContactPhotos(Uri photoUri) {
+ mContactPhotoManager.loadPhoto(mContactBackgroundView, photoUri,
+ null, mContactBackgroundView.getWidth(), false, true, null);
+ }
+
+ public boolean canEditNumberBeforeCall() {
+ return mHasEditNumberBeforeCallOption;
+ }
+
+ public boolean canPlaceCallsTo() {
+ return mCanPlaceCallsTo;
+ }
+
+ static final class ViewEntry {
+ public final String text;
+ public final Intent primaryIntent;
+ /** The description for accessibility of the primary action. */
+ public final String primaryDescription;
+
+ public CharSequence label = null;
+ /** Icon for the secondary action. */
+ public int secondaryIcon = 0;
+ /** Intent for the secondary action. If not null, an icon must be defined. */
+ public Intent secondaryIntent = null;
+ /** The description for accessibility of the secondary action. */
+ public String secondaryDescription = null;
+
+ public ViewEntry(String text, Intent intent, String description) {
+ this.text = text;
+ primaryIntent = intent;
+ primaryDescription = description;
+ }
+
+ public void setSecondaryAction(int icon, Intent intent, String description) {
+ secondaryIcon = icon;
+ secondaryIntent = intent;
+ secondaryDescription = description;
+ }
+ }
+
+ /** Disables the call button area, e.g., for private numbers. */
+ private void disableCallButton() {
+ mActivity.findViewById(R.id.call_and_sms).setVisibility(View.GONE);
+ }
+
+ /** Configures the call button area using the given entry. */
+ private void configureCallButton(ViewEntry entry) {
+ View convertView = mActivity.findViewById(R.id.call_and_sms);
+ convertView.setVisibility(View.VISIBLE);
+
+ ImageView icon = (ImageView) convertView.findViewById(R.id.call_and_sms_icon);
+ View divider = convertView.findViewById(R.id.call_and_sms_divider);
+ TextView text = (TextView) convertView.findViewById(R.id.call_and_sms_text);
+
+ View mainAction = convertView.findViewById(R.id.call_and_sms_main_action);
+ mainAction.setOnClickListener(mPrimaryActionListener);
+ mainAction.setTag(entry);
+ mainAction.setContentDescription(entry.primaryDescription);
+ mainAction.setOnLongClickListener(mPrimaryLongClickListener);
+
+ if (entry.secondaryIntent != null) {
+ icon.setOnClickListener(mSecondaryActionListener);
+ icon.setImageResource(entry.secondaryIcon);
+ icon.setVisibility(View.VISIBLE);
+ icon.setTag(entry);
+ icon.setContentDescription(entry.secondaryDescription);
+ divider.setVisibility(View.VISIBLE);
+ } else {
+ icon.setVisibility(View.GONE);
+ divider.setVisibility(View.GONE);
+ }
+ text.setText(entry.text);
+
+ TextView label = (TextView) convertView.findViewById(R.id.call_and_sms_label);
+ if (TextUtils.isEmpty(entry.label)) {
+ label.setVisibility(View.GONE);
+ } else {
+ label.setText(entry.label);
+ label.setVisibility(View.VISIBLE);
+ }
+ }
+
+ public boolean handleKeyDown(int keyCode, KeyEvent event) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_CALL: {
+ // Make sure phone isn't already busy before starting direct call
+ TelephonyManager tm = (TelephonyManager)
+ mActivity.getSystemService(Context.TELEPHONY_SERVICE);
+ if (tm.getCallState() == TelephonyManager.CALL_STATE_IDLE) {
+ mActivity.startActivity(CallUtil.getCallIntent(
+ Uri.fromParts(PhoneAccount.SCHEME_TEL, mNumber, null)));
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /** Returns the given text, forced to be left-to-right. */
+ private static CharSequence forceLeftToRight(CharSequence text) {
+ StringBuilder sb = new StringBuilder();
+ sb.append(LEFT_TO_RIGHT_EMBEDDING);
+ sb.append(text);
+ sb.append(POP_DIRECTIONAL_FORMATTING);
+ return sb.toString();
+ }
+}
diff --git a/src/com/android/dialer/PhoneCallDetails.java b/src/com/android/dialer/PhoneCallDetails.java
index 2de5f5f32..6bc75a5d7 100755
--- a/src/com/android/dialer/PhoneCallDetails.java
+++ b/src/com/android/dialer/PhoneCallDetails.java
@@ -26,7 +26,7 @@ import android.provider.ContactsContract.CommonDataKinds.Phone;
/**
* The details of a phone call to be shown in the UI.
*/
-public class PhoneCallDetails {
+public class PhoneCallDetails implements CallDetailHeader.Data {
/** The number of the other party involved in the call. */
public final CharSequence number;
/** The number presenting rules set by the network, e.g., {@link Calls#PRESENTATION_ALLOWED} */
@@ -154,4 +154,33 @@ public class PhoneCallDetails {
this.transcription = transcription;
this.durationType = durationType;
}
+
+ @Override
+ public CharSequence getName() {
+ return name;
+ }
+ @Override
+ public CharSequence getNumber() {
+ return number;
+ }
+ @Override
+ public int getNumberPresentation() {
+ return numberPresentation;
+ }
+ @Override
+ public int getNumberType() {
+ return numberType;
+ }
+ @Override
+ public CharSequence getNumberLabel() {
+ return numberLabel;
+ }
+ @Override
+ public CharSequence getFormattedNumber() {
+ return formattedNumber;
+ }
+ @Override
+ public Uri getContactUri() {
+ return contactUri;
+ }
}
diff --git a/src/com/android/dialer/calllog/CallLogActivity.java b/src/com/android/dialer/calllog/CallLogActivity.java
index d6ee030e5..10a77e10d 100755
--- a/src/com/android/dialer/calllog/CallLogActivity.java
+++ b/src/com/android/dialer/calllog/CallLogActivity.java
@@ -11,7 +11,7 @@
* 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.
+m * limitations under the License.
*/
package com.android.dialer.calllog;
@@ -50,14 +50,19 @@ import com.android.dialer.R;
import com.android.dialer.voicemail.VoicemailStatusHelper;
import com.android.dialer.voicemail.VoicemailStatusHelperImpl;
import com.android.dialerbind.analytics.AnalyticsActivity;
+import com.android.dialer.calllog.CallLogFragment;
+import com.android.dialer.callstats.CallStatsFragment;
+import com.android.dialer.widget.DoubleDatePickerDialog;
-public class CallLogActivity extends AnalyticsActivity implements CallLogQueryHandler.Listener {
+public class CallLogActivity extends AnalyticsActivity implements CallLogQueryHandler.Listener,
+ DoubleDatePickerDialog.OnDateSetListener {
private Handler mHandler;
private ViewPager mViewPager;
private ViewPagerTabs mViewPagerTabs;
private FragmentPagerAdapter mViewPagerAdapter;
private CallLogFragment mAllCallsFragment;
private CallLogFragment mMissedCallsFragment;
+ private CallStatsFragment mStatsFragment;
private CallLogFragment mVoicemailFragment;
private VoicemailStatusHelper mVoicemailStatusHelper;
@@ -72,10 +77,11 @@ public class CallLogActivity extends AnalyticsActivity implements CallLogQueryHa
private static final int TAB_INDEX_ALL = 0;
private static final int TAB_INDEX_MISSED = 1;
- private static final int TAB_INDEX_VOICEMAIL = 2;
+ private static final int TAB_INDEX_STATS = 2;
+ private static final int TAB_INDEX_VOICEMAIL = 3;
- private static final int TAB_INDEX_COUNT_DEFAULT = 2;
- private static final int TAB_INDEX_COUNT_WITH_VOICEMAIL = 3;
+ private static final int TAB_INDEX_COUNT_DEFAULT = 3;
+ private static final int TAB_INDEX_COUNT_WITH_VOICEMAIL = 4;
private boolean mHasActiveVoicemailProvider;
@@ -108,6 +114,9 @@ public class CallLogActivity extends AnalyticsActivity implements CallLogQueryHa
case TAB_INDEX_VOICEMAIL:
mVoicemailFragment = new CallLogFragment(Calls.VOICEMAIL_TYPE);
return mVoicemailFragment;
+ case TAB_INDEX_STATS:
+ mStatsFragment = new CallStatsFragment();
+ return mStatsFragment;
}
throw new IllegalStateException("No fragment at position " + position);
}
@@ -188,7 +197,8 @@ public class CallLogActivity extends AnalyticsActivity implements CallLogQueryHa
mTabTitles = new String[TAB_INDEX_COUNT_WITH_VOICEMAIL];
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_voicemail_title);
+ mTabTitles[2] = getString(R.string.call_log_stats_title);
+ mTabTitles[3] = getString(R.string.call_log_voicemail_title);
mViewPager = (ViewPager) findViewById(R.id.call_log_pager);
@@ -250,7 +260,6 @@ public class CallLogActivity extends AnalyticsActivity implements CallLogQueryHa
mViewPagerAdapter = new MSimViewPagerAdapter(getFragmentManager());
mViewPager.setAdapter(mViewPagerAdapter);
- mViewPager.setOffscreenPageLimit(1);
}
@Override
@@ -403,6 +412,8 @@ public class CallLogActivity extends AnalyticsActivity implements CallLogQueryHa
return mMissedCallsFragment;
case TAB_INDEX_VOICEMAIL:
return mVoicemailFragment;
+ case TAB_INDEX_STATS:
+ return mStatsFragment;
default:
throw new IllegalStateException("Unknown fragment index: "
+ position);
@@ -550,4 +561,9 @@ public class CallLogActivity extends AnalyticsActivity implements CallLogQueryHa
mSearchView.clearFocus();
mInSearchUi = false;
}
+
+ @Override
+ public void onDateSet(long from, long to) {
+ mStatsFragment.onDateSet(from, to);
+ }
}
diff --git a/src/com/android/dialer/calllog/CallLogAdapter.java b/src/com/android/dialer/calllog/CallLogAdapter.java
index fcce7736c..5fcb51638 100755
--- a/src/com/android/dialer/calllog/CallLogAdapter.java
+++ b/src/com/android/dialer/calllog/CallLogAdapter.java
@@ -24,8 +24,6 @@ import android.content.res.Resources;
import android.database.Cursor;
import android.graphics.drawable.Drawable;
import android.net.Uri;
-import android.os.Handler;
-import android.os.Message;
import android.provider.CallLog.Calls;
import android.provider.ContactsContract.PhoneLookup;
import android.telecom.PhoneAccountHandle;
@@ -53,6 +51,7 @@ import com.android.dialer.R;
import com.android.dialer.util.DialerUtils;
import com.android.dialer.util.ExpirableCache;
+import com.android.dialer.calllog.CallLogAdapterHelper.NumberWithCountryIso;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Objects;
@@ -63,7 +62,7 @@ import java.util.LinkedList;
* Adapter class to fill in data for the Call Log.
*/
public class CallLogAdapter extends GroupingListAdapter
- implements ViewTreeObserver.OnPreDrawListener, CallLogGroupBuilder.GroupCreator {
+ implements CallLogAdapterHelper.Callback, CallLogGroupBuilder.GroupCreator {
private static final int VOICEMAIL_TRANSCRIPTION_MAX_LINES = 10;
@@ -100,43 +99,6 @@ public class CallLogAdapter extends GroupingListAdapter
public void onReportButtonClick(String number);
}
- /**
- * Stores a phone number of a call with the country code where it originally occurred.
- * <p>
- * Note the country does not necessarily specifies the country of the phone number itself, but
- * it is the country in which the user was in when the call was placed or received.
- */
- private static final class NumberWithCountryIso {
- public final String number;
- public final String countryIso;
-
- public NumberWithCountryIso(String number, String countryIso) {
- this.number = number;
- this.countryIso = countryIso;
- }
-
- @Override
- public boolean equals(Object o) {
- if (o == null) return false;
- if (!(o instanceof NumberWithCountryIso)) return false;
- NumberWithCountryIso other = (NumberWithCountryIso) o;
- return TextUtils.equals(number, other.number)
- && TextUtils.equals(countryIso, other.countryIso);
- }
-
- @Override
- public int hashCode() {
- return (number == null ? 0 : number.hashCode())
- ^ (countryIso == null ? 0 : countryIso.hashCode());
- }
- }
-
- /** The time in millis to delay starting the thread processing requests. */
- private static final int START_PROCESSING_REQUESTS_DELAY_MILLIS = 1000;
-
- /** The size of the cache of contact info. */
- private static final int CONTACT_INFO_CACHE_SIZE = 100;
-
/** Constant used to indicate no row is expanded. */
private static final long NONE_EXPANDED = -1;
@@ -145,21 +107,10 @@ public class CallLogAdapter extends GroupingListAdapter
private final CallFetcher mCallFetcher;
private final Toast mReportedToast;
private final OnReportButtonClickListener mOnReportButtonClickListener;
- private ViewTreeObserver mViewTreeObserver = null;
private String mFilterString;
/**
- * A cache of the contact details for the phone numbers in the call log.
- * <p>
- * The content of the cache is expired (but not purged) whenever the application comes to
- * the foreground.
- * <p>
- * The key is number with the country in which the call was placed or received.
- */
- private ExpirableCache<NumberWithCountryIso, ContactInfo> mContactInfoCache;
-
- /**
* Tracks the call log row which was previously expanded. Used so that the closure of a
* previously expanded call log entry can be animated on rebind.
*/
@@ -184,65 +135,7 @@ public class CallLogAdapter extends GroupingListAdapter
*/
private HashMap<Long,Integer> mDayGroups = new HashMap<Long, Integer>();
- /**
- * A request for contact details for the given number.
- */
- private static final class ContactInfoRequest {
- /** The number to look-up. */
- public final String number;
- /** The country in which a call to or from this number was placed or received. */
- public final String countryIso;
- /** The cached contact information stored in the call log. */
- public final ContactInfo callLogInfo;
-
- public ContactInfoRequest(String number, String countryIso, ContactInfo callLogInfo) {
- this.number = number;
- this.countryIso = countryIso;
- this.callLogInfo = callLogInfo;
- }
-
- @Override
- public boolean equals(Object obj) {
- if (this == obj) return true;
- if (obj == null) return false;
- if (!(obj instanceof ContactInfoRequest)) return false;
-
- ContactInfoRequest other = (ContactInfoRequest) obj;
-
- if (!TextUtils.equals(number, other.number)) return false;
- if (!TextUtils.equals(countryIso, other.countryIso)) return false;
- if (!Objects.equal(callLogInfo, other.callLogInfo)) return false;
-
- return true;
- }
-
- @Override
- public int hashCode() {
- final int prime = 31;
- int result = 1;
- result = prime * result + ((callLogInfo == null) ? 0 : callLogInfo.hashCode());
- result = prime * result + ((countryIso == null) ? 0 : countryIso.hashCode());
- result = prime * result + ((number == null) ? 0 : number.hashCode());
- return result;
- }
- }
-
- /**
- * List of requests to update contact details.
- * <p>
- * Each request is made of a phone number to look up, and the contact info currently stored in
- * the call log for this number.
- * <p>
- * The requests are added when displaying the contacts and are processed by a background
- * thread.
- */
- private final LinkedList<ContactInfoRequest> mRequests;
-
private boolean mLoading = true;
- private static final int REDRAW = 1;
- private static final int START_THREAD = 2;
-
- private QueryThread mCallerIdThread;
/** Instance of helper class for managing views. */
private final CallLogListItemHelper mCallLogViewsHelper;
@@ -256,8 +149,7 @@ public class CallLogAdapter extends GroupingListAdapter
private CallItemExpandedListener mCallItemExpandedListener;
- /** Can be set to true by tests to disable processing of requests. */
- private volatile boolean mRequestProcessingDisabled = false;
+ private final CallLogAdapterHelper mAdapterHelper;
private boolean mIsCallLog = true;
@@ -315,34 +207,6 @@ public class CallLogAdapter extends GroupingListAdapter
}
}
- @Override
- public boolean onPreDraw() {
- // We only wanted to listen for the first draw (and this is it).
- unregisterPreDrawListener();
-
- // Only schedule a thread-creation message if the thread hasn't been
- // created yet. This is purely an optimization, to queue fewer messages.
- if (mCallerIdThread == null) {
- mHandler.sendEmptyMessageDelayed(START_THREAD, START_PROCESSING_REQUESTS_DELAY_MILLIS);
- }
-
- return true;
- }
-
- private Handler mHandler = new Handler() {
- @Override
- public void handleMessage(Message msg) {
- switch (msg.what) {
- case REDRAW:
- notifyDataSetChanged();
- break;
- case START_THREAD:
- startRequestProcessing();
- break;
- }
- }
- };
-
public CallLogAdapter(Context context, CallFetcher callFetcher,
ContactInfoHelper contactInfoHelper, CallItemExpandedListener callItemExpandedListener,
OnReportButtonClickListener onReportButtonClickListener, boolean isCallLog) {
@@ -358,9 +222,6 @@ public class CallLogAdapter extends GroupingListAdapter
mReportedToast = Toast.makeText(mContext, R.string.toast_caller_id_reported,
Toast.LENGTH_SHORT);
- mContactInfoCache = ExpirableCache.create(CONTACT_INFO_CACHE_SIZE);
- mRequests = new LinkedList<ContactInfoRequest>();
-
Resources resources = mContext.getResources();
CallTypeHelper callTypeHelper = new CallTypeHelper(resources);
mCallLogBackgroundColor = resources.getColor(R.color.background_dialer_list_items);
@@ -369,6 +230,8 @@ public class CallLogAdapter extends GroupingListAdapter
mContactPhotoManager = ContactPhotoManager.getInstance(mContext);
mPhoneNumberHelper = new PhoneNumberDisplayHelper(resources);
+ mAdapterHelper = new CallLogAdapterHelper(context, this,
+ contactInfoHelper, mPhoneNumberHelper);
PhoneCallDetailsHelper phoneCallDetailsHelper = new PhoneCallDetailsHelper(
resources, callTypeHelper, new PhoneNumberUtilsWrapper());
mCallLogViewsHelper =
@@ -399,177 +262,6 @@ public class CallLogAdapter extends GroupingListAdapter
}
}
- /**
- * Starts a background thread to process contact-lookup requests, unless one
- * has already been started.
- */
- private synchronized void startRequestProcessing() {
- // For unit-testing.
- if (mRequestProcessingDisabled) return;
-
- // Idempotence... if a thread is already started, don't start another.
- if (mCallerIdThread != null) return;
-
- mCallerIdThread = new QueryThread();
- mCallerIdThread.setPriority(Thread.MIN_PRIORITY);
- mCallerIdThread.start();
- }
-
- /**
- * Stops the background thread that processes updates and cancels any
- * pending requests to start it.
- */
- public synchronized void stopRequestProcessing() {
- // Remove any pending requests to start the processing thread.
- mHandler.removeMessages(START_THREAD);
- if (mCallerIdThread != null) {
- // Stop the thread; we are finished with it.
- mCallerIdThread.stopProcessing();
- mCallerIdThread.interrupt();
- mCallerIdThread = null;
- }
- }
-
- /**
- * Stop receiving onPreDraw() notifications.
- */
- private void unregisterPreDrawListener() {
- if (mViewTreeObserver != null && mViewTreeObserver.isAlive()) {
- mViewTreeObserver.removeOnPreDrawListener(this);
- }
- mViewTreeObserver = null;
- }
-
- public void invalidateCache() {
- mContactInfoCache.expireAll();
-
- // Restart the request-processing thread after the next draw.
- stopRequestProcessing();
- unregisterPreDrawListener();
- }
-
- /**
- * Enqueues a request to look up the contact details for the given phone number.
- * <p>
- * It also provides the current contact info stored in the call log for this number.
- * <p>
- * If the {@code immediate} parameter is true, it will start immediately the thread that looks
- * up the contact information (if it has not been already started). Otherwise, it will be
- * started with a delay. See {@link #START_PROCESSING_REQUESTS_DELAY_MILLIS}.
- */
- protected void enqueueRequest(String number, String countryIso, ContactInfo callLogInfo,
- boolean immediate) {
- ContactInfoRequest request = new ContactInfoRequest(number, countryIso, callLogInfo);
- synchronized (mRequests) {
- if (!mRequests.contains(request)) {
- mRequests.add(request);
- mRequests.notifyAll();
- }
- }
- if (immediate) startRequestProcessing();
- }
-
- /**
- * Queries the appropriate content provider for the contact associated with the number.
- * <p>
- * Upon completion it also updates the cache in the call log, if it is different from
- * {@code callLogInfo}.
- * <p>
- * The number might be either a SIP address or a phone number.
- * <p>
- * It returns true if it updated the content of the cache and we should therefore tell the
- * view to update its content.
- */
- private boolean queryContactInfo(String number, String countryIso, ContactInfo callLogInfo) {
- final ContactInfo info = mContactInfoHelper.lookupNumber(number, countryIso);
-
- if (info == null) {
- // The lookup failed, just return without requesting to update the view.
- return false;
- }
-
- // Check the existing entry in the cache: only if it has changed we should update the
- // view.
- NumberWithCountryIso numberCountryIso = new NumberWithCountryIso(number, countryIso);
- ContactInfo existingInfo = mContactInfoCache.getPossiblyExpired(numberCountryIso);
-
- final boolean isRemoteSource = info.sourceType != 0;
-
- // Don't force redraw if existing info in the cache is equal to {@link ContactInfo#EMPTY}
- // to avoid updating the data set for every new row that is scrolled into view.
- // see (https://googleplex-android-review.git.corp.google.com/#/c/166680/)
-
- // Exception: Photo uris for contacts from remote sources are not cached in the call log
- // cache, so we have to force a redraw for these contacts regardless.
- boolean updated = (existingInfo != ContactInfo.EMPTY || isRemoteSource) &&
- !info.equals(existingInfo);
-
- // Store the data in the cache so that the UI thread can use to display it. Store it
- // even if it has not changed so that it is marked as not expired.
- mContactInfoCache.put(numberCountryIso, info);
- // Update the call log even if the cache it is up-to-date: it is possible that the cache
- // contains the value from a different call log entry.
- updateCallLogContactInfoCache(number, countryIso, info, callLogInfo);
- return updated;
- }
-
- /*
- * Handles requests for contact name and number type.
- */
- private class QueryThread extends Thread {
- private volatile boolean mDone = false;
-
- public QueryThread() {
- super("CallLogAdapter.QueryThread");
- }
-
- public void stopProcessing() {
- mDone = true;
- }
-
- @Override
- public void run() {
- boolean needRedraw = false;
- while (true) {
- // Check if thread is finished, and if so return immediately.
- if (mDone) return;
-
- // Obtain next request, if any is available.
- // Keep synchronized section small.
- ContactInfoRequest req = null;
- synchronized (mRequests) {
- if (!mRequests.isEmpty()) {
- req = mRequests.removeFirst();
- }
- }
-
- if (req != null) {
- // Process the request. If the lookup succeeds, schedule a
- // redraw.
- needRedraw |= queryContactInfo(req.number, req.countryIso, req.callLogInfo);
- } else {
- // Throttle redraw rate by only sending them when there are
- // more requests.
- if (needRedraw) {
- needRedraw = false;
- mHandler.sendEmptyMessage(REDRAW);
- }
-
- // Wait until another request is available, or until this
- // thread is no longer needed (as indicated by being
- // interrupted).
- try {
- synchronized (mRequests) {
- mRequests.wait(1000);
- }
- } catch (InterruptedException ie) {
- // Ignore, and attempt to continue processing requests.
- }
- }
- }
- }
- }
-
@Override
protected void addGroups(Cursor cursor) {
mCallLogGroupBuilder.addGroups(cursor);
@@ -705,42 +397,8 @@ public class CallLogAdapter extends GroupingListAdapter
}
// Lookup contacts with this number
- NumberWithCountryIso numberCountryIso = new NumberWithCountryIso(number, countryIso);
- ExpirableCache.CachedValue<ContactInfo> cachedInfo =
- mContactInfoCache.getCachedValue(numberCountryIso);
- ContactInfo info = cachedInfo == null ? null : cachedInfo.getValue();
- if (!PhoneNumberUtilsWrapper.canPlaceCallsTo(number, numberPresentation)
- || isVoicemailNumber) {
- // If this is a number that cannot be dialed, there is no point in looking up a contact
- // for it.
- info = ContactInfo.EMPTY;
- } else if (cachedInfo == null) {
- mContactInfoCache.put(numberCountryIso, ContactInfo.EMPTY);
- // Use the cached contact info from the call log.
- info = cachedContactInfo;
- // The db request should happen on a non-UI thread.
- // Request the contact details immediately since they are currently missing.
- enqueueRequest(number, countryIso, cachedContactInfo, true);
- // We will format the phone number when we make the background request.
- } else {
- if (cachedInfo.isExpired()) {
- // The contact info is no longer up to date, we should request it. However, we
- // do not need to request them immediately.
- enqueueRequest(number, countryIso, cachedContactInfo, false);
- } else if (!callLogInfoMatches(cachedContactInfo, info)) {
- // The call log information does not match the one we have, look it up again.
- // We could simply update the call log directly, but that needs to be done in a
- // background thread, so it is easier to simply request a new lookup, which will, as
- // a side-effect, update the call log.
- enqueueRequest(number, countryIso, cachedContactInfo, false);
- }
-
- if (info == ContactInfo.EMPTY) {
- // Use the cached contact info from the call log.
- info = cachedContactInfo;
- }
- }
-
+ final ContactInfo info = mAdapterHelper.lookupContact(
+ number, numberPresentation, countryIso, cachedContactInfo);
final Uri lookupUri = info.lookupUri;
final String name = info.name;
final int ntype = info.type;
@@ -818,10 +476,7 @@ public class CallLogAdapter extends GroupingListAdapter
}
// Listen for the first draw
- if (mViewTreeObserver == null) {
- mViewTreeObserver = view.getViewTreeObserver();
- mViewTreeObserver.addOnPreDrawListener(this);
- }
+ mAdapterHelper.registerOnPreDrawListener(view);
bindBadge(view, info, details, callType);
}
@@ -1095,17 +750,14 @@ public class CallLogAdapter extends GroupingListAdapter
}
}
- /** Checks whether the contact info from the call log matches the one from the contacts db. */
- private boolean callLogInfoMatches(ContactInfo callLogInfo, ContactInfo info) {
- // The call log only contains a subset of the fields in the contacts db.
- // Only check those.
- return TextUtils.equals(callLogInfo.name, info.name)
- && callLogInfo.type == info.type
- && TextUtils.equals(callLogInfo.label, info.label);
+ @Override
+ public void dataSetChanged() {
+ notifyDataSetChanged();
}
/** Stores the updated contact info in the call log if it is different from the current one. */
- private void updateCallLogContactInfoCache(String number, String countryIso,
+ @Override
+ public void updateContactInfo(String number, String countryIso,
ContactInfo updatedInfo, ContactInfo callLogInfo) {
final ContentValues values = new ContentValues();
boolean needsUpdate = false;
@@ -1268,13 +920,18 @@ public class CallLogAdapter extends GroupingListAdapter
*/
@VisibleForTesting
void disableRequestProcessingForTest() {
- mRequestProcessingDisabled = true;
+ mAdapterHelper.disableRequestProcessingForTest();
}
@VisibleForTesting
void injectContactInfoForTest(String number, String countryIso, ContactInfo contactInfo) {
- NumberWithCountryIso numberCountryIso = new NumberWithCountryIso(number, countryIso);
- mContactInfoCache.put(numberCountryIso, contactInfo);
+ mAdapterHelper.injectContactInfoForTest(number, countryIso, contactInfo);
+ }
+
+ @VisibleForTesting
+ void enqueueRequest(String number, String countryIso, ContactInfo callLogInfo,
+ boolean immediate) {
+ mAdapterHelper.enqueueRequest(number, countryIso, callLogInfo, immediate);
}
@Override
@@ -1303,46 +960,16 @@ public class CallLogAdapter extends GroupingListAdapter
mDayGroups.clear();
}
- /*
- * Get the number from the Contacts, if available, since sometimes
- * the number provided by caller id may not be formatted properly
- * depending on the carrier (roaming) in use at the time of the
- * incoming call.
- * Logic : If the caller-id number starts with a "+", use it
- * Else if the number in the contacts starts with a "+", use that one
- * Else if the number in the contacts is longer, use that one
- */
+ public void stopRequestProcessing() {
+ mAdapterHelper.stopRequestProcessing();
+ }
+
+ public void invalidateCache() {
+ mAdapterHelper.invalidateCache();
+ }
+
public String getBetterNumberFromContacts(String number, String countryIso) {
- String matchingNumber = null;
- // Look in the cache first. If it's not found then query the Phones db
- NumberWithCountryIso numberCountryIso = new NumberWithCountryIso(number, countryIso);
- ContactInfo ci = mContactInfoCache.getPossiblyExpired(numberCountryIso);
- if (ci != null && ci != ContactInfo.EMPTY) {
- matchingNumber = ci.number;
- } else {
- try {
- Cursor phonesCursor = mContext.getContentResolver().query(
- Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, number),
- PhoneQuery._PROJECTION, null, null, null);
- if (phonesCursor != null) {
- try {
- if (phonesCursor.moveToFirst()) {
- matchingNumber = phonesCursor.getString(PhoneQuery.MATCHED_NUMBER);
- }
- } finally {
- phonesCursor.close();
- }
- }
- } catch (Exception e) {
- // Use the number from the call log
- }
- }
- if (!TextUtils.isEmpty(matchingNumber) &&
- (matchingNumber.startsWith("+")
- || matchingNumber.length() > number.length())) {
- number = matchingNumber;
- }
- return number;
+ return mAdapterHelper.getBetterNumberFromContacts(number, countryIso);
}
/**
@@ -1382,7 +1009,6 @@ public class CallLogAdapter extends GroupingListAdapter
}
public void onBadDataReported(String number) {
- mContactInfoCache.expireAll();
mReportedToast.show();
}
diff --git a/src/com/android/dialer/calllog/CallLogAdapterHelper.java b/src/com/android/dialer/calllog/CallLogAdapterHelper.java
new file mode 100644
index 000000000..a16935cfe
--- /dev/null
+++ b/src/com/android/dialer/calllog/CallLogAdapterHelper.java
@@ -0,0 +1,474 @@
+/*
+ * 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.calllog;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Message;
+import android.provider.ContactsContract.PhoneLookup;
+import android.text.TextUtils;
+import android.view.View;
+import android.view.ViewTreeObserver;
+
+import com.android.dialer.util.ExpirableCache;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Objects;
+
+import java.util.LinkedList;
+
+/**
+ * Adapter class to fill in data for the Call Log.
+ */
+public class CallLogAdapterHelper implements ViewTreeObserver.OnPreDrawListener {
+ public interface Callback {
+ void dataSetChanged();
+ void updateContactInfo(String number, String countryIso,
+ ContactInfo updatedInfo, ContactInfo callLogInfo);
+ }
+
+ /**
+ * Stores a phone number of a call with the country code where it originally occurred.
+ * <p>
+ * Note the country does not necessarily specifies the country of the phone number itself, but
+ * it is the country in which the user was in when the call was placed or received.
+ */
+ public static final class NumberWithCountryIso {
+ public final String number;
+ public final String countryIso;
+
+ public NumberWithCountryIso(String number, String countryIso) {
+ this.number = number;
+ this.countryIso = countryIso;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == null) return false;
+ if (!(o instanceof NumberWithCountryIso)) return false;
+ NumberWithCountryIso other = (NumberWithCountryIso) o;
+ return TextUtils.equals(number, other.number)
+ && TextUtils.equals(countryIso, other.countryIso);
+ }
+
+ @Override
+ public int hashCode() {
+ return (number == null ? 0 : number.hashCode())
+ ^ (countryIso == null ? 0 : countryIso.hashCode());
+ }
+ }
+
+ /**
+ * A request for contact details for the given number.
+ */
+ private static final class ContactInfoRequest {
+ /** The number to look-up. */
+ public final String number;
+ /** The country in which a call to or from this number was placed or received. */
+ public final String countryIso;
+ /** The cached contact information stored in the call log. */
+ public final ContactInfo callLogInfo;
+
+ public ContactInfoRequest(String number, String countryIso, ContactInfo callLogInfo) {
+ this.number = number;
+ this.countryIso = countryIso;
+ this.callLogInfo = callLogInfo;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) return true;
+ if (obj == null) return false;
+ if (!(obj instanceof ContactInfoRequest)) return false;
+
+ ContactInfoRequest other = (ContactInfoRequest) obj;
+
+ if (!TextUtils.equals(number, other.number)) return false;
+ if (!TextUtils.equals(countryIso, other.countryIso)) return false;
+ if (!Objects.equal(callLogInfo, other.callLogInfo)) return false;
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((callLogInfo == null) ? 0 : callLogInfo.hashCode());
+ result = prime * result + ((countryIso == null) ? 0 : countryIso.hashCode());
+ result = prime * result + ((number == null) ? 0 : number.hashCode());
+ return result;
+ }
+ }
+
+ /*
+ * Handles requests for contact name and number type.
+ */
+ private class QueryThread extends Thread {
+ private volatile boolean mDone = false;
+
+ public QueryThread() {
+ super("CallLogAdapter.QueryThread");
+ }
+
+ public void stopProcessing() {
+ mDone = true;
+ }
+
+ @Override
+ public void run() {
+ boolean needRedraw = false;
+ while (true) {
+ // Check if thread is finished, and if so return immediately.
+ if (mDone) return;
+
+ // Obtain next request, if any is available.
+ // Keep synchronized section small.
+ ContactInfoRequest req = null;
+ synchronized (mRequests) {
+ if (!mRequests.isEmpty()) {
+ req = mRequests.removeFirst();
+ }
+ }
+
+ if (req != null) {
+ // Process the request. If the lookup succeeds, schedule a
+ // redraw.
+ needRedraw |= queryContactInfo(req.number, req.countryIso, req.callLogInfo);
+ } else {
+ // Throttle redraw rate by only sending them when there are
+ // more requests.
+ if (needRedraw) {
+ needRedraw = false;
+ mHandler.sendEmptyMessage(REDRAW);
+ }
+
+ // Wait until another request is available, or until this
+ // thread is no longer needed (as indicated by being
+ // interrupted).
+ try {
+ synchronized (mRequests) {
+ mRequests.wait(1000);
+ }
+ } catch (InterruptedException ie) {
+ // Ignore, and attempt to continue processing requests.
+ }
+ }
+ }
+ }
+ }
+
+ private static final int REDRAW = 1;
+ private static final int START_THREAD = 2;
+
+ /** The time in millis to delay starting the thread processing requests. */
+ private static final int START_PROCESSING_REQUESTS_DELAY_MILLIS = 1000;
+
+ /** The size of the cache of contact info. */
+ private static final int CONTACT_INFO_CACHE_SIZE = 100;
+
+ private Callback mCb;
+ private final Context mContext;
+ private final ContactInfoHelper mContactInfoHelper;
+ private final PhoneNumberDisplayHelper mPhoneNumberHelper;
+
+ /**
+ * A cache of the contact details for the phone numbers in the call log.
+ * <p>
+ * The content of the cache is expired (but not purged) whenever the application comes to
+ * the foreground.
+ * <p>
+ * The key is number with the country in which the call was placed or received.
+ */
+ private ExpirableCache<NumberWithCountryIso, ContactInfo> mContactInfoCache;
+
+ private QueryThread mCallerIdThread;
+ /** Can be set to true by tests to disable processing of requests. */
+ private volatile boolean mRequestProcessingDisabled = false;
+
+ /**
+ * List of requests to update contact details.
+ * <p>
+ * Each request is made of a phone number to look up, and the contact info currently stored in
+ * the call log for this number.
+ * <p>
+ * The requests are added when displaying the contacts and are processed by a background
+ * thread.
+ */
+ private final LinkedList<ContactInfoRequest> mRequests;
+
+ private ViewTreeObserver mViewTreeObserver = null;
+
+ private Handler mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case REDRAW:
+ mCb.dataSetChanged();
+ break;
+ case START_THREAD:
+ startRequestProcessing();
+ break;
+ }
+ }
+ };
+
+ /**
+ * Enqueues a request to look up the contact details for the given phone number.
+ * <p>
+ * It also provides the current contact info stored in the call log for this number.
+ * <p>
+ * If the {@code immediate} parameter is true, it will start immediately the thread that looks
+ * up the contact information (if it has not been already started). Otherwise, it will be
+ * started with a delay. See {@link #START_PROCESSING_REQUESTS_DELAY_MILLIS}.
+ */
+ @VisibleForTesting
+ void enqueueRequest(String number, String countryIso, ContactInfo callLogInfo,
+ boolean immediate) {
+ ContactInfoRequest request = new ContactInfoRequest(number, countryIso, callLogInfo);
+ synchronized (mRequests) {
+ if (!mRequests.contains(request)) {
+ mRequests.add(request);
+ mRequests.notifyAll();
+ }
+ }
+ if (immediate) startRequestProcessing();
+ }
+
+ @Override
+ public boolean onPreDraw() {
+ // We only wanted to listen for the first draw (and this is it).
+ unregisterPreDrawListener();
+
+ // Only schedule a thread-creation message if the thread hasn't been
+ // created yet. This is purely an optimization, to queue fewer messages.
+ if (mCallerIdThread == null) {
+ mHandler.sendEmptyMessageDelayed(START_THREAD, START_PROCESSING_REQUESTS_DELAY_MILLIS);
+ }
+
+ return true;
+ }
+
+ public void registerOnPreDrawListener(View v) {
+ // Listen for the first draw
+ if (mViewTreeObserver == null) {
+ mViewTreeObserver = v.getViewTreeObserver();
+ mViewTreeObserver.addOnPreDrawListener(this);
+ }
+ }
+
+ /**
+ * Stop receiving onPreDraw() notifications.
+ */
+ private void unregisterPreDrawListener() {
+ if (mViewTreeObserver != null && mViewTreeObserver.isAlive()) {
+ mViewTreeObserver.removeOnPreDrawListener(this);
+ }
+ mViewTreeObserver = null;
+ }
+
+ public void invalidateCache() {
+ mContactInfoCache.expireAll();
+
+ // Restart the request-processing thread after the next draw.
+ stopRequestProcessing();
+ unregisterPreDrawListener();
+ }
+
+ /**
+ * Starts a background thread to process contact-lookup requests, unless one
+ * has already been started.
+ */
+ private synchronized void startRequestProcessing() {
+ // For unit-testing.
+ if (mRequestProcessingDisabled) return;
+
+ // Idempotence... if a thread is already started, don't start another.
+ if (mCallerIdThread != null) return;
+
+ mCallerIdThread = new QueryThread();
+ mCallerIdThread.setPriority(Thread.MIN_PRIORITY);
+ mCallerIdThread.start();
+ }
+
+ /**
+ * Stops the background thread that processes updates and cancels any
+ * pending requests to start it.
+ */
+ public synchronized void stopRequestProcessing() {
+ // Remove any pending requests to start the processing thread.
+ mHandler.removeMessages(START_THREAD);
+ if (mCallerIdThread != null) {
+ // Stop the thread; we are finished with it.
+ mCallerIdThread.stopProcessing();
+ mCallerIdThread.interrupt();
+ mCallerIdThread = null;
+ }
+ }
+
+ /**
+ * Queries the appropriate content provider for the contact associated with the number.
+ * <p>
+ * Upon completion it also updates the cache in the call log, if it is different from
+ * {@code callLogInfo}.
+ * <p>
+ * The number might be either a SIP address or a phone number.
+ * <p>
+ * It returns true if it updated the content of the cache and we should therefore tell the
+ * view to update its content.
+ */
+ private boolean queryContactInfo(String number, String countryIso, ContactInfo callLogInfo) {
+ final ContactInfo info = mContactInfoHelper.lookupNumber(number, countryIso);
+
+ if (info == null) {
+ // The lookup failed, just return without requesting to update the view.
+ return false;
+ }
+
+ // Check the existing entry in the cache: only if it has changed we should update the
+ // view.
+ NumberWithCountryIso numberCountryIso = new NumberWithCountryIso(number, countryIso);
+ ContactInfo existingInfo = mContactInfoCache.getPossiblyExpired(numberCountryIso);
+ boolean updated = (existingInfo != ContactInfo.EMPTY) && !info.equals(existingInfo);
+
+ // Store the data in the cache so that the UI thread can use to display it. Store it
+ // even if it has not changed so that it is marked as not expired.
+ mContactInfoCache.put(numberCountryIso, info);
+ mCb.updateContactInfo(number, countryIso, info, callLogInfo);
+ return updated;
+ }
+
+ /*
+ * Get the number from the Contacts, if available, since sometimes
+ * the number provided by caller id may not be formatted properly
+ * depending on the carrier (roaming) in use at the time of the
+ * incoming call.
+ * Logic : If the caller-id number starts with a "+", use it
+ * Else if the number in the contacts starts with a "+", use that one
+ * Else if the number in the contacts is longer, use that one
+ */
+ public String getBetterNumberFromContacts(String number, String countryIso) {
+ String matchingNumber = null;
+ // Look in the cache first. If it's not found then query the Phones db
+ NumberWithCountryIso numberCountryIso = new NumberWithCountryIso(number, countryIso);
+ ContactInfo ci = mContactInfoCache.getPossiblyExpired(numberCountryIso);
+ if (ci != null && ci != ContactInfo.EMPTY) {
+ matchingNumber = ci.number;
+ } else {
+ try {
+ Cursor phonesCursor = mContext.getContentResolver().query(
+ Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, number),
+ PhoneQuery._PROJECTION, null, null, null);
+ if (phonesCursor != null) {
+ if (phonesCursor.moveToFirst()) {
+ matchingNumber = phonesCursor.getString(PhoneQuery.MATCHED_NUMBER);
+ }
+ phonesCursor.close();
+ }
+ } catch (Exception e) {
+ // Use the number from the call log
+ }
+ }
+ if (!TextUtils.isEmpty(matchingNumber) &&
+ (matchingNumber.startsWith("+")
+ || matchingNumber.length() > number.length())) {
+ number = matchingNumber;
+ }
+ return number;
+ }
+
+ /** Checks whether the contact info from the call log matches the one from the contacts db. */
+ private boolean callLogInfoMatches(ContactInfo callLogInfo, ContactInfo info) {
+ // The call log only contains a subset of the fields in the contacts db.
+ // Only check those.
+ return TextUtils.equals(callLogInfo.name, info.name)
+ && callLogInfo.type == info.type
+ && TextUtils.equals(callLogInfo.label, info.label);
+ }
+
+
+ public ContactInfo lookupContact(String number, int numberPresentation,
+ String countryIso, ContactInfo cachedContactInfo) {
+ NumberWithCountryIso numberCountryIso = new NumberWithCountryIso(number, countryIso);
+ ExpirableCache.CachedValue<ContactInfo> cachedInfo =
+ mContactInfoCache.getCachedValue(numberCountryIso);
+ ContactInfo info = cachedInfo == null ? null : cachedInfo.getValue();
+ if (!PhoneNumberUtilsWrapper.canPlaceCallsTo(number, numberPresentation)
+ || new PhoneNumberUtilsWrapper().isVoicemailNumber(number)) {
+ // If this is a number that cannot be dialed, there is no point in looking up a contact
+ // for it.
+ info = ContactInfo.EMPTY;
+ } else if (cachedInfo == null) {
+ mContactInfoCache.put(numberCountryIso, ContactInfo.EMPTY);
+ // Use the cached contact info from the call log.
+ info = cachedContactInfo;
+ // The db request should happen on a non-UI thread.
+ // Request the contact details immediately since they are currently missing.
+ enqueueRequest(number, countryIso, cachedContactInfo, true);
+ // We will format the phone number when we make the background request.
+ } else {
+ if (cachedInfo.isExpired()) {
+ // The contact info is no longer up to date, we should request it. However, we
+ // do not need to request them immediately.
+ enqueueRequest(number, countryIso, cachedContactInfo, false);
+ } else if (!callLogInfoMatches(cachedContactInfo, info)) {
+ // The call log information does not match the one we have, look it up again.
+ // We could simply update the call log directly, but that needs to be done in a
+ // background thread, so it is easier to simply request a new lookup, which will, as
+ // a side-effect, update the call log.
+ enqueueRequest(number, countryIso, cachedContactInfo, false);
+ }
+
+ if (info == ContactInfo.EMPTY) {
+ // Use the cached contact info from the call log.
+ info = cachedContactInfo;
+ }
+ }
+
+ return info;
+ }
+
+ public CallLogAdapterHelper(Context context, Callback cb,
+ ContactInfoHelper contactInfoHelper,
+ PhoneNumberDisplayHelper phoneNumberHelper) {
+ mContext = context;
+ mCb = cb;
+ mContactInfoHelper = contactInfoHelper;
+ mPhoneNumberHelper = phoneNumberHelper;
+
+ mContactInfoCache = ExpirableCache.create(CONTACT_INFO_CACHE_SIZE);
+ mRequests = new LinkedList<ContactInfoRequest>();
+ }
+
+ /**
+ * Sets whether processing of requests for contact details should be enabled.
+ * <p>
+ * This method should be called in tests to disable such processing of requests when not
+ * needed.
+ */
+ @VisibleForTesting
+ void disableRequestProcessingForTest() {
+ mRequestProcessingDisabled = true;
+ }
+
+ @VisibleForTesting
+ void injectContactInfoForTest(String number, String countryIso, ContactInfo contactInfo) {
+ NumberWithCountryIso numberCountryIso = new NumberWithCountryIso(number, countryIso);
+ mContactInfoCache.put(numberCountryIso, contactInfo);
+ }
+}
diff --git a/src/com/android/dialer/calllog/ContactInfoHelper.java b/src/com/android/dialer/calllog/ContactInfoHelper.java
index 68654f28c..3274a9df5 100644
--- a/src/com/android/dialer/calllog/ContactInfoHelper.java
+++ b/src/com/android/dialer/calllog/ContactInfoHelper.java
@@ -15,6 +15,7 @@
package com.android.dialer.calllog;
import android.content.ContentUris;
+import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
@@ -24,13 +25,17 @@ import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.DisplayNameSources;
import android.provider.ContactsContract.PhoneLookup;
import android.provider.ContactsContract.RawContacts;
+import android.provider.Settings;
+import android.provider.Telephony;
import android.telephony.PhoneNumberUtils;
import android.text.TextUtils;
+import android.widget.Toast;
import com.android.contacts.common.util.Constants;
import com.android.contacts.common.util.PhoneNumberHelper;
import com.android.contacts.common.util.UriUtils;
import com.android.dialer.lookup.LookupCache;
+import com.android.dialer.R;
import com.android.dialer.service.CachedNumberLookupService;
import com.android.dialer.service.CachedNumberLookupService.CachedContactInfo;
import com.android.dialerbind.ObjectFactory;
@@ -324,7 +329,34 @@ public class ContactInfoHelper {
public boolean canReportAsInvalid(int sourceType, String objectId) {
return mCachedNumberLookupService != null
&& mCachedNumberLookupService.canReportAsInvalid(sourceType, objectId);
+ }
+
+ /**
+ * Checks whether calls can be blacklisted; that is, whether the
+ * phone blacklist is enabled
+ */
+ public boolean canBlacklistCalls() {
+ return Settings.System.getInt(mContext.getContentResolver(),
+ Settings.System.PHONE_BLACKLIST_ENABLED, 1) != 0;
}
+ /**
+ * Requests the given number to be added to the phone blacklist
+ *
+ * @param number the number to be blacklisted
+ */
+ public void addNumberToBlacklist(String number) {
+ ContentValues cv = new ContentValues();
+ cv.put(Telephony.Blacklist.PHONE_MODE, 1);
+
+ Uri uri = Uri.withAppendedPath(Telephony.Blacklist.CONTENT_FILTER_BYNUMBER_URI, number);
+ int count = mContext.getContentResolver().update(uri, cv, null, null);
+
+ if (count != 0) {
+ // Give the user some feedback
+ String message = mContext.getString(R.string.toast_added_to_blacklist, number);
+ Toast.makeText(mContext, message, Toast.LENGTH_SHORT).show();
+ }
+ }
}
diff --git a/src/com/android/dialer/callstats/CallStatsAdapter.java b/src/com/android/dialer/callstats/CallStatsAdapter.java
new file mode 100644
index 000000000..82ddde78d
--- /dev/null
+++ b/src/com/android/dialer/callstats/CallStatsAdapter.java
@@ -0,0 +1,249 @@
+/*
+ * 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.res.Resources;
+import android.net.Uri;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+
+import com.android.contacts.common.ContactPhotoManager;
+import com.android.contacts.common.CallUtil;
+import com.android.contacts.common.GeoUtil;
+import com.android.dialer.R;
+import com.android.dialer.calllog.CallLogAdapterHelper;
+import com.android.dialer.calllog.ContactInfo;
+import com.android.dialer.calllog.ContactInfoHelper;
+import com.android.dialer.calllog.PhoneNumberDisplayHelper;
+import com.android.dialer.calllog.PhoneNumberUtilsWrapper;
+
+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 ArrayAdapter<CallStatsDetails>
+ implements CallLogAdapterHelper.Callback {
+ /** Interface used to initiate a refresh of the content. */
+ public interface CallDataLoader {
+ public boolean isDataLoaded();
+ }
+
+ private final View.OnClickListener mPrimaryActionListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ IntentProvider intentProvider = (IntentProvider) view.getTag();
+ if (intentProvider != null) {
+ mContext.startActivity(intentProvider.getIntent(mContext));
+ }
+ }
+ };
+
+ private final Context mContext;
+ private final CallDataLoader mDataLoader;
+ private final CallLogAdapterHelper mAdapterHelper;
+ private final CallStatsDetailHelper mCallStatsDetailHelper;
+
+ private ArrayList<CallStatsDetails> mAllItems;
+ private CallStatsDetails mTotalItem;
+ private Map<ContactInfo, CallStatsDetails> mInfoLookup;
+
+ private int mType = CallStatsQueryHandler.CALL_TYPE_ALL;
+ private long mFilterFrom;
+ private long mFilterTo;
+ private boolean mSortByDuration;
+
+ private final ContactPhotoManager mContactPhotoManager;
+
+ 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, CallDataLoader dataLoader) {
+ super(context, R.layout.call_stats_list_item, R.id.number);
+
+ mContext = context;
+ mDataLoader = dataLoader;
+
+ setNotifyOnChange(false);
+
+ mAllItems = new ArrayList<CallStatsDetails>();
+ mTotalItem = new CallStatsDetails(null, 0, null, null, null, 0);
+ mInfoLookup = new ConcurrentHashMap<ContactInfo, CallStatsDetails>();
+
+ Resources resources = mContext.getResources();
+ PhoneNumberDisplayHelper phoneNumberHelper = new PhoneNumberDisplayHelper(resources);
+
+ final String currentCountryIso = GeoUtil.getCurrentCountryIso(mContext);
+ final ContactInfoHelper contactInfoHelper =
+ new ContactInfoHelper(mContext, currentCountryIso);
+
+ mAdapterHelper = new CallLogAdapterHelper(mContext, this,
+ contactInfoHelper, phoneNumberHelper);
+ mContactPhotoManager = ContactPhotoManager.getInstance(mContext);
+ mCallStatsDetailHelper = new CallStatsDetailHelper(resources,
+ new PhoneNumberUtilsWrapper());
+ }
+
+ 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);
+ mAdapterHelper.lookupContact(call.number, call.numberPresentation,
+ call.countryIso, entry.getKey());
+ }
+ }
+
+ public void updateDisplayedData(int type, boolean sortByDuration) {
+ mType = type;
+ mSortByDuration = sortByDuration;
+ Collections.sort(mAllItems, sortByDuration ? mDurationComparator : mCountComparator);
+
+ clear();
+
+ for (CallStatsDetails call : mAllItems) {
+ if (sortByDuration && call.getRequestedDuration(type) > 0) {
+ add(call);
+ } else if (!sortByDuration && call.getRequestedCount(type) > 0) {
+ add(call);
+ }
+ }
+
+ notifyDataSetChanged();
+ }
+
+ public void stopRequestProcessing() {
+ mAdapterHelper.stopRequestProcessing();
+ }
+
+ public String getBetterNumberFromContacts(String number, String countryIso) {
+ return mAdapterHelper.getBetterNumberFromContacts(number, countryIso);
+ }
+
+ public void invalidateCache() {
+ mAdapterHelper.invalidateCache();
+ }
+
+ public String getTotalCallCountString() {
+ return CallStatsDetailHelper.getCallCountString(
+ mContext.getResources(), mTotalItem.getRequestedCount(mType));
+ }
+
+ public String getFullDurationString(boolean withSeconds) {
+ final long duration = mTotalItem.getRequestedDuration(mType);
+ return CallStatsDetailHelper.getDurationString(
+ mContext.getResources(), duration, withSeconds);
+ }
+
+ @Override
+ public boolean isEmpty() {
+ if (!mDataLoader.isDataLoaded()) {
+ return false;
+ }
+ return super.isEmpty();
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ View v = convertView;
+ if (v == null) {
+ LayoutInflater inflater = (LayoutInflater)
+ getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ v = inflater.inflate(R.layout.call_stats_list_item, parent, false);
+ }
+
+ findAndCacheViews(v);
+ bindView(position, v);
+
+ return v;
+ }
+
+ private void bindView(int position, View v) {
+ final CallStatsListItemViews views = (CallStatsListItemViews) v.getTag();
+ final CallStatsDetails details = getItem(position);
+ final CallStatsDetails first = getItem(0);
+
+ views.primaryActionView.setVisibility(View.VISIBLE);
+ views.primaryActionView.setTag(IntentProvider.getCallStatsDetailIntentProvider(
+ details, mFilterFrom, mFilterTo, mSortByDuration));
+
+ mCallStatsDetailHelper.setCallStatsDetails(views.callStatsDetailViews,
+ details, first, mTotalItem, mType, mSortByDuration);
+ setPhoto(views, details.photoId, details.contactUri);
+
+ // Listen for the first draw
+ mAdapterHelper.registerOnPreDrawListener(v);
+ }
+
+ private void findAndCacheViews(View view) {
+ CallStatsListItemViews views = CallStatsListItemViews.fromView(view);
+ views.primaryActionView.setOnClickListener(mPrimaryActionListener);
+ view.setTag(views);
+ }
+
+ private void setPhoto(CallStatsListItemViews views, long photoId, Uri contactUri) {
+ views.quickContactView.assignContactUri(contactUri);
+ mContactPhotoManager.loadThumbnail(views.quickContactView, photoId, null,
+ false, true, null, null);
+ }
+
+ @Override
+ public void dataSetChanged() {
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public void updateContactInfo(String number, String countryIso,
+ ContactInfo updatedInfo, ContactInfo callLogInfo) {
+ CallStatsDetails details = mInfoLookup.get(callLogInfo);
+ if (details != null) {
+ details.updateFromInfo(updatedInfo);
+ }
+ }
+}
diff --git a/src/com/android/dialer/callstats/CallStatsDetailActivity.java b/src/com/android/dialer/callstats/CallStatsDetailActivity.java
new file mode 100644
index 000000000..dc64c2e74
--- /dev/null
+++ b/src/com/android/dialer/callstats/CallStatsDetailActivity.java
@@ -0,0 +1,274 @@
+/*
+ * 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.os.Bundle;
+import android.provider.CallLog.Calls;
+import android.os.AsyncTask;
+import android.text.format.DateUtils;
+import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.TextView;
+
+import com.android.contacts.common.CallUtil;
+import com.android.contacts.common.GeoUtil;
+import com.android.dialer.CallDetailHeader;
+import com.android.dialer.R;
+import com.android.dialer.calllog.ContactInfo;
+import com.android.dialer.calllog.ContactInfoHelper;
+import com.android.dialer.calllog.PhoneNumberDisplayHelper;
+import com.android.dialer.calllog.PhoneNumberUtilsWrapper;
+import com.android.dialer.widget.PieChartView;
+
+/**
+ * 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_FROM = "from";
+ public static final String EXTRA_TO = "to";
+ public static final String EXTRA_BY_DURATION = "by_duration";
+
+ private CallStatsDetailHelper mCallStatsDetailHelper;
+ private ContactInfoHelper mContactInfoHelper;
+ private CallDetailHeader mCallDetailHeader;
+ private Resources mResources;
+
+ private TextView mHeaderTextView;
+ private TextView mTotalSummary;
+ private TextView mTotalDuration;
+ private TextView mInSummary;
+ private TextView mInCount;
+ private TextView mInDuration;
+ private TextView mOutSummary;
+ private TextView mOutCount;
+ private TextView mOutDuration;
+ private TextView mMissedSummary;
+ private TextView mMissedCount;
+ private PieChartView mPieChart;
+
+ private CallStatsDetails mData;
+ private String mNumber = null;
+
+ private class UpdateContactTask extends AsyncTask<String, Void, ContactInfo> {
+ protected ContactInfo doInBackground(String... strings) {
+ ContactInfo info = mContactInfoHelper.lookupNumber(strings[0], strings[1]);
+ return info;
+ }
+
+ 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();
+
+ PhoneNumberDisplayHelper phoneNumberHelper = new PhoneNumberDisplayHelper(mResources);
+ mCallDetailHeader = new CallDetailHeader(this, phoneNumberHelper);
+ mCallStatsDetailHelper = new CallStatsDetailHelper(mResources,
+ new PhoneNumberUtilsWrapper());
+ mContactInfoHelper = new ContactInfoHelper(this, GeoUtil.getCurrentCountryIso(this));
+
+ mHeaderTextView = (TextView) findViewById(R.id.header_text);
+ mTotalSummary = (TextView) findViewById(R.id.total_summary);
+ mTotalDuration = (TextView) findViewById(R.id.total_duration);
+ mInSummary = (TextView) findViewById(R.id.in_summary);
+ mInCount = (TextView) findViewById(R.id.in_count);
+ mInDuration = (TextView) findViewById(R.id.in_duration);
+ mOutSummary = (TextView) findViewById(R.id.out_summary);
+ mOutCount = (TextView) findViewById(R.id.out_count);
+ mOutDuration = (TextView) findViewById(R.id.out_duration);
+ mMissedSummary = (TextView) findViewById(R.id.missed_summary);
+ mMissedCount = (TextView) findViewById(R.id.missed_count);
+ mPieChart = (PieChartView) findViewById(R.id.pie_chart);
+
+ configureActionBar();
+ Intent launchIntent = getIntent();
+ mData = (CallStatsDetails) launchIntent.getParcelableExtra(EXTRA_DETAILS);
+
+ 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, DateUtils.FORMAT_ABBREV_ALL));
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ new UpdateContactTask().execute(mData.number.toString(), mData.countryIso);
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ if (mCallDetailHeader.handleKeyDown(keyCode, event)) {
+ return true;
+ }
+
+ return super.onKeyDown(keyCode, event);
+ }
+
+ private void updateData() {
+ mNumber = mData.number.toString();
+
+ // Set the details header, based on the first phone call.
+ mCallStatsDetailHelper.setCallStatsDetailHeader(mHeaderTextView, mData);
+ mCallDetailHeader.updateViews(mNumber, mData.numberPresentation, mData);
+ mCallDetailHeader.loadContactPhotos(mData.photoUri);
+ invalidateOptionsMenu();
+
+ mPieChart.setOriginAngle(240);
+ mPieChart.removeAllSlices();
+
+ boolean byDuration = getIntent().getBooleanExtra(EXTRA_BY_DURATION, true);
+
+ mTotalSummary.setText(getString(R.string.call_stats_header_total_callsonly,
+ CallStatsDetailHelper.getCallCountString(mResources, mData.getTotalCount())));
+ mTotalDuration.setText(CallStatsDetailHelper.getDurationString(
+ mResources, mData.getFullDuration(), true));
+
+ if (shouldDisplay(Calls.INCOMING_TYPE, byDuration)) {
+ int percent = byDuration
+ ? mData.getDurationPercentage(Calls.INCOMING_TYPE)
+ : mData.getCountPercentage(Calls.INCOMING_TYPE);
+
+ mInSummary.setText(getString(R.string.call_stats_incoming, percent));
+ mInCount.setText(CallStatsDetailHelper.getCallCountString(
+ mResources, mData.incomingCount));
+ mInDuration.setText(CallStatsDetailHelper.getDurationString(
+ mResources, mData.inDuration, true));
+ mPieChart.addSlice(byDuration ? mData.inDuration : mData.incomingCount,
+ mResources.getColor(R.color.call_stats_incoming));
+ } else {
+ findViewById(R.id.in_container).setVisibility(View.GONE);
+ }
+
+ if (shouldDisplay(Calls.OUTGOING_TYPE, byDuration)) {
+ int percent = byDuration
+ ? mData.getDurationPercentage(Calls.OUTGOING_TYPE)
+ : mData.getCountPercentage(Calls.OUTGOING_TYPE);
+
+ mOutSummary.setText(getString(R.string.call_stats_outgoing, percent));
+ mOutCount.setText(CallStatsDetailHelper.getCallCountString(
+ mResources, mData.outgoingCount));
+ mOutDuration.setText(CallStatsDetailHelper.getDurationString(
+ mResources, mData.outDuration, true));
+ mPieChart.addSlice(byDuration ? mData.outDuration : mData.outgoingCount,
+ mResources.getColor(R.color.call_stats_outgoing));
+ } else {
+ findViewById(R.id.out_container).setVisibility(View.GONE);
+ }
+
+ if (shouldDisplay(Calls.MISSED_TYPE, false)) {
+ final String missedCount =
+ CallStatsDetailHelper.getCallCountString(mResources, mData.missedCount);
+
+ if (byDuration) {
+ mMissedSummary.setText(getString(R.string.call_stats_missed));
+ } else {
+ mMissedSummary.setText(getString(R.string.call_stats_missed_percent,
+ mData.getCountPercentage(Calls.MISSED_TYPE)));
+ mPieChart.addSlice(mData.missedCount, mResources.getColor(R.color.call_stats_missed));
+ }
+ mMissedCount.setText(CallStatsDetailHelper.getCallCountString(
+ mResources, mData.missedCount));
+ } else {
+ findViewById(R.id.missed_container).setVisibility(View.GONE);
+ }
+
+ mPieChart.generatePath();
+ findViewById(R.id.call_stats_detail).setVisibility(View.VISIBLE);
+ }
+
+ private boolean shouldDisplay(int type, boolean byDuration) {
+ if (byDuration) {
+ return mData.getRequestedDuration(type) != 0;
+ } else {
+ return mData.getRequestedCount(type) != 0;
+ }
+ }
+
+ @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(
+ mCallDetailHeader.canEditNumberBeforeCall());
+ 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 configureActionBar() {
+ ActionBar actionBar = getActionBar();
+ if (actionBar != null) {
+ actionBar.setDisplayOptions(ActionBar.DISPLAY_HOME_AS_UP
+ | ActionBar.DISPLAY_SHOW_HOME);
+ }
+ }
+
+ 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();
+ }
+}
diff --git a/src/com/android/dialer/callstats/CallStatsDetailHelper.java b/src/com/android/dialer/callstats/CallStatsDetailHelper.java
new file mode 100644
index 000000000..d9b3ea0f2
--- /dev/null
+++ b/src/com/android/dialer/callstats/CallStatsDetailHelper.java
@@ -0,0 +1,174 @@
+/*
+ * 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.res.Resources;
+import android.provider.CallLog.Calls;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
+import android.view.View;
+import android.widget.TextView;
+
+import com.android.dialer.R;
+import com.android.dialer.calllog.PhoneNumberDisplayHelper;
+import com.android.dialer.calllog.PhoneNumberUtilsWrapper;
+
+/**
+ * Class used to populate a detailed view for a callstats item
+ */
+public class CallStatsDetailHelper {
+
+ private final Resources mResources;
+ private final PhoneNumberDisplayHelper mPhoneNumberHelper;
+ private final PhoneNumberUtilsWrapper mPhoneNumberUtilsWrapper;
+
+ public CallStatsDetailHelper(Resources resources, PhoneNumberUtilsWrapper phoneUtils) {
+ mResources = resources;
+ mPhoneNumberHelper = new PhoneNumberDisplayHelper(resources);
+ mPhoneNumberUtilsWrapper = phoneUtils;
+ }
+
+ public void setCallStatsDetails(CallStatsDetailViews views,
+ CallStatsDetails details, CallStatsDetails first, CallStatsDetails total,
+ int type, boolean byDuration) {
+
+ CharSequence numberFormattedLabel = null;
+ // Only show a label if the number is shown and it is not a SIP address.
+ if (!TextUtils.isEmpty(details.number)
+ && !PhoneNumberUtils.isUriNumber(details.number.toString())) {
+ numberFormattedLabel = Phone.getTypeLabel(mResources,
+ details.numberType, details.numberLabel);
+ }
+
+ final CharSequence nameText;
+ final CharSequence numberText;
+ final CharSequence labelText;
+ final CharSequence displayNumber = mPhoneNumberHelper.getDisplayNumber(
+ details.number, details.numberPresentation, details.formattedNumber);
+
+ if (TextUtils.isEmpty(details.name)) {
+ nameText = displayNumber;
+ if (TextUtils.isEmpty(details.geocode)
+ || mPhoneNumberUtilsWrapper.isVoicemailNumber(details.number)) {
+ numberText = mResources.getString(R.string.call_log_empty_gecode);
+ } else {
+ numberText = details.geocode;
+ }
+ labelText = null;
+ } else {
+ nameText = details.name;
+ numberText = displayNumber;
+ labelText = numberFormattedLabel;
+ }
+
+ float in = 0, out = 0, missed = 0;
+ float ratio = getDetailValue(details, type, byDuration) /
+ getDetailValue(first, type, byDuration);
+
+ if (type == CallStatsQueryHandler.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;
+ }
+ } else if (type == Calls.INCOMING_TYPE) {
+ in = ratio;
+ } else if (type == Calls.OUTGOING_TYPE) {
+ out = ratio;
+ } else if (type == Calls.MISSED_TYPE) {
+ missed = ratio;
+ }
+
+ views.barView.setRatios(in, out, missed);
+ views.nameView.setText(nameText);
+ views.numberView.setText(numberText);
+ views.labelView.setText(labelText);
+ views.labelView.setVisibility(TextUtils.isEmpty(labelText) ? View.GONE : View.VISIBLE);
+
+ if (byDuration && type == Calls.MISSED_TYPE) {
+ views.percentView.setText(getCallCountString(mResources, details.missedCount));
+ } else {
+ float percent = getDetailValue(details, type, byDuration) * 100F /
+ getDetailValue(total, type, byDuration);
+ views.percentView.setText(String.format("%.1f%%", percent));
+ }
+ }
+
+ private float getDetailValue(CallStatsDetails details, int type, boolean byDuration) {
+ if (byDuration) {
+ return (float) details.getRequestedDuration(type);
+ } else {
+ return (float) details.getRequestedCount(type);
+ }
+ }
+
+ public void setCallStatsDetailHeader(TextView nameView, CallStatsDetails details) {
+ final CharSequence nameText;
+ final CharSequence displayNumber = mPhoneNumberHelper.getDisplayNumber(
+ details.number, details.numberPresentation,
+ mResources.getString(R.string.recentCalls_addToContact));
+
+ if (TextUtils.isEmpty(details.name)) {
+ nameText = displayNumber;
+ } else {
+ nameText = details.name;
+ }
+
+ nameView.setText(nameText);
+ }
+
+ public static String getCallCountString(Resources res, long count) {
+ return res.getQuantityString(R.plurals.call, (int) count, (int) count);
+ }
+
+ public static String getDurationString(Resources res, 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 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);
+ }
+}
diff --git a/src/com/android/dialer/callstats/CallStatsDetailViews.java b/src/com/android/dialer/callstats/CallStatsDetailViews.java
new file mode 100644
index 000000000..ea20f7235
--- /dev/null
+++ b/src/com/android/dialer/callstats/CallStatsDetailViews.java
@@ -0,0 +1,50 @@
+/*
+ * 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.view.View;
+import android.widget.TextView;
+
+import com.android.dialer.R;
+import com.android.dialer.widget.LinearColorBar;
+
+public final class CallStatsDetailViews {
+ public final TextView nameView;
+ public final TextView numberView;
+ public final TextView labelView;
+ public final TextView percentView;
+ public final LinearColorBar barView;
+
+ private CallStatsDetailViews(TextView nameView, TextView numberView,
+ TextView labelView, TextView percentView, LinearColorBar barView) {
+ this.nameView = nameView;
+ this.numberView = numberView;
+ this.labelView = labelView;
+ this.percentView = percentView;
+ this.barView = barView;
+ }
+
+ public static CallStatsDetailViews fromView(View view) {
+ return new CallStatsDetailViews(
+ (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));
+ }
+}
diff --git a/src/com/android/dialer/callstats/CallStatsDetails.java b/src/com/android/dialer/callstats/CallStatsDetails.java
new file mode 100644
index 000000000..377b5149c
--- /dev/null
+++ b/src/com/android/dialer/callstats/CallStatsDetails.java
@@ -0,0 +1,238 @@
+/*
+ * 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.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.provider.CallLog.Calls;
+import android.util.Log;
+
+import com.android.dialer.CallDetailHeader;
+import com.android.dialer.calllog.ContactInfo;
+
+/**
+ * Class to store statistical details for a given contact/number.
+ */
+public class CallStatsDetails implements CallDetailHeader.Data, 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 CallStatsDetails(CharSequence number, int numberPresentation,
+ 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);
+ }
+ }
+
+ @Override
+ public CharSequence getName() {
+ return name;
+ }
+ @Override
+ public CharSequence getNumber() {
+ return number;
+ }
+ @Override
+ public int getNumberPresentation() {
+ return numberPresentation;
+ }
+ @Override
+ public int getNumberType() {
+ return numberType;
+ }
+ @Override
+ public CharSequence getNumberLabel() {
+ return numberLabel;
+ }
+ @Override
+ public CharSequence getFormattedNumber() {
+ return formattedNumber;
+ }
+ @Override
+ public Uri getContactUri() {
+ return contactUri;
+ }
+
+ 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;
+ }
+
+ public long getFullDuration() {
+ return inDuration + outDuration;
+ }
+
+ public int getTotalCount() {
+ return incomingCount + outgoingCount + missedCount;
+ }
+
+ 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;
+ }
+ }
+
+ 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;
+ 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;
+ 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;
+ }
+
+ public void reset() {
+ this.inDuration = this.outDuration = 0;
+ this.incomingCount = this.outgoingCount = this.missedCount = 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);
+ }
+
+ 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();
+ }
+}
diff --git a/src/com/android/dialer/callstats/CallStatsFragment.java b/src/com/android/dialer/callstats/CallStatsFragment.java
new file mode 100644
index 000000000..e2790b0b2
--- /dev/null
+++ b/src/com/android/dialer/callstats/CallStatsFragment.java
@@ -0,0 +1,340 @@
+/*
+ * 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.ListFragment;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.provider.CallLog;
+import android.provider.ContactsContract;
+import android.telecom.PhoneAccount;
+import android.telephony.PhoneNumberUtils;
+import android.text.format.DateUtils;
+import android.text.TextUtils;
+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.ImageView;
+import android.widget.Spinner;
+import android.widget.TextView;
+
+import com.android.contacts.common.CallUtil;
+import com.android.contacts.common.util.Constants;
+import com.android.dialer.DialtactsActivity;
+import com.android.dialer.R;
+import com.android.dialer.calllog.ContactInfo;
+import com.android.dialer.calllog.PhoneNumberUtilsWrapper;
+import com.android.dialer.widget.DoubleDatePickerDialog;
+import com.android.internal.telephony.CallerInfo;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+public class CallStatsFragment extends ListFragment implements
+ CallStatsAdapter.CallDataLoader, CallStatsQueryHandler.Listener,
+ AdapterView.OnItemSelectedListener, DoubleDatePickerDialog.OnDateSetListener {
+ private static final String TAG = "CallStatsFragment";
+
+ private static final int[] CALL_DIRECTION_RESOURCES = new int[] {
+ R.drawable.ic_call_inout_holo_dark,
+ R.drawable.ic_call_incoming_holo_dark,
+ R.drawable.ic_call_outgoing_holo_dark,
+ R.drawable.ic_call_missed_holo_dark
+ };
+
+ private String[] mNavItems;
+ private Spinner mFilterSpinner;
+
+ private int mCallTypeFilter = CallStatsQueryHandler.CALL_TYPE_ALL;
+ private long mFilterFrom = -1;
+ private long mFilterTo = -1;
+ private boolean mSortByDuration = true;
+ private boolean mDataLoaded = false;
+
+ private CallStatsAdapter mAdapter;
+ private CallStatsQueryHandler mCallStatsQueryHandler;
+
+ private TextView mSumHeaderView;
+ private TextView mDateFilterView;
+
+ private boolean mRefreshDataRequired = true;
+ private final ContentObserver mObserver = new ContentObserver(new Handler()) {
+ @Override
+ public void onChange(boolean selfChange) {
+ mRefreshDataRequired = true;
+ }
+ };
+
+ public class CallStatsNavAdapter extends ArrayAdapter<String> {
+ public CallStatsNavAdapter(Context context, int textResourceId, Object[] objects) {
+ super(context, textResourceId, mNavItems);
+ }
+
+ @Override
+ public View getDropDownView(int position, View convertView, ViewGroup parent) {
+ return getCustomView(position, convertView, parent);
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ return getCustomView(position, convertView, parent);
+ }
+
+ public View getCustomView(int position, View convertView, ViewGroup parent) {
+ if (convertView == null) {
+ convertView = getLayoutInflater(null).inflate(
+ R.layout.call_stats_nav_item, parent, false);
+ }
+
+ TextView label = (TextView) convertView.findViewById(R.id.call_stats_nav_text);
+ label.setText(mNavItems[position]);
+
+ ImageView icon = (ImageView) convertView.findViewById(R.id.call_stats_nav_icon);
+ icon.setImageResource(CALL_DIRECTION_RESOURCES[position]);
+
+ return convertView;
+ }
+ }
+
+ @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);
+ mSumHeaderView = (TextView) view.findViewById(R.id.sum_header);
+ mDateFilterView = (TextView) view.findViewById(R.id.date_filter);
+ return view;
+ }
+
+ @Override
+ public void onViewCreated(View view, Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ mAdapter = new CallStatsAdapter(getActivity(), this);
+ setListAdapter(mAdapter);
+ getListView().setItemsCanFocus(true);
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ 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);
+ final MenuItem filterItem = menu.findItem(R.id.filter);
+
+ resetItem.setVisible(mFilterFrom != -1);
+ sortDurationItem.setVisible(!mSortByDuration);
+ sortCountItem.setVisible(mSortByDuration);
+
+ mFilterSpinner = (Spinner) filterItem.getActionView();
+ mNavItems = getResources().getStringArray(R.array.call_stats_nav_items);
+ CallStatsNavAdapter filterAdapter = new CallStatsNavAdapter(getActivity(),
+ android.R.layout.simple_list_item_1, mNavItems);
+ mFilterSpinner.setAdapter(filterAdapter);
+ mFilterSpinner.setOnItemSelectedListener(this);
+
+ 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();
+ 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) {
+ mCallTypeFilter = pos;
+ mAdapter.updateDisplayedData(mCallTypeFilter, mSortByDuration);
+ if (mDataLoaded) {
+ updateHeader();
+ }
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView<?> parent) {
+ }
+
+ @Override
+ public void onDateSet(long from, long to) {
+ mFilterFrom = from;
+ mFilterTo = to;
+ getActivity().invalidateOptionsMenu();
+ fetchCalls();
+ }
+
+ /**
+ * 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();
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ refreshData();
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ // Kill the requests thread
+ mAdapter.stopRequestProcessing();
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ mAdapter.stopRequestProcessing();
+ getActivity().getContentResolver().unregisterContentObserver(mObserver);
+ }
+
+ @Override
+ public boolean isDataLoaded() {
+ return mDataLoaded;
+ }
+
+ private void fetchCalls() {
+ mCallStatsQueryHandler.fetchCalls(mFilterFrom, mFilterTo);
+ }
+
+ 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));
+ }
+
+ if (mFilterFrom == -1) {
+ mDateFilterView.setVisibility(View.GONE);
+ } else {
+ mDateFilterView.setText(DateUtils.formatDateRange(getActivity(),
+ mFilterFrom, mFilterTo, 0));
+ mDateFilterView.setVisibility(View.VISIBLE);
+ }
+
+ getView().findViewById(R.id.call_stats_header).setVisibility(View.VISIBLE);
+ }
+
+ public void callSelectedEntry() {
+ int position = getListView().getSelectedItemPosition();
+ if (position < 0) {
+ // In touch mode you may often not have something selected, so
+ // just call the first entry to make sure that [send] calls
+ // the most recent entry.
+ position = 0;
+ }
+ final CallStatsDetails item = mAdapter.getItem(position);
+ String number = (String) item.number;
+
+ if (!PhoneNumberUtilsWrapper.canPlaceCallsTo(number, item.numberPresentation)) {
+ // This number can't be called, do nothing
+ return;
+ }
+
+ Uri callUri;
+ // If "number" is really a SIP address, construct a sip: URI.
+ if (PhoneNumberUtils.isUriNumber(number)) {
+ callUri = Uri.fromParts(PhoneAccount.SCHEME_SIP, number, null);
+ } else {
+ if (!number.startsWith("+")) {
+ // If the caller-id matches a contact with a better qualified
+ // number, use it
+ number = mAdapter.getBetterNumberFromContacts(number, item.countryIso);
+ }
+ callUri = Uri.fromParts(PhoneAccount.SCHEME_TEL, number, null);
+ }
+
+ final Intent intent = CallUtil.getCallIntent(callUri);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
+ | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
+ startActivity(intent);
+ }
+
+ /** 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;
+ }
+ }
+}
diff --git a/src/com/android/dialer/callstats/CallStatsListItemViews.java b/src/com/android/dialer/callstats/CallStatsListItemViews.java
new file mode 100644
index 000000000..4ebf247e9
--- /dev/null
+++ b/src/com/android/dialer/callstats/CallStatsListItemViews.java
@@ -0,0 +1,55 @@
+/*
+ * 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.view.View;
+import android.widget.QuickContactBadge;
+
+import com.android.dialer.R;
+
+/**
+ * Simple value object containing the various views within a call stat entry.
+ */
+public final class CallStatsListItemViews {
+ /** The quick contact badge for the contact. */
+ public final QuickContactBadge quickContactView;
+ /** The primary action view of the entry. */
+ public final View primaryActionView;
+ /** The details of the phone call. */
+ public final CallStatsDetailViews callStatsDetailViews;
+ /** The divider to be shown below items. */
+ public final View bottomDivider;
+
+ private CallStatsListItemViews(QuickContactBadge quickContactView, View primaryActionView,
+ CallStatsDetailViews callStatsDetailViews,
+ View bottomDivider) {
+ this.quickContactView = quickContactView;
+ this.primaryActionView = primaryActionView;
+ this.callStatsDetailViews = callStatsDetailViews;
+ this.bottomDivider = bottomDivider;
+ }
+
+ public static CallStatsListItemViews fromView(View view) {
+ return new CallStatsListItemViews(
+ (QuickContactBadge) view.findViewById(R.id.quick_contact_photo),
+ view.findViewById(R.id.primary_action_view),
+ CallStatsDetailViews.fromView(view),
+ view.findViewById(R.id.call_stats_divider));
+ }
+
+}
diff --git a/src/com/android/dialer/callstats/CallStatsQuery.java b/src/com/android/dialer/callstats/CallStatsQuery.java
new file mode 100644
index 000000000..390bbfcab
--- /dev/null
+++ b/src/com/android/dialer/callstats/CallStatsQuery.java
@@ -0,0 +1,59 @@
+/*
+ * 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
+ };
+
+ 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;
+}
diff --git a/src/com/android/dialer/callstats/CallStatsQueryHandler.java b/src/com/android/dialer/callstats/CallStatsQueryHandler.java
new file mode 100644
index 000000000..f3590554e
--- /dev/null
+++ b/src/com/android/dialer/callstats/CallStatsQueryHandler.java
@@ -0,0 +1,247 @@
+/*
+ * 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.util.Log;
+
+import com.android.contacts.common.CallUtil;
+import com.android.contacts.common.util.UriUtils;
+import com.android.dialer.calllog.ContactInfo;
+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;
+
+ public static final int CALL_TYPE_ALL = 0;
+
+ 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) {
+ 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));
+ }
+
+ 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 || !CallUtil.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);
+
+ pending = new CallStatsDetails(number, numberPresentation,
+ 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 (CallUtil.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;
+ }
+
+ public interface Listener {
+ void onCallsFetched(Map<ContactInfo, CallStatsDetails> calls);
+ }
+}
diff --git a/src/com/android/dialer/callstats/IntentProvider.java b/src/com/android/dialer/callstats/IntentProvider.java
new file mode 100644
index 000000000..8b02d0733
--- /dev/null
+++ b/src/com/android/dialer/callstats/IntentProvider.java
@@ -0,0 +1,54 @@
+/*
+ * 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 com.android.contacts.common.CallUtil;
+
+/**
+ * Class to get intents for a phone call or for a detailed statistical view
+ */
+public abstract class IntentProvider {
+ public abstract Intent getIntent(Context context);
+
+ public static IntentProvider getReturnCallIntentProvider(final String number) {
+ return new IntentProvider() {
+ @Override
+ public Intent getIntent(Context context) {
+ return CallUtil.getCallIntent(number);
+ }
+ };
+ }
+
+ public static IntentProvider getCallStatsDetailIntentProvider(final CallStatsDetails item,
+ final long from, final long to, final boolean byDuration) {
+ return new IntentProvider() {
+ @Override
+ public Intent getIntent(Context context) {
+ Intent intent = new Intent(context, CallStatsDetailActivity.class);
+ intent.putExtra(CallStatsDetailActivity.EXTRA_DETAILS, item);
+ intent.putExtra(CallStatsDetailActivity.EXTRA_FROM, from);
+ intent.putExtra(CallStatsDetailActivity.EXTRA_TO, to);
+ intent.putExtra(CallStatsDetailActivity.EXTRA_BY_DURATION, byDuration);
+ return intent;
+ }
+ };
+ }
+}
diff --git a/src/com/android/dialer/widget/AnchoredScrollView.java b/src/com/android/dialer/widget/AnchoredScrollView.java
new file mode 100644
index 000000000..a07b8f798
--- /dev/null
+++ b/src/com/android/dialer/widget/AnchoredScrollView.java
@@ -0,0 +1,101 @@
+/*
+ * 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.
+ */
+
+package com.android.dialer.widget;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.ScrollView;
+
+import com.android.dialer.R;
+
+/**
+ * A ScrollView that makes sure that the part of the 'anchored' view
+ * that is defined by the anchor is never scrolled out of view.
+ */
+public class AnchoredScrollView extends ScrollView {
+ private int mAnchorId;
+ private int mAnchoredId;
+ private boolean mAnchorAtBottom;
+
+ private View mAnchorView;
+ private View mAnchoredView;
+ private int mOrigAnchoredTop;
+
+ public AnchoredScrollView(Context context) {
+ this(context, null);
+ }
+
+ public AnchoredScrollView(Context context, AttributeSet attrs) {
+ this(context, attrs, com.android.internal.R.attr.scrollViewStyle);
+ }
+
+ public AnchoredScrollView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+ TypedArray a =
+ context.obtainStyledAttributes(attrs, R.styleable.AnchoredScrollView);
+
+ mAnchorId = a.getResourceId(R.styleable.AnchoredScrollView_anchorView, 0);
+ mAnchoredId = a.getResourceId(R.styleable.AnchoredScrollView_anchoredView, 0);
+ mAnchorAtBottom = a.getBoolean(R.styleable.AnchoredScrollView_anchorAtBottom, false);
+
+ a.recycle();
+ }
+
+ private boolean ensureViews() {
+ if (mAnchorView == null) {
+ mAnchorView = findViewTraversal(mAnchorId);
+ }
+ if (mAnchoredView == null) {
+ mAnchoredView = findViewTraversal(mAnchoredId);
+ }
+
+ return mAnchorView != null && mAnchoredView != null;
+ }
+
+ private int getAnchor() {
+ return mAnchorAtBottom ? mAnchorView.getBottom() : mAnchorView.getTop();
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ super.onLayout(changed, l, t, r, b);
+ if (ensureViews()) {
+ mOrigAnchoredTop = mAnchoredView.getTop();
+ }
+ }
+
+ @Override
+ protected void onScrollChanged(int l, int t, int oldl, int oldt) {
+ super.onScrollChanged(l, t, oldl, oldt);
+
+ if (!ensureViews()) {
+ return;
+ }
+
+ int currentOffset = mAnchoredView.getTop() - mOrigAnchoredTop;
+ int matchDistance = getAnchor() - getScrollY();
+ int desiredOffset = Math.max(-matchDistance, 0);
+ int neededOffset = desiredOffset - currentOffset;
+
+ if (neededOffset != 0) {
+ mAnchoredView.offsetTopAndBottom(neededOffset);
+ }
+ }
+}
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..0b824af55
--- /dev/null
+++ b/src/com/android/dialer/widget/LinearColorBar.java
@@ -0,0 +1,210 @@
+/*
+ * 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 int mBackgroundColor;
+ private int mBlueColor;
+ private int mGreenColor;
+ private int mRedColor;
+
+ 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;
+ }
+ }
+
+ 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) {
+ mFirstRatio = blue;
+ mSecondRatio = green;
+ mThirdRatio = red;
+ 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 indicatorLeft = right3;
+ 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 = left + width;
+ if (left < right) {
+ mRect.left = left;
+ mRect.right = right;
+ mPaint.setColor(mBackgroundColor);
+ canvas.drawRect(mRect, mPaint);
+ }
+ }
+}
diff --git a/src/com/android/dialer/widget/PieChartView.java b/src/com/android/dialer/widget/PieChartView.java
new file mode 100644
index 000000000..10df10da5
--- /dev/null
+++ b/src/com/android/dialer/widget/PieChartView.java
@@ -0,0 +1,261 @@
+/*
+ * 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.widget;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Paint.Style;
+import android.graphics.Path;
+import android.graphics.Path.Direction;
+import android.graphics.RadialGradient;
+import android.graphics.RectF;
+import android.graphics.Shader.TileMode;
+import android.util.AttributeSet;
+import android.view.View;
+
+import com.android.dialer.R;
+import com.google.common.collect.Lists;
+
+import java.util.ArrayList;
+
+/**
+ * Pie chart with multiple items.
+ */
+public class PieChartView extends View {
+ public static final String TAG = "PieChartView";
+ public static final boolean LOGD = false;
+
+ private static final boolean FILL_GRADIENT = false;
+
+ private ArrayList<Slice> mSlices = Lists.newArrayList();
+
+ private int mOriginAngle;
+ private Matrix mMatrix = new Matrix();
+
+ private Paint mPaintOutline = new Paint();
+
+ private Path mPathSide = new Path();
+ private Path mPathSideOutline = new Path();
+
+ private Path mPathOutline = new Path();
+
+ private int mSideWidth;
+
+ public class Slice {
+ public long value;
+
+ public Path path = new Path();
+ public Path pathSide = new Path();
+ public Path pathOutline = new Path();
+
+ public Paint paint;
+
+ public Slice(long value, int color) {
+ this.value = value;
+ this.paint = buildFillPaint(color, getResources());
+ }
+ }
+
+ public PieChartView(Context context) {
+ this(context, null);
+ }
+
+ public PieChartView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public PieChartView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+ TypedArray a = context.obtainStyledAttributes(attrs,
+ R.styleable.PieChartView, 0, 0);
+ int n = a.getIndexCount();
+ int outlineColor = 0;
+
+ for (int i = 0; i < n; i++) {
+ int attr = a.getIndex(i);
+
+ switch (attr) {
+ case R.styleable.PieChartView_outlineColor:
+ outlineColor = a.getInt(attr, 0);
+ break;
+ }
+ }
+
+ a.recycle();
+
+ mPaintOutline.setColor(outlineColor);
+ mPaintOutline.setStyle(Style.STROKE);
+ mPaintOutline.setStrokeWidth(3f * getResources().getDisplayMetrics().density);
+ mPaintOutline.setAntiAlias(true);
+
+ mSideWidth = (int) (20 * getResources().getDisplayMetrics().density);
+
+ setWillNotDraw(false);
+ }
+
+ private static Paint buildFillPaint(int color, Resources res) {
+ final Paint paint = new Paint();
+
+ paint.setColor(color);
+ paint.setStyle(Style.FILL_AND_STROKE);
+ paint.setAntiAlias(true);
+
+ if (FILL_GRADIENT) {
+ final int width = (int) (280 * res.getDisplayMetrics().density);
+ paint.setShader(new RadialGradient(0, 0, width, color, darken(color), TileMode.MIRROR));
+ }
+
+ return paint;
+ }
+
+ public void setOriginAngle(int originAngle) {
+ mOriginAngle = originAngle;
+ }
+
+ public void addSlice(long value, int color) {
+ mSlices.add(new Slice(value, color));
+ }
+
+ public void removeAllSlices() {
+ mSlices.clear();
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ final float centerX = getWidth() / 2;
+ final float centerY = getHeight() / 2;
+
+ mMatrix.reset();
+ mMatrix.postScale(0.665f, 0.95f, centerX, centerY);
+ mMatrix.postRotate(-40, centerX, centerY);
+
+ generatePath();
+ }
+
+ public void generatePath() {
+
+ long total = 0;
+ for (Slice slice : mSlices) {
+ slice.path.reset();
+ slice.pathSide.reset();
+ slice.pathOutline.reset();
+ total += slice.value;
+ }
+
+ mPathSide.reset();
+ mPathSideOutline.reset();
+ mPathOutline.reset();
+
+ // bail when not enough stats to render
+ if (total == 0) {
+ invalidate();
+ return;
+ }
+
+ final int width = getWidth();
+ final int height = getHeight();
+
+ final RectF rect = new RectF(0, 0, width, height);
+ final RectF rectSide = new RectF();
+ rectSide.set(rect);
+ rectSide.offset(-mSideWidth, 0);
+
+ mPathSide.addOval(rectSide, Direction.CW);
+ mPathSideOutline.addOval(rectSide, Direction.CW);
+ mPathOutline.addOval(rect, Direction.CW);
+
+ int startAngle = mOriginAngle;
+ for (Slice slice : mSlices) {
+ final int sweepAngle = (int) (slice.value * 360 / total);
+ final int endAngle = startAngle + sweepAngle;
+
+ final float startAngleMod = startAngle % 360;
+ final float endAngleMod = endAngle % 360;
+ final boolean startSideVisible = startAngleMod > 90 && startAngleMod < 270;
+ final boolean endSideVisible = endAngleMod > 90 && endAngleMod < 270;
+
+ // draw slice
+ slice.path.moveTo(rect.centerX(), rect.centerY());
+ slice.path.arcTo(rect, startAngle, sweepAngle);
+ slice.path.lineTo(rect.centerX(), rect.centerY());
+
+ if (startSideVisible || endSideVisible) {
+
+ // when start is beyond horizon, push until visible
+ final float startAngleSide = startSideVisible ? startAngle : 450;
+ final float endAngleSide = endSideVisible ? endAngle : 270;
+ final float sweepAngleSide = endAngleSide - startAngleSide;
+
+ // draw slice side
+ slice.pathSide.moveTo(rect.centerX(), rect.centerY());
+ slice.pathSide.arcTo(rect, startAngleSide, 0);
+ slice.pathSide.rLineTo(-mSideWidth, 0);
+ slice.pathSide.arcTo(rectSide, startAngleSide, sweepAngleSide);
+ slice.pathSide.rLineTo(mSideWidth, 0);
+ slice.pathSide.arcTo(rect, endAngleSide, -sweepAngleSide);
+ }
+
+ // draw slice outline
+ slice.pathOutline.moveTo(rect.centerX(), rect.centerY());
+ slice.pathOutline.arcTo(rect, startAngle, 0);
+ if (startSideVisible) {
+ slice.pathOutline.rLineTo(-mSideWidth, 0);
+ }
+ slice.pathOutline.moveTo(rect.centerX(), rect.centerY());
+ slice.pathOutline.arcTo(rect, startAngle + sweepAngle, 0);
+ if (endSideVisible) {
+ slice.pathOutline.rLineTo(-mSideWidth, 0);
+ }
+
+ startAngle += sweepAngle;
+ }
+
+ invalidate();
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+
+ canvas.concat(mMatrix);
+
+ for (Slice slice : mSlices) {
+ canvas.drawPath(slice.pathSide, slice.paint);
+ }
+ canvas.drawPath(mPathSideOutline, mPaintOutline);
+
+ for (Slice slice : mSlices) {
+ canvas.drawPath(slice.path, slice.paint);
+ canvas.drawPath(slice.pathOutline, mPaintOutline);
+ }
+ canvas.drawPath(mPathOutline, mPaintOutline);
+ }
+
+ public static int darken(int color) {
+ float[] hsv = new float[3];
+ Color.colorToHSV(color, hsv);
+ hsv[2] /= 2;
+ hsv[1] /= 2;
+ return Color.HSVToColor(hsv);
+ }
+
+}