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