blob: 269ed0b4175b6bd3cd214e165d1427829aa81473 [file] [log] [blame]
Jeff Sharkeyd5c5b9a2009-06-21 19:46:04 -07001/*
2 * Copyright (C) 2009 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;
18
19import com.android.contacts.NotifyingAsyncQueryHandler.QueryCompleteListener;
20
21import android.app.ExpandableListActivity;
22import android.content.ContentResolver;
23import android.content.ContentUris;
24import android.content.ContentValues;
25import android.content.Context;
26import android.content.SharedPreferences;
27import android.content.SharedPreferences.Editor;
28import android.content.pm.PackageManager;
29import android.content.pm.PackageManager.NameNotFoundException;
30import android.content.res.Resources;
31import android.database.CharArrayBuffer;
32import android.database.ContentObserver;
33import android.database.Cursor;
34import android.database.DataSetObserver;
35import android.net.Uri;
36import android.os.Bundle;
37import android.os.Handler;
38import android.preference.PreferenceManager;
39import android.provider.ContactsContract.Groups;
40import android.provider.ContactsContract.GroupsColumns;
41import android.util.Log;
42import android.view.LayoutInflater;
43import android.view.View;
44import android.view.ViewGroup;
45import android.widget.AdapterView;
46import android.widget.BaseExpandableListAdapter;
47import android.widget.CheckBox;
48import android.widget.ExpandableListView;
49import android.widget.SectionIndexer;
50import android.widget.TextView;
51import android.widget.AdapterView.OnItemClickListener;
52
53import java.util.ArrayList;
54import java.util.HashMap;
55import java.util.Map;
56
57/**
58 * Shows a list of all available {@link Groups} available, letting the user
59 * select which ones they want to be visible.
60 */
61public final class DisplayGroupsActivity extends ExpandableListActivity implements
62 QueryCompleteListener, OnItemClickListener {
63 private static final String TAG = "DisplayGroupsActivity";
64
65 public interface Prefs {
66 public static final String DISPLAY_ALL = "display_all";
67 public static final boolean DISPLAY_ALL_DEFAULT = true;
68
69 public static final String DISPLAY_ONLY_PHONES = "only_phones";
70 public static final boolean DISPLAY_ONLY_PHONES_DEFAULT = true;
71
72 }
73
74 private ExpandableListView mList;
75 private DisplayGroupsAdapter mAdapter;
76
77 private SharedPreferences mPrefs;
78 private NotifyingAsyncQueryHandler mHandler;
79
80 private static final int QUERY_TOKEN = 42;
81
82 private View mHeaderAll;
83 private View mHeaderPhones;
84 private View mHeaderSeparator;
85
86 @Override
87 protected void onCreate(Bundle icicle) {
88 super.onCreate(icicle);
89 setContentView(android.R.layout.expandable_list_content);
90
91 mList = getExpandableListView();
92 mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
93
94 boolean displayAll = mPrefs.getBoolean(Prefs.DISPLAY_ALL, Prefs.DISPLAY_ALL_DEFAULT);
95 boolean displayOnlyPhones = mPrefs.getBoolean(Prefs.DISPLAY_ONLY_PHONES,
96 Prefs.DISPLAY_ONLY_PHONES_DEFAULT);
97
98 final LayoutInflater inflater = getLayoutInflater();
99
100 // Add the "All contacts" header modifier.
101 mHeaderAll = inflater.inflate(R.layout.display_header, mList, false);
102 mHeaderAll.setId(R.id.header_all);
103 {
104 CheckBox checkbox = (CheckBox)mHeaderAll.findViewById(android.R.id.checkbox);
105 TextView text1 = (TextView)mHeaderAll.findViewById(android.R.id.text1);
106 checkbox.setChecked(displayAll);
107 text1.setText(R.string.showAllGroups);
108 }
109 mList.addHeaderView(mHeaderAll, null, true);
110
111
112 // Add the "Only contacts with phones" header modifier.
113 mHeaderPhones = inflater.inflate(R.layout.display_header, mList, false);
114 mHeaderPhones.setId(R.id.header_phones);
115 {
116 CheckBox checkbox = (CheckBox)mHeaderPhones.findViewById(android.R.id.checkbox);
117 TextView text1 = (TextView)mHeaderPhones.findViewById(android.R.id.text1);
118 TextView text2 = (TextView)mHeaderPhones.findViewById(android.R.id.text2);
119 checkbox.setChecked(displayOnlyPhones);
120 text1.setText(R.string.showFilterPhones);
121 text2.setText(R.string.showFilterPhonesDescrip);
122 }
123 mList.addHeaderView(mHeaderPhones, null, true);
124
125
126 // Add the separator before showing the detailed group list.
127 mHeaderSeparator = inflater.inflate(R.layout.list_separator, mList, false);
128 {
129 TextView text1 = (TextView)mHeaderSeparator;
130 text1.setText(R.string.headerContactGroups);
131 }
132 mList.addHeaderView(mHeaderSeparator, null, false);
133
134
135 final TextView allContactsView = (TextView)mHeaderAll.findViewById(android.R.id.text2);
136
137 mAdapter = new DisplayGroupsAdapter(this);
138 mAdapter.setAllContactsView(allContactsView);
139
140 mAdapter.setEnabled(!displayAll);
141 mAdapter.setChildDescripWithPhones(displayOnlyPhones);
142
143 setListAdapter(mAdapter);
144
145 // Catch clicks on the header views
146 mList.setOnItemClickListener(this);
147
148 mHandler = new NotifyingAsyncQueryHandler(this, this);
149 startQuery();
150
151 }
152
153 @Override
154 protected void onRestart() {
155 super.onRestart();
156 startQuery();
157 }
158
159 @Override
160 protected void onStop() {
161 super.onStop();
162 mHandler.cancelOperation(QUERY_TOKEN);
163 }
164
165
166 private void startQuery() {
167 mHandler.cancelOperation(QUERY_TOKEN);
168 mHandler.startQuery(QUERY_TOKEN, null, Groups.CONTENT_SUMMARY_URI,
169 Projections.PROJ_SUMMARY, null, null, Projections.SORT_ORDER);
170 }
171
172 public void onQueryComplete(int token, Object cookie, Cursor cursor) {
173 mAdapter.changeCursor(cursor);
174
175 // Expand all data sources
176 final int groupCount = mAdapter.getGroupCount();
177 for (int i = 0; i < groupCount; i++) {
178 mList.expandGroup(i);
179 }
180 }
181
182 /**
183 * Handle any clicks on header views added to our {@link #mAdapter}, which
184 * are usually the global modifier checkboxes.
185 */
186 public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
187 final CheckBox checkbox = (CheckBox)view.findViewById(android.R.id.checkbox);
188 switch (view.getId()) {
189 case R.id.header_all: {
190 checkbox.toggle();
191 final boolean displayAll = checkbox.isChecked();
192
193 Editor editor = mPrefs.edit();
194 editor.putBoolean(Prefs.DISPLAY_ALL, displayAll);
195 editor.commit();
196
197 mAdapter.setEnabled(!displayAll);
198 mAdapter.notifyDataSetChanged();
199
200 break;
201 }
202 case R.id.header_phones: {
203 checkbox.toggle();
204 final boolean displayOnlyPhones = checkbox.isChecked();
205
206 Editor editor = mPrefs.edit();
207 editor.putBoolean(Prefs.DISPLAY_ONLY_PHONES, displayOnlyPhones);
208 editor.commit();
209
210 mAdapter.setChildDescripWithPhones(displayOnlyPhones);
211 mAdapter.notifyDataSetChanged();
212
213 break;
214 }
215 }
216 }
217
218 /**
219 * Handle any clicks on {@link ExpandableListAdapter} children, which
220 * usually mean toggling its visible state.
221 */
222 @Override
223 public boolean onChildClick(ExpandableListView parent, View v, int groupPosition,
224 int childPosition, long id) {
225 if (!mAdapter.isEnabled()) {
226 return false;
227 }
228
229 final CheckBox checkbox = (CheckBox)v.findViewById(android.R.id.checkbox);
230 checkbox.toggle();
231
232 // Build visibility update and send down to database
233 final ContentResolver resolver = getContentResolver();
234 final ContentValues values = new ContentValues();
235
236 values.put(Groups.GROUP_VISIBLE, checkbox.isChecked() ? 1 : 0);
237
238 final long groupId = mAdapter.getChildId(groupPosition, childPosition);
239 final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
240
241 resolver.update(groupUri, values, null, null);
242
243 return true;
244 }
245
246 /**
247 * Helper for obtaining {@link Resources} instances that are based in an
248 * external package. Maintains internal cache to remain fast.
249 */
250 private static class ExternalResources {
251 private Context mContext;
252 private HashMap<String, Context> mCache = new HashMap<String, Context>();
253
254 public ExternalResources(Context context) {
255 mContext = context;
256 }
257
258 private Context getPackageContext(String packageName) throws NameNotFoundException {
259 Context theirContext = mCache.get(packageName);
260 if (theirContext == null) {
261 theirContext = mContext.createPackageContext(packageName, 0);
262 mCache.put(packageName, theirContext);
263 }
264 return theirContext;
265 }
266
267 public Resources getResources(String packageName) throws NameNotFoundException {
268 return getPackageContext(packageName).getResources();
269 }
270
271 public CharSequence getText(String packageName, int stringRes)
272 throws NameNotFoundException {
273 return getResources(packageName).getText(stringRes);
274 }
275 }
276
277 /**
278 * Adapter that shows all display groups as returned by a {@link Cursor}
279 * over {@link Groups#CONTENT_SUMMARY_URI}, along with their current visible
280 * status. Splits groups into sections based on {@link Groups#PACKAGE}.
281 */
282 private static class DisplayGroupsAdapter extends BaseExpandableListAdapter {
283 private boolean mDataValid;
284 private Cursor mCursor;
285 private Context mContext;
286 private Resources mResources;
287 private ExternalResources mExternalRes;
288 private LayoutInflater mInflater;
289 private int mRowIDColumn;
290
291 private TextView mAllContactsView;
292
293 private boolean mEnabled = true;
294 private boolean mChildWithPhones = false;
295
296 private ContentObserver mContentObserver = new MyChangeObserver();
297 private DataSetObserver mDataSetObserver = new MyDataSetObserver();
298
299 /**
300 * A single group in our expandable list.
301 */
302 private static class Group {
303 public long packageId = -1;
304 public String packageName = null;
305 public int firstPos;
306 public int lastPos;
307 public CharSequence label;
308 }
309
310 /**
311 * Maintain a list of all groups that need to be displayed by this
312 * adapter, usually built by walking across a single {@link Cursor} and
313 * finding the {@link Groups#PACKAGE} boundaries.
314 */
315 private static final ArrayList<Group> mGroups = new ArrayList<Group>();
316
317 public DisplayGroupsAdapter(Context context) {
318 mContext = context;
319 mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
320 mResources = context.getResources();
321 mExternalRes = new ExternalResources(mContext);
322 }
323
324 /**
325 * In group descriptions, show the number of contacts with phone
326 * numbers, in addition to the total contacts.
327 */
328 public void setChildDescripWithPhones(boolean withPhones) {
329 mChildWithPhones = withPhones;
330 }
331
332 /**
333 * Set a {@link TextView} to be filled with the total number of contacts
334 * across all available groups.
335 */
336 public void setAllContactsView(TextView allContactsView) {
337 mAllContactsView = allContactsView;
338 }
339
340 /**
341 * Set the {@link View#setEnabled(boolean)} state of any views
342 * constructed by this adapter.
343 */
344 public void setEnabled(boolean enabled) {
345 mEnabled = enabled;
346 }
347
348 /**
349 * Returns the {@link View#setEnabled(boolean)} value being set for any
350 * children views of this adapter.
351 */
352 public boolean isEnabled() {
353 return mEnabled;
354 }
355
356 /**
357 * Used internally to build the {@link #mGroups} mapping. Call when you
358 * have a valid cursor and are ready to rebuild the mapping.
359 */
360 private void buildInternalMapping() {
361 final PackageManager pm = mContext.getPackageManager();
362 int totalContacts = 0;
363 Group group = null;
364
365 mGroups.clear();
366 mCursor.moveToPosition(-1);
367 while (mCursor.moveToNext()) {
368 final int position = mCursor.getPosition();
Jeff Sharkeyc6ad3ab2009-07-21 19:30:15 -0700369 final long packageId = mCursor.getLong(Projections.COL_ID);
Jeff Sharkeyd5c5b9a2009-06-21 19:46:04 -0700370 totalContacts += mCursor.getInt(Projections.COL_SUMMARY_COUNT);
371 if (group == null || packageId != group.packageId) {
372 group = new Group();
373 group.packageId = packageId;
Jeff Sharkeyc6ad3ab2009-07-21 19:30:15 -0700374 group.packageName = mCursor.getString(Projections.COL_RES_PACKAGE);
Jeff Sharkeyd5c5b9a2009-06-21 19:46:04 -0700375 group.firstPos = position;
376 group.label = group.packageName;
377
378 try {
379 group.label = pm.getApplicationInfo(group.packageName, 0).loadLabel(pm);
380 } catch (NameNotFoundException e) {
381 Log.w(TAG, "couldn't find label for package " + group.packageName);
382 }
383
384 mGroups.add(group);
385 }
386 group.lastPos = position;
387 }
388
389 if (mAllContactsView != null) {
390 mAllContactsView.setText(mResources.getQuantityString(R.plurals.groupDescrip,
391 totalContacts, totalContacts));
392 }
393
394 }
395
396 /**
397 * Map the given group and child position into a flattened position on
398 * our single {@link Cursor}.
399 */
400 public int getCursorPosition(int groupPosition, int childPosition) {
401 // The actual cursor position for a child is simply stepping from
402 // the first position for that group.
403 final Group group = mGroups.get(groupPosition);
404 final int position = group.firstPos + childPosition;
405 return position;
406 }
407
408 public boolean hasStableIds() {
409 return true;
410 }
411
412 public boolean isChildSelectable(int groupPosition, int childPosition) {
413 return true;
414 }
415
416 public Object getChild(int groupPosition, int childPosition) {
417 if (mDataValid && mCursor != null) {
418 final int position = getCursorPosition(groupPosition, childPosition);
419 mCursor.moveToPosition(position);
420 return mCursor;
421 } else {
422 return null;
423 }
424 }
425
426 public long getChildId(int groupPosition, int childPosition) {
427 if (mDataValid && mCursor != null) {
428 final int position = getCursorPosition(groupPosition, childPosition);
429 if (mCursor.moveToPosition(position)) {
430 return mCursor.getLong(mRowIDColumn);
431 } else {
432 return 0;
433 }
434 } else {
435 return 0;
436 }
437 }
438
439 public int getChildrenCount(int groupPosition) {
440 if (mDataValid && mCursor != null) {
441 final Group group = mGroups.get(groupPosition);
442 final int size = group.lastPos - group.firstPos + 1;
443 return size;
444 } else {
445 return 0;
446 }
447 }
448
449 public Object getGroup(int groupPosition) {
450 if (mDataValid && mCursor != null) {
451 return mGroups.get(groupPosition);
452 } else {
453 return null;
454 }
455 }
456
457 public int getGroupCount() {
458 if (mDataValid && mCursor != null) {
459 return mGroups.size();
460 } else {
461 return 0;
462 }
463 }
464
465 public long getGroupId(int groupPosition) {
466 if (mDataValid && mCursor != null) {
467 final Group group = mGroups.get(groupPosition);
468 return group.packageId;
469 } else {
470 return 0;
471 }
472 }
473
474 public View getGroupView(int groupPosition, boolean isExpanded, View convertView,
475 ViewGroup parent) {
476 if (!mDataValid) {
477 throw new IllegalStateException("called with invalid cursor");
478 }
479
480 final Group group = mGroups.get(groupPosition);
481
482 if (convertView == null) {
483 convertView = mInflater.inflate(R.layout.display_group, parent, false);
484 }
485
486 final TextView text1 = (TextView)convertView.findViewById(android.R.id.text1);
487
488 text1.setText(group.label);
489
490 convertView.setEnabled(mEnabled);
491
492 return convertView;
493 }
494
495 public View getChildView(int groupPosition, int childPosition, boolean isLastChild,
496 View convertView, ViewGroup parent) {
497 if (!mDataValid) {
498 throw new IllegalStateException("called with invalid cursor");
499 }
500
501 final int position = getCursorPosition(groupPosition, childPosition);
502 if (!mCursor.moveToPosition(position)) {
503 throw new IllegalStateException("couldn't move cursor to position " + position);
504 }
505
506 if (convertView == null) {
507 convertView = mInflater.inflate(R.layout.display_child, parent, false);
508 }
509
510 final TextView text1 = (TextView)convertView.findViewById(android.R.id.text1);
511 final TextView text2 = (TextView)convertView.findViewById(android.R.id.text2);
512 final CheckBox checkbox = (CheckBox)convertView.findViewById(android.R.id.checkbox);
513
514 final int count = mCursor.getInt(Projections.COL_SUMMARY_COUNT);
515 final int withPhones = mCursor.getInt(Projections.COL_SUMMARY_WITH_PHONES);
516 final int membersVisible = mCursor.getInt(Projections.COL_GROUP_VISIBLE);
517
518 // Read title, but override with string resource when present
519 CharSequence title = mCursor.getString(Projections.COL_TITLE);
Jeff Sharkeyc6ad3ab2009-07-21 19:30:15 -0700520 if (!mCursor.isNull(Projections.COL_RES_TITLE)) {
521 final String packageName = mCursor.getString(Projections.COL_RES_PACKAGE);
522 final int titleRes = mCursor.getInt(Projections.COL_RES_TITLE);
Jeff Sharkeyd5c5b9a2009-06-21 19:46:04 -0700523 try {
524 title = mExternalRes.getText(packageName, titleRes);
525 } catch (NameNotFoundException e) {
526 Log.w(TAG, "couldn't load group title resource for " + packageName);
527 }
528 }
529
530 final int descripString = mChildWithPhones ? R.plurals.groupDescripPhones
531 : R.plurals.groupDescrip;
532
533 text1.setText(title);
534 text2.setText(mResources.getQuantityString(descripString, count, count, withPhones));
535 checkbox.setChecked((membersVisible == 1));
536
537 convertView.setEnabled(mEnabled);
538
539 return convertView;
540 }
541
542 public void changeCursor(Cursor cursor) {
543 if (cursor == mCursor) {
544 return;
545 }
546 if (mCursor != null) {
547 mCursor.unregisterContentObserver(mContentObserver);
548 mCursor.unregisterDataSetObserver(mDataSetObserver);
549 mCursor.close();
550 }
551 mCursor = cursor;
552 if (cursor != null) {
553 cursor.registerContentObserver(mContentObserver);
554 cursor.registerDataSetObserver(mDataSetObserver);
555 mRowIDColumn = cursor.getColumnIndexOrThrow("_id");
556 mDataValid = true;
557 buildInternalMapping();
558 // notify the observers about the new cursor
559 notifyDataSetChanged();
560 } else {
561 mRowIDColumn = -1;
562 mDataValid = false;
563 // notify the observers about the lack of a data set
564 notifyDataSetInvalidated();
565 }
566 }
567
568 protected void onContentChanged() {
569 if (mCursor != null && !mCursor.isClosed()) {
570 mDataValid = mCursor.requery();
571 }
572 }
573
574 private class MyChangeObserver extends ContentObserver {
575 public MyChangeObserver() {
576 super(new Handler());
577 }
578
579 @Override
580 public boolean deliverSelfNotifications() {
581 return true;
582 }
583
584 @Override
585 public void onChange(boolean selfChange) {
586 onContentChanged();
587 }
588 }
589
590 private class MyDataSetObserver extends DataSetObserver {
591 @Override
592 public void onChanged() {
593 mDataValid = true;
594 notifyDataSetChanged();
595 }
596
597 @Override
598 public void onInvalidated() {
599 mDataValid = false;
600 notifyDataSetInvalidated();
601 }
602 }
603
604 }
605
606 /**
607 * Database projections used locally.
608 */
609 private interface Projections {
610
611 public static final String[] PROJ_SUMMARY = new String[] {
612 Groups._ID,
Jeff Sharkeyd5c5b9a2009-06-21 19:46:04 -0700613 Groups.TITLE,
Jeff Sharkeyc6ad3ab2009-07-21 19:30:15 -0700614 Groups.RES_PACKAGE,
615 Groups.TITLE_RES,
Jeff Sharkeyd5c5b9a2009-06-21 19:46:04 -0700616 Groups.GROUP_VISIBLE,
617 Groups.SUMMARY_COUNT,
618 Groups.SUMMARY_WITH_PHONES,
619 };
620
Jeff Sharkeyc6ad3ab2009-07-21 19:30:15 -0700621 public static final String SORT_ORDER = Groups.ACCOUNT_TYPE + " ASC, "
622 + Groups.ACCOUNT_NAME + " ASC";
Jeff Sharkeyd5c5b9a2009-06-21 19:46:04 -0700623
624 public static final int COL_ID = 0;
Jeff Sharkeyc6ad3ab2009-07-21 19:30:15 -0700625 public static final int COL_TITLE = 1;
626 public static final int COL_RES_PACKAGE = 2;
627 public static final int COL_RES_TITLE = 3;
628 public static final int COL_GROUP_VISIBLE = 4;
629 public static final int COL_SUMMARY_COUNT = 5;
630 public static final int COL_SUMMARY_WITH_PHONES = 6;
Jeff Sharkeyd5c5b9a2009-06-21 19:46:04 -0700631
632 }
633
634}