• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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