diff options
Diffstat (limited to 'src/com/android/dialer/callstats')
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; + } + }; + } +} |