Java tutorial
/** * 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; } }