blob: 91610c652e2a66eedddf87e6a802fd5970fedfef [file] [log] [blame]
/*
* Copyright (C) 2011 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.selectcalendars;
import android.accounts.AccountManager;
import android.accounts.AuthenticatorDescription;
import android.app.FragmentManager;
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.graphics.Rect;
import android.net.Uri;
import android.provider.CalendarContract.Calendars;
import android.text.TextUtils;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.TouchDelegate;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.CursorTreeAdapter;
import android.widget.TextView;
import com.android.calendar.CalendarColorPickerDialog;
import com.android.calendar.R;
import com.android.calendar.Utils;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
public class SelectSyncedCalendarsMultiAccountAdapter extends CursorTreeAdapter implements
View.OnClickListener {
private static final String TAG = "Calendar";
private static final String COLOR_PICKER_DIALOG_TAG = "ColorPickerDialog";
private static final String IS_PRIMARY = "\"primary\"";
private static final String CALENDARS_ORDERBY = IS_PRIMARY + " DESC,"
+ Calendars.CALENDAR_DISPLAY_NAME + " COLLATE NOCASE";
private static final String ACCOUNT_SELECTION = Calendars.ACCOUNT_NAME + "=?"
+ " AND " + Calendars.ACCOUNT_TYPE + "=?";
private final LayoutInflater mInflater;
private final ContentResolver mResolver;
private final SelectSyncedCalendarsMultiAccountActivity mActivity;
private final FragmentManager mFragmentManager;
private final boolean mIsTablet;
private CalendarColorPickerDialog mColorPickerDialog;
private final View mView;
private final static Runnable mStopRefreshing = new Runnable() {
@Override
public void run() {
mRefresh = false;
}
};
private Map<String, AuthenticatorDescription> mTypeToAuthDescription
= new HashMap<String, AuthenticatorDescription>();
protected AuthenticatorDescription[] mAuthDescs;
// These track changes to the synced state of calendars
private Map<Long, Boolean> mCalendarChanges
= new HashMap<Long, Boolean>();
private Map<Long, Boolean> mCalendarInitialStates
= new HashMap<Long, Boolean>();
// Flag for when the cursors have all been closed to ensure no race condition with queries.
private boolean mClosedCursorsFlag;
// This is for keeping MatrixCursor copies so that we can requery in the background.
private Map<String, Cursor> mChildrenCursors
= new HashMap<String, Cursor>();
private 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 static String mSyncedText;
private static String mNotSyncedText;
// 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 int mColorViewTouchAreaIncrease;
private static final String[] PROJECTION = new String[] {
Calendars._ID,
Calendars.ACCOUNT_NAME,
Calendars.OWNER_ACCOUNT,
Calendars.CALENDAR_DISPLAY_NAME,
Calendars.CALENDAR_COLOR,
Calendars.VISIBLE,
Calendars.SYNC_EVENTS,
"(" + Calendars.ACCOUNT_NAME + "=" + 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 PRIMARY_COLUMN = 7;
private static final int TAG_ID_CALENDAR_ID = R.id.calendar;
private static final int TAG_ID_SYNC_CHECKBOX = R.id.sync;
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;
}
synchronized(mChildrenCursors) {
if (mClosedCursorsFlag || (mActivity != null && mActivity.isFinishing())) {
cursor.close();
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);
} 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) {
currentCursor.close();
}
}
}
/**
* Method for changing the sync state when a calendar's button is pressed.
*
* This gets called when the CheckBox for a calendar is clicked. It toggles
* the sync 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.
*/
@Override
public void onClick(View v) {
long id = (Long) v.getTag(TAG_ID_CALENDAR_ID);
boolean newState;
boolean initialState = mCalendarInitialStates.get(id);
if (mCalendarChanges.containsKey(id)) {
// Negate to reflect the click
newState = !mCalendarChanges.get(id);
} else {
// Negate to reflect the click
newState = !initialState;
}
if (newState == initialState) {
mCalendarChanges.remove(id);
} else {
mCalendarChanges.put(id, newState);
}
((CheckBox) v.getTag(TAG_ID_SYNC_CHECKBOX)).setChecked(newState);
setText(v, R.id.status, newState ? mSyncedText : mNotSyncedText);
}
public SelectSyncedCalendarsMultiAccountAdapter(Context context, Cursor acctsCursor,
SelectSyncedCalendarsMultiAccountActivity act) {
super(acctsCursor, context);
mSyncedText = context.getString(R.string.synced);
mNotSyncedText = context.getString(R.string.not_synced);
mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
mResolver = context.getContentResolver();
mActivity = act;
mFragmentManager = act.getFragmentManager();
mColorPickerDialog = (CalendarColorPickerDialog)
mFragmentManager.findFragmentByTag(COLOR_PICKER_DIALOG_TAG);
mIsTablet = Utils.getConfigBool(context, R.bool.tablet_config);
if (mCalendarsUpdater == null) {
mCalendarsUpdater = new AsyncCalendarsUpdater(mResolver);
}
if (acctsCursor == null || acctsCursor.getCount() == 0) {
Log.i(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;
mClosedCursorsFlag = false;
mColorViewTouchAreaIncrease = context.getResources()
.getDimensionPixelSize(R.dimen.color_view_touch_area_increase);
}
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 newSynced = mCalendarChanges.get(id);
Uri uri = ContentUris.withAppendedId(Calendars.CONTENT_URI, id);
ContentValues values = new ContentValues();
values.put(Calendars.VISIBLE, newSynced ? 1 : 0);
values.put(Calendars.SYNC_EVENTS, newSynced ? 1 : 0);
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(final String accountType) {
CharSequence label = null;
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) {
final long id = cursor.getLong(ID_COLUMN);
String name = cursor.getString(NAME_COLUMN);
String owner = cursor.getString(OWNER_COLUMN);
int color = Utils.getDisplayColorFromColor(cursor.getInt(COLOR_COLUMN));
final View colorSquare = view.findViewById(R.id.color);
colorSquare.setBackgroundColor(color);
final View delegateParent = (View) colorSquare.getParent();
delegateParent.post(new Runnable() {
@Override
public void run() {
final Rect r = new Rect();
colorSquare.getHitRect(r);
r.top -= mColorViewTouchAreaIncrease;
r.bottom += mColorViewTouchAreaIncrease;
r.left -= mColorViewTouchAreaIncrease;
r.right += mColorViewTouchAreaIncrease;
delegateParent.setTouchDelegate(new TouchDelegate(r, colorSquare));
}
});
colorSquare.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (mColorPickerDialog == null) {
mColorPickerDialog = CalendarColorPickerDialog.newInstance(id, mIsTablet);
} else {
mColorPickerDialog.setCalendarId(id);
}
mFragmentManager.executePendingTransactions();
if (!mColorPickerDialog.isAdded()) {
mColorPickerDialog.show(mFragmentManager, COLOR_PICKER_DIALOG_TAG);
}
}
});
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);
// First see if the user has already changed the state of this calendar
Boolean sync = mCalendarChanges.get(id);
if (sync == null) {
sync = cursor.getInt(SYNCED_COLUMN) == 1;
mCalendarInitialStates.put(id, sync);
}
CheckBox button = (CheckBox) view.findViewById(R.id.sync);
button.setChecked(sync);
setText(view, R.id.status, sync ? mSyncedText : mNotSyncedText);
view.setTag(TAG_ID_CALENDAR_ID, id);
view.setTag(TAG_ID_SYNC_CHECKBOX, button);
view.setOnClickListener(this);
}
@Override
protected void bindGroupView(View view, Context context, Cursor cursor, boolean isExpanded) {
int accountColumn = cursor.getColumnIndexOrThrow(Calendars.ACCOUNT_NAME);
int accountTypeColumn = cursor.getColumnIndexOrThrow(Calendars.ACCOUNT_TYPE);
String account = cursor.getString(accountColumn);
String accountType = cursor.getString(accountTypeColumn);
CharSequence accountLabel = getLabelForType(accountType);
setText(view, R.id.account, account);
if (accountLabel != null) {
setText(view, R.id.account_type, accountLabel.toString());
}
}
@Override
protected Cursor getChildrenCursor(Cursor groupCursor) {
int accountColumn = groupCursor.getColumnIndexOrThrow(Calendars.ACCOUNT_NAME);
int accountTypeColumn = groupCursor.getColumnIndexOrThrow(Calendars.ACCOUNT_TYPE);
String account = groupCursor.getString(accountColumn);
String accountType = groupCursor.getString(accountTypeColumn);
//Get all the calendars for just this account.
Cursor childCursor = mChildrenCursors.get(accountType + "#" + 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_sync_item, parent, false);
}
@Override
protected View newGroupView(Context context, Cursor cursor, boolean isExpanded,
ViewGroup parent) {
return mInflater.inflate(R.layout.account_item, parent, false);
}
public void closeChildrenCursors() {
synchronized (mChildrenCursors) {
for (String key : mChildrenCursors.keySet()) {
Cursor cursor = mChildrenCursors.get(key);
if (!cursor.isClosed()) {
cursor.close();
}
}
mChildrenCursors.clear();
mClosedCursorsFlag = true;
}
}
private class RefreshCalendars implements Runnable {
int mToken;
String mAccount;
String mAccountType;
public RefreshCalendars(int token, String account, String accountType) {
mToken = token;
mAccount = account;
mAccountType = accountType;
}
@Override
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,
mAccountType + "#" + mAccount,
Calendars.CONTENT_URI, PROJECTION,
ACCOUNT_SELECTION,
new String[] { mAccount, mAccountType } /*selectionArgs*/,
CALENDARS_ORDERBY);
}
}
}