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