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