1/*
2 * Copyright (C) 2015 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.contacts.dialog;
18
19import android.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.app.Activity;
22import android.content.Context;
23import android.content.Intent;
24import android.content.SharedPreferences;
25import android.net.Uri;
26import android.os.Bundle;
27import android.preference.PreferenceManager;
28import android.telecom.PhoneAccount;
29import android.telecom.PhoneAccountHandle;
30import android.telecom.TelecomManager;
31import android.text.Editable;
32import android.text.InputFilter;
33import android.text.TextUtils;
34import android.text.TextWatcher;
35import android.util.Log;
36import android.view.View;
37import android.view.ViewGroup;
38import android.view.ViewTreeObserver;
39import android.view.inputmethod.InputMethodManager;
40import android.widget.AdapterView;
41import android.widget.ArrayAdapter;
42import android.widget.EditText;
43import android.widget.ListView;
44import android.widget.QuickContactBadge;
45import android.widget.TextView;
46
47import com.android.contacts.CallUtil;
48import com.android.contacts.ContactPhotoManager;
49import com.android.contacts.R;
50import com.android.contacts.compat.CompatUtils;
51import com.android.contacts.compat.PhoneAccountSdkCompat;
52import com.android.contacts.compat.telecom.TelecomManagerCompat;
53import com.android.contacts.util.UriUtils;
54import com.android.phone.common.animation.AnimUtils;
55
56import java.nio.charset.Charset;
57import java.util.ArrayList;
58import java.util.List;
59
60/**
61 * Implements a dialog which prompts for a call subject for an outgoing call.  The dialog includes
62 * a pop up list of historical call subjects.
63 */
64public class CallSubjectDialog extends Activity {
65    private static final String TAG = "CallSubjectDialog";
66    private static final int CALL_SUBJECT_LIMIT = 16;
67    private static final int CALL_SUBJECT_HISTORY_SIZE = 5;
68
69    private static final int REQUEST_SUBJECT = 1001;
70
71    public static final String PREF_KEY_SUBJECT_HISTORY_COUNT = "subject_history_count";
72    public static final String PREF_KEY_SUBJECT_HISTORY_ITEM = "subject_history_item";
73
74    /**
75     * Activity intent argument bundle keys:
76     */
77    public static final String ARG_PHOTO_ID = "PHOTO_ID";
78    public static final String ARG_PHOTO_URI = "PHOTO_URI";
79    public static final String ARG_CONTACT_URI = "CONTACT_URI";
80    public static final String ARG_NAME_OR_NUMBER = "NAME_OR_NUMBER";
81    public static final String ARG_IS_BUSINESS = "IS_BUSINESS";
82    public static final String ARG_NUMBER = "NUMBER";
83    public static final String ARG_DISPLAY_NUMBER = "DISPLAY_NUMBER";
84    public static final String ARG_NUMBER_LABEL = "NUMBER_LABEL";
85    public static final String ARG_PHONE_ACCOUNT_HANDLE = "PHONE_ACCOUNT_HANDLE";
86
87    private int mAnimationDuration;
88    private Charset mMessageEncoding;
89    private View mBackgroundView;
90    private View mDialogView;
91    private QuickContactBadge mContactPhoto;
92    private TextView mNameView;
93    private TextView mNumberView;
94    private EditText mCallSubjectView;
95    private TextView mCharacterLimitView;
96    private View mHistoryButton;
97    private View mSendAndCallButton;
98    private ListView mSubjectList;
99
100    private int mLimit = CALL_SUBJECT_LIMIT;
101    private int mPhotoSize;
102    private SharedPreferences mPrefs;
103    private List<String> mSubjectHistory;
104
105    private long mPhotoID;
106    private Uri mPhotoUri;
107    private Uri mContactUri;
108    private String mNameOrNumber;
109    private boolean mIsBusiness;
110    private String mNumber;
111    private String mDisplayNumber;
112    private String mNumberLabel;
113    private PhoneAccountHandle mPhoneAccountHandle;
114
115    /**
116     * Handles changes to the text in the subject box.  Ensures the character limit is updated.
117     */
118    private final TextWatcher mTextWatcher = new TextWatcher() {
119        @Override
120        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
121            // no-op
122        }
123
124        @Override
125        public void onTextChanged(CharSequence s, int start, int before, int count) {
126            updateCharacterLimit();
127        }
128
129        @Override
130        public void afterTextChanged(Editable s) {
131            // no-op
132        }
133    };
134
135    /**
136     * Click listener which handles user clicks outside of the dialog.
137     */
138    private View.OnClickListener mBackgroundListener = new View.OnClickListener() {
139        @Override
140        public void onClick(View v) {
141            finish();
142        }
143    };
144
145    /**
146     * Handles displaying the list of past call subjects.
147     */
148    private final View.OnClickListener mHistoryOnClickListener = new View.OnClickListener() {
149        @Override
150        public void onClick(View v) {
151            hideSoftKeyboard(CallSubjectDialog.this, mCallSubjectView);
152            showCallHistory(mSubjectList.getVisibility() == View.GONE);
153        }
154    };
155
156    /**
157     * Handles starting a call with a call subject specified.
158     */
159    private final View.OnClickListener mSendAndCallOnClickListener = new View.OnClickListener() {
160        @Override
161        public void onClick(View v) {
162            String subject = mCallSubjectView.getText().toString();
163            Intent intent = CallUtil.getCallWithSubjectIntent(mNumber, mPhoneAccountHandle,
164                    subject);
165
166            TelecomManagerCompat.placeCall(
167                    CallSubjectDialog.this,
168                    (TelecomManager) getSystemService(Context.TELECOM_SERVICE),
169                    intent);
170
171            mSubjectHistory.add(subject);
172            saveSubjectHistory(mSubjectHistory);
173            finish();
174        }
175    };
176
177    /**
178     * Handles auto-hiding the call history when user clicks in the call subject field to give it
179     * focus.
180     */
181    private final View.OnClickListener mCallSubjectClickListener = new View.OnClickListener() {
182        @Override
183        public void onClick(View v) {
184            if (mSubjectList.getVisibility() == View.VISIBLE) {
185                showCallHistory(false);
186            }
187        }
188    };
189
190    /**
191     * Item click listener which handles user clicks on the items in the list view.  Dismisses
192     * the activity, returning the subject to the caller and closing the activity with the
193     * {@link Activity#RESULT_OK} result code.
194     */
195    private AdapterView.OnItemClickListener mItemClickListener =
196            new AdapterView.OnItemClickListener() {
197                @Override
198                public void onItemClick(AdapterView<?> arg0, View view, int position, long arg3) {
199                    mCallSubjectView.setText(mSubjectHistory.get(position));
200                    showCallHistory(false);
201                }
202            };
203
204    /**
205     * Show the call subject dialog given a phone number to dial (e.g. from the dialpad).
206     *
207     * @param activity The activity.
208     * @param number The number to dial.
209     */
210    public static void start(Activity activity, String number) {
211        start(activity,
212                -1 /* photoId */,
213                null /* photoUri */,
214                null /* contactUri */,
215                number /* nameOrNumber */,
216                false /* isBusiness */,
217                number /* number */,
218                null /* displayNumber */,
219                null /* numberLabel */,
220                null /* phoneAccountHandle */);
221    }
222
223    /**
224     * Creates a call subject dialog.
225     *
226     * @param activity The current activity.
227     * @param photoId The photo ID (used to populate contact photo).
228     * @param photoUri The photo Uri (used to populate contact photo).
229     * @param contactUri The Contact URI (used so quick contact can be invoked from contact photo).
230     * @param nameOrNumber The name or number of the callee.
231     * @param isBusiness {@code true} if a business is being called (used for contact photo).
232     * @param number The raw number to dial.
233     * @param displayNumber The number to dial, formatted for display.
234     * @param numberLabel The label for the number (if from a contact).
235     * @param phoneAccountHandle The phone account handle.
236     */
237    public static void start(Activity activity, long photoId, Uri photoUri, Uri contactUri,
238            String nameOrNumber, boolean isBusiness, String number, String displayNumber,
239            String numberLabel, PhoneAccountHandle phoneAccountHandle) {
240        Bundle arguments = new Bundle();
241        arguments.putLong(ARG_PHOTO_ID, photoId);
242        arguments.putParcelable(ARG_PHOTO_URI, photoUri);
243        arguments.putParcelable(ARG_CONTACT_URI, contactUri);
244        arguments.putString(ARG_NAME_OR_NUMBER, nameOrNumber);
245        arguments.putBoolean(ARG_IS_BUSINESS, isBusiness);
246        arguments.putString(ARG_NUMBER, number);
247        arguments.putString(ARG_DISPLAY_NUMBER, displayNumber);
248        arguments.putString(ARG_NUMBER_LABEL, numberLabel);
249        arguments.putParcelable(ARG_PHONE_ACCOUNT_HANDLE, phoneAccountHandle);
250        start(activity, arguments);
251    }
252
253    /**
254     * Shows the call subject dialog given a Bundle containing all the arguments required to
255     * display the dialog (e.g. from Quick Contacts).
256     *
257     * @param activity The activity.
258     * @param arguments The arguments bundle.
259     */
260    public static void start(Activity activity, Bundle arguments) {
261        Intent intent = new Intent(activity, CallSubjectDialog.class);
262        intent.putExtras(arguments);
263        activity.startActivity(intent);
264    }
265
266    /**
267     * Creates the dialog, inflating the layout and populating it with the name and phone number.
268     *
269     * @param savedInstanceState The last saved instance state of the Fragment,
270     * or null if this is a freshly created Fragment.
271     *
272     * @return Dialog instance.
273     */
274    @Override
275    public void onCreate(Bundle savedInstanceState) {
276        super.onCreate(savedInstanceState);
277        mAnimationDuration = getResources().getInteger(R.integer.call_subject_animation_duration);
278        mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
279        mPhotoSize = getResources().getDimensionPixelSize(
280                R.dimen.call_subject_dialog_contact_photo_size);
281        readArguments();
282        loadConfiguration();
283        mSubjectHistory = loadSubjectHistory(mPrefs);
284
285        setContentView(R.layout.dialog_call_subject);
286        getWindow().setLayout(ViewGroup.LayoutParams.MATCH_PARENT,
287                ViewGroup.LayoutParams.MATCH_PARENT);
288        mBackgroundView = findViewById(R.id.call_subject_dialog);
289        mBackgroundView.setOnClickListener(mBackgroundListener);
290        mDialogView = findViewById(R.id.dialog_view);
291        mContactPhoto = (QuickContactBadge) findViewById(R.id.contact_photo);
292        mNameView = (TextView) findViewById(R.id.name);
293        mNumberView = (TextView) findViewById(R.id.number);
294        mCallSubjectView = (EditText) findViewById(R.id.call_subject);
295        mCallSubjectView.addTextChangedListener(mTextWatcher);
296        mCallSubjectView.setOnClickListener(mCallSubjectClickListener);
297        InputFilter[] filters = new InputFilter[1];
298        filters[0] = new InputFilter.LengthFilter(mLimit);
299        mCallSubjectView.setFilters(filters);
300        mCharacterLimitView = (TextView) findViewById(R.id.character_limit);
301        mHistoryButton = findViewById(R.id.history_button);
302        mHistoryButton.setOnClickListener(mHistoryOnClickListener);
303        mHistoryButton.setVisibility(mSubjectHistory.isEmpty() ? View.GONE : View.VISIBLE);
304        mSendAndCallButton = findViewById(R.id.send_and_call_button);
305        mSendAndCallButton.setOnClickListener(mSendAndCallOnClickListener);
306        mSubjectList = (ListView) findViewById(R.id.subject_list);
307        mSubjectList.setOnItemClickListener(mItemClickListener);
308        mSubjectList.setVisibility(View.GONE);
309
310        updateContactInfo();
311        updateCharacterLimit();
312    }
313
314    /**
315     * Populates the contact info fields based on the current contact information.
316     */
317    private void updateContactInfo() {
318        if (mContactUri != null) {
319            setPhoto(mPhotoID, mPhotoUri, mContactUri, mNameOrNumber, mIsBusiness);
320        } else {
321            mContactPhoto.setVisibility(View.GONE);
322        }
323        mNameView.setText(mNameOrNumber);
324        if (!TextUtils.isEmpty(mNumberLabel) && !TextUtils.isEmpty(mDisplayNumber)) {
325            mNumberView.setVisibility(View.VISIBLE);
326            mNumberView.setText(getString(R.string.call_subject_type_and_number,
327                    mNumberLabel, mDisplayNumber));
328        } else {
329            mNumberView.setVisibility(View.GONE);
330            mNumberView.setText(null);
331        }
332    }
333
334    /**
335     * Reads arguments from the fragment arguments and populates the necessary instance variables.
336     */
337    private void readArguments() {
338        Bundle arguments = getIntent().getExtras();
339        if (arguments == null) {
340            Log.e(TAG, "Arguments cannot be null.");
341            return;
342        }
343        mPhotoID = arguments.getLong(ARG_PHOTO_ID);
344        mPhotoUri = arguments.getParcelable(ARG_PHOTO_URI);
345        mContactUri = arguments.getParcelable(ARG_CONTACT_URI);
346        mNameOrNumber = arguments.getString(ARG_NAME_OR_NUMBER);
347        mIsBusiness = arguments.getBoolean(ARG_IS_BUSINESS);
348        mNumber = arguments.getString(ARG_NUMBER);
349        mDisplayNumber = arguments.getString(ARG_DISPLAY_NUMBER);
350        mNumberLabel = arguments.getString(ARG_NUMBER_LABEL);
351        mPhoneAccountHandle = arguments.getParcelable(ARG_PHONE_ACCOUNT_HANDLE);
352    }
353
354    /**
355     * Updates the character limit display, coloring the text RED when the limit is reached or
356     * exceeded.
357     */
358    private void updateCharacterLimit() {
359        String subjectText = mCallSubjectView.getText().toString();
360        final int length;
361
362        // If a message encoding is specified, use that to count bytes in the message.
363        if (mMessageEncoding != null) {
364            length = subjectText.getBytes(mMessageEncoding).length;
365        } else {
366            // No message encoding specified, so just count characters entered.
367            length = subjectText.length();
368        }
369
370        mCharacterLimitView.setText(
371                getString(R.string.call_subject_limit, length, mLimit));
372        if (length >= mLimit) {
373            mCharacterLimitView.setTextColor(getResources().getColor(
374                    R.color.call_subject_limit_exceeded));
375        } else {
376            mCharacterLimitView.setTextColor(getResources().getColor(
377                    R.color.dialtacts_secondary_text_color));
378        }
379    }
380
381    /**
382     * Sets the photo on the quick contact photo.
383     *
384     * @param photoId
385     * @param photoUri
386     * @param contactUri
387     * @param displayName
388     * @param isBusiness
389     */
390    private void setPhoto(long photoId, Uri photoUri, Uri contactUri, String displayName,
391            boolean isBusiness) {
392        mContactPhoto.assignContactUri(contactUri);
393        if (CompatUtils.isLollipopCompatible()) {
394            mContactPhoto.setOverlay(null);
395        }
396
397        int contactType;
398        if (isBusiness) {
399            contactType = ContactPhotoManager.TYPE_BUSINESS;
400        } else {
401            contactType = ContactPhotoManager.TYPE_DEFAULT;
402        }
403
404        String lookupKey = null;
405        if (contactUri != null) {
406            lookupKey = UriUtils.getLookupKeyFromUri(contactUri);
407        }
408
409        ContactPhotoManager.DefaultImageRequest
410                request = new ContactPhotoManager.DefaultImageRequest(
411                displayName, lookupKey, contactType, true /* isCircular */);
412
413        if (photoId == 0 && photoUri != null) {
414            ContactPhotoManager.getInstance(this).loadPhoto(mContactPhoto, photoUri,
415                    mPhotoSize, false /* darkTheme */, true /* isCircular */, request);
416        } else {
417            ContactPhotoManager.getInstance(this).loadThumbnail(mContactPhoto, photoId,
418                    false /* darkTheme */, true /* isCircular */, request);
419        }
420    }
421
422    /**
423     * Loads the subject history from shared preferences.
424     *
425     * @param prefs Shared preferences.
426     * @return List of subject history strings.
427     */
428    public static List<String> loadSubjectHistory(SharedPreferences prefs) {
429        int historySize = prefs.getInt(PREF_KEY_SUBJECT_HISTORY_COUNT, 0);
430        List<String> subjects = new ArrayList(historySize);
431
432        for (int ix = 0 ; ix < historySize; ix++) {
433            String historyItem = prefs.getString(PREF_KEY_SUBJECT_HISTORY_ITEM + ix, null);
434            if (!TextUtils.isEmpty(historyItem)) {
435                subjects.add(historyItem);
436            }
437        }
438
439        return subjects;
440    }
441
442    /**
443     * Saves the subject history list to shared prefs, removing older items so that there are only
444     * {@link #CALL_SUBJECT_HISTORY_SIZE} items at most.
445     *
446     * @param history The history.
447     */
448    private void saveSubjectHistory(List<String> history) {
449        // Remove oldest subject(s).
450        while (history.size() > CALL_SUBJECT_HISTORY_SIZE) {
451            history.remove(0);
452        }
453
454        SharedPreferences.Editor editor = mPrefs.edit();
455        int historyCount = 0;
456        for (String subject : history) {
457            if (!TextUtils.isEmpty(subject)) {
458                editor.putString(PREF_KEY_SUBJECT_HISTORY_ITEM + historyCount,
459                        subject);
460                historyCount++;
461            }
462        }
463        editor.putInt(PREF_KEY_SUBJECT_HISTORY_COUNT, historyCount);
464        editor.apply();
465    }
466
467    /**
468     * Hide software keyboard for the given {@link View}.
469     */
470    public void hideSoftKeyboard(Context context, View view) {
471        InputMethodManager imm = (InputMethodManager) context.getSystemService(
472                Context.INPUT_METHOD_SERVICE);
473        if (imm != null) {
474            imm.hideSoftInputFromWindow(view.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS);
475        }
476    }
477
478    /**
479     * Hides or shows the call history list.
480     *
481     * @param show {@code true} if the call history should be shown, {@code false} otherwise.
482     */
483    private void showCallHistory(final boolean show) {
484        // Bail early if the visibility has not changed.
485        if ((show && mSubjectList.getVisibility() == View.VISIBLE) ||
486                (!show && mSubjectList.getVisibility() == View.GONE)) {
487            return;
488        }
489
490        final int dialogStartingBottom = mDialogView.getBottom();
491        if (show) {
492            // Showing the subject list; bind the list of history items to the list and show it.
493            ArrayAdapter<String> adapter = new ArrayAdapter<String>(CallSubjectDialog.this,
494                    R.layout.call_subject_history_list_item, mSubjectHistory);
495            mSubjectList.setAdapter(adapter);
496            mSubjectList.setVisibility(View.VISIBLE);
497        } else {
498            // Hiding the subject list.
499            mSubjectList.setVisibility(View.GONE);
500        }
501
502        // Use a ViewTreeObserver so that we can animate between the pre-layout and post-layout
503        // states.
504        final ViewTreeObserver observer = mBackgroundView.getViewTreeObserver();
505        observer.addOnPreDrawListener(
506                new ViewTreeObserver.OnPreDrawListener() {
507                    @Override
508                    public boolean onPreDraw() {
509                        // We don't want to continue getting called.
510                        if (observer.isAlive()) {
511                            observer.removeOnPreDrawListener(this);
512                        }
513
514                        // Determine the amount the dialog has shifted due to the relayout.
515                        int shiftAmount = dialogStartingBottom - mDialogView.getBottom();
516
517                        // If the dialog needs to be shifted, do that now.
518                        if (shiftAmount != 0) {
519                            // Start animation in translated state and animate to translationY 0.
520                            mDialogView.setTranslationY(shiftAmount);
521                            mDialogView.animate()
522                                    .translationY(0)
523                                    .setInterpolator(AnimUtils.EASE_OUT_EASE_IN)
524                                    .setDuration(mAnimationDuration)
525                                    .start();
526                        }
527
528                        if (show) {
529                            // Show the subhect list.
530                            mSubjectList.setTranslationY(mSubjectList.getHeight());
531
532                            mSubjectList.animate()
533                                    .translationY(0)
534                                    .setInterpolator(AnimUtils.EASE_OUT_EASE_IN)
535                                    .setDuration(mAnimationDuration)
536                                    .setListener(new AnimatorListenerAdapter() {
537                                        @Override
538                                        public void onAnimationEnd(Animator animation) {
539                                            super.onAnimationEnd(animation);
540                                        }
541
542                                        @Override
543                                        public void onAnimationStart(Animator animation) {
544                                            super.onAnimationStart(animation);
545                                            mSubjectList.setVisibility(View.VISIBLE);
546                                        }
547                                    })
548                                    .start();
549                        } else {
550                            // Hide the subject list.
551                            mSubjectList.setTranslationY(0);
552
553                            mSubjectList.animate()
554                                    .translationY(mSubjectList.getHeight())
555                                    .setInterpolator(AnimUtils.EASE_OUT_EASE_IN)
556                                    .setDuration(mAnimationDuration)
557                                    .setListener(new AnimatorListenerAdapter() {
558                                        @Override
559                                        public void onAnimationEnd(Animator animation) {
560                                            super.onAnimationEnd(animation);
561                                            mSubjectList.setVisibility(View.GONE);
562                                        }
563
564                                        @Override
565                                        public void onAnimationStart(Animator animation) {
566                                            super.onAnimationStart(animation);
567                                        }
568                                    })
569                                    .start();
570                        }
571                        return true;
572                    }
573                }
574        );
575    }
576
577    /**
578     * Loads the message encoding and maximum message length from the phone account extras for the
579     * current phone account.
580     */
581    private void loadConfiguration() {
582        // Only attempt to load configuration from the phone account extras if the SDK is N or
583        // later.  If we've got a prior SDK the default encoding and message length will suffice.
584        int sdk = android.os.Build.VERSION.SDK_INT;
585        if(sdk <= android.os.Build.VERSION_CODES.M) {
586            return;
587        }
588
589        if (mPhoneAccountHandle == null) {
590            return;
591        }
592
593        TelecomManager telecomManager =
594                (TelecomManager) getSystemService(Context.TELECOM_SERVICE);
595        final PhoneAccount account = telecomManager.getPhoneAccount(mPhoneAccountHandle);
596
597        Bundle phoneAccountExtras = PhoneAccountSdkCompat.getExtras(account);
598        if (phoneAccountExtras == null) {
599            return;
600        }
601
602        // Get limit, if provided; otherwise default to existing value.
603        mLimit = phoneAccountExtras
604                .getInt(PhoneAccountSdkCompat.EXTRA_CALL_SUBJECT_MAX_LENGTH, mLimit);
605
606        // Get charset; default to none (e.g. count characters 1:1).
607        String charsetName = phoneAccountExtras.getString(
608                PhoneAccountSdkCompat.EXTRA_CALL_SUBJECT_CHARACTER_ENCODING);
609
610        if (!TextUtils.isEmpty(charsetName)) {
611            try {
612                mMessageEncoding = Charset.forName(charsetName);
613            } catch (java.nio.charset.UnsupportedCharsetException uce) {
614                // Character set was invalid; log warning and fallback to none.
615                Log.w(TAG, "Invalid charset: " + charsetName);
616                mMessageEncoding = null;
617            }
618        } else {
619            // No character set specified, so count characters 1:1.
620            mMessageEncoding = null;
621        }
622    }
623}
624