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