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         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
    262     public void onCancel(DialogInterface dialog) {
    263         super.onCancel(dialog);
    264         getListener().onGroupNameEditCancelled();
    265     }
    266 
    267     @Override
    268     public void onSaveInstanceState(Bundle outState) {
    269         super.onSaveInstanceState(outState);
    270         outState.putString(KEY_GROUP_NAME, getGroupName());
    271     }
    272 
    273     @Override
    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
    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
    301     public void onLoaderReset(Loader<Cursor> loader) {
    302     }
    303 
    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 
    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 
    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 
    330     private String getGroupName() {
    331         return mGroupNameEditText == null || mGroupNameEditText.getText() == null
    332                 ? null : mGroupNameEditText.getText().toString();
    333     }
    334 
    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 
    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