Home | History | Annotate | Download | only in dialog
      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.lettertiles.LetterTileDrawable;
     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           getSystemService(TelecomManager.class).placeCall(intent.getData(), intent.getExtras());
    158           mSubjectHistory.add(subject);
    159           saveSubjectHistory(mSubjectHistory);
    160           finish();
    161         }
    162       };
    163   /** Click listener which handles user clicks outside of the dialog. */
    164   private View.OnClickListener mBackgroundListener =
    165       new View.OnClickListener() {
    166         @Override
    167         public void onClick(View v) {
    168           finish();
    169         }
    170       };
    171   /**
    172    * Item click listener which handles user clicks on the items in the list view. Dismisses the
    173    * activity, returning the subject to the caller and closing the activity with the {@link
    174    * Activity#RESULT_OK} result code.
    175    */
    176   private AdapterView.OnItemClickListener mItemClickListener =
    177       new AdapterView.OnItemClickListener() {
    178         @Override
    179         public void onItemClick(AdapterView<?> arg0, View view, int position, long arg3) {
    180           mCallSubjectView.setText(mSubjectHistory.get(position));
    181           showCallHistory(false);
    182         }
    183       };
    184 
    185   /**
    186    * Show the call subject dialog given a phone number to dial (e.g. from the dialpad).
    187    *
    188    * @param activity The activity.
    189    * @param number The number to dial.
    190    */
    191   public static void start(Activity activity, String number) {
    192     start(
    193         activity,
    194         -1 /* photoId */,
    195         null /* photoUri */,
    196         null /* contactUri */,
    197         number /* nameOrNumber */,
    198         number /* number */,
    199         null /* displayNumber */,
    200         null /* numberLabel */,
    201         LetterTileDrawable.TYPE_DEFAULT,
    202         null /* phoneAccountHandle */);
    203   }
    204 
    205   /**
    206    * Creates a call subject dialog.
    207    *
    208    * @param activity The current activity.
    209    * @param photoId The photo ID (used to populate contact photo).
    210    * @param contactUri The Contact URI (used so quick contact can be invoked from contact photo).
    211    * @param nameOrNumber The name or number of the callee.
    212    * @param number The raw number to dial.
    213    * @param displayNumber The number to dial, formatted for display.
    214    * @param numberLabel The label for the number (if from a contact).
    215    * @param contactType The contact type according to {@link ContactPhotoManager}.
    216    * @param phoneAccountHandle The phone account handle.
    217    */
    218   public static void start(
    219       Activity activity,
    220       long photoId,
    221       Uri photoUri,
    222       Uri contactUri,
    223       String nameOrNumber,
    224       String number,
    225       String displayNumber,
    226       String numberLabel,
    227       int contactType,
    228       PhoneAccountHandle phoneAccountHandle) {
    229     Bundle arguments = new Bundle();
    230     arguments.putLong(ARG_PHOTO_ID, photoId);
    231     arguments.putParcelable(ARG_PHOTO_URI, photoUri);
    232     arguments.putParcelable(ARG_CONTACT_URI, contactUri);
    233     arguments.putString(ARG_NAME_OR_NUMBER, nameOrNumber);
    234     arguments.putString(ARG_NUMBER, number);
    235     arguments.putString(ARG_DISPLAY_NUMBER, displayNumber);
    236     arguments.putString(ARG_NUMBER_LABEL, numberLabel);
    237     arguments.putInt(ARG_CONTACT_TYPE, contactType);
    238     arguments.putParcelable(ARG_PHONE_ACCOUNT_HANDLE, phoneAccountHandle);
    239     start(activity, arguments);
    240   }
    241 
    242   /**
    243    * Shows the call subject dialog given a Bundle containing all the arguments required to display
    244    * the dialog (e.g. from Quick Contacts).
    245    *
    246    * @param activity The activity.
    247    * @param arguments The arguments bundle.
    248    */
    249   public static void start(Activity activity, Bundle arguments) {
    250     Intent intent = new Intent(activity, CallSubjectDialog.class);
    251     intent.putExtras(arguments);
    252     activity.startActivity(intent);
    253   }
    254 
    255   /**
    256    * Loads the subject history from shared preferences.
    257    *
    258    * @param prefs Shared preferences.
    259    * @return List of subject history strings.
    260    */
    261   public static List<String> loadSubjectHistory(SharedPreferences prefs) {
    262     int historySize = prefs.getInt(PREF_KEY_SUBJECT_HISTORY_COUNT, 0);
    263     List<String> subjects = new ArrayList(historySize);
    264 
    265     for (int ix = 0; ix < historySize; ix++) {
    266       String historyItem = prefs.getString(PREF_KEY_SUBJECT_HISTORY_ITEM + ix, null);
    267       if (!TextUtils.isEmpty(historyItem)) {
    268         subjects.add(historyItem);
    269       }
    270     }
    271 
    272     return subjects;
    273   }
    274 
    275   /**
    276    * Creates the dialog, inflating the layout and populating it with the name and phone number.
    277    *
    278    * @param savedInstanceState The last saved instance state of the Fragment, or null if this is a
    279    *     freshly created Fragment.
    280    * @return Dialog instance.
    281    */
    282   @Override
    283   public void onCreate(Bundle savedInstanceState) {
    284     super.onCreate(savedInstanceState);
    285     mAnimationDuration = getResources().getInteger(R.integer.call_subject_animation_duration);
    286     mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
    287     readArguments();
    288     loadConfiguration();
    289     mSubjectHistory = loadSubjectHistory(mPrefs);
    290 
    291     setContentView(R.layout.dialog_call_subject);
    292     getWindow().setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
    293     mBackgroundView = findViewById(R.id.call_subject_dialog);
    294     mBackgroundView.setOnClickListener(mBackgroundListener);
    295     mDialogView = findViewById(R.id.dialog_view);
    296     mContactPhoto = (QuickContactBadge) findViewById(R.id.contact_photo);
    297     mNameView = (TextView) findViewById(R.id.name);
    298     mNumberView = (TextView) findViewById(R.id.number);
    299     mCallSubjectView = (EditText) findViewById(R.id.call_subject);
    300     mCallSubjectView.addTextChangedListener(mTextWatcher);
    301     mCallSubjectView.setOnClickListener(mCallSubjectClickListener);
    302     InputFilter[] filters = new InputFilter[1];
    303     filters[0] = new InputFilter.LengthFilter(mLimit);
    304     mCallSubjectView.setFilters(filters);
    305     mCharacterLimitView = (TextView) findViewById(R.id.character_limit);
    306     mHistoryButton = findViewById(R.id.history_button);
    307     mHistoryButton.setOnClickListener(mHistoryOnClickListener);
    308     mHistoryButton.setVisibility(mSubjectHistory.isEmpty() ? View.GONE : View.VISIBLE);
    309     mSendAndCallButton = findViewById(R.id.send_and_call_button);
    310     mSendAndCallButton.setOnClickListener(mSendAndCallOnClickListener);
    311     mSubjectList = (ListView) findViewById(R.id.subject_list);
    312     mSubjectList.setOnItemClickListener(mItemClickListener);
    313     mSubjectList.setVisibility(View.GONE);
    314 
    315     updateContactInfo();
    316     updateCharacterLimit();
    317   }
    318 
    319   /** Populates the contact info fields based on the current contact information. */
    320   private void updateContactInfo() {
    321     if (mContactUri != null) {
    322       ContactPhotoManager.getInstance(this)
    323           .loadDialerThumbnailOrPhoto(
    324               mContactPhoto, mContactUri, mPhotoID, mPhotoUri, mNameOrNumber, mContactType);
    325     } else {
    326       mContactPhoto.setVisibility(View.GONE);
    327     }
    328     mNameView.setText(mNameOrNumber);
    329     if (!TextUtils.isEmpty(mDisplayNumber)) {
    330       mNumberView.setVisibility(View.VISIBLE);
    331       mNumberView.setText(
    332           TextUtils.isEmpty(mNumberLabel)
    333               ? mDisplayNumber
    334               : getString(R.string.call_subject_type_and_number, mNumberLabel, mDisplayNumber));
    335     } else {
    336       mNumberView.setVisibility(View.GONE);
    337       mNumberView.setText(null);
    338     }
    339   }
    340 
    341   /** Reads arguments from the fragment arguments and populates the necessary instance variables. */
    342   private void readArguments() {
    343     Bundle arguments = getIntent().getExtras();
    344     if (arguments == null) {
    345       LogUtil.e("CallSubjectDialog.readArguments", "arguments cannot be null");
    346       return;
    347     }
    348     mPhotoID = arguments.getLong(ARG_PHOTO_ID);
    349     mPhotoUri = arguments.getParcelable(ARG_PHOTO_URI);
    350     mContactUri = arguments.getParcelable(ARG_CONTACT_URI);
    351     mNameOrNumber = arguments.getString(ARG_NAME_OR_NUMBER);
    352     mNumber = arguments.getString(ARG_NUMBER);
    353     mDisplayNumber = arguments.getString(ARG_DISPLAY_NUMBER);
    354     mNumberLabel = arguments.getString(ARG_NUMBER_LABEL);
    355     mContactType = arguments.getInt(ARG_CONTACT_TYPE, LetterTileDrawable.TYPE_DEFAULT);
    356     mPhoneAccountHandle = arguments.getParcelable(ARG_PHONE_ACCOUNT_HANDLE);
    357   }
    358 
    359   /**
    360    * Updates the character limit display, coloring the text RED when the limit is reached or
    361    * exceeded.
    362    */
    363   private void updateCharacterLimit() {
    364     String subjectText = mCallSubjectView.getText().toString();
    365     final int length;
    366 
    367     // If a message encoding is specified, use that to count bytes in the message.
    368     if (mMessageEncoding != null) {
    369       length = subjectText.getBytes(mMessageEncoding).length;
    370     } else {
    371       // No message encoding specified, so just count characters entered.
    372       length = subjectText.length();
    373     }
    374 
    375     mCharacterLimitView.setText(getString(R.string.call_subject_limit, length, mLimit));
    376     if (length >= mLimit) {
    377       mCharacterLimitView.setTextColor(
    378           getResources().getColor(R.color.call_subject_limit_exceeded));
    379     } else {
    380       mCharacterLimitView.setTextColor(
    381           getResources().getColor(R.color.dialer_secondary_text_color));
    382     }
    383   }
    384 
    385   /**
    386    * Saves the subject history list to shared prefs, removing older items so that there are only
    387    * {@link #CALL_SUBJECT_HISTORY_SIZE} items at most.
    388    *
    389    * @param history The history.
    390    */
    391   private void saveSubjectHistory(List<String> history) {
    392     // Remove oldest subject(s).
    393     while (history.size() > CALL_SUBJECT_HISTORY_SIZE) {
    394       history.remove(0);
    395     }
    396 
    397     SharedPreferences.Editor editor = mPrefs.edit();
    398     int historyCount = 0;
    399     for (String subject : history) {
    400       if (!TextUtils.isEmpty(subject)) {
    401         editor.putString(PREF_KEY_SUBJECT_HISTORY_ITEM + historyCount, subject);
    402         historyCount++;
    403       }
    404     }
    405     editor.putInt(PREF_KEY_SUBJECT_HISTORY_COUNT, historyCount);
    406     editor.apply();
    407   }
    408 
    409   /** Hide software keyboard for the given {@link View}. */
    410   public void hideSoftKeyboard(Context context, View view) {
    411     InputMethodManager imm =
    412         (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
    413     if (imm != null) {
    414       imm.hideSoftInputFromWindow(view.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS);
    415     }
    416   }
    417 
    418   /**
    419    * Hides or shows the call history list.
    420    *
    421    * @param show {@code true} if the call history should be shown, {@code false} otherwise.
    422    */
    423   private void showCallHistory(final boolean show) {
    424     // Bail early if the visibility has not changed.
    425     if ((show && mSubjectList.getVisibility() == View.VISIBLE)
    426         || (!show && mSubjectList.getVisibility() == View.GONE)) {
    427       return;
    428     }
    429 
    430     final int dialogStartingBottom = mDialogView.getBottom();
    431     if (show) {
    432       // Showing the subject list; bind the list of history items to the list and show it.
    433       ArrayAdapter<String> adapter =
    434           new ArrayAdapter<String>(
    435               CallSubjectDialog.this, R.layout.call_subject_history_list_item, mSubjectHistory);
    436       mSubjectList.setAdapter(adapter);
    437       mSubjectList.setVisibility(View.VISIBLE);
    438     } else {
    439       // Hiding the subject list.
    440       mSubjectList.setVisibility(View.GONE);
    441     }
    442 
    443     // Use a ViewTreeObserver so that we can animate between the pre-layout and post-layout
    444     // states.
    445     ViewUtil.doOnPreDraw(
    446         mBackgroundView,
    447         true,
    448         new Runnable() {
    449           @Override
    450           public void run() {
    451             // Determine the amount the dialog has shifted due to the relayout.
    452             int shiftAmount = dialogStartingBottom - mDialogView.getBottom();
    453 
    454             // If the dialog needs to be shifted, do that now.
    455             if (shiftAmount != 0) {
    456               // Start animation in translated state and animate to translationY 0.
    457               mDialogView.setTranslationY(shiftAmount);
    458               mDialogView
    459                   .animate()
    460                   .translationY(0)
    461                   .setInterpolator(AnimUtils.EASE_OUT_EASE_IN)
    462                   .setDuration(mAnimationDuration)
    463                   .start();
    464             }
    465 
    466             if (show) {
    467               // Show the subject list.
    468               mSubjectList.setTranslationY(mSubjectList.getHeight());
    469 
    470               mSubjectList
    471                   .animate()
    472                   .translationY(0)
    473                   .setInterpolator(AnimUtils.EASE_OUT_EASE_IN)
    474                   .setDuration(mAnimationDuration)
    475                   .setListener(
    476                       new AnimatorListenerAdapter() {
    477                         @Override
    478                         public void onAnimationEnd(Animator animation) {
    479                           super.onAnimationEnd(animation);
    480                         }
    481 
    482                         @Override
    483                         public void onAnimationStart(Animator animation) {
    484                           super.onAnimationStart(animation);
    485                           mSubjectList.setVisibility(View.VISIBLE);
    486                         }
    487                       })
    488                   .start();
    489             } else {
    490               // Hide the subject list.
    491               mSubjectList.setTranslationY(0);
    492 
    493               mSubjectList
    494                   .animate()
    495                   .translationY(mSubjectList.getHeight())
    496                   .setInterpolator(AnimUtils.EASE_OUT_EASE_IN)
    497                   .setDuration(mAnimationDuration)
    498                   .setListener(
    499                       new AnimatorListenerAdapter() {
    500                         @Override
    501                         public void onAnimationEnd(Animator animation) {
    502                           super.onAnimationEnd(animation);
    503                           mSubjectList.setVisibility(View.GONE);
    504                         }
    505 
    506                         @Override
    507                         public void onAnimationStart(Animator animation) {
    508                           super.onAnimationStart(animation);
    509                         }
    510                       })
    511                   .start();
    512             }
    513           }
    514         });
    515   }
    516 
    517   /**
    518    * Loads the message encoding and maximum message length from the phone account extras for the
    519    * current phone account.
    520    */
    521   private void loadConfiguration() {
    522     // Only attempt to load configuration from the phone account extras if the SDK is N or
    523     // later.  If we've got a prior SDK the default encoding and message length will suffice.
    524     if (VERSION.SDK_INT < VERSION_CODES.N) {
    525       return;
    526     }
    527 
    528     if (mPhoneAccountHandle == null) {
    529       return;
    530     }
    531 
    532     TelecomManager telecomManager = (TelecomManager) getSystemService(Context.TELECOM_SERVICE);
    533     final PhoneAccount account = telecomManager.getPhoneAccount(mPhoneAccountHandle);
    534 
    535     Bundle phoneAccountExtras = account.getExtras();
    536     if (phoneAccountExtras == null) {
    537       return;
    538     }
    539 
    540     // Get limit, if provided; otherwise default to existing value.
    541     mLimit = phoneAccountExtras.getInt(PhoneAccount.EXTRA_CALL_SUBJECT_MAX_LENGTH, mLimit);
    542 
    543     // Get charset; default to none (e.g. count characters 1:1).
    544     String charsetName =
    545         phoneAccountExtras.getString(PhoneAccount.EXTRA_CALL_SUBJECT_CHARACTER_ENCODING);
    546 
    547     if (!TextUtils.isEmpty(charsetName)) {
    548       try {
    549         mMessageEncoding = Charset.forName(charsetName);
    550       } catch (java.nio.charset.UnsupportedCharsetException uce) {
    551         // Character set was invalid; log warning and fallback to none.
    552         LogUtil.e("CallSubjectDialog.loadConfiguration", "invalid charset: " + charsetName);
    553         mMessageEncoding = null;
    554       }
    555     } else {
    556       // No character set specified, so count characters 1:1.
    557       mMessageEncoding = null;
    558     }
    559   }
    560 }
    561