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