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