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