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