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