1535 lines
58 KiB
Java
1535 lines
58 KiB
Java
|
/*
|
||
|
* Copyright (C) 2011 The Android Open Source Project
|
||
|
*
|
||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||
|
* you may not use this file except in compliance with the License.
|
||
|
* You may obtain a copy of the License at
|
||
|
*
|
||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||
|
*
|
||
|
* Unless required by applicable law or agreed to in writing, software
|
||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
|
* See the License for the specific language governing permissions and
|
||
|
* limitations under the License.
|
||
|
*/
|
||
|
|
||
|
package com.android.dialer.app.calllog;
|
||
|
|
||
|
import android.app.Activity;
|
||
|
import android.content.ContentUris;
|
||
|
import android.content.DialogInterface;
|
||
|
import android.content.DialogInterface.OnCancelListener;
|
||
|
import android.content.res.Resources;
|
||
|
import android.database.Cursor;
|
||
|
import android.net.Uri;
|
||
|
import android.os.AsyncTask;
|
||
|
import android.os.Build.VERSION;
|
||
|
import android.os.Build.VERSION_CODES;
|
||
|
import android.os.Bundle;
|
||
|
import android.os.Trace;
|
||
|
import android.provider.CallLog;
|
||
|
import android.provider.ContactsContract.CommonDataKinds.Phone;
|
||
|
import android.support.annotation.MainThread;
|
||
|
import android.support.annotation.NonNull;
|
||
|
import android.support.annotation.Nullable;
|
||
|
import android.support.annotation.VisibleForTesting;
|
||
|
import android.support.annotation.WorkerThread;
|
||
|
import android.support.v7.app.AlertDialog;
|
||
|
import android.support.v7.app.AppCompatActivity;
|
||
|
import android.support.v7.widget.RecyclerView;
|
||
|
import android.support.v7.widget.RecyclerView.ViewHolder;
|
||
|
import android.telecom.PhoneAccountHandle;
|
||
|
import android.telephony.PhoneNumberUtils;
|
||
|
import android.text.TextUtils;
|
||
|
import android.util.ArrayMap;
|
||
|
import android.util.ArraySet;
|
||
|
import android.util.SparseArray;
|
||
|
import android.view.ActionMode;
|
||
|
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 com.android.contacts.common.ContactsUtils;
|
||
|
import com.android.dialer.app.R;
|
||
|
import com.android.dialer.app.calllog.CallLogFragment.CallLogFragmentListener;
|
||
|
import com.android.dialer.app.calllog.CallLogGroupBuilder.GroupCreator;
|
||
|
import com.android.dialer.app.calllog.calllogcache.CallLogCache;
|
||
|
import com.android.dialer.app.contactinfo.ContactInfoCache;
|
||
|
import com.android.dialer.app.voicemail.VoicemailPlaybackPresenter;
|
||
|
import com.android.dialer.app.voicemail.VoicemailPlaybackPresenter.OnVoicemailDeletedListener;
|
||
|
import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler;
|
||
|
import com.android.dialer.calldetails.CallDetailsEntries;
|
||
|
import com.android.dialer.calldetails.CallDetailsEntries.CallDetailsEntry;
|
||
|
import com.android.dialer.calllogutils.CallbackActionHelper.CallbackAction;
|
||
|
import com.android.dialer.calllogutils.PhoneCallDetails;
|
||
|
import com.android.dialer.common.Assert;
|
||
|
import com.android.dialer.common.FragmentUtils.FragmentUtilListener;
|
||
|
import com.android.dialer.common.LogUtil;
|
||
|
import com.android.dialer.common.concurrent.AsyncTaskExecutor;
|
||
|
import com.android.dialer.common.concurrent.AsyncTaskExecutors;
|
||
|
import com.android.dialer.compat.android.provider.VoicemailCompat;
|
||
|
import com.android.dialer.configprovider.ConfigProviderComponent;
|
||
|
import com.android.dialer.contacts.ContactsComponent;
|
||
|
import com.android.dialer.duo.Duo;
|
||
|
import com.android.dialer.duo.DuoComponent;
|
||
|
import com.android.dialer.duo.DuoListener;
|
||
|
import com.android.dialer.enrichedcall.EnrichedCallCapabilities;
|
||
|
import com.android.dialer.enrichedcall.EnrichedCallComponent;
|
||
|
import com.android.dialer.enrichedcall.EnrichedCallManager;
|
||
|
import com.android.dialer.logging.ContactSource;
|
||
|
import com.android.dialer.logging.ContactSource.Type;
|
||
|
import com.android.dialer.logging.DialerImpression;
|
||
|
import com.android.dialer.logging.Logger;
|
||
|
import com.android.dialer.logging.LoggingBindings.ContactsProviderMatchInfo;
|
||
|
import com.android.dialer.logging.UiAction;
|
||
|
import com.android.dialer.main.MainActivityPeer;
|
||
|
import com.android.dialer.performancereport.PerformanceReport;
|
||
|
import com.android.dialer.phonenumbercache.CallLogQuery;
|
||
|
import com.android.dialer.phonenumbercache.ContactInfo;
|
||
|
import com.android.dialer.phonenumbercache.ContactInfoHelper;
|
||
|
import com.android.dialer.phonenumberutil.PhoneNumberHelper;
|
||
|
import com.android.dialer.spam.SpamComponent;
|
||
|
import com.android.dialer.telecom.TelecomUtil;
|
||
|
import com.android.dialer.util.PermissionsUtil;
|
||
|
import com.google.i18n.phonenumbers.NumberParseException;
|
||
|
import com.google.i18n.phonenumbers.PhoneNumberUtil;
|
||
|
import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber;
|
||
|
import java.util.ArrayList;
|
||
|
import java.util.Map;
|
||
|
import java.util.Set;
|
||
|
import java.util.concurrent.ConcurrentHashMap;
|
||
|
import java.util.concurrent.ConcurrentMap;
|
||
|
|
||
|
/** Adapter class to fill in data for the Call Log. */
|
||
|
public class CallLogAdapter extends GroupingListAdapter
|
||
|
implements GroupCreator, OnVoicemailDeletedListener, DuoListener {
|
||
|
|
||
|
// Types of activities the call log adapter is used for
|
||
|
public static final int ACTIVITY_TYPE_CALL_LOG = 1;
|
||
|
public static final int ACTIVITY_TYPE_DIALTACTS = 2;
|
||
|
private static final int NO_EXPANDED_LIST_ITEM = -1;
|
||
|
public static final int ALERT_POSITION = 0;
|
||
|
private static final int VIEW_TYPE_ALERT = 1;
|
||
|
private static final int VIEW_TYPE_CALLLOG = 2;
|
||
|
|
||
|
private static final String KEY_EXPANDED_POSITION = "expanded_position";
|
||
|
private static final String KEY_EXPANDED_ROW_ID = "expanded_row_id";
|
||
|
private static final String KEY_ACTION_MODE = "action_mode_selected_items";
|
||
|
|
||
|
public static final String LOAD_DATA_TASK_IDENTIFIER = "load_data";
|
||
|
|
||
|
public static final String ENABLE_CALL_LOG_MULTI_SELECT = "enable_call_log_multiselect";
|
||
|
public static final boolean ENABLE_CALL_LOG_MULTI_SELECT_FLAG = true;
|
||
|
|
||
|
@VisibleForTesting static final String FILTER_EMERGENCY_CALLS_FLAG = "filter_emergency_calls";
|
||
|
|
||
|
protected final Activity activity;
|
||
|
protected final VoicemailPlaybackPresenter voicemailPlaybackPresenter;
|
||
|
/** Cache for repeated requests to Telecom/Telephony. */
|
||
|
protected final CallLogCache callLogCache;
|
||
|
|
||
|
private final CallFetcher callFetcher;
|
||
|
private final OnActionModeStateChangedListener actionModeStateChangedListener;
|
||
|
private final MultiSelectRemoveView multiSelectRemoveView;
|
||
|
@NonNull private final FilteredNumberAsyncQueryHandler filteredNumberAsyncQueryHandler;
|
||
|
private final int activityType;
|
||
|
|
||
|
/** Instance of helper class for managing views. */
|
||
|
private final CallLogListItemHelper callLogListItemHelper;
|
||
|
/** Helper to group call log entries. */
|
||
|
private final CallLogGroupBuilder callLogGroupBuilder;
|
||
|
|
||
|
private final AsyncTaskExecutor asyncTaskExecutor = AsyncTaskExecutors.createAsyncTaskExecutor();
|
||
|
private ContactInfoCache contactInfoCache;
|
||
|
// Tracks the position of the currently expanded list item.
|
||
|
private int currentlyExpandedPosition = RecyclerView.NO_POSITION;
|
||
|
// Tracks the rowId of the currently expanded list item, so the position can be updated if there
|
||
|
// are any changes to the call log entries, such as additions or removals.
|
||
|
private long currentlyExpandedRowId = NO_EXPANDED_LIST_ITEM;
|
||
|
|
||
|
private final CallLogAlertManager callLogAlertManager;
|
||
|
|
||
|
public ActionMode actionMode = null;
|
||
|
public boolean selectAllMode = false;
|
||
|
public boolean deselectAllMode = false;
|
||
|
private final SparseArray<String> selectedItems = new SparseArray<>();
|
||
|
|
||
|
/**
|
||
|
* Maps a raw input number to match info. We only log one MatchInfo per raw input number to reduce
|
||
|
* the amount of data logged.
|
||
|
*
|
||
|
* <p>Note that this has to be a {@link ConcurrentMap} as the match info for each row in the UI is
|
||
|
* loaded in a background thread spawned when the ViewHolder is bound.
|
||
|
*/
|
||
|
private final ConcurrentMap<String, ContactsProviderMatchInfo> contactsProviderMatchInfos =
|
||
|
new ConcurrentHashMap<>();
|
||
|
|
||
|
private final ActionMode.Callback actionModeCallback =
|
||
|
new ActionMode.Callback() {
|
||
|
|
||
|
// Called when the action mode is created; startActionMode() was called
|
||
|
@Override
|
||
|
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
|
||
|
if (activity != null) {
|
||
|
announceforAccessibility(
|
||
|
activity.getCurrentFocus(),
|
||
|
activity.getString(R.string.description_entering_bulk_action_mode));
|
||
|
}
|
||
|
actionMode = mode;
|
||
|
// Inflate a menu resource providing context menu items
|
||
|
MenuInflater inflater = mode.getMenuInflater();
|
||
|
inflater.inflate(R.menu.actionbar_delete, menu);
|
||
|
multiSelectRemoveView.showMultiSelectRemoveView(true);
|
||
|
actionModeStateChangedListener.onActionModeStateChanged(mode, true);
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
// Called each time the action mode is shown. Always called after onCreateActionMode, but
|
||
|
// may be called multiple times if the mode is invalidated.
|
||
|
@Override
|
||
|
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
|
||
|
return false; // Return false if nothing is done
|
||
|
}
|
||
|
|
||
|
// Called when the user selects a contextual menu item
|
||
|
@Override
|
||
|
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
|
||
|
if (item.getItemId() == R.id.action_bar_delete_menu_item) {
|
||
|
Logger.get(activity).logImpression(DialerImpression.Type.MULTISELECT_TAP_DELETE_ICON);
|
||
|
if (selectedItems.size() > 0) {
|
||
|
showDeleteSelectedItemsDialog();
|
||
|
}
|
||
|
return true;
|
||
|
} else {
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Called when the user exits the action mode
|
||
|
@Override
|
||
|
public void onDestroyActionMode(ActionMode mode) {
|
||
|
if (activity != null) {
|
||
|
announceforAccessibility(
|
||
|
activity.getCurrentFocus(),
|
||
|
activity.getString(R.string.description_leaving_bulk_action_mode));
|
||
|
}
|
||
|
selectedItems.clear();
|
||
|
actionMode = null;
|
||
|
selectAllMode = false;
|
||
|
deselectAllMode = false;
|
||
|
multiSelectRemoveView.showMultiSelectRemoveView(false);
|
||
|
actionModeStateChangedListener.onActionModeStateChanged(null, false);
|
||
|
notifyDataSetChanged();
|
||
|
}
|
||
|
};
|
||
|
|
||
|
private void showDeleteSelectedItemsDialog() {
|
||
|
SparseArray<String> voicemailsToDeleteOnConfirmation = selectedItems.clone();
|
||
|
new AlertDialog.Builder(activity)
|
||
|
.setCancelable(true)
|
||
|
.setTitle(
|
||
|
activity
|
||
|
.getResources()
|
||
|
.getQuantityString(
|
||
|
R.plurals.delete_voicemails_confirmation_dialog_title, selectedItems.size()))
|
||
|
.setPositiveButton(
|
||
|
R.string.voicemailMultiSelectDeleteConfirm,
|
||
|
new DialogInterface.OnClickListener() {
|
||
|
@Override
|
||
|
public void onClick(final DialogInterface dialog, final int button) {
|
||
|
LogUtil.i(
|
||
|
"CallLogAdapter.showDeleteSelectedItemsDialog",
|
||
|
"onClick, these items to delete " + voicemailsToDeleteOnConfirmation);
|
||
|
deleteSelectedItems(voicemailsToDeleteOnConfirmation);
|
||
|
actionMode.finish();
|
||
|
dialog.cancel();
|
||
|
Logger.get(activity)
|
||
|
.logImpression(
|
||
|
DialerImpression.Type.MULTISELECT_DELETE_ENTRY_VIA_CONFIRMATION_DIALOG);
|
||
|
}
|
||
|
})
|
||
|
.setOnCancelListener(
|
||
|
new OnCancelListener() {
|
||
|
@Override
|
||
|
public void onCancel(DialogInterface dialogInterface) {
|
||
|
Logger.get(activity)
|
||
|
.logImpression(
|
||
|
DialerImpression.Type
|
||
|
.MULTISELECT_CANCEL_CONFIRMATION_DIALOG_VIA_CANCEL_TOUCH);
|
||
|
dialogInterface.cancel();
|
||
|
}
|
||
|
})
|
||
|
.setNegativeButton(
|
||
|
R.string.voicemailMultiSelectDeleteCancel,
|
||
|
new DialogInterface.OnClickListener() {
|
||
|
@Override
|
||
|
public void onClick(final DialogInterface dialog, final int button) {
|
||
|
Logger.get(activity)
|
||
|
.logImpression(
|
||
|
DialerImpression.Type
|
||
|
.MULTISELECT_CANCEL_CONFIRMATION_DIALOG_VIA_CANCEL_BUTTON);
|
||
|
dialog.cancel();
|
||
|
}
|
||
|
})
|
||
|
.show();
|
||
|
Logger.get(activity)
|
||
|
.logImpression(DialerImpression.Type.MULTISELECT_DISPLAY_DELETE_CONFIRMATION_DIALOG);
|
||
|
}
|
||
|
|
||
|
private void deleteSelectedItems(SparseArray<String> voicemailsToDelete) {
|
||
|
for (int i = 0; i < voicemailsToDelete.size(); i++) {
|
||
|
String voicemailUri = voicemailsToDelete.get(voicemailsToDelete.keyAt(i));
|
||
|
LogUtil.i("CallLogAdapter.deleteSelectedItems", "deleting uri:" + voicemailUri);
|
||
|
CallLogAsyncTaskUtil.deleteVoicemail(activity, Uri.parse(voicemailUri), null);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private final View.OnLongClickListener longPressListener =
|
||
|
new View.OnLongClickListener() {
|
||
|
@Override
|
||
|
public boolean onLongClick(View v) {
|
||
|
if (ConfigProviderComponent.get(v.getContext())
|
||
|
.getConfigProvider()
|
||
|
.getBoolean(ENABLE_CALL_LOG_MULTI_SELECT, ENABLE_CALL_LOG_MULTI_SELECT_FLAG)
|
||
|
&& voicemailPlaybackPresenter != null) {
|
||
|
if (v.getId() == R.id.primary_action_view || v.getId() == R.id.quick_contact_photo) {
|
||
|
if (actionMode == null) {
|
||
|
Logger.get(activity)
|
||
|
.logImpression(
|
||
|
DialerImpression.Type.MULTISELECT_LONG_PRESS_ENTER_MULTI_SELECT_MODE);
|
||
|
actionMode = v.startActionMode(actionModeCallback);
|
||
|
}
|
||
|
Logger.get(activity)
|
||
|
.logImpression(DialerImpression.Type.MULTISELECT_LONG_PRESS_TAP_ENTRY);
|
||
|
CallLogListItemViewHolder viewHolder = (CallLogListItemViewHolder) v.getTag();
|
||
|
viewHolder.quickContactView.setVisibility(View.GONE);
|
||
|
viewHolder.checkBoxView.setVisibility(View.VISIBLE);
|
||
|
expandCollapseListener.onClick(v);
|
||
|
return true;
|
||
|
}
|
||
|
}
|
||
|
return true;
|
||
|
}
|
||
|
};
|
||
|
|
||
|
@VisibleForTesting
|
||
|
public View.OnClickListener getExpandCollapseListener() {
|
||
|
return expandCollapseListener;
|
||
|
}
|
||
|
|
||
|
/** The OnClickListener used to expand or collapse the action buttons of a call log entry. */
|
||
|
private final View.OnClickListener expandCollapseListener =
|
||
|
new View.OnClickListener() {
|
||
|
@Override
|
||
|
public void onClick(View v) {
|
||
|
PerformanceReport.recordClick(UiAction.Type.CLICK_CALL_LOG_ITEM);
|
||
|
|
||
|
CallLogListItemViewHolder viewHolder = (CallLogListItemViewHolder) v.getTag();
|
||
|
if (viewHolder == null) {
|
||
|
return;
|
||
|
}
|
||
|
if (actionMode != null && viewHolder.voicemailUri != null) {
|
||
|
selectAllMode = false;
|
||
|
deselectAllMode = false;
|
||
|
multiSelectRemoveView.setSelectAllModeToFalse();
|
||
|
int id = getVoicemailId(viewHolder.voicemailUri);
|
||
|
if (selectedItems.get(id) != null) {
|
||
|
Logger.get(activity)
|
||
|
.logImpression(DialerImpression.Type.MULTISELECT_SINGLE_PRESS_UNSELECT_ENTRY);
|
||
|
uncheckMarkCallLogEntry(viewHolder, id);
|
||
|
} else {
|
||
|
Logger.get(activity)
|
||
|
.logImpression(DialerImpression.Type.MULTISELECT_SINGLE_PRESS_SELECT_ENTRY);
|
||
|
checkMarkCallLogEntry(viewHolder);
|
||
|
// select all check box logic
|
||
|
if (getItemCount() == selectedItems.size()) {
|
||
|
LogUtil.i(
|
||
|
"mExpandCollapseListener.onClick",
|
||
|
"getitem count %d is equal to items select count %d, check select all box",
|
||
|
getItemCount(),
|
||
|
selectedItems.size());
|
||
|
multiSelectRemoveView.tapSelectAll();
|
||
|
}
|
||
|
}
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (voicemailPlaybackPresenter != null) {
|
||
|
// Always reset the voicemail playback state on expand or collapse.
|
||
|
voicemailPlaybackPresenter.resetAll();
|
||
|
}
|
||
|
|
||
|
// If enriched call capabilities were unknown on the initial load,
|
||
|
// viewHolder.isCallComposerCapable may be unset. Check here if we have the capabilities
|
||
|
// as a last attempt at getting them before showing the expanded view to the user
|
||
|
EnrichedCallCapabilities capabilities = null;
|
||
|
|
||
|
if (viewHolder.number != null) {
|
||
|
capabilities = getEnrichedCallManager().getCapabilities(viewHolder.number);
|
||
|
}
|
||
|
|
||
|
if (capabilities == null) {
|
||
|
capabilities = EnrichedCallCapabilities.NO_CAPABILITIES;
|
||
|
}
|
||
|
|
||
|
viewHolder.isCallComposerCapable = capabilities.isCallComposerCapable();
|
||
|
|
||
|
if (capabilities.isTemporarilyUnavailable()) {
|
||
|
LogUtil.i(
|
||
|
"mExpandCollapseListener.onClick",
|
||
|
"%s is temporarily unavailable, requesting capabilities",
|
||
|
LogUtil.sanitizePhoneNumber(viewHolder.number));
|
||
|
// Refresh the capabilities when temporarily unavailable.
|
||
|
// Similarly to when we request capabilities the first time, the 'Share and call' button
|
||
|
// won't pop in with the new capabilities. Instead the row needs to be collapsed and
|
||
|
// expanded again.
|
||
|
getEnrichedCallManager().requestCapabilities(viewHolder.number);
|
||
|
}
|
||
|
|
||
|
if (viewHolder.rowId == currentlyExpandedRowId) {
|
||
|
// Hide actions, if the clicked item is the expanded item.
|
||
|
viewHolder.showActions(false);
|
||
|
|
||
|
currentlyExpandedPosition = RecyclerView.NO_POSITION;
|
||
|
currentlyExpandedRowId = NO_EXPANDED_LIST_ITEM;
|
||
|
} else {
|
||
|
if (viewHolder.callType == CallLog.Calls.MISSED_TYPE) {
|
||
|
CallLogAsyncTaskUtil.markCallAsRead(activity, viewHolder.callIds);
|
||
|
if (activityType == ACTIVITY_TYPE_DIALTACTS) {
|
||
|
Assert.checkState(
|
||
|
v.getContext() instanceof MainActivityPeer.PeerSupplier,
|
||
|
"%s is not a PeerSupplier",
|
||
|
v.getContext().getClass());
|
||
|
// This is really bad, but we must do this to prevent a dependency cycle, enforce
|
||
|
// best practices in new code, and avoid refactoring DialtactsActivity.
|
||
|
((FragmentUtilListener) ((MainActivityPeer.PeerSupplier) v.getContext()).getPeer())
|
||
|
.getImpl(CallLogFragmentListener.class)
|
||
|
.updateTabUnreadCounts();
|
||
|
}
|
||
|
}
|
||
|
expandViewHolderActions(viewHolder);
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
|
||
|
@Nullable
|
||
|
public RecyclerView.OnScrollListener getOnScrollListener() {
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
private void checkMarkCallLogEntry(CallLogListItemViewHolder viewHolder) {
|
||
|
announceforAccessibility(
|
||
|
activity.getCurrentFocus(),
|
||
|
activity.getString(
|
||
|
R.string.description_selecting_bulk_action_mode, viewHolder.nameOrNumber));
|
||
|
viewHolder.quickContactView.setVisibility(View.GONE);
|
||
|
viewHolder.checkBoxView.setVisibility(View.VISIBLE);
|
||
|
selectedItems.put(getVoicemailId(viewHolder.voicemailUri), viewHolder.voicemailUri);
|
||
|
updateActionBar();
|
||
|
}
|
||
|
|
||
|
private void announceforAccessibility(View view, String announcement) {
|
||
|
if (view != null) {
|
||
|
view.announceForAccessibility(announcement);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private void updateActionBar() {
|
||
|
if (actionMode == null && selectedItems.size() > 0) {
|
||
|
Logger.get(activity)
|
||
|
.logImpression(DialerImpression.Type.MULTISELECT_ROTATE_AND_SHOW_ACTION_MODE);
|
||
|
activity.startActionMode(actionModeCallback);
|
||
|
}
|
||
|
if (actionMode != null) {
|
||
|
actionMode.setTitle(
|
||
|
activity
|
||
|
.getResources()
|
||
|
.getString(
|
||
|
R.string.voicemailMultiSelectActionBarTitle,
|
||
|
Integer.toString(selectedItems.size())));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private void uncheckMarkCallLogEntry(CallLogListItemViewHolder viewHolder, int id) {
|
||
|
announceforAccessibility(
|
||
|
activity.getCurrentFocus(),
|
||
|
activity.getString(
|
||
|
R.string.description_unselecting_bulk_action_mode, viewHolder.nameOrNumber));
|
||
|
selectedItems.delete(id);
|
||
|
viewHolder.checkBoxView.setVisibility(View.GONE);
|
||
|
viewHolder.quickContactView.setVisibility(View.VISIBLE);
|
||
|
updateActionBar();
|
||
|
}
|
||
|
|
||
|
private static int getVoicemailId(String voicemailUri) {
|
||
|
Assert.checkArgument(voicemailUri != null);
|
||
|
Assert.checkArgument(voicemailUri.length() > 0);
|
||
|
return (int) ContentUris.parseId(Uri.parse(voicemailUri));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* A list of {@link CallLogQuery#ID} that will be hidden. The hide might be temporary so instead
|
||
|
* if removing an item, it will be shown as an invisible view. This simplifies the calculation of
|
||
|
* item position.
|
||
|
*/
|
||
|
@NonNull private Set<Long> hiddenRowIds = new ArraySet<>();
|
||
|
/**
|
||
|
* Holds a list of URIs that are pending deletion or undo. If the activity ends before the undo
|
||
|
* timeout, all of the pending URIs will be deleted.
|
||
|
*
|
||
|
* <p>TODO(twyen): move this and OnVoicemailDeletedListener to somewhere like {@link
|
||
|
* VisualVoicemailCallLogFragment}. The CallLogAdapter does not need to know about what to do with
|
||
|
* hidden item or what to hide.
|
||
|
*/
|
||
|
@NonNull private final Set<Uri> hiddenItemUris = new ArraySet<>();
|
||
|
|
||
|
private CallLogListItemViewHolder.OnClickListener blockReportSpamListener;
|
||
|
|
||
|
/**
|
||
|
* Map, keyed by call ID, used to track the callback action for a call. Calls associated with the
|
||
|
* same callback action will be put into the same primary call group in {@link
|
||
|
* com.android.dialer.app.calllog.CallLogGroupBuilder}. This information is used to set the
|
||
|
* callback icon and trigger the corresponding action.
|
||
|
*/
|
||
|
private final Map<Long, Integer> callbackActions = new ArrayMap<>();
|
||
|
|
||
|
/**
|
||
|
* Map, keyed by call ID, used to track the day group for a call. As call log entries are put into
|
||
|
* the primary call groups in {@link com.android.dialer.app.calllog.CallLogGroupBuilder}, they are
|
||
|
* also assigned a secondary "day group". This map tracks the day group assigned to all calls in
|
||
|
* the call log. This information is used to trigger the display of a day group header above the
|
||
|
* call log entry at the start of a day group. Note: Multiple calls are grouped into a single
|
||
|
* primary "call group" in the call log, and the cursor used to bind rows includes all of these
|
||
|
* calls. When determining if a day group change has occurred it is necessary to look at the last
|
||
|
* entry in the call log to determine its day group. This map provides a means of determining the
|
||
|
* previous day group without having to reverse the cursor to the start of the previous day call
|
||
|
* log entry.
|
||
|
*/
|
||
|
private final Map<Long, Integer> dayGroups = new ArrayMap<>();
|
||
|
|
||
|
private boolean loading = true;
|
||
|
|
||
|
private boolean isSpamEnabled;
|
||
|
|
||
|
public CallLogAdapter(
|
||
|
Activity activity,
|
||
|
ViewGroup alertContainer,
|
||
|
CallFetcher callFetcher,
|
||
|
MultiSelectRemoveView multiSelectRemoveView,
|
||
|
OnActionModeStateChangedListener actionModeStateChangedListener,
|
||
|
CallLogCache callLogCache,
|
||
|
ContactInfoCache contactInfoCache,
|
||
|
VoicemailPlaybackPresenter voicemailPlaybackPresenter,
|
||
|
@NonNull FilteredNumberAsyncQueryHandler filteredNumberAsyncQueryHandler,
|
||
|
int activityType) {
|
||
|
super();
|
||
|
|
||
|
this.activity = activity;
|
||
|
this.callFetcher = callFetcher;
|
||
|
this.actionModeStateChangedListener = actionModeStateChangedListener;
|
||
|
this.multiSelectRemoveView = multiSelectRemoveView;
|
||
|
this.voicemailPlaybackPresenter = voicemailPlaybackPresenter;
|
||
|
if (this.voicemailPlaybackPresenter != null) {
|
||
|
this.voicemailPlaybackPresenter.setOnVoicemailDeletedListener(this);
|
||
|
}
|
||
|
|
||
|
this.activityType = activityType;
|
||
|
|
||
|
this.contactInfoCache = contactInfoCache;
|
||
|
|
||
|
if (!PermissionsUtil.hasContactsReadPermissions(activity)) {
|
||
|
this.contactInfoCache.disableRequestProcessing();
|
||
|
}
|
||
|
|
||
|
Resources resources = this.activity.getResources();
|
||
|
|
||
|
this.callLogCache = callLogCache;
|
||
|
|
||
|
PhoneCallDetailsHelper phoneCallDetailsHelper =
|
||
|
new PhoneCallDetailsHelper(this.activity, resources, this.callLogCache);
|
||
|
callLogListItemHelper =
|
||
|
new CallLogListItemHelper(phoneCallDetailsHelper, resources, this.callLogCache);
|
||
|
callLogGroupBuilder = new CallLogGroupBuilder(activity.getApplicationContext(), this);
|
||
|
this.filteredNumberAsyncQueryHandler = Assert.isNotNull(filteredNumberAsyncQueryHandler);
|
||
|
|
||
|
blockReportSpamListener =
|
||
|
new BlockReportSpamListener(
|
||
|
this.activity,
|
||
|
this.activity.findViewById(R.id.call_log_fragment_root),
|
||
|
((AppCompatActivity) this.activity).getSupportFragmentManager(),
|
||
|
this,
|
||
|
this.filteredNumberAsyncQueryHandler);
|
||
|
setHasStableIds(true);
|
||
|
|
||
|
callLogAlertManager =
|
||
|
new CallLogAlertManager(this, LayoutInflater.from(this.activity), alertContainer);
|
||
|
}
|
||
|
|
||
|
private void expandViewHolderActions(CallLogListItemViewHolder viewHolder) {
|
||
|
if (!TextUtils.isEmpty(viewHolder.voicemailUri)) {
|
||
|
Logger.get(activity).logImpression(DialerImpression.Type.VOICEMAIL_EXPAND_ENTRY);
|
||
|
}
|
||
|
|
||
|
int lastExpandedPosition = currentlyExpandedPosition;
|
||
|
// Show the actions for the clicked list item.
|
||
|
viewHolder.showActions(true);
|
||
|
currentlyExpandedPosition = viewHolder.getAdapterPosition();
|
||
|
currentlyExpandedRowId = viewHolder.rowId;
|
||
|
|
||
|
// If another item is expanded, notify it that it has changed. Its actions will be
|
||
|
// hidden when it is re-binded because we change mCurrentlyExpandedRowId above.
|
||
|
if (lastExpandedPosition != RecyclerView.NO_POSITION) {
|
||
|
notifyItemChanged(lastExpandedPosition);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public void onSaveInstanceState(Bundle outState) {
|
||
|
outState.putInt(KEY_EXPANDED_POSITION, currentlyExpandedPosition);
|
||
|
outState.putLong(KEY_EXPANDED_ROW_ID, currentlyExpandedRowId);
|
||
|
|
||
|
ArrayList<String> listOfSelectedItems = new ArrayList<>();
|
||
|
|
||
|
if (selectedItems.size() > 0) {
|
||
|
for (int i = 0; i < selectedItems.size(); i++) {
|
||
|
int id = selectedItems.keyAt(i);
|
||
|
String voicemailUri = selectedItems.valueAt(i);
|
||
|
LogUtil.i(
|
||
|
"CallLogAdapter.onSaveInstanceState", "index %d, id=%d, uri=%s ", i, id, voicemailUri);
|
||
|
listOfSelectedItems.add(voicemailUri);
|
||
|
}
|
||
|
}
|
||
|
outState.putStringArrayList(KEY_ACTION_MODE, listOfSelectedItems);
|
||
|
|
||
|
LogUtil.i(
|
||
|
"CallLogAdapter.onSaveInstanceState",
|
||
|
"saved: %d, selectedItemsSize:%d",
|
||
|
listOfSelectedItems.size(),
|
||
|
selectedItems.size());
|
||
|
}
|
||
|
|
||
|
public void onRestoreInstanceState(Bundle savedInstanceState) {
|
||
|
if (savedInstanceState != null) {
|
||
|
currentlyExpandedPosition =
|
||
|
savedInstanceState.getInt(KEY_EXPANDED_POSITION, RecyclerView.NO_POSITION);
|
||
|
currentlyExpandedRowId =
|
||
|
savedInstanceState.getLong(KEY_EXPANDED_ROW_ID, NO_EXPANDED_LIST_ITEM);
|
||
|
// Restoring multi selected entries
|
||
|
ArrayList<String> listOfSelectedItems =
|
||
|
savedInstanceState.getStringArrayList(KEY_ACTION_MODE);
|
||
|
if (listOfSelectedItems != null) {
|
||
|
LogUtil.i(
|
||
|
"CallLogAdapter.onRestoreInstanceState",
|
||
|
"restored selectedItemsList:%d",
|
||
|
listOfSelectedItems.size());
|
||
|
|
||
|
if (!listOfSelectedItems.isEmpty()) {
|
||
|
for (int i = 0; i < listOfSelectedItems.size(); i++) {
|
||
|
String voicemailUri = listOfSelectedItems.get(i);
|
||
|
int id = getVoicemailId(voicemailUri);
|
||
|
LogUtil.i(
|
||
|
"CallLogAdapter.onRestoreInstanceState",
|
||
|
"restoring selected index %d, id=%d, uri=%s ",
|
||
|
i,
|
||
|
id,
|
||
|
voicemailUri);
|
||
|
selectedItems.put(id, voicemailUri);
|
||
|
}
|
||
|
|
||
|
LogUtil.i(
|
||
|
"CallLogAdapter.onRestoreInstance",
|
||
|
"restored selectedItems %s",
|
||
|
selectedItems.toString());
|
||
|
updateActionBar();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/** Requery on background thread when {@link Cursor} changes. */
|
||
|
@Override
|
||
|
protected void onContentChanged() {
|
||
|
callFetcher.fetchCalls();
|
||
|
}
|
||
|
|
||
|
public void setLoading(boolean loading) {
|
||
|
this.loading = loading;
|
||
|
}
|
||
|
|
||
|
public boolean isEmpty() {
|
||
|
if (loading) {
|
||
|
// We don't want the empty state to show when loading.
|
||
|
return false;
|
||
|
} else {
|
||
|
return getItemCount() == 0;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public void clearFilteredNumbersCache() {
|
||
|
filteredNumberAsyncQueryHandler.clearCache();
|
||
|
}
|
||
|
|
||
|
public void onResume() {
|
||
|
contactsProviderMatchInfos.clear();
|
||
|
if (PermissionsUtil.hasPermission(activity, android.Manifest.permission.READ_CONTACTS)) {
|
||
|
contactInfoCache.start();
|
||
|
}
|
||
|
isSpamEnabled = SpamComponent.get(activity).spamSettings().isSpamEnabled();
|
||
|
getDuo().registerListener(this);
|
||
|
notifyDataSetChanged();
|
||
|
}
|
||
|
|
||
|
public void onPause() {
|
||
|
// The call log can be resumed/paused without loading any contacts. Don't log these events.
|
||
|
if (!contactsProviderMatchInfos.isEmpty()) {
|
||
|
Logger.get(activity).logContactsProviderMetrics(contactsProviderMatchInfos.values());
|
||
|
}
|
||
|
|
||
|
getDuo().unregisterListener(this);
|
||
|
pauseCache();
|
||
|
for (Uri uri : hiddenItemUris) {
|
||
|
CallLogAsyncTaskUtil.deleteVoicemail(activity, uri, null);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public void onStop() {
|
||
|
getEnrichedCallManager().clearCachedData();
|
||
|
}
|
||
|
|
||
|
public CallLogAlertManager getAlertManager() {
|
||
|
return callLogAlertManager;
|
||
|
}
|
||
|
|
||
|
@VisibleForTesting
|
||
|
/* package */ void pauseCache() {
|
||
|
contactInfoCache.stop();
|
||
|
callLogCache.reset();
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
protected void addGroups(Cursor cursor) {
|
||
|
callLogGroupBuilder.addGroups(cursor);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||
|
if (viewType == VIEW_TYPE_ALERT) {
|
||
|
return callLogAlertManager.createViewHolder(parent);
|
||
|
}
|
||
|
return createCallLogEntryViewHolder(parent);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Creates a new call log entry {@link ViewHolder}.
|
||
|
*
|
||
|
* @param parent the parent view.
|
||
|
* @return The {@link ViewHolder}.
|
||
|
*/
|
||
|
private ViewHolder createCallLogEntryViewHolder(ViewGroup parent) {
|
||
|
LayoutInflater inflater = LayoutInflater.from(activity);
|
||
|
View view = inflater.inflate(R.layout.call_log_list_item, parent, false);
|
||
|
CallLogListItemViewHolder viewHolder =
|
||
|
CallLogListItemViewHolder.create(
|
||
|
view,
|
||
|
activity,
|
||
|
blockReportSpamListener,
|
||
|
expandCollapseListener,
|
||
|
longPressListener,
|
||
|
actionModeStateChangedListener,
|
||
|
callLogCache,
|
||
|
callLogListItemHelper,
|
||
|
voicemailPlaybackPresenter);
|
||
|
|
||
|
viewHolder.callLogEntryView.setTag(viewHolder);
|
||
|
|
||
|
viewHolder.primaryActionView.setTag(viewHolder);
|
||
|
viewHolder.quickContactView.setTag(viewHolder);
|
||
|
|
||
|
return viewHolder;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Binds the views in the entry to the data in the call log. TODO: This gets called 20-30 times
|
||
|
* when Dialer starts up for a single call log entry and should not. It invokes cross-process
|
||
|
* methods and the repeat execution can get costly.
|
||
|
*
|
||
|
* @param viewHolder The view corresponding to this entry.
|
||
|
* @param position The position of the entry.
|
||
|
*/
|
||
|
@Override
|
||
|
public void onBindViewHolder(ViewHolder viewHolder, int position) {
|
||
|
Trace.beginSection("onBindViewHolder: " + position);
|
||
|
switch (getItemViewType(position)) {
|
||
|
case VIEW_TYPE_ALERT:
|
||
|
// Do nothing
|
||
|
break;
|
||
|
default:
|
||
|
bindCallLogListViewHolder(viewHolder, position);
|
||
|
break;
|
||
|
}
|
||
|
Trace.endSection();
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void onViewRecycled(ViewHolder viewHolder) {
|
||
|
if (viewHolder.getItemViewType() == VIEW_TYPE_CALLLOG) {
|
||
|
CallLogListItemViewHolder views = (CallLogListItemViewHolder) viewHolder;
|
||
|
updateCheckMarkedStatusOfEntry(views);
|
||
|
|
||
|
if (views.asyncTask != null) {
|
||
|
views.asyncTask.cancel(true);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void onViewAttachedToWindow(ViewHolder viewHolder) {
|
||
|
if (viewHolder.getItemViewType() == VIEW_TYPE_CALLLOG) {
|
||
|
((CallLogListItemViewHolder) viewHolder).isAttachedToWindow = true;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void onViewDetachedFromWindow(ViewHolder viewHolder) {
|
||
|
if (viewHolder.getItemViewType() == VIEW_TYPE_CALLLOG) {
|
||
|
((CallLogListItemViewHolder) viewHolder).isAttachedToWindow = false;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Binds the view holder for the call log list item view.
|
||
|
*
|
||
|
* @param viewHolder The call log list item view holder.
|
||
|
* @param position The position of the list item.
|
||
|
*/
|
||
|
private void bindCallLogListViewHolder(final ViewHolder viewHolder, final int position) {
|
||
|
Cursor c = (Cursor) getItem(position);
|
||
|
if (c == null) {
|
||
|
return;
|
||
|
}
|
||
|
CallLogListItemViewHolder views = (CallLogListItemViewHolder) viewHolder;
|
||
|
updateCheckMarkedStatusOfEntry(views);
|
||
|
|
||
|
views.isLoaded = false;
|
||
|
int groupSize = getGroupSize(position);
|
||
|
CallDetailsEntries callDetailsEntries = createCallDetailsEntries(c, groupSize);
|
||
|
PhoneCallDetails details = createPhoneCallDetails(c, groupSize, views);
|
||
|
if (isHiddenRow(views.number, c.getLong(CallLogQuery.ID))) {
|
||
|
views.callLogEntryView.setVisibility(View.GONE);
|
||
|
views.dayGroupHeader.setVisibility(View.GONE);
|
||
|
return;
|
||
|
} else {
|
||
|
views.callLogEntryView.setVisibility(View.VISIBLE);
|
||
|
// dayGroupHeader will be restored after loadAndRender() if it is needed.
|
||
|
}
|
||
|
if (currentlyExpandedRowId == views.rowId) {
|
||
|
views.inflateActionViewStub();
|
||
|
}
|
||
|
loadAndRender(views, views.rowId, details, callDetailsEntries);
|
||
|
}
|
||
|
|
||
|
private void updateCheckMarkedStatusOfEntry(CallLogListItemViewHolder views) {
|
||
|
if (selectedItems.size() > 0 && views.voicemailUri != null) {
|
||
|
int id = getVoicemailId(views.voicemailUri);
|
||
|
if (selectedItems.get(id) != null) {
|
||
|
checkMarkCallLogEntry(views);
|
||
|
} else {
|
||
|
uncheckMarkCallLogEntry(views, id);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private boolean isHiddenRow(@Nullable String number, long rowId) {
|
||
|
if (isHideableEmergencyNumberRow(number)) {
|
||
|
return true;
|
||
|
}
|
||
|
if (hiddenRowIds.contains(rowId)) {
|
||
|
return true;
|
||
|
}
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
private boolean isHideableEmergencyNumberRow(@Nullable String number) {
|
||
|
if (!ConfigProviderComponent.get(activity)
|
||
|
.getConfigProvider()
|
||
|
.getBoolean(FILTER_EMERGENCY_CALLS_FLAG, false)) {
|
||
|
return false;
|
||
|
}
|
||
|
return number != null && PhoneNumberUtils.isEmergencyNumber(number);
|
||
|
}
|
||
|
|
||
|
private void loadAndRender(
|
||
|
final CallLogListItemViewHolder viewHolder,
|
||
|
final long rowId,
|
||
|
final PhoneCallDetails details,
|
||
|
final CallDetailsEntries callDetailsEntries) {
|
||
|
LogUtil.d("CallLogAdapter.loadAndRender", "position: %d", viewHolder.getAdapterPosition());
|
||
|
// Reset block and spam information since this view could be reused which may contain
|
||
|
// outdated data.
|
||
|
viewHolder.isSpam = false;
|
||
|
viewHolder.blockId = null;
|
||
|
viewHolder.isSpamFeatureEnabled = false;
|
||
|
|
||
|
// Attempt to set the isCallComposerCapable field. If capabilities are unknown for this number,
|
||
|
// the value will be false while capabilities are requested. mExpandCollapseListener will
|
||
|
// attempt to set the field properly in that case
|
||
|
viewHolder.isCallComposerCapable = isCallComposerCapable(viewHolder.number);
|
||
|
viewHolder.setDetailedPhoneDetails(callDetailsEntries);
|
||
|
final AsyncTask<Void, Void, Boolean> loadDataTask =
|
||
|
new AsyncTask<Void, Void, Boolean>() {
|
||
|
@Override
|
||
|
protected Boolean doInBackground(Void... params) {
|
||
|
viewHolder.blockId =
|
||
|
filteredNumberAsyncQueryHandler.getBlockedIdSynchronous(
|
||
|
viewHolder.number, viewHolder.countryIso);
|
||
|
details.isBlocked = viewHolder.blockId != null;
|
||
|
if (isCancelled()) {
|
||
|
return false;
|
||
|
}
|
||
|
if (isSpamEnabled) {
|
||
|
viewHolder.isSpamFeatureEnabled = true;
|
||
|
// Only display the call as a spam call if there are incoming calls in the list.
|
||
|
// Call log cards with only outgoing calls should never be displayed as spam.
|
||
|
viewHolder.isSpam =
|
||
|
details.hasIncomingCalls()
|
||
|
&& SpamComponent.get(activity)
|
||
|
.spam()
|
||
|
.checkSpamStatusSynchronous(viewHolder.number, viewHolder.countryIso);
|
||
|
details.isSpam = viewHolder.isSpam;
|
||
|
}
|
||
|
return !isCancelled() && loadData(viewHolder, rowId, details);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
protected void onPostExecute(Boolean success) {
|
||
|
viewHolder.isLoaded = true;
|
||
|
if (success) {
|
||
|
viewHolder.callbackAction = getCallbackAction(viewHolder.rowId);
|
||
|
int currentDayGroup = getDayGroup(viewHolder.rowId);
|
||
|
if (currentDayGroup != details.previousGroup) {
|
||
|
viewHolder.dayGroupHeaderVisibility = View.VISIBLE;
|
||
|
viewHolder.dayGroupHeaderText = getGroupDescription(currentDayGroup);
|
||
|
} else {
|
||
|
viewHolder.dayGroupHeaderVisibility = View.GONE;
|
||
|
}
|
||
|
render(viewHolder, details, rowId);
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
|
||
|
viewHolder.asyncTask = loadDataTask;
|
||
|
asyncTaskExecutor.submit(LOAD_DATA_TASK_IDENTIFIER, loadDataTask);
|
||
|
}
|
||
|
|
||
|
@MainThread
|
||
|
private boolean isCallComposerCapable(@Nullable String number) {
|
||
|
if (number == null) {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
EnrichedCallCapabilities capabilities = getEnrichedCallManager().getCapabilities(number);
|
||
|
if (capabilities == null) {
|
||
|
getEnrichedCallManager().requestCapabilities(number);
|
||
|
return false;
|
||
|
}
|
||
|
return capabilities.isCallComposerCapable();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Initialize PhoneCallDetails by reading all data from cursor. This method must be run on main
|
||
|
* thread since cursor is not thread safe.
|
||
|
*/
|
||
|
@MainThread
|
||
|
private PhoneCallDetails createPhoneCallDetails(
|
||
|
Cursor cursor, int count, final CallLogListItemViewHolder views) {
|
||
|
Assert.isMainThread();
|
||
|
final String number = cursor.getString(CallLogQuery.NUMBER);
|
||
|
final String postDialDigits = cursor.getString(CallLogQuery.POST_DIAL_DIGITS);
|
||
|
final String viaNumber = cursor.getString(CallLogQuery.VIA_NUMBER);
|
||
|
final int numberPresentation = cursor.getInt(CallLogQuery.NUMBER_PRESENTATION);
|
||
|
final ContactInfo cachedContactInfo = ContactInfoHelper.getContactInfo(cursor);
|
||
|
final int transcriptionState =
|
||
|
(VERSION.SDK_INT >= VERSION_CODES.O)
|
||
|
? cursor.getInt(CallLogQuery.TRANSCRIPTION_STATE)
|
||
|
: VoicemailCompat.TRANSCRIPTION_NOT_STARTED;
|
||
|
final PhoneCallDetails details =
|
||
|
new PhoneCallDetails(number, numberPresentation, postDialDigits);
|
||
|
details.viaNumber = viaNumber;
|
||
|
details.countryIso = cursor.getString(CallLogQuery.COUNTRY_ISO);
|
||
|
details.date = cursor.getLong(CallLogQuery.DATE);
|
||
|
details.duration = cursor.getLong(CallLogQuery.DURATION);
|
||
|
details.features = getCallFeatures(cursor, count);
|
||
|
details.geocode = cursor.getString(CallLogQuery.GEOCODED_LOCATION);
|
||
|
details.transcription = cursor.getString(CallLogQuery.TRANSCRIPTION);
|
||
|
details.transcriptionState = transcriptionState;
|
||
|
details.callTypes = getCallTypes(cursor, count);
|
||
|
|
||
|
details.accountComponentName = cursor.getString(CallLogQuery.ACCOUNT_COMPONENT_NAME);
|
||
|
details.accountId = cursor.getString(CallLogQuery.ACCOUNT_ID);
|
||
|
details.cachedContactInfo = cachedContactInfo;
|
||
|
|
||
|
if (!cursor.isNull(CallLogQuery.DATA_USAGE)) {
|
||
|
details.dataUsage = cursor.getLong(CallLogQuery.DATA_USAGE);
|
||
|
}
|
||
|
|
||
|
views.rowId = cursor.getLong(CallLogQuery.ID);
|
||
|
// Stash away the Ids of the calls so that we can support deleting a row in the call log.
|
||
|
views.callIds = getCallIds(cursor, count);
|
||
|
details.previousGroup = getPreviousDayGroup(cursor);
|
||
|
|
||
|
// Store values used when the actions ViewStub is inflated on expansion.
|
||
|
views.number = number;
|
||
|
views.countryIso = details.countryIso;
|
||
|
views.postDialDigits = details.postDialDigits;
|
||
|
views.numberPresentation = numberPresentation;
|
||
|
|
||
|
if (details.callTypes[0] == CallLog.Calls.VOICEMAIL_TYPE
|
||
|
|| details.callTypes[0] == CallLog.Calls.MISSED_TYPE) {
|
||
|
details.isRead = cursor.getInt(CallLogQuery.IS_READ) == 1;
|
||
|
}
|
||
|
views.callType = cursor.getInt(CallLogQuery.CALL_TYPE);
|
||
|
views.voicemailUri = cursor.getString(CallLogQuery.VOICEMAIL_URI);
|
||
|
details.voicemailUri = views.voicemailUri;
|
||
|
|
||
|
return details;
|
||
|
}
|
||
|
|
||
|
@MainThread
|
||
|
private CallDetailsEntries createCallDetailsEntries(Cursor cursor, int count) {
|
||
|
Assert.isMainThread();
|
||
|
int position = cursor.getPosition();
|
||
|
CallDetailsEntries.Builder entries = CallDetailsEntries.newBuilder();
|
||
|
for (int i = 0; i < count; i++) {
|
||
|
CallDetailsEntry.Builder entry =
|
||
|
CallDetailsEntry.newBuilder()
|
||
|
.setCallId(cursor.getLong(CallLogQuery.ID))
|
||
|
.setCallType(cursor.getInt(CallLogQuery.CALL_TYPE))
|
||
|
.setDataUsage(cursor.getLong(CallLogQuery.DATA_USAGE))
|
||
|
.setDate(cursor.getLong(CallLogQuery.DATE))
|
||
|
.setDuration(cursor.getLong(CallLogQuery.DURATION))
|
||
|
.setFeatures(cursor.getInt(CallLogQuery.FEATURES))
|
||
|
|
||
|
.setCallMappingId(String.valueOf(cursor.getLong(CallLogQuery.DATE)));
|
||
|
|
||
|
|
||
|
String phoneAccountComponentName = cursor.getString(CallLogQuery.ACCOUNT_COMPONENT_NAME);
|
||
|
if (DuoComponent.get(activity).getDuo().isDuoAccount(phoneAccountComponentName)) {
|
||
|
entry.setIsDuoCall(true);
|
||
|
}
|
||
|
|
||
|
entries.addEntries(entry.build());
|
||
|
cursor.moveToNext();
|
||
|
}
|
||
|
cursor.moveToPosition(position);
|
||
|
return entries.build();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Load data for call log. Any expensive operation should be put here to avoid blocking main
|
||
|
* thread. Do NOT put any cursor operation here since it's not thread safe.
|
||
|
*/
|
||
|
@WorkerThread
|
||
|
private boolean loadData(CallLogListItemViewHolder views, long rowId, PhoneCallDetails details) {
|
||
|
Assert.isWorkerThread();
|
||
|
if (rowId != views.rowId) {
|
||
|
LogUtil.i(
|
||
|
"CallLogAdapter.loadData",
|
||
|
"rowId of viewHolder changed after load task is issued, aborting load");
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
final PhoneAccountHandle accountHandle =
|
||
|
TelecomUtil.composePhoneAccountHandle(details.accountComponentName, details.accountId);
|
||
|
|
||
|
final boolean isVoicemailNumber = callLogCache.isVoicemailNumber(accountHandle, details.number);
|
||
|
|
||
|
// Note: Binding of the action buttons is done as required in configureActionViews when the
|
||
|
// user expands the actions ViewStub.
|
||
|
|
||
|
ContactInfo info = ContactInfo.EMPTY;
|
||
|
if (PhoneNumberHelper.canPlaceCallsTo(details.number, details.numberPresentation)
|
||
|
&& !isVoicemailNumber) {
|
||
|
// Lookup contacts with this number
|
||
|
// Only do remote lookup in first 5 rows.
|
||
|
int position = views.getAdapterPosition();
|
||
|
info =
|
||
|
contactInfoCache.getValue(
|
||
|
details.number + details.postDialDigits,
|
||
|
details.countryIso,
|
||
|
details.cachedContactInfo,
|
||
|
position
|
||
|
< ConfigProviderComponent.get(activity)
|
||
|
.getConfigProvider()
|
||
|
.getLong("number_of_call_to_do_remote_lookup", 5L));
|
||
|
logCp2Metrics(details, info);
|
||
|
}
|
||
|
CharSequence formattedNumber =
|
||
|
info.formattedNumber == null
|
||
|
? null
|
||
|
: PhoneNumberUtils.createTtsSpannable(info.formattedNumber);
|
||
|
details.updateDisplayNumber(activity, formattedNumber, isVoicemailNumber);
|
||
|
|
||
|
views.displayNumber = details.displayNumber;
|
||
|
views.accountHandle = accountHandle;
|
||
|
details.accountHandle = accountHandle;
|
||
|
|
||
|
if (!TextUtils.isEmpty(info.name) || !TextUtils.isEmpty(info.nameAlternative)) {
|
||
|
details.contactUri = info.lookupUri;
|
||
|
details.namePrimary = info.name;
|
||
|
details.nameAlternative = info.nameAlternative;
|
||
|
details.nameDisplayOrder =
|
||
|
ContactsComponent.get(activity).contactDisplayPreferences().getDisplayOrder();
|
||
|
details.numberType = info.type;
|
||
|
details.numberLabel = info.label;
|
||
|
details.photoUri = info.photoUri;
|
||
|
details.sourceType = info.sourceType;
|
||
|
details.objectId = info.objectId;
|
||
|
details.contactUserType = info.userType;
|
||
|
}
|
||
|
LogUtil.d(
|
||
|
"CallLogAdapter.loadData",
|
||
|
"position:%d, update geo info: %s, cequint caller id geo: %s, photo uri: %s <- %s",
|
||
|
views.getAdapterPosition(),
|
||
|
details.geocode,
|
||
|
info.geoDescription,
|
||
|
details.photoUri,
|
||
|
info.photoUri);
|
||
|
if (!TextUtils.isEmpty(info.geoDescription)) {
|
||
|
details.geocode = info.geoDescription;
|
||
|
}
|
||
|
|
||
|
views.info = info;
|
||
|
views.numberType = getNumberType(activity.getResources(), details);
|
||
|
|
||
|
callLogListItemHelper.updatePhoneCallDetails(details);
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
private static String getNumberType(Resources res, PhoneCallDetails details) {
|
||
|
// Label doesn't make much sense if the information is coming from CNAP or Cequint Caller ID.
|
||
|
if (details.sourceType == ContactSource.Type.SOURCE_TYPE_CNAP
|
||
|
|| details.sourceType == ContactSource.Type.SOURCE_TYPE_CEQUINT_CALLER_ID) {
|
||
|
return "";
|
||
|
}
|
||
|
// Returns empty label instead of "custom" if the custom label is empty.
|
||
|
if (details.numberType == Phone.TYPE_CUSTOM && TextUtils.isEmpty(details.numberLabel)) {
|
||
|
return "";
|
||
|
}
|
||
|
return (String) Phone.getTypeLabel(res, details.numberType, details.numberLabel);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Render item view given position. This is running on UI thread so DO NOT put any expensive
|
||
|
* operation into it.
|
||
|
*/
|
||
|
@MainThread
|
||
|
private void render(CallLogListItemViewHolder views, PhoneCallDetails details, long rowId) {
|
||
|
Assert.isMainThread();
|
||
|
if (rowId != views.rowId) {
|
||
|
LogUtil.i(
|
||
|
"CallLogAdapter.render",
|
||
|
"rowId of viewHolder changed after load task is issued, aborting render");
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Default case: an item in the call log.
|
||
|
views.primaryActionView.setVisibility(View.VISIBLE);
|
||
|
views.workIconView.setVisibility(
|
||
|
details.contactUserType == ContactsUtils.USER_TYPE_WORK ? View.VISIBLE : View.GONE);
|
||
|
|
||
|
if (selectAllMode && views.voicemailUri != null) {
|
||
|
selectedItems.put(getVoicemailId(views.voicemailUri), views.voicemailUri);
|
||
|
}
|
||
|
if (deselectAllMode && views.voicemailUri != null) {
|
||
|
selectedItems.delete(getVoicemailId(views.voicemailUri));
|
||
|
}
|
||
|
if (views.voicemailUri != null
|
||
|
&& selectedItems.get(getVoicemailId(views.voicemailUri)) != null) {
|
||
|
views.checkBoxView.setVisibility(View.VISIBLE);
|
||
|
views.quickContactView.setVisibility(View.GONE);
|
||
|
} else if (views.voicemailUri != null) {
|
||
|
views.checkBoxView.setVisibility(View.GONE);
|
||
|
views.quickContactView.setVisibility(View.VISIBLE);
|
||
|
}
|
||
|
callLogListItemHelper.setPhoneCallDetails(views, details);
|
||
|
if (currentlyExpandedRowId == views.rowId) {
|
||
|
// In case ViewHolders were added/removed, update the expanded position if the rowIds
|
||
|
// match so that we can restore the correct expanded state on rebind.
|
||
|
currentlyExpandedPosition = views.getAdapterPosition();
|
||
|
views.showActions(true);
|
||
|
} else {
|
||
|
views.showActions(false);
|
||
|
}
|
||
|
views.dayGroupHeader.setVisibility(views.dayGroupHeaderVisibility);
|
||
|
views.dayGroupHeader.setText(views.dayGroupHeaderText);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public int getItemCount() {
|
||
|
return super.getItemCount() + (callLogAlertManager.isEmpty() ? 0 : 1);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public int getItemViewType(int position) {
|
||
|
if (position == ALERT_POSITION && !callLogAlertManager.isEmpty()) {
|
||
|
return VIEW_TYPE_ALERT;
|
||
|
}
|
||
|
return VIEW_TYPE_CALLLOG;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Retrieves an item at the specified position, taking into account the presence of a promo card.
|
||
|
*
|
||
|
* @param position The position to retrieve.
|
||
|
* @return The item at that position.
|
||
|
*/
|
||
|
@Override
|
||
|
public Object getItem(int position) {
|
||
|
return super.getItem(position - (callLogAlertManager.isEmpty() ? 0 : 1));
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public long getItemId(int position) {
|
||
|
Cursor cursor = (Cursor) getItem(position);
|
||
|
if (cursor != null) {
|
||
|
return cursor.getLong(CallLogQuery.ID);
|
||
|
} else {
|
||
|
return 0;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public int getGroupSize(int position) {
|
||
|
return super.getGroupSize(position - (callLogAlertManager.isEmpty() ? 0 : 1));
|
||
|
}
|
||
|
|
||
|
protected boolean isCallLogActivity() {
|
||
|
return activityType == ACTIVITY_TYPE_CALL_LOG;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* In order to implement the "undo" function, when a voicemail is "deleted" i.e. when the user
|
||
|
* clicks the delete button, the deleted item is temporarily hidden from the list. If a user
|
||
|
* clicks delete on a second item before the first item's undo option has expired, the first item
|
||
|
* is immediately deleted so that only one item can be "undoed" at a time.
|
||
|
*/
|
||
|
@Override
|
||
|
public void onVoicemailDeleted(CallLogListItemViewHolder viewHolder, Uri uri) {
|
||
|
hiddenRowIds.add(viewHolder.rowId);
|
||
|
// Save the new hidden item uri in case the activity is suspend before the undo has timed out.
|
||
|
hiddenItemUris.add(uri);
|
||
|
|
||
|
collapseExpandedCard();
|
||
|
notifyItemChanged(viewHolder.getAdapterPosition());
|
||
|
// The next item might have to update its day group label
|
||
|
notifyItemChanged(viewHolder.getAdapterPosition() + 1);
|
||
|
}
|
||
|
|
||
|
private void collapseExpandedCard() {
|
||
|
currentlyExpandedRowId = NO_EXPANDED_LIST_ITEM;
|
||
|
currentlyExpandedPosition = RecyclerView.NO_POSITION;
|
||
|
}
|
||
|
|
||
|
/** When the list is changing all stored position is no longer valid. */
|
||
|
public void invalidatePositions() {
|
||
|
currentlyExpandedPosition = RecyclerView.NO_POSITION;
|
||
|
}
|
||
|
|
||
|
/** When the user clicks "undo", the hidden item is unhidden. */
|
||
|
@Override
|
||
|
public void onVoicemailDeleteUndo(long rowId, int adapterPosition, Uri uri) {
|
||
|
hiddenItemUris.remove(uri);
|
||
|
hiddenRowIds.remove(rowId);
|
||
|
notifyItemChanged(adapterPosition);
|
||
|
// The next item might have to update its day group label
|
||
|
notifyItemChanged(adapterPosition + 1);
|
||
|
}
|
||
|
|
||
|
/** This callback signifies that a database deletion has completed. */
|
||
|
@Override
|
||
|
public void onVoicemailDeletedInDatabase(long rowId, Uri uri) {
|
||
|
hiddenItemUris.remove(uri);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Retrieves the day group of the previous call in the call log. Used to determine if the day
|
||
|
* group has changed and to trigger display of the day group text.
|
||
|
*
|
||
|
* @param cursor The call log cursor.
|
||
|
* @return The previous day group, or DAY_GROUP_NONE if this is the first call.
|
||
|
*/
|
||
|
private int getPreviousDayGroup(Cursor cursor) {
|
||
|
// We want to restore the position in the cursor at the end.
|
||
|
int startingPosition = cursor.getPosition();
|
||
|
moveToPreviousNonHiddenRow(cursor);
|
||
|
if (cursor.isBeforeFirst()) {
|
||
|
cursor.moveToPosition(startingPosition);
|
||
|
return CallLogGroupBuilder.DAY_GROUP_NONE;
|
||
|
}
|
||
|
int result = getDayGroup(cursor.getLong(CallLogQuery.ID));
|
||
|
cursor.moveToPosition(startingPosition);
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
private void moveToPreviousNonHiddenRow(Cursor cursor) {
|
||
|
while (cursor.moveToPrevious() && hiddenRowIds.contains(cursor.getLong(CallLogQuery.ID))) {}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Given a call ID, look up its callback action. Callback action data are populated in {@link
|
||
|
* com.android.dialer.app.calllog.CallLogGroupBuilder}.
|
||
|
*
|
||
|
* @param callId The call ID to retrieve the callback action.
|
||
|
* @return The callback action for the call.
|
||
|
*/
|
||
|
@MainThread
|
||
|
private int getCallbackAction(long callId) {
|
||
|
Integer result = callbackActions.get(callId);
|
||
|
if (result != null) {
|
||
|
return result;
|
||
|
}
|
||
|
return CallbackAction.NONE;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Given a call ID, look up the day group the call belongs to. Day group data are populated in
|
||
|
* {@link com.android.dialer.app.calllog.CallLogGroupBuilder}.
|
||
|
*
|
||
|
* @param callId The call ID to retrieve the day group.
|
||
|
* @return The day group for the call.
|
||
|
*/
|
||
|
@MainThread
|
||
|
private int getDayGroup(long callId) {
|
||
|
Integer result = dayGroups.get(callId);
|
||
|
if (result != null) {
|
||
|
return result;
|
||
|
}
|
||
|
return CallLogGroupBuilder.DAY_GROUP_NONE;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns the call types for the given number of items in the cursor.
|
||
|
*
|
||
|
* <p>It uses the next {@code count} rows in the cursor to extract the types.
|
||
|
*
|
||
|
* <p>It position in the cursor is unchanged by this function.
|
||
|
*/
|
||
|
private static int[] getCallTypes(Cursor cursor, int count) {
|
||
|
int position = cursor.getPosition();
|
||
|
int[] callTypes = new int[count];
|
||
|
for (int index = 0; index < count; ++index) {
|
||
|
callTypes[index] = cursor.getInt(CallLogQuery.CALL_TYPE);
|
||
|
cursor.moveToNext();
|
||
|
}
|
||
|
cursor.moveToPosition(position);
|
||
|
return callTypes;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Determine the features which were enabled for any of the calls that make up a call log entry.
|
||
|
*
|
||
|
* @param cursor The cursor.
|
||
|
* @param count The number of calls for the current call log entry.
|
||
|
* @return The features.
|
||
|
*/
|
||
|
private int getCallFeatures(Cursor cursor, int count) {
|
||
|
int features = 0;
|
||
|
int position = cursor.getPosition();
|
||
|
for (int index = 0; index < count; ++index) {
|
||
|
features |= cursor.getInt(CallLogQuery.FEATURES);
|
||
|
cursor.moveToNext();
|
||
|
}
|
||
|
cursor.moveToPosition(position);
|
||
|
return features;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Sets whether processing of requests for contact details should be enabled.
|
||
|
*
|
||
|
* <p>This method should be called in tests to disable such processing of requests when not
|
||
|
* needed.
|
||
|
*/
|
||
|
@VisibleForTesting
|
||
|
void disableRequestProcessingForTest() {
|
||
|
// TODO: Remove this and test the cache directly.
|
||
|
contactInfoCache.disableRequestProcessing();
|
||
|
}
|
||
|
|
||
|
@VisibleForTesting
|
||
|
void injectContactInfoForTest(String number, String countryIso, ContactInfo contactInfo) {
|
||
|
// TODO: Remove this and test the cache directly.
|
||
|
contactInfoCache.injectContactInfoForTest(number, countryIso, contactInfo);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Stores the callback action associated with a call in the call log.
|
||
|
*
|
||
|
* @param rowId The row ID of the current call.
|
||
|
* @param callbackAction The current call's callback action.
|
||
|
*/
|
||
|
@Override
|
||
|
@MainThread
|
||
|
public void setCallbackAction(long rowId, @CallbackAction int callbackAction) {
|
||
|
callbackActions.put(rowId, callbackAction);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Stores the day group associated with a call in the call log.
|
||
|
*
|
||
|
* @param rowId The row ID of the current call.
|
||
|
* @param dayGroup The day group the call belongs in.
|
||
|
*/
|
||
|
@Override
|
||
|
@MainThread
|
||
|
public void setDayGroup(long rowId, int dayGroup) {
|
||
|
dayGroups.put(rowId, dayGroup);
|
||
|
}
|
||
|
|
||
|
/** Clears the day group associations on re-bind of the call log. */
|
||
|
@Override
|
||
|
@MainThread
|
||
|
public void clearDayGroups() {
|
||
|
dayGroups.clear();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Retrieves the call Ids represented by the current call log row.
|
||
|
*
|
||
|
* @param cursor Call log cursor to retrieve call Ids from.
|
||
|
* @param groupSize Number of calls associated with the current call log row.
|
||
|
* @return Array of call Ids.
|
||
|
*/
|
||
|
private long[] getCallIds(final Cursor cursor, final int groupSize) {
|
||
|
// We want to restore the position in the cursor at the end.
|
||
|
int startingPosition = cursor.getPosition();
|
||
|
long[] ids = new long[groupSize];
|
||
|
// Copy the ids of the rows in the group.
|
||
|
for (int index = 0; index < groupSize; ++index) {
|
||
|
ids[index] = cursor.getLong(CallLogQuery.ID);
|
||
|
cursor.moveToNext();
|
||
|
}
|
||
|
cursor.moveToPosition(startingPosition);
|
||
|
return ids;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Determines the description for a day group.
|
||
|
*
|
||
|
* @param group The day group to retrieve the description for.
|
||
|
* @return The day group description.
|
||
|
*/
|
||
|
private CharSequence getGroupDescription(int group) {
|
||
|
if (group == CallLogGroupBuilder.DAY_GROUP_TODAY) {
|
||
|
return activity.getResources().getString(R.string.call_log_header_today);
|
||
|
} else if (group == CallLogGroupBuilder.DAY_GROUP_YESTERDAY) {
|
||
|
return activity.getResources().getString(R.string.call_log_header_yesterday);
|
||
|
} else {
|
||
|
return activity.getResources().getString(R.string.call_log_header_other);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@NonNull
|
||
|
private EnrichedCallManager getEnrichedCallManager() {
|
||
|
return EnrichedCallComponent.get(activity).getEnrichedCallManager();
|
||
|
}
|
||
|
|
||
|
@NonNull
|
||
|
private Duo getDuo() {
|
||
|
return DuoComponent.get(activity).getDuo();
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void onDuoStateChanged() {
|
||
|
notifyDataSetChanged();
|
||
|
}
|
||
|
|
||
|
public void onAllSelected() {
|
||
|
selectAllMode = true;
|
||
|
deselectAllMode = false;
|
||
|
selectedItems.clear();
|
||
|
for (int i = 0; i < getItemCount(); i++) {
|
||
|
Cursor c = (Cursor) getItem(i);
|
||
|
if (c != null) {
|
||
|
Assert.checkArgument(CallLogQuery.VOICEMAIL_URI == c.getColumnIndex("voicemail_uri"));
|
||
|
String voicemailUri = c.getString(CallLogQuery.VOICEMAIL_URI);
|
||
|
selectedItems.put(getVoicemailId(voicemailUri), voicemailUri);
|
||
|
}
|
||
|
}
|
||
|
updateActionBar();
|
||
|
notifyDataSetChanged();
|
||
|
}
|
||
|
|
||
|
public void onAllDeselected() {
|
||
|
selectAllMode = false;
|
||
|
deselectAllMode = true;
|
||
|
selectedItems.clear();
|
||
|
updateActionBar();
|
||
|
notifyDataSetChanged();
|
||
|
}
|
||
|
|
||
|
@WorkerThread
|
||
|
private void logCp2Metrics(PhoneCallDetails details, ContactInfo contactInfo) {
|
||
|
if (details == null) {
|
||
|
return;
|
||
|
}
|
||
|
CharSequence inputNumber = details.number;
|
||
|
if (inputNumber == null) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
ContactsProviderMatchInfo.Builder matchInfo =
|
||
|
ContactsProviderMatchInfo.builder()
|
||
|
.setInputNumberLength(PhoneNumberUtils.normalizeNumber(inputNumber.toString()).length())
|
||
|
.setInputNumberHasPostdialDigits(
|
||
|
!PhoneNumberUtils.extractPostDialPortion(inputNumber.toString()).isEmpty()
|
||
|
|| (details.postDialDigits != null && !details.postDialDigits.isEmpty()));
|
||
|
|
||
|
PhoneNumberUtil phoneNumberUtil = PhoneNumberUtil.getInstance();
|
||
|
try {
|
||
|
PhoneNumber phoneNumber = phoneNumberUtil.parse(inputNumber, details.countryIso);
|
||
|
matchInfo.setInputNumberValid(phoneNumberUtil.isValidNumber(phoneNumber));
|
||
|
} catch (NumberParseException e) {
|
||
|
// Do nothing
|
||
|
matchInfo.setInputNumberValid(false);
|
||
|
}
|
||
|
|
||
|
if (contactInfo != null
|
||
|
&& contactInfo.number != null
|
||
|
&& contactInfo.sourceType == Type.SOURCE_TYPE_DIRECTORY) {
|
||
|
matchInfo
|
||
|
.setMatchedContact(true)
|
||
|
.setMatchedNumberLength(PhoneNumberUtils.normalizeNumber(contactInfo.number).length())
|
||
|
.setMatchedNumberHasPostdialDigits(
|
||
|
!PhoneNumberUtils.extractPostDialPortion(contactInfo.number).isEmpty());
|
||
|
}
|
||
|
|
||
|
contactsProviderMatchInfos.put(inputNumber.toString(), matchInfo.build());
|
||
|
}
|
||
|
|
||
|
/** Interface used to initiate a refresh of the content. */
|
||
|
public interface CallFetcher {
|
||
|
|
||
|
void fetchCalls();
|
||
|
}
|
||
|
|
||
|
/** Interface used to allow single tap multi select for contact photos. */
|
||
|
public interface OnActionModeStateChangedListener {
|
||
|
|
||
|
void onActionModeStateChanged(ActionMode mode, boolean isEnabled);
|
||
|
|
||
|
boolean isActionModeStateEnabled();
|
||
|
}
|
||
|
|
||
|
/** Interface used to hide the fragments. */
|
||
|
public interface MultiSelectRemoveView {
|
||
|
|
||
|
void showMultiSelectRemoveView(boolean show);
|
||
|
|
||
|
void setSelectAllModeToFalse();
|
||
|
|
||
|
void tapSelectAll();
|
||
|
}
|
||
|
}
|