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 private static final String COLLATE_NOCASE = " COLLATE NOCASE";
48 private static final String IS_PRIMARY = "\"primary\"";
49 private static final String CALENDARS_ORDERBY = IS_PRIMARY + " DESC," + Calendars.DISPLAY_NAME +
51 private static final String ACCOUNT_SELECTION = Calendars._SYNC_ACCOUNT + "=?"
52 + " AND " + Calendars._SYNC_ACCOUNT_TYPE + "=?";
54 // The drawables used for the button to change the visible and sync states on a calendar
55 private static final int[] SYNC_VIS_BUTTON_RES = new int[] {
56 R.drawable.widget_show,
57 R.drawable.widget_sync,
61 private final LayoutInflater mInflater;
62 private final ContentResolver mResolver;
63 private final SelectCalendarsActivity mActivity;
64 private final View mView;
65 private final static Runnable mStopRefreshing = new Runnable() {
70 private Map<String, AuthenticatorDescription> mTypeToAuthDescription
71 = new HashMap<String, AuthenticatorDescription>();
72 protected AuthenticatorDescription[] mAuthDescs;
74 // These track changes to the visible (selected) and synced state of calendars
75 private Map<Long, Boolean[]> mCalendarChanges
76 = new HashMap<Long, Boolean[]>();
77 private Map<Long, Boolean[]> mCalendarInitialStates
78 = new HashMap<Long, Boolean[]>();
79 private static final int SELECTED_INDEX = 0;
80 private static final int SYNCED_INDEX = 1;
81 private static final int CHANGES_SIZE = 2;
83 // This is for keeping MatrixCursor copies so that we can requery in the background.
84 private static Map<String, Cursor> mChildrenCursors
85 = new HashMap<String, Cursor>();
87 private static AsyncCalendarsUpdater mCalendarsUpdater;
88 // This is to keep our update tokens separate from other tokens. Since we cancel old updates
89 // when a new update comes in, we'd like to leave a token space that won't be canceled.
90 private static final int MIN_UPDATE_TOKEN = 1000;
91 private static int mUpdateToken = MIN_UPDATE_TOKEN;
92 // How long to wait between requeries of the calendars to see if anything has changed.
93 private static final int REFRESH_DELAY = 5000;
94 // How long to keep refreshing for
95 private static final int REFRESH_DURATION = 60000;
96 private static boolean mRefresh = true;
97 private int mNumAccounts;
99 private static String syncedVisible;
100 private static String syncedNotVisible;
101 private static String notSyncedNotVisible;
103 // This is to keep track of whether or not multiple calendars have the same display name
104 private static HashMap<String, Boolean> mIsDuplicateName = new HashMap<String, Boolean>();
106 private static final String[] PROJECTION = new String[] {
108 Calendars._SYNC_ACCOUNT,
109 Calendars.OWNER_ACCOUNT,
110 Calendars.DISPLAY_NAME,
113 Calendars.SYNC_EVENTS,
114 "(" + Calendars._SYNC_ACCOUNT + "=" + Calendars.OWNER_ACCOUNT + ") AS " + IS_PRIMARY,
116 //Keep these in sync with the projection
117 private static final int ID_COLUMN = 0;
118 private static final int ACCOUNT_COLUMN = 1;
119 private static final int OWNER_COLUMN = 2;
120 private static final int NAME_COLUMN = 3;
121 private static final int COLOR_COLUMN = 4;
122 private static final int SELECTED_COLUMN = 5;
123 private static final int SYNCED_COLUMN = 6;
124 private static final int PRIMARY_COLUMN = 7;
126 private class AsyncCalendarsUpdater extends AsyncQueryHandler {
128 public AsyncCalendarsUpdater(ContentResolver cr) {
133 protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
138 Cursor currentCursor = mChildrenCursors.get(cookie);
139 // Check if the new cursor has the same content as our old cursor
140 if (currentCursor != null) {
141 if (Utils.compareCursors(currentCursor, cursor)) {
146 // If not then make a new matrix cursor for our Map
147 MatrixCursor newCursor = Utils.matrixCursorFromCursor(cursor);
149 // And update our list of duplicated names
150 Utils.checkForDuplicateNames(mIsDuplicateName, newCursor, NAME_COLUMN);
152 mChildrenCursors.put((String)cookie, newCursor);
154 setChildrenCursor(token, newCursor);
155 mActivity.startManagingCursor(newCursor);
156 } catch (NullPointerException e) {
157 Log.w(TAG, "Adapter expired, try again on the next query: " + e);
159 // Clean up our old cursor if we had one. We have to do this after setting the new
160 // cursor so that our view doesn't throw on an invalid cursor.
161 if (currentCursor != null) {
162 mActivity.stopManagingCursor(currentCursor);
163 currentCursor.close();
171 * Method for changing the sync/vis state when a calendar's button is pressed.
173 * This gets called when the MultiStateButton for a calendar is clicked. It cycles the sync/vis
174 * state for the associated calendar and saves a change of state to a hashmap. It also compares
175 * against the original value and removes any changes from the hashmap if this is back
176 * at its initial state.
178 public void onClick(View v) {
179 View view = (View)v.getTag();
180 long id = (Long)view.getTag();
181 Uri uri = ContentUris.withAppendedId(Calendars.CONTENT_URI, id);
182 String status = syncedNotVisible;
184 Boolean[] initialState = mCalendarInitialStates.get(id);
185 if (mCalendarChanges.containsKey(id)) {
186 change = mCalendarChanges.get(id);
188 change = new Boolean[CHANGES_SIZE];
189 change[SELECTED_INDEX] = initialState[SELECTED_INDEX];
190 change[SYNCED_INDEX] = initialState[SYNCED_INDEX];
191 mCalendarChanges.put(id, change);
194 if (change[SELECTED_INDEX]) {
195 change[SELECTED_INDEX] = false;
196 status = syncedNotVisible;
198 else if (change[SYNCED_INDEX]) {
199 change[SYNCED_INDEX] = false;
200 status = notSyncedNotVisible;
204 change[SYNCED_INDEX] = true;
205 change[SELECTED_INDEX] = true;
206 status = syncedVisible;
208 setText(view, R.id.status, status);
209 if (change[SELECTED_INDEX] == initialState[SELECTED_INDEX] &&
210 change[SYNCED_INDEX] == initialState[SYNCED_INDEX]) {
211 mCalendarChanges.remove(id);
215 public SelectCalendarsAdapter(Context context, Cursor cursor, SelectCalendarsActivity act) {
216 super(cursor, context);
217 syncedVisible = context.getString(R.string.synced_visible);
218 syncedNotVisible = context.getString(R.string.synced_not_visible);
219 notSyncedNotVisible = context.getString(R.string.not_synced_not_visible);
221 mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
222 mResolver = context.getContentResolver();
224 if (mCalendarsUpdater == null) {
225 mCalendarsUpdater = new AsyncCalendarsUpdater(mResolver);
228 mNumAccounts = cursor.getCount();
229 if(mNumAccounts == 0) {
230 //Should never happen since Calendar requires an account exist to use it.
231 Log.e(TAG, "SelectCalendarsAdapter: No accounts were returned!");
233 //Collect proper description for account types
234 mAuthDescs = AccountManager.get(context).getAuthenticatorTypes();
235 for (int i = 0; i < mAuthDescs.length; i++) {
236 mTypeToAuthDescription.put(mAuthDescs[i].type, mAuthDescs[i]);
238 mView = mActivity.getExpandableListView();
242 public void startRefreshStopDelay() {
244 mView.postDelayed(mStopRefreshing, REFRESH_DURATION);
247 public void cancelRefreshStopDelay() {
248 mView.removeCallbacks(mStopRefreshing);
252 * Write back the changes that have been made. The sync code will pick up any changes and
253 * do updates on its own.
255 public void doSaveAction() {
256 // Cancel the previous operation
257 mCalendarsUpdater.cancelOperation(mUpdateToken);
259 // This is to allow us to do queries and updates with the same AsyncQueryHandler without
260 // accidently canceling queries.
261 if(mUpdateToken < MIN_UPDATE_TOKEN) mUpdateToken = MIN_UPDATE_TOKEN;
263 Iterator<Long> changeKeys = mCalendarChanges.keySet().iterator();
264 while (changeKeys.hasNext()) {
265 long id = changeKeys.next();
266 Boolean[] change = mCalendarChanges.get(id);
267 int newSelected = change[SELECTED_INDEX] ? 1 : 0;
268 int newSynced = change[SYNCED_INDEX] ? 1 : 0;
270 Uri uri = ContentUris.withAppendedId(Calendars.CONTENT_URI, id);
271 ContentValues values = new ContentValues();
272 values.put(Calendars.SELECTED, newSelected);
273 values.put(Calendars.SYNC_EVENTS, newSynced);
274 mCalendarsUpdater.startUpdate(mUpdateToken, id, uri, values, null, null);
278 private static void setText(View view, int id, String text) {
279 if (TextUtils.isEmpty(text)) {
282 TextView textView = (TextView) view.findViewById(id);
283 textView.setText(text);
287 * Gets the label associated with a particular account type. If none found, return null.
288 * @param accountType the type of account
289 * @return a CharSequence for the label or null if one cannot be found.
291 protected CharSequence getLabelForType(final String accountType) {
292 CharSequence label = null;
293 if (mTypeToAuthDescription.containsKey(accountType)) {
295 AuthenticatorDescription desc = mTypeToAuthDescription.get(accountType);
296 Context authContext = mActivity.createPackageContext(desc.packageName, 0);
297 label = authContext.getResources().getText(desc.labelId);
298 } catch (PackageManager.NameNotFoundException e) {
299 Log.w(TAG, "No label for account type " + ", type " + accountType);
306 protected void bindChildView(View view, Context context, Cursor cursor, boolean isLastChild) {
307 String account = cursor.getString(ACCOUNT_COLUMN);
308 String status = notSyncedNotVisible;
310 int position = cursor.getPosition();
311 long id = cursor.getLong(ID_COLUMN);
313 // First see if the user has already changed the state of this calendar
314 Boolean[] initialState = mCalendarChanges.get(id);
315 // if we haven't already started making changes update the initial state in case it changed
316 if (initialState == null) {
317 initialState = new Boolean[CHANGES_SIZE];
318 initialState[SELECTED_INDEX] = cursor.getInt(SELECTED_COLUMN) == 1;
319 initialState[SYNCED_INDEX] = cursor.getInt(SYNCED_COLUMN) == 1;
320 mCalendarInitialStates.put(id, initialState);
323 if(initialState[SYNCED_INDEX]) {
324 if(initialState[SELECTED_INDEX]) {
325 status = syncedVisible;
328 status = syncedNotVisible;
333 view.findViewById(R.id.color)
334 .setBackgroundDrawable(Utils.getColorChip(cursor.getInt(COLOR_COLUMN)));
335 String name = cursor.getString(NAME_COLUMN);
336 String owner = cursor.getString(OWNER_COLUMN);
337 if (mIsDuplicateName.containsKey(name) && mIsDuplicateName.get(name) &&
338 !name.equalsIgnoreCase(owner)) {
339 name = new StringBuilder(name)
340 .append(Utils.OPEN_EMAIL_MARKER)
342 .append(Utils.CLOSE_EMAIL_MARKER)
345 setText(view, R.id.calendar, name);
346 setText(view, R.id.status, status);
347 MultiStateButton button = (MultiStateButton) view.findViewById(R.id.multiStateButton);
349 //Set up the listeners so a click on the button will change the state.
350 //The view already uses the onChildClick method in the activity.
353 button.setOnClickListener(this);
354 button.setButtonResources(SYNC_VIS_BUTTON_RES);
355 button.setState(state);
359 protected void bindGroupView(View view, Context context, Cursor cursor, boolean isExpanded) {
360 int accountColumn = cursor.getColumnIndexOrThrow(Calendars._SYNC_ACCOUNT);
361 int accountTypeColumn = cursor.getColumnIndexOrThrow(Calendars._SYNC_ACCOUNT_TYPE);
362 String account = cursor.getString(accountColumn);
363 String accountType = cursor.getString(accountTypeColumn);
364 setText(view, R.id.account, account);
365 setText(view, R.id.account_type, getLabelForType(accountType).toString());
369 protected Cursor getChildrenCursor(Cursor groupCursor) {
370 int accountColumn = groupCursor.getColumnIndexOrThrow(Calendars._SYNC_ACCOUNT);
371 int accountTypeColumn = groupCursor.getColumnIndexOrThrow(Calendars._SYNC_ACCOUNT_TYPE);
372 String account = groupCursor.getString(accountColumn);
373 String accountType = groupCursor.getString(accountTypeColumn);
374 //Get all the calendars for just this account.
375 Cursor childCursor = mChildrenCursors.get(account);
376 new RefreshCalendars(groupCursor.getPosition(), account, accountType).run();
381 protected View newChildView(Context context, Cursor cursor, boolean isLastChild,
383 return mInflater.inflate(R.layout.calendar_item, parent, false);
387 protected View newGroupView(Context context, Cursor cursor, boolean isExpanded,
389 return mInflater.inflate(R.layout.account_item, parent, false);
392 private class RefreshCalendars implements Runnable {
398 public RefreshCalendars(int token, String cookie, String accountType) {
401 mAccountType = accountType;
405 mCalendarsUpdater.cancelOperation(mToken);
406 // Set up a refresh for some point in the future if we haven't stopped updates yet
408 mView.postDelayed(new RefreshCalendars(mToken, mAccount, mAccountType),
411 mCalendarsUpdater.startQuery(mToken,
413 Calendars.CONTENT_URI, PROJECTION,
415 new String[] { mAccount, mAccountType } /*selectionArgs*/,