/* * This source is part of the * _____ ___ ____ * __ / / _ \/ _ | / __/___ _______ _ * / // / , _/ __ |/ _/_/ _ \/ __/ _ `/ * \___/_/|_/_/ |_/_/ (_)___/_/ \_, / * /___/ * repository. * * Copyright (C) 2013 Benoit 'BoD' Lubek (BoD@JRAF.org) * * This program 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. * * This program 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 org.jraf.android.bikey.backend.ride; import java.util.Date; import java.util.UUID; import android.content.ContentResolver; import android.content.ContentUris; import android.content.Context; import android.content.Intent; import android.database.Cursor; import android.net.Uri; import android.preference.PreferenceManager; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.WorkerThread; import android.text.TextUtils; import android.text.format.DateUtils; import org.jraf.android.bikey.R; import org.jraf.android.bikey.app.Application; import org.jraf.android.bikey.app.collect.LogCollectorService; import org.jraf.android.bikey.backend.log.LogManager; import org.jraf.android.bikey.backend.provider.BikeyProvider; import org.jraf.android.bikey.backend.provider.log.LogContentValues; import org.jraf.android.bikey.backend.provider.log.LogSelection; import org.jraf.android.bikey.backend.provider.ride.RideColumns; import org.jraf.android.bikey.backend.provider.ride.RideContentValues; import org.jraf.android.bikey.backend.provider.ride.RideCursor; import org.jraf.android.bikey.backend.provider.ride.RideSelection; import org.jraf.android.bikey.backend.provider.ride.RideState; import org.jraf.android.bikey.common.Constants; import org.jraf.android.util.listeners.Listeners; import org.jraf.android.util.log.Log; public class RideManager { private static final RideManager INSTANCE = new RideManager(); public static RideManager get() { return INSTANCE; } private final Context mContext; private Listeners<RideListener> mListeners = Listeners.newInstance(); private RideManager() { mContext = Application.getApplication(); } @WorkerThread public Uri create(String name) { RideContentValues values = new RideContentValues(); values.putUuid(UUID.randomUUID().toString()); values.putCreatedDate(new Date()); if (!TextUtils.isEmpty(name)) { values.putName(name); } values.putState(RideState.CREATED); values.putDuration(0L); values.putDistance(0f); return values.insert(mContext); } @WorkerThread public int delete(long[] ids) { // First pause any active rides in the list pauseRides(ids); // Mark rides as deleted RideSelection rideSelection = new RideSelection(); rideSelection.id(ids); RideContentValues rideContentValues = new RideContentValues(); rideContentValues.putState(RideState.DELETED); int res = rideContentValues.update(mContext, rideSelection); // Delete logs LogSelection logSelection = new LogSelection(); logSelection.rideId(ids); logSelection.delete(mContext); // If we just deleted the current ride, select another ride to be the current ride (if any). Uri currentRideUri = getCurrentRide(); if (currentRideUri != null) { long currentRideId = Long.valueOf(currentRideUri.getLastPathSegment()); for (long id : ids) { if (currentRideId == id) { Uri nextRideUri = getMostRecentRide(); setCurrentRide(nextRideUri); break; } } } return res; } @WorkerThread public void merge(long[] ids) { // First pause any active rides in the list pauseRides(ids); // Choose the master ride (the one with the earliest creation date) String[] projection = {RideColumns._ID}; RideSelection rideSelection = new RideSelection(); rideSelection.id(ids); rideSelection.orderByCreatedDate(); ContentResolver contentResolver = mContext.getContentResolver(); RideCursor rideCursor = rideSelection.query(contentResolver, projection); long masterRideId; try { rideCursor.moveToNext(); masterRideId = rideCursor.getId(); } finally { rideCursor.close(); } // Calculate the total duration projection = new String[] {"sum(" + RideColumns.DURATION + ")"}; Cursor c = contentResolver.query(RideColumns.CONTENT_URI, projection, rideSelection.sel(), rideSelection.args(), null); long totalDuration = 0; try { if (c.moveToNext()) { totalDuration = c.getLong(0); } } finally { c.close(); } // Merge for (long mergedRideId : ids) { if (mergedRideId == masterRideId) continue; // Update logs LogSelection logSelection = new LogSelection(); logSelection.rideId(mergedRideId); LogContentValues values = new LogContentValues(); values.putRideId(masterRideId); values.update(contentResolver, logSelection); // Delete merged ride rideSelection = new RideSelection(); rideSelection.id(mergedRideId); // Do not notify yet Uri contentUri = BikeyProvider.notify(RideColumns.CONTENT_URI, false); contentResolver.delete(contentUri, rideSelection.sel(), rideSelection.args()); } // Rename master ride Uri masterRideUri = ContentUris.withAppendedId(RideColumns.CONTENT_URI, masterRideId); String name = getName(masterRideUri); if (name == null) { name = mContext.getString(R.string.ride_list_mergedRide); } else { name = mContext.getString(R.string.ride_list_mergedRide_append, name); } RideContentValues values = new RideContentValues(); values.putName(name); contentResolver.update(masterRideUri, values.values(), null, null); // Update master ride total distance float distance = LogManager.get().getTotalDistance(masterRideUri); updateTotalDistance(masterRideUri, distance); // Update master ride total duration updateDuration(masterRideUri, totalDuration); } private void pauseRides(long[] ids) { for (long rideId : ids) { Uri rideUri = ContentUris.withAppendedId(RideColumns.CONTENT_URI, rideId); RideState state = getState(rideUri); if (state == RideState.ACTIVE) { mContext.startService(new Intent(LogCollectorService.ACTION_STOP_COLLECTING, rideUri, mContext, LogCollectorService.class)); break; } } } @WorkerThread public void activate(@NonNull Uri rideUri) { // Get first activated date Date firstActivatedDate = getFirstActivatedDate(rideUri); // Update state RideContentValues values = new RideContentValues(); values.putState(RideState.ACTIVE); // Update activated date Date now = new Date(); values.putActivatedDate(now); // Update first activated date, only if first time if (firstActivatedDate == null) { values.putFirstActivatedDate(now); } mContext.getContentResolver().update(rideUri, values.values(), null, null); // Dispatch to listeners mListeners.dispatch(listener -> listener.onActivated(rideUri)); } @WorkerThread public void updateTotalDistance(@NonNull Uri rideUri, float distance) { RideContentValues values = new RideContentValues(); values.putDistance(distance); mContext.getContentResolver().update(rideUri, values.values(), null, null); } @WorkerThread private void updateDuration(@NonNull Uri rideUri, long duration) { RideContentValues values = new RideContentValues(); values.putDuration(duration); mContext.getContentResolver().update(rideUri, values.values(), null, null); } @WorkerThread public void updateName(@NonNull Uri rideUri, String name) { RideContentValues values = new RideContentValues(); if (TextUtils.isEmpty(name)) { values.putNameNull(); } else { values.putName(name); } mContext.getContentResolver().update(rideUri, values.values(), null, null); } @WorkerThread public void pause(@NonNull Uri rideUri) { // Get current activated date / duration String[] projection = {RideColumns.ACTIVATED_DATE, RideColumns.DURATION}; RideCursor c = new RideCursor(mContext.getContentResolver().query(rideUri, projection, null, null, null)); try { if (!c.moveToNext()) { Log.w("Could not pause ride, uri " + rideUri + " not found"); return; } long activatedDate = c.getActivatedDate().getTime(); long duration = c.getDuration(); // Update duration, state, and reset activated date duration += System.currentTimeMillis() - activatedDate; RideContentValues values = new RideContentValues(); values.putState(RideState.PAUSED); values.putDuration(duration); values.putActivatedDate(0l); mContext.getContentResolver().update(rideUri, values.values(), null, null); // Dispatch to listeners mListeners.dispatch(listener -> listener.onPaused(rideUri)); } finally { c.close(); } } /** * Queries all the columns for the given ride. * Do not forget to call {@link Cursor#close()} on the returned Cursor. */ public RideCursor query(@NonNull Uri rideUri) { if (rideUri == null) throw new IllegalArgumentException("null rideUri"); Cursor c = mContext.getContentResolver().query(rideUri, null, null, null, null); if (!c.moveToNext()) { c.close(); throw new IllegalArgumentException(rideUri + " not found"); } return new RideCursor(c); } @WorkerThread @Nullable public Uri getCurrentRide() { String currentRideUriStr = PreferenceManager.getDefaultSharedPreferences(mContext).getString(Constants.PREF_CURRENT_RIDE_URI, null); if (!TextUtils.isEmpty(currentRideUriStr)) { Uri currentRideUri = Uri.parse(currentRideUriStr); return currentRideUri; } return null; } @WorkerThread public void setCurrentRide(@NonNull Uri rideUri) { PreferenceManager.getDefaultSharedPreferences(mContext).edit().putString(Constants.PREF_CURRENT_RIDE_URI, rideUri.toString()).apply(); } @WorkerThread private Uri getMostRecentRide() { String[] projection = {RideColumns._ID}; // Return a ride, prioritizing ACTIVE ones first, then sorting by creation date. Cursor c = mContext.getContentResolver().query(RideColumns.CONTENT_URI, projection, null, null, RideColumns.STATE + ", " + RideColumns.CREATED_DATE + " DESC"); try { if (!c.moveToNext()) return null; long id = c.getLong(0); return ContentUris.withAppendedId(RideColumns.CONTENT_URI, id); } finally { c.close(); } } @WorkerThread public Date getActivatedDate(@NonNull Uri rideUri) { RideCursor c = query(rideUri); try { return c.getActivatedDate(); } finally { c.close(); } } @WorkerThread private Date getFirstActivatedDate(@NonNull Uri rideUri) { RideCursor c = query(rideUri); try { return c.getFirstActivatedDate(); } finally { c.close(); } } @WorkerThread public long getDuration(@NonNull Uri rideUri) { RideCursor c = query(rideUri); try { return c.getDuration(); } finally { c.close(); } } @WorkerThread public RideState getState(@NonNull Uri rideUri) { RideCursor c = query(rideUri); try { return c.getState(); } finally { c.close(); } } @WorkerThread public String getDisplayName(@NonNull Uri rideUri) { RideCursor c = query(rideUri); try { String name = c.getName(); long createdDateLong = c.getCreatedDate().getTime(); String createdDateTimeStr = DateUtils.formatDateTime(mContext, createdDateLong, DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME); if (name == null) { return createdDateTimeStr; } return name + " (" + createdDateTimeStr + ")"; } finally { c.close(); } } @WorkerThread public String getName(@NonNull Uri rideUri) { RideCursor c = query(rideUri); try { return c.getName(); } finally { c.close(); } } @WorkerThread public boolean isExistingRide(@NonNull Uri rideUri) { Cursor c = mContext.getContentResolver().query(rideUri, null, null, null, null); try { return c.moveToNext(); } finally { c.close(); } } /* * Listeners. */ public void addListener(@NonNull RideListener listener) { mListeners.add(listener); } public void removeListener(@NonNull RideListener listener) { mListeners.remove(listener); } }