• 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.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