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