Home | History | Annotate | Download | only in calendar
      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