1 /* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.dialer.app.calllog; 18 19 import android.app.Activity; 20 import android.content.ContentUris; 21 import android.content.DialogInterface; 22 import android.content.res.Resources; 23 import android.database.Cursor; 24 import android.net.Uri; 25 import android.os.AsyncTask; 26 import android.os.Build.VERSION; 27 import android.os.Build.VERSION_CODES; 28 import android.os.Bundle; 29 import android.os.Trace; 30 import android.provider.CallLog; 31 import android.provider.ContactsContract.CommonDataKinds.Phone; 32 import android.support.annotation.MainThread; 33 import android.support.annotation.NonNull; 34 import android.support.annotation.Nullable; 35 import android.support.annotation.VisibleForTesting; 36 import android.support.annotation.WorkerThread; 37 import android.support.v7.app.AlertDialog; 38 import android.support.v7.widget.RecyclerView; 39 import android.support.v7.widget.RecyclerView.ViewHolder; 40 import android.telecom.PhoneAccountHandle; 41 import android.text.TextUtils; 42 import android.util.ArrayMap; 43 import android.util.ArraySet; 44 import android.util.SparseArray; 45 import android.view.ActionMode; 46 import android.view.LayoutInflater; 47 import android.view.Menu; 48 import android.view.MenuInflater; 49 import android.view.MenuItem; 50 import android.view.View; 51 import android.view.ViewGroup; 52 import com.android.contacts.common.ContactsUtils; 53 import com.android.contacts.common.compat.PhoneNumberUtilsCompat; 54 import com.android.contacts.common.preference.ContactsPreferences; 55 import com.android.dialer.app.Bindings; 56 import com.android.dialer.app.DialtactsActivity; 57 import com.android.dialer.app.R; 58 import com.android.dialer.app.calllog.CallLogGroupBuilder.GroupCreator; 59 import com.android.dialer.app.calllog.calllogcache.CallLogCache; 60 import com.android.dialer.app.contactinfo.ContactInfoCache; 61 import com.android.dialer.app.voicemail.VoicemailPlaybackPresenter; 62 import com.android.dialer.app.voicemail.VoicemailPlaybackPresenter.OnVoicemailDeletedListener; 63 import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler; 64 import com.android.dialer.calldetails.CallDetailsEntries; 65 import com.android.dialer.calldetails.CallDetailsEntries.CallDetailsEntry; 66 import com.android.dialer.calllogutils.PhoneAccountUtils; 67 import com.android.dialer.calllogutils.PhoneCallDetails; 68 import com.android.dialer.common.Assert; 69 import com.android.dialer.common.ConfigProviderBindings; 70 import com.android.dialer.common.LogUtil; 71 import com.android.dialer.common.concurrent.AsyncTaskExecutor; 72 import com.android.dialer.common.concurrent.AsyncTaskExecutors; 73 import com.android.dialer.enrichedcall.EnrichedCallCapabilities; 74 import com.android.dialer.enrichedcall.EnrichedCallComponent; 75 import com.android.dialer.enrichedcall.EnrichedCallManager; 76 import com.android.dialer.enrichedcall.historyquery.proto.HistoryResult; 77 import com.android.dialer.lightbringer.Lightbringer; 78 import com.android.dialer.lightbringer.LightbringerComponent; 79 import com.android.dialer.lightbringer.LightbringerListener; 80 import com.android.dialer.logging.ContactSource; 81 import com.android.dialer.logging.DialerImpression; 82 import com.android.dialer.logging.Logger; 83 import com.android.dialer.phonenumbercache.CallLogQuery; 84 import com.android.dialer.phonenumbercache.ContactInfo; 85 import com.android.dialer.phonenumbercache.ContactInfoHelper; 86 import com.android.dialer.phonenumberutil.PhoneNumberHelper; 87 import com.android.dialer.spam.Spam; 88 import com.android.dialer.util.PermissionsUtil; 89 import java.util.Collections; 90 import java.util.List; 91 import java.util.Map; 92 import java.util.Set; 93 94 /** Adapter class to fill in data for the Call Log. */ 95 public class CallLogAdapter extends GroupingListAdapter 96 implements GroupCreator, OnVoicemailDeletedListener, LightbringerListener { 97 98 // Types of activities the call log adapter is used for 99 public static final int ACTIVITY_TYPE_CALL_LOG = 1; 100 public static final int ACTIVITY_TYPE_DIALTACTS = 2; 101 private static final int NO_EXPANDED_LIST_ITEM = -1; 102 public static final int ALERT_POSITION = 0; 103 private static final int VIEW_TYPE_ALERT = 1; 104 private static final int VIEW_TYPE_CALLLOG = 2; 105 106 private static final String KEY_EXPANDED_POSITION = "expanded_position"; 107 private static final String KEY_EXPANDED_ROW_ID = "expanded_row_id"; 108 109 public static final String LOAD_DATA_TASK_IDENTIFIER = "load_data"; 110 111 public static final String ENABLE_CALL_LOG_MULTI_SELECT = "enable_call_log_multiselect"; 112 public static final boolean ENABLE_CALL_LOG_MULTI_SELECT_FLAG = false; 113 114 protected final Activity mActivity; 115 protected final VoicemailPlaybackPresenter mVoicemailPlaybackPresenter; 116 /** Cache for repeated requests to Telecom/Telephony. */ 117 protected final CallLogCache mCallLogCache; 118 119 private final CallFetcher mCallFetcher; 120 @NonNull private final FilteredNumberAsyncQueryHandler mFilteredNumberAsyncQueryHandler; 121 private final int mActivityType; 122 123 /** Instance of helper class for managing views. */ 124 private final CallLogListItemHelper mCallLogListItemHelper; 125 /** Helper to group call log entries. */ 126 private final CallLogGroupBuilder mCallLogGroupBuilder; 127 128 private final AsyncTaskExecutor mAsyncTaskExecutor = AsyncTaskExecutors.createAsyncTaskExecutor(); 129 private ContactInfoCache mContactInfoCache; 130 // Tracks the position of the currently expanded list item. 131 private int mCurrentlyExpandedPosition = RecyclerView.NO_POSITION; 132 // Tracks the rowId of the currently expanded list item, so the position can be updated if there 133 // are any changes to the call log entries, such as additions or removals. 134 private long mCurrentlyExpandedRowId = NO_EXPANDED_LIST_ITEM; 135 136 private final CallLogAlertManager mCallLogAlertManager; 137 138 public ActionMode mActionMode = null; 139 private final SparseArray<String> selectedItems = new SparseArray<>(); 140 141 private final ActionMode.Callback mActionModeCallback = 142 new ActionMode.Callback() { 143 144 // Called when the action mode is created; startActionMode() was called 145 @Override 146 public boolean onCreateActionMode(ActionMode mode, Menu menu) { 147 mActionMode = mode; 148 // Inflate a menu resource providing context menu items 149 MenuInflater inflater = mode.getMenuInflater(); 150 inflater.inflate(R.menu.actionbar_delete, menu); 151 return true; 152 } 153 154 // Called each time the action mode is shown. Always called after onCreateActionMode, but 155 // may be called multiple times if the mode is invalidated. 156 @Override 157 public boolean onPrepareActionMode(ActionMode mode, Menu menu) { 158 return false; // Return false if nothing is done 159 } 160 161 // Called when the user selects a contextual menu item 162 @Override 163 public boolean onActionItemClicked(ActionMode mode, MenuItem item) { 164 if (item.getItemId() == R.id.action_bar_delete_menu_item) { 165 if (selectedItems.size() > 0) { 166 showDeleteSelectedItemsDialog(); 167 } 168 mode.finish(); 169 return true; 170 } else { 171 return false; 172 } 173 } 174 175 // Called when the user exits the action mode 176 @Override 177 public void onDestroyActionMode(ActionMode mode) { 178 selectedItems.clear(); 179 mActionMode = null; 180 notifyDataSetChanged(); 181 } 182 }; 183 184 // Todo (uabdullah): Use plurals http://b/37751831 185 private void showDeleteSelectedItemsDialog() { 186 AlertDialog.Builder builder = new AlertDialog.Builder(mActivity); 187 Assert.checkArgument(selectedItems.size() > 0); 188 String voicemailString = 189 selectedItems.size() == 1 190 ? mActivity.getResources().getString(R.string.voicemailMultiSelectVoicemail) 191 : mActivity.getResources().getString(R.string.voicemailMultiSelectVoicemails); 192 String deleteVoicemailTitle = 193 mActivity 194 .getResources() 195 .getString(R.string.voicemailMultiSelectDialogTitle, voicemailString); 196 SparseArray<String> voicemailsToDeleteOnConfirmation = selectedItems.clone(); 197 builder.setTitle(deleteVoicemailTitle); 198 199 builder.setPositiveButton( 200 mActivity.getResources().getString(R.string.voicemailMultiSelectDeleteConfirm), 201 new DialogInterface.OnClickListener() { 202 @Override 203 public void onClick(DialogInterface dialog, int id) { 204 deleteSelectedItems(voicemailsToDeleteOnConfirmation); 205 dialog.cancel(); 206 } 207 }); 208 209 builder.setNegativeButton( 210 mActivity.getResources().getString(R.string.voicemailMultiSelectDeleteCancel), 211 new DialogInterface.OnClickListener() { 212 @Override 213 public void onClick(DialogInterface dialog, int id) { 214 dialog.cancel(); 215 } 216 }); 217 218 AlertDialog dialog = builder.create(); 219 dialog.show(); 220 } 221 222 private void deleteSelectedItems(SparseArray<String> voicemailsToDelete) { 223 for (int i = 0; i < voicemailsToDelete.size(); i++) { 224 String voicemailUri = voicemailsToDelete.get(voicemailsToDelete.keyAt(i)); 225 CallLogAsyncTaskUtil.deleteVoicemail(mActivity, Uri.parse(voicemailUri), null); 226 } 227 } 228 229 private final View.OnLongClickListener mLongPressListener = 230 new View.OnLongClickListener() { 231 @Override 232 public boolean onLongClick(View v) { 233 if (ConfigProviderBindings.get(v.getContext()) 234 .getBoolean(ENABLE_CALL_LOG_MULTI_SELECT, ENABLE_CALL_LOG_MULTI_SELECT_FLAG) 235 && mVoicemailPlaybackPresenter != null) { 236 if (v.getId() == R.id.primary_action_view || v.getId() == R.id.quick_contact_photo) { 237 if (mActionMode == null) { 238 mActionMode = v.startActionMode(mActionModeCallback); 239 } 240 CallLogListItemViewHolder viewHolder = (CallLogListItemViewHolder) v.getTag(); 241 viewHolder.quickContactView.setVisibility(View.GONE); 242 viewHolder.checkBoxView.setVisibility(View.VISIBLE); 243 mExpandCollapseListener.onClick(v); 244 return true; 245 } 246 } 247 return true; 248 } 249 }; 250 251 /** The OnClickListener used to expand or collapse the action buttons of a call log entry. */ 252 private final View.OnClickListener mExpandCollapseListener = 253 new View.OnClickListener() { 254 @Override 255 public void onClick(View v) { 256 CallLogListItemViewHolder viewHolder = (CallLogListItemViewHolder) v.getTag(); 257 if (viewHolder == null) { 258 return; 259 } 260 if (mActionMode != null && viewHolder.voicemailUri != null) { 261 int id = getVoicemailId(viewHolder.voicemailUri); 262 if (selectedItems.get(id) != null) { 263 selectedItems.delete(id); 264 viewHolder.checkBoxView.setVisibility(View.GONE); 265 viewHolder.quickContactView.setVisibility(View.VISIBLE); 266 } else { 267 viewHolder.quickContactView.setVisibility(View.GONE); 268 viewHolder.checkBoxView.setVisibility(View.VISIBLE); 269 selectedItems.put(getVoicemailId(viewHolder.voicemailUri), viewHolder.voicemailUri); 270 } 271 272 if (selectedItems.size() == 0) { 273 mActionMode.finish(); 274 return; 275 } 276 mActionMode.setTitle(Integer.toString(selectedItems.size())); 277 return; 278 } 279 280 if (mVoicemailPlaybackPresenter != null) { 281 // Always reset the voicemail playback state on expand or collapse. 282 mVoicemailPlaybackPresenter.resetAll(); 283 } 284 285 // If enriched call capabilities were unknown on the initial load, 286 // viewHolder.isCallComposerCapable may be unset. Check here if we have the capabilities 287 // as a last attempt at getting them before showing the expanded view to the user 288 EnrichedCallCapabilities capabilities = 289 getEnrichedCallManager().getCapabilities(viewHolder.number); 290 viewHolder.isCallComposerCapable = 291 capabilities != null && capabilities.supportsCallComposer(); 292 generateAndMapNewCallDetailsEntriesHistoryResults( 293 viewHolder.number, 294 viewHolder.getDetailedPhoneDetails(), 295 getAllHistoricalData(viewHolder.number, viewHolder.getDetailedPhoneDetails())); 296 297 if (viewHolder.rowId == mCurrentlyExpandedRowId) { 298 // Hide actions, if the clicked item is the expanded item. 299 viewHolder.showActions(false); 300 301 mCurrentlyExpandedPosition = RecyclerView.NO_POSITION; 302 mCurrentlyExpandedRowId = NO_EXPANDED_LIST_ITEM; 303 } else { 304 if (viewHolder.callType == CallLog.Calls.MISSED_TYPE) { 305 CallLogAsyncTaskUtil.markCallAsRead(mActivity, viewHolder.callIds); 306 if (mActivityType == ACTIVITY_TYPE_DIALTACTS) { 307 ((DialtactsActivity) v.getContext()).updateTabUnreadCounts(); 308 } 309 } 310 expandViewHolderActions(viewHolder); 311 } 312 } 313 }; 314 315 private static int getVoicemailId(String voicemailUri) { 316 Assert.checkArgument(voicemailUri != null); 317 Assert.checkArgument(voicemailUri.length() > 0); 318 return (int) ContentUris.parseId(Uri.parse(voicemailUri)); 319 } 320 321 /** 322 * A list of {@link CallLogQuery#ID} that will be hidden. The hide might be temporary so instead 323 * if removing an item, it will be shown as an invisible view. This simplifies the calculation of 324 * item position. 325 */ 326 @NonNull private Set<Long> mHiddenRowIds = new ArraySet<>(); 327 /** 328 * Holds a list of URIs that are pending deletion or undo. If the activity ends before the undo 329 * timeout, all of the pending URIs will be deleted. 330 * 331 * <p>TODO: move this and OnVoicemailDeletedListener to somewhere like {@link 332 * VisualVoicemailCallLogFragment}. The CallLogAdapter does not need to know about what to do with 333 * hidden item or what to hide. 334 */ 335 @NonNull private final Set<Uri> mHiddenItemUris = new ArraySet<>(); 336 337 private CallLogListItemViewHolder.OnClickListener mBlockReportSpamListener; 338 /** 339 * Map, keyed by call Id, used to track the day group for a call. As call log entries are put into 340 * the primary call groups in {@link com.android.dialer.app.calllog.CallLogGroupBuilder}, they are 341 * also assigned a secondary "day group". This map tracks the day group assigned to all calls in 342 * the call log. This information is used to trigger the display of a day group header above the 343 * call log entry at the start of a day group. Note: Multiple calls are grouped into a single 344 * primary "call group" in the call log, and the cursor used to bind rows includes all of these 345 * calls. When determining if a day group change has occurred it is necessary to look at the last 346 * entry in the call log to determine its day group. This map provides a means of determining the 347 * previous day group without having to reverse the cursor to the start of the previous day call 348 * log entry. 349 */ 350 private Map<Long, Integer> mDayGroups = new ArrayMap<>(); 351 352 private boolean mLoading = true; 353 private ContactsPreferences mContactsPreferences; 354 355 private boolean mIsSpamEnabled; 356 357 public CallLogAdapter( 358 Activity activity, 359 ViewGroup alertContainer, 360 CallFetcher callFetcher, 361 CallLogCache callLogCache, 362 ContactInfoCache contactInfoCache, 363 VoicemailPlaybackPresenter voicemailPlaybackPresenter, 364 @NonNull FilteredNumberAsyncQueryHandler filteredNumberAsyncQueryHandler, 365 int activityType) { 366 super(); 367 368 mActivity = activity; 369 mCallFetcher = callFetcher; 370 mVoicemailPlaybackPresenter = voicemailPlaybackPresenter; 371 if (mVoicemailPlaybackPresenter != null) { 372 mVoicemailPlaybackPresenter.setOnVoicemailDeletedListener(this); 373 } 374 375 mActivityType = activityType; 376 377 mContactInfoCache = contactInfoCache; 378 379 if (!PermissionsUtil.hasContactsReadPermissions(activity)) { 380 mContactInfoCache.disableRequestProcessing(); 381 } 382 383 Resources resources = mActivity.getResources(); 384 385 mCallLogCache = callLogCache; 386 387 PhoneCallDetailsHelper phoneCallDetailsHelper = 388 new PhoneCallDetailsHelper(mActivity, resources, mCallLogCache); 389 mCallLogListItemHelper = 390 new CallLogListItemHelper(phoneCallDetailsHelper, resources, mCallLogCache); 391 mCallLogGroupBuilder = new CallLogGroupBuilder(this); 392 mFilteredNumberAsyncQueryHandler = Assert.isNotNull(filteredNumberAsyncQueryHandler); 393 394 mContactsPreferences = new ContactsPreferences(mActivity); 395 396 mBlockReportSpamListener = 397 new BlockReportSpamListener( 398 mActivity, 399 ((Activity) mActivity).getFragmentManager(), 400 this, 401 mFilteredNumberAsyncQueryHandler); 402 setHasStableIds(true); 403 404 mCallLogAlertManager = 405 new CallLogAlertManager(this, LayoutInflater.from(mActivity), alertContainer); 406 } 407 408 private void expandViewHolderActions(CallLogListItemViewHolder viewHolder) { 409 if (!TextUtils.isEmpty(viewHolder.voicemailUri)) { 410 Logger.get(mActivity).logImpression(DialerImpression.Type.VOICEMAIL_EXPAND_ENTRY); 411 } 412 413 int lastExpandedPosition = mCurrentlyExpandedPosition; 414 // Show the actions for the clicked list item. 415 viewHolder.showActions(true); 416 mCurrentlyExpandedPosition = viewHolder.getAdapterPosition(); 417 mCurrentlyExpandedRowId = viewHolder.rowId; 418 419 // If another item is expanded, notify it that it has changed. Its actions will be 420 // hidden when it is re-binded because we change mCurrentlyExpandedRowId above. 421 if (lastExpandedPosition != RecyclerView.NO_POSITION) { 422 notifyItemChanged(lastExpandedPosition); 423 } 424 } 425 426 public void onSaveInstanceState(Bundle outState) { 427 outState.putInt(KEY_EXPANDED_POSITION, mCurrentlyExpandedPosition); 428 outState.putLong(KEY_EXPANDED_ROW_ID, mCurrentlyExpandedRowId); 429 } 430 431 public void onRestoreInstanceState(Bundle savedInstanceState) { 432 if (savedInstanceState != null) { 433 mCurrentlyExpandedPosition = 434 savedInstanceState.getInt(KEY_EXPANDED_POSITION, RecyclerView.NO_POSITION); 435 mCurrentlyExpandedRowId = 436 savedInstanceState.getLong(KEY_EXPANDED_ROW_ID, NO_EXPANDED_LIST_ITEM); 437 } 438 } 439 440 /** Requery on background thread when {@link Cursor} changes. */ 441 @Override 442 protected void onContentChanged() { 443 mCallFetcher.fetchCalls(); 444 } 445 446 public void setLoading(boolean loading) { 447 mLoading = loading; 448 } 449 450 public boolean isEmpty() { 451 if (mLoading) { 452 // We don't want the empty state to show when loading. 453 return false; 454 } else { 455 return getItemCount() == 0; 456 } 457 } 458 459 public void clearFilteredNumbersCache() { 460 mFilteredNumberAsyncQueryHandler.clearCache(); 461 } 462 463 public void onResume() { 464 if (PermissionsUtil.hasPermission(mActivity, android.Manifest.permission.READ_CONTACTS)) { 465 mContactInfoCache.start(); 466 } 467 mContactsPreferences.refreshValue(ContactsPreferences.DISPLAY_ORDER_KEY); 468 mIsSpamEnabled = Spam.get(mActivity).isSpamEnabled(); 469 getLightbringer().registerListener(this); 470 notifyDataSetChanged(); 471 } 472 473 public void onPause() { 474 getLightbringer().unregisterListener(this); 475 pauseCache(); 476 for (Uri uri : mHiddenItemUris) { 477 CallLogAsyncTaskUtil.deleteVoicemail(mActivity, uri, null); 478 } 479 } 480 481 public void onStop() { 482 getEnrichedCallManager().clearCachedData(); 483 } 484 485 public CallLogAlertManager getAlertManager() { 486 return mCallLogAlertManager; 487 } 488 489 @VisibleForTesting 490 /* package */ void pauseCache() { 491 mContactInfoCache.stop(); 492 mCallLogCache.reset(); 493 } 494 495 @Override 496 protected void addGroups(Cursor cursor) { 497 mCallLogGroupBuilder.addGroups(cursor); 498 } 499 500 @Override 501 public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 502 if (viewType == VIEW_TYPE_ALERT) { 503 return mCallLogAlertManager.createViewHolder(parent); 504 } 505 return createCallLogEntryViewHolder(parent); 506 } 507 508 /** 509 * Creates a new call log entry {@link ViewHolder}. 510 * 511 * @param parent the parent view. 512 * @return The {@link ViewHolder}. 513 */ 514 private ViewHolder createCallLogEntryViewHolder(ViewGroup parent) { 515 LayoutInflater inflater = LayoutInflater.from(mActivity); 516 View view = inflater.inflate(R.layout.call_log_list_item, parent, false); 517 CallLogListItemViewHolder viewHolder = 518 CallLogListItemViewHolder.create( 519 view, 520 mActivity, 521 mBlockReportSpamListener, 522 mExpandCollapseListener, 523 mLongPressListener, 524 mCallLogCache, 525 mCallLogListItemHelper, 526 mVoicemailPlaybackPresenter); 527 528 viewHolder.callLogEntryView.setTag(viewHolder); 529 530 viewHolder.primaryActionView.setTag(viewHolder); 531 viewHolder.quickContactView.setTag(viewHolder); 532 533 return viewHolder; 534 } 535 536 /** 537 * Binds the views in the entry to the data in the call log. TODO: This gets called 20-30 times 538 * when Dialer starts up for a single call log entry and should not. It invokes cross-process 539 * methods and the repeat execution can get costly. 540 * 541 * @param viewHolder The view corresponding to this entry. 542 * @param position The position of the entry. 543 */ 544 @Override 545 public void onBindViewHolder(ViewHolder viewHolder, int position) { 546 Trace.beginSection("onBindViewHolder: " + position); 547 switch (getItemViewType(position)) { 548 case VIEW_TYPE_ALERT: 549 //Do nothing 550 break; 551 default: 552 bindCallLogListViewHolder(viewHolder, position); 553 break; 554 } 555 Trace.endSection(); 556 } 557 558 @Override 559 public void onViewRecycled(ViewHolder viewHolder) { 560 if (viewHolder.getItemViewType() == VIEW_TYPE_CALLLOG) { 561 CallLogListItemViewHolder views = (CallLogListItemViewHolder) viewHolder; 562 if (views.asyncTask != null) { 563 views.asyncTask.cancel(true); 564 } 565 } 566 } 567 568 @Override 569 public void onViewAttachedToWindow(ViewHolder viewHolder) { 570 if (viewHolder.getItemViewType() == VIEW_TYPE_CALLLOG) { 571 ((CallLogListItemViewHolder) viewHolder).isAttachedToWindow = true; 572 } 573 } 574 575 @Override 576 public void onViewDetachedFromWindow(ViewHolder viewHolder) { 577 if (viewHolder.getItemViewType() == VIEW_TYPE_CALLLOG) { 578 ((CallLogListItemViewHolder) viewHolder).isAttachedToWindow = false; 579 } 580 } 581 582 /** 583 * Binds the view holder for the call log list item view. 584 * 585 * @param viewHolder The call log list item view holder. 586 * @param position The position of the list item. 587 */ 588 private void bindCallLogListViewHolder(final ViewHolder viewHolder, final int position) { 589 Cursor c = (Cursor) getItem(position); 590 if (c == null) { 591 return; 592 } 593 CallLogListItemViewHolder views = (CallLogListItemViewHolder) viewHolder; 594 views.isLoaded = false; 595 int groupSize = getGroupSize(position); 596 CallDetailsEntries callDetailsEntries = createCallDetailsEntries(c, groupSize); 597 PhoneCallDetails details = createPhoneCallDetails(c, groupSize, views); 598 if (mHiddenRowIds.contains(c.getLong(CallLogQuery.ID))) { 599 views.callLogEntryView.setVisibility(View.GONE); 600 views.dayGroupHeader.setVisibility(View.GONE); 601 return; 602 } else { 603 views.callLogEntryView.setVisibility(View.VISIBLE); 604 // dayGroupHeader will be restored after loadAndRender() if it is needed. 605 } 606 if (mCurrentlyExpandedRowId == views.rowId) { 607 views.inflateActionViewStub(); 608 } 609 loadAndRender(views, views.rowId, details, callDetailsEntries); 610 } 611 612 private void loadAndRender( 613 final CallLogListItemViewHolder views, 614 final long rowId, 615 final PhoneCallDetails details, 616 final CallDetailsEntries callDetailsEntries) { 617 LogUtil.d("CallLogAdapter.loadAndRender", "position: %d", views.getAdapterPosition()); 618 // Reset block and spam information since this view could be reused which may contain 619 // outdated data. 620 views.isSpam = false; 621 views.blockId = null; 622 views.isSpamFeatureEnabled = false; 623 624 // Attempt to set the isCallComposerCapable field. If capabilities are unknown for this number, 625 // the value will be false while capabilities are requested. mExpandCollapseListener will 626 // attempt to set the field properly in that case 627 views.isCallComposerCapable = isCallComposerCapable(views.number); 628 CallDetailsEntries updatedCallDetailsEntries = 629 generateAndMapNewCallDetailsEntriesHistoryResults( 630 views.number, 631 callDetailsEntries, 632 getAllHistoricalData(views.number, callDetailsEntries)); 633 views.setDetailedPhoneDetails(updatedCallDetailsEntries); 634 views.lightbringerReady = getLightbringer().isReachable(mActivity, views.number); 635 final AsyncTask<Void, Void, Boolean> loadDataTask = 636 new AsyncTask<Void, Void, Boolean>() { 637 @Override 638 protected Boolean doInBackground(Void... params) { 639 views.blockId = 640 mFilteredNumberAsyncQueryHandler.getBlockedIdSynchronous( 641 views.number, views.countryIso); 642 details.isBlocked = views.blockId != null; 643 if (isCancelled()) { 644 return false; 645 } 646 if (mIsSpamEnabled) { 647 views.isSpamFeatureEnabled = true; 648 // Only display the call as a spam call if there are incoming calls in the list. 649 // Call log cards with only outgoing calls should never be displayed as spam. 650 views.isSpam = 651 details.hasIncomingCalls() 652 && Spam.get(mActivity) 653 .checkSpamStatusSynchronous(views.number, views.countryIso); 654 details.isSpam = views.isSpam; 655 } 656 return !isCancelled() && loadData(views, rowId, details); 657 } 658 659 @Override 660 protected void onPostExecute(Boolean success) { 661 views.isLoaded = true; 662 if (success) { 663 int currentGroup = getDayGroupForCall(views.rowId); 664 if (currentGroup != details.previousGroup) { 665 views.dayGroupHeaderVisibility = View.VISIBLE; 666 views.dayGroupHeaderText = getGroupDescription(currentGroup); 667 } else { 668 views.dayGroupHeaderVisibility = View.GONE; 669 } 670 render(views, details, rowId); 671 } 672 } 673 }; 674 675 views.asyncTask = loadDataTask; 676 mAsyncTaskExecutor.submit(LOAD_DATA_TASK_IDENTIFIER, loadDataTask); 677 } 678 679 @MainThread 680 private boolean isCallComposerCapable(@Nullable String number) { 681 if (number == null) { 682 return false; 683 } 684 685 EnrichedCallCapabilities capabilities = getEnrichedCallManager().getCapabilities(number); 686 if (capabilities == null) { 687 getEnrichedCallManager().requestCapabilities(number); 688 return false; 689 } 690 return capabilities.supportsCallComposer(); 691 } 692 693 @NonNull 694 private Map<CallDetailsEntry, List<HistoryResult>> getAllHistoricalData( 695 @Nullable String number, @NonNull CallDetailsEntries entries) { 696 if (number == null) { 697 return Collections.emptyMap(); 698 } 699 700 Map<CallDetailsEntry, List<HistoryResult>> historicalData = 701 getEnrichedCallManager().getAllHistoricalData(number, entries); 702 if (historicalData == null) { 703 getEnrichedCallManager().requestAllHistoricalData(number, entries); 704 return Collections.emptyMap(); 705 } 706 return historicalData; 707 } 708 709 private static CallDetailsEntries generateAndMapNewCallDetailsEntriesHistoryResults( 710 @Nullable String number, 711 @NonNull CallDetailsEntries callDetailsEntries, 712 @NonNull Map<CallDetailsEntry, List<HistoryResult>> mappedResults) { 713 if (number == null) { 714 return callDetailsEntries; 715 } 716 CallDetailsEntries.Builder mutableCallDetailsEntries = CallDetailsEntries.newBuilder(); 717 for (CallDetailsEntry entry : callDetailsEntries.getEntriesList()) { 718 CallDetailsEntry.Builder newEntry = CallDetailsEntry.newBuilder().mergeFrom(entry); 719 List<HistoryResult> results = mappedResults.get(entry); 720 if (results != null) { 721 newEntry.addAllHistoryResults(mappedResults.get(entry)); 722 LogUtil.v( 723 "CallLogAdapter.generateAndMapNewCallDetailsEntriesHistoryResults", 724 "mapped %d results", 725 newEntry.getHistoryResultsList().size()); 726 } 727 mutableCallDetailsEntries.addEntries(newEntry.build()); 728 } 729 return mutableCallDetailsEntries.build(); 730 } 731 732 /** 733 * Initialize PhoneCallDetails by reading all data from cursor. This method must be run on main 734 * thread since cursor is not thread safe. 735 */ 736 @MainThread 737 private PhoneCallDetails createPhoneCallDetails( 738 Cursor cursor, int count, final CallLogListItemViewHolder views) { 739 Assert.isMainThread(); 740 final String number = cursor.getString(CallLogQuery.NUMBER); 741 final String postDialDigits = 742 (VERSION.SDK_INT >= VERSION_CODES.N) ? cursor.getString(CallLogQuery.POST_DIAL_DIGITS) : ""; 743 final String viaNumber = 744 (VERSION.SDK_INT >= VERSION_CODES.N) ? cursor.getString(CallLogQuery.VIA_NUMBER) : ""; 745 final int numberPresentation = cursor.getInt(CallLogQuery.NUMBER_PRESENTATION); 746 final ContactInfo cachedContactInfo = ContactInfoHelper.getContactInfo(cursor); 747 final PhoneCallDetails details = 748 new PhoneCallDetails(number, numberPresentation, postDialDigits); 749 details.viaNumber = viaNumber; 750 details.countryIso = cursor.getString(CallLogQuery.COUNTRY_ISO); 751 details.date = cursor.getLong(CallLogQuery.DATE); 752 details.duration = cursor.getLong(CallLogQuery.DURATION); 753 details.features = getCallFeatures(cursor, count); 754 details.geocode = cursor.getString(CallLogQuery.GEOCODED_LOCATION); 755 details.transcription = cursor.getString(CallLogQuery.TRANSCRIPTION); 756 details.callTypes = getCallTypes(cursor, count); 757 758 details.accountComponentName = cursor.getString(CallLogQuery.ACCOUNT_COMPONENT_NAME); 759 details.accountId = cursor.getString(CallLogQuery.ACCOUNT_ID); 760 details.cachedContactInfo = cachedContactInfo; 761 762 if (!cursor.isNull(CallLogQuery.DATA_USAGE)) { 763 details.dataUsage = cursor.getLong(CallLogQuery.DATA_USAGE); 764 } 765 766 views.rowId = cursor.getLong(CallLogQuery.ID); 767 // Stash away the Ids of the calls so that we can support deleting a row in the call log. 768 views.callIds = getCallIds(cursor, count); 769 details.previousGroup = getPreviousDayGroup(cursor); 770 771 // Store values used when the actions ViewStub is inflated on expansion. 772 views.number = number; 773 views.countryIso = details.countryIso; 774 views.postDialDigits = details.postDialDigits; 775 views.numberPresentation = numberPresentation; 776 777 if (details.callTypes[0] == CallLog.Calls.VOICEMAIL_TYPE 778 || details.callTypes[0] == CallLog.Calls.MISSED_TYPE) { 779 details.isRead = cursor.getInt(CallLogQuery.IS_READ) == 1; 780 } 781 views.callType = cursor.getInt(CallLogQuery.CALL_TYPE); 782 views.voicemailUri = cursor.getString(CallLogQuery.VOICEMAIL_URI); 783 784 return details; 785 } 786 787 @MainThread 788 private static CallDetailsEntries createCallDetailsEntries(Cursor cursor, int count) { 789 Assert.isMainThread(); 790 int position = cursor.getPosition(); 791 CallDetailsEntries.Builder entries = CallDetailsEntries.newBuilder(); 792 for (int i = 0; i < count; i++) { 793 CallDetailsEntry.Builder entry = 794 CallDetailsEntry.newBuilder() 795 .setCallId(cursor.getLong(CallLogQuery.ID)) 796 .setCallType(cursor.getInt(CallLogQuery.CALL_TYPE)) 797 .setDataUsage(cursor.getLong(CallLogQuery.DATA_USAGE)) 798 .setDate(cursor.getLong(CallLogQuery.DATE)) 799 .setDuration(cursor.getLong(CallLogQuery.DURATION)) 800 .setFeatures(cursor.getInt(CallLogQuery.FEATURES)); 801 entries.addEntries(entry.build()); 802 cursor.moveToNext(); 803 } 804 cursor.moveToPosition(position); 805 return entries.build(); 806 } 807 808 /** 809 * Load data for call log. Any expensive operation should be put here to avoid blocking main 810 * thread. Do NOT put any cursor operation here since it's not thread safe. 811 */ 812 @WorkerThread 813 private boolean loadData(CallLogListItemViewHolder views, long rowId, PhoneCallDetails details) { 814 Assert.isWorkerThread(); 815 if (rowId != views.rowId) { 816 LogUtil.i( 817 "CallLogAdapter.loadData", 818 "rowId of viewHolder changed after load task is issued, aborting load"); 819 return false; 820 } 821 822 final PhoneAccountHandle accountHandle = 823 PhoneAccountUtils.getAccount(details.accountComponentName, details.accountId); 824 825 final boolean isVoicemailNumber = 826 mCallLogCache.isVoicemailNumber(accountHandle, details.number); 827 828 // Note: Binding of the action buttons is done as required in configureActionViews when the 829 // user expands the actions ViewStub. 830 831 ContactInfo info = ContactInfo.EMPTY; 832 if (PhoneNumberHelper.canPlaceCallsTo(details.number, details.numberPresentation) 833 && !isVoicemailNumber) { 834 // Lookup contacts with this number 835 // Only do remote lookup in first 5 rows. 836 int position = views.getAdapterPosition(); 837 info = 838 mContactInfoCache.getValue( 839 details.number + details.postDialDigits, 840 details.countryIso, 841 details.cachedContactInfo, 842 position 843 < Bindings.get(mActivity) 844 .getConfigProvider() 845 .getLong("number_of_call_to_do_remote_lookup", 5L)); 846 } 847 CharSequence formattedNumber = 848 info.formattedNumber == null 849 ? null 850 : PhoneNumberUtilsCompat.createTtsSpannable(info.formattedNumber); 851 details.updateDisplayNumber(mActivity, formattedNumber, isVoicemailNumber); 852 853 views.displayNumber = details.displayNumber; 854 views.accountHandle = accountHandle; 855 details.accountHandle = accountHandle; 856 857 if (!TextUtils.isEmpty(info.name) || !TextUtils.isEmpty(info.nameAlternative)) { 858 details.contactUri = info.lookupUri; 859 details.namePrimary = info.name; 860 details.nameAlternative = info.nameAlternative; 861 details.nameDisplayOrder = mContactsPreferences.getDisplayOrder(); 862 details.numberType = info.type; 863 details.numberLabel = info.label; 864 details.photoUri = info.photoUri; 865 details.sourceType = info.sourceType; 866 details.objectId = info.objectId; 867 details.contactUserType = info.userType; 868 } 869 LogUtil.d( 870 "CallLogAdapter.loadData", 871 "position:%d, update geo info: %s, cequint caller id geo: %s, photo uri: %s <- %s", 872 views.getAdapterPosition(), 873 details.geocode, 874 info.geoDescription, 875 details.photoUri, 876 info.photoUri); 877 if (!TextUtils.isEmpty(info.geoDescription)) { 878 details.geocode = info.geoDescription; 879 } 880 881 views.info = info; 882 views.numberType = getNumberType(mActivity.getResources(), details); 883 884 mCallLogListItemHelper.updatePhoneCallDetails(details); 885 return true; 886 } 887 888 private static String getNumberType(Resources res, PhoneCallDetails details) { 889 // Label doesn't make much sense if the information is coming from CNAP or Cequint Caller ID. 890 if (details.sourceType == ContactSource.Type.SOURCE_TYPE_CNAP 891 || details.sourceType == ContactSource.Type.SOURCE_TYPE_CEQUINT_CALLER_ID) { 892 return ""; 893 } 894 // Returns empty label instead of "custom" if the custom label is empty. 895 if (details.numberType == Phone.TYPE_CUSTOM && TextUtils.isEmpty(details.numberLabel)) { 896 return ""; 897 } 898 return (String) Phone.getTypeLabel(res, details.numberType, details.numberLabel); 899 } 900 901 /** 902 * Render item view given position. This is running on UI thread so DO NOT put any expensive 903 * operation into it. 904 */ 905 @MainThread 906 private void render(CallLogListItemViewHolder views, PhoneCallDetails details, long rowId) { 907 Assert.isMainThread(); 908 if (rowId != views.rowId) { 909 LogUtil.i( 910 "CallLogAdapter.render", 911 "rowId of viewHolder changed after load task is issued, aborting render"); 912 return; 913 } 914 915 // Default case: an item in the call log. 916 views.primaryActionView.setVisibility(View.VISIBLE); 917 views.workIconView.setVisibility( 918 details.contactUserType == ContactsUtils.USER_TYPE_WORK ? View.VISIBLE : View.GONE); 919 920 if (views.voicemailUri != null 921 && selectedItems.get(getVoicemailId(views.voicemailUri)) != null) { 922 views.checkBoxView.setVisibility(View.VISIBLE); 923 views.quickContactView.setVisibility(View.GONE); 924 } else if (views.voicemailUri != null) { 925 views.checkBoxView.setVisibility(View.GONE); 926 views.quickContactView.setVisibility(View.VISIBLE); 927 } 928 929 mCallLogListItemHelper.setPhoneCallDetails(views, details); 930 if (mCurrentlyExpandedRowId == views.rowId) { 931 // In case ViewHolders were added/removed, update the expanded position if the rowIds 932 // match so that we can restore the correct expanded state on rebind. 933 mCurrentlyExpandedPosition = views.getAdapterPosition(); 934 views.showActions(true); 935 } else { 936 views.showActions(false); 937 } 938 views.dayGroupHeader.setVisibility(views.dayGroupHeaderVisibility); 939 views.dayGroupHeader.setText(views.dayGroupHeaderText); 940 } 941 942 @Override 943 public int getItemCount() { 944 return super.getItemCount() + (mCallLogAlertManager.isEmpty() ? 0 : 1); 945 } 946 947 @Override 948 public int getItemViewType(int position) { 949 if (position == ALERT_POSITION && !mCallLogAlertManager.isEmpty()) { 950 return VIEW_TYPE_ALERT; 951 } 952 return VIEW_TYPE_CALLLOG; 953 } 954 955 /** 956 * Retrieves an item at the specified position, taking into account the presence of a promo card. 957 * 958 * @param position The position to retrieve. 959 * @return The item at that position. 960 */ 961 @Override 962 public Object getItem(int position) { 963 return super.getItem(position - (mCallLogAlertManager.isEmpty() ? 0 : 1)); 964 } 965 966 @Override 967 public long getItemId(int position) { 968 Cursor cursor = (Cursor) getItem(position); 969 if (cursor != null) { 970 return cursor.getLong(CallLogQuery.ID); 971 } else { 972 return 0; 973 } 974 } 975 976 @Override 977 public int getGroupSize(int position) { 978 return super.getGroupSize(position - (mCallLogAlertManager.isEmpty() ? 0 : 1)); 979 } 980 981 protected boolean isCallLogActivity() { 982 return mActivityType == ACTIVITY_TYPE_CALL_LOG; 983 } 984 985 /** 986 * In order to implement the "undo" function, when a voicemail is "deleted" i.e. when the user 987 * clicks the delete button, the deleted item is temporarily hidden from the list. If a user 988 * clicks delete on a second item before the first item's undo option has expired, the first item 989 * is immediately deleted so that only one item can be "undoed" at a time. 990 */ 991 @Override 992 public void onVoicemailDeleted(CallLogListItemViewHolder viewHolder, Uri uri) { 993 mHiddenRowIds.add(viewHolder.rowId); 994 // Save the new hidden item uri in case the activity is suspend before the undo has timed out. 995 mHiddenItemUris.add(uri); 996 997 collapseExpandedCard(); 998 notifyItemChanged(viewHolder.getAdapterPosition()); 999 // The next item might have to update its day group label 1000 notifyItemChanged(viewHolder.getAdapterPosition() + 1); 1001 } 1002 1003 private void collapseExpandedCard() { 1004 mCurrentlyExpandedRowId = NO_EXPANDED_LIST_ITEM; 1005 mCurrentlyExpandedPosition = RecyclerView.NO_POSITION; 1006 } 1007 1008 /** When the list is changing all stored position is no longer valid. */ 1009 public void invalidatePositions() { 1010 mCurrentlyExpandedPosition = RecyclerView.NO_POSITION; 1011 } 1012 1013 /** When the user clicks "undo", the hidden item is unhidden. */ 1014 @Override 1015 public void onVoicemailDeleteUndo(long rowId, int adapterPosition, Uri uri) { 1016 mHiddenItemUris.remove(uri); 1017 mHiddenRowIds.remove(rowId); 1018 notifyItemChanged(adapterPosition); 1019 // The next item might have to update its day group label 1020 notifyItemChanged(adapterPosition + 1); 1021 } 1022 1023 /** This callback signifies that a database deletion has completed. */ 1024 @Override 1025 public void onVoicemailDeletedInDatabase(long rowId, Uri uri) { 1026 mHiddenItemUris.remove(uri); 1027 } 1028 1029 /** 1030 * Retrieves the day group of the previous call in the call log. Used to determine if the day 1031 * group has changed and to trigger display of the day group text. 1032 * 1033 * @param cursor The call log cursor. 1034 * @return The previous day group, or DAY_GROUP_NONE if this is the first call. 1035 */ 1036 private int getPreviousDayGroup(Cursor cursor) { 1037 // We want to restore the position in the cursor at the end. 1038 int startingPosition = cursor.getPosition(); 1039 moveToPreviousNonHiddenRow(cursor); 1040 if (cursor.isBeforeFirst()) { 1041 cursor.moveToPosition(startingPosition); 1042 return CallLogGroupBuilder.DAY_GROUP_NONE; 1043 } 1044 int result = getDayGroupForCall(cursor.getLong(CallLogQuery.ID)); 1045 cursor.moveToPosition(startingPosition); 1046 return result; 1047 } 1048 1049 private void moveToPreviousNonHiddenRow(Cursor cursor) { 1050 while (cursor.moveToPrevious() && mHiddenRowIds.contains(cursor.getLong(CallLogQuery.ID))) {} 1051 } 1052 1053 /** 1054 * Given a call Id, look up the day group that the call belongs to. The day group data is 1055 * populated in {@link com.android.dialer.app.calllog.CallLogGroupBuilder}. 1056 * 1057 * @param callId The call to retrieve the day group for. 1058 * @return The day group for the call. 1059 */ 1060 @MainThread 1061 private int getDayGroupForCall(long callId) { 1062 Integer result = mDayGroups.get(callId); 1063 if (result != null) { 1064 return result; 1065 } 1066 return CallLogGroupBuilder.DAY_GROUP_NONE; 1067 } 1068 1069 /** 1070 * Returns the call types for the given number of items in the cursor. 1071 * 1072 * <p>It uses the next {@code count} rows in the cursor to extract the types. 1073 * 1074 * <p>It position in the cursor is unchanged by this function. 1075 */ 1076 private static int[] getCallTypes(Cursor cursor, int count) { 1077 int position = cursor.getPosition(); 1078 int[] callTypes = new int[count]; 1079 for (int index = 0; index < count; ++index) { 1080 callTypes[index] = cursor.getInt(CallLogQuery.CALL_TYPE); 1081 cursor.moveToNext(); 1082 } 1083 cursor.moveToPosition(position); 1084 return callTypes; 1085 } 1086 1087 /** 1088 * Determine the features which were enabled for any of the calls that make up a call log entry. 1089 * 1090 * @param cursor The cursor. 1091 * @param count The number of calls for the current call log entry. 1092 * @return The features. 1093 */ 1094 private int getCallFeatures(Cursor cursor, int count) { 1095 int features = 0; 1096 int position = cursor.getPosition(); 1097 for (int index = 0; index < count; ++index) { 1098 features |= cursor.getInt(CallLogQuery.FEATURES); 1099 cursor.moveToNext(); 1100 } 1101 cursor.moveToPosition(position); 1102 return features; 1103 } 1104 1105 /** 1106 * Sets whether processing of requests for contact details should be enabled. 1107 * 1108 * <p>This method should be called in tests to disable such processing of requests when not 1109 * needed. 1110 */ 1111 @VisibleForTesting 1112 void disableRequestProcessingForTest() { 1113 // TODO: Remove this and test the cache directly. 1114 mContactInfoCache.disableRequestProcessing(); 1115 } 1116 1117 @VisibleForTesting 1118 void injectContactInfoForTest(String number, String countryIso, ContactInfo contactInfo) { 1119 // TODO: Remove this and test the cache directly. 1120 mContactInfoCache.injectContactInfoForTest(number, countryIso, contactInfo); 1121 } 1122 1123 /** 1124 * Stores the day group associated with a call in the call log. 1125 * 1126 * @param rowId The row Id of the current call. 1127 * @param dayGroup The day group the call belongs in. 1128 */ 1129 @Override 1130 @MainThread 1131 public void setDayGroup(long rowId, int dayGroup) { 1132 if (!mDayGroups.containsKey(rowId)) { 1133 mDayGroups.put(rowId, dayGroup); 1134 } 1135 } 1136 1137 /** Clears the day group associations on re-bind of the call log. */ 1138 @Override 1139 @MainThread 1140 public void clearDayGroups() { 1141 mDayGroups.clear(); 1142 } 1143 1144 /** 1145 * Retrieves the call Ids represented by the current call log row. 1146 * 1147 * @param cursor Call log cursor to retrieve call Ids from. 1148 * @param groupSize Number of calls associated with the current call log row. 1149 * @return Array of call Ids. 1150 */ 1151 private long[] getCallIds(final Cursor cursor, final int groupSize) { 1152 // We want to restore the position in the cursor at the end. 1153 int startingPosition = cursor.getPosition(); 1154 long[] ids = new long[groupSize]; 1155 // Copy the ids of the rows in the group. 1156 for (int index = 0; index < groupSize; ++index) { 1157 ids[index] = cursor.getLong(CallLogQuery.ID); 1158 cursor.moveToNext(); 1159 } 1160 cursor.moveToPosition(startingPosition); 1161 return ids; 1162 } 1163 1164 /** 1165 * Determines the description for a day group. 1166 * 1167 * @param group The day group to retrieve the description for. 1168 * @return The day group description. 1169 */ 1170 private CharSequence getGroupDescription(int group) { 1171 if (group == CallLogGroupBuilder.DAY_GROUP_TODAY) { 1172 return mActivity.getResources().getString(R.string.call_log_header_today); 1173 } else if (group == CallLogGroupBuilder.DAY_GROUP_YESTERDAY) { 1174 return mActivity.getResources().getString(R.string.call_log_header_yesterday); 1175 } else { 1176 return mActivity.getResources().getString(R.string.call_log_header_other); 1177 } 1178 } 1179 1180 @NonNull 1181 private EnrichedCallManager getEnrichedCallManager() { 1182 return EnrichedCallComponent.get(mActivity).getEnrichedCallManager(); 1183 } 1184 1185 @NonNull 1186 private Lightbringer getLightbringer() { 1187 return LightbringerComponent.get(mActivity).getLightbringer(); 1188 } 1189 1190 @Override 1191 public void onLightbringerStateChanged() { 1192 notifyDataSetChanged(); 1193 } 1194 1195 /** Interface used to initiate a refresh of the content. */ 1196 public interface CallFetcher { 1197 1198 void fetchCalls(); 1199 } 1200 } 1201