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