2 * Copyright (C) 2007 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.android.calendar;
19 import android.accounts.AccountManager;
20 import android.accounts.AuthenticatorDescription;
21 import android.content.AsyncQueryHandler;
22 import android.content.ContentResolver;
23 import android.content.ContentUris;
24 import android.content.ContentValues;
25 import android.content.Context;
26 import android.content.pm.PackageManager;
27 import android.database.Cursor;
28 import android.database.MatrixCursor;
29 import android.net.Uri;
30 import android.provider.Calendar.Calendars;
31 import android.text.TextUtils;
32 import android.util.Log;
33 import android.view.LayoutInflater;
34 import android.view.View;
35 import android.view.ViewGroup;
36 import android.widget.CursorTreeAdapter;
37 import android.widget.TextView;
39 import java.util.HashMap;
40 import java.util.Iterator;
43 public class SelectCalendarsAdapter extends CursorTreeAdapter implements View.OnClickListener {
45 private static final String TAG = "Calendar";
47 // The drawables used for the button to change the visible and sync states on a calendar
48 private static final int[] SYNC_VIS_BUTTON_RES = new int[] {
49 R.drawable.widget_show,
50 R.drawable.widget_sync,
54 private final LayoutInflater mInflater;
55 private final ContentResolver mResolver;
56 private final SelectCalendarsActivity mActivity;
57 private Map<String, AuthenticatorDescription> mTypeToAuthDescription
58 = new HashMap<String, AuthenticatorDescription>();
59 protected AuthenticatorDescription[] mAuthDescs;
61 // These track changes to the visible (selected) and synced state of calendars
62 private Map<Long, Boolean[]> mCalendarChanges
63 = new HashMap<Long, Boolean[]>();
64 private Map<Long, Boolean[]> mCalendarInitialStates
65 = new HashMap<Long, Boolean[]>();
66 private static final int SELECTED_INDEX = 0;
67 private static final int SYNCED_INDEX = 1;
68 private static final int CHANGES_SIZE = 2;
70 // This is for keeping MatrixCursor copies so that we can requery in the background.
71 private static Map<String, Cursor> mChildrenCursors
72 = new HashMap<String, Cursor>();
74 private static AsyncCalendarsUpdater mCalendarsUpdater;
75 // This is to keep our update tokens separate from other tokens. Since we cancel old updates
76 // when a new update comes in, we'd like to leave a token space that won't be canceled.
77 private static final int MIN_UPDATE_TOKEN = 1000;
78 private static int mUpdateToken = MIN_UPDATE_TOKEN;
80 private static String syncedVisible;
81 private static String syncedNotVisible;
82 private static String notSyncedNotVisible;
84 // This is to keep track of whether or not multiple calendars have the same display name
85 private static HashMap<String, Boolean> mIsDuplicateName = new HashMap<String, Boolean>();
87 private static final String[] PROJECTION = new String[] {
89 Calendars._SYNC_ACCOUNT,
90 Calendars.OWNER_ACCOUNT,
91 Calendars.DISPLAY_NAME,
96 //Keep these in sync with the projection
97 private static final int ID_COLUMN = 0;
98 private static final int ACCOUNT_COLUMN = 1;
99 private static final int OWNER_COLUMN = 2;
100 private static final int NAME_COLUMN = 3;
101 private static final int COLOR_COLUMN = 4;
102 private static final int SELECTED_COLUMN = 5;
103 private static final int SYNCED_COLUMN = 6;
105 private class AsyncCalendarsUpdater extends AsyncQueryHandler {
107 public AsyncCalendarsUpdater(ContentResolver cr) {
112 protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
117 Cursor currentCursor = mChildrenCursors.get(cookie);
118 // Check if the new cursor has the same content as our old cursor
119 if (currentCursor != null) {
120 if (compareCursors(currentCursor, cursor)) {
124 mActivity.stopManagingCursor(currentCursor);
125 currentCursor.close();
126 mChildrenCursors.remove(cookie);
129 // If not then make a new matrix cursor for our Map
130 MatrixCursor newCursor = matrixCursorFromCursor(cursor);
131 // And update our list of duplicated names
132 Utils.checkForDuplicateNames(mIsDuplicateName, cursor, NAME_COLUMN);
134 mChildrenCursors.put((String)cookie, newCursor);
136 setChildrenCursor(token, newCursor);
137 mActivity.startManagingCursor(newCursor);
138 } catch (NullPointerException e) {
139 Log.w(TAG, "Adapter expired, try again on the next query: " + e.getMessage());
145 * Compares two cursors to see if they contain the same data.
147 * @return Returns true of the cursors contain the same data and are not null, false
150 private boolean compareCursors(Cursor c1, Cursor c2) {
151 if(c1 == null || c2 == null) {
155 int numColumns = c1.getColumnCount();
156 if (numColumns != c2.getColumnCount()) {
160 if (c1.getCount() != c2.getCount()) {
164 c1.moveToPosition(-1);
165 c2.moveToPosition(-1);
166 while(c1.moveToNext() && c2.moveToNext()) {
167 for(int i = 0; i < numColumns; i++) {
168 if(!c1.getString(i).equals(c2.getString(i))) {
177 private MatrixCursor matrixCursorFromCursor(Cursor cursor) {
178 MatrixCursor newCursor = new MatrixCursor(cursor.getColumnNames());
179 int numColumns = cursor.getColumnCount();
180 int count = cursor.getCount();
181 String data[] = new String[numColumns];
182 cursor.moveToPosition(-1);
183 while(cursor.moveToNext()) {
184 for(int i = 0; i < numColumns; i++) {
185 data[i] = cursor.getString(i);
187 newCursor.addRow(data);
194 * Method for changing the sync/vis state when a calendar's button is pressed.
196 * This gets called when the MultiStateButton for a calendar is clicked. It cycles the sync/vis
197 * state for the associated calendar and saves a change of state to a hashmap. It also compares
198 * against the original value and removes any changes from the hashmap if this is back
199 * at its initial state.
201 public void onClick(View v) {
202 View view = (View)v.getTag();
203 long id = (Long)view.getTag();
204 Uri uri = ContentUris.withAppendedId(Calendars.CONTENT_URI, id);
205 String status = syncedNotVisible;
207 Boolean[] initialState = mCalendarInitialStates.get(id);
208 if (mCalendarChanges.containsKey(id)) {
209 change = mCalendarChanges.get(id);
211 change = new Boolean[CHANGES_SIZE];
212 change[SELECTED_INDEX] = initialState[SELECTED_INDEX];
213 change[SYNCED_INDEX] = initialState[SYNCED_INDEX];
214 mCalendarChanges.put(id, change);
217 if (change[SELECTED_INDEX]) {
218 change[SELECTED_INDEX] = false;
219 status = syncedNotVisible;
221 else if (change[SYNCED_INDEX]) {
222 change[SYNCED_INDEX] = false;
223 status = notSyncedNotVisible;
227 change[SYNCED_INDEX] = true;
228 change[SELECTED_INDEX] = true;
229 status = syncedVisible;
231 setText(view, R.id.status, status);
232 if (change[SELECTED_INDEX] == initialState[SELECTED_INDEX] &&
233 change[SYNCED_INDEX] == initialState[SYNCED_INDEX]) {
234 mCalendarChanges.remove(id);
238 public SelectCalendarsAdapter(Context context, Cursor cursor, SelectCalendarsActivity act) {
239 super(cursor, context);
240 syncedVisible = context.getString(R.string.synced_visible);
241 syncedNotVisible = context.getString(R.string.synced_not_visible);
242 notSyncedNotVisible = context.getString(R.string.not_synced_not_visible);
244 mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
245 mResolver = context.getContentResolver();
247 if (mCalendarsUpdater == null) {
248 mCalendarsUpdater = new AsyncCalendarsUpdater(mResolver);
250 if(cursor.getCount() == 0) {
251 //Should never happen since Calendar requires an account exist to use it.
252 Log.e(TAG, "SelectCalendarsAdapter: No accounts were returned!");
254 //Collect proper description for account types
255 mAuthDescs = AccountManager.get(context).getAuthenticatorTypes();
256 for (int i = 0; i < mAuthDescs.length; i++) {
257 mTypeToAuthDescription.put(mAuthDescs[i].type, mAuthDescs[i]);
262 * Write back the changes that have been made. The sync code will pick up any changes and
263 * do updates on its own.
265 public void doSaveAction() {
266 // Cancel the previous operation
267 mCalendarsUpdater.cancelOperation(mUpdateToken);
269 // This is to allow us to do queries and updates with the same AsyncQueryHandler without
270 // accidently canceling queries.
271 if(mUpdateToken < MIN_UPDATE_TOKEN) mUpdateToken = MIN_UPDATE_TOKEN;
273 Iterator<Long> changeKeys = mCalendarChanges.keySet().iterator();
274 while (changeKeys.hasNext()) {
275 long id = changeKeys.next();
276 Boolean[] change = mCalendarChanges.get(id);
277 int newSelected = change[SELECTED_INDEX] ? 1 : 0;
278 int newSynced = change[SYNCED_INDEX] ? 1 : 0;
280 Uri uri = ContentUris.withAppendedId(Calendars.CONTENT_URI, id);
281 ContentValues values = new ContentValues();
282 values.put(Calendars.SELECTED, newSelected);
283 values.put(Calendars.SYNC_EVENTS, newSynced);
284 mCalendarsUpdater.startUpdate(mUpdateToken, id, uri, values, null, null);
288 private static void setText(View view, int id, String text) {
289 if (TextUtils.isEmpty(text)) {
292 TextView textView = (TextView) view.findViewById(id);
293 textView.setText(text);
297 * Gets the label associated with a particular account type. If none found, return null.
298 * @param accountType the type of account
299 * @return a CharSequence for the label or null if one cannot be found.
301 protected CharSequence getLabelForType(final String accountType) {
302 CharSequence label = null;
303 if (mTypeToAuthDescription.containsKey(accountType)) {
305 AuthenticatorDescription desc = mTypeToAuthDescription.get(accountType);
306 Context authContext = mActivity.createPackageContext(desc.packageName, 0);
307 label = authContext.getResources().getText(desc.labelId);
308 } catch (PackageManager.NameNotFoundException e) {
309 Log.w(TAG, "No label for account type " + ", type " + accountType);
316 protected void bindChildView(View view, Context context, Cursor cursor, boolean isLastChild) {
317 String account = cursor.getString(ACCOUNT_COLUMN);
318 String status = notSyncedNotVisible;
320 int position = cursor.getPosition();
321 long id = cursor.getLong(ID_COLUMN);
323 // First see if the user has already changed the state of this calendar
324 Boolean[] initialState = mCalendarChanges.get(id);
325 // if not just grab the initial state
326 if (initialState == null) {
327 initialState = mCalendarInitialStates.get(id);
329 // and create a new initial state if we've never seen this calendar before.
330 if(initialState == null) {
331 initialState = new Boolean[CHANGES_SIZE];
332 initialState[SELECTED_INDEX] = cursor.getInt(SELECTED_COLUMN) == 1;
333 initialState[SYNCED_INDEX] = cursor.getInt(SYNCED_COLUMN) == 1;
334 mCalendarInitialStates.put(id, initialState);
337 if(initialState[SYNCED_INDEX]) {
338 if(initialState[SELECTED_INDEX]) {
339 status = syncedVisible;
342 status = syncedNotVisible;
347 view.findViewById(R.id.color)
348 .setBackgroundDrawable(Utils.getColorChip(cursor.getInt(COLOR_COLUMN)));
349 String name = cursor.getString(NAME_COLUMN);
350 String owner = cursor.getString(OWNER_COLUMN);
351 if (mIsDuplicateName.containsKey(name) && mIsDuplicateName.get(name) &&
352 !name.equalsIgnoreCase(owner)) {
353 name = new StringBuilder(name)
354 .append(Utils.OPEN_EMAIL_MARKER)
356 .append(Utils.CLOSE_EMAIL_MARKER)
359 setText(view, R.id.calendar, name);
360 setText(view, R.id.status, status);
361 MultiStateButton button = (MultiStateButton) view.findViewById(R.id.multiStateButton);
363 //Set up the listeners so a click on the button will change the state.
364 //The view already uses the onChildClick method in the activity.
367 button.setOnClickListener(this);
368 button.setButtonResources(SYNC_VIS_BUTTON_RES);
369 button.setState(state);
373 protected void bindGroupView(View view, Context context, Cursor cursor, boolean isExpanded) {
374 int accountColumn = cursor.getColumnIndexOrThrow(Calendars._SYNC_ACCOUNT);
375 int accountTypeColumn = cursor.getColumnIndexOrThrow(Calendars._SYNC_ACCOUNT_TYPE);
376 String account = cursor.getString(accountColumn);
377 String accountType = cursor.getString(accountTypeColumn);
378 setText(view, R.id.account, account);
379 setText(view, R.id.account_type, getLabelForType(accountType).toString());
383 protected Cursor getChildrenCursor(Cursor groupCursor) {
384 int accountColumn = groupCursor.getColumnIndexOrThrow(Calendars._SYNC_ACCOUNT);
385 String account = groupCursor.getString(accountColumn);
386 //Get all the calendars for just this account.
387 Cursor childCursor = mChildrenCursors.get(account);
388 mCalendarsUpdater.startQuery(groupCursor.getPosition(),
390 Calendars.CONTENT_URI, PROJECTION,
391 Calendars._SYNC_ACCOUNT + "=\"" + account + "\"" /*Selection*/,
392 null /* selectionArgs */,
393 Calendars.DISPLAY_NAME);
398 protected View newChildView(Context context, Cursor cursor, boolean isLastChild,
400 return mInflater.inflate(R.layout.calendar_item, parent, false);
404 protected View newGroupView(Context context, Cursor cursor, boolean isExpanded,
406 return mInflater.inflate(R.layout.account_item, parent, false);