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