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 */ 70 public void onGroupTitleUpdated(String title); 71 72 /** 73 * The number of group members has been determined 74 */ 75 public void onGroupSizeUpdated(String size); 76 77 /** 78 * The account type and dataset have been determined. 79 */ 80 public void onAccountTypeUpdated(String accountTypeString, String dataSet); 81 82 /** 83 * User decided to go to Edit-Mode 84 */ 85 public void onEditRequested(Uri groupUri); 86 87 /** 88 * Contact is selected and should launch details page 89 */ 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 127 public GroupDetailFragment() { 128 } 129 130 @Override 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 145 public void onDetach() { 146 super.onDetach(); 147 mContext = null; 148 } 149 150 @Override 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 166 public void loadGroup(Uri groupUri) { 167 mGroupUri= groupUri; 168 startGroupMetadataLoader(); 169 } 170 171 public void setQuickContact(boolean enableQuickContact) { 172 mAdapter.enableQuickContact(enableQuickContact); 173 } 174 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 189 public void setListener(Listener value) { 190 mListener = value; 191 } 192 193 public void setShowGroupSourceInActionBar(boolean show) { 194 mShowGroupActionInActionBar = show; 195 } 196 197 public Uri getGroupUri() { 198 return mGroupUri; 199 } 200 201 /** 202 * Start the loader to retrieve the metadata for this group. 203 */ 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 */ 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 data.moveToPosition(-1); 249 if (data.moveToNext()) { 250 boolean deleted = data.getInt(GroupMetaDataLoader.DELETED) == 1; 251 if (!deleted) { 252 bindGroupMetaData(data); 253 254 // Retrieve the list of members 255 startGroupMembersLoader(); 256 return; 257 } 258 } 259 updateSize(-1); 260 updateTitle(null); 261 } 262 263 @Override 264 public void onLoaderReset(Loader<Cursor> loader) {} 265 }; 266 267 /** 268 * The listener for the group members list loader 269 */ 270 private final LoaderManager.LoaderCallbacks<Cursor> mGroupMemberListLoaderListener = 271 new LoaderCallbacks<Cursor>() { 272 273 @Override 274 public CursorLoader onCreateLoader(int id, Bundle args) { 275 return GroupMemberLoader.constructLoaderForGroupDetailQuery(mContext, mGroupId); 276 } 277 278 @Override 279 public void onLoadFinished(Loader<Cursor> loader, Cursor data) { 280 updateSize(data.getCount()); 281 mAdapter.setContactCursor(data); 282 mMemberListView.setEmptyView(mEmptyView); 283 } 284 285 @Override 286 public void onLoaderReset(Loader<Cursor> loader) {} 287 }; 288 289 private void bindGroupMetaData(Cursor cursor) { 290 cursor.moveToPosition(-1); 291 if (cursor.moveToNext()) { 292 mAccountTypeString = cursor.getString(GroupMetaDataLoader.ACCOUNT_TYPE); 293 mDataSet = cursor.getString(GroupMetaDataLoader.DATA_SET); 294 mGroupId = cursor.getLong(GroupMetaDataLoader.GROUP_ID); 295 mGroupName = cursor.getString(GroupMetaDataLoader.TITLE); 296 mIsReadOnly = cursor.getInt(GroupMetaDataLoader.IS_READ_ONLY) == 1; 297 updateTitle(mGroupName); 298 // Must call invalidate so that the option menu will get updated 299 getActivity().invalidateOptionsMenu (); 300 301 final String accountTypeString = cursor.getString(GroupMetaDataLoader.ACCOUNT_TYPE); 302 final String dataSet = cursor.getString(GroupMetaDataLoader.DATA_SET); 303 updateAccountType(accountTypeString, dataSet); 304 } 305 } 306 307 private void updateTitle(String title) { 308 if (mGroupTitle != null) { 309 mGroupTitle.setText(title); 310 } else { 311 mListener.onGroupTitleUpdated(title); 312 } 313 } 314 315 /** 316 * Display the count of the number of group members. 317 * @param size of the group (can be -1 if no size could be determined) 318 */ 319 private void updateSize(int size) { 320 String groupSizeString; 321 if (size == -1) { 322 groupSizeString = null; 323 } else { 324 AccountType accountType = mAccountTypeManager.getAccountType(mAccountTypeString, 325 mDataSet); 326 final CharSequence dispLabel = accountType.getDisplayLabel(mContext); 327 if (!TextUtils.isEmpty(dispLabel)) { 328 String groupSizeTemplateString = getResources().getQuantityString( 329 R.plurals.num_contacts_in_group, size); 330 groupSizeString = String.format(groupSizeTemplateString, size, dispLabel); 331 } else { 332 String groupSizeTemplateString = getResources().getQuantityString( 333 R.plurals.group_list_num_contacts_in_group, size); 334 groupSizeString = String.format(groupSizeTemplateString, size); 335 } 336 } 337 338 if (mGroupSize != null) { 339 mGroupSize.setText(groupSizeString); 340 } else { 341 mListener.onGroupSizeUpdated(groupSizeString); 342 } 343 } 344 345 /** 346 * Once the account type, group source action, and group source URI have been determined 347 * (based on the result from the {@link Loader}), then we can display this to the user in 1 of 348 * 2 ways depending on screen size and orientation: either as a button in the action bar or as 349 * a button in a static header on the page. 350 * We also use isGroupMembershipEditable() of accountType to determine whether or not we should 351 * display the Edit option in the Actionbar. 352 */ 353 private void updateAccountType(final String accountTypeString, final String dataSet) { 354 final AccountTypeManager manager = AccountTypeManager.getInstance(getActivity()); 355 final AccountType accountType = 356 manager.getAccountType(accountTypeString, dataSet); 357 358 mIsMembershipEditable = accountType.isGroupMembershipEditable(); 359 360 // If the group action should be shown in the action bar, then pass the data to the 361 // listener who will take care of setting up the view and click listener. There is nothing 362 // else to be done by this {@link Fragment}. 363 if (mShowGroupActionInActionBar) { 364 mListener.onAccountTypeUpdated(accountTypeString, dataSet); 365 return; 366 } 367 368 // Otherwise, if the {@link Fragment} needs to create and setup the button, then first 369 // verify that there is a valid action. 370 if (!TextUtils.isEmpty(accountType.getViewGroupActivity())) { 371 if (mGroupSourceView == null) { 372 mGroupSourceView = GroupDetailDisplayUtils.getNewGroupSourceView(mContext); 373 // Figure out how to add the view to the fragment. 374 // If there is a static header with a container for the group source view, insert 375 // the view there. 376 if (mGroupSourceViewContainer != null) { 377 mGroupSourceViewContainer.addView(mGroupSourceView); 378 } 379 } 380 381 // Rebind the data since this action can change if the loader returns updated data 382 mGroupSourceView.setVisibility(View.VISIBLE); 383 GroupDetailDisplayUtils.bindGroupSourceView(mContext, mGroupSourceView, 384 accountTypeString, dataSet); 385 mGroupSourceView.setOnClickListener(new OnClickListener() { 386 @Override 387 public void onClick(View v) { 388 final Uri uri = ContentUris.withAppendedId(Groups.CONTENT_URI, mGroupId); 389 final Intent intent = new Intent(Intent.ACTION_VIEW, uri); 390 intent.setClassName(accountType.syncAdapterPackageName, 391 accountType.getViewGroupActivity()); 392 try { 393 startActivity(intent); 394 } catch (ActivityNotFoundException e) { 395 Log.e(TAG, "startActivity() failed: " + e); 396 Toast.makeText(getActivity(), R.string.missing_app, 397 Toast.LENGTH_SHORT).show(); 398 } 399 } 400 }); 401 } else if (mGroupSourceView != null) { 402 mGroupSourceView.setVisibility(View.GONE); 403 } 404 } 405 406 @Override 407 public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, 408 int totalItemCount) { 409 } 410 411 @Override 412 public void onScrollStateChanged(AbsListView view, int scrollState) { 413 if (scrollState == OnScrollListener.SCROLL_STATE_FLING) { 414 mPhotoManager.pause(); 415 } else { 416 mPhotoManager.resume(); 417 } 418 } 419 420 @Override 421 public void onCreateOptionsMenu(Menu menu, final MenuInflater inflater) { 422 inflater.inflate(R.menu.view_group, menu); 423 } 424 425 public boolean isOptionsMenuChanged() { 426 return mOptionsMenuGroupDeletable != isGroupDeletable() && 427 mOptionsMenuGroupEditable != isGroupEditableAndPresent(); 428 } 429 430 public boolean isGroupDeletable() { 431 return mGroupUri != null && !mIsReadOnly; 432 } 433 434 public boolean isGroupEditableAndPresent() { 435 return mGroupUri != null && mIsMembershipEditable; 436 } 437 438 @Override 439 public void onPrepareOptionsMenu(Menu menu) { 440 mOptionsMenuGroupDeletable = isGroupDeletable() && isVisible(); 441 mOptionsMenuGroupEditable = isGroupEditableAndPresent() && isVisible(); 442 443 final MenuItem editMenu = menu.findItem(R.id.menu_edit_group); 444 editMenu.setVisible(mOptionsMenuGroupEditable); 445 446 final MenuItem deleteMenu = menu.findItem(R.id.menu_delete_group); 447 deleteMenu.setVisible(mOptionsMenuGroupDeletable); 448 } 449 450 @Override 451 public boolean onOptionsItemSelected(MenuItem item) { 452 switch (item.getItemId()) { 453 case R.id.menu_edit_group: { 454 if (mListener != null) mListener.onEditRequested(mGroupUri); 455 break; 456 } 457 case R.id.menu_delete_group: { 458 GroupDeletionDialogFragment.show(getFragmentManager(), mGroupId, mGroupName, 459 mCloseActivityAfterDelete); 460 return true; 461 } 462 } 463 return false; 464 } 465 466 public void closeActivityAfterDelete(boolean closeActivity) { 467 mCloseActivityAfterDelete = closeActivity; 468 } 469 470 public long getGroupId() { 471 return mGroupId; 472 } 473 } 474