Export & import via Storage Access Framework.

This commit is contained in:
Dennis Guse
2019-12-28 19:56:37 +01:00
parent 90e19e282c
commit a230504925
9 changed files with 136 additions and 166 deletions
@@ -69,7 +69,7 @@ public class DescriptionGeneratorTest {
track.setTripStatistics(stats);
track.setCategory("hiking");
String expected = //"Created by"
"<a href='https://github.com/OpenTracksApp/opentracks'>OpenTracks</a><p>"
"<a href='https://github.com/OpenTracksApp/OpenTracks'>OpenTracks</a><p>"
+ "Name: -<br>"
+ "Activity type: hiking<br>"
+ "Description: -<br>"
-3
View File
@@ -25,7 +25,6 @@ limitations under the License.
android:hardwareAccelerated="true"
android:icon="@drawable/ic_logo_color_24dp"
android:label="@string/app_name"
android:requestLegacyExternalStorage="true"
android:theme="@style/ThemeCustom">
<!-- Activities -->
@@ -152,6 +151,4 @@ limitations under the License.
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
</manifest>
@@ -228,6 +228,7 @@ public class ContentProviderUtils {
* @param dir the directory
*/
private void deleteDirectoryRecurse(Context context, File dir) {
// TODO Does not work with
if (FileUtils.isDirectory(dir)) {
for (File child : dir.listFiles()) {
deleteDirectoryRecurse(context, child);
@@ -16,21 +16,20 @@
package de.dennisguse.opentracks.io.file.exporter;
import android.Manifest;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.DialogInterface;
import android.content.pm.PackageManager;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.core.app.ActivityCompat;
import androidx.annotation.Nullable;
import androidx.documentfile.provider.DocumentFile;
import androidx.fragment.app.FragmentActivity;
import java.io.File;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.fragments.FileTypeDialogFragment;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
@@ -39,7 +38,6 @@ import de.dennisguse.opentracks.util.FileUtils;
/**
* An activity for saving tracks to the external storage.
* If saving a specific track, option to save it to a temp directory and play the track afterward.
*
* @author Rodrigo Damazio
*/
@@ -53,34 +51,39 @@ public class ExportActivity extends FragmentActivity implements FileTypeDialogFr
private String directoryDisplayName;
private ExportAsyncTask exportAsyncTask;
private ProgressDialog progressDialog;
private int processedTrackCount;
private int totalTrackCount;
private Uri directoryUri;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
if (!FileUtils.isExternalStorageWriteable() || ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) {
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, EXTERNAL_STORAGE_PERMISSION_REQUEST_CODE);
} else {
fileTypeDialogStart();
}
startActivityForResult(intent, EXTERNAL_STORAGE_PERMISSION_REQUEST_CODE);
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent resultData) {
super.onActivityResult(requestCode, resultCode, resultData);
if (requestCode == EXTERNAL_STORAGE_PERMISSION_REQUEST_CODE) {
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) {
Toast.makeText(this, R.string.external_storage_not_writable, Toast.LENGTH_LONG).show();
} else {
if (resultCode == Activity.RESULT_OK) {
// final int takeFlags = resultData.getFlags()
// & (Intent.FLAG_GRANT_READ_URI_PERMISSION
// | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
//// Check for the freshest data.
// getContentResolver().takePersistableUriPermission(uri, takeFlags);
directoryUri = resultData.getData();
fileTypeDialogStart();
} else {
Toast.makeText(this, R.string.external_storage_not_writable, Toast.LENGTH_LONG).show();
finish();
}
return;
}
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
private void fileTypeDialogStart() {
@@ -89,21 +92,14 @@ public class ExportActivity extends FragmentActivity implements FileTypeDialogFr
@Override
public void onFileTypeDone(TrackFileFormat trackFileFormat) {
if (!FileUtils.isExternalStorageWriteable()) {
Toast.makeText(this, R.string.external_storage_not_writable, Toast.LENGTH_LONG).show();
finish();
return;
}
File directory = new File(FileUtils.getPath(trackFileFormat.getExtension()));
if (!FileUtils.ensureDirectoryExists(directory)) {
Toast.makeText(this, R.string.external_storage_not_writable, Toast.LENGTH_LONG).show();
finish();
return;
DocumentFile pickedDirectory = DocumentFile.fromTreeUri(this, directoryUri);
DocumentFile exportDirectory = pickedDirectory.findFile(FileUtils.SDCARD_TOP_DIR);
if (exportDirectory == null) {
exportDirectory = pickedDirectory.createDirectory(FileUtils.SDCARD_TOP_DIR);
}
directoryDisplayName = FileUtils.getPathDisplayName(trackFileFormat.getExtension());
exportAsyncTask = new ExportAsyncTask(this, trackFileFormat, directory);
exportAsyncTask = new ExportAsyncTask(this, trackFileFormat, exportDirectory);
exportAsyncTask.execute();
}
@@ -22,16 +22,16 @@ import android.os.AsyncTask;
import android.os.PowerManager.WakeLock;
import android.util.Log;
import java.io.File;
import androidx.documentfile.provider.DocumentFile;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import de.dennisguse.opentracks.content.ContentProviderUtils;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TracksColumns;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.util.FileUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.SystemUtils;
@@ -45,7 +45,7 @@ public class ExportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
private static final String TAG = ExportAsyncTask.class.getSimpleName();
private final TrackFileFormat trackFileFormat;
private final File directory;
private final DocumentFile directory;
private final Context context;
private final ContentProviderUtils contentProviderUtils;
private ExportActivity exportActivity;
@@ -63,7 +63,7 @@ public class ExportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
* @param trackFileFormat the track file format
* @param directory the directory to write the file
*/
public ExportAsyncTask(ExportActivity exportActivity, TrackFileFormat trackFileFormat, File directory) {
public ExportAsyncTask(ExportActivity exportActivity, TrackFileFormat trackFileFormat, DocumentFile directory) {
this.exportActivity = exportActivity;
this.trackFileFormat = trackFileFormat;
this.directory = directory;
@@ -156,13 +156,13 @@ public class ExportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
};
TrackExporter trackExporter = trackFileFormat.newTrackExporter(context, tracks, trackExporterListener);
Track track = tracks[0];
String fileName = FileUtils.buildUniqueFileName(directory, track.getName(), trackFileFormat.getExtension());
File file = new File(directory, fileName);
try (FileOutputStream fileOutputStream = new FileOutputStream(file)) {
if (trackExporter.writeTrack(context, fileOutputStream)) {
String fileName = track.getId() + "." + trackFileFormat.getExtension();
DocumentFile file = directory.createFile(trackFileFormat.getMimeType(), fileName);
try (OutputStream outputStream = context.getContentResolver().openOutputStream(file.getUri())) {
if (trackExporter.writeTrack(context, outputStream)) {
return true;
} else {
if (!file.delete()) {
@@ -16,21 +16,20 @@
package de.dennisguse.opentracks.io.file.importer;
import android.Manifest;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.DialogInterface;
import android.content.pm.PackageManager;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.core.app.ActivityCompat;
import androidx.annotation.Nullable;
import androidx.documentfile.provider.DocumentFile;
import androidx.fragment.app.FragmentActivity;
import java.io.File;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.fragments.FileTypeDialogFragment;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
@@ -38,8 +37,7 @@ import de.dennisguse.opentracks.util.DialogUtils;
import de.dennisguse.opentracks.util.FileUtils;
/**
* An activity to import files from the external storage. Optionally to import
* one specific file.
* An activity to import files from the external storage.
*
* @author Rodrigo Damazio
*/
@@ -50,70 +48,58 @@ public class ImportActivity extends FragmentActivity implements FileTypeDialogFr
private static final int DIALOG_PROGRESS_ID = 0;
private static final int DIALOG_RESULT_ID = 1;
private String directoryDisplayName;
private ImportAsyncTask importAsyncTask;
private ProgressDialog progressDialog;
// the path on the external storage to import
private String directoryDisplayName;
private int importedTrackCount;
private int totalTrackCount;
private Uri directoryUri;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (!FileUtils.isExternalStorageWriteable() || ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) {
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, EXTERNAL_STORAGE_PERMISSION_REQUEST_CODE);
} else {
fileTypeDialogStart();
}
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
startActivityForResult(intent, EXTERNAL_STORAGE_PERMISSION_REQUEST_CODE);
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent resultData) {
super.onActivityResult(requestCode, resultCode, resultData);
if (requestCode == EXTERNAL_STORAGE_PERMISSION_REQUEST_CODE) {
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) {
Toast.makeText(this, R.string.external_storage_not_readable, Toast.LENGTH_LONG).show();
} else {
if (resultCode == Activity.RESULT_OK) {
// final int takeFlags = resultData.getFlags()
// & (Intent.FLAG_GRANT_READ_URI_PERMISSION
// | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
//// Check for the freshest data.
// getContentResolver().takePersistableUriPermission(uri, takeFlags);
directoryUri = resultData.getData();
fileTypeDialogStart();
} else {
Toast.makeText(this, R.string.external_storage_not_writable, Toast.LENGTH_LONG).show();
finish();
}
return;
}
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
private void fileTypeDialogStart() {
FileTypeDialogFragment.showDialog(getSupportFragmentManager(), R.string.import_selection_title, R.string.import_selection_option);
}
@Override
public void onFileTypeDone(TrackFileFormat trackFileFormat) {
if (!FileUtils.isExternalStorageAvailable()) {
Toast.makeText(this, R.string.external_storage_not_available, Toast.LENGTH_LONG).show();
finish();
return;
}
DocumentFile pickedDirectory = DocumentFile.fromTreeUri(this, directoryUri);
directoryDisplayName = FileUtils.getPathDisplayName(trackFileFormat.getExtension());
String directoryPath = FileUtils.getPath(trackFileFormat.getExtension());
if (!FileUtils.isDirectory(new File(directoryPath))) {
Toast.makeText(this, getString(R.string.import_no_directory, directoryDisplayName), Toast.LENGTH_LONG)
.show();
finish();
return;
}
//TODO (still needed?): getLastNonConfiguration instance returned ExportAsyncTask before
// Object retained = getLastNonConfigurationInstance();
// if (retained instanceof ImportAsyncTask) {
// importAsyncTask = (ImportAsyncTask) retained;
// importAsyncTask.setActivity(this);
// } else {
importAsyncTask = new ImportAsyncTask(this, trackFileFormat, directoryPath);
importAsyncTask = new ImportAsyncTask(this, trackFileFormat, pickedDirectory);
importAsyncTask.execute();
// }
}
@Override
@@ -154,8 +140,7 @@ public class ImportActivity extends FragmentActivity implements FileTypeDialogFr
} else {
iconId = R.drawable.ic_dialog_error_24dp;
titleId = R.string.generic_error_title;
message = getString(
R.string.import_error, importedTrackCount, totalFiles, directoryDisplayName);
message = getString(R.string.import_error, importedTrackCount, totalFiles, directoryDisplayName);
}
return new AlertDialog.Builder(this).setCancelable(true).setIcon(iconId)
.setMessage(message).setOnCancelListener(new DialogInterface.OnCancelListener() {
@@ -22,9 +22,10 @@ import android.os.AsyncTask;
import android.os.PowerManager.WakeLock;
import android.util.Log;
import java.io.File;
import java.io.FileInputStream;
import androidx.documentfile.provider.DocumentFile;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
@@ -44,7 +45,7 @@ public class ImportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
private static final String TAG = ImportAsyncTask.class.getSimpleName();
private final TrackFileFormat trackFileFormat;
private final String path;
private final DocumentFile directory;
// TODO Can be removed, or?
private Context context;
// TODO Use weak reference
@@ -66,12 +67,12 @@ public class ImportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
*
* @param importActivity the activity currently associated with this AsyncTask
* @param trackFileFormat the track file format
* @param path path to import GPX files
* @param directory path to import GPX files
*/
public ImportAsyncTask(ImportActivity importActivity, TrackFileFormat trackFileFormat, String path) {
public ImportAsyncTask(ImportActivity importActivity, TrackFileFormat trackFileFormat, DocumentFile directory) {
this.importActivity = importActivity;
this.trackFileFormat = trackFileFormat;
this.path = path;
this.directory = directory;
context = importActivity.getApplicationContext();
completed = false;
@@ -99,39 +100,26 @@ public class ImportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
}
}
@Override
protected Boolean doInBackground(Void... params) {
try {
Thread.currentThread().setPriority(Thread.MAX_PRIORITY); // TODO Should be set to previous level at the end of this method.
// Get the wake lock if not recording or paused
boolean isRecording = PreferencesUtils.isRecording(importActivity);
boolean isPaused = PreferencesUtils.isRecordingTrackPaused(importActivity);
if (!isRecording || isPaused) {
wakeLock = SystemUtils.acquireWakeLock(importActivity, wakeLock);
}
/**
* Gets a list of files.
* If importAll is true, returns a list of the files under the path directory.
* If importAll is false, returns a list containing just the path file.
*/
private static List<DocumentFile> getFiles(DocumentFile file, TrackFileFormat trackFileFormat) {
List<DocumentFile> files = new ArrayList<>();
List<File> files = getFiles();
totalTrackCount = files.size();
if (totalTrackCount == 0) {
return true;
}
for (int i = 0; i < totalTrackCount; i++) {
if (isCancelled()) {
// If cancelled, return true to show the number of files imported
return true;
for (DocumentFile candidate : file.listFiles()) {
if (!candidate.isDirectory()) {
String extension = FileUtils.getExtension(candidate.getName());
if (trackFileFormat.getExtension().equals(extension)) {
files.add(candidate);
}
if (importFile(files.get(i))) {
importTrackCount++;
}
publishProgress(i + 1, totalTrackCount);
}
return true;
} finally {
if (wakeLock != null && wakeLock.isHeld()) {
wakeLock.release();
} else {
files.addAll(getFiles(candidate, trackFileFormat));
}
}
return files;
}
@Override
@@ -159,16 +147,49 @@ public class ImportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
}
}
@Override
protected Boolean doInBackground(Void... params) {
try {
Thread.currentThread().setPriority(Thread.MAX_PRIORITY); // TODO Should be set to previous level at the end of this method.
// Get the wake lock if not recording or paused
boolean isRecording = PreferencesUtils.isRecording(importActivity);
boolean isPaused = PreferencesUtils.isRecordingTrackPaused(importActivity);
if (!isRecording || isPaused) {
wakeLock = SystemUtils.acquireWakeLock(importActivity, wakeLock);
}
List<DocumentFile> files = getFiles(directory, trackFileFormat);
totalTrackCount = files.size();
for (int i = 0; i < totalTrackCount; i++) {
if (isCancelled()) {
// If cancelled, return true to show the number of files imported
return true;
}
if (importFile(files.get(i))) {
importTrackCount++;
}
publishProgress(i + 1, totalTrackCount);
}
return true;
} finally {
if (wakeLock != null && wakeLock.isHeld()) {
wakeLock.release();
}
}
}
/**
* Imports a file.
*
* @param file the file
*/
private boolean importFile(final File file) {
private boolean importFile(final DocumentFile file) {
TrackImporter trackImporter;
if (trackFileFormat == TrackFileFormat.GPX) {
trackImporter = new GpxFileTrackImporter(importActivity);
} else { //KML or KMZ
} else {
//KML or KMZ
String extension = FileUtils.getExtension(file.getName());
if (TrackFileFormat.KML_ONLY_TRACK.getExtension().equals(extension)) {
trackImporter = new KmlFileTrackImporter(importActivity, -1L);
@@ -181,37 +202,12 @@ public class ImportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
}
}
try (FileInputStream fileInputStream = new FileInputStream(file)) {
trackId = trackImporter.importFile(fileInputStream);
try (InputStream inputStream = context.getContentResolver().openInputStream(file.getUri())) {
trackId = trackImporter.importFile(inputStream);
return trackId != -1L;
} catch (IOException e) {
Log.e(TAG, "Unable to import file", e);
return false;
}
}
/**
* Gets a list of files.
* If importAll is true, returns a list of the files under the path directory.
* If importAll is false, returns a list containing just the path file.
*/
private List<File> getFiles() {
List<File> files = new ArrayList<>();
File file = new File(path);
File[] candidates = file.listFiles();
if (candidates == null) {
return files;
}
for (File candidate : candidates) {
if (!FileUtils.isDirectory(candidate)) {
String extension = FileUtils.getExtension(candidate.getName());
if (trackFileFormat.getExtension().equals(extension)) {
files.add(candidate);
}
}
}
return files;
}
}
@@ -39,7 +39,7 @@ public class FileUtils {
/**
* Our external storage directory.
*/
static final String SDCARD_TOP_DIR = "OpenTracks";
public static final String SDCARD_TOP_DIR = "OpenTracks"; // TODO RENAME
private static final String TRACK_PREFIX = "OpenTracks";
/**
* The maximum FAT32 path length. See the FAT32 spec at
@@ -54,15 +54,6 @@ public class FileUtils {
private FileUtils() {
}
/**
* Returns true if the external storage is available.
*/
public static boolean isExternalStorageAvailable() {
String state = Environment.getExternalStorageState();
return Environment.MEDIA_MOUNTED.equals(state)
|| Environment.MEDIA_MOUNTED_READ_ONLY.equals(state);
}
/**
* Returns true if the external storage is writable.
*/
@@ -76,6 +67,7 @@ public class FileUtils {
*
* @param dir the directory
*/
@Deprecated // TODO REMOVE" rewrite all locations this was used.
public static boolean isDirectory(File dir) {
return dir.exists() && dir.isDirectory();
}
@@ -86,6 +78,7 @@ public class FileUtils {
* @return whether the directory exists (either already existed or was
* successfully created)
*/
@Deprecated // TODO REMOVE" rewrite all locations this was used.
public static boolean ensureDirectoryExists(File dir) {
if (isDirectory(dir)) {
return true;
@@ -93,10 +86,12 @@ public class FileUtils {
return dir.mkdirs();
}
@Deprecated // TODO REMOVE" rewrite all locations this was used.
public static File getPhotoDir() {
return new File(getPath(PICTURES_DIR));
}
@Deprecated // TODO REMOVE" rewrite all locations this was used.
public static File getPhotoDir(long trackId) {
return new File(getPath(PICTURES_DIR, TRACK_PREFIX + trackId));
}
@@ -122,6 +117,7 @@ public class FileUtils {
*
* @param components the components
*/
@Deprecated // TODO REMOVE" rewrite all locations this was used.
public static String getPath(String... components) {
return Environment.getExternalStorageDirectory() + getPathDisplayName(components);
}
+1 -2
View File
@@ -805,8 +805,7 @@ limitations under the License.
dialog when there is no files to import.
%1$s the directory. E.g., '/kml'.">
There were no files to import. Connect your device to your computer and place the files to
import in %1$s.
There were no files to import. Connect your device to your computer and place the files to import in %1$s.
</string>
<string name="import_no_file_title" translation_description="When importing from the external storage, the title in the result
dialog when there is no files to import.">