Home | History | Annotate | Download | only in group
      1 /*
      2  * Copyright (C) 2011 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, software
     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 
     17 package com.android.contacts.group;
     18 
     19 import android.app.Activity;
     20 import android.app.Fragment;
     21 import android.app.LoaderManager;
     22 import android.app.LoaderManager.LoaderCallbacks;
     23 import android.content.ActivityNotFoundException;
     24 import android.content.ContentUris;
     25 import android.content.Context;
     26 import android.content.CursorLoader;
     27 import android.content.Intent;
     28 import android.content.Loader;
     29 import android.content.res.Resources;
     30 import android.database.Cursor;
     31 import android.graphics.Rect;
     32 import android.net.Uri;
     33 import android.os.Bundle;
     34 import android.provider.ContactsContract.Groups;
     35 import android.text.TextUtils;
     36 import android.util.Log;
     37 import android.view.LayoutInflater;
     38 import android.view.Menu;
     39 import android.view.MenuInflater;
     40 import android.view.MenuItem;
     41 import android.view.View;
     42 import android.view.View.OnClickListener;
     43 import android.view.ViewGroup;
     44 import android.widget.AbsListView;
     45 import android.widget.AbsListView.OnScrollListener;
     46 import android.widget.ListView;
     47 import android.widget.TextView;
     48 import android.widget.Toast;
     49 
     50 import com.android.contacts.GroupMemberLoader;
     51 import com.android.contacts.GroupMetaDataLoader;
     52 import com.android.contacts.R;
     53 import com.android.contacts.common.ContactPhotoManager;
     54 import com.android.contacts.common.util.ImplicitIntentsUtil;
     55 import com.android.contacts.interactions.GroupDeletionDialogFragment;
     56 import com.android.contacts.common.list.ContactTileAdapter;
     57 import com.android.contacts.common.list.ContactTileView;
     58 import com.android.contacts.list.GroupMemberTileAdapter;
     59 import com.android.contacts.common.model.AccountTypeManager;
     60 import com.android.contacts.common.model.account.AccountType;
     61 
     62 /**
     63  * Displays the details of a group and shows a list of actions possible for the group.
     64  */
     65 public class GroupDetailFragment extends Fragment implements OnScrollListener {
     66 
     67     public static interface Listener {
     68         /**
     69          * The group title has been loaded
     70          */
     71         public void onGroupTitleUpdated(String title);
     72 
     73         /**
     74          * The number of group members has been determined
     75          */
     76         public void onGroupSizeUpdated(String size);
     77 
     78         /**
     79          * The account type and dataset have been determined.
     80          */
     81         public void onAccountTypeUpdated(String accountTypeString, String dataSet);
     82 
     83         /**
     84          * User decided to go to Edit-Mode
     85          */
     86         public void onEditRequested(Uri groupUri);
     87 
     88         /**
     89          * Contact is selected and should launch details page
     90          */
     91         public void onContactSelected(Uri contactUri);
     92     }
     93 
     94     private static final String TAG = "GroupDetailFragment";
     95 
     96     private static final int LOADER_METADATA = 0;
     97     private static final int LOADER_MEMBERS = 1;
     98 
     99     private Context mContext;
    100 
    101     private View mRootView;
    102     private ViewGroup mGroupSourceViewContainer;
    103     private View mGroupSourceView;
    104     private TextView mGroupTitle;
    105     private TextView mGroupSize;
    106     private ListView mMemberListView;
    107     private View mEmptyView;
    108 
    109     private Listener mListener;
    110 
    111     private ContactTileAdapter mAdapter;
    112     private ContactPhotoManager mPhotoManager;
    113     private AccountTypeManager mAccountTypeManager;
    114 
    115     private Uri mGroupUri;
    116     private long mGroupId;
    117     private String mGroupName;
    118     private String mAccountTypeString;
    119     private String mDataSet;
    120     private boolean mIsReadOnly;
    121     private boolean mIsMembershipEditable;
    122 
    123     private boolean mShowGroupActionInActionBar;
    124     private boolean mOptionsMenuGroupDeletable;
    125     private boolean mOptionsMenuGroupEditable;
    126     private boolean mCloseActivityAfterDelete;
    127 
    128     public GroupDetailFragment() {
    129     }
    130 
    131     @Override
    132     public void onAttach(Activity activity) {
    133         super.onAttach(activity);
    134         mContext = activity;
    135         mAccountTypeManager = AccountTypeManager.getInstance(mContext);
    136 
    137         Resources res = getResources();
    138         int columnCount = res.getInteger(R.integer.contact_tile_column_count);
    139 
    140         mAdapter = new GroupMemberTileAdapter(activity, mContactTileListener, columnCount);
    141 
    142         configurePhotoLoader();
    143     }
    144 
    145     @Override
    146     public void onDetach() {
    147         super.onDetach();
    148         mContext = null;
    149     }
    150 
    151     @Override
    152     public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
    153         setHasOptionsMenu(true);
    154         mRootView = inflater.inflate(R.layout.group_detail_fragment, container, false);
    155         mGroupTitle = (TextView) mRootView.findViewById(R.id.group_title);
    156         mGroupSize = (TextView) mRootView.findViewById(R.id.group_size);
    157         mGroupSourceViewContainer = (ViewGroup) mRootView.findViewById(
    158                 R.id.group_source_view_container);
    159         mEmptyView = mRootView.findViewById(android.R.id.empty);
    160         mMemberListView = (ListView) mRootView.findViewById(android.R.id.list);
    161         mMemberListView.setItemsCanFocus(true);
    162         mMemberListView.setAdapter(mAdapter);
    163 
    164         return mRootView;
    165     }
    166 
    167     public void loadGroup(Uri groupUri) {
    168         mGroupUri= groupUri;
    169         startGroupMetadataLoader();
    170     }
    171 
    172     public void setQuickContact(boolean enableQuickContact) {
    173         mAdapter.enableQuickContact(enableQuickContact);
    174     }
    175 
    176     private void configurePhotoLoader() {
    177         if (mContext != null) {
    178             if (mPhotoManager == null) {
    179                 mPhotoManager = ContactPhotoManager.getInstance(mContext);
    180             }
    181             if (mMemberListView != null) {
    182                 mMemberListView.setOnScrollListener(this);
    183             }
    184             if (mAdapter != null) {
    185                 mAdapter.setPhotoLoader(mPhotoManager);
    186             }
    187         }
    188     }
    189 
    190     public void setListener(Listener value) {
    191         mListener = value;
    192     }
    193 
    194     public void setShowGroupSourceInActionBar(boolean show) {
    195         mShowGroupActionInActionBar = show;
    196     }
    197 
    198     public Uri getGroupUri() {
    199         return mGroupUri;
    200     }
    201 
    202     /**
    203      * Start the loader to retrieve the metadata for this group.
    204      */
    205     private void startGroupMetadataLoader() {
    206         getLoaderManager().restartLoader(LOADER_METADATA, null, mGroupMetadataLoaderListener);
    207     }
    208 
    209     /**
    210      * Start the loader to retrieve the list of group members.
    211      */
    212     private void startGroupMembersLoader() {
    213         getLoaderManager().restartLoader(LOADER_MEMBERS, null, mGroupMemberListLoaderListener);
    214     }
    215 
    216     private final ContactTileView.Listener mContactTileListener =
    217             new ContactTileView.Listener() {
    218 
    219         @Override
    220         public void onContactSelected(Uri contactUri, Rect targetRect) {
    221             mListener.onContactSelected(contactUri);
    222         }
    223 
    224         @Override
    225         public void onCallNumberDirectly(String phoneNumber) {
    226             // No need to call phone number directly from People app.
    227             Log.w(TAG, "unexpected invocation of onCallNumberDirectly()");
    228         }
    229 
    230         @Override
    231         public int getApproximateTileWidth() {
    232             return getView().getWidth() / mAdapter.getColumnCount();
    233         }
    234     };
    235 
    236     /**
    237      * The listener for the group metadata loader.
    238      */
    239     private final LoaderManager.LoaderCallbacks<Cursor> mGroupMetadataLoaderListener =
    240             new LoaderCallbacks<Cursor>() {
    241 
    242         @Override
    243         public CursorLoader onCreateLoader(int id, Bundle args) {
    244             return new GroupMetaDataLoader(mContext, mGroupUri);
    245         }
    246 
    247         @Override
    248         public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
    249             if (data == null || data.isClosed()) {
    250                 Log.e(TAG, "Failed to load group metadata");
    251                 return;
    252             }
    253             data.moveToPosition(-1);
    254             if (data.moveToNext()) {
    255                 boolean deleted = data.getInt(GroupMetaDataLoader.DELETED) == 1;
    256                 if (!deleted) {
    257                     bindGroupMetaData(data);
    258 
    259                     // Retrieve the list of members
    260                     startGroupMembersLoader();
    261                     return;
    262                 }
    263             }
    264             updateSize(-1);
    265             updateTitle(null);
    266         }
    267 
    268         @Override
    269         public void onLoaderReset(Loader<Cursor> loader) {}
    270     };
    271 
    272     /**
    273      * The listener for the group members list loader
    274      */
    275     private final LoaderManager.LoaderCallbacks<Cursor> mGroupMemberListLoaderListener =
    276             new LoaderCallbacks<Cursor>() {
    277 
    278         @Override
    279         public CursorLoader onCreateLoader(int id, Bundle args) {
    280             return GroupMemberLoader.constructLoaderForGroupDetailQuery(mContext, mGroupId);
    281         }
    282 
    283         @Override
    284         public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
    285             if (data == null || data.isClosed()) {
    286                 Log.e(TAG, "Failed to load group members");
    287                 return;
    288             }
    289             updateSize(data.getCount());
    290             mAdapter.setContactCursor(data);
    291             mMemberListView.setEmptyView(mEmptyView);
    292         }
    293 
    294         @Override
    295         public void onLoaderReset(Loader<Cursor> loader) {}
    296     };
    297 
    298     private void bindGroupMetaData(Cursor cursor) {
    299         cursor.moveToPosition(-1);
    300         if (cursor.moveToNext()) {
    301             mAccountTypeString = cursor.getString(GroupMetaDataLoader.ACCOUNT_TYPE);
    302             mDataSet = cursor.getString(GroupMetaDataLoader.DATA_SET);
    303             mGroupId = cursor.getLong(GroupMetaDataLoader.GROUP_ID);
    304             mGroupName = cursor.getString(GroupMetaDataLoader.TITLE);
    305             mIsReadOnly = cursor.getInt(GroupMetaDataLoader.IS_READ_ONLY) == 1;
    306             updateTitle(mGroupName);
    307             // Must call invalidate so that the option menu will get updated
    308             getActivity().invalidateOptionsMenu ();
    309 
    310             final String accountTypeString = cursor.getString(GroupMetaDataLoader.ACCOUNT_TYPE);
    311             final String dataSet = cursor.getString(GroupMetaDataLoader.DATA_SET);
    312             updateAccountType(accountTypeString, dataSet);
    313         }
    314     }
    315 
    316     private void updateTitle(String title) {
    317         if (mGroupTitle != null) {
    318             mGroupTitle.setText(title);
    319         } else {
    320             mListener.onGroupTitleUpdated(title);
    321         }
    322     }
    323 
    324     /**
    325      * Display the count of the number of group members.
    326      * @param size of the group (can be -1 if no size could be determined)
    327      */
    328     private void updateSize(int size) {
    329         String groupSizeString;
    330         if (size == -1) {
    331             groupSizeString = null;
    332         } else {
    333             AccountType accountType = mAccountTypeManager.getAccountType(mAccountTypeString,
    334                     mDataSet);
    335             final CharSequence dispLabel = accountType.getDisplayLabel(mContext);
    336             if (!TextUtils.isEmpty(dispLabel)) {
    337                 String groupSizeTemplateString = getResources().getQuantityString(
    338                         R.plurals.num_contacts_in_group, size);
    339                 groupSizeString = String.format(groupSizeTemplateString, size, dispLabel);
    340             } else {
    341                 String groupSizeTemplateString = getResources().getQuantityString(
    342                         R.plurals.group_list_num_contacts_in_group, size);
    343                 groupSizeString = String.format(groupSizeTemplateString, size);
    344             }
    345         }
    346 
    347         if (mGroupSize != null) {
    348             mGroupSize.setText(groupSizeString);
    349         } else {
    350             mListener.onGroupSizeUpdated(groupSizeString);
    351         }
    352     }
    353 
    354     /**
    355      * Once the account type, group source action, and group source URI have been determined
    356      * (based on the result from the {@link Loader}), then we can display this to the user in 1 of
    357      * 2 ways depending on screen size and orientation: either as a button in the action bar or as
    358      * a button in a static header on the page.
    359      * We also use isGroupMembershipEditable() of accountType to determine whether or not we should
    360      * display the Edit option in the Actionbar.
    361      */
    362     private void updateAccountType(final String accountTypeString, final String dataSet) {
    363         final AccountTypeManager manager = AccountTypeManager.getInstance(getActivity());
    364         final AccountType accountType =
    365                 manager.getAccountType(accountTypeString, dataSet);
    366 
    367         mIsMembershipEditable = accountType.isGroupMembershipEditable();
    368 
    369         // If the group action should be shown in the action bar, then pass the data to the
    370         // listener who will take care of setting up the view and click listener. There is nothing
    371         // else to be done by this {@link Fragment}.
    372         if (mShowGroupActionInActionBar) {
    373             mListener.onAccountTypeUpdated(accountTypeString, dataSet);
    374             return;
    375         }
    376 
    377         // Otherwise, if the {@link Fragment} needs to create and setup the button, then first
    378         // verify that there is a valid action.
    379         if (!TextUtils.isEmpty(accountType.getViewGroupActivity())) {
    380             if (mGroupSourceView == null) {
    381                 mGroupSourceView = GroupDetailDisplayUtils.getNewGroupSourceView(mContext);
    382                 // Figure out how to add the view to the fragment.
    383                 // If there is a static header with a container for the group source view, insert
    384                 // the view there.
    385                 if (mGroupSourceViewContainer != null) {
    386                     mGroupSourceViewContainer.addView(mGroupSourceView);
    387                 }
    388             }
    389 
    390             // Rebind the data since this action can change if the loader returns updated data
    391             mGroupSourceView.setVisibility(View.VISIBLE);
    392             GroupDetailDisplayUtils.bindGroupSourceView(mContext, mGroupSourceView,
    393                     accountTypeString, dataSet);
    394             mGroupSourceView.setOnClickListener(new OnClickListener() {
    395                 @Override
    396                 public void onClick(View v) {
    397                     final Uri uri = ContentUris.withAppendedId(Groups.CONTENT_URI, mGroupId);
    398                     final Intent intent = new Intent(Intent.ACTION_VIEW, uri);
    399                     intent.setClassName(accountType.syncAdapterPackageName,
    400                             accountType.getViewGroupActivity());
    401                     try {
    402                         ImplicitIntentsUtil.startActivityInApp(getActivity(), intent);
    403                     } catch (ActivityNotFoundException e) {
    404                         Log.e(TAG, "startActivity() failed: " + e);
    405                         Toast.makeText(getActivity(), R.string.missing_app,
    406                                 Toast.LENGTH_SHORT).show();
    407                     }
    408                 }
    409             });
    410         } else if (mGroupSourceView != null) {
    411             mGroupSourceView.setVisibility(View.GONE);
    412         }
    413     }
    414 
    415     @Override
    416     public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
    417             int totalItemCount) {
    418     }
    419 
    420     @Override
    421     public void onScrollStateChanged(AbsListView view, int scrollState) {
    422         if (scrollState == OnScrollListener.SCROLL_STATE_FLING) {
    423             mPhotoManager.pause();
    424         } else {
    425             mPhotoManager.resume();
    426         }
    427     }
    428 
    429     @Override
    430     public void onCreateOptionsMenu(Menu menu, final MenuInflater inflater) {
    431         inflater.inflate(R.menu.view_group, menu);
    432     }
    433 
    434     public boolean isOptionsMenuChanged() {
    435         return mOptionsMenuGroupDeletable != isGroupDeletable() &&
    436                 mOptionsMenuGroupEditable != isGroupEditableAndPresent();
    437     }
    438 
    439     public boolean isGroupDeletable() {
    440         return mGroupUri != null && !mIsReadOnly;
    441     }
    442 
    443     public boolean isGroupEditableAndPresent() {
    444         return mGroupUri != null && mIsMembershipEditable;
    445     }
    446 
    447     @Override
    448     public void onPrepareOptionsMenu(Menu menu) {
    449         mOptionsMenuGroupDeletable = isGroupDeletable() && isVisible();
    450         mOptionsMenuGroupEditable = isGroupEditableAndPresent() && isVisible();
    451 
    452         final MenuItem editMenu = menu.findItem(R.id.menu_edit_group);
    453         editMenu.setVisible(mOptionsMenuGroupEditable);
    454 
    455         final MenuItem deleteMenu = menu.findItem(R.id.menu_delete_group);
    456         deleteMenu.setVisible(mOptionsMenuGroupDeletable);
    457     }
    458 
    459     @Override
    460     public boolean onOptionsItemSelected(MenuItem item) {
    461         switch (item.getItemId()) {
    462             case R.id.menu_edit_group: {
    463                 if (mListener != null) mListener.onEditRequested(mGroupUri);
    464                 break;
    465             }
    466             case R.id.menu_delete_group: {
    467                 GroupDeletionDialogFragment.show(getFragmentManager(), mGroupId, mGroupName,
    468                         mCloseActivityAfterDelete);
    469                 return true;
    470             }
    471         }
    472         return false;
    473     }
    474 
    475     public void closeActivityAfterDelete(boolean closeActivity) {
    476         mCloseActivityAfterDelete = closeActivity;
    477     }
    478 
    479     public long getGroupId() {
    480         return mGroupId;
    481     }
    482 }
    483