com.podcatcher.deluxe.model.sync.gpodder.GpodderEpisodeMetadataSyncController.java Source code

Java tutorial

Introduction

Here is the source code for com.podcatcher.deluxe.model.sync.gpodder.GpodderEpisodeMetadataSyncController.java

Source

/** Copyright 2012-2014 Kevin Hausmann
 *
 * This file is part of Podcatcher Deluxe.
 *
 * Podcatcher Deluxe is free software: you can redistribute it
 * and/or modify it under the terms of the GNU General Public License as 
 * published by the Free Software Foundation, either version 3 of the License,
 * or (at your option) any later version.
 *
 * Podcatcher Deluxe is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Podcatcher Deluxe. If not, see <http://www.gnu.org/licenses/>.
 */

package com.podcatcher.deluxe.model.sync.gpodder;

import android.content.Context;
import android.os.AsyncTask;

import com.dragontek.mygpoclient.api.EpisodeAction;
import com.dragontek.mygpoclient.api.EpisodeActionChanges;
import com.podcatcher.deluxe.model.types.Episode;
import com.podcatcher.deluxe.model.types.EpisodeMetadata;

import org.apache.http.auth.AuthenticationException;

import java.io.IOException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Map.Entry;
import java.util.TimeZone;

/**
 * An episode metadata sync controller for the gpodder.net service. This
 * operates by keeping track of the local changes to episodes and publishing
 * everything once {@link #syncEpisodeMetadata()} is called.
 */
abstract class GpodderEpisodeMetadataSyncController extends GpodderPodcastListSyncController {

    /**
     * The list of changes to sync out to the service
     */
    private List<EpisodeAction> actions = Collections.synchronizedList(new ArrayList<EpisodeAction>());

    /**
     * The preference key for the last sync time stamp
     */
    private static final String GPODDER_LAST_SYNC_ACTIONS = "GPODDER_LAST_SYNC_ACTIONS";
    /**
     * The last sync time stamp we are using
     */
    private long lastSyncTimeStamp;

    /**
     * The date format the gpodder.net service understands
     */
    private final DateFormat gpodderTimeStampFormat;

    /**
     * The list of episode action the gpodder.net service understands
     */
    private static enum Action {
        DOWNLOAD, PLAY, DELETE, NEW
    }

    /**
     * The sync running flag
     */
    private boolean syncRunning = false;
    /**
     * The ignore actions flag
     */
    private boolean ignoreNewActions = false;

    /**
     * Our async task triggering the actual sync machinery
     */
    private class SyncEpisodeMetadataTask extends AsyncTask<Void, Entry<Episode, EpisodeAction>, Void> {

        /**
         * The reason for failure if it occurs
         */
        private Throwable cause;

        @Override
        protected Void doInBackground(Void... params) {
            try {
                // 0. Wait for the episode metadata to be available in the
                // manager because otherwise we cannot update it
                episodeManager.blockUntilEpisodeMetadataIsLoaded();

                // 1. Get the episode actions from server and apply them to the
                // local model if the episode is actually present. Giving no
                // device id here will make sure that we get changes from all
                // devices. We do give the last sync time stamp, so only changed
                // that occurred afterwards are returned.
                final EpisodeActionChanges changes = client.downloadEpisodeActions(lastSyncTimeStamp);
                // Go walk through actions and act on them
                for (EpisodeAction action : changes.actions) {
                    // Only act if we know the podcast
                    if (podcastManager.findPodcastForUrl(action.podcast) == null)
                        continue;

                    // Get us an episode
                    final EpisodeMetadata meta = new EpisodeMetadata();
                    meta.podcastUrl = action.podcast;
                    final Episode episode = meta.marshalEpisode(action.episode);
                    // Act on the episode action if in receive mode
                    if (episode != null && SyncMode.SEND_RECEIVE.equals(mode))
                        //noinspection unchecked
                        publishProgress(new AbstractMap.SimpleEntry<>(episode, action));
                }

                // 2. Upload local changes and clear them from the local action
                // list, the actions triggered above are not included since the
                // controller set the ignoreNewActions flag before applying
                // those.
                final List<EpisodeAction> copy = new ArrayList<>(actions);
                lastSyncTimeStamp = client.uploadEpisodeActions(copy);

                // 3. Remove all actions already taken care of (we can call this
                // here because the list is synchronized). Unless new action
                // arrived while the controller was busy, the local action list
                // should be empty afterwards.
                actions.removeAll(changes.actions);
                actions.removeAll(copy);
            } catch (AuthenticationException | IOException | InterruptedException e) {
                this.cause = e;
                cancel(true);
            }

            return null;
        }

        @SafeVarargs
        @Override
        protected final void onProgressUpdate(Entry<Episode, EpisodeAction>... values) {
            // Go change local model as needed
            final Episode episode = values[0].getKey();
            final EpisodeAction episodeAction = values[0].getValue();
            final Action action = Action.valueOf(episodeAction.action.toUpperCase(Locale.US).trim());

            // Make sure we do not pick up the same action again
            ignoreNewActions = true;
            switch (action) {
            case PLAY:
                // If no position is given there is nothing to do
                if (episodeAction.position != null) {
                    final int remoteValue = episodeAction.position * 1000;
                    final int localValue = episodeManager.getResumeAt(episode);
                    // Only act if the remote value differs by more than one sec
                    if (Math.abs(remoteValue - localValue) > 1000)
                        episodeManager.setResumeAt(episode, remoteValue);
                }
                break;
            case NEW:
                // Only act if the value is not set yet
                if (episodeManager.getResumeAt(episode) != 0)
                    episodeManager.setResumeAt(episode, null);
                if (!episodeManager.getState(episode))
                    episodeManager.setState(episode, true);
                break;
            default:
                break;
            }
            // Re-enable action monitoring
            ignoreNewActions = false;
        }

        protected void onPostExecute(Void nothing) {
            // Make sure we take note of the time stamp
            preferences.edit().putLong(GPODDER_LAST_SYNC_ACTIONS, lastSyncTimeStamp).apply();

            syncRunning = false;
            listener.onSyncCompleted(getImpl());
        }

        @Override
        protected void onCancelled(Void nothing) {
            syncRunning = false;
            listener.onSyncFailed(getImpl(), cause);
        }
    }

    protected GpodderEpisodeMetadataSyncController(Context context) {
        super(context);

        // Create the date time format for the time stamp sent to gpodder.net
        final TimeZone zone = TimeZone.getTimeZone("UTC");
        this.gpodderTimeStampFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.US);
        gpodderTimeStampFormat.setTimeZone(zone);

        // Recover the last synced time stamp
        this.lastSyncTimeStamp = preferences.getLong(GPODDER_LAST_SYNC_ACTIONS, 0);
    }

    @Override
    public boolean isRunning() {
        return syncRunning || super.isRunning();
    }

    @Override
    protected synchronized void syncEpisodeMetadata() {
        if (!syncRunning) {
            syncRunning = true;

            new SyncEpisodeMetadataTask().executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, (Void) null);
        }
    }

    @Override
    public void onStateChanged(Episode episode, boolean newState) {
        // Limit the number of actions added here since this will make the whole
        // app hang here if too many episodes are marked old at once
        // if (!ignoreNewActions && actions.size() < 25 && newState)
        // actions.add(prepareAction(episode, Action.NEW, 0));
    }

    @Override
    public void onResumeAtChanged(Episode episode, Integer millis) {
        if (!ignoreNewActions)
            // Send "new" event when the "resume at" was reset
            if (millis == null)
                actions.add(prepareAction(episode, Action.NEW, 0));
            // Otherwise update the server on the play position
            else
                actions.add(prepareAction(episode, Action.PLAY, millis / 1000));
    }

    @Override
    public void onDownloadSuccess(Episode episode) {
        actions.add(prepareAction(episode, Action.DOWNLOAD, 0));
    }

    @Override
    public void onDownloadDeleted(Episode episode) {
        actions.add(prepareAction(episode, Action.DELETE, 0));
    }

    private EpisodeAction prepareAction(Episode episode, Action action, int position) {
        return new EpisodeAction(episode.getPodcast().getUrl(), episode.getMediaUrl(),
                action.toString().toLowerCase(Locale.US), deviceId, gpodderTimeStampFormat.format(new Date()), null,
                Action.PLAY.equals(action) ? position : null, null);
    }
}