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