OSDN Git Service

Merge "Some minor code fixes including cursor management."
[android-x86/packages-apps-Calendar.git] / src / com / android / calendar / SelectCalendarsAdapter.java
1 /*
2  * Copyright (C) 2007 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
17 package com.android.calendar;
18
19 import java.util.HashMap;
20 import java.util.Iterator;
21 import java.util.Map;
22
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;
44
45 public class SelectCalendarsAdapter extends CursorTreeAdapter implements View.OnClickListener {
46
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;
51
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};
54
55     private static final String TAG = "Calendar";
56
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,
61         R.drawable.widget_off
62     };
63
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;
70
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;
79
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>();
83
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;
89
90     private static String syncedVisible;
91     private static String syncedNotVisible;
92     private static String notSyncedNotVisible;
93
94     private static final String[] PROJECTION = new String[] {
95       Calendars._ID,
96       Calendars._SYNC_ACCOUNT,
97       Calendars.DISPLAY_NAME,
98       Calendars.COLOR,
99       Calendars.SELECTED,
100       Calendars.SYNC_EVENTS
101     };
102
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;
110
111     private class AsyncCalendarsUpdater extends AsyncQueryHandler {
112
113         public AsyncCalendarsUpdater(ContentResolver cr) {
114             super(cr);
115         }
116
117         @Override
118         protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
119             if(cursor == null) {
120                 return;
121             }
122
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)) {
127                     cursor.close();
128                     return;
129                 } else {
130                     mActivity.stopManagingCursor(currentCursor);
131                     currentCursor.close();
132                     mChildrenCursors.remove(cookie);
133                 }
134             }
135             // If not then make a new matrix cursor for our Map
136             MatrixCursor newCursor = matrixCursorFromCursor(cursor);
137             mChildrenCursors.put((String)cookie, newCursor);
138             try {
139                 setChildrenCursor(token, newCursor);
140                 mActivity.startManagingCursor(newCursor);
141             } catch (NullPointerException e) {
142                 Log.w(TAG, "Adapter expired, try again on the next query: " + e.getMessage());
143             }
144             cursor.close();
145         }
146
147         /**
148          * Compares two cursors to see if they contain the same data.
149          *
150          * @return Returns true of the cursors contain the same data and are not null, false
151          * otherwise
152          */
153         private boolean compareCursors(Cursor c1, Cursor c2) {
154             if(c1 == null || c2 == null) {
155                 return false;
156             }
157
158             int numColumns = c1.getColumnCount();
159             if (numColumns != c2.getColumnCount()) {
160                 return false;
161             }
162
163             if (c1.getCount() != c2.getCount()) {
164                 return false;
165             }
166
167             c1.moveToPosition(-1);
168             c2.moveToPosition(-1);
169             while(c1.moveToNext() && c2.moveToNext()) {
170                 for(int i = 0; i < numColumns; i++) {
171                     if(!c1.getString(i).equals(c2.getString(i))) {
172                         return false;
173                     }
174                 }
175             }
176
177             return true;
178         }
179
180         private MatrixCursor matrixCursorFromCursor(Cursor cursor) {
181             MatrixCursor newCursor = new MatrixCursor(cursor.getColumnNames());
182             int numColumns = cursor.getColumnCount();
183             int count = cursor.getCount();
184             String data[] = new String[numColumns];
185             cursor.moveToPosition(-1);
186             while(cursor.moveToNext()) {
187                 for(int i = 0; i < numColumns; i++) {
188                     data[i] = cursor.getString(i);
189                 }
190                 newCursor.addRow(data);
191             }
192             return newCursor;
193         }
194     }
195
196     /**
197      * Method for changing the sync/vis state when a calendar's button is pressed.
198      *
199      * This gets called when the MultiStateButton for a calendar is clicked. It cycles the sync/vis
200      * state for the associated calendar and saves a change of state to a hashmap. It also compares
201      * against the original value and removes any changes from the hashmap if this is back
202      * at its initial state.
203      */
204     public void onClick(View v) {
205         View view = (View)v.getTag();
206         long id = (Long)view.getTag();
207         Uri uri = ContentUris.withAppendedId(Calendars.CONTENT_URI, id);
208         String status = syncedNotVisible;
209         Boolean[] change;
210         Boolean[] initialState = mCalendarInitialStates.get(id);
211         if (mCalendarChanges.containsKey(id)) {
212             change = mCalendarChanges.get(id);
213         } else {
214             change = new Boolean[CHANGES_SIZE];
215             change[SELECTED_INDEX] = initialState[SELECTED_INDEX];
216             change[SYNCED_INDEX] = initialState[SYNCED_INDEX];
217             mCalendarChanges.put(id, change);
218         }
219
220         if (change[SELECTED_INDEX]) {
221             change[SELECTED_INDEX] = false;
222             status = syncedNotVisible;
223         }
224         else if (change[SYNCED_INDEX]) {
225             change[SYNCED_INDEX] = false;
226             status = notSyncedNotVisible;
227         }
228         else
229         {
230             change[SYNCED_INDEX] = true;
231             change[SELECTED_INDEX] = true;
232             status = syncedVisible;
233         }
234         setText(view, R.id.status, status);
235         if (change[SELECTED_INDEX] == initialState[SELECTED_INDEX] &&
236                 change[SYNCED_INDEX] == initialState[SYNCED_INDEX]) {
237             mCalendarChanges.remove(id);
238         }
239     }
240
241     public SelectCalendarsAdapter(Context context, Cursor cursor, SelectCalendarsActivity act) {
242         super(cursor, context);
243         syncedVisible = context.getString(R.string.synced_visible);
244         syncedNotVisible = context.getString(R.string.synced_not_visible);
245         notSyncedNotVisible = context.getString(R.string.not_synced_not_visible);
246
247         mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
248         mResolver = context.getContentResolver();
249         mActivity = act;
250         if (mCalendarsUpdater == null) {
251             mCalendarsUpdater = new AsyncCalendarsUpdater(mResolver);
252         }
253         if(cursor.getCount() == 0) {
254             //Should never happen since Calendar requires an account exist to use it.
255             Log.e(TAG, "SelectCalendarsAdapter: No accounts were returned!");
256         }
257         //Collect proper description for account types
258         mAuthDescs = AccountManager.get(context).getAuthenticatorTypes();
259         for (int i = 0; i < mAuthDescs.length; i++) {
260             mTypeToAuthDescription.put(mAuthDescs[i].type, mAuthDescs[i]);
261         }
262     }
263
264     /*
265      * Write back the changes that have been made. The sync code will pick up any changes and
266      * do updates on its own.
267      */
268     public void doSaveAction() {
269         // Cancel the previous operation
270         mCalendarsUpdater.cancelOperation(mUpdateToken);
271         mUpdateToken++;
272         // This is to allow us to do queries and updates with the same AsyncQueryHandler without
273         // accidently canceling queries.
274         if(mUpdateToken < MIN_UPDATE_TOKEN) mUpdateToken = MIN_UPDATE_TOKEN;
275
276         Iterator<Long> changeKeys = mCalendarChanges.keySet().iterator();
277         while (changeKeys.hasNext()) {
278             long id = changeKeys.next();
279             Boolean[] change = mCalendarChanges.get(id);
280             int newSelected = change[SELECTED_INDEX] ? 1 : 0;
281             int newSynced = change[SYNCED_INDEX] ? 1 : 0;
282
283             Uri uri = ContentUris.withAppendedId(Calendars.CONTENT_URI, id);
284             ContentValues values = new ContentValues();
285             values.put(Calendars.SELECTED, newSelected);
286             values.put(Calendars.SYNC_EVENTS, newSynced);
287             mCalendarsUpdater.startUpdate(mUpdateToken, id, uri, values, null, null);
288         }
289     }
290
291     private static void setText(View view, int id, String text) {
292         if (TextUtils.isEmpty(text)) {
293             return;
294         }
295         TextView textView = (TextView) view.findViewById(id);
296         textView.setText(text);
297     }
298
299     /**
300      * Gets the label associated with a particular account type. If none found, return null.
301      * @param accountType the type of account
302      * @return a CharSequence for the label or null if one cannot be found.
303      */
304     protected CharSequence getLabelForType(final String accountType) {
305         CharSequence label = null;
306         if (mTypeToAuthDescription.containsKey(accountType)) {
307              try {
308                  AuthenticatorDescription desc = mTypeToAuthDescription.get(accountType);
309                  Context authContext = mActivity.createPackageContext(desc.packageName, 0);
310                  label = authContext.getResources().getText(desc.labelId);
311              } catch (PackageManager.NameNotFoundException e) {
312                  Log.w(TAG, "No label for account type " + ", type " + accountType);
313              }
314         }
315         return label;
316     }
317
318     private Drawable getColorChip(int color) {
319
320         /*
321          * We want the color chip to have a nice gradient using
322          * the color of the calendar. To do this we use a GradientDrawable.
323          * The color supplied has an alpha of FF so we first do:
324          * color & 0x00FFFFFF
325          * to clear the alpha. Then we add our alpha to it.
326          * We use 3 colors to get a step effect where it starts off very
327          * light and quickly becomes dark and then a slow transition to
328          * be even darker.
329          */
330         color &= CLEAR_ALPHA_MASK;
331         int startColor = color | HIGH_ALPHA;
332         int middleColor = color | MED_ALPHA;
333         int endColor = color | LOW_ALPHA;
334         int[] colors = new int[] {startColor, middleColor, endColor};
335         GradientDrawable d = new GradientDrawable(GradientDrawable.Orientation.LEFT_RIGHT, colors);
336         d.setCornerRadii(CORNERS);
337         return d;
338     }
339
340     @Override
341     protected void bindChildView(View view, Context context, Cursor cursor, boolean isLastChild) {
342         String account = cursor.getString(ACCOUNT_COLUMN);
343         String status = notSyncedNotVisible;
344         int state = 2;
345         int position = cursor.getPosition();
346         long id = cursor.getLong(ID_COLUMN);
347
348         // First see if the user has already changed the state of this calendar
349         Boolean[] initialState = mCalendarChanges.get(id);
350         // if not just grab the initial state
351         if (initialState == null) {
352             initialState = mCalendarInitialStates.get(id);
353         }
354         // and create a new initial state if we've never seen this calendar before.
355         if(initialState == null) {
356             initialState = new Boolean[CHANGES_SIZE];
357             initialState[SELECTED_INDEX] = cursor.getInt(SELECTED_COLUMN) == 1;
358             initialState[SYNCED_INDEX] = cursor.getInt(SYNCED_COLUMN) == 1;
359             mCalendarInitialStates.put(id, initialState);
360         }
361
362         if(initialState[SYNCED_INDEX]) {
363             if(initialState[SELECTED_INDEX]) {
364                 status = syncedVisible;
365                 state = 0;
366             } else {
367                 status = syncedNotVisible;
368                 state = 1;
369             }
370         }
371
372         view.findViewById(R.id.color)
373             .setBackgroundDrawable(getColorChip(cursor.getInt(COLOR_COLUMN)));
374         setText(view, R.id.calendar, cursor.getString(NAME_COLUMN));
375         setText(view, R.id.status, status);
376         MultiStateButton button = (MultiStateButton) view.findViewById(R.id.multiStateButton);
377
378         //Set up the listeners so a click on the button will change the state.
379         //The view already uses the onChildClick method in the activity.
380         button.setTag(view);
381         view.setTag(id);
382         button.setOnClickListener(this);
383         button.setButtonResources(SYNC_VIS_BUTTON_RES);
384         button.setState(state);
385     }
386
387     @Override
388     protected void bindGroupView(View view, Context context, Cursor cursor, boolean isExpanded) {
389         int accountColumn = cursor.getColumnIndexOrThrow(Calendars._SYNC_ACCOUNT);
390         int accountTypeColumn = cursor.getColumnIndexOrThrow(Calendars._SYNC_ACCOUNT_TYPE);
391         String account = cursor.getString(accountColumn);
392         String accountType = cursor.getString(accountTypeColumn);
393         setText(view, R.id.account, account);
394         setText(view, R.id.account_type, getLabelForType(accountType).toString());
395     }
396
397     @Override
398     protected Cursor getChildrenCursor(Cursor groupCursor) {
399         int accountColumn = groupCursor.getColumnIndexOrThrow(Calendars._SYNC_ACCOUNT);
400         String account = groupCursor.getString(accountColumn);
401         //Get all the calendars for just this account.
402         Cursor childCursor = mChildrenCursors.get(account);
403         mCalendarsUpdater.startQuery(groupCursor.getPosition(),
404                 account,
405                 Calendars.CONTENT_URI, PROJECTION,
406                 Calendars._SYNC_ACCOUNT + "=\"" + account + "\"" /*Selection*/,
407                 null /* selectionArgs */,
408                 Calendars.DISPLAY_NAME);
409         return childCursor;
410     }
411
412     @Override
413     protected View newChildView(Context context, Cursor cursor, boolean isLastChild,
414             ViewGroup parent) {
415         return mInflater.inflate(R.layout.calendar_item, parent, false);
416     }
417
418     @Override
419     protected View newGroupView(Context context, Cursor cursor, boolean isExpanded,
420             ViewGroup parent) {
421         return mInflater.inflate(R.layout.account_item, parent, false);
422     }
423 }