OSDN Git Service

47048f54bcf2e8d68487b601f82df8ef614cefc3
[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 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;
38
39 import java.util.HashMap;
40 import java.util.Iterator;
41 import java.util.Map;
42
43 public class SelectCalendarsAdapter extends CursorTreeAdapter implements View.OnClickListener {
44
45     private static final String TAG = "Calendar";
46
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 +
50             COLLATE_NOCASE;
51
52     // The drawables used for the button to change the visible and sync states on a calendar
53     private static final int[] SYNC_VIS_BUTTON_RES = new int[] {
54         R.drawable.widget_show,
55         R.drawable.widget_sync,
56         R.drawable.widget_off
57     };
58
59     private final LayoutInflater mInflater;
60     private final ContentResolver mResolver;
61     private final SelectCalendarsActivity mActivity;
62     private Map<String, AuthenticatorDescription> mTypeToAuthDescription
63         = new HashMap<String, AuthenticatorDescription>();
64     protected AuthenticatorDescription[] mAuthDescs;
65
66     // These track changes to the visible (selected) and synced state of calendars
67     private Map<Long, Boolean[]> mCalendarChanges
68         = new HashMap<Long, Boolean[]>();
69     private Map<Long, Boolean[]> mCalendarInitialStates
70         = new HashMap<Long, Boolean[]>();
71     private static final int SELECTED_INDEX = 0;
72     private static final int SYNCED_INDEX = 1;
73     private static final int CHANGES_SIZE = 2;
74
75     // This is for keeping MatrixCursor copies so that we can requery in the background.
76     private static Map<String, Cursor> mChildrenCursors
77         = new HashMap<String, Cursor>();
78
79     private static AsyncCalendarsUpdater mCalendarsUpdater;
80     // This is to keep our update tokens separate from other tokens. Since we cancel old updates
81     // when a new update comes in, we'd like to leave a token space that won't be canceled.
82     private static final int MIN_UPDATE_TOKEN = 1000;
83     private static int mUpdateToken = MIN_UPDATE_TOKEN;
84
85     private static String syncedVisible;
86     private static String syncedNotVisible;
87     private static String notSyncedNotVisible;
88
89     // This is to keep track of whether or not multiple calendars have the same display name
90     private static HashMap<String, Boolean> mIsDuplicateName = new HashMap<String, Boolean>();
91
92     private static final String[] PROJECTION = new String[] {
93       Calendars._ID,
94       Calendars._SYNC_ACCOUNT,
95       Calendars.OWNER_ACCOUNT,
96       Calendars.DISPLAY_NAME,
97       Calendars.COLOR,
98       Calendars.SELECTED,
99       Calendars.SYNC_EVENTS,
100       "(" + Calendars._SYNC_ACCOUNT + "=" + Calendars.OWNER_ACCOUNT + ") AS " + IS_PRIMARY,
101     };
102     //Keep these in sync with the projection
103     private static final int ID_COLUMN = 0;
104     private static final int ACCOUNT_COLUMN = 1;
105     private static final int OWNER_COLUMN = 2;
106     private static final int NAME_COLUMN = 3;
107     private static final int COLOR_COLUMN = 4;
108     private static final int SELECTED_COLUMN = 5;
109     private static final int SYNCED_COLUMN = 6;
110     private static final int PRIMARY_COLUMN = 7;
111
112     private class AsyncCalendarsUpdater extends AsyncQueryHandler {
113
114         public AsyncCalendarsUpdater(ContentResolver cr) {
115             super(cr);
116         }
117
118         @Override
119         protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
120             if(cursor == null) {
121                 return;
122             }
123
124             Cursor currentCursor = mChildrenCursors.get(cookie);
125             // Check if the new cursor has the same content as our old cursor
126             if (currentCursor != null) {
127                 if (compareCursors(currentCursor, cursor)) {
128                     cursor.close();
129                     return;
130                 } else {
131                     mActivity.stopManagingCursor(currentCursor);
132                     currentCursor.close();
133                     mChildrenCursors.remove(cookie);
134                 }
135             }
136             // If not then make a new matrix cursor for our Map
137             MatrixCursor newCursor = matrixCursorFromCursor(cursor);
138             // And update our list of duplicated names
139             Utils.checkForDuplicateNames(mIsDuplicateName, cursor, NAME_COLUMN);
140
141             mChildrenCursors.put((String)cookie, newCursor);
142             try {
143                 setChildrenCursor(token, newCursor);
144                 mActivity.startManagingCursor(newCursor);
145             } catch (NullPointerException e) {
146                 Log.w(TAG, "Adapter expired, try again on the next query: " + e.getMessage());
147             }
148             cursor.close();
149         }
150
151         /**
152          * Compares two cursors to see if they contain the same data.
153          *
154          * @return Returns true of the cursors contain the same data and are not null, false
155          * otherwise
156          */
157         private boolean compareCursors(Cursor c1, Cursor c2) {
158             if(c1 == null || c2 == null) {
159                 return false;
160             }
161
162             int numColumns = c1.getColumnCount();
163             if (numColumns != c2.getColumnCount()) {
164                 return false;
165             }
166
167             if (c1.getCount() != c2.getCount()) {
168                 return false;
169             }
170
171             c1.moveToPosition(-1);
172             c2.moveToPosition(-1);
173             while(c1.moveToNext() && c2.moveToNext()) {
174                 for(int i = 0; i < numColumns; i++) {
175                     if(!c1.getString(i).equals(c2.getString(i))) {
176                         return false;
177                     }
178                 }
179             }
180
181             return true;
182         }
183
184         private MatrixCursor matrixCursorFromCursor(Cursor cursor) {
185             MatrixCursor newCursor = new MatrixCursor(cursor.getColumnNames());
186             int numColumns = cursor.getColumnCount();
187             String data[] = new String[numColumns];
188             cursor.moveToPosition(-1);
189             while (cursor.moveToNext()) {
190                 for (int i = 0; i < numColumns; i++) {
191                     data[i] = cursor.getString(i);
192                 }
193                 newCursor.addRow(data);
194             }
195             return newCursor;
196         }
197     }
198
199     /**
200      * Method for changing the sync/vis state when a calendar's button is pressed.
201      *
202      * This gets called when the MultiStateButton for a calendar is clicked. It cycles the sync/vis
203      * state for the associated calendar and saves a change of state to a hashmap. It also compares
204      * against the original value and removes any changes from the hashmap if this is back
205      * at its initial state.
206      */
207     public void onClick(View v) {
208         View view = (View)v.getTag();
209         long id = (Long)view.getTag();
210         Uri uri = ContentUris.withAppendedId(Calendars.CONTENT_URI, id);
211         String status = syncedNotVisible;
212         Boolean[] change;
213         Boolean[] initialState = mCalendarInitialStates.get(id);
214         if (mCalendarChanges.containsKey(id)) {
215             change = mCalendarChanges.get(id);
216         } else {
217             change = new Boolean[CHANGES_SIZE];
218             change[SELECTED_INDEX] = initialState[SELECTED_INDEX];
219             change[SYNCED_INDEX] = initialState[SYNCED_INDEX];
220             mCalendarChanges.put(id, change);
221         }
222
223         if (change[SELECTED_INDEX]) {
224             change[SELECTED_INDEX] = false;
225             status = syncedNotVisible;
226         }
227         else if (change[SYNCED_INDEX]) {
228             change[SYNCED_INDEX] = false;
229             status = notSyncedNotVisible;
230         }
231         else
232         {
233             change[SYNCED_INDEX] = true;
234             change[SELECTED_INDEX] = true;
235             status = syncedVisible;
236         }
237         setText(view, R.id.status, status);
238         if (change[SELECTED_INDEX] == initialState[SELECTED_INDEX] &&
239                 change[SYNCED_INDEX] == initialState[SYNCED_INDEX]) {
240             mCalendarChanges.remove(id);
241         }
242     }
243
244     public SelectCalendarsAdapter(Context context, Cursor cursor, SelectCalendarsActivity act) {
245         super(cursor, context);
246         syncedVisible = context.getString(R.string.synced_visible);
247         syncedNotVisible = context.getString(R.string.synced_not_visible);
248         notSyncedNotVisible = context.getString(R.string.not_synced_not_visible);
249
250         mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
251         mResolver = context.getContentResolver();
252         mActivity = act;
253         if (mCalendarsUpdater == null) {
254             mCalendarsUpdater = new AsyncCalendarsUpdater(mResolver);
255         }
256         if(cursor.getCount() == 0) {
257             //Should never happen since Calendar requires an account exist to use it.
258             Log.e(TAG, "SelectCalendarsAdapter: No accounts were returned!");
259         }
260         //Collect proper description for account types
261         mAuthDescs = AccountManager.get(context).getAuthenticatorTypes();
262         for (int i = 0; i < mAuthDescs.length; i++) {
263             mTypeToAuthDescription.put(mAuthDescs[i].type, mAuthDescs[i]);
264         }
265     }
266
267     /*
268      * Write back the changes that have been made. The sync code will pick up any changes and
269      * do updates on its own.
270      */
271     public void doSaveAction() {
272         // Cancel the previous operation
273         mCalendarsUpdater.cancelOperation(mUpdateToken);
274         mUpdateToken++;
275         // This is to allow us to do queries and updates with the same AsyncQueryHandler without
276         // accidently canceling queries.
277         if(mUpdateToken < MIN_UPDATE_TOKEN) mUpdateToken = MIN_UPDATE_TOKEN;
278
279         Iterator<Long> changeKeys = mCalendarChanges.keySet().iterator();
280         while (changeKeys.hasNext()) {
281             long id = changeKeys.next();
282             Boolean[] change = mCalendarChanges.get(id);
283             int newSelected = change[SELECTED_INDEX] ? 1 : 0;
284             int newSynced = change[SYNCED_INDEX] ? 1 : 0;
285
286             Uri uri = ContentUris.withAppendedId(Calendars.CONTENT_URI, id);
287             ContentValues values = new ContentValues();
288             values.put(Calendars.SELECTED, newSelected);
289             values.put(Calendars.SYNC_EVENTS, newSynced);
290             mCalendarsUpdater.startUpdate(mUpdateToken, id, uri, values, null, null);
291         }
292     }
293
294     private static void setText(View view, int id, String text) {
295         if (TextUtils.isEmpty(text)) {
296             return;
297         }
298         TextView textView = (TextView) view.findViewById(id);
299         textView.setText(text);
300     }
301
302     /**
303      * Gets the label associated with a particular account type. If none found, return null.
304      * @param accountType the type of account
305      * @return a CharSequence for the label or null if one cannot be found.
306      */
307     protected CharSequence getLabelForType(final String accountType) {
308         CharSequence label = null;
309         if (mTypeToAuthDescription.containsKey(accountType)) {
310              try {
311                  AuthenticatorDescription desc = mTypeToAuthDescription.get(accountType);
312                  Context authContext = mActivity.createPackageContext(desc.packageName, 0);
313                  label = authContext.getResources().getText(desc.labelId);
314              } catch (PackageManager.NameNotFoundException e) {
315                  Log.w(TAG, "No label for account type " + ", type " + accountType);
316              }
317         }
318         return label;
319     }
320
321     @Override
322     protected void bindChildView(View view, Context context, Cursor cursor, boolean isLastChild) {
323         String account = cursor.getString(ACCOUNT_COLUMN);
324         String status = notSyncedNotVisible;
325         int state = 2;
326         int position = cursor.getPosition();
327         long id = cursor.getLong(ID_COLUMN);
328
329         // First see if the user has already changed the state of this calendar
330         Boolean[] initialState = mCalendarChanges.get(id);
331         // if not just grab the initial state
332         if (initialState == null) {
333             initialState = mCalendarInitialStates.get(id);
334         }
335         // and create a new initial state if we've never seen this calendar before.
336         if(initialState == null) {
337             initialState = new Boolean[CHANGES_SIZE];
338             initialState[SELECTED_INDEX] = cursor.getInt(SELECTED_COLUMN) == 1;
339             initialState[SYNCED_INDEX] = cursor.getInt(SYNCED_COLUMN) == 1;
340             mCalendarInitialStates.put(id, initialState);
341         }
342
343         if(initialState[SYNCED_INDEX]) {
344             if(initialState[SELECTED_INDEX]) {
345                 status = syncedVisible;
346                 state = 0;
347             } else {
348                 status = syncedNotVisible;
349                 state = 1;
350             }
351         }
352
353         view.findViewById(R.id.color)
354             .setBackgroundDrawable(Utils.getColorChip(cursor.getInt(COLOR_COLUMN)));
355         String name = cursor.getString(NAME_COLUMN);
356         String owner = cursor.getString(OWNER_COLUMN);
357         if (mIsDuplicateName.containsKey(name) && mIsDuplicateName.get(name) &&
358                 !name.equalsIgnoreCase(owner)) {
359             name = new StringBuilder(name)
360                     .append(Utils.OPEN_EMAIL_MARKER)
361                     .append(owner)
362                     .append(Utils.CLOSE_EMAIL_MARKER)
363                     .toString();
364         }
365         setText(view, R.id.calendar, name);
366         setText(view, R.id.status, status);
367         MultiStateButton button = (MultiStateButton) view.findViewById(R.id.multiStateButton);
368
369         //Set up the listeners so a click on the button will change the state.
370         //The view already uses the onChildClick method in the activity.
371         button.setTag(view);
372         view.setTag(id);
373         button.setOnClickListener(this);
374         button.setButtonResources(SYNC_VIS_BUTTON_RES);
375         button.setState(state);
376     }
377
378     @Override
379     protected void bindGroupView(View view, Context context, Cursor cursor, boolean isExpanded) {
380         int accountColumn = cursor.getColumnIndexOrThrow(Calendars._SYNC_ACCOUNT);
381         int accountTypeColumn = cursor.getColumnIndexOrThrow(Calendars._SYNC_ACCOUNT_TYPE);
382         String account = cursor.getString(accountColumn);
383         String accountType = cursor.getString(accountTypeColumn);
384         setText(view, R.id.account, account);
385         setText(view, R.id.account_type, getLabelForType(accountType).toString());
386     }
387
388     @Override
389     protected Cursor getChildrenCursor(Cursor groupCursor) {
390         int accountColumn = groupCursor.getColumnIndexOrThrow(Calendars._SYNC_ACCOUNT);
391         String account = groupCursor.getString(accountColumn);
392         //Get all the calendars for just this account.
393         Cursor childCursor = mChildrenCursors.get(account);
394         mCalendarsUpdater.startQuery(groupCursor.getPosition(),
395                 account,
396                 Calendars.CONTENT_URI, PROJECTION,
397                 Calendars._SYNC_ACCOUNT + "=\"" + account + "\"" /*Selection*/,
398                 null /* selectionArgs */,
399                 CALENDARS_ORDERBY);
400         return childCursor;
401     }
402
403     @Override
404     protected View newChildView(Context context, Cursor cursor, boolean isLastChild,
405             ViewGroup parent) {
406         return mInflater.inflate(R.layout.calendar_item, parent, false);
407     }
408
409     @Override
410     protected View newGroupView(Context context, Cursor cursor, boolean isExpanded,
411             ViewGroup parent) {
412         return mInflater.inflate(R.layout.account_item, parent, false);
413     }
414 }