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