Home | History | Annotate | Download | only in group
      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 {
     71         void onGroupNameEditCancelled();
     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 
     92     public static GroupNameEditDialogFragment newInstanceForCreation(
     93             AccountWithDataSet account, String callbackAction) {
     94         return newInstance(account, callbackAction, NO_GROUP_ID, null);
     95     }
     96 
     97     public static GroupNameEditDialogFragment newInstanceForUpdate(
     98             AccountWithDataSet account, String callbackAction, long groupId, String groupName) {
     99         return newInstance(account, callbackAction, groupId, groupName);
    100     }
    101 
    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
    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
    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      */
    221     public void setListener(Listener listener) {
    222         mListener = listener;
    223     }
    224 
    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 
    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
    267     public void onCancel(DialogInterface dialog) {
    268         super.onCancel(dialog);
    269         getListener().onGroupNameEditCancelled();
    270     }
    271 
    272     @Override
    273     public void onSaveInstanceState(Bundle outState) {
    274         super.onSaveInstanceState(outState);
    275         outState.putString(KEY_GROUP_NAME, getGroupName());
    276     }
    277 
    278     @Override
    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
    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
    317     public void onLoaderReset(Loader<Cursor> loader) {
    318     }
    319 
    320     private void showInputMethod(View view) {
    321         final InputMethodManager imm = (InputMethodManager) getActivity().getSystemService(
    322                 Context.INPUT_METHOD_SERVICE);
    323         if (imm != null) {
    324             imm.showSoftInput(view, /* flags */ 0);
    325         }
    326     }
    327 
    328     private void hideInputMethod() {
    329         final InputMethodManager imm = (InputMethodManager) getActivity().getSystemService(
    330                 Context.INPUT_METHOD_SERVICE);
    331         if (imm != null && mGroupNameEditText != null) {
    332             imm.hideSoftInputFromWindow(mGroupNameEditText.getWindowToken(), /* flags */ 0);
    333         }
    334     }
    335 
    336     private Listener getListener() {
    337         if (mListener != null) {
    338             return mListener;
    339         } else if (getActivity() instanceof Listener) {
    340             return (Listener) getActivity();
    341         } else {
    342             return Listener.None;
    343         }
    344     }
    345 
    346     private String getGroupName() {
    347         return mGroupNameEditText == null || mGroupNameEditText.getText() == null
    348                 ? null : mGroupNameEditText.getText().toString();
    349     }
    350 
    351     private String getSelection() {
    352         final StringBuilder builder = new StringBuilder();
    353         builder.append(Groups.ACCOUNT_NAME).append("=? AND ")
    354                .append(Groups.ACCOUNT_TYPE).append("=? AND ")
    355                .append(Groups.DELETED).append("=?");
    356         if (mAccount.dataSet != null) {
    357             builder.append(" AND ").append(Groups.DATA_SET).append("=?");
    358         }
    359         return builder.toString();
    360     }
    361 
    362     private String[] getSelectionArgs() {
    363         final int len = mAccount.dataSet == null ? 3 : 4;
    364         final String[] args = new String[len];
    365         args[0] = mAccount.name;
    366         args[1] = mAccount.type;
    367         args[2] = "0"; // Not deleted
    368         if (mAccount.dataSet != null) {
    369             args[3] = mAccount.dataSet;
    370         }
    371         return args;
    372     }
    373 }
    374