| /* |
| * Copyright (C) 2016 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, softwareateCre |
| * 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.group; |
| |
| import android.app.Dialog; |
| import android.app.DialogFragment; |
| import android.app.LoaderManager; |
| import android.content.Context; |
| import android.content.CursorLoader; |
| import android.content.DialogInterface; |
| import android.content.DialogInterface.OnClickListener; |
| import android.content.Intent; |
| import android.content.Loader; |
| import android.database.Cursor; |
| import android.os.Bundle; |
| import android.provider.ContactsContract.Groups; |
| import android.text.Editable; |
| import android.text.TextUtils; |
| import android.text.TextWatcher; |
| import android.view.View; |
| import android.view.WindowManager; |
| import android.view.inputmethod.InputMethodManager; |
| import android.widget.Button; |
| import android.widget.EditText; |
| import android.widget.TextView; |
| |
| import androidx.appcompat.app.AlertDialog; |
| |
| import com.android.contacts.ContactSaveService; |
| import com.android.contacts.R; |
| import com.android.contacts.model.account.AccountWithDataSet; |
| |
| import com.google.android.material.textfield.TextInputLayout; |
| import com.google.common.base.Strings; |
| |
| import java.util.Collections; |
| import java.util.HashSet; |
| import java.util.Set; |
| |
| /** |
| * Edits the name of a group. |
| */ |
| public final class GroupNameEditDialogFragment extends DialogFragment implements |
| LoaderManager.LoaderCallbacks<Cursor> { |
| |
| private static final String KEY_GROUP_NAME = "groupName"; |
| |
| private static final String ARG_IS_INSERT = "isInsert"; |
| private static final String ARG_GROUP_NAME = "groupName"; |
| private static final String ARG_ACCOUNT = "account"; |
| private static final String ARG_CALLBACK_ACTION = "callbackAction"; |
| private static final String ARG_GROUP_ID = "groupId"; |
| |
| private static final long NO_GROUP_ID = -1; |
| |
| |
| /** Callbacks for hosts of the {@link GroupNameEditDialogFragment}. */ |
| public interface Listener { |
| void onGroupNameEditCancelled(); |
| |
| void onGroupNameEditCompleted(String name); |
| |
| public static final Listener None = new Listener() { |
| @Override |
| public void onGroupNameEditCancelled() { |
| } |
| |
| @Override |
| public void onGroupNameEditCompleted(String name) { |
| } |
| }; |
| } |
| |
| private boolean mIsInsert; |
| private String mGroupName; |
| private long mGroupId; |
| private Listener mListener; |
| private AccountWithDataSet mAccount; |
| private EditText mGroupNameEditText; |
| private TextInputLayout mGroupNameTextLayout; |
| private Set<String> mExistingGroups = Collections.emptySet(); |
| |
| public static GroupNameEditDialogFragment newInstanceForCreation( |
| AccountWithDataSet account, String callbackAction) { |
| return newInstance(account, callbackAction, NO_GROUP_ID, null); |
| } |
| |
| public static GroupNameEditDialogFragment newInstanceForUpdate( |
| AccountWithDataSet account, String callbackAction, long groupId, String groupName) { |
| return newInstance(account, callbackAction, groupId, groupName); |
| } |
| |
| private static GroupNameEditDialogFragment newInstance( |
| AccountWithDataSet account, String callbackAction, long groupId, String groupName) { |
| if (account == null) { |
| throw new IllegalArgumentException("Invalid account"); |
| } |
| final boolean isInsert = groupId == NO_GROUP_ID; |
| final Bundle args = new Bundle(); |
| args.putBoolean(ARG_IS_INSERT, isInsert); |
| args.putLong(ARG_GROUP_ID, groupId); |
| args.putString(ARG_GROUP_NAME, groupName); |
| args.putParcelable(ARG_ACCOUNT, account); |
| args.putString(ARG_CALLBACK_ACTION, callbackAction); |
| |
| final GroupNameEditDialogFragment dialog = new GroupNameEditDialogFragment(); |
| dialog.setArguments(args); |
| return dialog; |
| } |
| |
| @Override |
| public void onCreate(Bundle savedInstanceState) { |
| super.onCreate(savedInstanceState); |
| setStyle(STYLE_NORMAL, R.style.ContactsAlertDialogThemeAppCompat); |
| final Bundle args = getArguments(); |
| if (savedInstanceState == null) { |
| mGroupName = args.getString(KEY_GROUP_NAME); |
| } else { |
| mGroupName = savedInstanceState.getString(ARG_GROUP_NAME); |
| } |
| |
| mGroupId = args.getLong(ARG_GROUP_ID, NO_GROUP_ID); |
| mIsInsert = args.getBoolean(ARG_IS_INSERT, true); |
| mAccount = getArguments().getParcelable(ARG_ACCOUNT); |
| |
| // There is only one loader so the id arg doesn't matter. |
| getLoaderManager().initLoader(0, null, this); |
| } |
| |
| @Override |
| public Dialog onCreateDialog(Bundle savedInstanceState) { |
| // Build a dialog with two buttons and a view of a single EditText input field |
| final TextView title = (TextView) View.inflate(getActivity(), R.layout.dialog_title, null); |
| title.setText(mIsInsert |
| ? R.string.group_name_dialog_insert_title |
| : R.string.group_name_dialog_update_title); |
| final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity(), getTheme()) |
| .setCustomTitle(title) |
| .setView(R.layout.group_name_edit_dialog) |
| .setNegativeButton(android.R.string.cancel, new OnClickListener() { |
| @Override |
| public void onClick(DialogInterface dialog, int which) { |
| hideInputMethod(); |
| getListener().onGroupNameEditCancelled(); |
| dismiss(); |
| } |
| }) |
| // The Positive button listener is defined below in the OnShowListener to |
| // allow for input validation |
| .setPositiveButton(android.R.string.ok, null); |
| |
| // Disable the create button when the name is empty |
| final AlertDialog alertDialog = builder.create(); |
| alertDialog.getWindow().setSoftInputMode( |
| WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE); |
| alertDialog.setOnShowListener(new DialogInterface.OnShowListener() { |
| @Override |
| public void onShow(DialogInterface dialog) { |
| mGroupNameEditText = (EditText) alertDialog.findViewById(android.R.id.text1); |
| mGroupNameTextLayout = |
| (TextInputLayout) alertDialog.findViewById(R.id.text_input_layout); |
| if (!TextUtils.isEmpty(mGroupName)) { |
| mGroupNameEditText.setText(mGroupName); |
| // Guard against already created group names that are longer than the max |
| final int maxLength = getResources().getInteger( |
| R.integer.group_name_max_length); |
| mGroupNameEditText.setSelection( |
| mGroupName.length() > maxLength ? maxLength : mGroupName.length()); |
| } |
| showInputMethod(mGroupNameEditText); |
| |
| final Button createButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE); |
| createButton.setEnabled(!TextUtils.isEmpty(getGroupName())); |
| |
| // Override the click listener to prevent dismissal if creating a duplicate group. |
| createButton.setOnClickListener(new View.OnClickListener() { |
| @Override |
| public void onClick(View v) { |
| maybePersistCurrentGroupName(v); |
| } |
| }); |
| mGroupNameEditText.addTextChangedListener(new TextWatcher() { |
| @Override |
| public void beforeTextChanged(CharSequence s, int start, int count, int after) { |
| } |
| |
| @Override |
| public void onTextChanged(CharSequence s, int start, int before, int count) { |
| } |
| |
| @Override |
| public void afterTextChanged(Editable s) { |
| mGroupNameTextLayout.setError(null); |
| createButton.setEnabled(!TextUtils.isEmpty(s)); |
| } |
| }); |
| } |
| }); |
| |
| return alertDialog; |
| } |
| |
| /** |
| * Sets the listener for the rename |
| * |
| * Setting a listener on a fragment is error prone since it will be lost if the fragment |
| * is recreated. This exists because it is used from a view class (GroupMembersView) which |
| * needs to modify it's state when this fragment updates the name. |
| * |
| * @param listener the listener. can be null |
| */ |
| public void setListener(Listener listener) { |
| mListener = listener; |
| } |
| |
| private boolean hasNameChanged() { |
| final String name = Strings.nullToEmpty(getGroupName()); |
| final String originalName = getArguments().getString(ARG_GROUP_NAME); |
| return (mIsInsert && !name.isEmpty()) || !name.equals(originalName); |
| } |
| |
| private void maybePersistCurrentGroupName(View button) { |
| if (!hasNameChanged()) { |
| dismiss(); |
| return; |
| } |
| String name = getGroupName(); |
| // Trim group name, when group is saved. |
| // When "Group" exists, do not save " Group ". This behavior is the same as Google Contacts. |
| if (!TextUtils.isEmpty(name)) { |
| name = name.trim(); |
| } |
| // Note we don't check if the loader finished populating mExistingGroups. It's not the |
| // end of the world if the user ends up with a duplicate group and in practice it should |
| // never really happen (the query should complete much sooner than the user can edit the |
| // label) |
| if (mExistingGroups.contains(name)) { |
| mGroupNameTextLayout.setError( |
| getString(R.string.groupExistsErrorMessage)); |
| button.setEnabled(false); |
| return; |
| } |
| final String callbackAction = getArguments().getString(ARG_CALLBACK_ACTION); |
| final Intent serviceIntent; |
| if (mIsInsert) { |
| serviceIntent = ContactSaveService.createNewGroupIntent(getActivity(), mAccount, |
| name, null, getActivity().getClass(), callbackAction); |
| } else { |
| serviceIntent = ContactSaveService.createGroupRenameIntent(getActivity(), mGroupId, |
| name, getActivity().getClass(), callbackAction); |
| } |
| ContactSaveService.startService(getActivity(), serviceIntent); |
| getListener().onGroupNameEditCompleted(mGroupName); |
| dismiss(); |
| } |
| |
| @Override |
| public void onCancel(DialogInterface dialog) { |
| super.onCancel(dialog); |
| getListener().onGroupNameEditCancelled(); |
| } |
| |
| @Override |
| public void onSaveInstanceState(Bundle outState) { |
| super.onSaveInstanceState(outState); |
| outState.putString(KEY_GROUP_NAME, getGroupName()); |
| } |
| |
| @Override |
| public Loader<Cursor> onCreateLoader(int id, Bundle args) { |
| // Only a single loader so id is ignored. |
| return new CursorLoader(getActivity(), Groups.CONTENT_SUMMARY_URI, |
| new String[]{Groups.TITLE, Groups.SYSTEM_ID, Groups.ACCOUNT_TYPE, |
| Groups.SUMMARY_COUNT, Groups.GROUP_IS_READ_ONLY}, |
| getSelection(), getSelectionArgs(), null); |
| } |
| |
| @Override |
| public void onLoadFinished(Loader<Cursor> loader, Cursor data) { |
| mExistingGroups = new HashSet<>(); |
| final GroupUtil.GroupsProjection projection = new GroupUtil.GroupsProjection(data); |
| // Initialize cursor's position. If Activity relaunched by orientation change, |
| // only onLoadFinished is called. OnCreateLoader is not called. |
| // The cursor's position is remain end position by moveToNext when the last onLoadFinished |
| // was called. Therefore, if cursor position was not initialized mExistingGroups is empty. |
| data.moveToPosition(-1); |
| while (data.moveToNext()) { |
| String title = projection.getTitle(data); |
| // Trim existing group name. |
| // When " Group " exists, do not save "Group". |
| // This behavior is the same as Google Contacts. |
| if (!TextUtils.isEmpty(title)) { |
| title = title.trim(); |
| } |
| // Empty system groups aren't shown in the nav drawer so it would be confusing to tell |
| // the user that they already exist. Instead we allow them to create a duplicate |
| // group in this case. This is how the web handles this case as well (it creates a |
| // new non-system group if a new group with a title that matches a system group is |
| // create). |
| if (projection.isEmptyFFCGroup(data)) { |
| continue; |
| } |
| mExistingGroups.add(title); |
| } |
| } |
| |
| @Override |
| public void onLoaderReset(Loader<Cursor> loader) { |
| } |
| |
| private void showInputMethod(View view) { |
| if (getActivity() == null) { |
| return; |
| } |
| final InputMethodManager imm = (InputMethodManager) getActivity().getSystemService( |
| Context.INPUT_METHOD_SERVICE); |
| if (imm != null) { |
| imm.showSoftInput(view, /* flags */ 0); |
| } |
| } |
| |
| private void hideInputMethod() { |
| final InputMethodManager imm = (InputMethodManager) getActivity().getSystemService( |
| Context.INPUT_METHOD_SERVICE); |
| if (imm != null && mGroupNameEditText != null) { |
| imm.hideSoftInputFromWindow(mGroupNameEditText.getWindowToken(), /* flags */ 0); |
| } |
| } |
| |
| private Listener getListener() { |
| if (mListener != null) { |
| return mListener; |
| } else if (getActivity() instanceof Listener) { |
| return (Listener) getActivity(); |
| } else { |
| return Listener.None; |
| } |
| } |
| |
| private String getGroupName() { |
| return mGroupNameEditText == null || mGroupNameEditText.getText() == null |
| ? null : mGroupNameEditText.getText().toString(); |
| } |
| |
| private String getSelection() { |
| final StringBuilder builder = new StringBuilder(); |
| builder.append(Groups.ACCOUNT_NAME).append(mAccount.name == null ? " IS NULL " : "=?") |
| .append(" AND ") |
| .append(Groups.ACCOUNT_TYPE).append(mAccount.type == null ? " IS NULL " : "=?") |
| .append(" AND ") |
| .append(Groups.DATA_SET).append(mAccount.dataSet == null ? " IS NULL " : "=?") |
| .append(" AND ") |
| .append(Groups.DELETED).append("=0"); |
| return builder.toString(); |
| } |
| |
| private String[] getSelectionArgs() { |
| if (mAccount.isNullAccount()) { |
| return null; |
| } else if (mAccount.dataSet == null) { |
| return new String[]{ |
| mAccount.name, |
| mAccount.type |
| }; |
| } else { |
| return new String[]{ |
| mAccount.name, |
| mAccount.type, |
| mAccount.dataSet |
| }; |
| } |
| } |
| } |