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