1 /* 2 * Copyright (C) 2016 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, softwareateCre 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 package com.android.contacts.group; 17 18 import android.app.Dialog; 19 import android.app.DialogFragment; 20 import android.app.LoaderManager; 21 import android.content.Context; 22 import android.content.CursorLoader; 23 import android.content.DialogInterface; 24 import android.content.DialogInterface.OnClickListener; 25 import android.content.Intent; 26 import android.content.Loader; 27 import android.database.Cursor; 28 import android.os.Bundle; 29 import android.provider.ContactsContract.Groups; 30 import com.google.android.material.textfield.TextInputLayout; 31 import androidx.appcompat.app.AlertDialog; 32 import android.text.Editable; 33 import android.text.TextUtils; 34 import android.text.TextWatcher; 35 import android.view.View; 36 import android.view.WindowManager; 37 import android.view.inputmethod.InputMethodManager; 38 import android.widget.Button; 39 import android.widget.EditText; 40 import android.widget.TextView; 41 42 import com.android.contacts.ContactSaveService; 43 import com.android.contacts.R; 44 import com.android.contacts.model.account.AccountWithDataSet; 45 46 import com.google.common.base.Strings; 47 48 import java.util.Collections; 49 import java.util.HashSet; 50 import java.util.Set; 51 52 /** 53 * Edits the name of a group. 54 */ 55 public final class GroupNameEditDialogFragment extends DialogFragment implements 56 LoaderManager.LoaderCallbacks<Cursor> { 57 58 private static final String KEY_GROUP_NAME = "groupName"; 59 60 private static final String ARG_IS_INSERT = "isInsert"; 61 private static final String ARG_GROUP_NAME = "groupName"; 62 private static final String ARG_ACCOUNT = "account"; 63 private static final String ARG_CALLBACK_ACTION = "callbackAction"; 64 private static final String ARG_GROUP_ID = "groupId"; 65 66 private static final long NO_GROUP_ID = -1; 67 68 69 /** Callbacks for hosts of the {@link GroupNameEditDialogFragment}. */ 70 public interface Listener { onGroupNameEditCancelled()71 void onGroupNameEditCancelled(); onGroupNameEditCompleted(String name)72 void onGroupNameEditCompleted(String name); 73 74 public static final Listener None = new Listener() { 75 @Override 76 public void onGroupNameEditCancelled() { } 77 78 @Override 79 public void onGroupNameEditCompleted(String name) { } 80 }; 81 } 82 83 private boolean mIsInsert; 84 private String mGroupName; 85 private long mGroupId; 86 private Listener mListener; 87 private AccountWithDataSet mAccount; 88 private EditText mGroupNameEditText; 89 private TextInputLayout mGroupNameTextLayout; 90 private Set<String> mExistingGroups = Collections.emptySet(); 91 newInstanceForCreation( AccountWithDataSet account, String callbackAction)92 public static GroupNameEditDialogFragment newInstanceForCreation( 93 AccountWithDataSet account, String callbackAction) { 94 return newInstance(account, callbackAction, NO_GROUP_ID, null); 95 } 96 newInstanceForUpdate( AccountWithDataSet account, String callbackAction, long groupId, String groupName)97 public static GroupNameEditDialogFragment newInstanceForUpdate( 98 AccountWithDataSet account, String callbackAction, long groupId, String groupName) { 99 return newInstance(account, callbackAction, groupId, groupName); 100 } 101 newInstance( AccountWithDataSet account, String callbackAction, long groupId, String groupName)102 private static GroupNameEditDialogFragment newInstance( 103 AccountWithDataSet account, String callbackAction, long groupId, String groupName) { 104 if (account == null || account.name == null || account.type == null) { 105 throw new IllegalArgumentException("Invalid account"); 106 } 107 final boolean isInsert = groupId == NO_GROUP_ID; 108 final Bundle args = new Bundle(); 109 args.putBoolean(ARG_IS_INSERT, isInsert); 110 args.putLong(ARG_GROUP_ID, groupId); 111 args.putString(ARG_GROUP_NAME, groupName); 112 args.putParcelable(ARG_ACCOUNT, account); 113 args.putString(ARG_CALLBACK_ACTION, callbackAction); 114 115 final GroupNameEditDialogFragment dialog = new GroupNameEditDialogFragment(); 116 dialog.setArguments(args); 117 return dialog; 118 } 119 120 @Override onCreate(Bundle savedInstanceState)121 public void onCreate(Bundle savedInstanceState) { 122 super.onCreate(savedInstanceState); 123 setStyle(STYLE_NORMAL, R.style.ContactsAlertDialogThemeAppCompat); 124 final Bundle args = getArguments(); 125 if (savedInstanceState == null) { 126 mGroupName = args.getString(KEY_GROUP_NAME); 127 } else { 128 mGroupName = savedInstanceState.getString(ARG_GROUP_NAME); 129 } 130 131 mGroupId = args.getLong(ARG_GROUP_ID, NO_GROUP_ID); 132 mIsInsert = args.getBoolean(ARG_IS_INSERT, true); 133 mAccount = getArguments().getParcelable(ARG_ACCOUNT); 134 135 // There is only one loader so the id arg doesn't matter. 136 getLoaderManager().initLoader(0, null, this); 137 } 138 139 @Override onCreateDialog(Bundle savedInstanceState)140 public Dialog onCreateDialog(Bundle savedInstanceState) { 141 // Build a dialog with two buttons and a view of a single EditText input field 142 final TextView title = (TextView) View.inflate(getActivity(), R.layout.dialog_title, null); 143 title.setText(mIsInsert 144 ? R.string.group_name_dialog_insert_title 145 : R.string.group_name_dialog_update_title); 146 final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity(), getTheme()) 147 .setCustomTitle(title) 148 .setView(R.layout.group_name_edit_dialog) 149 .setNegativeButton(android.R.string.cancel, new OnClickListener() { 150 @Override 151 public void onClick(DialogInterface dialog, int which) { 152 hideInputMethod(); 153 getListener().onGroupNameEditCancelled(); 154 dismiss(); 155 } 156 }) 157 // The Positive button listener is defined below in the OnShowListener to 158 // allow for input validation 159 .setPositiveButton(android.R.string.ok, null); 160 161 // Disable the create button when the name is empty 162 final AlertDialog alertDialog = builder.create(); 163 alertDialog.getWindow().setSoftInputMode( 164 WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE); 165 alertDialog.setOnShowListener(new DialogInterface.OnShowListener() { 166 @Override 167 public void onShow(DialogInterface dialog) { 168 mGroupNameEditText = (EditText) alertDialog.findViewById(android.R.id.text1); 169 mGroupNameTextLayout = 170 (TextInputLayout) alertDialog.findViewById(R.id.text_input_layout); 171 if (!TextUtils.isEmpty(mGroupName)) { 172 mGroupNameEditText.setText(mGroupName); 173 // Guard against already created group names that are longer than the max 174 final int maxLength = getResources().getInteger( 175 R.integer.group_name_max_length); 176 mGroupNameEditText.setSelection( 177 mGroupName.length() > maxLength ? maxLength : mGroupName.length()); 178 } 179 showInputMethod(mGroupNameEditText); 180 181 final Button createButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE); 182 createButton.setEnabled(!TextUtils.isEmpty(getGroupName())); 183 184 // Override the click listener to prevent dismissal if creating a duplicate group. 185 createButton.setOnClickListener(new View.OnClickListener() { 186 @Override 187 public void onClick(View v) { 188 maybePersistCurrentGroupName(v); 189 } 190 }); 191 mGroupNameEditText.addTextChangedListener(new TextWatcher() { 192 @Override 193 public void beforeTextChanged(CharSequence s, int start, int count, int after) { 194 } 195 196 @Override 197 public void onTextChanged(CharSequence s, int start, int before, int count) { 198 } 199 200 @Override 201 public void afterTextChanged(Editable s) { 202 mGroupNameTextLayout.setError(null); 203 createButton.setEnabled(!TextUtils.isEmpty(s)); 204 } 205 }); 206 } 207 }); 208 209 return alertDialog; 210 } 211 212 /** 213 * Sets the listener for the rename 214 * 215 * Setting a listener on a fragment is error prone since it will be lost if the fragment 216 * is recreated. This exists because it is used from a view class (GroupMembersView) which 217 * needs to modify it's state when this fragment updates the name. 218 * 219 * @param listener the listener. can be null 220 */ setListener(Listener listener)221 public void setListener(Listener listener) { 222 mListener = listener; 223 } 224 hasNameChanged()225 private boolean hasNameChanged() { 226 final String name = Strings.nullToEmpty(getGroupName()); 227 final String originalName = getArguments().getString(ARG_GROUP_NAME); 228 return (mIsInsert && !name.isEmpty()) || !name.equals(originalName); 229 } 230 maybePersistCurrentGroupName(View button)231 private void maybePersistCurrentGroupName(View button) { 232 if (!hasNameChanged()) { 233 dismiss(); 234 return; 235 } 236 String name = getGroupName(); 237 // Trim group name, when group is saved. 238 // When "Group" exists, do not save " Group ". This behavior is the same as Google Contacts. 239 if (!TextUtils.isEmpty(name)) { 240 name = name.trim(); 241 } 242 // Note we don't check if the loader finished populating mExistingGroups. It's not the 243 // end of the world if the user ends up with a duplicate group and in practice it should 244 // never really happen (the query should complete much sooner than the user can edit the 245 // label) 246 if (mExistingGroups.contains(name)) { 247 mGroupNameTextLayout.setError( 248 getString(R.string.groupExistsErrorMessage)); 249 button.setEnabled(false); 250 return; 251 } 252 final String callbackAction = getArguments().getString(ARG_CALLBACK_ACTION); 253 final Intent serviceIntent; 254 if (mIsInsert) { 255 serviceIntent = ContactSaveService.createNewGroupIntent(getActivity(), mAccount, 256 name, null, getActivity().getClass(), callbackAction); 257 } else { 258 serviceIntent = ContactSaveService.createGroupRenameIntent(getActivity(), mGroupId, 259 name, getActivity().getClass(), callbackAction); 260 } 261 ContactSaveService.startService(getActivity(), serviceIntent); 262 getListener().onGroupNameEditCompleted(mGroupName); 263 dismiss(); 264 } 265 266 @Override onCancel(DialogInterface dialog)267 public void onCancel(DialogInterface dialog) { 268 super.onCancel(dialog); 269 getListener().onGroupNameEditCancelled(); 270 } 271 272 @Override onSaveInstanceState(Bundle outState)273 public void onSaveInstanceState(Bundle outState) { 274 super.onSaveInstanceState(outState); 275 outState.putString(KEY_GROUP_NAME, getGroupName()); 276 } 277 278 @Override onCreateLoader(int id, Bundle args)279 public Loader<Cursor> onCreateLoader(int id, Bundle args) { 280 // Only a single loader so id is ignored. 281 return new CursorLoader(getActivity(), Groups.CONTENT_SUMMARY_URI, 282 new String[] { Groups.TITLE, Groups.SYSTEM_ID, Groups.ACCOUNT_TYPE, 283 Groups.SUMMARY_COUNT, Groups.GROUP_IS_READ_ONLY}, 284 getSelection(), getSelectionArgs(), null); 285 } 286 287 @Override onLoadFinished(Loader<Cursor> loader, Cursor data)288 public void onLoadFinished(Loader<Cursor> loader, Cursor data) { 289 mExistingGroups = new HashSet<>(); 290 final GroupUtil.GroupsProjection projection = new GroupUtil.GroupsProjection(data); 291 // Initialize cursor's position. If Activity relaunched by orientation change, 292 // only onLoadFinished is called. OnCreateLoader is not called. 293 // The cursor's position is remain end position by moveToNext when the last onLoadFinished 294 // was called. Therefore, if cursor position was not initialized mExistingGroups is empty. 295 data.moveToPosition(-1); 296 while (data.moveToNext()) { 297 String title = projection.getTitle(data); 298 // Trim existing group name. 299 // When " Group " exists, do not save "Group". 300 // This behavior is the same as Google Contacts. 301 if (!TextUtils.isEmpty(title)) { 302 title = title.trim(); 303 } 304 // Empty system groups aren't shown in the nav drawer so it would be confusing to tell 305 // the user that they already exist. Instead we allow them to create a duplicate 306 // group in this case. This is how the web handles this case as well (it creates a 307 // new non-system group if a new group with a title that matches a system group is 308 // create). 309 if (projection.isEmptyFFCGroup(data)) { 310 continue; 311 } 312 mExistingGroups.add(title); 313 } 314 } 315 316 @Override onLoaderReset(Loader<Cursor> loader)317 public void onLoaderReset(Loader<Cursor> loader) { 318 } 319 showInputMethod(View view)320 private void showInputMethod(View view) { 321 if (getActivity() == null) { 322 return; 323 } 324 final InputMethodManager imm = (InputMethodManager) getActivity().getSystemService( 325 Context.INPUT_METHOD_SERVICE); 326 if (imm != null) { 327 imm.showSoftInput(view, /* flags */ 0); 328 } 329 } 330 hideInputMethod()331 private void hideInputMethod() { 332 final InputMethodManager imm = (InputMethodManager) getActivity().getSystemService( 333 Context.INPUT_METHOD_SERVICE); 334 if (imm != null && mGroupNameEditText != null) { 335 imm.hideSoftInputFromWindow(mGroupNameEditText.getWindowToken(), /* flags */ 0); 336 } 337 } 338 getListener()339 private Listener getListener() { 340 if (mListener != null) { 341 return mListener; 342 } else if (getActivity() instanceof Listener) { 343 return (Listener) getActivity(); 344 } else { 345 return Listener.None; 346 } 347 } 348 getGroupName()349 private String getGroupName() { 350 return mGroupNameEditText == null || mGroupNameEditText.getText() == null 351 ? null : mGroupNameEditText.getText().toString(); 352 } 353 getSelection()354 private String getSelection() { 355 final StringBuilder builder = new StringBuilder(); 356 builder.append(Groups.ACCOUNT_NAME).append("=? AND ") 357 .append(Groups.ACCOUNT_TYPE).append("=? AND ") 358 .append(Groups.DELETED).append("=?"); 359 if (mAccount.dataSet != null) { 360 builder.append(" AND ").append(Groups.DATA_SET).append("=?"); 361 } 362 return builder.toString(); 363 } 364 getSelectionArgs()365 private String[] getSelectionArgs() { 366 final int len = mAccount.dataSet == null ? 3 : 4; 367 final String[] args = new String[len]; 368 args[0] = mAccount.name; 369 args[1] = mAccount.type; 370 args[2] = "0"; // Not deleted 371 if (mAccount.dataSet != null) { 372 args[3] = mAccount.dataSet; 373 } 374 return args; 375 } 376 } 377