| /* |
| * Copyright (C) 2015 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License |
| */ |
| |
| package com.android.contacts.dialog; |
| |
| import android.animation.Animator; |
| import android.animation.AnimatorListenerAdapter; |
| import android.app.Activity; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.SharedPreferences; |
| import android.net.Uri; |
| import android.os.Bundle; |
| import android.preference.PreferenceManager; |
| import android.telecom.PhoneAccount; |
| import android.telecom.PhoneAccountHandle; |
| import android.telecom.TelecomManager; |
| import android.text.Editable; |
| import android.text.InputFilter; |
| import android.text.TextUtils; |
| import android.text.TextWatcher; |
| import android.util.Log; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.view.ViewTreeObserver; |
| import android.view.inputmethod.InputMethodManager; |
| import android.widget.AdapterView; |
| import android.widget.ArrayAdapter; |
| import android.widget.EditText; |
| import android.widget.ListView; |
| import android.widget.QuickContactBadge; |
| import android.widget.TextView; |
| |
| import com.android.contacts.CallUtil; |
| import com.android.contacts.ContactPhotoManager; |
| import com.android.contacts.R; |
| import com.android.contacts.compat.CompatUtils; |
| import com.android.contacts.compat.PhoneAccountSdkCompat; |
| import com.android.contacts.compat.telecom.TelecomManagerCompat; |
| import com.android.contacts.util.UriUtils; |
| import com.android.phone.common.animation.AnimUtils; |
| |
| import java.nio.charset.Charset; |
| import java.util.ArrayList; |
| import java.util.List; |
| |
| /** |
| * Implements a dialog which prompts for a call subject for an outgoing call. The dialog includes |
| * a pop up list of historical call subjects. |
| */ |
| public class CallSubjectDialog extends Activity { |
| private static final String TAG = "CallSubjectDialog"; |
| private static final int CALL_SUBJECT_LIMIT = 16; |
| private static final int CALL_SUBJECT_HISTORY_SIZE = 5; |
| |
| private static final int REQUEST_SUBJECT = 1001; |
| |
| public static final String PREF_KEY_SUBJECT_HISTORY_COUNT = "subject_history_count"; |
| public static final String PREF_KEY_SUBJECT_HISTORY_ITEM = "subject_history_item"; |
| |
| /** |
| * Activity intent argument bundle keys: |
| */ |
| public static final String ARG_PHOTO_ID = "PHOTO_ID"; |
| public static final String ARG_PHOTO_URI = "PHOTO_URI"; |
| public static final String ARG_CONTACT_URI = "CONTACT_URI"; |
| public static final String ARG_NAME_OR_NUMBER = "NAME_OR_NUMBER"; |
| public static final String ARG_IS_BUSINESS = "IS_BUSINESS"; |
| public static final String ARG_NUMBER = "NUMBER"; |
| public static final String ARG_DISPLAY_NUMBER = "DISPLAY_NUMBER"; |
| public static final String ARG_NUMBER_LABEL = "NUMBER_LABEL"; |
| public static final String ARG_PHONE_ACCOUNT_HANDLE = "PHONE_ACCOUNT_HANDLE"; |
| |
| private int mAnimationDuration; |
| private Charset mMessageEncoding; |
| private View mBackgroundView; |
| private View mDialogView; |
| private QuickContactBadge mContactPhoto; |
| private TextView mNameView; |
| private TextView mNumberView; |
| private EditText mCallSubjectView; |
| private TextView mCharacterLimitView; |
| private View mHistoryButton; |
| private View mSendAndCallButton; |
| private ListView mSubjectList; |
| |
| private int mLimit = CALL_SUBJECT_LIMIT; |
| private int mPhotoSize; |
| private SharedPreferences mPrefs; |
| private List<String> mSubjectHistory; |
| |
| private long mPhotoID; |
| private Uri mPhotoUri; |
| private Uri mContactUri; |
| private String mNameOrNumber; |
| private boolean mIsBusiness; |
| private String mNumber; |
| private String mDisplayNumber; |
| private String mNumberLabel; |
| private PhoneAccountHandle mPhoneAccountHandle; |
| |
| /** |
| * Handles changes to the text in the subject box. Ensures the character limit is updated. |
| */ |
| private final TextWatcher mTextWatcher = new TextWatcher() { |
| @Override |
| public void beforeTextChanged(CharSequence s, int start, int count, int after) { |
| // no-op |
| } |
| |
| @Override |
| public void onTextChanged(CharSequence s, int start, int before, int count) { |
| updateCharacterLimit(); |
| } |
| |
| @Override |
| public void afterTextChanged(Editable s) { |
| // no-op |
| } |
| }; |
| |
| /** |
| * Click listener which handles user clicks outside of the dialog. |
| */ |
| private View.OnClickListener mBackgroundListener = new View.OnClickListener() { |
| @Override |
| public void onClick(View v) { |
| finish(); |
| } |
| }; |
| |
| /** |
| * Handles displaying the list of past call subjects. |
| */ |
| private final View.OnClickListener mHistoryOnClickListener = new View.OnClickListener() { |
| @Override |
| public void onClick(View v) { |
| hideSoftKeyboard(CallSubjectDialog.this, mCallSubjectView); |
| showCallHistory(mSubjectList.getVisibility() == View.GONE); |
| } |
| }; |
| |
| /** |
| * Handles starting a call with a call subject specified. |
| */ |
| private final View.OnClickListener mSendAndCallOnClickListener = new View.OnClickListener() { |
| @Override |
| public void onClick(View v) { |
| String subject = mCallSubjectView.getText().toString(); |
| Intent intent = CallUtil.getCallWithSubjectIntent(mNumber, mPhoneAccountHandle, |
| subject); |
| |
| TelecomManagerCompat.placeCall( |
| CallSubjectDialog.this, |
| (TelecomManager) getSystemService(Context.TELECOM_SERVICE), |
| intent); |
| |
| mSubjectHistory.add(subject); |
| saveSubjectHistory(mSubjectHistory); |
| finish(); |
| } |
| }; |
| |
| /** |
| * Handles auto-hiding the call history when user clicks in the call subject field to give it |
| * focus. |
| */ |
| private final View.OnClickListener mCallSubjectClickListener = new View.OnClickListener() { |
| @Override |
| public void onClick(View v) { |
| if (mSubjectList.getVisibility() == View.VISIBLE) { |
| showCallHistory(false); |
| } |
| } |
| }; |
| |
| /** |
| * Item click listener which handles user clicks on the items in the list view. Dismisses |
| * the activity, returning the subject to the caller and closing the activity with the |
| * {@link Activity#RESULT_OK} result code. |
| */ |
| private AdapterView.OnItemClickListener mItemClickListener = |
| new AdapterView.OnItemClickListener() { |
| @Override |
| public void onItemClick(AdapterView<?> arg0, View view, int position, long arg3) { |
| mCallSubjectView.setText(mSubjectHistory.get(position)); |
| showCallHistory(false); |
| } |
| }; |
| |
| /** |
| * Show the call subject dialog given a phone number to dial (e.g. from the dialpad). |
| * |
| * @param activity The activity. |
| * @param number The number to dial. |
| */ |
| public static void start(Activity activity, String number) { |
| start(activity, |
| -1 /* photoId */, |
| null /* photoUri */, |
| null /* contactUri */, |
| number /* nameOrNumber */, |
| false /* isBusiness */, |
| number /* number */, |
| null /* displayNumber */, |
| null /* numberLabel */, |
| null /* phoneAccountHandle */); |
| } |
| |
| /** |
| * Creates a call subject dialog. |
| * |
| * @param activity The current activity. |
| * @param photoId The photo ID (used to populate contact photo). |
| * @param photoUri The photo Uri (used to populate contact photo). |
| * @param contactUri The Contact URI (used so quick contact can be invoked from contact photo). |
| * @param nameOrNumber The name or number of the callee. |
| * @param isBusiness {@code true} if a business is being called (used for contact photo). |
| * @param number The raw number to dial. |
| * @param displayNumber The number to dial, formatted for display. |
| * @param numberLabel The label for the number (if from a contact). |
| * @param phoneAccountHandle The phone account handle. |
| */ |
| public static void start(Activity activity, long photoId, Uri photoUri, Uri contactUri, |
| String nameOrNumber, boolean isBusiness, String number, String displayNumber, |
| String numberLabel, PhoneAccountHandle phoneAccountHandle) { |
| Bundle arguments = new Bundle(); |
| arguments.putLong(ARG_PHOTO_ID, photoId); |
| arguments.putParcelable(ARG_PHOTO_URI, photoUri); |
| arguments.putParcelable(ARG_CONTACT_URI, contactUri); |
| arguments.putString(ARG_NAME_OR_NUMBER, nameOrNumber); |
| arguments.putBoolean(ARG_IS_BUSINESS, isBusiness); |
| arguments.putString(ARG_NUMBER, number); |
| arguments.putString(ARG_DISPLAY_NUMBER, displayNumber); |
| arguments.putString(ARG_NUMBER_LABEL, numberLabel); |
| arguments.putParcelable(ARG_PHONE_ACCOUNT_HANDLE, phoneAccountHandle); |
| start(activity, arguments); |
| } |
| |
| /** |
| * Shows the call subject dialog given a Bundle containing all the arguments required to |
| * display the dialog (e.g. from Quick Contacts). |
| * |
| * @param activity The activity. |
| * @param arguments The arguments bundle. |
| */ |
| public static void start(Activity activity, Bundle arguments) { |
| Intent intent = new Intent(activity, CallSubjectDialog.class); |
| intent.putExtras(arguments); |
| activity.startActivity(intent); |
| } |
| |
| /** |
| * Creates the dialog, inflating the layout and populating it with the name and phone number. |
| * |
| * @param savedInstanceState The last saved instance state of the Fragment, |
| * or null if this is a freshly created Fragment. |
| * |
| * @return Dialog instance. |
| */ |
| @Override |
| public void onCreate(Bundle savedInstanceState) { |
| super.onCreate(savedInstanceState); |
| getWindow().setHideOverlayWindows(true); |
| mAnimationDuration = getResources().getInteger(R.integer.call_subject_animation_duration); |
| mPrefs = PreferenceManager.getDefaultSharedPreferences(this); |
| mPhotoSize = getResources().getDimensionPixelSize( |
| R.dimen.call_subject_dialog_contact_photo_size); |
| readArguments(); |
| loadConfiguration(); |
| mSubjectHistory = loadSubjectHistory(mPrefs); |
| |
| setContentView(R.layout.dialog_call_subject); |
| getWindow().setLayout(ViewGroup.LayoutParams.MATCH_PARENT, |
| ViewGroup.LayoutParams.MATCH_PARENT); |
| mBackgroundView = findViewById(R.id.call_subject_dialog); |
| mBackgroundView.setOnClickListener(mBackgroundListener); |
| mDialogView = findViewById(R.id.dialog_view); |
| mContactPhoto = (QuickContactBadge) findViewById(R.id.contact_photo); |
| mNameView = (TextView) findViewById(R.id.name); |
| mNumberView = (TextView) findViewById(R.id.number); |
| mCallSubjectView = (EditText) findViewById(R.id.call_subject); |
| mCallSubjectView.addTextChangedListener(mTextWatcher); |
| mCallSubjectView.setOnClickListener(mCallSubjectClickListener); |
| InputFilter[] filters = new InputFilter[1]; |
| filters[0] = new InputFilter.LengthFilter(mLimit); |
| mCallSubjectView.setFilters(filters); |
| mCharacterLimitView = (TextView) findViewById(R.id.character_limit); |
| mHistoryButton = findViewById(R.id.history_button); |
| mHistoryButton.setOnClickListener(mHistoryOnClickListener); |
| mHistoryButton.setVisibility(mSubjectHistory.isEmpty() ? View.GONE : View.VISIBLE); |
| mSendAndCallButton = findViewById(R.id.send_and_call_button); |
| mSendAndCallButton.setOnClickListener(mSendAndCallOnClickListener); |
| mSubjectList = (ListView) findViewById(R.id.subject_list); |
| mSubjectList.setOnItemClickListener(mItemClickListener); |
| mSubjectList.setVisibility(View.GONE); |
| |
| updateContactInfo(); |
| updateCharacterLimit(); |
| } |
| |
| /** |
| * Populates the contact info fields based on the current contact information. |
| */ |
| private void updateContactInfo() { |
| if (mContactUri != null) { |
| setPhoto(mPhotoID, mPhotoUri, mContactUri, mNameOrNumber, mIsBusiness); |
| } else { |
| mContactPhoto.setVisibility(View.GONE); |
| } |
| mNameView.setText(mNameOrNumber); |
| if (!TextUtils.isEmpty(mNumberLabel) && !TextUtils.isEmpty(mDisplayNumber)) { |
| mNumberView.setVisibility(View.VISIBLE); |
| mNumberView.setText(getString(R.string.call_subject_type_and_number, |
| mNumberLabel, mDisplayNumber)); |
| } else { |
| mNumberView.setVisibility(View.GONE); |
| mNumberView.setText(null); |
| } |
| } |
| |
| /** |
| * Reads arguments from the fragment arguments and populates the necessary instance variables. |
| */ |
| private void readArguments() { |
| Bundle arguments = getIntent().getExtras(); |
| if (arguments == null) { |
| Log.e(TAG, "Arguments cannot be null."); |
| return; |
| } |
| mPhotoID = arguments.getLong(ARG_PHOTO_ID); |
| mPhotoUri = arguments.getParcelable(ARG_PHOTO_URI); |
| mContactUri = arguments.getParcelable(ARG_CONTACT_URI); |
| mNameOrNumber = arguments.getString(ARG_NAME_OR_NUMBER); |
| mIsBusiness = arguments.getBoolean(ARG_IS_BUSINESS); |
| mNumber = arguments.getString(ARG_NUMBER); |
| mDisplayNumber = arguments.getString(ARG_DISPLAY_NUMBER); |
| mNumberLabel = arguments.getString(ARG_NUMBER_LABEL); |
| mPhoneAccountHandle = arguments.getParcelable(ARG_PHONE_ACCOUNT_HANDLE); |
| } |
| |
| /** |
| * Updates the character limit display, coloring the text RED when the limit is reached or |
| * exceeded. |
| */ |
| private void updateCharacterLimit() { |
| String subjectText = mCallSubjectView.getText().toString(); |
| final int length; |
| |
| // If a message encoding is specified, use that to count bytes in the message. |
| if (mMessageEncoding != null) { |
| length = subjectText.getBytes(mMessageEncoding).length; |
| } else { |
| // No message encoding specified, so just count characters entered. |
| length = subjectText.length(); |
| } |
| |
| mCharacterLimitView.setText( |
| getString(R.string.call_subject_limit, length, mLimit)); |
| if (length >= mLimit) { |
| mCharacterLimitView.setTextColor(getResources().getColor( |
| R.color.call_subject_limit_exceeded)); |
| } else { |
| mCharacterLimitView.setTextColor(getResources().getColor( |
| R.color.dialtacts_secondary_text_color)); |
| } |
| } |
| |
| /** |
| * Sets the photo on the quick contact photo. |
| * |
| * @param photoId |
| * @param photoUri |
| * @param contactUri |
| * @param displayName |
| * @param isBusiness |
| */ |
| private void setPhoto(long photoId, Uri photoUri, Uri contactUri, String displayName, |
| boolean isBusiness) { |
| mContactPhoto.assignContactUri(contactUri); |
| if (CompatUtils.isLollipopCompatible()) { |
| mContactPhoto.setOverlay(null); |
| } |
| |
| int contactType; |
| if (isBusiness) { |
| contactType = ContactPhotoManager.TYPE_BUSINESS; |
| } else { |
| contactType = ContactPhotoManager.TYPE_DEFAULT; |
| } |
| |
| String lookupKey = null; |
| if (contactUri != null) { |
| lookupKey = UriUtils.getLookupKeyFromUri(contactUri); |
| } |
| |
| ContactPhotoManager.DefaultImageRequest |
| request = new ContactPhotoManager.DefaultImageRequest( |
| displayName, lookupKey, contactType, true /* isCircular */); |
| |
| if (photoId == 0 && photoUri != null) { |
| ContactPhotoManager.getInstance(this).loadPhoto(mContactPhoto, photoUri, |
| mPhotoSize, false /* darkTheme */, true /* isCircular */, request); |
| } else { |
| ContactPhotoManager.getInstance(this).loadThumbnail(mContactPhoto, photoId, |
| false /* darkTheme */, true /* isCircular */, request); |
| } |
| } |
| |
| /** |
| * Loads the subject history from shared preferences. |
| * |
| * @param prefs Shared preferences. |
| * @return List of subject history strings. |
| */ |
| public static List<String> loadSubjectHistory(SharedPreferences prefs) { |
| int historySize = prefs.getInt(PREF_KEY_SUBJECT_HISTORY_COUNT, 0); |
| List<String> subjects = new ArrayList(historySize); |
| |
| for (int ix = 0 ; ix < historySize; ix++) { |
| String historyItem = prefs.getString(PREF_KEY_SUBJECT_HISTORY_ITEM + ix, null); |
| if (!TextUtils.isEmpty(historyItem)) { |
| subjects.add(historyItem); |
| } |
| } |
| |
| return subjects; |
| } |
| |
| /** |
| * Saves the subject history list to shared prefs, removing older items so that there are only |
| * {@link #CALL_SUBJECT_HISTORY_SIZE} items at most. |
| * |
| * @param history The history. |
| */ |
| private void saveSubjectHistory(List<String> history) { |
| // Remove oldest subject(s). |
| while (history.size() > CALL_SUBJECT_HISTORY_SIZE) { |
| history.remove(0); |
| } |
| |
| SharedPreferences.Editor editor = mPrefs.edit(); |
| int historyCount = 0; |
| for (String subject : history) { |
| if (!TextUtils.isEmpty(subject)) { |
| editor.putString(PREF_KEY_SUBJECT_HISTORY_ITEM + historyCount, |
| subject); |
| historyCount++; |
| } |
| } |
| editor.putInt(PREF_KEY_SUBJECT_HISTORY_COUNT, historyCount); |
| editor.apply(); |
| } |
| |
| /** |
| * Hide software keyboard for the given {@link View}. |
| */ |
| public void hideSoftKeyboard(Context context, View view) { |
| InputMethodManager imm = (InputMethodManager) context.getSystemService( |
| Context.INPUT_METHOD_SERVICE); |
| if (imm != null) { |
| imm.hideSoftInputFromWindow(view.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS); |
| } |
| } |
| |
| /** |
| * Hides or shows the call history list. |
| * |
| * @param show {@code true} if the call history should be shown, {@code false} otherwise. |
| */ |
| private void showCallHistory(final boolean show) { |
| // Bail early if the visibility has not changed. |
| if ((show && mSubjectList.getVisibility() == View.VISIBLE) || |
| (!show && mSubjectList.getVisibility() == View.GONE)) { |
| return; |
| } |
| |
| final int dialogStartingBottom = mDialogView.getBottom(); |
| if (show) { |
| // Showing the subject list; bind the list of history items to the list and show it. |
| ArrayAdapter<String> adapter = new ArrayAdapter<String>(CallSubjectDialog.this, |
| R.layout.call_subject_history_list_item, mSubjectHistory); |
| mSubjectList.setAdapter(adapter); |
| mSubjectList.setVisibility(View.VISIBLE); |
| } else { |
| // Hiding the subject list. |
| mSubjectList.setVisibility(View.GONE); |
| } |
| |
| // Use a ViewTreeObserver so that we can animate between the pre-layout and post-layout |
| // states. |
| final ViewTreeObserver observer = mBackgroundView.getViewTreeObserver(); |
| observer.addOnPreDrawListener( |
| new ViewTreeObserver.OnPreDrawListener() { |
| @Override |
| public boolean onPreDraw() { |
| // We don't want to continue getting called. |
| if (observer.isAlive()) { |
| observer.removeOnPreDrawListener(this); |
| } |
| |
| // Determine the amount the dialog has shifted due to the relayout. |
| int shiftAmount = dialogStartingBottom - mDialogView.getBottom(); |
| |
| // If the dialog needs to be shifted, do that now. |
| if (shiftAmount != 0) { |
| // Start animation in translated state and animate to translationY 0. |
| mDialogView.setTranslationY(shiftAmount); |
| mDialogView.animate() |
| .translationY(0) |
| .setInterpolator(AnimUtils.EASE_OUT_EASE_IN) |
| .setDuration(mAnimationDuration) |
| .start(); |
| } |
| |
| if (show) { |
| // Show the subhect list. |
| mSubjectList.setTranslationY(mSubjectList.getHeight()); |
| |
| mSubjectList.animate() |
| .translationY(0) |
| .setInterpolator(AnimUtils.EASE_OUT_EASE_IN) |
| .setDuration(mAnimationDuration) |
| .setListener(new AnimatorListenerAdapter() { |
| @Override |
| public void onAnimationEnd(Animator animation) { |
| super.onAnimationEnd(animation); |
| } |
| |
| @Override |
| public void onAnimationStart(Animator animation) { |
| super.onAnimationStart(animation); |
| mSubjectList.setVisibility(View.VISIBLE); |
| } |
| }) |
| .start(); |
| } else { |
| // Hide the subject list. |
| mSubjectList.setTranslationY(0); |
| |
| mSubjectList.animate() |
| .translationY(mSubjectList.getHeight()) |
| .setInterpolator(AnimUtils.EASE_OUT_EASE_IN) |
| .setDuration(mAnimationDuration) |
| .setListener(new AnimatorListenerAdapter() { |
| @Override |
| public void onAnimationEnd(Animator animation) { |
| super.onAnimationEnd(animation); |
| mSubjectList.setVisibility(View.GONE); |
| } |
| |
| @Override |
| public void onAnimationStart(Animator animation) { |
| super.onAnimationStart(animation); |
| } |
| }) |
| .start(); |
| } |
| return true; |
| } |
| } |
| ); |
| } |
| |
| /** |
| * Loads the message encoding and maximum message length from the phone account extras for the |
| * current phone account. |
| */ |
| private void loadConfiguration() { |
| // Only attempt to load configuration from the phone account extras if the SDK is N or |
| // later. If we've got a prior SDK the default encoding and message length will suffice. |
| int sdk = android.os.Build.VERSION.SDK_INT; |
| if(sdk <= android.os.Build.VERSION_CODES.M) { |
| return; |
| } |
| |
| if (mPhoneAccountHandle == null) { |
| return; |
| } |
| |
| TelecomManager telecomManager = |
| (TelecomManager) getSystemService(Context.TELECOM_SERVICE); |
| final PhoneAccount account = telecomManager.getPhoneAccount(mPhoneAccountHandle); |
| |
| Bundle phoneAccountExtras = PhoneAccountSdkCompat.getExtras(account); |
| if (phoneAccountExtras == null) { |
| return; |
| } |
| |
| // Get limit, if provided; otherwise default to existing value. |
| mLimit = phoneAccountExtras |
| .getInt(PhoneAccountSdkCompat.EXTRA_CALL_SUBJECT_MAX_LENGTH, mLimit); |
| |
| // Get charset; default to none (e.g. count characters 1:1). |
| String charsetName = phoneAccountExtras.getString( |
| PhoneAccountSdkCompat.EXTRA_CALL_SUBJECT_CHARACTER_ENCODING); |
| |
| if (!TextUtils.isEmpty(charsetName)) { |
| try { |
| mMessageEncoding = Charset.forName(charsetName); |
| } catch (java.nio.charset.UnsupportedCharsetException uce) { |
| // Character set was invalid; log warning and fallback to none. |
| Log.w(TAG, "Invalid charset: " + charsetName); |
| mMessageEncoding = null; |
| } |
| } else { |
| // No character set specified, so count characters 1:1. |
| mMessageEncoding = null; |
| } |
| } |
| } |