/* * Copyright (C) 2007 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.calendar; import android.accounts.AccountManager; import android.accounts.AuthenticatorDescription; import android.content.AsyncQueryHandler; import android.content.ContentResolver; import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; import android.content.pm.PackageManager; import android.database.Cursor; import android.database.MatrixCursor; import android.net.Uri; import android.provider.Calendar.Calendars; import android.text.TextUtils; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.CursorTreeAdapter; import android.widget.TextView; import java.util.HashMap; import java.util.Iterator; import java.util.Map; public class SelectCalendarsAdapter extends CursorTreeAdapter implements View.OnClickListener { private static final String TAG = "Calendar"; private static final String COLLATE_NOCASE = " COLLATE NOCASE"; private static final String IS_PRIMARY = "\"primary\""; private static final String CALENDARS_ORDERBY = IS_PRIMARY + " DESC," + Calendars.DISPLAY_NAME + COLLATE_NOCASE; private static final String ACCOUNT_SELECTION = Calendars._SYNC_ACCOUNT + "=?" + " AND " + Calendars._SYNC_ACCOUNT_TYPE + "=?"; // The drawables used for the button to change the visible and sync states on a calendar private static final int[] SYNC_VIS_BUTTON_RES = new int[] { R.drawable.widget_show, R.drawable.widget_sync, R.drawable.widget_off }; private static int state_synced_visible = 0; private static int state_synced_not_visible = 1; private static int state_not_synced_not_visible = 2; // The drawables used for the button to change the visible and sync states on a calendar private static final int[] VIS_BUTTON_RES = new int[] { R.drawable.widget_off_sync_show, R.drawable.widget_off }; private static int state_local_not_synced_visible = 0; private static int state_local_not_synced_not_visible = 1; private static String syncedVisible; private static String syncedNotVisible; private static String notSyncedVisible; private static String notSyncedNotVisible; private final LayoutInflater mInflater; private final ContentResolver mResolver; private final SelectCalendarsActivity mActivity; private final View mView; private final static Runnable mStopRefreshing = new Runnable() { public void run() { mRefresh = false; } }; private Map<String, AuthenticatorDescription> mTypeToAuthDescription = new HashMap<String, AuthenticatorDescription>(); protected AuthenticatorDescription[] mAuthDescs; // These track changes to the visible (selected) and synced state of calendars private Map<Long, Boolean[]> mCalendarChanges = new HashMap<Long, Boolean[]>(); private Map<Long, Boolean[]> mCalendarInitialStates = new HashMap<Long, Boolean[]>(); private static final int SELECTED_INDEX = 0; private static final int SYNCED_INDEX = 1; private static final int SYNC_TYPE_INDEX = 2; private static final int CHANGES_SIZE = 3; // This is for keeping MatrixCursor copies so that we can requery in the background. private static Map<String, Cursor> mChildrenCursors = new HashMap<String, Cursor>(); private static AsyncCalendarsUpdater mCalendarsUpdater; // This is to keep our update tokens separate from other tokens. Since we cancel old updates // when a new update comes in, we'd like to leave a token space that won't be canceled. private static final int MIN_UPDATE_TOKEN = 1000; private static int mUpdateToken = MIN_UPDATE_TOKEN; // How long to wait between requeries of the calendars to see if anything has changed. private static final int REFRESH_DELAY = 5000; // How long to keep refreshing for private static final int REFRESH_DURATION = 60000; private static boolean mRefresh = true; private int mNumAccounts; // This is to keep track of whether or not multiple calendars have the same display name private static HashMap<String, Boolean> mIsDuplicateName = new HashMap<String, Boolean>(); private static final String[] PROJECTION = new String[] { Calendars._ID, Calendars._SYNC_ACCOUNT, Calendars.OWNER_ACCOUNT, Calendars.DISPLAY_NAME, Calendars.COLOR, Calendars.SELECTED, Calendars.SYNC_EVENTS, Calendars._SYNC_ACCOUNT_TYPE, "(" + Calendars._SYNC_ACCOUNT + "=" + Calendars.OWNER_ACCOUNT + ") AS " + IS_PRIMARY, }; //Keep these in sync with the projection private static final int ID_COLUMN = 0; private static final int ACCOUNT_COLUMN = 1; private static final int OWNER_COLUMN = 2; private static final int NAME_COLUMN = 3; private static final int COLOR_COLUMN = 4; private static final int SELECTED_COLUMN = 5; private static final int SYNCED_COLUMN = 6; private static final int SYNCED_TYPE_COLUMN = 7; private static final int PRIMARY_COLUMN = 8; private class AsyncCalendarsUpdater extends AsyncQueryHandler { public AsyncCalendarsUpdater(ContentResolver cr) { super(cr); } @Override protected void onQueryComplete(int token, Object cookie, Cursor cursor) { if(cursor == null) { return; } Cursor currentCursor = mChildrenCursors.get(cookie); // Check if the new cursor has the same content as our old cursor if (currentCursor != null) { if (Utils.compareCursors(currentCursor, cursor)) { cursor.close(); return; } } // If not then make a new matrix cursor for our Map MatrixCursor newCursor = Utils.matrixCursorFromCursor(cursor); cursor.close(); // And update our list of duplicated names Utils.checkForDuplicateNames(mIsDuplicateName, newCursor, NAME_COLUMN); mChildrenCursors.put((String)cookie, newCursor); try { setChildrenCursor(token, newCursor); mActivity.startManagingCursor(newCursor); } catch (NullPointerException e) { Log.w(TAG, "Adapter expired, try again on the next query: " + e); } // Clean up our old cursor if we had one. We have to do this after setting the new // cursor so that our view doesn't throw on an invalid cursor. if (currentCursor != null) { mActivity.stopManagingCursor(currentCursor); currentCursor.close(); } } } /** * Method for changing the sync/vis state when a calendar's button is pressed. * * This gets called when the MultiStateButton for a calendar is clicked. It cycles the sync/vis * state for the associated calendar and saves a change of state to a hashmap. It also compares * against the original value and removes any changes from the hashmap if this is back * at its initial state. */ public void onClick(View v) { View view = (View)v.getTag(); long id = (Long)view.getTag(); Uri uri = ContentUris.withAppendedId(Calendars.CONTENT_URI, id); String status = syncedNotVisible; Boolean[] change; Boolean[] initialState = mCalendarInitialStates.get(id); if (mCalendarChanges.containsKey(id)) { change = mCalendarChanges.get(id); } else { change = new Boolean[CHANGES_SIZE]; change[SELECTED_INDEX] = initialState[SELECTED_INDEX]; change[SYNCED_INDEX] = initialState[SYNCED_INDEX]; change[SYNC_TYPE_INDEX] = initialState[SYNC_TYPE_INDEX]; mCalendarChanges.put(id, change); } //local calendar if(change[SYNC_TYPE_INDEX]){ if (change[SELECTED_INDEX]) { change[SELECTED_INDEX] = false; status = notSyncedNotVisible; }else{ change[SELECTED_INDEX] = true; status = notSyncedVisible; } } else { if (change[SELECTED_INDEX]&&change[SYNCED_INDEX]) { change[SELECTED_INDEX] = false; status = syncedNotVisible; } else if (change[SELECTED_INDEX]||change[SYNCED_INDEX]) { change[SELECTED_INDEX] = false; change[SYNCED_INDEX] = false; status = notSyncedNotVisible; } else { change[SYNCED_INDEX] = true; change[SELECTED_INDEX] = true; status = syncedVisible; } } setText(view, R.id.status, status); if (change[SELECTED_INDEX] == initialState[SELECTED_INDEX] && change[SYNCED_INDEX] == initialState[SYNCED_INDEX]) { mCalendarChanges.remove(id); } } public SelectCalendarsAdapter(Context context, Cursor cursor, SelectCalendarsActivity act) { super(cursor, context); syncedVisible = context.getString(R.string.synced_visible); syncedNotVisible = context.getString(R.string.synced_not_visible); notSyncedVisible = context.getString(R.string.not_synced_visible); notSyncedNotVisible = context.getString(R.string.not_synced_not_visible); mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); mResolver = context.getContentResolver(); mActivity = act; if (mCalendarsUpdater == null) { mCalendarsUpdater = new AsyncCalendarsUpdater(mResolver); } mNumAccounts = cursor.getCount(); if(mNumAccounts == 0) { //Should never happen since Calendar requires an account exist to use it. Log.e(TAG, "SelectCalendarsAdapter: No accounts were returned!"); } //Collect proper description for account types mAuthDescs = AccountManager.get(context).getAuthenticatorTypes(); for (int i = 0; i < mAuthDescs.length; i++) { mTypeToAuthDescription.put(mAuthDescs[i].type, mAuthDescs[i]); } mView = mActivity.getExpandableListView(); mRefresh = true; } public void startRefreshStopDelay() { mRefresh = true; mView.postDelayed(mStopRefreshing, REFRESH_DURATION); } public void cancelRefreshStopDelay() { mView.removeCallbacks(mStopRefreshing); } /* * Write back the changes that have been made. The sync code will pick up any changes and * do updates on its own. */ public void doSaveAction() { // Cancel the previous operation mCalendarsUpdater.cancelOperation(mUpdateToken); mUpdateToken++; // This is to allow us to do queries and updates with the same AsyncQueryHandler without // accidently canceling queries. if(mUpdateToken < MIN_UPDATE_TOKEN) mUpdateToken = MIN_UPDATE_TOKEN; Iterator<Long> changeKeys = mCalendarChanges.keySet().iterator(); while (changeKeys.hasNext()) { long id = changeKeys.next(); Boolean[] change = mCalendarChanges.get(id); int newSelected = change[SELECTED_INDEX] ? 1 : 0; int newSynced = change[SYNCED_INDEX] ? 1 : 0; Uri uri = ContentUris.withAppendedId(Calendars.CONTENT_URI, id); ContentValues values = new ContentValues(); values.put(Calendars.SELECTED, newSelected); values.put(Calendars.SYNC_EVENTS, newSynced); mCalendarsUpdater.startUpdate(mUpdateToken, id, uri, values, null, null); } } private static void setText(View view, int id, String text) { if (TextUtils.isEmpty(text)) { return; } TextView textView = (TextView) view.findViewById(id); textView.setText(text); } /** * Gets the label associated with a particular account type. If none found, return null. * @param accountType the type of account * @return a CharSequence for the label or null if one cannot be found. */ protected CharSequence getLabelForType(Context cxt, final String accountType) { CharSequence label = null; if(accountType.equals("Local")){ label = cxt.getText(R.string.local_account); } else if (mTypeToAuthDescription.containsKey(accountType)) { try { AuthenticatorDescription desc = mTypeToAuthDescription.get(accountType); Context authContext = mActivity.createPackageContext(desc.packageName, 0); label = authContext.getResources().getText(desc.labelId); } catch (PackageManager.NameNotFoundException e) { Log.w(TAG, "No label for account type " + ", type " + accountType); } } return label; } @Override protected void bindChildView(View view, Context context, Cursor cursor, boolean isLastChild) { String account = cursor.getString(ACCOUNT_COLUMN); int position = cursor.getPosition(); long id = cursor.getLong(ID_COLUMN); String status = notSyncedNotVisible; int state = state_not_synced_not_visible; // First see if the user has already changed the state of this calendar Boolean[] initialState = mCalendarChanges.get(id); // if we haven't already started making changes update the initial state in case it changed if (initialState == null) { initialState = new Boolean[CHANGES_SIZE]; initialState[SELECTED_INDEX] = cursor.getInt(SELECTED_COLUMN) == 1; initialState[SYNCED_INDEX] = cursor.getInt(SYNCED_COLUMN) == 1; initialState[SYNC_TYPE_INDEX] = cursor.getString(SYNCED_TYPE_COLUMN).equals("Local"); mCalendarInitialStates.put(id, initialState); } if (initialState[SYNC_TYPE_INDEX]) { status = notSyncedNotVisible; state = state_local_not_synced_not_visible; } else { status = notSyncedNotVisible; state = state_not_synced_not_visible; } if(initialState[SYNC_TYPE_INDEX]){ if(initialState[SELECTED_INDEX]) { status = notSyncedVisible; state = state_local_not_synced_visible; } else { status = notSyncedNotVisible; state = state_local_not_synced_not_visible; } } else { if(initialState[SYNCED_INDEX]) { if(initialState[SELECTED_INDEX]) { status = syncedVisible; state = state_synced_visible; } else { status = syncedNotVisible; state = state_synced_not_visible; } } else { if (initialState[SELECTED_INDEX]) { status = notSyncedVisible; state = state_not_synced_not_visible; Log.e("SelectCalendarsAdapter","it should not get this state!!!"); } else { status = notSyncedNotVisible; state = state_not_synced_not_visible; } } } view.findViewById(R.id.color) .setBackgroundDrawable(Utils.getColorChip(cursor.getInt(COLOR_COLUMN))); String name = cursor.getString(NAME_COLUMN); String owner = cursor.getString(OWNER_COLUMN); if (mIsDuplicateName.containsKey(name) && mIsDuplicateName.get(name) && !name.equalsIgnoreCase(owner)) { name = new StringBuilder(name) .append(Utils.OPEN_EMAIL_MARKER) .append(owner) .append(Utils.CLOSE_EMAIL_MARKER) .toString(); } setText(view, R.id.calendar, name); setText(view, R.id.status, status); MultiStateButton button = (MultiStateButton) view.findViewById(R.id.multiStateButton); //Set up the listeners so a click on the button will change the state. //The view already uses the onChildClick method in the activity. button.setTag(view); view.setTag(id); button.setOnClickListener(this); if (initialState[SYNC_TYPE_INDEX]) button.setButtonResources(VIS_BUTTON_RES); else button.setButtonResources(SYNC_VIS_BUTTON_RES); button.setState(state); } @Override protected void bindGroupView(View view, Context context, Cursor cursor, boolean isExpanded) { int accountColumn = cursor.getColumnIndexOrThrow(Calendars._SYNC_ACCOUNT); int accountTypeColumn = cursor.getColumnIndexOrThrow(Calendars._SYNC_ACCOUNT_TYPE); String account = cursor.getString(accountColumn); String accountType = cursor.getString(accountTypeColumn); setText(view, R.id.account, account); setText(view, R.id.account_type, getLabelForType(context,accountType).toString()); } @Override protected Cursor getChildrenCursor(Cursor groupCursor) { int accountColumn = groupCursor.getColumnIndexOrThrow(Calendars._SYNC_ACCOUNT); int accountTypeColumn = groupCursor.getColumnIndexOrThrow(Calendars._SYNC_ACCOUNT_TYPE); String account = groupCursor.getString(accountColumn); String accountType = groupCursor.getString(accountTypeColumn); //Get all the calendars for just this account. Cursor childCursor = mChildrenCursors.get(account); new RefreshCalendars(groupCursor.getPosition(), account, accountType).run(); return childCursor; } @Override protected View newChildView(Context context, Cursor cursor, boolean isLastChild, ViewGroup parent) { return mInflater.inflate(R.layout.calendar_item, parent, false); } @Override protected View newGroupView(Context context, Cursor cursor, boolean isExpanded, ViewGroup parent) { return mInflater.inflate(R.layout.account_item, parent, false); } private class RefreshCalendars implements Runnable { int mToken; String mAccount; String mAccountType; public RefreshCalendars(int token, String cookie, String accountType) { mToken = token; mAccount = cookie; mAccountType = accountType; } public void run() { mCalendarsUpdater.cancelOperation(mToken); // Set up a refresh for some point in the future if we haven't stopped updates yet if(mRefresh) { mView.postDelayed(new RefreshCalendars(mToken, mAccount, mAccountType), REFRESH_DELAY); } mCalendarsUpdater.startQuery(mToken, mAccount, Calendars.CONTENT_URI, PROJECTION, ACCOUNT_SELECTION, new String[] { mAccount, mAccountType } /*selectionArgs*/, CALENDARS_ORDERBY); } } }