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