net.naonedbus.manager.impl.HoraireManager.java Source code

Java tutorial

Introduction

Here is the source code for net.naonedbus.manager.impl.HoraireManager.java

Source

/**
 * Copyright (C) 2013 Romain Guefveneu.
 *   
 *  This file is part of naonedbus.
 *   
 *  Naonedbus 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.
 *  
 *  Naonedbus 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 this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package net.naonedbus.manager.impl;

import java.io.IOException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentLinkedQueue;

import net.naonedbus.BuildConfig;
import net.naonedbus.bean.Arret;
import net.naonedbus.bean.NextHoraireTask;
import net.naonedbus.bean.horaire.Horaire;
import net.naonedbus.bean.horaire.ScheduleToken;
import net.naonedbus.manager.SQLiteManager;
import net.naonedbus.provider.impl.HoraireProvider;
import net.naonedbus.provider.table.HoraireTable;
import net.naonedbus.rest.controller.impl.HoraireController;

import org.joda.time.DateMidnight;
import org.joda.time.DateTime;
import org.joda.time.Minutes;

import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.util.Log;

import com.bugsense.trace.BugSenseHandler;

public class HoraireManager extends SQLiteManager<Horaire> {

    private static final String LOG_TAG = "HoraireManager";
    private static final boolean DBG = BuildConfig.DEBUG;

    private static final int DAYS_IN_CACHE = 3;
    private static final int END_OF_TRIP_HOURS = 4;

    private static HoraireManager sInstance;

    private SimpleDateFormat mIso8601Format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    private final HoraireController mController;
    private final ConcurrentLinkedQueue<NextHoraireTask> mSchedulesTasksQueue;
    private final Set<ScheduleToken> mEmptySchedules;
    private Thread mLoadThread;
    private Object mDatabaseLock;

    private int mColId;
    private int mColHoraire;
    private int mColJour;
    private int mColTerminus;

    /**
     * Make it singleton !
     */
    public static synchronized HoraireManager getInstance() {
        if (sInstance == null) {
            sInstance = new HoraireManager();
        }
        return sInstance;
    }

    private HoraireManager() {
        super(HoraireProvider.CONTENT_URI);
        mController = new HoraireController();
        mSchedulesTasksQueue = new ConcurrentLinkedQueue<NextHoraireTask>();
        mEmptySchedules = new HashSet<ScheduleToken>();
        mDatabaseLock = new Object();
    }

    @Override
    public void onIndexCursor(final Cursor c) {
        mColId = c.getColumnIndex(HoraireTable._ID);
        mColJour = c.getColumnIndex(HoraireTable.JOUR);
        mColHoraire = c.getColumnIndex(HoraireTable.HORAIRE);
        mColTerminus = c.getColumnIndex(HoraireTable.TERMINUS);
    }

    @Override
    public Horaire getSingleFromCursor(final Cursor c) {
        final Horaire item = new Horaire();
        item.setId(c.getInt(mColId));
        try {
            item.setJour(new DateMidnight(mIso8601Format.parse(c.getString(mColJour))));
            item.setHoraire(new DateTime(mIso8601Format.parse(c.getString(mColHoraire))));
        } catch (ParseException e) {
            e.printStackTrace();
        }
        item.setTerminus(c.getString(mColTerminus));
        item.setSection(new DateMidnight(item.getHoraire()));
        return item;
    }

    /**
     * Indique si le cache contient les horaires de l'arrt pour la date donne.
     * 
     * @return {@code true} si le cache contient tous les horaires pour l'arret
     *         et la date demande {@code false} si les donnes ne sont pas en
     *         cache.
     */
    public boolean isInDB(final ContentResolver contentResolver, final Arret arret, final DateMidnight date) {
        return isInDB(contentResolver, arret, date, 1);
    }

    /**
     * Indique si le cache contient les horaires de l'arrt pour la date donne.
     * 
     * @return {@code true} si le cache contient tous les horaires pour l'arret
     *         et la date demande {@code false} si les donnes ne sont pas en
     *         cache.
     */
    public boolean isInDB(final ContentResolver contentResolver, final Arret arret, final DateMidnight date,
            final int count) {

        final ScheduleToken token = new ScheduleToken(date.getMillis(), arret.getId());
        return isInDB(contentResolver, token, count);
    }

    /**
     * Indique si le cache contient les horaires de l'arrt pour la date donne.
     * 
     * @return {@code true} si le cache contient tous les horaires pour l'arret
     *         et la date demande {@code false} si les donnes ne sont pas en
     *         cache.
     */
    public boolean isInDB(final ContentResolver contentResolver, final ScheduleToken token) {
        return isInDB(contentResolver, token, 1);
    }

    /**
     * Indique si le cache contient les horaires du token donn.
     * 
     * @return {@code true} si le cache contient tous les horaires pour l'arret
     *         et la date demande {@code false} si les donnes ne sont pas en
     *         cache.
     */
    public boolean isInDB(final ContentResolver contentResolver, final ScheduleToken token, int count) {
        if (mEmptySchedules.contains(token))
            return true;

        final Uri.Builder builder = HoraireProvider.CONTENT_URI.buildUpon();
        builder.path(HoraireProvider.HORAIRE_JOUR_URI_PATH_QUERY);
        builder.appendQueryParameter(HoraireProvider.PARAM_ARRET_ID, String.valueOf(token.getArretId()));
        builder.appendQueryParameter(HoraireProvider.PARAM_JOUR, mIso8601Format.format(token.getDate()));

        synchronized (mDatabaseLock) {
            final Cursor c = contentResolver.query(builder.build(), null, null, null, null);
            final int cursorCount = c.getCount();
            c.close();
            return cursorCount >= count;
        }
    }

    /**
     * Supprimer les horaires de plus d'1 jour.
     */
    public void clearOldSchedules(final ContentResolver contentResolver) {
        if (DBG)
            Log.i(LOG_TAG, "Nettoyage du cache horaires");

        synchronized (mDatabaseLock) {
            contentResolver.delete(HoraireProvider.CONTENT_URI, HoraireTable.JOUR + " < ?",
                    new String[] { String.valueOf(new DateMidnight().minusDays(1).getMillis()) });
        }
    }

    /**
     * Supprimer tous les horaires.
     */
    public void clearSchedules(final ContentResolver contentResolver) {
        if (DBG)
            Log.i(LOG_TAG, "Suppression du cache horaires");
        synchronized (mDatabaseLock) {
            contentResolver.delete(HoraireProvider.CONTENT_URI, null, null);
        }
    }

    private ContentValues getContentValues(final int arretId, final Horaire horaire) {
        final ContentValues values = new ContentValues();
        values.put(HoraireTable.ID_ARRET, arretId);
        values.put(HoraireTable.TERMINUS, horaire.getTerminus());
        values.put(HoraireTable.JOUR, mIso8601Format.format(horaire.getJour().toDate()));
        values.put(HoraireTable.HORAIRE, mIso8601Format.format(horaire.getHoraire().toDate()));
        return values;
    }

    private void fillDB(final ContentResolver contentResolver, final ScheduleToken token,
            final List<Horaire> schedule) {
        if (DBG)
            Log.i(LOG_TAG, "Sauvegarde des horaires : " + token.getArretId() + " \t " + new Date(token.getDate())
                    + " \t " + (schedule == null ? null : schedule.size()));

        if (schedule != null) {
            if (schedule.size() == 0) {
                // Ajouter un marqueur pour indiquer que l'on a dj cherch
                // les horaires

                //            mEmptySchedules.add(token);
            } else {
                // Ajouter les horaires dans la db
                final ContentValues[] values = new ContentValues[schedule.size()];
                for (int i = 0; i < schedule.size(); i++) {
                    values[i] = getContentValues(token.getArretId(), schedule.get(i));
                }

                synchronized (mDatabaseLock) {
                    contentResolver.bulkInsert(HoraireProvider.CONTENT_URI, values);
                }
            }
        }
    }

    /**
     * Rcuprer les horaires d'un arrt.
     * 
     * @throws IOException
     */
    public List<Horaire> getSchedules(final ContentResolver contentResolver, final Arret arret,
            final DateMidnight date) throws IOException {
        return getSchedules(contentResolver, arret, date, null);
    }

    /**
     * Rcuprer les horaires d'un arrt.
     * 
     * @throws IOException
     */
    public List<Horaire> getSchedules(final ContentResolver contentResolver, final Arret arret,
            final DateMidnight date, final DateTime after) throws IOException {
        // Le cache ne doit stocker que les horaires du jour et du lendemain.

        final DateMidnight cacheLimit = new DateMidnight().plusDays(DAYS_IN_CACHE);
        final DateMidnight today = new DateMidnight();
        final DateTime now = new DateTime();
        List<Horaire> horaires;

        if (date.isBefore(cacheLimit)) {

            final ScheduleToken todayToken = new ScheduleToken(date.getMillis(), arret.getId());

            // Partie atomique
            synchronized (mDatabaseLock) {

                if (!isInDB(contentResolver, todayToken)) {
                    // Charger les horaires depuis le web et les stocker en base

                    if (date.isEqual(today) && now.getHourOfDay() < END_OF_TRIP_HOURS) {
                        // Charger la veille si besoin (pour les horaires aprs
                        // minuit)
                        final ScheduleToken yesterdayToken = new ScheduleToken(date.minusDays(1).getMillis(),
                                arret.getId());

                        if (isInDB(contentResolver, yesterdayToken)) {
                            horaires = mController.getAllFromWeb(arret, date.minusDays(1));
                            fillDB(contentResolver, yesterdayToken, horaires);
                        }
                    }

                    horaires = mController.getAllFromWeb(arret, date);
                    fillDB(contentResolver, todayToken, horaires);
                }

                // Charger les horaires depuis la base
                final Uri.Builder builder = HoraireProvider.CONTENT_URI.buildUpon();
                builder.path(HoraireProvider.HORAIRE_JOUR_URI_PATH_QUERY);
                builder.appendQueryParameter(HoraireProvider.PARAM_ARRET_ID, String.valueOf(arret.getId()));
                builder.appendQueryParameter(HoraireProvider.PARAM_JOUR, mIso8601Format.format(date.toDate()));
                builder.appendQueryParameter(HoraireProvider.PARAM_INCLUDE_LAST_DAY_TRIP, "true");

                // Eviter l'affichage de doublons
                if (after != null) {
                    builder.appendQueryParameter(HoraireProvider.PARAM_AFTER_TIME,
                            mIso8601Format.format(after.toDate()));
                }

                final Cursor cursor = contentResolver.query(builder.build(), null, null, null, null);
                horaires = getFromCursor(cursor);
                cursor.close();
            }

        } else {
            horaires = mController.getAllFromWeb(arret, date);
        }

        return horaires;
    }

    /**
     * Rcuprer les prochains horaires d'un arrt.
     * 
     * @throws IOException
     */
    public List<Horaire> getNextSchedules(final ContentResolver contentResolver, final Arret arret,
            final DateMidnight date, final int limit) throws IOException {
        return getNextSchedules(contentResolver, arret, date, limit, 0);
    }

    /**
     * Rcuprer les prochains horaires d'un arrt
     * 
     * @throws IOException
     */
    public List<Horaire> getNextSchedules(final ContentResolver contentResolver, final Arret arret,
            DateMidnight date, final int limit, final int minuteDelay) throws IOException {
        if (DBG)
            Log.d(LOG_TAG, "getNextHoraires " + arret + " : " + date + "\t" + limit);

        List<Horaire> schedules;
        final DateTime now = new DateTime().minusMinutes(minuteDelay).withSecondOfMinute(0).withMillisOfSecond(0);
        final List<Horaire> nextSchedules = new ArrayList<Horaire>();
        int schedulesCount = 0; // Juste renvoyer le bon nombre d'horaires
        int loopCount = 0; // Limiter le nombre d'itrations
        DateTime after = null; // Dernier horaire charg

        do {
            schedules = getSchedules(contentResolver, arret, date, after);
            for (final Horaire schedule : schedules) {
                if (schedule.getHoraire().isAfter(now) || schedule.getHoraire().isEqual(now)) {
                    nextSchedules.add(schedule);
                    if (++schedulesCount >= limit) {
                        break;
                    }
                }
            }

            if (schedules.size() > 0)
                after = new DateTime(schedules.get(schedules.size() - 1).getHoraire());
            else
                after = null;

            date = date.plusDays(1);
            loopCount++;
        } while ((loopCount < 2) && (nextSchedules.size() < limit));

        return nextSchedules;
    }

    /**
     * Rcuprer le nombre de minutes jusqu'au prochain horaire.
     * 
     * @throws IOException
     */
    public Integer getMinutesToNextSchedule(final ContentResolver contentResolver, final Arret arret)
            throws IOException {

        final List<Horaire> nextSchedules = getNextSchedules(contentResolver, arret, new DateMidnight(), 1);
        Integer result = null;

        if (nextSchedules.size() > 0) {
            final Horaire next = nextSchedules.get(0);

            final DateTime itemDateTime = new DateTime(next.getHoraire()).withSecondOfMinute(0)
                    .withMillisOfSecond(0);
            final DateTime now = new DateTime().withSecondOfMinute(0).withMillisOfSecond(0);

            result = Minutes.minutesBetween(now, itemDateTime).getMinutes();
        }

        return result;
    }

    /**
     * Ajouter une demande de recherche d'horaire A la fin du chargement, le
     * signal TimeService.ACTION_APPWIDGET_UPDATE est envoy.
     */
    public synchronized void schedule(final NextHoraireTask task) {
        if (DBG)
            Log.i(LOG_TAG, "Planification de la tche " + task);

        mSchedulesTasksQueue.add(task);
        if (mLoadThread == null || !mLoadThread.isAlive()) {
            mLoadThread = new Thread(loadHoraireTask);
            mLoadThread.setPriority(Thread.MIN_PRIORITY);
            mLoadThread.start();
        }
    }

    /**
     * Thread de chargement des horaires selon la file
     */
    private final Runnable loadHoraireTask = new Runnable() {

        private final String LOG_TAG = "HoraireManager$loadHoraireTask";

        @Override
        public void run() {
            if (DBG)
                Log.i(LOG_TAG, "Dmarrage du thread de chargement des horaires");

            final DateMidnight today = new DateMidnight();
            NextHoraireTask task;

            while ((task = mSchedulesTasksQueue.poll()) != null) {
                if (isInDB(task.getContext().getContentResolver(), task.getArret(), today)) {
                    onPostLoad(task);
                } else {
                    load(task, today);
                }
            }

            if (DBG)
                Log.i(LOG_TAG, "Fin du thread de chargement des horaires");
        }

        private void load(final NextHoraireTask task, final DateMidnight today) {
            if (DBG)
                Log.d(LOG_TAG, "Rcupration des horaires de l'arrt " + task.getArret().getCodeArret());

            try {

                getNextSchedules(task.getContext().getContentResolver(), task.getArret(), today, task.getLimit());

            } catch (final IOException e) {
                if (DBG)
                    Log.e(LOG_TAG,
                            "Erreur de rcupration des horaires de l'arrt " + task.getArret().getCodeArret(),
                            e);
                task.setThrowable(e);
            } catch (final Exception e) {
                if (DBG)
                    Log.e(LOG_TAG,
                            "Erreur de rcupration des horaires de l'arrt " + task.getArret().getCodeArret(),
                            e);
                task.setThrowable(e);
                BugSenseHandler.sendExceptionMessage("Erreur lors du chargement des horaires", null, e);
            }

            onPostLoad(task);
        }

        private void onPostLoad(final NextHoraireTask task) {
            final Intent intent = new Intent(task.getActionCallback());
            intent.putExtra("id", task.getId());

            if (task.getThrowable() != null) {
                intent.putExtra("throwable", task.getThrowable());
            }

            task.getContext().sendBroadcast(intent);
        }

    };

    @Override
    protected ContentValues getContentValues(final Horaire item) {
        return null;
    }

}