diff options
author | Danny Baumann <dannybaumann@web.de> | 2014-11-12 17:01:47 -0800 |
---|---|---|
committer | Matt Garnes <matt@cyngn.com> | 2014-12-12 13:12:50 -0800 |
commit | b1b2e29404ed00258c2734e342257c26905426d6 (patch) | |
tree | f03d87a715518c477f91bac8ee694fc63ec35740 | |
parent | 642979f4992e53d0e74bbbc94237a4805121120e (diff) | |
download | android_packages_apps_Dialer-b1b2e29404ed00258c2734e342257c26905426d6.tar.gz android_packages_apps_Dialer-b1b2e29404ed00258c2734e342257c26905426d6.tar.bz2 android_packages_apps_Dialer-b1b2e29404ed00258c2734e342257c26905426d6.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
39 files changed, 4645 insertions, 563 deletions
diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 6139004c2..43535583e 100755 --- 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 Binary files differnew file mode 100644 index 000000000..1dbf4b485 --- /dev/null +++ b/res/drawable-hdpi/ic_call_inout_holo_dark.png diff --git a/res/drawable-mdpi/ic_call_inout_holo_dark.png b/res/drawable-mdpi/ic_call_inout_holo_dark.png Binary files differnew file mode 100644 index 000000000..88b92a195 --- /dev/null +++ b/res/drawable-mdpi/ic_call_inout_holo_dark.png diff --git a/res/drawable-xhdpi/ic_call_inout_holo_dark.png b/res/drawable-xhdpi/ic_call_inout_holo_dark.png Binary files differnew file mode 100644 index 000000000..f133f164c --- /dev/null +++ b/res/drawable-xhdpi/ic_call_inout_holo_dark.png 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 aa7465174..f92024be7 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); @@ -454,6 +440,9 @@ public class CallDetailActivity extends AnalyticsActivity implements ProximitySe final Uri photoUri = firstDetails.photoUri; final long subId = firstDetails.accountId; + // 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 = PhoneNumberUtilsWrapper.canPlaceCallsTo(mNumber, numberPresentation); @@ -461,39 +450,7 @@ public class CallDetailActivity extends AnalyticsActivity implements ProximitySe final boolean isVoicemailNumber = phoneUtils.isVoicemailNumber(subId, mNumber); final boolean isSipNumber = phoneUtils.isSipNumber(mNumber); - final CharSequence callLocationOrType = getNumberTypeOrLocation(firstDetails); - - final CharSequence displayNumber = mPhoneNumberHelper.getDisplayNumber( - subId, - 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(); @@ -502,35 +459,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.accountId, - 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); } @@ -639,68 +568,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); @@ -866,13 +733,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 d59ecbf2c..fe4ef86f3 100755 --- a/src/com/android/dialer/PhoneCallDetails.java +++ b/src/com/android/dialer/PhoneCallDetails.java @@ -27,7 +27,7 @@ import android.telephony.SubscriptionManager; /** * 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} */ @@ -174,4 +174,33 @@ public class PhoneCallDetails { this.durationType = durationType; this.accountId = accountId; } + + @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 8d8e22301..829a48bec 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; @@ -54,6 +52,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; @@ -64,7 +63,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; @@ -101,43 +100,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; @@ -146,21 +108,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. */ @@ -185,65 +136,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; @@ -257,8 +150,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; @@ -316,34 +208,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) { @@ -359,9 +223,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); @@ -370,6 +231,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 = @@ -400,177 +263,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); @@ -712,42 +404,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; @@ -827,10 +485,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); } @@ -1104,17 +759,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; @@ -1277,13 +929,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 @@ -1312,46 +969,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); } /** @@ -1391,7 +1018,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); + } + +} |