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