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.support.design.widget.TextInputLayout; 31 import android.support.v7.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 final String name = getGroupName(); 237 // Note we don't check if the loader finished populating mExistingGroups. It's not the 238 // end of the world if the user ends up with a duplicate group and in practice it should 239 // never really happen (the query should complete much sooner than the user can edit the 240 // label) 241 if (mExistingGroups.contains(name)) { 242 mGroupNameTextLayout.setError( 243 getString(R.string.groupExistsErrorMessage)); 244 button.setEnabled(false); 245 return; 246 } 247 final String callbackAction = getArguments().getString(ARG_CALLBACK_ACTION); 248 final Intent serviceIntent; 249 if (mIsInsert) { 250 serviceIntent = ContactSaveService.createNewGroupIntent(getActivity(), mAccount, 251 name, null, getActivity().getClass(), callbackAction); 252 } else { 253 serviceIntent = ContactSaveService.createGroupRenameIntent(getActivity(), mGroupId, 254 name, getActivity().getClass(), callbackAction); 255 } 256 ContactSaveService.startService(getActivity(), serviceIntent); 257 getListener().onGroupNameEditCompleted(mGroupName); 258 dismiss(); 259 } 260 261 @Override onCancel(DialogInterface dialog)262 public void onCancel(DialogInterface dialog) { 263 super.onCancel(dialog); 264 getListener().onGroupNameEditCancelled(); 265 } 266 267 @Override onSaveInstanceState(Bundle outState)268 public void onSaveInstanceState(Bundle outState) { 269 super.onSaveInstanceState(outState); 270 outState.putString(KEY_GROUP_NAME, getGroupName()); 271 } 272 273 @Override onCreateLoader(int id, Bundle args)274 public Loader<Cursor> onCreateLoader(int id, Bundle args) { 275 // Only a single loader so id is ignored. 276 return new CursorLoader(getActivity(), Groups.CONTENT_SUMMARY_URI, 277 new String[] { Groups.TITLE, Groups.SYSTEM_ID, Groups.ACCOUNT_TYPE, 278 Groups.SUMMARY_COUNT, Groups.GROUP_IS_READ_ONLY}, 279 getSelection(), getSelectionArgs(), null); 280 } 281 282 @Override onLoadFinished(Loader<Cursor> loader, Cursor data)283 public void onLoadFinished(Loader<Cursor> loader, Cursor data) { 284 mExistingGroups = new HashSet<>(); 285 final GroupUtil.GroupsProjection projection = new GroupUtil.GroupsProjection(data); 286 while (data.moveToNext()) { 287 final String title = projection.getTitle(data); 288 // Empty system groups aren't shown in the nav drawer so it would be confusing to tell 289 // the user that they already exist. Instead we allow them to create a duplicate 290 // group in this case. This is how the web handles this case as well (it creates a 291 // new non-system group if a new group with a title that matches a system group is 292 // create). 293 if (projection.isEmptyFFCGroup(data)) { 294 continue; 295 } 296 mExistingGroups.add(title); 297 } 298 } 299 300 @Override onLoaderReset(Loader<Cursor> loader)301 public void onLoaderReset(Loader<Cursor> loader) { 302 } 303 showInputMethod(View view)304 private void showInputMethod(View view) { 305 final InputMethodManager imm = (InputMethodManager) getActivity().getSystemService( 306 Context.INPUT_METHOD_SERVICE); 307 if (imm != null) { 308 imm.showSoftInput(view, /* flags */ 0); 309 } 310 } 311 hideInputMethod()312 private void hideInputMethod() { 313 final InputMethodManager imm = (InputMethodManager) getActivity().getSystemService( 314 Context.INPUT_METHOD_SERVICE); 315 if (imm != null && mGroupNameEditText != null) { 316 imm.hideSoftInputFromWindow(mGroupNameEditText.getWindowToken(), /* flags */ 0); 317 } 318 } 319 getListener()320 private Listener getListener() { 321 if (mListener != null) { 322 return mListener; 323 } else if (getActivity() instanceof Listener) { 324 return (Listener) getActivity(); 325 } else { 326 return Listener.None; 327 } 328 } 329 getGroupName()330 private String getGroupName() { 331 return mGroupNameEditText == null || mGroupNameEditText.getText() == null 332 ? null : mGroupNameEditText.getText().toString(); 333 } 334 getSelection()335 private String getSelection() { 336 final StringBuilder builder = new StringBuilder(); 337 builder.append(Groups.ACCOUNT_NAME).append("=? AND ") 338 .append(Groups.ACCOUNT_TYPE).append("=? AND ") 339 .append(Groups.DELETED).append("=?"); 340 if (mAccount.dataSet != null) { 341 builder.append(" AND ").append(Groups.DATA_SET).append("=?"); 342 } 343 return builder.toString(); 344 } 345 getSelectionArgs()346 private String[] getSelectionArgs() { 347 final int len = mAccount.dataSet == null ? 3 : 4; 348 final String[] args = new String[len]; 349 args[0] = mAccount.name; 350 args[1] = mAccount.type; 351 args[2] = "0"; // Not deleted 352 if (mAccount.dataSet != null) { 353 args[3] = mAccount.dataSet; 354 } 355 return args; 356 } 357 } 358