OSDN Git Service

original
[gb-231r1-is01/Gingerbread_2.3.3_r1_IS01.git] / packages / apps / Calendar / 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     private static final String ACCOUNT_SELECTION = Calendars._SYNC_ACCOUNT + "=?"
52             + " AND " + Calendars._SYNC_ACCOUNT_TYPE + "=?";
53
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,
58         R.drawable.widget_off
59     };
60
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() {
66         public void run() {
67             mRefresh = false;
68         }
69     };
70     private Map<String, AuthenticatorDescription> mTypeToAuthDescription
71         = new HashMap<String, AuthenticatorDescription>();
72     protected AuthenticatorDescription[] mAuthDescs;
73
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;
82
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>();
86
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;
98
99     private static String syncedVisible;
100     private static String syncedNotVisible;
101     private static String notSyncedNotVisible;
102
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>();
105
106     private static final String[] PROJECTION = new String[] {
107       Calendars._ID,
108       Calendars._SYNC_ACCOUNT,
109       Calendars.OWNER_ACCOUNT,
110       Calendars.DISPLAY_NAME,
111       Calendars.COLOR,
112       Calendars.SELECTED,
113       Calendars.SYNC_EVENTS,
114       "(" + Calendars._SYNC_ACCOUNT + "=" + Calendars.OWNER_ACCOUNT + ") AS " + IS_PRIMARY,
115     };
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;
125
126     private class AsyncCalendarsUpdater extends AsyncQueryHandler {
127
128         public AsyncCalendarsUpdater(ContentResolver cr) {
129             super(cr);
130         }
131
132         @Override
133         protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
134             if(cursor == null) {
135                 return;
136             }
137
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)) {
142                     cursor.close();
143                     return;
144                 }
145             }
146             // If not then make a new matrix cursor for our Map
147             MatrixCursor newCursor = Utils.matrixCursorFromCursor(cursor);
148             cursor.close();
149             // And update our list of duplicated names
150             Utils.checkForDuplicateNames(mIsDuplicateName, newCursor, NAME_COLUMN);
151
152             mChildrenCursors.put((String)cookie, newCursor);
153             try {
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);
158             }
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();
164             }
165         }
166     }
167
168
169
170     /**
171      * Method for changing the sync/vis state when a calendar's button is pressed.
172      *
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.
177      */
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;
183         Boolean[] change;
184         Boolean[] initialState = mCalendarInitialStates.get(id);
185         if (mCalendarChanges.containsKey(id)) {
186             change = mCalendarChanges.get(id);
187         } else {
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);
192         }
193
194         if (change[SELECTED_INDEX]) {
195             change[SELECTED_INDEX] = false;
196             status = syncedNotVisible;
197         }
198         else if (change[SYNCED_INDEX]) {
199             change[SYNCED_INDEX] = false;
200             status = notSyncedNotVisible;
201         }
202         else
203         {
204             change[SYNCED_INDEX] = true;
205             change[SELECTED_INDEX] = true;
206             status = syncedVisible;
207         }
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);
212         }
213     }
214
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);
220
221         mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
222         mResolver = context.getContentResolver();
223         mActivity = act;
224         if (mCalendarsUpdater == null) {
225             mCalendarsUpdater = new AsyncCalendarsUpdater(mResolver);
226         }
227
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!");
232         }
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]);
237         }
238         mView = mActivity.getExpandableListView();
239         mRefresh = true;
240     }
241
242     public void startRefreshStopDelay() {
243         mRefresh = true;
244         mView.postDelayed(mStopRefreshing, REFRESH_DURATION);
245     }
246
247     public void cancelRefreshStopDelay() {
248         mView.removeCallbacks(mStopRefreshing);
249     }
250
251     /*
252      * Write back the changes that have been made. The sync code will pick up any changes and
253      * do updates on its own.
254      */
255     public void doSaveAction() {
256         // Cancel the previous operation
257         mCalendarsUpdater.cancelOperation(mUpdateToken);
258         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;
262
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;
269
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);
275         }
276     }
277
278     private static void setText(View view, int id, String text) {
279         if (TextUtils.isEmpty(text)) {
280             return;
281         }
282         TextView textView = (TextView) view.findViewById(id);
283         textView.setText(text);
284     }
285
286     /**
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.
290      */
291     protected CharSequence getLabelForType(final String accountType) {
292         CharSequence label = null;
293         if (mTypeToAuthDescription.containsKey(accountType)) {
294              try {
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);
300              }
301         }
302         return label;
303     }
304
305     @Override
306     protected void bindChildView(View view, Context context, Cursor cursor, boolean isLastChild) {
307         String account = cursor.getString(ACCOUNT_COLUMN);
308         String status = notSyncedNotVisible;
309         int state = 2;
310         int position = cursor.getPosition();
311         long id = cursor.getLong(ID_COLUMN);
312
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);
321         }
322
323         if(initialState[SYNCED_INDEX]) {
324             if(initialState[SELECTED_INDEX]) {
325                 status = syncedVisible;
326                 state = 0;
327             } else {
328                 status = syncedNotVisible;
329                 state = 1;
330             }
331         }
332
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)
341                     .append(owner)
342                     .append(Utils.CLOSE_EMAIL_MARKER)
343                     .toString();
344         }
345         setText(view, R.id.calendar, name);
346         setText(view, R.id.status, status);
347         MultiStateButton button = (MultiStateButton) view.findViewById(R.id.multiStateButton);
348
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.
351         button.setTag(view);
352         view.setTag(id);
353         button.setOnClickListener(this);
354         button.setButtonResources(SYNC_VIS_BUTTON_RES);
355         button.setState(state);
356     }
357
358     @Override
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());
366     }
367
368     @Override
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();
377         return childCursor;
378     }
379
380     @Override
381     protected View newChildView(Context context, Cursor cursor, boolean isLastChild,
382             ViewGroup parent) {
383         return mInflater.inflate(R.layout.calendar_item, parent, false);
384     }
385
386     @Override
387     protected View newGroupView(Context context, Cursor cursor, boolean isExpanded,
388             ViewGroup parent) {
389         return mInflater.inflate(R.layout.account_item, parent, false);
390     }
391
392     private class RefreshCalendars implements Runnable {
393
394         int mToken;
395         String mAccount;
396         String mAccountType;
397
398         public RefreshCalendars(int token, String cookie, String accountType) {
399             mToken = token;
400             mAccount = cookie;
401             mAccountType = accountType;
402         }
403
404         public void run() {
405             mCalendarsUpdater.cancelOperation(mToken);
406             // Set up a refresh for some point in the future if we haven't stopped updates yet
407             if(mRefresh) {
408                 mView.postDelayed(new RefreshCalendars(mToken, mAccount, mAccountType),
409                         REFRESH_DELAY);
410             }
411             mCalendarsUpdater.startQuery(mToken,
412                     mAccount,
413                     Calendars.CONTENT_URI, PROJECTION,
414                     ACCOUNT_SELECTION,
415                     new String[] { mAccount, mAccountType } /*selectionArgs*/,
416                     CALENDARS_ORDERBY);
417         }
418     }
419 }