1 /* 2 * Copyright (C) 2017 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.calldetails; 18 19 import android.Manifest.permission; 20 import android.annotation.SuppressLint; 21 import android.app.Activity; 22 import android.app.LoaderManager.LoaderCallbacks; 23 import android.content.ActivityNotFoundException; 24 import android.content.Context; 25 import android.content.Intent; 26 import android.content.Loader; 27 import android.database.Cursor; 28 import android.os.AsyncTask; 29 import android.os.Bundle; 30 import android.provider.CallLog; 31 import android.provider.CallLog.Calls; 32 import android.support.annotation.NonNull; 33 import android.support.annotation.Nullable; 34 import android.support.annotation.RequiresPermission; 35 import android.support.v7.app.AppCompatActivity; 36 import android.support.v7.widget.LinearLayoutManager; 37 import android.support.v7.widget.RecyclerView; 38 import android.support.v7.widget.Toolbar; 39 import android.view.View; 40 import android.widget.Toast; 41 import com.android.dialer.CoalescedIds; 42 import com.android.dialer.assisteddialing.ui.AssistedDialingSettingActivity; 43 import com.android.dialer.calldetails.CallDetailsEntries.CallDetailsEntry; 44 import com.android.dialer.callintent.CallInitiationType; 45 import com.android.dialer.callintent.CallIntentBuilder; 46 import com.android.dialer.calllog.database.contract.AnnotatedCallLogContract.AnnotatedCallLog; 47 import com.android.dialer.common.Assert; 48 import com.android.dialer.common.LogUtil; 49 import com.android.dialer.common.concurrent.AsyncTaskExecutors; 50 import com.android.dialer.common.concurrent.DialerExecutor.FailureListener; 51 import com.android.dialer.common.concurrent.DialerExecutor.SuccessListener; 52 import com.android.dialer.common.concurrent.DialerExecutor.Worker; 53 import com.android.dialer.common.concurrent.DialerExecutorComponent; 54 import com.android.dialer.constants.ActivityRequestCodes; 55 import com.android.dialer.dialercontact.DialerContact; 56 import com.android.dialer.duo.Duo; 57 import com.android.dialer.duo.DuoComponent; 58 import com.android.dialer.enrichedcall.EnrichedCallComponent; 59 import com.android.dialer.enrichedcall.EnrichedCallManager; 60 import com.android.dialer.enrichedcall.historyquery.proto.HistoryResult; 61 import com.android.dialer.logging.DialerImpression; 62 import com.android.dialer.logging.Logger; 63 import com.android.dialer.logging.UiAction; 64 import com.android.dialer.performancereport.PerformanceReport; 65 import com.android.dialer.postcall.PostCall; 66 import com.android.dialer.precall.PreCall; 67 import com.android.dialer.protos.ProtoParsers; 68 import com.google.common.base.Optional; 69 import com.google.common.base.Preconditions; 70 import com.google.i18n.phonenumbers.NumberParseException; 71 import com.google.i18n.phonenumbers.PhoneNumberUtil; 72 import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber; 73 import java.lang.ref.WeakReference; 74 import java.util.Collections; 75 import java.util.List; 76 import java.util.Map; 77 78 /** Displays the details of a specific call log entry. */ 79 public class CallDetailsActivity extends AppCompatActivity { 80 private static final int CALL_DETAILS_LOADER_ID = 0; 81 82 public static final String EXTRA_PHONE_NUMBER = "phone_number"; 83 public static final String EXTRA_HAS_ENRICHED_CALL_DATA = "has_enriched_call_data"; 84 public static final String EXTRA_CALL_DETAILS_ENTRIES = "call_details_entries"; 85 public static final String EXTRA_COALESCED_CALL_LOG_IDS = "coalesced_call_log_ids"; 86 public static final String EXTRA_CONTACT = "contact"; 87 public static final String EXTRA_CAN_REPORT_CALLER_ID = "can_report_caller_id"; 88 public static final String EXTRA_CAN_SUPPORT_ASSISTED_DIALING = "can_support_assisted_dialing"; 89 90 private final CallDetailsHeaderViewHolder.CallDetailsHeaderListener callDetailsHeaderListener = 91 new CallDetailsHeaderListener(this); 92 private final CallDetailsFooterViewHolder.DeleteCallDetailsListener deleteCallDetailsListener = 93 new DeleteCallDetailsListener(this); 94 private final CallDetailsFooterViewHolder.ReportCallIdListener reportCallIdListener = 95 new ReportCallIdListener(this); 96 private final EnrichedCallManager.HistoricalDataChangedListener 97 enrichedCallHistoricalDataChangedListener = 98 new EnrichedCallHistoricalDataChangedListener(this); 99 100 private CallDetailsEntries entries; 101 private DialerContact contact; 102 private CallDetailsAdapter adapter; 103 104 // This will be present only when the activity is launched from the new call log UI, i.e., a list 105 // of coalesced annotated call log IDs is included in the intent. 106 private Optional<CoalescedIds> coalescedCallLogIds = Optional.absent(); 107 108 public static boolean isLaunchIntent(Intent intent) { 109 return intent.getComponent() != null 110 && CallDetailsActivity.class.getName().equals(intent.getComponent().getClassName()); 111 } 112 113 /** 114 * Returns an {@link Intent} for launching the {@link CallDetailsActivity} from the old call log 115 * UI. 116 */ 117 public static Intent newInstance( 118 Context context, 119 CallDetailsEntries details, 120 DialerContact contact, 121 boolean canReportCallerId, 122 boolean canSupportAssistedDialing) { 123 Intent intent = new Intent(context, CallDetailsActivity.class); 124 ProtoParsers.put(intent, EXTRA_CONTACT, Assert.isNotNull(contact)); 125 ProtoParsers.put(intent, EXTRA_CALL_DETAILS_ENTRIES, Assert.isNotNull(details)); 126 intent.putExtra(EXTRA_CAN_REPORT_CALLER_ID, canReportCallerId); 127 intent.putExtra(EXTRA_CAN_SUPPORT_ASSISTED_DIALING, canSupportAssistedDialing); 128 return intent; 129 } 130 131 /** 132 * Returns an {@link Intent} for launching the {@link CallDetailsActivity} from the new call log 133 * UI. 134 */ 135 public static Intent newInstance( 136 Context context, 137 CoalescedIds coalescedAnnotatedCallLogIds, 138 DialerContact contact, 139 boolean canReportCallerId, 140 boolean canSupportAssistedDialing) { 141 Intent intent = new Intent(context, CallDetailsActivity.class); 142 ProtoParsers.put(intent, EXTRA_CONTACT, Assert.isNotNull(contact)); 143 ProtoParsers.put( 144 intent, EXTRA_COALESCED_CALL_LOG_IDS, Assert.isNotNull(coalescedAnnotatedCallLogIds)); 145 intent.putExtra(EXTRA_CAN_REPORT_CALLER_ID, canReportCallerId); 146 intent.putExtra(EXTRA_CAN_SUPPORT_ASSISTED_DIALING, canSupportAssistedDialing); 147 return intent; 148 } 149 150 @Override 151 protected void onCreate(Bundle savedInstanceState) { 152 super.onCreate(savedInstanceState); 153 setContentView(R.layout.call_details_activity); 154 Toolbar toolbar = findViewById(R.id.toolbar); 155 toolbar.setTitle(R.string.call_details); 156 toolbar.setNavigationOnClickListener( 157 v -> { 158 PerformanceReport.recordClick(UiAction.Type.CLOSE_CALL_DETAIL_WITH_CANCEL_BUTTON); 159 finish(); 160 }); 161 onHandleIntent(getIntent()); 162 } 163 164 @Override 165 protected void onResume() { 166 super.onResume(); 167 168 // Some calls may not be recorded (eg. from quick contact), 169 // so we should restart recording after these calls. (Recorded call is stopped) 170 PostCall.restartPerformanceRecordingIfARecentCallExist(this); 171 if (!PerformanceReport.isRecording()) { 172 PerformanceReport.startRecording(); 173 } 174 175 PostCall.promptUserForMessageIfNecessary(this, findViewById(R.id.recycler_view)); 176 177 EnrichedCallComponent.get(this) 178 .getEnrichedCallManager() 179 .registerHistoricalDataChangedListener(enrichedCallHistoricalDataChangedListener); 180 EnrichedCallComponent.get(this) 181 .getEnrichedCallManager() 182 .requestAllHistoricalData(contact.getNumber(), entries); 183 } 184 185 @Override 186 protected void onPause() { 187 super.onPause(); 188 189 EnrichedCallComponent.get(this) 190 .getEnrichedCallManager() 191 .unregisterHistoricalDataChangedListener(enrichedCallHistoricalDataChangedListener); 192 } 193 194 @Override 195 protected void onNewIntent(Intent intent) { 196 super.onNewIntent(intent); 197 onHandleIntent(intent); 198 } 199 200 private void onHandleIntent(Intent intent) { 201 boolean hasCallDetailsEntries = intent.hasExtra(EXTRA_CALL_DETAILS_ENTRIES); 202 boolean hasCoalescedCallLogIds = intent.hasExtra(EXTRA_COALESCED_CALL_LOG_IDS); 203 Assert.checkArgument( 204 (hasCallDetailsEntries && !hasCoalescedCallLogIds) 205 || (!hasCallDetailsEntries && hasCoalescedCallLogIds), 206 "One and only one of EXTRA_CALL_DETAILS_ENTRIES and EXTRA_COALESCED_CALL_LOG_IDS " 207 + "can be included in the intent."); 208 209 contact = ProtoParsers.getTrusted(intent, EXTRA_CONTACT, DialerContact.getDefaultInstance()); 210 if (hasCallDetailsEntries) { 211 entries = 212 ProtoParsers.getTrusted( 213 intent, EXTRA_CALL_DETAILS_ENTRIES, CallDetailsEntries.getDefaultInstance()); 214 } else { 215 entries = CallDetailsEntries.getDefaultInstance(); 216 coalescedCallLogIds = 217 Optional.of( 218 ProtoParsers.getTrusted( 219 intent, EXTRA_COALESCED_CALL_LOG_IDS, CoalescedIds.getDefaultInstance())); 220 getLoaderManager() 221 .initLoader( 222 CALL_DETAILS_LOADER_ID, /* args = */ null, new CallDetailsLoaderCallbacks(this)); 223 } 224 225 adapter = 226 new CallDetailsAdapter( 227 this /* context */, 228 contact, 229 entries.getEntriesList(), 230 callDetailsHeaderListener, 231 reportCallIdListener, 232 deleteCallDetailsListener); 233 234 RecyclerView recyclerView = findViewById(R.id.recycler_view); 235 recyclerView.setLayoutManager(new LinearLayoutManager(this)); 236 recyclerView.setAdapter(adapter); 237 PerformanceReport.logOnScrollStateChange(recyclerView); 238 } 239 240 @Override 241 public void onBackPressed() { 242 PerformanceReport.recordClick(UiAction.Type.PRESS_ANDROID_BACK_BUTTON); 243 super.onBackPressed(); 244 } 245 246 /** 247 * {@link LoaderCallbacks} for {@link CallDetailsCursorLoader}, which loads call detail entries 248 * from {@link AnnotatedCallLog}. 249 */ 250 private static final class CallDetailsLoaderCallbacks implements LoaderCallbacks<Cursor> { 251 private final CallDetailsActivity activity; 252 253 CallDetailsLoaderCallbacks(CallDetailsActivity callDetailsActivity) { 254 this.activity = callDetailsActivity; 255 } 256 257 @Override 258 public Loader<Cursor> onCreateLoader(int id, Bundle args) { 259 Assert.checkState(activity.coalescedCallLogIds.isPresent()); 260 261 return new CallDetailsCursorLoader(activity, activity.coalescedCallLogIds.get()); 262 } 263 264 @Override 265 public void onLoadFinished(Loader<Cursor> loader, Cursor data) { 266 updateCallDetailsEntries(CallDetailsCursorLoader.toCallDetailsEntries(data)); 267 } 268 269 @Override 270 public void onLoaderReset(Loader<Cursor> loader) { 271 updateCallDetailsEntries(CallDetailsEntries.getDefaultInstance()); 272 } 273 274 private void updateCallDetailsEntries(CallDetailsEntries newEntries) { 275 activity.entries = newEntries; 276 activity.adapter.updateCallDetailsEntries(newEntries.getEntriesList()); 277 EnrichedCallComponent.get(activity) 278 .getEnrichedCallManager() 279 .requestAllHistoricalData(activity.contact.getNumber(), newEntries); 280 } 281 } 282 283 /** Delete specified calls from the call log. */ 284 private static class DeleteCallsTask extends AsyncTask<Void, Void, Void> { 285 // Use a weak reference to hold the Activity so that there is no memory leak. 286 private final WeakReference<Activity> activityWeakReference; 287 288 private final DialerContact contact; 289 private final CallDetailsEntries callDetailsEntries; 290 private final String callIds; 291 292 DeleteCallsTask( 293 Activity activity, DialerContact contact, CallDetailsEntries callDetailsEntries) { 294 this.activityWeakReference = new WeakReference<>(activity); 295 this.contact = contact; 296 this.callDetailsEntries = callDetailsEntries; 297 298 StringBuilder callIds = new StringBuilder(); 299 for (CallDetailsEntry entry : callDetailsEntries.getEntriesList()) { 300 if (callIds.length() != 0) { 301 callIds.append(","); 302 } 303 callIds.append(entry.getCallId()); 304 } 305 this.callIds = callIds.toString(); 306 } 307 308 @Override 309 // Suppress the lint check here as the user will not be able to see call log entries if 310 // permission.WRITE_CALL_LOG is not granted. 311 @SuppressLint("MissingPermission") 312 @RequiresPermission(value = permission.WRITE_CALL_LOG) 313 protected Void doInBackground(Void... params) { 314 Activity activity = activityWeakReference.get(); 315 if (activity == null) { 316 return null; 317 } 318 319 activity 320 .getContentResolver() 321 .delete( 322 Calls.CONTENT_URI, 323 CallLog.Calls._ID + " IN (" + callIds + ")" /* where */, 324 null /* selectionArgs */); 325 return null; 326 } 327 328 @Override 329 public void onPostExecute(Void result) { 330 Activity activity = activityWeakReference.get(); 331 if (activity == null) { 332 return; 333 } 334 335 Intent data = new Intent(); 336 data.putExtra(EXTRA_PHONE_NUMBER, contact.getNumber()); 337 for (CallDetailsEntry entry : callDetailsEntries.getEntriesList()) { 338 if (entry.getHistoryResultsCount() > 0) { 339 data.putExtra(EXTRA_HAS_ENRICHED_CALL_DATA, true); 340 break; 341 } 342 } 343 344 activity.setResult(RESULT_OK, data); 345 activity.finish(); 346 } 347 } 348 349 private static final class CallDetailsHeaderListener 350 implements CallDetailsHeaderViewHolder.CallDetailsHeaderListener { 351 private final WeakReference<CallDetailsActivity> activityWeakReference; 352 353 CallDetailsHeaderListener(CallDetailsActivity activity) { 354 this.activityWeakReference = new WeakReference<>(activity); 355 } 356 357 @Override 358 public void placeImsVideoCall(String phoneNumber) { 359 Logger.get(getActivity()) 360 .logImpression(DialerImpression.Type.CALL_DETAILS_IMS_VIDEO_CALL_BACK); 361 PreCall.start( 362 getActivity(), 363 new CallIntentBuilder(phoneNumber, CallInitiationType.Type.CALL_DETAILS) 364 .setIsVideoCall(true)); 365 } 366 367 @Override 368 public void placeDuoVideoCall(String phoneNumber) { 369 Logger.get(getActivity()) 370 .logImpression(DialerImpression.Type.CALL_DETAILS_LIGHTBRINGER_CALL_BACK); 371 Duo duo = DuoComponent.get(getActivity()).getDuo(); 372 if (!duo.isReachable(getActivity(), phoneNumber)) { 373 placeImsVideoCall(phoneNumber); 374 return; 375 } 376 377 try { 378 getActivity() 379 .startActivityForResult( 380 duo.getIntent(getActivity(), phoneNumber), ActivityRequestCodes.DIALTACTS_DUO); 381 } catch (ActivityNotFoundException e) { 382 Toast.makeText(getActivity(), R.string.activity_not_available, Toast.LENGTH_SHORT).show(); 383 } 384 } 385 386 @Override 387 public void placeVoiceCall(String phoneNumber, String postDialDigits) { 388 Logger.get(getActivity()).logImpression(DialerImpression.Type.CALL_DETAILS_VOICE_CALL_BACK); 389 390 boolean canSupportedAssistedDialing = 391 getActivity() 392 .getIntent() 393 .getExtras() 394 .getBoolean(EXTRA_CAN_SUPPORT_ASSISTED_DIALING, false); 395 CallIntentBuilder callIntentBuilder = 396 new CallIntentBuilder(phoneNumber + postDialDigits, CallInitiationType.Type.CALL_DETAILS); 397 if (canSupportedAssistedDialing) { 398 callIntentBuilder.setAllowAssistedDial(true); 399 } 400 401 PreCall.start(getActivity(), callIntentBuilder); 402 } 403 404 private CallDetailsActivity getActivity() { 405 return Preconditions.checkNotNull(activityWeakReference.get()); 406 } 407 408 @Override 409 public void openAssistedDialingSettings(View unused) { 410 Intent intent = new Intent(getActivity(), AssistedDialingSettingActivity.class); 411 getActivity().startActivity(intent); 412 } 413 414 @Override 415 public void createAssistedDialerNumberParserTask( 416 AssistedDialingNumberParseWorker worker, 417 SuccessListener<Integer> successListener, 418 FailureListener failureListener) { 419 DialerExecutorComponent.get(getActivity().getApplicationContext()) 420 .dialerExecutorFactory() 421 .createUiTaskBuilder( 422 getActivity().getFragmentManager(), 423 "CallDetailsActivity.createAssistedDialerNumberParserTask", 424 new AssistedDialingNumberParseWorker()) 425 .onSuccess(successListener) 426 .onFailure(failureListener) 427 .build() 428 .executeParallel(getActivity().contact.getNumber()); 429 } 430 } 431 432 static class AssistedDialingNumberParseWorker implements Worker<String, Integer> { 433 434 @Override 435 public Integer doInBackground(@NonNull String phoneNumber) { 436 PhoneNumber parsedNumber = null; 437 try { 438 parsedNumber = PhoneNumberUtil.getInstance().parse(phoneNumber, null); 439 } catch (NumberParseException e) { 440 LogUtil.w( 441 "AssistedDialingNumberParseWorker.doInBackground", 442 "couldn't parse phone number: " + LogUtil.sanitizePii(phoneNumber), 443 e); 444 return 0; 445 } 446 return parsedNumber.getCountryCode(); 447 } 448 } 449 450 private static final class DeleteCallDetailsListener 451 implements CallDetailsFooterViewHolder.DeleteCallDetailsListener { 452 private static final String ASYNC_TASK_ID = "task_delete"; 453 454 private final WeakReference<CallDetailsActivity> activityWeakReference; 455 456 DeleteCallDetailsListener(CallDetailsActivity activity) { 457 this.activityWeakReference = new WeakReference<>(activity); 458 } 459 460 @Override 461 public void delete() { 462 AsyncTaskExecutors.createAsyncTaskExecutor() 463 .submit( 464 ASYNC_TASK_ID, 465 new DeleteCallsTask(getActivity(), getActivity().contact, getActivity().entries)); 466 } 467 468 private CallDetailsActivity getActivity() { 469 return Preconditions.checkNotNull(activityWeakReference.get()); 470 } 471 } 472 473 private static final class ReportCallIdListener 474 implements CallDetailsFooterViewHolder.ReportCallIdListener { 475 private final WeakReference<Activity> activityWeakReference; 476 477 ReportCallIdListener(Activity activity) { 478 this.activityWeakReference = new WeakReference<>(activity); 479 } 480 481 @Override 482 public void reportCallId(String number) { 483 ReportDialogFragment.newInstance(number) 484 .show(getActivity().getFragmentManager(), null /* tag */); 485 } 486 487 @Override 488 public boolean canReportCallerId(String number) { 489 return getActivity().getIntent().getExtras().getBoolean(EXTRA_CAN_REPORT_CALLER_ID, false); 490 } 491 492 private Activity getActivity() { 493 return Preconditions.checkNotNull(activityWeakReference.get()); 494 } 495 } 496 497 private static final class EnrichedCallHistoricalDataChangedListener 498 implements EnrichedCallManager.HistoricalDataChangedListener { 499 private final WeakReference<CallDetailsActivity> activityWeakReference; 500 501 EnrichedCallHistoricalDataChangedListener(CallDetailsActivity activity) { 502 this.activityWeakReference = new WeakReference<>(activity); 503 } 504 505 @Override 506 public void onHistoricalDataChanged() { 507 CallDetailsActivity activity = getActivity(); 508 Map<CallDetailsEntry, List<HistoryResult>> mappedResults = 509 getAllHistoricalData(activity.contact.getNumber(), activity.entries); 510 511 activity.adapter.updateCallDetailsEntries( 512 generateAndMapNewCallDetailsEntriesHistoryResults( 513 activity.contact.getNumber(), activity.entries, mappedResults) 514 .getEntriesList()); 515 } 516 517 private CallDetailsActivity getActivity() { 518 return Preconditions.checkNotNull(activityWeakReference.get()); 519 } 520 521 @NonNull 522 private Map<CallDetailsEntry, List<HistoryResult>> getAllHistoricalData( 523 @Nullable String number, @NonNull CallDetailsEntries entries) { 524 if (number == null) { 525 return Collections.emptyMap(); 526 } 527 528 Map<CallDetailsEntry, List<HistoryResult>> historicalData = 529 EnrichedCallComponent.get(getActivity()) 530 .getEnrichedCallManager() 531 .getAllHistoricalData(number, entries); 532 if (historicalData == null) { 533 return Collections.emptyMap(); 534 } 535 return historicalData; 536 } 537 538 private static CallDetailsEntries generateAndMapNewCallDetailsEntriesHistoryResults( 539 @Nullable String number, 540 @NonNull CallDetailsEntries callDetailsEntries, 541 @NonNull Map<CallDetailsEntry, List<HistoryResult>> mappedResults) { 542 if (number == null) { 543 return callDetailsEntries; 544 } 545 CallDetailsEntries.Builder mutableCallDetailsEntries = CallDetailsEntries.newBuilder(); 546 for (CallDetailsEntry entry : callDetailsEntries.getEntriesList()) { 547 CallDetailsEntry.Builder newEntry = CallDetailsEntry.newBuilder().mergeFrom(entry); 548 List<HistoryResult> results = mappedResults.get(entry); 549 if (results != null) { 550 newEntry.addAllHistoryResults(mappedResults.get(entry)); 551 LogUtil.v( 552 "CallDetailsActivity.generateAndMapNewCallDetailsEntriesHistoryResults", 553 "mapped %d results", 554 newEntry.getHistoryResultsList().size()); 555 } 556 mutableCallDetailsEntries.addEntries(newEntry.build()); 557 } 558 return mutableCallDetailsEntries.build(); 559 } 560 } 561 } 562