• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2011 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.selectcalendars;
18 
19 import com.android.calendar.R;
20 import com.android.calendar.Utils;
21 
22 import android.accounts.AccountManager;
23 import android.accounts.AuthenticatorDescription;
24 import android.content.AsyncQueryHandler;
25 import android.content.ContentResolver;
26 import android.content.ContentUris;
27 import android.content.ContentValues;
28 import android.content.Context;
29 import android.content.pm.PackageManager;
30 import android.database.Cursor;
31 import android.database.MatrixCursor;
32 import android.net.Uri;
33 import android.provider.CalendarContract.Calendars;
34 import android.text.TextUtils;
35 import android.util.Log;
36 import android.view.LayoutInflater;
37 import android.view.View;
38 import android.view.ViewGroup;
39 import android.widget.CheckBox;
40 import android.widget.CursorTreeAdapter;
41 import android.widget.TextView;
42 
43 import java.util.HashMap;
44 import java.util.Iterator;
45 import java.util.Map;
46 
47 public class SelectSyncedCalendarsMultiAccountAdapter extends CursorTreeAdapter implements
48         View.OnClickListener {
49 
50     private static final String TAG = "Calendar";
51 
52     private static final String IS_PRIMARY = "\"primary\"";
53     private static final String CALENDARS_ORDERBY = IS_PRIMARY + " DESC,"
54             + Calendars.CALENDAR_DISPLAY_NAME + " COLLATE NOCASE";
55     private static final String ACCOUNT_SELECTION = Calendars.ACCOUNT_NAME + "=?"
56             + " AND " + Calendars.ACCOUNT_TYPE + "=?";
57 
58     private final LayoutInflater mInflater;
59     private final ContentResolver mResolver;
60     private final SelectSyncedCalendarsMultiAccountActivity mActivity;
61     private final View mView;
62     private final static Runnable mStopRefreshing = new Runnable() {
63         public void run() {
64             mRefresh = false;
65         }
66     };
67     private Map<String, AuthenticatorDescription> mTypeToAuthDescription
68         = new HashMap<String, AuthenticatorDescription>();
69     protected AuthenticatorDescription[] mAuthDescs;
70 
71     // These track changes to the 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 
77     // This is for keeping MatrixCursor copies so that we can requery in the background.
78     private static Map<String, Cursor> mChildrenCursors
79         = new HashMap<String, Cursor>();
80 
81     private static AsyncCalendarsUpdater mCalendarsUpdater;
82     // This is to keep our update tokens separate from other tokens. Since we cancel old updates
83     // when a new update comes in, we'd like to leave a token space that won't be canceled.
84     private static final int MIN_UPDATE_TOKEN = 1000;
85     private static int mUpdateToken = MIN_UPDATE_TOKEN;
86     // How long to wait between requeries of the calendars to see if anything has changed.
87     private static final int REFRESH_DELAY = 5000;
88     // How long to keep refreshing for
89     private static final int REFRESH_DURATION = 60000;
90     private static boolean mRefresh = true;
91     private int mNumAccounts;
92 
93     private static String mSyncedText;
94     private static String mNotSyncedText;
95 
96     // This is to keep track of whether or not multiple calendars have the same display name
97     private static HashMap<String, Boolean> mIsDuplicateName = new HashMap<String, Boolean>();
98 
99     private static final String[] PROJECTION = new String[] {
100       Calendars._ID,
101       Calendars.ACCOUNT_NAME,
102       Calendars.OWNER_ACCOUNT,
103       Calendars.CALENDAR_DISPLAY_NAME,
104       Calendars.CALENDAR_COLOR,
105       Calendars.VISIBLE,
106       Calendars.SYNC_EVENTS,
107       "(" + Calendars.ACCOUNT_NAME + "=" + Calendars.OWNER_ACCOUNT + ") AS " + IS_PRIMARY,
108     };
109     //Keep these in sync with the projection
110     private static final int ID_COLUMN = 0;
111     private static final int ACCOUNT_COLUMN = 1;
112     private static final int OWNER_COLUMN = 2;
113     private static final int NAME_COLUMN = 3;
114     private static final int COLOR_COLUMN = 4;
115     private static final int SELECTED_COLUMN = 5;
116     private static final int SYNCED_COLUMN = 6;
117     private static final int PRIMARY_COLUMN = 7;
118 
119     private static final int TAG_ID_CALENDAR_ID = R.id.calendar;
120     private static final int TAG_ID_SYNC_CHECKBOX = R.id.sync;
121 
122     private class AsyncCalendarsUpdater extends AsyncQueryHandler {
123 
AsyncCalendarsUpdater(ContentResolver cr)124         public AsyncCalendarsUpdater(ContentResolver cr) {
125             super(cr);
126         }
127 
128         @Override
onQueryComplete(int token, Object cookie, Cursor cursor)129         protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
130             if(cursor == null) {
131                 return;
132             }
133 
134             Cursor currentCursor = mChildrenCursors.get(cookie);
135             // Check if the new cursor has the same content as our old cursor
136             if (currentCursor != null) {
137                 if (Utils.compareCursors(currentCursor, cursor)) {
138                     cursor.close();
139                     return;
140                 }
141             }
142             // If not then make a new matrix cursor for our Map
143             MatrixCursor newCursor = Utils.matrixCursorFromCursor(cursor);
144             cursor.close();
145             // And update our list of duplicated names
146             Utils.checkForDuplicateNames(mIsDuplicateName, newCursor, NAME_COLUMN);
147 
148             mChildrenCursors.put((String)cookie, newCursor);
149             try {
150                 setChildrenCursor(token, newCursor);
151                 mActivity.startManagingCursor(newCursor);
152             } catch (NullPointerException e) {
153                 Log.w(TAG, "Adapter expired, try again on the next query: " + e);
154             }
155             // Clean up our old cursor if we had one. We have to do this after setting the new
156             // cursor so that our view doesn't throw on an invalid cursor.
157             if (currentCursor != null) {
158                 mActivity.stopManagingCursor(currentCursor);
159                 currentCursor.close();
160             }
161         }
162     }
163 
164     /**
165      * Method for changing the sync state when a calendar's button is pressed.
166      *
167      * This gets called when the CheckBox for a calendar is clicked. It toggles
168      * the sync state for the associated calendar and saves a change of state to
169      * a hashmap. It also compares against the original value and removes any
170      * changes from the hashmap if this is back at its initial state.
171      */
onClick(View v)172     public void onClick(View v) {
173         long id = (Long) v.getTag(TAG_ID_CALENDAR_ID);
174         boolean newState;
175         boolean initialState = mCalendarInitialStates.get(id);
176         if (mCalendarChanges.containsKey(id)) {
177             // Negate to reflect the click
178             newState = !mCalendarChanges.get(id);
179         } else {
180             // Negate to reflect the click
181             newState = !initialState;
182         }
183 
184         if (newState == initialState) {
185             mCalendarChanges.remove(id);
186         } else {
187             mCalendarChanges.put(id, newState);
188         }
189 
190         ((CheckBox) v.getTag(TAG_ID_SYNC_CHECKBOX)).setChecked(newState);
191         setText(v, R.id.status, newState ? mSyncedText : mNotSyncedText);
192     }
193 
SelectSyncedCalendarsMultiAccountAdapter(Context context, Cursor acctsCursor, SelectSyncedCalendarsMultiAccountActivity act)194     public SelectSyncedCalendarsMultiAccountAdapter(Context context, Cursor acctsCursor,
195             SelectSyncedCalendarsMultiAccountActivity act) {
196         super(acctsCursor, context);
197         mSyncedText = context.getString(R.string.synced);
198         mNotSyncedText = context.getString(R.string.not_synced);
199 
200         mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
201         mResolver = context.getContentResolver();
202         mActivity = act;
203         if (mCalendarsUpdater == null) {
204             mCalendarsUpdater = new AsyncCalendarsUpdater(mResolver);
205         }
206 
207         mNumAccounts = acctsCursor.getCount();
208         if (mNumAccounts == 0) {
209             // Should never happen since Calendar requires an account exist to
210             // use it.
211             Log.e(TAG, "SelectCalendarsAdapter: No accounts were returned!");
212         }
213         // Collect proper description for account types
214         mAuthDescs = AccountManager.get(context).getAuthenticatorTypes();
215         for (int i = 0; i < mAuthDescs.length; i++) {
216             mTypeToAuthDescription.put(mAuthDescs[i].type, mAuthDescs[i]);
217         }
218         mView = mActivity.getExpandableListView();
219         mRefresh = true;
220     }
221 
startRefreshStopDelay()222     public void startRefreshStopDelay() {
223         mRefresh = true;
224         mView.postDelayed(mStopRefreshing, REFRESH_DURATION);
225     }
226 
cancelRefreshStopDelay()227     public void cancelRefreshStopDelay() {
228         mView.removeCallbacks(mStopRefreshing);
229     }
230 
231     /*
232      * Write back the changes that have been made. The sync code will pick up any changes and
233      * do updates on its own.
234      */
doSaveAction()235     public void doSaveAction() {
236         // Cancel the previous operation
237         mCalendarsUpdater.cancelOperation(mUpdateToken);
238         mUpdateToken++;
239         // This is to allow us to do queries and updates with the same AsyncQueryHandler without
240         // accidently canceling queries.
241         if(mUpdateToken < MIN_UPDATE_TOKEN) {
242             mUpdateToken = MIN_UPDATE_TOKEN;
243         }
244 
245         Iterator<Long> changeKeys = mCalendarChanges.keySet().iterator();
246         while (changeKeys.hasNext()) {
247             long id = changeKeys.next();
248             boolean newSynced = mCalendarChanges.get(id);
249 
250             Uri uri = ContentUris.withAppendedId(Calendars.CONTENT_URI, id);
251             ContentValues values = new ContentValues();
252             values.put(Calendars.VISIBLE, newSynced ? 1 : 0);
253             values.put(Calendars.SYNC_EVENTS, newSynced ? 1 : 0);
254             mCalendarsUpdater.startUpdate(mUpdateToken, id, uri, values, null, null);
255         }
256     }
257 
setText(View view, int id, String text)258     private static void setText(View view, int id, String text) {
259         if (TextUtils.isEmpty(text)) {
260             return;
261         }
262         TextView textView = (TextView) view.findViewById(id);
263         textView.setText(text);
264     }
265 
266     /**
267      * Gets the label associated with a particular account type. If none found, return null.
268      * @param accountType the type of account
269      * @return a CharSequence for the label or null if one cannot be found.
270      */
getLabelForType(final String accountType)271     protected CharSequence getLabelForType(final String accountType) {
272         CharSequence label = null;
273         if (mTypeToAuthDescription.containsKey(accountType)) {
274              try {
275                  AuthenticatorDescription desc = mTypeToAuthDescription.get(accountType);
276                  Context authContext = mActivity.createPackageContext(desc.packageName, 0);
277                  label = authContext.getResources().getText(desc.labelId);
278              } catch (PackageManager.NameNotFoundException e) {
279                  Log.w(TAG, "No label for account type " + ", type " + accountType);
280              }
281         }
282         return label;
283     }
284 
285     @Override
bindChildView(View view, Context context, Cursor cursor, boolean isLastChild)286     protected void bindChildView(View view, Context context, Cursor cursor, boolean isLastChild) {
287         view.findViewById(R.id.color).setBackgroundColor(cursor.getInt(COLOR_COLUMN));
288         String name = cursor.getString(NAME_COLUMN);
289         String owner = cursor.getString(OWNER_COLUMN);
290         if (mIsDuplicateName.containsKey(name) && mIsDuplicateName.get(name) &&
291                 !name.equalsIgnoreCase(owner)) {
292             name = new StringBuilder(name)
293                     .append(Utils.OPEN_EMAIL_MARKER)
294                     .append(owner)
295                     .append(Utils.CLOSE_EMAIL_MARKER)
296                     .toString();
297         }
298         setText(view, R.id.calendar, name);
299 
300         // First see if the user has already changed the state of this calendar
301         long id = cursor.getLong(ID_COLUMN);
302         Boolean sync = mCalendarChanges.get(id);
303         if (sync == null) {
304             sync = cursor.getInt(SYNCED_COLUMN) == 1;
305             mCalendarInitialStates.put(id, sync);
306         }
307 
308         CheckBox button = (CheckBox) view.findViewById(R.id.sync);
309         button.setChecked(sync);
310         setText(view, R.id.status, sync ? mSyncedText : mNotSyncedText);
311 
312         view.setTag(TAG_ID_CALENDAR_ID, id);
313         view.setTag(TAG_ID_SYNC_CHECKBOX, button);
314         view.setOnClickListener(this);
315     }
316 
317     @Override
bindGroupView(View view, Context context, Cursor cursor, boolean isExpanded)318     protected void bindGroupView(View view, Context context, Cursor cursor, boolean isExpanded) {
319         int accountColumn = cursor.getColumnIndexOrThrow(Calendars.ACCOUNT_NAME);
320         int accountTypeColumn = cursor.getColumnIndexOrThrow(Calendars.ACCOUNT_TYPE);
321         String account = cursor.getString(accountColumn);
322         String accountType = cursor.getString(accountTypeColumn);
323         CharSequence accountLabel = getLabelForType(accountType);
324         setText(view, R.id.account, account);
325         if (accountLabel != null) {
326             setText(view, R.id.account_type, accountLabel.toString());
327         }
328     }
329 
330     @Override
getChildrenCursor(Cursor groupCursor)331     protected Cursor getChildrenCursor(Cursor groupCursor) {
332         int accountColumn = groupCursor.getColumnIndexOrThrow(Calendars.ACCOUNT_NAME);
333         int accountTypeColumn = groupCursor.getColumnIndexOrThrow(Calendars.ACCOUNT_TYPE);
334         String account = groupCursor.getString(accountColumn);
335         String accountType = groupCursor.getString(accountTypeColumn);
336         //Get all the calendars for just this account.
337         Cursor childCursor = mChildrenCursors.get(accountType + "#" + account);
338         new RefreshCalendars(groupCursor.getPosition(), account, accountType).run();
339         return childCursor;
340     }
341 
342     @Override
newChildView(Context context, Cursor cursor, boolean isLastChild, ViewGroup parent)343     protected View newChildView(Context context, Cursor cursor, boolean isLastChild,
344             ViewGroup parent) {
345         return mInflater.inflate(R.layout.calendar_sync_item, parent, false);
346     }
347 
348     @Override
newGroupView(Context context, Cursor cursor, boolean isExpanded, ViewGroup parent)349     protected View newGroupView(Context context, Cursor cursor, boolean isExpanded,
350             ViewGroup parent) {
351         return mInflater.inflate(R.layout.account_item, parent, false);
352     }
353 
354     private class RefreshCalendars implements Runnable {
355 
356         int mToken;
357         String mAccount;
358         String mAccountType;
359 
RefreshCalendars(int token, String account, String accountType)360         public RefreshCalendars(int token, String account, String accountType) {
361             mToken = token;
362             mAccount = account;
363             mAccountType = accountType;
364         }
365 
run()366         public void run() {
367             mCalendarsUpdater.cancelOperation(mToken);
368             // Set up a refresh for some point in the future if we haven't stopped updates yet
369             if(mRefresh) {
370                 mView.postDelayed(new RefreshCalendars(mToken, mAccount, mAccountType),
371                         REFRESH_DELAY);
372             }
373             mCalendarsUpdater.startQuery(mToken,
374                     mAccountType + "#" + mAccount,
375                     Calendars.CONTENT_URI, PROJECTION,
376                     ACCOUNT_SELECTION,
377                     new String[] { mAccount, mAccountType } /*selectionArgs*/,
378                     CALENDARS_ORDERBY);
379         }
380     }
381 }
382