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