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