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 java.util.HashMap;
20 import java.util.Iterator;
23 import android.accounts.AccountManager;
24 import android.accounts.AuthenticatorDescription;
25 import android.content.AsyncQueryHandler;
26 import android.content.ContentResolver;
27 import android.content.ContentUris;
28 import android.content.ContentValues;
29 import android.content.Context;
30 import android.content.pm.PackageManager;
31 import android.database.Cursor;
32 import android.database.MatrixCursor;
33 import android.graphics.drawable.Drawable;
34 import android.graphics.drawable.GradientDrawable;
35 import android.net.Uri;
36 import android.provider.Calendar.Calendars;
37 import android.text.TextUtils;
38 import android.util.Log;
39 import android.view.LayoutInflater;
40 import android.view.View;
41 import android.view.ViewGroup;
42 import android.widget.CursorTreeAdapter;
43 import android.widget.TextView;
45 public class SelectCalendarsAdapter extends CursorTreeAdapter implements View.OnClickListener {
47 private static final int CLEAR_ALPHA_MASK = 0x00FFFFFF;
48 private static final int HIGH_ALPHA = 255 << 24;
49 private static final int MED_ALPHA = 180 << 24;
50 private static final int LOW_ALPHA = 150 << 24;
52 /* The corner should be rounded on the top right and bottom right */
53 private static final float[] CORNERS = new float[] {0, 0, 5, 5, 5, 5, 0, 0};
55 private static final String TAG = "Calendar";
57 // The drawables used for the button to change the visible and sync states on a calendar
58 private static final int[] SYNC_VIS_BUTTON_RES = new int[] {
59 R.drawable.widget_show,
60 R.drawable.widget_sync,
64 private final LayoutInflater mInflater;
65 private final ContentResolver mResolver;
66 private final SelectCalendarsActivity mActivity;
67 private Map<String, AuthenticatorDescription> mTypeToAuthDescription
68 = new HashMap<String, AuthenticatorDescription>();
69 protected AuthenticatorDescription[] mAuthDescs;
71 // These track changes to the visible (selected) and synced state of calendars
72 private Map<Long, Boolean[]> mCalendarChanges
73 = new HashMap<Long, Boolean[]>();
74 private Map<Long, Boolean[]> mCalendarInitialStates
75 = new HashMap<Long, Boolean[]>();
76 private static final int SELECTED_INDEX = 0;
77 private static final int SYNCED_INDEX = 1;
78 private static final int CHANGES_SIZE = 2;
80 // This is for keeping MatrixCursor copies so that we can requery in the background.
81 private static Map<String, Cursor> mChildrenCursors
82 = new HashMap<String, Cursor>();
84 private static AsyncCalendarsUpdater mCalendarsUpdater;
85 // This is to keep our update tokens separate from other tokens. Since we cancel old updates
86 // when a new update comes in, we'd like to leave a token space that won't be canceled.
87 private static final int MIN_UPDATE_TOKEN = 1000;
88 private static int mUpdateToken = MIN_UPDATE_TOKEN;
90 private static String syncedVisible;
91 private static String syncedNotVisible;
92 private static String notSyncedNotVisible;
94 private static final String[] PROJECTION = new String[] {
96 Calendars._SYNC_ACCOUNT,
97 Calendars.DISPLAY_NAME,
100 Calendars.SYNC_EVENTS
103 //Keep these in sync with the projection
104 private static final int ID_COLUMN = 0;
105 private static final int ACCOUNT_COLUMN = 1;
106 private static final int NAME_COLUMN = 2;
107 private static final int COLOR_COLUMN = 3;
108 private static final int SELECTED_COLUMN = 4;
109 private static final int SYNCED_COLUMN = 5;
111 private class AsyncCalendarsUpdater extends AsyncQueryHandler {
113 public AsyncCalendarsUpdater(ContentResolver cr) {
118 protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
123 Cursor currentCursor = mChildrenCursors.get(cookie);
124 // Check if the new cursor has the same content as our old cursor
125 if (currentCursor != null) {
126 if (compareCursors(currentCursor, cursor)) {
130 // If not then make a new matrix cursor for our Map
131 MatrixCursor newCursor = matrixCursorFromCursor(cursor);
132 mChildrenCursors.put((String)cookie, newCursor);
134 setChildrenCursor(token, newCursor);
135 } catch (NullPointerException e) {
136 Log.w(TAG, "Adapter expired, try again on the next query: " + e.getMessage());
141 * Compares two cursors to see if they contain the same data.
143 private boolean compareCursors(Cursor c1, Cursor c2) {
144 if(c1 == null || c2 == null) {
148 int numColumns = c1.getColumnCount();
149 if (numColumns != c2.getColumnCount()) {
153 if (c1.getCount() != c2.getCount()) {
157 c1.moveToPosition(-1);
158 c2.moveToPosition(-1);
159 while(c1.moveToNext() && c2.moveToNext()) {
160 for(int i = 0; i < numColumns; i++) {
161 if(!c1.getString(i).equals(c2.getString(i))) {
170 private MatrixCursor matrixCursorFromCursor(Cursor cursor) {
171 MatrixCursor newCursor = new MatrixCursor(cursor.getColumnNames());
172 int numColumns = cursor.getColumnCount();
173 int count = cursor.getCount();
174 String data[] = new String[numColumns];
175 cursor.moveToPosition(-1);
176 while(cursor.moveToNext()) {
177 for(int i = 0; i < numColumns; i++) {
178 data[i] = cursor.getString(i);
180 newCursor.addRow(data);
187 * Method for changing the sync/vis state when a calendar's button is pressed.
189 * This gets called when the MultiStateButton for a calendar is clicked. It cycles the sync/vis
190 * state for the associated calendar and saves a change of state to a hashmap. It also compares
191 * against the original value and removes any changes from the hashmap if this is back
192 * at its initial state.
194 public void onClick(View v) {
195 View view = (View)v.getTag();
196 long id = (Long)view.getTag();
197 Uri uri = ContentUris.withAppendedId(Calendars.CONTENT_URI, id);
198 String status = syncedNotVisible;
200 Boolean[] initialState = mCalendarInitialStates.get(id);
201 if (mCalendarChanges.containsKey(id)) {
202 change = mCalendarChanges.get(id);
204 change = new Boolean[CHANGES_SIZE];
205 change[SELECTED_INDEX] = initialState[SELECTED_INDEX];
206 change[SYNCED_INDEX] = initialState[SYNCED_INDEX];
207 mCalendarChanges.put(id, change);
210 if (change[SELECTED_INDEX]) {
211 change[SELECTED_INDEX] = false;
212 status = syncedNotVisible;
214 else if (change[SYNCED_INDEX]) {
215 change[SYNCED_INDEX] = false;
216 status = notSyncedNotVisible;
220 change[SYNCED_INDEX] = true;
221 change[SELECTED_INDEX] = true;
222 status = syncedVisible;
224 setText(view, R.id.status, status);
225 if (change[SELECTED_INDEX] == initialState[SELECTED_INDEX] &&
226 change[SYNCED_INDEX] == initialState[SYNCED_INDEX]) {
227 mCalendarChanges.remove(id);
231 public SelectCalendarsAdapter(Context context, Cursor cursor, SelectCalendarsActivity act) {
232 super(cursor, context);
233 syncedVisible = context.getString(R.string.synced_visible);
234 syncedNotVisible = context.getString(R.string.synced_not_visible);
235 notSyncedNotVisible = context.getString(R.string.not_synced_not_visible);
237 mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
238 mResolver = context.getContentResolver();
240 if (mCalendarsUpdater == null) {
241 mCalendarsUpdater = new AsyncCalendarsUpdater(mResolver);
243 if(cursor.getCount() == 0) {
244 //Should never happen since Calendar requires an account exist to use it.
245 Log.e(TAG, "SelectCalendarsAdapter: No accounts were returned!");
247 //Collect proper description for account types
248 mAuthDescs = AccountManager.get(context).getAuthenticatorTypes();
249 for (int i = 0; i < mAuthDescs.length; i++) {
250 mTypeToAuthDescription.put(mAuthDescs[i].type, mAuthDescs[i]);
255 * Write back the changes that have been made. The sync code will pick up any changes and
256 * do updates on its own.
258 public void doSaveAction() {
259 // Cancel the previous operation
260 mCalendarsUpdater.cancelOperation(mUpdateToken);
262 // This is to allow us to do queries and updates with the same AsyncQueryHandler without
263 // accidently canceling queries.
264 if(mUpdateToken < MIN_UPDATE_TOKEN) mUpdateToken = MIN_UPDATE_TOKEN;
266 Iterator<Long> changeKeys = mCalendarChanges.keySet().iterator();
267 while (changeKeys.hasNext()) {
268 long id = changeKeys.next();
269 Boolean[] change = mCalendarChanges.get(id);
270 int newSelected = change[SELECTED_INDEX] ? 1 : 0;
271 int newSynced = change[SYNCED_INDEX] ? 1 : 0;
273 Uri uri = ContentUris.withAppendedId(Calendars.CONTENT_URI, id);
274 ContentValues values = new ContentValues();
275 values.put(Calendars.SELECTED, newSelected);
276 values.put(Calendars.SYNC_EVENTS, newSynced);
277 mCalendarsUpdater.startUpdate(mUpdateToken, id, uri, values, null, null);
281 private static void setText(View view, int id, String text) {
282 if (TextUtils.isEmpty(text)) {
285 TextView textView = (TextView) view.findViewById(id);
286 textView.setText(text);
290 * Gets the label associated with a particular account type. If none found, return null.
291 * @param accountType the type of account
292 * @return a CharSequence for the label or null if one cannot be found.
294 protected CharSequence getLabelForType(final String accountType) {
295 CharSequence label = null;
296 if (mTypeToAuthDescription.containsKey(accountType)) {
298 AuthenticatorDescription desc = mTypeToAuthDescription.get(accountType);
299 Context authContext = mActivity.createPackageContext(desc.packageName, 0);
300 label = authContext.getResources().getText(desc.labelId);
301 } catch (PackageManager.NameNotFoundException e) {
302 Log.w(TAG, "No label for account type " + ", type " + accountType);
308 private Drawable getColorChip(int color) {
311 * We want the color chip to have a nice gradient using
312 * the color of the calendar. To do this we use a GradientDrawable.
313 * The color supplied has an alpha of FF so we first do:
315 * to clear the alpha. Then we add our alpha to it.
316 * We use 3 colors to get a step effect where it starts off very
317 * light and quickly becomes dark and then a slow transition to
320 color &= CLEAR_ALPHA_MASK;
321 int startColor = color | HIGH_ALPHA;
322 int middleColor = color | MED_ALPHA;
323 int endColor = color | LOW_ALPHA;
324 int[] colors = new int[] {startColor, middleColor, endColor};
325 GradientDrawable d = new GradientDrawable(GradientDrawable.Orientation.LEFT_RIGHT, colors);
326 d.setCornerRadii(CORNERS);
331 protected void bindChildView(View view, Context context, Cursor cursor, boolean isLastChild) {
332 String account = cursor.getString(ACCOUNT_COLUMN);
333 String status = notSyncedNotVisible;
335 int position = cursor.getPosition();
336 long id = cursor.getLong(ID_COLUMN);
338 // First see if the user has already changed the state of this calendar
339 Boolean[] initialState = mCalendarChanges.get(id);
340 // if not just grab the initial state
341 if (initialState == null) {
342 initialState = mCalendarInitialStates.get(id);
344 // and create a new initial state if we've never seen this calendar before.
345 if(initialState == null) {
346 initialState = new Boolean[CHANGES_SIZE];
347 initialState[SELECTED_INDEX] = cursor.getInt(SELECTED_COLUMN) == 1;
348 initialState[SYNCED_INDEX] = cursor.getInt(SYNCED_COLUMN) == 1;
349 mCalendarInitialStates.put(id, initialState);
352 if(initialState[SYNCED_INDEX]) {
353 if(initialState[SELECTED_INDEX]) {
354 status = syncedVisible;
357 status = syncedNotVisible;
362 view.findViewById(R.id.color)
363 .setBackgroundDrawable(getColorChip(cursor.getInt(COLOR_COLUMN)));
364 setText(view, R.id.calendar, cursor.getString(NAME_COLUMN));
365 setText(view, R.id.status, status);
366 MultiStateButton button = (MultiStateButton) view.findViewById(R.id.multiStateButton);
368 //Set up the listeners so a click on the button will change the state.
369 //The view already uses the onChildClick method in the activity.
372 button.setOnClickListener(this);
373 button.setButtonResources(SYNC_VIS_BUTTON_RES);
374 button.setState(state);
378 protected void bindGroupView(View view, Context context, Cursor cursor, boolean isExpanded) {
379 int accountColumn = cursor.getColumnIndexOrThrow(Calendars._SYNC_ACCOUNT);
380 int accountTypeColumn = cursor.getColumnIndexOrThrow(Calendars._SYNC_ACCOUNT_TYPE);
381 String account = cursor.getString(accountColumn);
382 String accountType = cursor.getString(accountTypeColumn);
383 setText(view, R.id.account, account);
384 setText(view, R.id.account_type, getLabelForType(accountType).toString());
388 protected Cursor getChildrenCursor(Cursor groupCursor) {
389 int accountColumn = groupCursor.getColumnIndexOrThrow(Calendars._SYNC_ACCOUNT);
390 String account = groupCursor.getString(accountColumn);
391 //Get all the calendars for just this account.
392 Cursor childCursor = mChildrenCursors.get(account);
393 mCalendarsUpdater.startQuery(groupCursor.getPosition(),
395 Calendars.CONTENT_URI, PROJECTION,
396 Calendars._SYNC_ACCOUNT + "=\"" + account + "\"" /*Selection*/,
397 null /* selectionArgs */,
398 Calendars.DISPLAY_NAME);
403 protected View newChildView(Context context, Cursor cursor, boolean isLastChild,
405 return mInflater.inflate(R.layout.calendar_item, parent, false);
409 protected View newGroupView(Context context, Cursor cursor, boolean isExpanded,
411 return mInflater.inflate(R.layout.account_item, parent, false);