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 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