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