/* * Copyright 2008 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package com.google.android.apps.mytracks.io; import com.google.android.apps.mytracks.MyTracksConstants; import com.google.android.apps.mytracks.content.MyTracksLocation; import com.google.android.apps.mytracks.content.MyTracksProviderUtils; import com.google.android.apps.mytracks.content.Track; import com.google.android.apps.mytracks.content.Waypoint; import com.google.android.apps.mytracks.content.MyTracksProviderUtils.LocationIterator; import com.google.android.apps.mytracks.util.FileUtils; import com.google.android.apps.mytracks.util.MyTracksUtils; import com.google.android.maps.mytracks.R; import android.app.Activity; import android.content.Context; import android.database.Cursor; import android.location.Location; import android.util.Log; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.OutputStream; /** * The class which exports tracks to the SD card. * This writer is format-neutral - it handles creating the output file * and reading the track to be exported, but requires an instance of * {@link TrackFormatWriter} to actually format the data. * * @author Sandor Dornbush * @author Rodrigo Damazio */ public class TrackWriter { private final Context context; private final MyTracksProviderUtils providerUtils; private final Track track; private final TrackFormatWriter writer; private final FileUtils fileUtils; private Runnable onCompletion = null; private boolean success = false; private int errorMessage = -1; private File directory = null; private File file = null; public TrackWriter(Context context, MyTracksProviderUtils providerUtils, Track track, TrackFormatWriter writer) { this.context = context; this.providerUtils = providerUtils; this.track = track; this.writer = writer; this.fileUtils = new FileUtils(); } /** * Sets a completion callback. * * @param onCompletion Runnable that will be executed when finished */ public void setOnCompletion(Runnable onCompletion) { this.onCompletion = onCompletion; } /** * Sets a custom directory where the file will be written. */ public void setDirectory(File directory) { this.directory = directory; } public String getAbsolutePath() { return file.getAbsolutePath(); } /** * Writes the given track id to the SD card. * This is non-blocking. */ public void writeTrackAsync() { Thread t = new Thread() { @Override public void run() { writeTrack(); } }; t.start(); } /** * Writes the given track id to the SD card. * This is blocking. */ public void writeTrack() { // Open the input and output success = false; errorMessage = R.string.error_track_does_not_exist; if (track != null) { if (openFile()) { writeDocument(); } } finished(); } public int getErrorMessage() { return errorMessage; } public boolean wasSuccess() { return success; } /* * Helper methods: * =============== */ private void finished() { if (onCompletion != null) { runOnUiThread(onCompletion); return; } } /** * Runs the given runnable in the UI thread. */ protected void runOnUiThread(Runnable runnable) { if (context instanceof Activity) { ((Activity) context).runOnUiThread(runnable); } } /** * Opens the file and prepares the format writer for it. * * @return true on success, false otherwise (and errorMessage is set) */ protected boolean openFile() { if (!canWriteFile()) { return false; } // Make sure the file doesn't exist yet (possibly by changing the filename) String fileName = fileUtils.buildUniqueFileName( directory, track.getName(), writer.getExtension()); if (fileName == null) { Log.e(MyTracksConstants.TAG, "Unable to get a unique filename for " + fileName); return false; } Log.i(MyTracksConstants.TAG, "Writing track to: " + fileName); try { writer.prepare(track, newOutputStream(fileName)); } catch (FileNotFoundException e) { Log.e(MyTracksConstants.TAG, "Failed to open output file.", e); errorMessage = R.string.io_write_failed; return false; } return true; } /** * Checks and returns whether we're ready to create the output file. */ protected boolean canWriteFile() { if (directory == null) { String dirName = fileUtils.buildExternalDirectoryPath(writer.getExtension()); directory = newFile(dirName); } if (!fileUtils.isSdCardAvailable()) { Log.i(MyTracksConstants.TAG, "Could not find SD card."); errorMessage = R.string.io_no_external_storage_found; return false; } if (!fileUtils.ensureDirectoryExists(directory)) { Log.i(MyTracksConstants.TAG, "Could not create export directory."); errorMessage = R.string.io_create_dir_failed; return false; } return true; } /** * Creates a new output stream to write to the given filename. * * @throws FileNotFoundException if the file could't be created */ protected OutputStream newOutputStream(String fileName) throws FileNotFoundException { file = new File(directory, fileName); return new FileOutputStream(file); } /** * Creates a new file object for the given path. */ protected File newFile(String path) { return new File(path); } /** * Writes the waypoints for the given track. * * @param trackId the ID of the track to write waypoints for */ private void writeWaypoints(long trackId) { // TODO: Stream through he waypoints in chunks. // I am leaving the number of waypoints very high which should not be a // problem because we don't try to load them into objects all at the // same time. Cursor cursor = null; cursor = providerUtils.getWaypointsCursor(trackId, 0, MyTracksConstants.MAX_LOADED_WAYPOINTS_POINTS); if (cursor != null) { try { if (cursor.moveToFirst()) { // Yes, this will skip the 1st way point and that is intentional // as the 1st points holds the stats for the current/last segment. while (cursor.moveToNext()) { Waypoint wpt = providerUtils.createWaypoint(cursor); writer.writeWaypoint(wpt); } } } finally { if (cursor != null) { cursor.close(); } } } } /** * Does the actual work of writing the track to the now open file. */ void writeDocument() { Log.d(MyTracksConstants.TAG, "Started writing track."); writer.writeHeader(); writeWaypoints(track.getId()); writeLocations(); writer.writeFooter(); writer.close(); success = true; Log.d(MyTracksConstants.TAG, "Done writing track."); errorMessage = R.string.io_write_finished; } private void writeLocations() { boolean wroteFirst = false; boolean segmentOpen = false; boolean isLastValid = false; class TrackWriterLocationFactory implements MyTracksProviderUtils.LocationFactory { Location currentLocation; Location lastLocation; @Override public Location createLocation() { if (currentLocation == null) { currentLocation = new MyTracksLocation(""); } return currentLocation; } public void swapLocations() { Location tmpLoc = lastLocation; lastLocation = currentLocation; currentLocation = tmpLoc; if (currentLocation != null) { currentLocation.reset(); } } }; TrackWriterLocationFactory locationFactory = new TrackWriterLocationFactory(); LocationIterator it = providerUtils.getLocationIterator(track.getId(), 0, false, locationFactory); try { if (!it.hasNext()) { Log.w(MyTracksConstants.TAG, "Unable to get any points to write"); return; } while (it.hasNext()) { Location loc = it.next(); boolean isValid = MyTracksUtils.isValidLocation(loc); boolean validSegment = isValid && isLastValid; if (!wroteFirst && validSegment) { // Found the first two consecutive points which are valid writer.writeBeginTrack(locationFactory.lastLocation); wroteFirst = true; } if (validSegment) { if (!segmentOpen) { // Start a segment for this point writer.writeOpenSegment(); segmentOpen = true; // Write the previous point, which we had previously skipped writer.writeLocation(locationFactory.lastLocation); } // Write the current point writer.writeLocation(loc); } else { if (segmentOpen) { writer.writeCloseSegment(); segmentOpen = false; } } locationFactory.swapLocations(); isLastValid = isValid; } if (segmentOpen) { writer.writeCloseSegment(); segmentOpen = false; } if (wroteFirst) { writer.writeEndTrack(locationFactory.lastLocation); } } finally { it.close(); } } }