summaryrefslogtreecommitdiffstats
path: root/src/com/android/dialer/callstats
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/dialer/callstats')
-rw-r--r--src/com/android/dialer/callstats/CallStatsAdapter.java249
-rw-r--r--src/com/android/dialer/callstats/CallStatsDetailActivity.java274
-rw-r--r--src/com/android/dialer/callstats/CallStatsDetailHelper.java174
-rw-r--r--src/com/android/dialer/callstats/CallStatsDetailViews.java50
-rw-r--r--src/com/android/dialer/callstats/CallStatsDetails.java238
-rw-r--r--src/com/android/dialer/callstats/CallStatsFragment.java340
-rw-r--r--src/com/android/dialer/callstats/CallStatsListItemViews.java55
-rw-r--r--src/com/android/dialer/callstats/CallStatsQuery.java59
-rw-r--r--src/com/android/dialer/callstats/CallStatsQueryHandler.java247
-rw-r--r--src/com/android/dialer/callstats/IntentProvider.java54
10 files changed, 1740 insertions, 0 deletions
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;
+ }
+ };
+ }
+}