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 17package com.android.contacts.group; 18 19import android.app.Activity; 20import android.app.Fragment; 21import android.app.LoaderManager; 22import android.app.LoaderManager.LoaderCallbacks; 23import android.content.ContentUris; 24import android.content.Context; 25import android.content.CursorLoader; 26import android.content.Intent; 27import android.content.Loader; 28import android.content.res.Resources; 29import android.database.Cursor; 30import android.graphics.Rect; 31import android.net.Uri; 32import android.os.Bundle; 33import android.provider.ContactsContract.Groups; 34import android.text.TextUtils; 35import android.util.Log; 36import android.view.LayoutInflater; 37import android.view.Menu; 38import android.view.MenuInflater; 39import android.view.MenuItem; 40import android.view.View; 41import android.view.View.OnClickListener; 42import android.view.ViewGroup; 43import android.widget.AbsListView; 44import android.widget.AbsListView.OnScrollListener; 45import android.widget.ListView; 46import android.widget.TextView; 47 48import com.android.contacts.ContactPhotoManager; 49import com.android.contacts.GroupMemberLoader; 50import com.android.contacts.GroupMetaDataLoader; 51import com.android.contacts.R; 52import com.android.contacts.interactions.GroupDeletionDialogFragment; 53import com.android.contacts.list.ContactTileAdapter; 54import com.android.contacts.list.ContactTileAdapter.DisplayType; 55import com.android.contacts.list.ContactTileView; 56import com.android.contacts.model.AccountTypeManager; 57import com.android.contacts.model.account.AccountType; 58 59/** 60 * Displays the details of a group and shows a list of actions possible for the group. 61 */ 62public class GroupDetailFragment extends Fragment implements OnScrollListener { 63 64 public static interface Listener { 65 /** 66 * The group title has been loaded 67 */ 68 public void onGroupTitleUpdated(String title); 69 70 /** 71 * The number of group members has been determined 72 */ 73 public void onGroupSizeUpdated(String size); 74 75 /** 76 * The account type and dataset have been determined. 77 */ 78 public void onAccountTypeUpdated(String accountTypeString, String dataSet); 79 80 /** 81 * User decided to go to Edit-Mode 82 */ 83 public void onEditRequested(Uri groupUri); 84 85 /** 86 * Contact is selected and should launch details page 87 */ 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 119 private boolean mShowGroupActionInActionBar; 120 private boolean mOptionsMenuGroupDeletable; 121 private boolean mOptionsMenuGroupPresent; 122 private boolean mCloseActivityAfterDelete; 123 124 public GroupDetailFragment() { 125 } 126 127 @Override 128 public void onAttach(Activity activity) { 129 super.onAttach(activity); 130 mContext = activity; 131 mAccountTypeManager = AccountTypeManager.getInstance(mContext); 132 133 Resources res = getResources(); 134 int columnCount = res.getInteger(R.integer.contact_tile_column_count); 135 136 mAdapter = new ContactTileAdapter(activity, mContactTileListener, columnCount, 137 DisplayType.GROUP_MEMBERS); 138 139 configurePhotoLoader(); 140 } 141 142 @Override 143 public void onDetach() { 144 super.onDetach(); 145 mContext = null; 146 } 147 148 @Override 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 164 public void loadGroup(Uri groupUri) { 165 mGroupUri= groupUri; 166 startGroupMetadataLoader(); 167 } 168 169 public void setQuickContact(boolean enableQuickContact) { 170 mAdapter.enableQuickContact(enableQuickContact); 171 } 172 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 187 public void setListener(Listener value) { 188 mListener = value; 189 } 190 191 public void setShowGroupSourceInActionBar(boolean show) { 192 mShowGroupActionInActionBar = show; 193 } 194 195 public Uri getGroupUri() { 196 return mGroupUri; 197 } 198 199 /** 200 * Start the loader to retrieve the metadata for this group. 201 */ 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 */ 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 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 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 */ 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 */ 343 private void updateAccountType(final String accountTypeString, final String dataSet) { 344 345 // If the group action should be shown in the action bar, then pass the data to the 346 // listener who will take care of setting up the view and click listener. There is nothing 347 // else to be done by this {@link Fragment}. 348 if (mShowGroupActionInActionBar) { 349 mListener.onAccountTypeUpdated(accountTypeString, dataSet); 350 return; 351 } 352 353 final AccountTypeManager manager = AccountTypeManager.getInstance(getActivity()); 354 final AccountType accountType = 355 manager.getAccountType(accountTypeString, dataSet); 356 357 // Otherwise, if the {@link Fragment} needs to create and setup the button, then first 358 // verify that there is a valid action. 359 if (!TextUtils.isEmpty(accountType.getViewGroupActivity())) { 360 if (mGroupSourceView == null) { 361 mGroupSourceView = GroupDetailDisplayUtils.getNewGroupSourceView(mContext); 362 // Figure out how to add the view to the fragment. 363 // If there is a static header with a container for the group source view, insert 364 // the view there. 365 if (mGroupSourceViewContainer != null) { 366 mGroupSourceViewContainer.addView(mGroupSourceView); 367 } 368 } 369 370 // Rebind the data since this action can change if the loader returns updated data 371 mGroupSourceView.setVisibility(View.VISIBLE); 372 GroupDetailDisplayUtils.bindGroupSourceView(mContext, mGroupSourceView, 373 accountTypeString, dataSet); 374 mGroupSourceView.setOnClickListener(new OnClickListener() { 375 @Override 376 public void onClick(View v) { 377 final Uri uri = ContentUris.withAppendedId(Groups.CONTENT_URI, mGroupId); 378 final Intent intent = new Intent(Intent.ACTION_VIEW, uri); 379 intent.setClassName(accountType.syncAdapterPackageName, 380 accountType.getViewGroupActivity()); 381 startActivity(intent); 382 } 383 }); 384 } else if (mGroupSourceView != null) { 385 mGroupSourceView.setVisibility(View.GONE); 386 } 387 } 388 389 @Override 390 public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, 391 int totalItemCount) { 392 } 393 394 @Override 395 public void onScrollStateChanged(AbsListView view, int scrollState) { 396 if (scrollState == OnScrollListener.SCROLL_STATE_FLING) { 397 mPhotoManager.pause(); 398 } else { 399 mPhotoManager.resume(); 400 } 401 } 402 403 @Override 404 public void onCreateOptionsMenu(Menu menu, final MenuInflater inflater) { 405 inflater.inflate(R.menu.view_group, menu); 406 } 407 408 public boolean isOptionsMenuChanged() { 409 return mOptionsMenuGroupDeletable != isGroupDeletable() && 410 mOptionsMenuGroupPresent != isGroupPresent(); 411 } 412 413 public boolean isGroupDeletable() { 414 return mGroupUri != null && !mIsReadOnly; 415 } 416 417 public boolean isGroupPresent() { 418 return mGroupUri != null; 419 } 420 421 @Override 422 public void onPrepareOptionsMenu(Menu menu) { 423 mOptionsMenuGroupDeletable = isGroupDeletable() && isVisible(); 424 mOptionsMenuGroupPresent = isGroupPresent() && isVisible(); 425 426 final MenuItem editMenu = menu.findItem(R.id.menu_edit_group); 427 editMenu.setVisible(mOptionsMenuGroupPresent); 428 429 final MenuItem deleteMenu = menu.findItem(R.id.menu_delete_group); 430 deleteMenu.setVisible(mOptionsMenuGroupDeletable); 431 } 432 433 @Override 434 public boolean onOptionsItemSelected(MenuItem item) { 435 switch (item.getItemId()) { 436 case R.id.menu_edit_group: { 437 if (mListener != null) mListener.onEditRequested(mGroupUri); 438 break; 439 } 440 case R.id.menu_delete_group: { 441 GroupDeletionDialogFragment.show(getFragmentManager(), mGroupId, mGroupName, 442 mCloseActivityAfterDelete); 443 return true; 444 } 445 } 446 return false; 447 } 448 449 public void closeActivityAfterDelete(boolean closeActivity) { 450 mCloseActivityAfterDelete = closeActivity; 451 } 452 453 public long getGroupId() { 454 return mGroupId; 455 } 456} 457