1/*
2 * Copyright (C) 2009 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
17package com.android.dialer;
18
19import android.content.ContentUris;
20import android.content.Context;
21import android.content.Intent;
22import android.content.res.Resources;
23import android.net.Uri;
24import android.os.Bundle;
25import android.provider.ContactsContract.CommonDataKinds.Phone;
26import android.support.v7.app.AppCompatActivity;
27import android.text.BidiFormatter;
28import android.text.TextDirectionHeuristics;
29import android.text.TextUtils;
30import android.util.Log;
31import android.view.LayoutInflater;
32import android.view.Menu;
33import android.view.MenuItem;
34import android.view.MotionEvent;
35import android.view.View;
36import android.widget.ListView;
37import android.widget.QuickContactBadge;
38import android.widget.TextView;
39import android.widget.Toast;
40
41import com.android.contacts.common.CallUtil;
42import com.android.contacts.common.ClipboardUtils;
43import com.android.contacts.common.ContactPhotoManager;
44import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest;
45import com.android.contacts.common.GeoUtil;
46import com.android.contacts.common.compat.CompatUtils;
47import com.android.contacts.common.interactions.TouchPointManager;
48import com.android.contacts.common.preference.ContactsPreferences;
49import com.android.contacts.common.testing.NeededForTesting;
50import com.android.contacts.common.util.UriUtils;
51import com.android.dialer.calllog.CallDetailHistoryAdapter;
52import com.android.dialer.calllog.CallLogAsyncTaskUtil;
53import com.android.dialer.calllog.CallLogAsyncTaskUtil.CallLogAsyncTaskListener;
54import com.android.dialer.calllog.CallTypeHelper;
55import com.android.dialer.calllog.ContactInfoHelper;
56import com.android.dialer.calllog.PhoneAccountUtils;
57import com.android.dialer.compat.FilteredNumberCompat;
58import com.android.dialer.database.FilteredNumberAsyncQueryHandler;
59import com.android.dialer.database.FilteredNumberAsyncQueryHandler.OnCheckBlockedListener;
60import com.android.dialer.filterednumber.BlockNumberDialogFragment;
61import com.android.dialer.filterednumber.FilteredNumbersUtil;
62import com.android.dialer.logging.InteractionEvent;
63import com.android.dialer.logging.Logger;
64import com.android.dialer.util.DialerUtils;
65import com.android.dialer.util.IntentUtil.CallIntentBuilder;
66import com.android.dialer.util.PhoneNumberUtil;
67import com.android.dialer.util.TelecomUtil;
68import com.android.incallui.Call.LogState;
69
70/**
71 * Displays the details of a specific call log entry.
72 * <p>
73 * This activity can be either started with the URI of a single call log entry, or with the
74 * {@link #EXTRA_CALL_LOG_IDS} extra to specify a group of call log entries.
75 */
76public class CallDetailActivity extends AppCompatActivity
77        implements MenuItem.OnMenuItemClickListener, View.OnClickListener,
78                BlockNumberDialogFragment.Callback {
79    private static final String TAG = CallDetailActivity.class.getSimpleName();
80
81     /** A long array extra containing ids of call log entries to display. */
82    public static final String EXTRA_CALL_LOG_IDS = "EXTRA_CALL_LOG_IDS";
83    /** If we are started with a voicemail, we'll find the uri to play with this extra. */
84    public static final String EXTRA_VOICEMAIL_URI = "EXTRA_VOICEMAIL_URI";
85    /** If the activity was triggered from a notification. */
86    public static final String EXTRA_FROM_NOTIFICATION = "EXTRA_FROM_NOTIFICATION";
87
88    public static final String VOICEMAIL_FRAGMENT_TAG = "voicemail_fragment";
89
90    private CallLogAsyncTaskListener mCallLogAsyncTaskListener = new CallLogAsyncTaskListener() {
91        @Override
92        public void onDeleteCall() {
93            finish();
94        }
95
96        @Override
97        public void onDeleteVoicemail() {
98            finish();
99        }
100
101        @Override
102        public void onGetCallDetails(PhoneCallDetails[] details) {
103            if (details == null) {
104                // Somewhere went wrong: we're going to bail out and show error to users.
105                Toast.makeText(mContext, R.string.toast_call_detail_error,
106                        Toast.LENGTH_SHORT).show();
107                finish();
108                return;
109            }
110
111            // All calls are from the same number and same contact, so pick the first detail.
112            mDetails = details[0];
113            mNumber = TextUtils.isEmpty(mDetails.number) ? null : mDetails.number.toString();
114            mPostDialDigits = TextUtils.isEmpty(mDetails.postDialDigits)
115                    ? "" : mDetails.postDialDigits;
116            mDisplayNumber = mDetails.displayNumber;
117
118            final CharSequence callLocationOrType = getNumberTypeOrLocation(mDetails);
119
120            final CharSequence displayNumber;
121            if (!TextUtils.isEmpty(mDetails.postDialDigits)) {
122                displayNumber = mDetails.number + mDetails.postDialDigits;
123            } else {
124                displayNumber = mDetails.displayNumber;
125            }
126
127            final String displayNumberStr = mBidiFormatter.unicodeWrap(
128                    displayNumber.toString(), TextDirectionHeuristics.LTR);
129
130            mDetails.nameDisplayOrder = mContactsPreferences.getDisplayOrder();
131
132            if (!TextUtils.isEmpty(mDetails.getPreferredName())) {
133                mCallerName.setText(mDetails.getPreferredName());
134                mCallerNumber.setText(callLocationOrType + " " + displayNumberStr);
135            } else {
136                mCallerName.setText(displayNumberStr);
137                if (!TextUtils.isEmpty(callLocationOrType)) {
138                    mCallerNumber.setText(callLocationOrType);
139                    mCallerNumber.setVisibility(View.VISIBLE);
140                } else {
141                    mCallerNumber.setVisibility(View.GONE);
142                }
143            }
144
145            CharSequence accountLabel = PhoneAccountUtils.getAccountLabel(mContext,
146                    mDetails.accountHandle);
147            CharSequence accountContentDescription =
148                    PhoneCallDetails.createAccountLabelDescription(mResources, mDetails.viaNumber,
149                            accountLabel);
150            if (!TextUtils.isEmpty(mDetails.viaNumber)) {
151                if (!TextUtils.isEmpty(accountLabel)) {
152                    accountLabel = mResources.getString(R.string.call_log_via_number_phone_account,
153                            accountLabel, mDetails.viaNumber);
154                } else {
155                    accountLabel = mResources.getString(R.string.call_log_via_number,
156                            mDetails.viaNumber);
157                }
158            }
159            if (!TextUtils.isEmpty(accountLabel)) {
160                mAccountLabel.setText(accountLabel);
161                mAccountLabel.setContentDescription(accountContentDescription);
162                mAccountLabel.setVisibility(View.VISIBLE);
163            } else {
164                mAccountLabel.setVisibility(View.GONE);
165            }
166
167            final boolean canPlaceCallsTo =
168                    PhoneNumberUtil.canPlaceCallsTo(mNumber, mDetails.numberPresentation);
169            mCallButton.setVisibility(canPlaceCallsTo ? View.VISIBLE : View.GONE);
170            mCopyNumberActionItem.setVisibility(canPlaceCallsTo ? View.VISIBLE : View.GONE);
171
172            updateBlockActionItemVisibility(canPlaceCallsTo ? View.VISIBLE : View.GONE);
173
174            final boolean isSipNumber = PhoneNumberUtil.isSipNumber(mNumber);
175            final boolean isVoicemailNumber =
176                    PhoneNumberUtil.isVoicemailNumber(mContext, mDetails.accountHandle, mNumber);
177            final boolean showEditNumberBeforeCallAction =
178                    canPlaceCallsTo && !isSipNumber && !isVoicemailNumber;
179            mEditBeforeCallActionItem.setVisibility(
180                    showEditNumberBeforeCallAction ? View.VISIBLE : View.GONE);
181
182            final boolean showReportAction = mContactInfoHelper.canReportAsInvalid(
183                    mDetails.sourceType, mDetails.objectId);
184            mReportActionItem.setVisibility(
185                    showReportAction ? View.VISIBLE : View.GONE);
186
187            invalidateOptionsMenu();
188
189            mHistoryList.setAdapter(
190                    new CallDetailHistoryAdapter(mContext, mInflater, mCallTypeHelper, details));
191
192            updateFilteredNumberChanges();
193            updateContactPhoto();
194
195            findViewById(R.id.call_detail).setVisibility(View.VISIBLE);
196        }
197
198        /**
199         * Determines the location geocode text for a call, or the phone number type
200         * (if available).
201         *
202         * @param details The call details.
203         * @return The phone number type or location.
204         */
205        private CharSequence getNumberTypeOrLocation(PhoneCallDetails details) {
206            if (!TextUtils.isEmpty(details.namePrimary)) {
207                return Phone.getTypeLabel(mResources, details.numberType,
208                        details.numberLabel);
209            } else {
210                return details.geocode;
211            }
212        }
213    };
214
215    private Context mContext;
216    private ContactInfoHelper mContactInfoHelper;
217    private ContactsPreferences mContactsPreferences;
218    private CallTypeHelper mCallTypeHelper;
219    private ContactPhotoManager mContactPhotoManager;
220    private FilteredNumberAsyncQueryHandler mFilteredNumberAsyncQueryHandler;
221    private BidiFormatter mBidiFormatter = BidiFormatter.getInstance();
222    private LayoutInflater mInflater;
223    private Resources mResources;
224
225    private PhoneCallDetails mDetails;
226    protected String mNumber;
227    private Uri mVoicemailUri;
228    private String mPostDialDigits = "";
229    private String mDisplayNumber;
230
231    private ListView mHistoryList;
232    private QuickContactBadge mQuickContactBadge;
233    private TextView mCallerName;
234    private TextView mCallerNumber;
235    private TextView mAccountLabel;
236    private View mCallButton;
237
238    private TextView mBlockNumberActionItem;
239    private View mEditBeforeCallActionItem;
240    private View mReportActionItem;
241    private View mCopyNumberActionItem;
242
243    private Integer mBlockedNumberId;
244
245    @Override
246    protected void onCreate(Bundle icicle) {
247        super.onCreate(icicle);
248
249        mContext = this;
250        mResources = getResources();
251        mContactInfoHelper = new ContactInfoHelper(this, GeoUtil.getCurrentCountryIso(this));
252        mContactsPreferences = new ContactsPreferences(mContext);
253        mCallTypeHelper = new CallTypeHelper(getResources());
254        mFilteredNumberAsyncQueryHandler =
255                new FilteredNumberAsyncQueryHandler(getContentResolver());
256
257        mVoicemailUri = getIntent().getParcelableExtra(EXTRA_VOICEMAIL_URI);
258
259        getSupportActionBar().setDisplayHomeAsUpEnabled(true);
260
261        setContentView(R.layout.call_detail);
262        mInflater = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE);
263
264        mHistoryList = (ListView) findViewById(R.id.history);
265        mHistoryList.addHeaderView(mInflater.inflate(R.layout.call_detail_header, null));
266        mHistoryList.addFooterView(
267                mInflater.inflate(R.layout.call_detail_footer, null), null, false);
268
269        mQuickContactBadge = (QuickContactBadge) findViewById(R.id.quick_contact_photo);
270        mQuickContactBadge.setOverlay(null);
271        if (CompatUtils.hasPrioritizedMimeType()) {
272            mQuickContactBadge.setPrioritizedMimeType(Phone.CONTENT_ITEM_TYPE);
273        }
274        mCallerName = (TextView) findViewById(R.id.caller_name);
275        mCallerNumber = (TextView) findViewById(R.id.caller_number);
276        mAccountLabel = (TextView) findViewById(R.id.phone_account_label);
277        mContactPhotoManager = ContactPhotoManager.getInstance(this);
278
279        mCallButton = findViewById(R.id.call_back_button);
280        mCallButton.setOnClickListener(new View.OnClickListener() {
281            @Override
282            public void onClick(View view) {
283                if (TextUtils.isEmpty(mNumber)) {
284                    return;
285                }
286                mContext.startActivity(
287                        new CallIntentBuilder(getDialableNumber())
288                                .setCallInitiationType(LogState.INITIATION_CALL_DETAILS)
289                                .build());
290            }
291        });
292
293
294        mBlockNumberActionItem = (TextView) findViewById(R.id.call_detail_action_block);
295        updateBlockActionItemVisibility(View.VISIBLE);
296        mBlockNumberActionItem.setOnClickListener(this);
297        mEditBeforeCallActionItem = findViewById(R.id.call_detail_action_edit_before_call);
298        mEditBeforeCallActionItem.setOnClickListener(this);
299        mReportActionItem = findViewById(R.id.call_detail_action_report);
300        mReportActionItem.setOnClickListener(this);
301
302        mCopyNumberActionItem = findViewById(R.id.call_detail_action_copy);
303        mCopyNumberActionItem.setOnClickListener(this);
304
305        if (getIntent().getBooleanExtra(EXTRA_FROM_NOTIFICATION, false)) {
306            closeSystemDialogs();
307        }
308    }
309
310    private void updateBlockActionItemVisibility(int visibility) {
311        if (!FilteredNumberCompat.canAttemptBlockOperations(mContext)) {
312            visibility = View.GONE;
313        }
314        mBlockNumberActionItem.setVisibility(visibility);
315    }
316
317    @Override
318    public void onResume() {
319        super.onResume();
320        mContactsPreferences.refreshValue(ContactsPreferences.DISPLAY_ORDER_KEY);
321        getCallDetails();
322    }
323
324    @Override
325    public boolean dispatchTouchEvent(MotionEvent ev) {
326        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
327            TouchPointManager.getInstance().setPoint((int) ev.getRawX(), (int) ev.getRawY());
328        }
329        return super.dispatchTouchEvent(ev);
330    }
331
332    public void getCallDetails() {
333        CallLogAsyncTaskUtil.getCallDetails(this, getCallLogEntryUris(), mCallLogAsyncTaskListener);
334    }
335
336    /**
337     * Returns the list of URIs to show.
338     * <p>
339     * There are two ways the URIs can be provided to the activity: as the data on the intent, or as
340     * a list of ids in the call log added as an extra on the URI.
341     * <p>
342     * If both are available, the data on the intent takes precedence.
343     */
344    private Uri[] getCallLogEntryUris() {
345        final Uri uri = getIntent().getData();
346        if (uri != null) {
347            // If there is a data on the intent, it takes precedence over the extra.
348            return new Uri[]{ uri };
349        }
350        final long[] ids = getIntent().getLongArrayExtra(EXTRA_CALL_LOG_IDS);
351        final int numIds = ids == null ? 0 : ids.length;
352        final Uri[] uris = new Uri[numIds];
353        for (int index = 0; index < numIds; ++index) {
354            uris[index] = ContentUris.withAppendedId(
355                    TelecomUtil.getCallLogUri(CallDetailActivity.this), ids[index]);
356        }
357        return uris;
358    }
359
360    @Override
361    public boolean onCreateOptionsMenu(Menu menu) {
362        final MenuItem deleteMenuItem = menu.add(
363                Menu.NONE,
364                R.id.call_detail_delete_menu_item,
365                Menu.NONE,
366                R.string.call_details_delete);
367        deleteMenuItem.setIcon(R.drawable.ic_delete_24dp);
368        deleteMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
369        deleteMenuItem.setOnMenuItemClickListener(this);
370
371        return super.onCreateOptionsMenu(menu);
372    }
373
374    @Override
375    public boolean onMenuItemClick(MenuItem item) {
376        if (item.getItemId() == R.id.call_detail_delete_menu_item) {
377            if (hasVoicemail()) {
378                CallLogAsyncTaskUtil.deleteVoicemail(
379                        this, mVoicemailUri, mCallLogAsyncTaskListener);
380            } else {
381                final StringBuilder callIds = new StringBuilder();
382                for (Uri callUri : getCallLogEntryUris()) {
383                    if (callIds.length() != 0) {
384                        callIds.append(",");
385                    }
386                    callIds.append(ContentUris.parseId(callUri));
387                }
388                CallLogAsyncTaskUtil.deleteCalls(
389                        this, callIds.toString(), mCallLogAsyncTaskListener);
390            }
391        }
392        return true;
393    }
394
395    @Override
396    public void onClick(View view) {
397        int resId = view.getId();
398        if (resId == R.id.call_detail_action_block) {
399            FilteredNumberCompat
400                    .showBlockNumberDialogFlow(mContext.getContentResolver(), mBlockedNumberId,
401                            mNumber, mDetails.countryIso, mDisplayNumber, R.id.call_detail,
402                            getFragmentManager(), this);
403        } else if (resId == R.id.call_detail_action_copy) {
404            ClipboardUtils.copyText(mContext, null, mNumber, true);
405        } else if (resId == R.id.call_detail_action_edit_before_call) {
406            Intent dialIntent = new Intent(Intent.ACTION_DIAL,
407                    CallUtil.getCallUri(getDialableNumber()));
408            DialerUtils.startActivityWithErrorToast(mContext, dialIntent);
409        } else {
410            Log.wtf(TAG, "Unexpected onClick event from " + view);
411        }
412    }
413
414    @Override
415    public void onFilterNumberSuccess() {
416        Logger.logInteraction(InteractionEvent.BLOCK_NUMBER_CALL_DETAIL);
417        updateFilteredNumberChanges();
418    }
419
420    @Override
421    public void onUnfilterNumberSuccess() {
422        Logger.logInteraction(InteractionEvent.UNBLOCK_NUMBER_CALL_DETAIL);
423        updateFilteredNumberChanges();
424    }
425
426    @Override
427    public void onChangeFilteredNumberUndo() {
428        updateFilteredNumberChanges();
429    }
430
431    private void updateFilteredNumberChanges() {
432        if (mDetails == null ||
433                !FilteredNumbersUtil.canBlockNumber(this, mNumber, mDetails.countryIso)) {
434            return;
435        }
436
437        final boolean success = mFilteredNumberAsyncQueryHandler.isBlockedNumber(
438                new OnCheckBlockedListener() {
439                    @Override
440                    public void onCheckComplete(Integer id) {
441                        mBlockedNumberId = id;
442                        updateBlockActionItem();
443                    }
444                }, mNumber, mDetails.countryIso);
445
446        if (!success) {
447            updateBlockActionItem();
448        }
449    }
450
451    // Loads and displays the contact photo.
452    private void updateContactPhoto() {
453        if (mDetails == null) {
454            return;
455        }
456
457        final boolean isVoicemailNumber =
458                PhoneNumberUtil.isVoicemailNumber(mContext, mDetails.accountHandle, mNumber);
459        final boolean isBusiness = mContactInfoHelper.isBusiness(mDetails.sourceType);
460        int contactType = ContactPhotoManager.TYPE_DEFAULT;
461        if (isVoicemailNumber) {
462            contactType = ContactPhotoManager.TYPE_VOICEMAIL;
463        } else if (isBusiness) {
464            contactType = ContactPhotoManager.TYPE_BUSINESS;
465        }
466
467        final String displayName = TextUtils.isEmpty(mDetails.namePrimary)
468                ? mDetails.displayNumber : mDetails.namePrimary.toString();
469        final String lookupKey = mDetails.contactUri == null
470                ? null : UriUtils.getLookupKeyFromUri(mDetails.contactUri);
471
472        final DefaultImageRequest request =
473                new DefaultImageRequest(displayName, lookupKey, contactType, true /* isCircular */);
474
475        mQuickContactBadge.assignContactUri(mDetails.contactUri);
476        mQuickContactBadge.setContentDescription(
477                mResources.getString(R.string.description_contact_details, displayName));
478
479        mContactPhotoManager.loadDirectoryPhoto(mQuickContactBadge, mDetails.photoUri,
480                false /* darkTheme */, true /* isCircular */, request);
481    }
482
483    private void updateBlockActionItem() {
484        if (mBlockedNumberId == null) {
485            mBlockNumberActionItem.setText(R.string.action_block_number);
486            mBlockNumberActionItem.setCompoundDrawablesRelativeWithIntrinsicBounds(
487                    R.drawable.ic_call_detail_block, 0, 0, 0);
488        } else {
489            mBlockNumberActionItem.setText(R.string.action_unblock_number);
490            mBlockNumberActionItem.setCompoundDrawablesRelativeWithIntrinsicBounds(
491                    R.drawable.ic_call_detail_unblock, 0, 0, 0);
492        }
493    }
494
495    private void closeSystemDialogs() {
496        sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
497    }
498
499    private String getDialableNumber() {
500        return mNumber + mPostDialDigits;
501    }
502
503    @NeededForTesting
504    public boolean hasVoicemail() {
505        return mVoicemailUri != null;
506    }
507}
508