Import: split ImportActivity into fragments.

The fragements keep the state while the activity is re-created (e.g., orientation change).
This commit is contained in:
Dennis Guse
2020-07-09 23:32:03 +02:00
parent 41a4a5527a
commit e9eb764d12
7 changed files with 366 additions and 178 deletions
@@ -18,7 +18,6 @@ package de.dennisguse.opentracks.fragments;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;
import androidx.annotation.NonNull;
@@ -69,17 +68,14 @@ public class ConfirmDeleteDialogFragment extends DialogFragment {
public Dialog onCreateDialog(Bundle savedInstanceState) {
final long[] trackIds = getArguments().getLongArray(KEY_TRACK_IDS);
int titleId;
int messageId;
titleId = trackIds.length > 1 ? R.string.generic_delete_selected_confirm_title : R.string.track_delete_one_confirm_title;
messageId = trackIds.length > 1 ? R.string.track_delete_multiple_confirm_message : R.string.track_delete_one_confirm_message;
int titleId = trackIds.length > 1 ? R.string.generic_delete_selected_confirm_title : R.string.track_delete_one_confirm_title;
int messageId = trackIds.length > 1 ? R.string.track_delete_multiple_confirm_message : R.string.track_delete_one_confirm_message;
return DialogUtils.createConfirmationDialog(
getActivity(), titleId, getString(messageId), new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
caller.onConfirmDeleteDone(trackIds);
}
});
getActivity(),
titleId,
getString(messageId),
(dialog, which) -> caller.onConfirmDeleteDone(trackIds)
);
}
/**
@@ -89,12 +85,6 @@ public class ConfirmDeleteDialogFragment extends DialogFragment {
*/
public interface ConfirmDeleteCaller {
/**
* Called when confirm delete is done.
*
* @param trackIds list of track ids to delete. To delete all, set to size 1
* with trackIds[0] == -1L
*/
void onConfirmDeleteDone(long[] trackIds);
}
}
@@ -0,0 +1,111 @@
/*
* Copyright 2012 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 de.dennisguse.opentracks.fragments;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.Context;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.FragmentManager;
import de.dennisguse.opentracks.R;
public class ImportFinishedDialogFragment extends DialogFragment {
private static final String TAG = ImportFinishedDialogFragment.class.getSimpleName();
private static final String EXTRA_DIRECTORY_NAME_KEY = "directory_name";
private static final String EXTRA_IMPORTED_TRACK_COUNT = "imported_track_count";
private static final String EXTRA_FILE_COUNT = "file_count";
private static final String FINISHED_DIALOG_TAG = "finished_dialog_tag";
private DismissCallback caller;
public static void showDialog(FragmentManager fragmentManager, String directoryDisplayName, int successCount, int fileCount) {
Bundle bundle = new Bundle();
bundle.putString(EXTRA_DIRECTORY_NAME_KEY, directoryDisplayName);
bundle.putInt(EXTRA_IMPORTED_TRACK_COUNT, successCount);
bundle.putInt(EXTRA_FILE_COUNT, fileCount);
ImportFinishedDialogFragment dialogFragment = new ImportFinishedDialogFragment();
dialogFragment.setArguments(bundle);
dialogFragment.show(fragmentManager, FINISHED_DIALOG_TAG);
}
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
try {
caller = (DismissCallback) context;
} catch (ClassCastException e) {
throw new ClassCastException(context + " must implement " + DismissCallback.class.getSimpleName());
}
}
@Override
@NonNull
public Dialog onCreateDialog(Bundle savedInstanceState) {
String directoryDisplayName = getArguments().getString(EXTRA_DIRECTORY_NAME_KEY);
int importedTrackCount = getArguments().getInt(EXTRA_IMPORTED_TRACK_COUNT);
int totalTrackCount = getArguments().getInt(EXTRA_FILE_COUNT);
int iconId;
int titleId;
String message;
String totalFiles = getResources().getQuantityString(R.plurals.files, totalTrackCount, totalTrackCount);
if (importedTrackCount == totalTrackCount) {
if (totalTrackCount == 0) {
iconId = R.drawable.ic_dialog_info_24dp;
titleId = R.string.import_no_file_title;
message = getString(R.string.import_no_file, directoryDisplayName);
} else {
iconId = R.drawable.ic_dialog_success_24dp;
titleId = R.string.generic_success_title;
message = getString(R.string.import_success, totalFiles, directoryDisplayName);
}
} else {
iconId = R.drawable.ic_dialog_error_24dp;
titleId = R.string.generic_error_title;
message = getString(R.string.import_error, importedTrackCount, totalFiles, directoryDisplayName);
}
Dialog dialog = new AlertDialog.Builder(getContext())
.setCancelable(true).setIcon(iconId)
.setMessage(message)
.setPositiveButton(R.string.generic_ok, (dialogInterface, which) -> {
dialogInterface.dismiss();
caller.onDismissed();
})
.setTitle(titleId)
.create();
dialog.setCanceledOnTouchOutside(false);
return dialog;
}
@Override
public void onDetach() {
super.onDetach();
caller = null;
}
public interface DismissCallback {
void onDismissed();
}
}
@@ -0,0 +1,224 @@
/*
* Copyright 2012 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 de.dennisguse.opentracks.fragments;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.documentfile.provider.DocumentFile;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.FragmentManager;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.io.file.importer.GpxFileTrackImporter;
import de.dennisguse.opentracks.io.file.importer.KmlFileTrackImporter;
import de.dennisguse.opentracks.io.file.importer.KmzTrackImporter;
import de.dennisguse.opentracks.io.file.importer.TrackImporter;
import de.dennisguse.opentracks.util.DialogUtils;
import de.dennisguse.opentracks.util.FileUtils;
public class ImportProgressDialogFragment extends DialogFragment {
private static final String EXTRA_DIRECTORY_URI_KEY = "directory_uri";
private static final String TAG = ImportProgressDialogFragment.class.getSimpleName();
public static final String IMPORT_DIALOG_TAG = "import_dialog_tag";
private DismissCallback caller;
private ImportThread importThread;
private String directoryDisplayName;
private int trackImportSuccessCount;
private int fileCount;
/**
* Create a new instance.
*/
public static void showDialog(FragmentManager fragmentManager, Uri documentFileUri) {
Bundle bundle = new Bundle();
bundle.putParcelable(EXTRA_DIRECTORY_URI_KEY, documentFileUri);
ImportProgressDialogFragment dialogFragment = new ImportProgressDialogFragment();
dialogFragment.setArguments(bundle);
dialogFragment.setRetainInstance(true);
dialogFragment.show(fragmentManager, IMPORT_DIALOG_TAG);
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// This fragment uses a background thread, so it should not be recreated during activity re-creation.
setRetainInstance(true);
Uri directoryUri = getArguments().getParcelable(EXTRA_DIRECTORY_URI_KEY);
DocumentFile documentFile = DocumentFile.fromTreeUri(getContext(), directoryUri);
directoryDisplayName = FileUtils.getPath(documentFile);
importThread = new ImportThread(documentFile);
}
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
try {
caller = (DismissCallback) context;
} catch (ClassCastException e) {
throw new ClassCastException(context + " must implement " + DismissCallback.class.getSimpleName());
}
}
@Override
@NonNull
public Dialog onCreateDialog(Bundle savedInstanceState) {
ProgressDialog dialog = DialogUtils.createHorizontalProgressDialog(
getContext(), R.string.import_progress_message,
dialogInterface -> {
importThread.interrupt();
dialogInterface.dismiss();
}, directoryDisplayName);
if (trackImportSuccessCount == 0) {
dialog.setIndeterminate(true);
} else {
setProgress();
}
return dialog;
}
@Override
public void onStart() {
super.onStart();
setProgress();
if (!importThread.isAlive()) {
importThread.start();
}
}
@Override
public void onPause() {
super.onPause();
if (!importThread.isAlive()) {
importThread.interrupt();
}
}
@Override
public void onDetach() {
super.onDetach();
caller = null;
}
private void onProgress(int trackImportSuccessCount, int fileCount) {
this.trackImportSuccessCount = trackImportSuccessCount;
this.fileCount = fileCount;
setProgress();
}
private void setProgress() {
ProgressDialog dialog = (ProgressDialog) getDialog();
if (dialog != null) {
dialog.setIndeterminate(false);
dialog.setMax(fileCount);
dialog.setProgress(Math.min(trackImportSuccessCount, fileCount));
}
}
private void onImportCompleted(int successCount, int fileCount) {
if (caller != null && !isDetached()) {
caller.onImportFinished(directoryDisplayName, successCount, fileCount);
}
dismissAllowingStateLoss();
}
private class ImportThread extends Thread {
private DocumentFile file;
ImportThread(DocumentFile file) {
this.file = file;
}
@Override
public void run() {
Log.w(TAG, "" + this);
Context context = ImportProgressDialogFragment.this.getContext();
List<DocumentFile> files = FileUtils.getFiles(file);
int importedTrackCount = 0;
for (int i = 0; i < files.size(); i++) {
onProgress(i, files.size());
if (Thread.interrupted()) {
return;
}
if (importFile(context, files.get(i))) {
importedTrackCount++;
}
}
onImportCompleted(importedTrackCount, files.size());
}
/**
* Imports a file.
*
* @param file the file
*/
private boolean importFile(final Context context, final DocumentFile file) {
TrackImporter trackImporter;
String fileExtension = FileUtils.getExtension(file);
if (TrackFileFormat.GPX.getExtension().equals(fileExtension)) {
trackImporter = new GpxFileTrackImporter(context);
} else if (TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA.getExtension().equals(fileExtension)) {
trackImporter = new KmlFileTrackImporter(ImportProgressDialogFragment.this.getContext(), -1L);
} else if (TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES.getExtension().equals(fileExtension)) {
trackImporter = new KmzTrackImporter(context, -1L, file.getUri());
} else {
Log.i(TAG, "Unsupported file format.");
return false;
}
try (InputStream inputStream = context.getContentResolver().openInputStream(file.getUri())) {
return trackImporter.importFile(inputStream) != -1L;
} catch (IOException e) {
Log.e(TAG, "Unable to import file", e);
return false;
}
}
}
public interface DismissCallback {
void onImportFinished(String directoryDisplayName, int successCount, int fileCount);
}
}
@@ -16,192 +16,53 @@
package de.dennisguse.opentracks.io.file.importer;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import androidx.documentfile.provider.DocumentFile;
import androidx.annotation.NonNull;
import androidx.fragment.app.FragmentActivity;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.util.DialogUtils;
import de.dennisguse.opentracks.util.FileUtils;
import de.dennisguse.opentracks.fragments.ImportFinishedDialogFragment;
import de.dennisguse.opentracks.fragments.ImportProgressDialogFragment;
/**
* An activity to import files from the external storage.
*
* @author Rodrigo Damazio
*/
public class ImportActivity extends FragmentActivity {
public class ImportActivity extends FragmentActivity implements ImportProgressDialogFragment.DismissCallback, ImportFinishedDialogFragment.DismissCallback {
private static final String TAG = ImportActivity.class.getSimpleName();
public static final String EXTRA_DIRECTORY_URI_KEY = "directory_uri";
private static final int DIALOG_PROGRESS_ID = 0;
private static final int DIALOG_RESULT_ID = 1;
private DocumentFile pickedDirectory;
private String directoryDisplayName;
private Thread importTask = new ImportThread();
private ProgressDialog progressDialog;
private int importedTrackCount;
private int totalTrackCount;
private static final String BUNDLE_ACTIVITY_RECREATED = "activity_recreated";
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Uri directoryUri = getIntent().getParcelableExtra(EXTRA_DIRECTORY_URI_KEY);
pickedDirectory = DocumentFile.fromTreeUri(this, directoryUri);
directoryDisplayName = FileUtils.getPath(pickedDirectory);
}
@Override
protected void onStart() {
super.onStart();
showDialog(DIALOG_PROGRESS_ID);
importTask.start();
}
@Override
protected void onPause() {
super.onPause();
importTask.interrupt();
}
@Override
protected Dialog onCreateDialog(int id) {
switch (id) {
case DIALOG_PROGRESS_ID:
progressDialog = DialogUtils.createHorizontalProgressDialog(
this, R.string.import_progress_message, dialog -> {
importTask.interrupt();
dialog.dismiss();
finish();
}, directoryDisplayName);
return progressDialog;
case DIALOG_RESULT_ID:
int iconId;
int titleId;
String message;
String totalFiles = getResources()
.getQuantityString(R.plurals.files, totalTrackCount, totalTrackCount);
if (importedTrackCount == totalTrackCount) {
if (totalTrackCount == 0) {
iconId = R.drawable.ic_dialog_info_24dp;
titleId = R.string.import_no_file_title;
message = getString(R.string.import_no_file, directoryDisplayName);
} else {
iconId = R.drawable.ic_dialog_success_24dp;
titleId = R.string.generic_success_title;
message = getString(R.string.import_success, totalFiles, directoryDisplayName);
}
} else {
iconId = R.drawable.ic_dialog_error_24dp;
titleId = R.string.generic_error_title;
message = getString(R.string.import_error, importedTrackCount, totalFiles, directoryDisplayName);
}
return new AlertDialog.Builder(this).setCancelable(true).setIcon(iconId)
.setMessage(message).setOnCancelListener(dialogInterface -> {
dialogInterface.dismiss();
finish();
}).setPositiveButton(R.string.generic_ok, (dialogInterface, which) -> {
dialogInterface.dismiss();
finish();
}).setTitle(titleId).create();
default:
return null;
if (savedInstanceState == null || !savedInstanceState.getBoolean(BUNDLE_ACTIVITY_RECREATED, false)) {
Uri directoryUri = getIntent().getParcelableExtra(EXTRA_DIRECTORY_URI_KEY);
ImportProgressDialogFragment.showDialog(getSupportFragmentManager(), directoryUri);
}
}
/**
* Invokes when the associated AsyncTask completes.
*
* @param aSuccessCount the number of files successfully imported
* @param aTotalCount the number of files to import
*/
public void onAsyncTaskCompleted(int aSuccessCount, int aTotalCount) {
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBoolean(BUNDLE_ACTIVITY_RECREATED, true);
}
@Override
public void onImportFinished(final String directoryDisplayName, final int successCount, final int fileCount) {
runOnUiThread(() -> {
importedTrackCount = aSuccessCount;
totalTrackCount = aTotalCount;
removeDialog(DIALOG_PROGRESS_ID);
showDialog(DIALOG_RESULT_ID);
ImportFinishedDialogFragment.showDialog(getSupportFragmentManager(), directoryDisplayName, successCount, fileCount);
});
}
/**
* Sets the progress dialog value.
*
* @param number the number of files imported
* @param max the maximum number of files
*/
public void setProgressDialogValue(int number, int max) {
if (progressDialog != null) {
progressDialog.setIndeterminate(false);
progressDialog.setMax(max);
progressDialog.setProgress(Math.min(number, max));
}
}
public class ImportThread extends Thread {
@Override
public void run() {
List<DocumentFile> files = FileUtils.getFiles(pickedDirectory);
int totalTrackCount = files.size();
int importedTrackCount = 0;
for (int i = 0; i < totalTrackCount; i++) {
if (Thread.interrupted()) {
return;
}
if (importFile(files.get(i))) {
importedTrackCount++;
}
setProgressDialogValue(i + 1, totalTrackCount);
}
onAsyncTaskCompleted(importedTrackCount, totalTrackCount);
}
/**
* Imports a file.
*
* @param file the file
*/
private boolean importFile(final DocumentFile file) {
TrackImporter trackImporter;
String fileExtension = FileUtils.getExtension(file);
if (TrackFileFormat.GPX.getExtension().equals(fileExtension)) {
trackImporter = new GpxFileTrackImporter(ImportActivity.this);
} else if (TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA.getExtension().equals(fileExtension)) {
trackImporter = new KmlFileTrackImporter(ImportActivity.this, -1L);
} else if (TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES.getExtension().equals(fileExtension)) {
trackImporter = new KmzTrackImporter(ImportActivity.this, -1L, file.getUri());
} else {
Log.i(TAG, "Unsupported file format.");
return false;
}
try (InputStream inputStream = ImportActivity.this.getContentResolver().openInputStream(file.getUri())) {
return trackImporter.importFile(inputStream) != -1L;
} catch (IOException e) {
Log.e(TAG, "Unable to import file", e);
return false;
}
}
@Override
public void onDismissed() {
finish();
}
}
@@ -60,7 +60,7 @@ public class KmzTrackImporter implements TrackImporter {
* @param importTrackId track id to import to. This should not be -1L so that images in the kmz file can be imported.
* @param uriFile URI of the kmz file.
*/
KmzTrackImporter(Context context, long importTrackId, Uri uriFile) {
public KmzTrackImporter(Context context, long importTrackId, Uri uriFile) {
this.context = context;
this.importTrackId = importTrackId;
this.uriKmzFile = uriFile;
@@ -22,7 +22,7 @@ import java.io.InputStream;
*
* @author Jimmy Shih
*/
interface TrackImporter {
public interface TrackImporter {
/**
* Import a file.
@@ -309,6 +309,8 @@ public class FileUtils {
public static ArrayList<DocumentFile> getFiles(DocumentFile file) {
ArrayList<DocumentFile> files = new ArrayList<>();
if (!file.isDirectory()) return files;
for (DocumentFile candidate : file.listFiles()) {
if (!candidate.isDirectory()) {
files.add(candidate);