org.dmfs.webcal.fragments.CalendarItemFragment.java Source code

Java tutorial

Introduction

Here is the source code for org.dmfs.webcal.fragments.CalendarItemFragment.java

Source

/*
 * Copyright (C) 2014 SchedJoules
 *
 * 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 org.dmfs.webcal.fragments;

import java.net.URI;
import java.util.TimeZone;

import org.dmfs.android.calendarcontent.provider.CalendarContentContract;
import org.dmfs.android.calendarcontent.provider.CalendarContentContract.ContentItem;
import org.dmfs.android.calendarcontent.provider.CalendarContentContract.PaymentStatus;
import org.dmfs.android.calendarcontent.provider.CalendarContentContract.SubscribedCalendars;
import org.dmfs.android.calendarcontent.provider.CalendarContentContract.SubscriptionId;
import org.dmfs.android.retentionmagic.annotations.Parameter;
import org.dmfs.android.webcalreader.provider.WebCalReaderContract;
import org.dmfs.asynctools.PetriNet;
import org.dmfs.asynctools.PetriNet.Place;
import org.dmfs.asynctools.PetriNet.Transition;
import org.dmfs.webcal.EventsPreviewActivity;
import org.dmfs.webcal.R;
import org.dmfs.webcal.adapters.EventListAdapter;
import org.dmfs.webcal.adapters.SectionTitlesAdapter;
import org.dmfs.webcal.adapters.SectionTitlesAdapter.SectionIndexer;
import org.dmfs.webcal.fragments.CalendarTitleFragment.SwitchStatusListener;
import org.dmfs.webcal.utils.Event;
import org.dmfs.webcal.utils.ProtectedBackgroundJob;

import android.app.ActionBar;
import android.app.Activity;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.text.format.DateUtils;
import android.text.format.Time;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ListView;
import android.widget.ProgressBar;

import com.schedjoules.analytics.Analytics;

public class CalendarItemFragment extends SubscribeableItemFragment
        implements LoaderManager.LoaderCallbacks<Cursor>, SwitchStatusListener, OnItemClickListener {
    /**
     * The time to wait before showing a progress indicator. Often the list loads much faster and we don't want the indicator to flash up.
     */
    private final static int PROGRESS_INDICATOR_DELAY = 50;

    private static final String TAG = "CalendarItemFragment";

    private static final String ARG_CONTENT_URI = "content_uri";
    private static final String ARG_TITLE = "title";
    private static final String ARG_ICON = "icon";

    private static final int LOADER_CALENDAR_ITEM = 24234;
    private static final int LOADER_SUBSCRIBED_CALENDAR = LOADER_CALENDAR_ITEM + 1;
    private static final int LOADER_SUBSCRIPTION = LOADER_CALENDAR_ITEM + 2;
    private static final int LOADER_PREVIEW = LOADER_CALENDAR_ITEM + 3;

    /**
     * The projection we use when loading the calendar item.
     */
    private final static String[] PROJECTION = new String[] { CalendarContentContract.ContentItem.TITLE,
            CalendarContentContract.ContentItem.ICON_ID, CalendarContentContract.ContentItem.URL,
            ContentItem.STARRED, ContentItem._ID };

    /**
     * Column indices of the {@link CalendarItemFragment#PROJECTION} we use.
     */
    private interface COLUMNS {
        int TITLE = 0;
        int ICON = 1;
        int URL = 2;
        int STARRED = 3;
        @SuppressWarnings("unused")
        int ID = 4;
    }

    /**
     * The id of the calendar item.
     */
    private long mId;

    /**
     * The content {@link Uri} of the calendar item.
     */
    @Parameter(key = ARG_CONTENT_URI)
    private Uri mContentUri;

    /**
     * The title of the calendar item.
     */
    @Parameter(key = ARG_TITLE)
    private String mTitle;

    /**
     * The icon is initialized with the icon passed to {@link #newInstance(Uri, String, long)} or {@link #newInstance(Context, long, String, long)}. If the
     * calendar has it's own icon, it replaces the initial icon later on.
     */
    @Parameter(key = ARG_ICON)
    private long mIcon;

    /**
     * The current sync status of the calendar.
     */
    private boolean mSynced = false;

    /**
     * The current favorite status of the calendar.
     */
    private boolean mStarred;

    /**
     * The {@link Uri} of the calendar subscription, if there is any.
     */
    private Uri mSubscriptionUri;

    /**
     * The name of the calendar.
     */
    private String mCalendarName;

    /**
     * The actual URL of the calendar data.
     */
    private URI mCalendarUrl;

    private CalendarTitleFragment mTitleFragment;

    private ProgressBar mProgressBar;
    private EventListAdapter mListAdapter;
    private ActionBar mActionBar;
    private SectionTitlesAdapter mSectionAdapter;
    private ListView mListView;

    private final Place mWaitingForCalendarItem = new Place(1);
    private final Place mWaitingForCalendarSubscription = new Place(1);
    private final Place mWaitingForPayment = new Place(1);
    private final Place mDone = new Place();

    /**
     * This {@link Transition} is fired whenever the payment status changes or when a free trial is started or ends.
     */
    private Transition<Void> mPaymentStatusUpdated = new Transition<Void>(mWaitingForPayment, mWaitingForPayment,
            mDone) {
        @Override
        protected void execute(Void data) {
            // nothing to do, this just enables the mDone place to update the UI
        }
    };

    /**
     * This {@link Transition} is fired whenever the calendar item is (re-) loaded.
     */
    private Transition<Cursor> mItemLoaded = new Transition<Cursor>(mWaitingForCalendarItem,
            mWaitingForCalendarItem) {

        @Override
        protected void execute(Cursor cursor) {
            if (cursor != null && cursor.moveToFirst()) {
                /*
                 * We have an item.
                 * 
                 * Get the values and update the UI as good as we can.
                 */

                // load the icon if there is any
                long iconId = cursor.getLong(COLUMNS.ICON);
                if (iconId > 0) {
                    mIcon = iconId;
                }

                // load the calendar title
                mCalendarName = cursor.getString(COLUMNS.TITLE);

                // get the calendar URL
                try {
                    mCalendarUrl = URI.create(cursor.getString(COLUMNS.URL));
                } catch (IllegalArgumentException e) {
                    // TODO: not a valid URI, we shouldn't continue
                }

                mStarred = cursor.getInt(COLUMNS.STARRED) > 0;

                // update the UI
                mTitleFragment.setTitle(mCalendarName);
                mTitleFragment.setIcon(mIcon);
                mTitleFragment.setId(mId);
                mTitleFragment.setStarred(mStarred);

                // update action bar
                if (mActionBar != null) {
                    mActionBar.setTitle(mCalendarName);
                    mActionBar.setSubtitle(mTitle);
                }

                // update the adapter if necessary
                if (mListAdapter.getCursor() == null) {
                    loadCalendar();
                }
            }
        }
    };

    /**
     * A {@link Transition} that's fired whenever the calendar subscription has been loaded.
     */
    private Transition<Cursor> mSubscriptionLoaded = new Transition<Cursor>(mWaitingForCalendarSubscription,
            mWaitingForCalendarSubscription, mDone) {

        @Override
        protected void execute(Cursor cursor) {
            // the subscription has been loaded (or not)
            if (cursor != null && cursor.moveToFirst()) {
                // we have a subscription

                // get the id
                long id = cursor.getLong(cursor.getColumnIndex(SubscribedCalendars._ID));

                // build the Uri
                mSubscriptionUri = SubscribedCalendars.getItemContentUri(getActivity(), id);

                // get the sync status
                mSynced = cursor.getInt(cursor.getColumnIndex(SubscribedCalendars.SYNC_ENABLED)) > 0;
            }

            // set the sync switch accordingly
            mTitleFragment.setSwitchChecked(mSynced);

            // invalidate options to show/hide settings option
            getActivity().invalidateOptionsMenu();
        }
    };

    /**
     * This {@link Transition} is fired after {@link #mPaymentStatusUpdated} and {@link #mSubscriptionLoaded} are fired.
     */
    private Transition<Void> mEnableSwitch = new Transition<Void>(2, mDone, 1, mDone) {
        @Override
        protected void execute(Void data) {
            mTitleFragment.enableSwitch(isPurchased() || inFreeTrial());
        }
    }.setAutoFire(true);

    /**
     * The {@link PetriNet} engine.
     */
    private PetriNet mPetriNet = new PetriNet(mPaymentStatusUpdated, mItemLoaded, mSubscriptionLoaded,
            mEnableSwitch);
    private final Handler mHandler = new Handler();

    /**
     * Create a new CalendarItemFragment.
     * 
     * @param contentUri
     *            The content {@link Uri} of the calendar item.
     * @param title
     *            The title of the page.
     * @param icon
     *            The icon id of the calendar.
     * @return The {@link CalendarItemFragment}.
     */
    public static CalendarItemFragment newInstance(Uri contentUri, String title, long icon) {
        CalendarItemFragment result = new CalendarItemFragment();
        Bundle args = new Bundle();
        args.putParcelable(ARG_CONTENT_URI, contentUri);
        args.putString(ARG_TITLE, title);
        args.putLong(ARG_ICON, icon);
        result.setArguments(args);
        return result;
    }

    /**
     * Create a new CalendarItemFragment.
     * 
     * @param context
     *            A {@link Context}.
     * @param id
     *            The calendar item id.
     * @param title
     *            The title of the page.
     * @param icon
     *            The icon id of the calendar.
     * @return The {@link CalendarItemFragment}.
     */
    public static CalendarItemFragment newInstance(Context context, long id, String title, long icon) {
        return newInstance(ContentItem.getItemContentUri(context, id), title, icon);
    }

    public CalendarItemFragment() {
        // Obligatory unparameterized constructor
    }

    @Override
    public void onAttach(final Activity activity) {
        super.onAttach(activity);
        mActionBar = activity.getActionBar();
        mActionBar.removeAllTabs();
        mActionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
    }

    @Override
    public View onCreateItemView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View returnView = inflater.inflate(R.layout.fragment_calendar_item, container, false);

        // TODO: don't put the progress indicator into the header view
        View progressView = inflater.inflate(R.layout.progress_indicator, null, false);

        mProgressBar = (ProgressBar) progressView.findViewById(android.R.id.progress);

        mListView = (ListView) returnView.findViewById(android.R.id.list);

        mListView.addHeaderView(progressView);
        mListView.setOnItemClickListener(this);
        mListView.setHeaderDividersEnabled(false);
        mListAdapter = new EventListAdapter(inflater.getContext(), null);
        mListView.setAdapter(mSectionAdapter = new SectionTitlesAdapter(inflater.getContext(), mListAdapter,
                new SectionIndexer() {

                    @Override
                    public String getSectionTitle(int index) {
                        Time start = new Time(TimeZone.getDefault().getID());
                        start.set(index & 0x00ff, (index >> 8) & 0x00ff, (index >> 16) & 0x0ffff);

                        return DateUtils.formatDateTime(getActivity(), start.toMillis(true),
                                DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR
                                        | DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_ABBREV_WEEKDAY);
                    }

                    @Override
                    public int getSectionIndex(Object object) {
                        Cursor cursor = (Cursor) object;

                        Time start = new Time(
                                cursor.getString(cursor.getColumnIndex(WebCalReaderContract.Events.TIMZONE)));
                        start.set(cursor.getLong(cursor.getColumnIndex(WebCalReaderContract.Events.DTSTART)));
                        boolean allday = cursor
                                .getInt(cursor.getColumnIndex(WebCalReaderContract.Events.IS_ALLDAY)) == 1;
                        start.allDay = allday;

                        // we return an encoded date as index
                        return (start.year << 16) + (start.month << 8) + start.monthDay;

                    }
                }, R.layout.events_preview_list_section_header));

        FragmentManager fm = getChildFragmentManager();
        FragmentTransaction ft = fm.beginTransaction();

        mTitleFragment = (CalendarTitleFragment) fm.findFragmentById(R.id.calendar_title_fragment_container);
        if (mTitleFragment == null) {
            mTitleFragment = CalendarTitleFragment.newInstance();
            ft.replace(R.id.calendar_title_fragment_container, mTitleFragment);
        }

        if (!ft.isEmpty()) {
            ft.commit();
        }

        LoaderManager lm = getLoaderManager();
        lm.initLoader(LOADER_CALENDAR_ITEM, null, this);
        lm.initLoader(LOADER_SUBSCRIBED_CALENDAR, null, this);
        lm.initLoader(LOADER_SUBSCRIPTION, null, this);

        // set this to true, so the menu is cleared automatically when leaving the fragment, otherwise the star icon will stay visible
        setHasOptionsMenu(true);

        return returnView;
    }

    @Override
    public void onStart() {
        super.onStart();
        mId = ContentUris.parseId(mContentUri);

        Analytics.screen(mTitle + "/calendar_preview", null, String.valueOf(ContentItem.getApiId(mId)));
    }

    @Override
    public void onResume() {
        super.onResume();

        // setup action bar, we do that here to be sure that it already has been created
        mActionBar = getActivity().getActionBar();
        mActionBar.setTitle(mCalendarName);
        mActionBar.setSubtitle(mTitle);
    }

    @Override
    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
        super.onCreateOptionsMenu(menu, inflater);
        inflater.inflate(R.menu.calendar_item, menu);

        // only show options if the calendar is synced
        menu.findItem(R.id.menu_settings).setVisible(mSynced);
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        int id = item.getItemId();
        if (id == R.id.menu_settings) {
            Analytics.screen("calendar-settings", null, String.valueOf(ContentItem.getApiId(mId)));

            CalendarSettingsFragment settings = CalendarSettingsFragment.newInstance(mSubscriptionUri);
            settings.show(getChildFragmentManager(), null);
        }
        return super.onOptionsItemSelected(item);
    }

    @Override
    public Loader<Cursor> onCreateLoader(int loaderId, Bundle params) {
        Activity activity = getActivity();

        switch (loaderId) {
        case LOADER_CALENDAR_ITEM:
            return new CursorLoader(activity, mContentUri, PROJECTION, null, null, null);

        case LOADER_SUBSCRIBED_CALENDAR:
            return new CursorLoader(activity, SubscribedCalendars.getContentUri(activity), null,
                    SubscribedCalendars.ITEM_ID + "=" + ContentUris.parseId(mContentUri), null, null);

        case LOADER_PREVIEW:
            // show the loader indicator delayed
            mHandler.postDelayed(mProgressIndicator, PROGRESS_INDICATOR_DELAY);
            if (mCalendarUrl != null) {
                return new CursorLoader(getActivity(),
                        WebCalReaderContract.Events.getEventsUri(getActivity(), mCalendarUrl, 60 * 1000), null,
                        null, null, null);
            } else {
                return null;
            }

        case LOADER_SUBSCRIPTION:
            return new CursorLoader(activity, PaymentStatus.getContentUri(activity), null, null, null, null);

        default:
            return null;
        }

    }

    @Override
    public void onLoadFinished(Loader<Cursor> loader, final Cursor cursor) {
        switch (loader.getId()) {
        case LOADER_CALENDAR_ITEM:
            mHandler.post(new Runnable() {
                @Override
                public void run() {
                    mPetriNet.fire(mItemLoaded, cursor);
                }
            });
            break;

        case LOADER_SUBSCRIBED_CALENDAR:
            mHandler.post(new Runnable() {
                @Override
                public void run() {
                    mPetriNet.fire(mSubscriptionLoaded, cursor);
                }
            });
            break;
        case LOADER_PREVIEW:
            if (cursor != null && cursor.getCount() > 0) {
                mHandler.removeCallbacks(mProgressIndicator);
                mProgressBar.setVisibility(View.GONE);
            }

            if (cursor == null) {
                Log.e(TAG, "No events received");
                // mMessage.setVisibility(View.VISIBLE);
                mHandler.removeCallbacks(mProgressIndicator);
                mProgressBar.setVisibility(View.GONE);
                return;
            }
            Cursor oldCursor = mListAdapter.swapCursor(cursor);

            // this appears to be necessary for some reason, even though the adapter should know that the data set has changed
            mListAdapter.notifyDataSetChanged();

            if (oldCursor == null || oldCursor.getCount() == 0) {
                goToToday();
            }

            if (oldCursor != null) {
                oldCursor.close();
            }
            break;

        case LOADER_SUBSCRIPTION:

            mHandler.post(new Runnable() {
                @Override
                public void run() {
                    CalendarItemFragment.this.setPurchaseableItem(cursor);
                }
            });
            break;
        }
    }

    private void goToToday() {
        Time now = new Time(TimeZone.getDefault().getID());
        now.setToNow();
        int nowIdx = (now.year << 16) + (now.month << 8) + now.monthDay;

        if (mSectionAdapter != null) {
            for (int i = 0, count = mSectionAdapter.getCount(); i < count; ++i) {
                long id = mSectionAdapter.getItemId(i);
                if (SectionTitlesAdapter.itemPos(id) == SectionTitlesAdapter.HEADER_ID) {
                    if (SectionTitlesAdapter.sectionId(id) >= nowIdx) {
                        mListView.setSelectionFromTop(Math.min(mSectionAdapter.getCount() - 1, i + 1), 0);
                        return;
                    }
                }
            }
        }

        // all events in the past, go to the end of the list
        mListView.setSelectionFromTop(mSectionAdapter.getCount() - 1, 0);
    }

    @Override
    public void onLoaderReset(Loader<Cursor> loader) {
        // at present we don't do anything here
    }

    @Override
    public boolean onSyncSwitchToggle(boolean status) {
        if (isResumed() && status != mSynced) {
            if (isPurchased() || inFreeTrial()) {
                Analytics.event("sync-enabled", "calendar-action", Boolean.toString(status), null,
                        String.valueOf(ContentItem.getApiId(mId)), null);
                setCalendarSynced(status);
            } else if (!status) {
                Analytics.event("sync-enabled", "calendar-action", Boolean.toString(status), null,
                        String.valueOf(ContentItem.getApiId(mId)), null);
                setCalendarSynced(false);
                mTitleFragment.enableSwitch(isPurchased());
            }
        }
        return false;
    };

    @Override
    public void onPurchase(boolean success, boolean freeTrial) {
        if (success) {
            setCalendarSynced(true);
        } else {
            mTitleFragment.setSwitchChecked(mSynced && inFreeTrial());
        }
    }

    private void setCalendarSynced(final boolean status) {
        final Activity activity = getActivity();
        if (mSynced != status) {
            new ProtectedBackgroundJob<Void, Uri>(activity) {
                @Override
                protected void doPostExecute(Uri result) {
                    mSubscriptionUri = result;
                }

                @Override
                protected Uri doInBackground(Void... params) {
                    mSynced = status;
                    if (status) {
                        if (mSubscriptionUri != null) {
                            // calendar already exists, we just have to enable sync
                            ContentValues values = new ContentValues();
                            values.put(SubscribedCalendars.SYNC_ENABLED, 1);
                            SubscribedCalendars.updateCalendar(activity, mSubscriptionUri, values);
                            return mSubscriptionUri;
                        } else {
                            return SubscribedCalendars.addCalendar(activity, ContentUris.parseId(mContentUri),
                                    mCalendarName, (int) (Math.random() * 0x1000000) + 0xff000000);
                        }
                    } else {
                        if (mSubscriptionUri != null) {
                            SubscribedCalendars.disableCalendar(activity, mSubscriptionUri);
                        }
                        return null;
                    }
                }

            }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
        }

    }

    @Override
    public void onItemClick(AdapterView<?> listView, View view, int position, long id) {
        Cursor cursor = (Cursor) listView.getAdapter().getItem(position);

        Time start = new Time(cursor.getString(cursor.getColumnIndex(WebCalReaderContract.Events.TIMZONE)));
        start.set(cursor.getLong(cursor.getColumnIndex(WebCalReaderContract.Events.DTSTART)));
        start.allDay = cursor.getInt(cursor.getColumnIndex(WebCalReaderContract.Events.IS_ALLDAY)) != 0;

        Time end = new Time(cursor.getString(cursor.getColumnIndex(WebCalReaderContract.Events.TIMZONE)));
        end.set(cursor.getLong(cursor.getColumnIndex(WebCalReaderContract.Events.DTEND)));
        end.allDay = start.allDay;

        Event event = new Event(start, end,
                cursor.getString(cursor.getColumnIndex(WebCalReaderContract.Events.TITLE)),
                cursor.getString(cursor.getColumnIndex(WebCalReaderContract.Events.DESCRIPTION)),
                cursor.getString(cursor.getColumnIndex(WebCalReaderContract.Events.LOCATION)));

        EventsPreviewActivity.show(getActivity(), event, mCalendarName, mIcon, mTitle);
    }

    @Override
    public String getItemTitle() {
        return mCalendarName;
    }

    public void loadCalendar() {
        // the the adapter has no cursor yet
        LoaderManager loaderManager = getLoaderManager();
        if (loaderManager != null) {
            if (loaderManager.getLoader(LOADER_PREVIEW) == null) {
                Loader<Cursor> initLoader = loaderManager.initLoader(LOADER_PREVIEW, null,
                        CalendarItemFragment.this);
                if (initLoader != null) {
                    initLoader.forceLoad();
                } else {
                    Log.e(TAG, "Newly created Loader is null");
                }
            } else {
                loaderManager.restartLoader(LOADER_PREVIEW, null, CalendarItemFragment.this);
            }
        } else {
            Log.e(TAG, "Loader Manager is null");
        }
    }

    @Override
    public String getGoogleSubscriptionId() {
        return SubscriptionId.getSubscriptionId(getActivity());
    }

    @Override
    public void onPaymentStatusChange() {
        mSynced &= (isPurchased() || inFreeTrial());
        mPetriNet.fire(mPaymentStatusUpdated, null);
        getActivity().invalidateOptionsMenu();
    }

    /**
     * Runnable that shows the progress indicator loading of the calendar takes longer than {@value #PROGRESS_INDICATOR_DELAY} milliseconds.
     * 
     * @see #PROGRESS_INDICATOR_DELAY
     */
    private final Runnable mProgressIndicator = new Runnable() {
        @Override
        public void run() {
            mProgressBar.setVisibility(View.VISIBLE);
        }
    };
}