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