/*------------------------------------------------------------------------------ ** Ident: Sogeti Smart Mobile Solutions ** Author: rene ** Copyright: (c) Apr 24, 2011 Sogeti Nederland B.V. All Rights Reserved. **------------------------------------------------------------------------------ ** Sogeti Nederland B.V. | No part of this file may be reproduced ** Distributed Software Engineering | or transmitted in any form or by any ** Lange Dreef 17 | means, electronic or mechanical, for the ** 4131 NJ Vianen | purpose, without the express written ** The Netherlands | permission of the copyright holder. *------------------------------------------------------------------------------ * * This file is part of OpenGPSTracker. * * OpenGPSTracker 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. * * OpenGPSTracker 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 OpenGPSTracker. If not, see <http://www.gnu.org/licenses/>. * */ package nl.sogeti.android.gpstracker.adapter; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.OptionalDataException; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Observable; import java.util.Set; import java.util.Vector; import nl.sogeti.android.gpstracker.R; import nl.sogeti.android.gpstracker.db.GPStracking.MetaData; import nl.sogeti.android.gpstracker.util.Constants; import nl.sogeti.android.gpstracker.util.Pair; import android.content.ContentResolver; import android.content.Context; import android.database.Cursor; import android.util.Log; import android.widget.ArrayAdapter; import android.widget.SpinnerAdapter; /** * Model containing agregrated data retrieved from the GoBreadcrumbs.com API * * @version $Id:$ * @author rene (c) May 9, 2011, Sogeti B.V. */ public class BreadcrumbsTracks extends Observable { public static final String DESCRIPTION = "DESCRIPTION"; public static final String NAME = "NAME"; public static final String ENDTIME = "ENDTIME"; public static final String TRACK_ID = "BREADCRUMBS_TRACK_ID"; public static final String BUNDLE_ID = "BREADCRUMBS_BUNDLE_ID"; public static final String ACTIVITY_ID = "BREADCRUMBS_ACTIVITY_ID"; public static final String DIFFICULTY = "DIFFICULTY"; public static final String STARTTIME = "STARTTIME"; public static final String ISPUBLIC = "ISPUBLIC"; public static final String RATING = "RATING"; public static final String LATITUDE = "LATITUDE"; public static final String LONGITUDE = "LONGITUDE"; public static final String TOTALDISTANCE = "TOTALDISTANCE"; public static final String TOTALTIME = "TOTALTIME"; private static final String TAG = "OGT.BreadcrumbsTracks"; private static final Integer CACHE_VERSION = new Integer(3); private static final String BREADCRUMSB_BUNDLES_CACHE_FILE = "breadcrumbs_bundles_cache.data"; private static final String BREADCRUMSB_ACTIVITY_CACHE_FILE = "breadcrumbs_activity_cache.data"; /** * Time in milliseconds that a persisted breadcrumbs cache is used without a * refresh */ private static final long CACHE_TIMEOUT = 1000 * 60;//1000*60*10 ; /** * Mapping from bundleId to a list of trackIds */ private static Map<Integer, List<Integer>> sBundlesWithTracks; /** * Map from activityId to a dictionary containing keys like NAME */ private static Map<Integer, Map<String, String>> sActivityMappings; /** * Map from bundleId to a dictionary containing keys like NAME and * DESCRIPTION */ private static Map<Integer, Map<String, String>> sBundleMappings; /** * Map from trackId to a dictionary containing keys like NAME, ISPUBLIC, * DESCRIPTION and more */ private static Map<Integer, Map<String, String>> sTrackMappings; /** * Cache of OGT Tracks that have a Breadcrumbs track id stored in the * meta-data table */ private Map<Long, Integer> mSyncedTracks = null; private static Set<Pair<Integer, Integer>> sScheduledTracksLoading; static { BreadcrumbsTracks.initCacheVariables(); } private static void initCacheVariables() { sBundlesWithTracks = new LinkedHashMap<Integer, List<Integer>>(); sActivityMappings = new HashMap<Integer, Map<String, String>>(); sBundleMappings = new HashMap<Integer, Map<String, String>>(); sTrackMappings = new HashMap<Integer, Map<String, String>>(); sScheduledTracksLoading = new HashSet<Pair<Integer, Integer>>(); } private ContentResolver mResolver; /** * Constructor: create a new BreadcrumbsTracks. * * @param resolver Content resolver to obtain local Breadcrumbs references */ public BreadcrumbsTracks(ContentResolver resolver) { mResolver = resolver; } public void addActivity(Integer activityId, String activityName) { if( BreadcrumbsAdapter.DEBUG ) { Log.d( TAG, "addActivity(Integer "+activityId+" String "+activityName+")"); } if (!sActivityMappings.containsKey(activityId)) { sActivityMappings.put(activityId, new HashMap<String, String>()); } sActivityMappings.get(activityId).put(NAME, activityName); setChanged(); notifyObservers(); } /** * Add bundle to the track list * * @param activityId * @param bundleId * @param bundleName * @param bundleDescription */ public void addBundle(Integer bundleId, String bundleName, String bundleDescription) { if( BreadcrumbsAdapter.DEBUG ) { Log.d( TAG, "addBundle(Integer "+bundleId+", String "+bundleName+", String "+bundleDescription+")"); } if (!sBundleMappings.containsKey(bundleId)) { sBundleMappings.put(bundleId, new HashMap<String, String>()); } if (!sBundlesWithTracks.containsKey(bundleId)) { sBundlesWithTracks.put(bundleId, new ArrayList<Integer>()); } sBundleMappings.get(bundleId).put(NAME, bundleName); sBundleMappings.get(bundleId).put(DESCRIPTION, bundleDescription); setChanged(); notifyObservers(); } /** * Add track to tracklist * * @param trackId * @param trackName * @param bundleId * @param trackDescription * @param difficulty * @param startTime * @param endTime * @param isPublic * @param lat * @param lng * @param totalDistance * @param totalTime * @param trackRating */ public void addTrack(Integer trackId, String trackName, Integer bundleId, String trackDescription, String difficulty, String startTime, String endTime, String isPublic, Float lat, Float lng, Float totalDistance, Integer totalTime, String trackRating) { if( BreadcrumbsAdapter.DEBUG ) { Log.d( TAG, "addTrack(Integer "+trackId+", String "+trackName+", Integer "+bundleId +"..."); } if (!sBundlesWithTracks.containsKey(bundleId)) { sBundlesWithTracks.put(bundleId, new ArrayList<Integer>()); } if (!sBundlesWithTracks.get(bundleId).contains(trackId)) { sBundlesWithTracks.get(bundleId).add(trackId); sScheduledTracksLoading.remove(Pair.create(Constants.BREADCRUMBS_TRACK_ITEM_VIEW_TYPE, trackId)); } if (!sTrackMappings.containsKey(trackId)) { sTrackMappings.put(trackId, new HashMap<String, String>()); } putForTrack(trackId, NAME, trackName); putForTrack(trackId, ISPUBLIC, isPublic); putForTrack(trackId, STARTTIME, startTime); putForTrack(trackId, ENDTIME, endTime); putForTrack(trackId, DESCRIPTION, trackDescription); putForTrack(trackId, DIFFICULTY, difficulty); putForTrack(trackId, RATING, trackRating); putForTrack(trackId, LATITUDE, lat); putForTrack(trackId, LONGITUDE, lng); putForTrack(trackId, TOTALDISTANCE, totalDistance); putForTrack(trackId, TOTALTIME, totalTime); notifyObservers(); } public void addSyncedTrack(Long trackId, Integer bcTrackId) { if (mSyncedTracks == null) { isLocalTrackOnline(-1l); } mSyncedTracks.put(trackId, bcTrackId); setChanged(); notifyObservers(); } public void addTracksLoadingScheduled(Pair<Integer, Integer> item) { sScheduledTracksLoading.add(item); setChanged(); notifyObservers(); } /** * Cleans old bundles based a set of all bundles * * @param mBundleIds */ public void setAllBundleIds(Set<Integer> mBundleIds) { for (Integer oldBundleId : getAllBundleIds()) { if (!mBundleIds.contains(oldBundleId)) { removeBundle(oldBundleId); } } } public void setAllTracksForBundleId(Integer mBundleId, Set<Integer> updatedbcTracksIdList) { List<Integer> trackIdList = sBundlesWithTracks.get(mBundleId); for (int location = 0; location < trackIdList.size(); location++) { Integer oldTrackId = trackIdList.get(location); if (!updatedbcTracksIdList.contains(oldTrackId)) { removeTrack(mBundleId, oldTrackId); } } setChanged(); notifyObservers(); } private void putForTrack(Integer trackId, String key, Object value) { if (value != null) { sTrackMappings.get(trackId).put(key, value.toString()); } setChanged(); notifyObservers(); } /** * Remove a bundle * * @param deletedId */ public void removeBundle(Integer deletedId) { sBundleMappings.remove(deletedId); sBundlesWithTracks.remove(deletedId); setChanged(); notifyObservers(); } /** * Remove a track * * @param deletedId */ public void removeTrack(Integer bundleId, Integer trackId) { sTrackMappings.remove(trackId); if (sBundlesWithTracks.containsKey(bundleId)) { sBundlesWithTracks.get(bundleId).remove(trackId); } setChanged(); notifyObservers(); mResolver.delete(MetaData.CONTENT_URI, MetaData.TRACK + " = ? AND " + MetaData.KEY + " = ? ", new String[] { trackId.toString(), TRACK_ID }); if (mSyncedTracks != null && mSyncedTracks.containsKey(trackId)) { mSyncedTracks.remove(trackId); } } public int positions() { int size = 0; for (Integer bundleId : sBundlesWithTracks.keySet()) { // One row for the Bundle header size += 1; int bundleSize = sBundlesWithTracks.get(bundleId) != null ? sBundlesWithTracks.get(bundleId).size() : 0; // One row per track in each bundle size += bundleSize; } return size; } public Integer getBundleIdForTrackId(Integer trackId) { for (Integer bundlId : sBundlesWithTracks.keySet()) { List<Integer> trackIds = sBundlesWithTracks.get(bundlId); if (trackIds.contains(trackId)) { return bundlId; } } return null; } /** * Get all bundles * * @return */ public Integer[] getAllBundleIds() { return sBundlesWithTracks.keySet().toArray(new Integer[sBundlesWithTracks.keySet().size()]); } /** * * @param position list postition 0...n * @return a pair of a TYPE and an ID */ public Pair<Integer, Integer> getItemForPosition(int position) { int countdown = position; for (Integer bundleId : sBundlesWithTracks.keySet()) { if (countdown == 0) { return Pair.create(Constants.BREADCRUMBS_BUNDLE_ITEM_VIEW_TYPE, bundleId); } countdown--; int bundleSize = sBundlesWithTracks.get(bundleId) != null ? sBundlesWithTracks.get(bundleId).size() : 0; if (countdown < bundleSize) { Integer trackId = sBundlesWithTracks.get(bundleId).get(countdown); return Pair.create(Constants.BREADCRUMBS_TRACK_ITEM_VIEW_TYPE, trackId); } countdown -= bundleSize; } return null; } public String getValueForItem(Pair<Integer, Integer> item, String key) { String value = null; switch (item.first) { case Constants.BREADCRUMBS_BUNDLE_ITEM_VIEW_TYPE: value = sBundleMappings.get(item.second).get(key); break; case Constants.BREADCRUMBS_TRACK_ITEM_VIEW_TYPE: value = sTrackMappings.get(item.second).get(key); break; default: value = null; break; } return value; } public SpinnerAdapter getActivityAdapter(Context ctx) { List<String> activities = new Vector<String>(); for (Integer activityId : sActivityMappings.keySet()) { String name = sActivityMappings.get(activityId).get(NAME); name = name != null ? name : ""; activities.add(name); } Collections.sort(activities); ArrayAdapter<String> adapter = new ArrayAdapter<String>(ctx, android.R.layout.simple_spinner_item, activities); adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); return adapter; } public SpinnerAdapter getBundleAdapter(Context ctx) { List<String> bundles = new Vector<String>(); for (Integer bundleId : sBundlesWithTracks.keySet()) { bundles.add(sBundleMappings.get(bundleId).get(NAME)); } Collections.sort(bundles); if (!bundles.contains(ctx.getString(R.string.app_name))) { bundles.add(ctx.getString(R.string.app_name)); } ArrayAdapter<String> adapter = new ArrayAdapter<String>(ctx, android.R.layout.simple_spinner_item, bundles); adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); return adapter; } public static Integer getIdForActivity(String selectedItem) { if (selectedItem == null) { return -1; } for (Integer activityId : sActivityMappings.keySet()) { Map<String, String> mapping = sActivityMappings.get(activityId); if (mapping != null && selectedItem.equals(mapping.get(NAME))) { return activityId; } } return -1; } public static Integer getIdForBundle(Integer activityId, String selectedItem) { for (Integer bundleId : sBundlesWithTracks.keySet()) { if (selectedItem.equals(sBundleMappings.get(bundleId).get(NAME))) { return bundleId; } } return -1; } private boolean isLocalTrackOnline(Long qtrackId) { if (mSyncedTracks == null) { mSyncedTracks = new HashMap<Long, Integer>(); Cursor cursor = null; try { cursor = mResolver.query(MetaData.CONTENT_URI, new String[] { MetaData.TRACK, MetaData.VALUE }, MetaData.KEY + " = ? ", new String[] { TRACK_ID }, null); if (cursor.moveToFirst()) { do { Long trackId = cursor.getLong(0); try { Integer bcTrackId = Integer.valueOf(cursor.getString(1)); mSyncedTracks.put(trackId, bcTrackId); } catch (NumberFormatException e) { Log.w(TAG, "Illigal value stored as track id", e); } } while (cursor.moveToNext()); } } finally { if (cursor != null) { cursor.close(); } } setChanged(); notifyObservers(); } boolean synced = mSyncedTracks.containsKey(qtrackId); return synced; } public boolean isLocalTrackSynced(Long qtrackId) { boolean uploaded = isLocalTrackOnline(qtrackId); boolean synced = sTrackMappings.containsKey(mSyncedTracks.get(qtrackId)); return uploaded && synced; } public boolean areTracksLoaded(Pair<Integer, Integer> item) { return sBundlesWithTracks.containsKey(item.second) && item.first == Constants.BREADCRUMBS_TRACK_ITEM_VIEW_TYPE; } public boolean areTracksLoadingScheduled(Pair<Integer, Integer> item) { return sScheduledTracksLoading.contains(item); } /** * Read the static breadcrumbs data from private file * * @param ctx * @return is refresh is needed */ @SuppressWarnings("unchecked") public boolean readCache(Context ctx) { FileInputStream fis = null; ObjectInputStream ois = null; Date bundlesPersisted = null, activitiesPersisted = null; Object[] cache; synchronized (BREADCRUMSB_BUNDLES_CACHE_FILE) { try { fis = ctx.openFileInput(BREADCRUMSB_BUNDLES_CACHE_FILE); ois = new ObjectInputStream(fis); cache = (Object[]) ois.readObject(); // new Object[] { CACHE_VERSION, new Date(), sActivitiesWithBundles, sBundlesWithTracks, sBundleMappings, sTrackMappings }; if (cache[0] instanceof Integer && CACHE_VERSION.equals(cache[0])) { bundlesPersisted = (Date) cache[1]; Map<Integer, List<Integer>> bundles = (Map<Integer, List<Integer>>) cache[2]; Map<Integer, Map<String, String>> bundlemappings = (Map<Integer, Map<String, String>>) cache[3]; Map<Integer, Map<String, String>> trackmappings = (Map<Integer, Map<String, String>>) cache[4]; sBundlesWithTracks = bundles != null ? bundles : sBundlesWithTracks; sBundleMappings = bundlemappings != null ? bundlemappings : sBundleMappings; sTrackMappings = trackmappings != null ? trackmappings : sTrackMappings; } else { clearPersistentCache(ctx); } fis = ctx.openFileInput(BREADCRUMSB_ACTIVITY_CACHE_FILE); ois = new ObjectInputStream(fis); cache = (Object[]) ois.readObject(); // new Object[] { CACHE_VERSION, new Date(), sActivityMappings }; if (cache[0] instanceof Integer && CACHE_VERSION.equals(cache[0])) { activitiesPersisted = (Date) cache[1]; Map<Integer, Map<String, String>> activitymappings = (Map<Integer, Map<String, String>>) cache[2]; sActivityMappings = activitymappings != null ? activitymappings : sActivityMappings; } else { clearPersistentCache(ctx); } } catch (OptionalDataException e) { clearPersistentCache(ctx); Log.w(TAG, "Unable to read persisted breadcrumbs cache", e); } catch (ClassNotFoundException e) { clearPersistentCache(ctx); Log.w(TAG, "Unable to read persisted breadcrumbs cache", e); } catch (IOException e) { clearPersistentCache(ctx); Log.w(TAG, "Unable to read persisted breadcrumbs cache", e); } catch (ClassCastException e) { clearPersistentCache(ctx); Log.w(TAG, "Unable to read persisted breadcrumbs cache", e); } catch (ArrayIndexOutOfBoundsException e) { clearPersistentCache(ctx); Log.w(TAG, "Unable to read persisted breadcrumbs cache", e); } finally { if (fis != null) { try { fis.close(); } catch (IOException e) { Log.w(TAG, "Error closing file stream after reading cache", e); } } if (ois != null) { try { ois.close(); } catch (IOException e) { Log.w(TAG, "Error closing object stream after reading cache", e); } } } } setChanged(); notifyObservers(); boolean refreshNeeded = false; refreshNeeded = refreshNeeded || bundlesPersisted == null || activitiesPersisted == null; refreshNeeded = refreshNeeded || (activitiesPersisted.getTime() < new Date().getTime() - CACHE_TIMEOUT * 10); refreshNeeded = refreshNeeded || (bundlesPersisted.getTime() < new Date().getTime() - CACHE_TIMEOUT); return refreshNeeded; } public void persistCache(Context ctx) { FileOutputStream fos = null; ObjectOutputStream oos = null; Object[] cache; synchronized (BREADCRUMSB_BUNDLES_CACHE_FILE) { try { fos = ctx.openFileOutput(BREADCRUMSB_BUNDLES_CACHE_FILE, Context.MODE_PRIVATE); oos = new ObjectOutputStream(fos); cache = new Object[] { CACHE_VERSION, new Date(), sBundlesWithTracks, sBundleMappings, sTrackMappings }; oos.writeObject(cache); fos = ctx.openFileOutput(BREADCRUMSB_ACTIVITY_CACHE_FILE, Context.MODE_PRIVATE); oos = new ObjectOutputStream(fos); cache = new Object[] { CACHE_VERSION, new Date(), sActivityMappings }; oos.writeObject(cache); } catch (FileNotFoundException e) { Log.e(TAG, "Error in file stream during persist cache", e); } catch (IOException e) { Log.e(TAG, "Error in object stream during persist cache", e); } finally { if (fos != null) { try { fos.close(); } catch (IOException e) { Log.w(TAG, "Error closing file stream after writing cache", e); } } if (oos != null) { try { oos.close(); } catch (IOException e) { Log.w(TAG, "Error closing object stream after writing cache", e); } } } } } public void clearAllCache(Context ctx) { BreadcrumbsTracks.initCacheVariables(); setChanged(); clearPersistentCache(ctx); notifyObservers(); } public void clearPersistentCache(Context ctx) { Log.w(TAG, "Deleting old Breadcrumbs cache files"); synchronized (BREADCRUMSB_BUNDLES_CACHE_FILE) { ctx.deleteFile(BREADCRUMSB_ACTIVITY_CACHE_FILE); ctx.deleteFile(BREADCRUMSB_BUNDLES_CACHE_FILE); } } @Override public String toString() { return "BreadcrumbsTracks [mActivityMappings=" + sActivityMappings + ", mBundleMappings=" + sBundleMappings + ", mTrackMappings=" + sTrackMappings + ", mActivities=" + sActivityMappings + ", mBundles=" + sBundlesWithTracks + "]"; } }