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 124 public AsyncCalendarsUpdater(ContentResolver cr) { 125 super(cr); 126 } 127 128 @Override 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 */ 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 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 222 public void startRefreshStopDelay() { 223 mRefresh = true; 224 mView.postDelayed(mStopRefreshing, REFRESH_DURATION); 225 } 226 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 */ 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 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 */ 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 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 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 setText(view, R.id.account, account); 324 setText(view, R.id.account_type, getLabelForType(accountType).toString()); 325 } 326 327 @Override 328 protected Cursor getChildrenCursor(Cursor groupCursor) { 329 int accountColumn = groupCursor.getColumnIndexOrThrow(Calendars.ACCOUNT_NAME); 330 int accountTypeColumn = groupCursor.getColumnIndexOrThrow(Calendars.ACCOUNT_TYPE); 331 String account = groupCursor.getString(accountColumn); 332 String accountType = groupCursor.getString(accountTypeColumn); 333 //Get all the calendars for just this account. 334 Cursor childCursor = mChildrenCursors.get(account); 335 new RefreshCalendars(groupCursor.getPosition(), account, accountType).run(); 336 return childCursor; 337 } 338 339 @Override 340 protected View newChildView(Context context, Cursor cursor, boolean isLastChild, 341 ViewGroup parent) { 342 return mInflater.inflate(R.layout.calendar_sync_item, parent, false); 343 } 344 345 @Override 346 protected View newGroupView(Context context, Cursor cursor, boolean isExpanded, 347 ViewGroup parent) { 348 return mInflater.inflate(R.layout.account_item, parent, false); 349 } 350 351 private class RefreshCalendars implements Runnable { 352 353 int mToken; 354 String mAccount; 355 String mAccountType; 356 357 public RefreshCalendars(int token, String cookie, String accountType) { 358 mToken = token; 359 mAccount = cookie; 360 mAccountType = accountType; 361 } 362 363 public void run() { 364 mCalendarsUpdater.cancelOperation(mToken); 365 // Set up a refresh for some point in the future if we haven't stopped updates yet 366 if(mRefresh) { 367 mView.postDelayed(new RefreshCalendars(mToken, mAccount, mAccountType), 368 REFRESH_DELAY); 369 } 370 mCalendarsUpdater.startQuery(mToken, 371 mAccount, 372 Calendars.CONTENT_URI, PROJECTION, 373 ACCOUNT_SELECTION, 374 new String[] { mAccount, mAccountType } /*selectionArgs*/, 375 CALENDARS_ORDERBY); 376 } 377 } 378 } 379