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.content.Context; 23 import android.content.Intent; 24 import android.os.Bundle; 25 import android.provider.CallLog; 26 import android.provider.CallLog.Calls; 27 import android.support.annotation.CallSuper; 28 import android.support.annotation.MainThread; 29 import android.support.annotation.NonNull; 30 import android.support.annotation.Nullable; 31 import android.support.annotation.RequiresPermission; 32 import android.support.v7.app.AppCompatActivity; 33 import android.support.v7.widget.LinearLayoutManager; 34 import android.support.v7.widget.RecyclerView; 35 import android.support.v7.widget.Toolbar; 36 import android.view.View; 37 import com.android.dialer.assisteddialing.ui.AssistedDialingSettingActivity; 38 import com.android.dialer.calldetails.CallDetailsEntries.CallDetailsEntry; 39 import com.android.dialer.callintent.CallInitiationType; 40 import com.android.dialer.callintent.CallIntentBuilder; 41 import com.android.dialer.common.Assert; 42 import com.android.dialer.common.LogUtil; 43 import com.android.dialer.common.concurrent.DialerExecutor.FailureListener; 44 import com.android.dialer.common.concurrent.DialerExecutor.SuccessListener; 45 import com.android.dialer.common.concurrent.DialerExecutor.Worker; 46 import com.android.dialer.common.concurrent.DialerExecutorComponent; 47 import com.android.dialer.common.concurrent.UiListener; 48 import com.android.dialer.common.database.Selection; 49 import com.android.dialer.enrichedcall.EnrichedCallComponent; 50 import com.android.dialer.enrichedcall.EnrichedCallManager; 51 import com.android.dialer.enrichedcall.historyquery.proto.HistoryResult; 52 import com.android.dialer.glidephotomanager.PhotoInfo; 53 import com.android.dialer.logging.DialerImpression; 54 import com.android.dialer.logging.Logger; 55 import com.android.dialer.logging.UiAction; 56 import com.android.dialer.performancereport.PerformanceReport; 57 import com.android.dialer.postcall.PostCall; 58 import com.android.dialer.precall.PreCall; 59 import com.android.dialer.rtt.RttTranscriptActivity; 60 import com.android.dialer.rtt.RttTranscriptUtil; 61 import com.android.dialer.theme.base.ThemeComponent; 62 import com.google.common.base.Preconditions; 63 import com.google.common.collect.ImmutableSet; 64 import com.google.i18n.phonenumbers.NumberParseException; 65 import com.google.i18n.phonenumbers.PhoneNumberUtil; 66 import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber; 67 import java.lang.ref.WeakReference; 68 import java.util.ArrayList; 69 import java.util.Collections; 70 import java.util.List; 71 import java.util.Map; 72 73 /** 74 * Contains common logic shared between {@link OldCallDetailsActivity} and {@link 75 * CallDetailsActivity}. 76 */ 77 abstract class CallDetailsActivityCommon extends AppCompatActivity { 78 79 public static final String EXTRA_PHONE_NUMBER = "phone_number"; 80 public static final String EXTRA_HAS_ENRICHED_CALL_DATA = "has_enriched_call_data"; 81 public static final String EXTRA_CAN_REPORT_CALLER_ID = "can_report_caller_id"; 82 public static final String EXTRA_CAN_SUPPORT_ASSISTED_DIALING = "can_support_assisted_dialing"; 83 84 private final CallDetailsEntryViewHolder.CallDetailsEntryListener callDetailsEntryListener = 85 new CallDetailsEntryListener(this); 86 private final CallDetailsHeaderViewHolder.CallDetailsHeaderListener callDetailsHeaderListener = 87 new CallDetailsHeaderListener(this); 88 private final CallDetailsFooterViewHolder.DeleteCallDetailsListener deleteCallDetailsListener = 89 new DeleteCallDetailsListener(this); 90 private final CallDetailsFooterViewHolder.ReportCallIdListener reportCallIdListener = 91 new ReportCallIdListener(this); 92 private final EnrichedCallManager.HistoricalDataChangedListener 93 enrichedCallHistoricalDataChangedListener = 94 new EnrichedCallHistoricalDataChangedListener(this); 95 96 private CallDetailsAdapterCommon adapter; 97 private CallDetailsEntries callDetailsEntries; 98 private UiListener<ImmutableSet<String>> checkRttTranscriptAvailabilityListener; 99 100 /** 101 * Handles the intent that launches {@link OldCallDetailsActivity} or {@link CallDetailsActivity}, 102 * e.g., extract data from intent extras, start loading data, etc. 103 */ handleIntent(Intent intent)104 protected abstract void handleIntent(Intent intent); 105 106 /** Creates an adapter for {@link OldCallDetailsActivity} or {@link CallDetailsActivity}. */ createAdapter( CallDetailsEntryViewHolder.CallDetailsEntryListener callDetailsEntryListener, CallDetailsHeaderViewHolder.CallDetailsHeaderListener callDetailsHeaderListener, CallDetailsFooterViewHolder.ReportCallIdListener reportCallIdListener, CallDetailsFooterViewHolder.DeleteCallDetailsListener deleteCallDetailsListener)107 protected abstract CallDetailsAdapterCommon createAdapter( 108 CallDetailsEntryViewHolder.CallDetailsEntryListener callDetailsEntryListener, 109 CallDetailsHeaderViewHolder.CallDetailsHeaderListener callDetailsHeaderListener, 110 CallDetailsFooterViewHolder.ReportCallIdListener reportCallIdListener, 111 CallDetailsFooterViewHolder.DeleteCallDetailsListener deleteCallDetailsListener); 112 113 /** Returns the phone number of the call details. */ getNumber()114 protected abstract String getNumber(); 115 116 @Override 117 @CallSuper onCreate(Bundle savedInstanceState)118 protected void onCreate(Bundle savedInstanceState) { 119 super.onCreate(savedInstanceState); 120 setTheme(ThemeComponent.get(this).theme().getApplicationThemeRes()); 121 setContentView(R.layout.call_details_activity); 122 Toolbar toolbar = findViewById(R.id.toolbar); 123 toolbar.setTitle(R.string.call_details); 124 toolbar.setNavigationOnClickListener( 125 v -> { 126 PerformanceReport.recordClick(UiAction.Type.CLOSE_CALL_DETAIL_WITH_CANCEL_BUTTON); 127 finish(); 128 }); 129 checkRttTranscriptAvailabilityListener = 130 DialerExecutorComponent.get(this) 131 .createUiListener(getFragmentManager(), "Query RTT transcript availability"); 132 handleIntent(getIntent()); 133 setupRecyclerViewForEntries(); 134 } 135 136 @Override 137 @CallSuper onResume()138 protected void onResume() { 139 super.onResume(); 140 141 // Some calls may not be recorded (eg. from quick contact), 142 // so we should restart recording after these calls. (Recorded call is stopped) 143 PostCall.restartPerformanceRecordingIfARecentCallExist(this); 144 if (!PerformanceReport.isRecording()) { 145 PerformanceReport.startRecording(); 146 } 147 148 PostCall.promptUserForMessageIfNecessary(this, findViewById(R.id.recycler_view)); 149 150 EnrichedCallComponent.get(this) 151 .getEnrichedCallManager() 152 .registerHistoricalDataChangedListener(enrichedCallHistoricalDataChangedListener); 153 EnrichedCallComponent.get(this) 154 .getEnrichedCallManager() 155 .requestAllHistoricalData(getNumber(), callDetailsEntries); 156 } 157 loadRttTranscriptAvailability()158 protected void loadRttTranscriptAvailability() { 159 ImmutableSet.Builder<String> builder = ImmutableSet.builder(); 160 for (CallDetailsEntry entry : callDetailsEntries.getEntriesList()) { 161 builder.add(entry.getCallMappingId()); 162 } 163 checkRttTranscriptAvailabilityListener.listen( 164 this, 165 RttTranscriptUtil.getAvailableRttTranscriptIds(this, builder.build()), 166 this::updateCallDetailsEntriesWithRttTranscriptAvailability, 167 throwable -> { 168 throw new RuntimeException(throwable); 169 }); 170 } 171 updateCallDetailsEntriesWithRttTranscriptAvailability( ImmutableSet<String> availableTranscripIds)172 private void updateCallDetailsEntriesWithRttTranscriptAvailability( 173 ImmutableSet<String> availableTranscripIds) { 174 CallDetailsEntries.Builder mutableCallDetailsEntries = CallDetailsEntries.newBuilder(); 175 for (CallDetailsEntry entry : callDetailsEntries.getEntriesList()) { 176 CallDetailsEntry.Builder newEntry = CallDetailsEntry.newBuilder().mergeFrom(entry); 177 newEntry.setHasRttTranscript(availableTranscripIds.contains(entry.getCallMappingId())); 178 mutableCallDetailsEntries.addEntries(newEntry.build()); 179 } 180 setCallDetailsEntries(mutableCallDetailsEntries.build()); 181 } 182 183 @Override 184 @CallSuper onPause()185 protected void onPause() { 186 super.onPause(); 187 188 EnrichedCallComponent.get(this) 189 .getEnrichedCallManager() 190 .unregisterHistoricalDataChangedListener(enrichedCallHistoricalDataChangedListener); 191 } 192 193 @Override 194 @CallSuper onNewIntent(Intent intent)195 protected void onNewIntent(Intent intent) { 196 super.onNewIntent(intent); 197 198 handleIntent(intent); 199 setupRecyclerViewForEntries(); 200 } 201 setupRecyclerViewForEntries()202 private void setupRecyclerViewForEntries() { 203 adapter = 204 createAdapter( 205 callDetailsEntryListener, 206 callDetailsHeaderListener, 207 reportCallIdListener, 208 deleteCallDetailsListener); 209 210 RecyclerView recyclerView = findViewById(R.id.recycler_view); 211 recyclerView.setLayoutManager(new LinearLayoutManager(this)); 212 recyclerView.setAdapter(adapter); 213 PerformanceReport.logOnScrollStateChange(recyclerView); 214 } 215 getAdapter()216 final CallDetailsAdapterCommon getAdapter() { 217 return adapter; 218 } 219 220 @Override 221 @CallSuper onBackPressed()222 public void onBackPressed() { 223 PerformanceReport.recordClick(UiAction.Type.PRESS_ANDROID_BACK_BUTTON); 224 super.onBackPressed(); 225 } 226 227 @MainThread setCallDetailsEntries(CallDetailsEntries entries)228 protected final void setCallDetailsEntries(CallDetailsEntries entries) { 229 Assert.isMainThread(); 230 this.callDetailsEntries = entries; 231 if (adapter != null) { 232 adapter.updateCallDetailsEntries(entries); 233 } 234 } 235 getCallDetailsEntries()236 protected final CallDetailsEntries getCallDetailsEntries() { 237 return callDetailsEntries; 238 } 239 240 /** A {@link Worker} that deletes specified entries from the call log. */ 241 private static final class DeleteCallsWorker implements Worker<CallDetailsEntries, Void> { 242 // Use a weak reference to hold the Activity so that there is no memory leak. 243 private final WeakReference<Context> contextWeakReference; 244 DeleteCallsWorker(Context context)245 DeleteCallsWorker(Context context) { 246 this.contextWeakReference = new WeakReference<>(context); 247 } 248 249 @Override 250 // Suppress the lint check here as the user will not be able to see call log entries if 251 // permission.WRITE_CALL_LOG is not granted. 252 @SuppressLint("MissingPermission") 253 @RequiresPermission(value = permission.WRITE_CALL_LOG) doInBackground(CallDetailsEntries callDetailsEntries)254 public Void doInBackground(CallDetailsEntries callDetailsEntries) { 255 Context context = contextWeakReference.get(); 256 if (context == null) { 257 return null; 258 } 259 260 Selection selection = 261 Selection.builder() 262 .and(Selection.column(CallLog.Calls._ID).in(getCallLogIdList(callDetailsEntries))) 263 .build(); 264 265 context 266 .getContentResolver() 267 .delete(Calls.CONTENT_URI, selection.getSelection(), selection.getSelectionArgs()); 268 return null; 269 } 270 getCallLogIdList(CallDetailsEntries callDetailsEntries)271 private static List<String> getCallLogIdList(CallDetailsEntries callDetailsEntries) { 272 Assert.checkArgument(callDetailsEntries.getEntriesCount() > 0); 273 274 List<String> idStrings = new ArrayList<>(callDetailsEntries.getEntriesCount()); 275 276 for (CallDetailsEntry entry : callDetailsEntries.getEntriesList()) { 277 idStrings.add(String.valueOf(entry.getCallId())); 278 } 279 280 return idStrings; 281 } 282 } 283 284 private static final class CallDetailsEntryListener 285 implements CallDetailsEntryViewHolder.CallDetailsEntryListener { 286 private final WeakReference<CallDetailsActivityCommon> activityWeakReference; 287 CallDetailsEntryListener(CallDetailsActivityCommon activity)288 CallDetailsEntryListener(CallDetailsActivityCommon activity) { 289 this.activityWeakReference = new WeakReference<>(activity); 290 } 291 292 @Override showRttTranscript(String transcriptId, String primaryText, PhotoInfo photoInfo)293 public void showRttTranscript(String transcriptId, String primaryText, PhotoInfo photoInfo) { 294 getActivity() 295 .startActivity( 296 RttTranscriptActivity.getIntent(getActivity(), transcriptId, primaryText, photoInfo)); 297 } 298 getActivity()299 private CallDetailsActivityCommon getActivity() { 300 return Preconditions.checkNotNull(activityWeakReference.get()); 301 } 302 } 303 304 private static final class CallDetailsHeaderListener 305 implements CallDetailsHeaderViewHolder.CallDetailsHeaderListener { 306 private final WeakReference<CallDetailsActivityCommon> activityWeakReference; 307 CallDetailsHeaderListener(CallDetailsActivityCommon activity)308 CallDetailsHeaderListener(CallDetailsActivityCommon activity) { 309 this.activityWeakReference = new WeakReference<>(activity); 310 } 311 312 @Override placeImsVideoCall(String phoneNumber)313 public void placeImsVideoCall(String phoneNumber) { 314 Logger.get(getActivity()) 315 .logImpression(DialerImpression.Type.CALL_DETAILS_IMS_VIDEO_CALL_BACK); 316 PreCall.start( 317 getActivity(), 318 new CallIntentBuilder(phoneNumber, CallInitiationType.Type.CALL_DETAILS) 319 .setIsVideoCall(true)); 320 } 321 322 @Override placeDuoVideoCall(String phoneNumber)323 public void placeDuoVideoCall(String phoneNumber) { 324 Logger.get(getActivity()) 325 .logImpression(DialerImpression.Type.CALL_DETAILS_LIGHTBRINGER_CALL_BACK); 326 PreCall.start( 327 getActivity(), 328 new CallIntentBuilder(phoneNumber, CallInitiationType.Type.CALL_DETAILS) 329 .setIsDuoCall(true) 330 .setIsVideoCall(true)); 331 } 332 333 @Override placeVoiceCall(String phoneNumber, String postDialDigits)334 public void placeVoiceCall(String phoneNumber, String postDialDigits) { 335 Logger.get(getActivity()).logImpression(DialerImpression.Type.CALL_DETAILS_VOICE_CALL_BACK); 336 337 boolean canSupportedAssistedDialing = 338 getActivity() 339 .getIntent() 340 .getExtras() 341 .getBoolean(EXTRA_CAN_SUPPORT_ASSISTED_DIALING, false); 342 CallIntentBuilder callIntentBuilder = 343 new CallIntentBuilder(phoneNumber + postDialDigits, CallInitiationType.Type.CALL_DETAILS); 344 if (canSupportedAssistedDialing) { 345 callIntentBuilder.setAllowAssistedDial(true); 346 } 347 348 PreCall.start(getActivity(), callIntentBuilder); 349 } 350 getActivity()351 private CallDetailsActivityCommon getActivity() { 352 return Preconditions.checkNotNull(activityWeakReference.get()); 353 } 354 355 @Override openAssistedDialingSettings(View unused)356 public void openAssistedDialingSettings(View unused) { 357 Intent intent = new Intent(getActivity(), AssistedDialingSettingActivity.class); 358 getActivity().startActivity(intent); 359 } 360 361 @Override createAssistedDialerNumberParserTask( AssistedDialingNumberParseWorker worker, SuccessListener<Integer> successListener, FailureListener failureListener)362 public void createAssistedDialerNumberParserTask( 363 AssistedDialingNumberParseWorker worker, 364 SuccessListener<Integer> successListener, 365 FailureListener failureListener) { 366 DialerExecutorComponent.get(getActivity().getApplicationContext()) 367 .dialerExecutorFactory() 368 .createUiTaskBuilder( 369 getActivity().getFragmentManager(), 370 "CallDetailsActivityCommon.createAssistedDialerNumberParserTask", 371 new AssistedDialingNumberParseWorker()) 372 .onSuccess(successListener) 373 .onFailure(failureListener) 374 .build() 375 .executeParallel(getActivity().getNumber()); 376 } 377 } 378 379 static final class AssistedDialingNumberParseWorker implements Worker<String, Integer> { 380 381 @Override doInBackground(@onNull String phoneNumber)382 public Integer doInBackground(@NonNull String phoneNumber) { 383 PhoneNumber parsedNumber; 384 try { 385 parsedNumber = PhoneNumberUtil.getInstance().parse(phoneNumber, null); 386 } catch (NumberParseException e) { 387 LogUtil.w( 388 "AssistedDialingNumberParseWorker.doInBackground", 389 "couldn't parse phone number: " + LogUtil.sanitizePii(phoneNumber), 390 e); 391 return 0; 392 } 393 return parsedNumber.getCountryCode(); 394 } 395 } 396 397 private static final class DeleteCallDetailsListener 398 implements CallDetailsFooterViewHolder.DeleteCallDetailsListener { 399 400 private final WeakReference<CallDetailsActivityCommon> activityWeakReference; 401 DeleteCallDetailsListener(CallDetailsActivityCommon activity)402 DeleteCallDetailsListener(CallDetailsActivityCommon activity) { 403 this.activityWeakReference = new WeakReference<>(activity); 404 } 405 406 @Override delete()407 public void delete() { 408 CallDetailsActivityCommon activity = getActivity(); 409 Logger.get(activity).logImpression(DialerImpression.Type.USER_DELETED_CALL_LOG_ITEM); 410 DialerExecutorComponent.get(activity) 411 .dialerExecutorFactory() 412 .createNonUiTaskBuilder(new DeleteCallsWorker(activity)) 413 .onSuccess( 414 unused -> { 415 Intent data = new Intent(); 416 data.putExtra(EXTRA_PHONE_NUMBER, activity.getNumber()); 417 for (CallDetailsEntry entry : activity.getCallDetailsEntries().getEntriesList()) { 418 if (entry.getHistoryResultsCount() > 0) { 419 data.putExtra(EXTRA_HAS_ENRICHED_CALL_DATA, true); 420 break; 421 } 422 } 423 424 activity.setResult(RESULT_OK, data); 425 activity.finish(); 426 }) 427 .build() 428 .executeSerial(activity.getCallDetailsEntries()); 429 } 430 getActivity()431 private CallDetailsActivityCommon getActivity() { 432 return Preconditions.checkNotNull(activityWeakReference.get()); 433 } 434 } 435 436 private static final class ReportCallIdListener 437 implements CallDetailsFooterViewHolder.ReportCallIdListener { 438 private final WeakReference<Activity> activityWeakReference; 439 ReportCallIdListener(Activity activity)440 ReportCallIdListener(Activity activity) { 441 this.activityWeakReference = new WeakReference<>(activity); 442 } 443 444 @Override reportCallId(String number)445 public void reportCallId(String number) { 446 ReportDialogFragment.newInstance(number) 447 .show(getActivity().getFragmentManager(), null /* tag */); 448 } 449 450 @Override canReportCallerId(String number)451 public boolean canReportCallerId(String number) { 452 return getActivity().getIntent().getExtras().getBoolean(EXTRA_CAN_REPORT_CALLER_ID, false); 453 } 454 getActivity()455 private Activity getActivity() { 456 return Preconditions.checkNotNull(activityWeakReference.get()); 457 } 458 } 459 460 private static final class EnrichedCallHistoricalDataChangedListener 461 implements EnrichedCallManager.HistoricalDataChangedListener { 462 private final WeakReference<CallDetailsActivityCommon> activityWeakReference; 463 EnrichedCallHistoricalDataChangedListener(CallDetailsActivityCommon activity)464 EnrichedCallHistoricalDataChangedListener(CallDetailsActivityCommon activity) { 465 this.activityWeakReference = new WeakReference<>(activity); 466 } 467 468 @Override onHistoricalDataChanged()469 public void onHistoricalDataChanged() { 470 CallDetailsActivityCommon activity = getActivity(); 471 Map<CallDetailsEntry, List<HistoryResult>> mappedResults = 472 getAllHistoricalData(activity.getNumber(), activity.callDetailsEntries); 473 474 activity.setCallDetailsEntries( 475 generateAndMapNewCallDetailsEntriesHistoryResults( 476 activity.getNumber(), activity.callDetailsEntries, mappedResults)); 477 } 478 getActivity()479 private CallDetailsActivityCommon getActivity() { 480 return Preconditions.checkNotNull(activityWeakReference.get()); 481 } 482 483 @NonNull getAllHistoricalData( @ullable String number, @NonNull CallDetailsEntries entries)484 private Map<CallDetailsEntry, List<HistoryResult>> getAllHistoricalData( 485 @Nullable String number, @NonNull CallDetailsEntries entries) { 486 if (number == null) { 487 return Collections.emptyMap(); 488 } 489 490 Map<CallDetailsEntry, List<HistoryResult>> historicalData = 491 EnrichedCallComponent.get(getActivity()) 492 .getEnrichedCallManager() 493 .getAllHistoricalData(number, entries); 494 if (historicalData == null) { 495 return Collections.emptyMap(); 496 } 497 return historicalData; 498 } 499 generateAndMapNewCallDetailsEntriesHistoryResults( @ullable String number, @NonNull CallDetailsEntries callDetailsEntries, @NonNull Map<CallDetailsEntry, List<HistoryResult>> mappedResults)500 private static CallDetailsEntries generateAndMapNewCallDetailsEntriesHistoryResults( 501 @Nullable String number, 502 @NonNull CallDetailsEntries callDetailsEntries, 503 @NonNull Map<CallDetailsEntry, List<HistoryResult>> mappedResults) { 504 if (number == null) { 505 return callDetailsEntries; 506 } 507 CallDetailsEntries.Builder mutableCallDetailsEntries = CallDetailsEntries.newBuilder(); 508 for (CallDetailsEntry entry : callDetailsEntries.getEntriesList()) { 509 CallDetailsEntry.Builder newEntry = CallDetailsEntry.newBuilder().mergeFrom(entry); 510 List<HistoryResult> results = mappedResults.get(entry); 511 if (results != null) { 512 newEntry.addAllHistoryResults(mappedResults.get(entry)); 513 LogUtil.v( 514 "CallDetailsActivityCommon.generateAndMapNewCallDetailsEntriesHistoryResults", 515 "mapped %d results", 516 newEntry.getHistoryResultsList().size()); 517 } 518 mutableCallDetailsEntries.addEntries(newEntry.build()); 519 } 520 return mutableCallDetailsEntries.build(); 521 } 522 } 523 } 524