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          */
onGroupTitleUpdated(String title)71         public void onGroupTitleUpdated(String title);
72 
73         /**
74          * The number of group members has been determined
75          */
onGroupSizeUpdated(String size)76         public void onGroupSizeUpdated(String size);
77 
78         /**
79          * The account type and dataset have been determined.
80          */
onAccountTypeUpdated(String accountTypeString, String dataSet)81         public void onAccountTypeUpdated(String accountTypeString, String dataSet);
82 
83         /**
84          * User decided to go to Edit-Mode
85          */
onEditRequested(Uri groupUri)86         public void onEditRequested(Uri groupUri);
87 
88         /**
89          * Contact is selected and should launch details page
90          */
onContactSelected(Uri contactUri)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 
GroupDetailFragment()128     public GroupDetailFragment() {
129     }
130 
131     @Override
onAttach(Activity activity)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
onDetach()146     public void onDetach() {
147         super.onDetach();
148         mContext = null;
149     }
150 
151     @Override
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState)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 
loadGroup(Uri groupUri)167     public void loadGroup(Uri groupUri) {
168         mGroupUri= groupUri;
169         startGroupMetadataLoader();
170     }
171 
setQuickContact(boolean enableQuickContact)172     public void setQuickContact(boolean enableQuickContact) {
173         mAdapter.enableQuickContact(enableQuickContact);
174     }
175 
configurePhotoLoader()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 
setListener(Listener value)190     public void setListener(Listener value) {
191         mListener = value;
192     }
193 
setShowGroupSourceInActionBar(boolean show)194     public void setShowGroupSourceInActionBar(boolean show) {
195         mShowGroupActionInActionBar = show;
196     }
197 
getGroupUri()198     public Uri getGroupUri() {
199         return mGroupUri;
200     }
201 
202     /**
203      * Start the loader to retrieve the metadata for this group.
204      */
startGroupMetadataLoader()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      */
startGroupMembersLoader()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 
bindGroupMetaData(Cursor cursor)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 
updateTitle(String title)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      */
updateSize(int size)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      */
updateAccountType(final String accountTypeString, final String dataSet)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
onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount)416     public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
417             int totalItemCount) {
418     }
419 
420     @Override
onScrollStateChanged(AbsListView view, int scrollState)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
onCreateOptionsMenu(Menu menu, final MenuInflater inflater)430     public void onCreateOptionsMenu(Menu menu, final MenuInflater inflater) {
431         inflater.inflate(R.menu.view_group, menu);
432     }
433 
isOptionsMenuChanged()434     public boolean isOptionsMenuChanged() {
435         return mOptionsMenuGroupDeletable != isGroupDeletable() &&
436                 mOptionsMenuGroupEditable != isGroupEditableAndPresent();
437     }
438 
isGroupDeletable()439     public boolean isGroupDeletable() {
440         return mGroupUri != null && !mIsReadOnly;
441     }
442 
isGroupEditableAndPresent()443     public boolean isGroupEditableAndPresent() {
444         return mGroupUri != null && mIsMembershipEditable;
445     }
446 
447     @Override
onPrepareOptionsMenu(Menu menu)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
onOptionsItemSelected(MenuItem item)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 
closeActivityAfterDelete(boolean closeActivity)475     public void closeActivityAfterDelete(boolean closeActivity) {
476         mCloseActivityAfterDelete = closeActivity;
477     }
478 
getGroupId()479     public long getGroupId() {
480         return mGroupId;
481     }
482 }
483