From fed2e356c381e8bf04ac906fa55b1c6eca60db7c Mon Sep 17 00:00:00 2001 From: nazar-kutz Date: Tue, 20 Apr 2021 19:30:33 +0300 Subject: [PATCH 1/8] SRTM Meter / Feet dialogs - fix bugs and refactoring --- .../base/MultipleSelectionBottomSheet.java | 4 +- .../plus/base/SelectionBottomSheet.java | 27 ++- .../plus/download/DownloadResources.java | 2 +- .../net/osmand/plus/download/IndexItem.java | 3 + .../plus/download/MultipleDownloadItem.java | 3 - ...UiHelper.java => SelectIndexesHelper.java} | 199 +++++++++++------- .../plus/download/SrtmDownloadItem.java | 7 +- .../plus/download/ui/ItemViewHolder.java | 6 +- 8 files changed, 157 insertions(+), 94 deletions(-) rename OsmAnd/src/net/osmand/plus/download/{SelectIndexesUiHelper.java => SelectIndexesHelper.java} (60%) diff --git a/OsmAnd/src/net/osmand/plus/base/MultipleSelectionBottomSheet.java b/OsmAnd/src/net/osmand/plus/base/MultipleSelectionBottomSheet.java index fc7d75416a..35481944f2 100644 --- a/OsmAnd/src/net/osmand/plus/base/MultipleSelectionBottomSheet.java +++ b/OsmAnd/src/net/osmand/plus/base/MultipleSelectionBottomSheet.java @@ -94,9 +94,9 @@ public class MultipleSelectionBottomSheet extends SelectionBottomSheet { } @Override - protected void notifyUiInitialized() { + protected void notifyUiCreated() { onSelectedItemsChanged(); - super.notifyUiInitialized(); + super.notifyUiCreated(); } private void onSelectedItemsChanged() { diff --git a/OsmAnd/src/net/osmand/plus/base/SelectionBottomSheet.java b/OsmAnd/src/net/osmand/plus/base/SelectionBottomSheet.java index f861500078..dd6dc993eb 100644 --- a/OsmAnd/src/net/osmand/plus/base/SelectionBottomSheet.java +++ b/OsmAnd/src/net/osmand/plus/base/SelectionBottomSheet.java @@ -52,7 +52,7 @@ public abstract class SelectionBottomSheet extends MenuBottomSheetDialogFragment protected int activeColorRes; protected int secondaryColorRes; - private OnUiInitializedAdapter onUiInitializedAdapter; + private DialogStateListener dialogStateListener; private OnApplySelectionListener onApplySelectionListener; protected List allItems = new ArrayList<>(); @@ -64,7 +64,7 @@ public abstract class SelectionBottomSheet extends MenuBottomSheetDialogFragment public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) { View mainView = super.onCreateView(inflater, parent, savedInstanceState); createSelectionListIfPossible(); - notifyUiInitialized(); + notifyUiCreated(); return mainView; } @@ -153,8 +153,8 @@ public abstract class SelectionBottomSheet extends MenuBottomSheetDialogFragment } } - public void setOnUiInitializedAdapter(OnUiInitializedAdapter onUiInitializedAdapter) { - this.onUiInitializedAdapter = onUiInitializedAdapter; + public void setDialogStateListener(DialogStateListener dialogStateListener) { + this.dialogStateListener = dialogStateListener; } public void setOnApplySelectionListener(OnApplySelectionListener onApplySelectionListener) { @@ -194,9 +194,9 @@ public abstract class SelectionBottomSheet extends MenuBottomSheetDialogFragment protected abstract boolean shouldShowDivider(); - protected void notifyUiInitialized() { - if (onUiInitializedAdapter != null) { - onUiInitializedAdapter.onUiInitialized(); + protected void notifyUiCreated() { + if (dialogStateListener != null) { + dialogStateListener.onDialogCreated(); } } @@ -240,8 +240,17 @@ public abstract class SelectionBottomSheet extends MenuBottomSheetDialogFragment } } - public interface OnUiInitializedAdapter { - void onUiInitialized(); + @Override + public void onDestroy() { + if (dialogStateListener != null) { + dialogStateListener.onCloseDialog(); + } + super.onDestroy(); + } + + public interface DialogStateListener { + void onDialogCreated(); + void onCloseDialog(); } public interface OnApplySelectionListener { diff --git a/OsmAnd/src/net/osmand/plus/download/DownloadResources.java b/OsmAnd/src/net/osmand/plus/download/DownloadResources.java index e2320b16a8..3966f68814 100644 --- a/OsmAnd/src/net/osmand/plus/download/DownloadResources.java +++ b/OsmAnd/src/net/osmand/plus/download/DownloadResources.java @@ -490,7 +490,7 @@ public class DownloadResources extends DownloadResourceGroup { private void replaceIndividualSrtmWithGroups(@NonNull WorldRegion region) { DownloadResourceGroup group = getRegionMapsGroup(region); if (group != null) { - boolean useMetersByDefault = SrtmDownloadItem.shouldUseMetersByDefault(app); + boolean useMetersByDefault = SrtmDownloadItem.isUseMetricByDefault(app); boolean listModified = false; DownloadActivityType srtmType = DownloadActivityType.SRTM_COUNTRY_FILE; List individualItems = group.getIndividualDownloadItems(); diff --git a/OsmAnd/src/net/osmand/plus/download/IndexItem.java b/OsmAnd/src/net/osmand/plus/download/IndexItem.java index 82da2619a3..4f12d032dc 100644 --- a/OsmAnd/src/net/osmand/plus/download/IndexItem.java +++ b/OsmAnd/src/net/osmand/plus/download/IndexItem.java @@ -233,6 +233,9 @@ public class IndexItem extends DownloadItem implements Comparable { @Nullable @Override public String getAdditionalDescription(Context ctx) { + if (getType() == DownloadActivityType.SRTM_COUNTRY_FILE) { + return SrtmDownloadItem.getAbbreviationInScopes(ctx, this); + } return null; } diff --git a/OsmAnd/src/net/osmand/plus/download/MultipleDownloadItem.java b/OsmAnd/src/net/osmand/plus/download/MultipleDownloadItem.java index d30c9fc83b..eaa3b4b92a 100644 --- a/OsmAnd/src/net/osmand/plus/download/MultipleDownloadItem.java +++ b/OsmAnd/src/net/osmand/plus/download/MultipleDownloadItem.java @@ -147,9 +147,6 @@ public class MultipleDownloadItem extends DownloadItem { @Nullable @Override public String getAdditionalDescription(Context ctx) { - for (DownloadItem item : items) { - return item.getAdditionalDescription(ctx); - } return null; } diff --git a/OsmAnd/src/net/osmand/plus/download/SelectIndexesUiHelper.java b/OsmAnd/src/net/osmand/plus/download/SelectIndexesHelper.java similarity index 60% rename from OsmAnd/src/net/osmand/plus/download/SelectIndexesUiHelper.java rename to OsmAnd/src/net/osmand/plus/download/SelectIndexesHelper.java index 845082daae..a6af99212a 100644 --- a/OsmAnd/src/net/osmand/plus/download/SelectIndexesUiHelper.java +++ b/OsmAnd/src/net/osmand/plus/download/SelectIndexesHelper.java @@ -13,7 +13,7 @@ import net.osmand.plus.base.ModeSelectionBottomSheet; import net.osmand.plus.base.MultipleSelectionWithModeBottomSheet; import net.osmand.plus.base.SelectionBottomSheet; import net.osmand.plus.base.SelectionBottomSheet.OnApplySelectionListener; -import net.osmand.plus.base.SelectionBottomSheet.OnUiInitializedAdapter; +import net.osmand.plus.base.SelectionBottomSheet.DialogStateListener; import net.osmand.plus.base.SelectionBottomSheet.SelectableItem; import net.osmand.plus.widgets.multistatetoggle.RadioItem; import net.osmand.plus.widgets.multistatetoggle.RadioItem.OnRadioItemClickListener; @@ -22,11 +22,12 @@ import net.osmand.util.Algorithms; import java.text.DateFormat; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import static net.osmand.plus.download.MultipleDownloadItem.getIndexItem; -public class SelectIndexesUiHelper { +public class SelectIndexesHelper { private final OsmandApplication app; private final AppCompatActivity activity; @@ -34,58 +35,70 @@ public class SelectIndexesUiHelper { private final ItemsToDownloadSelectedListener listener; private final DateFormat dateFormat; private final boolean showRemoteDate; + private final List itemsToDownload; private final DownloadItem downloadItem; + private final boolean useMetricByDefault; private SelectionBottomSheet dialog; - private SelectIndexesUiHelper(@NonNull DownloadItem downloadItem, - @NonNull AppCompatActivity activity, - @NonNull DateFormat dateFormat, - boolean showRemoteDate, - @NonNull ItemsToDownloadSelectedListener listener) { + private SelectIndexesHelper(@NonNull DownloadItem downloadItem, + @NonNull AppCompatActivity activity, + @NonNull DateFormat dateFormat, + boolean showRemoteDate, + @NonNull ItemsToDownloadSelectedListener listener) { this.app = (OsmandApplication) activity.getApplicationContext(); this.activity = activity; - this.downloadItem = downloadItem; this.dateFormat = dateFormat; this.showRemoteDate = showRemoteDate; this.listener = listener; + this.downloadItem = downloadItem; + this.itemsToDownload = getItemsToDownload(downloadItem); + this.useMetricByDefault = SrtmDownloadItem.isUseMetricByDefault(app); } - public static void showDialog(@NonNull DownloadItem i, + public static void showDialog(@NonNull DownloadItem di, @NonNull AppCompatActivity a, @NonNull DateFormat df, boolean showRemoteDate, @NonNull ItemsToDownloadSelectedListener l) { - new SelectIndexesUiHelper(i, a, df, showRemoteDate, l).showDialogInternal(); - } - - private void showDialogInternal() { - if (downloadItem.getType() == DownloadActivityType.SRTM_COUNTRY_FILE) { - if (downloadItem instanceof MultipleDownloadItem) { - showSrtmMultipleSelectionDialog(); + SelectIndexesHelper h = new SelectIndexesHelper(di, a, df, showRemoteDate, l); + if (di.getType() == DownloadActivityType.SRTM_COUNTRY_FILE) { + if (di instanceof MultipleDownloadItem) { + h.showSrtmMultipleSelectionDialog(); } else { - showSrtmModeSelectionDialog(); + h.showSrtmTypeSelectionDialog(); } - } else if (downloadItem instanceof MultipleDownloadItem) { - showMultipleSelectionDialog(); + } else if (di instanceof MultipleDownloadItem) { + h.showMultipleSelectionDialog(); } } private void showMultipleSelectionDialog() { + MultipleDownloadItem mdi = (MultipleDownloadItem) downloadItem; List allItems = new ArrayList<>(); List selectedItems = new ArrayList<>(); - prepareItems(allItems, selectedItems); - MultipleSelectionBottomSheet msDialog = MultipleSelectionBottomSheet.showInstance( - activity, allItems, selectedItems, true); + for (DownloadItem di : mdi.getAllItems()) { + SelectableItem si = createSelectableItem(di); + allItems.add(si); + if (itemsToDownload.contains(di)) { + selectedItems.add(si); + } + } + + MultipleSelectionBottomSheet msDialog = + MultipleSelectionBottomSheet.showInstance(activity, allItems, selectedItems, true); this.dialog = msDialog; - msDialog.setOnUiInitializedAdapter(new OnUiInitializedAdapter() { + msDialog.setDialogStateListener(new DialogStateListener() { @Override - public void onUiInitialized() { + public void onDialogCreated() { dialog.setTitle(app.getString(R.string.welmode_download_maps)); } + + @Override + public void onCloseDialog() { } }); msDialog.setSelectionUpdateListener(new SelectionUpdateListener() { @@ -99,25 +112,40 @@ public class SelectIndexesUiHelper { } private void showSrtmMultipleSelectionDialog() { + MultipleDownloadItem mdi = (MultipleDownloadItem) downloadItem; List allItems = new ArrayList<>(); List selectedItems = new ArrayList<>(); - prepareItems(allItems, selectedItems); - SrtmDownloadItem srtmItem = (SrtmDownloadItem) ((MultipleDownloadItem)downloadItem).getAllItems().get(0); - final int selectedModeOrder = srtmItem.isUseMetric() ? 0 : 1; - final List radioItems = createSrtmRadioItems(); + for (DownloadItem di : mdi.getAllItems()) { + SelectableItem si = createSrtmSelectableItem((SrtmDownloadItem) di); + allItems.add(si); + if (itemsToDownload.contains(di)) { + selectedItems.add(si); + } + } + + final RadioItem meterBtn = createSrtmRadioBtn(true); + final RadioItem feetBtn = createSrtmRadioBtn(false); + List radioItems = new ArrayList<>(); + radioItems.add(meterBtn); + radioItems.add(feetBtn); MultipleSelectionBottomSheet msDialog = MultipleSelectionWithModeBottomSheet.showInstance( activity, allItems, selectedItems, radioItems, true); this.dialog = msDialog; - msDialog.setOnUiInitializedAdapter(new OnUiInitializedAdapter() { + msDialog.setDialogStateListener(new DialogStateListener() { @Override - public void onUiInitialized() { + public void onDialogCreated() { dialog.setTitle(app.getString(R.string.welmode_download_maps)); - dialog.setSelectedMode(radioItems.get(selectedModeOrder)); + dialog.setSelectedMode(useMetricByDefault ? meterBtn : feetBtn); dialog.setSecondaryDescription(app.getString(R.string.srtm_download_list_help_message)); } + + @Override + public void onCloseDialog() { + resetUseMeters(); + } }); msDialog.setSelectionUpdateListener(new SelectionUpdateListener() { @@ -130,58 +158,47 @@ public class SelectIndexesUiHelper { msDialog.setOnApplySelectionListener(getOnApplySelectionListener(listener)); } - private void showSrtmModeSelectionDialog() { + private void showSrtmTypeSelectionDialog() { SrtmDownloadItem srtmItem = (SrtmDownloadItem) downloadItem; - final int selectedModeOrder = srtmItem.isUseMetric() ? 0 : 1; - final List radioItems = createSrtmRadioItems(); - SelectableItem preview = createSelectableItem(srtmItem); + final RadioItem meterBtn = createSrtmRadioBtn(true); + final RadioItem feetBtn = createSrtmRadioBtn(false); + List radioItems = new ArrayList<>(); + radioItems.add(meterBtn); + radioItems.add(feetBtn); + + SelectableItem preview = createSrtmSelectableItem(srtmItem); dialog = ModeSelectionBottomSheet.showInstance(activity, preview, radioItems, true); - dialog.setOnUiInitializedAdapter(new OnUiInitializedAdapter() { + dialog.setDialogStateListener(new DialogStateListener() { @Override - public void onUiInitialized() { - ModeSelectionBottomSheet dialog = (ModeSelectionBottomSheet) SelectIndexesUiHelper.this.dialog; + public void onDialogCreated() { + ModeSelectionBottomSheet dialog = (ModeSelectionBottomSheet) SelectIndexesHelper.this.dialog; dialog.setTitle(app.getString(R.string.srtm_unit_format)); dialog.setPrimaryDescription(app.getString(R.string.srtm_download_single_help_message)); updateSize(); - dialog.setSelectedMode(radioItems.get(selectedModeOrder)); + dialog.setSelectedMode(useMetricByDefault ? meterBtn : feetBtn); + } + + @Override + public void onCloseDialog() { + resetUseMeters(); } }); dialog.setOnApplySelectionListener(getOnApplySelectionListener(listener)); } - private void prepareItems(List allItems, - List selectedItems) { - final MultipleDownloadItem multipleDownloadItem = (MultipleDownloadItem) downloadItem; - final List itemsToDownload = getItemsToDownload(multipleDownloadItem); - for (DownloadItem downloadItem : multipleDownloadItem.getAllItems()) { - SelectableItem selectableItem = createSelectableItem(downloadItem); - allItems.add(selectableItem); - - if (itemsToDownload.contains(downloadItem)) { - selectedItems.add(selectableItem); - } - } - } - - private List createSrtmRadioItems() { - List radioItems = new ArrayList<>(); - radioItems.add(createSrtmRadioBtn(R.string.shared_string_meters, true)); - radioItems.add(createSrtmRadioBtn(R.string.shared_string_feet, false)); - return radioItems; - } - - private RadioItem createSrtmRadioBtn(int titleId, - final boolean useMeters) { + private RadioItem createSrtmRadioBtn(final boolean useMeters) { + int titleId = useMeters ? R.string.shared_string_meters : R.string.shared_string_feet; String title = Algorithms.capitalizeFirstLetter(app.getString(titleId)); RadioItem radioItem = new TextRadioItem(title); radioItem.setOnClickListener(new OnRadioItemClickListener() { @Override public boolean onRadioItemClick(RadioItem radioItem, View view) { - updateDialogListItems(useMeters); + setUseMetersForAllItems(useMeters); + updateListItems(); updateSize(); return true; } @@ -189,22 +206,45 @@ public class SelectIndexesUiHelper { return radioItem; } - private void updateDialogListItems(boolean useMeters) { + private SelectableItem createSelectableItem(DownloadItem item) { + SelectableItem selectableItem = new SelectableItem(); + updateSelectableItem(selectableItem, item); + selectableItem.setObject(item); + return selectableItem; + } + + private SelectableItem createSrtmSelectableItem(SrtmDownloadItem item) { + SelectableItem selectableItem = new SelectableItem(); + updateSelectableItem(selectableItem, item.getDefaultIndexItem()); + selectableItem.setObject(item); + return selectableItem; + } + + private void updateListItems() { List items = new ArrayList<>(dialog.getAllItems()); - for (SelectableItem item : items) { - DownloadItem downloadItem = (DownloadItem) item.getObject(); - if (downloadItem instanceof SrtmDownloadItem) { - ((SrtmDownloadItem) downloadItem).setUseMetric(useMeters); - updateSelectableItem(item, downloadItem); + for (SelectableItem selectableItem : items) { + DownloadItem di = (DownloadItem) selectableItem.getObject(); + if (di instanceof SrtmDownloadItem) { + di = ((SrtmDownloadItem) di).getDefaultIndexItem(); } + updateSelectableItem(selectableItem, di); } dialog.setItems(items); } - private SelectableItem createSelectableItem(DownloadItem item) { - SelectableItem selectableItem = new SelectableItem(); - updateSelectableItem(selectableItem, item); - return selectableItem; + private void resetUseMeters() { + boolean useMeters = SrtmDownloadItem.isUseMetricByDefault(app); + setUseMetersForAllItems(useMeters); + } + + private void setUseMetersForAllItems(boolean useMeters) { + for (SelectableItem item : dialog.getAllItems()) { + DownloadItem downloadItem = (DownloadItem) item.getObject(); + if (downloadItem instanceof SrtmDownloadItem) { + SrtmDownloadItem srtmItem = (SrtmDownloadItem) downloadItem; + srtmItem.setUseMetric(useMeters); + } + } } private void updateSelectableItem(SelectableItem selectableItem, @@ -221,7 +261,6 @@ public class SelectIndexesUiHelper { selectableItem.setDescription(description); selectableItem.setIconId(downloadItem.getType().getIconResource()); - selectableItem.setObject(downloadItem); } private OnApplySelectionListener getOnApplySelectionListener(final ItemsToDownloadSelectedListener listener) { @@ -257,13 +296,23 @@ public class SelectIndexesUiHelper { double totalSizeMb = 0.0d; for (SelectableItem i : selectableItems) { Object obj = i.getObject(); - if (obj instanceof DownloadItem) { + if (obj instanceof SrtmDownloadItem) { + SrtmDownloadItem srtm = (SrtmDownloadItem) obj; + totalSizeMb += srtm.getDefaultIndexItem().getSizeToDownloadInMb(); + } else if (obj instanceof DownloadItem) { totalSizeMb += ((DownloadItem) obj).getSizeToDownloadInMb(); } } return totalSizeMb; } + private static List getItemsToDownload(DownloadItem di) { + if (di instanceof MultipleDownloadItem) { + return getItemsToDownload((MultipleDownloadItem) di); + } + return Collections.emptyList(); + } + private static List getItemsToDownload(MultipleDownloadItem md) { if (md.hasActualDataToDownload()) { // download left regions diff --git a/OsmAnd/src/net/osmand/plus/download/SrtmDownloadItem.java b/OsmAnd/src/net/osmand/plus/download/SrtmDownloadItem.java index 8567f97409..217b9eb0ee 100644 --- a/OsmAnd/src/net/osmand/plus/download/SrtmDownloadItem.java +++ b/OsmAnd/src/net/osmand/plus/download/SrtmDownloadItem.java @@ -53,6 +53,11 @@ public class SrtmDownloadItem extends DownloadItem { return index; } } + return getDefaultIndexItem(); + } + + @NonNull + public IndexItem getDefaultIndexItem() { for (IndexItem index : indexes) { if (useMetric && isMetricItem(index) || !useMetric && !isMetricItem(index)) { return index; @@ -135,7 +140,7 @@ public class SrtmDownloadItem extends DownloadItem { return getAbbreviationInScopes(ctx, this); } - public static boolean shouldUseMetersByDefault(@NonNull OsmandApplication app) { + public static boolean isUseMetricByDefault(@NonNull OsmandApplication app) { MetricsConstants metricSystem = app.getSettings().METRIC_SYSTEM.get(); return metricSystem != MetricsConstants.MILES_AND_FEET; } diff --git a/OsmAnd/src/net/osmand/plus/download/ui/ItemViewHolder.java b/OsmAnd/src/net/osmand/plus/download/ui/ItemViewHolder.java index 135db84bc9..ec4347eb71 100644 --- a/OsmAnd/src/net/osmand/plus/download/ui/ItemViewHolder.java +++ b/OsmAnd/src/net/osmand/plus/download/ui/ItemViewHolder.java @@ -41,8 +41,8 @@ import net.osmand.plus.download.DownloadActivityType; import net.osmand.plus.download.DownloadResourceGroup; import net.osmand.plus.download.DownloadResources; import net.osmand.plus.download.IndexItem; -import net.osmand.plus.download.SelectIndexesUiHelper; -import net.osmand.plus.download.SelectIndexesUiHelper.ItemsToDownloadSelectedListener; +import net.osmand.plus.download.SelectIndexesHelper; +import net.osmand.plus.download.SelectIndexesHelper.ItemsToDownloadSelectedListener; import net.osmand.plus.download.MultipleDownloadItem; import net.osmand.plus.download.ui.LocalIndexesFragment.LocalIndexOperationTask; import net.osmand.plus.helpers.FileNameTranslationHelper; @@ -492,7 +492,7 @@ public class ItemViewHolder { } private void selectIndexesToDownload(DownloadItem item) { - SelectIndexesUiHelper.showDialog(item, context, dateFormat, showRemoteDate, + SelectIndexesHelper.showDialog(item, context, dateFormat, showRemoteDate, new ItemsToDownloadSelectedListener() { @Override public void onItemsToDownloadSelected(List indexes) { From 1629183994dd862336fd662c2aadada794503296 Mon Sep 17 00:00:00 2001 From: max-klaus Date: Tue, 20 Apr 2021 20:53:39 +0300 Subject: [PATCH 2/8] Finished backup test activity --- OsmAnd/res/layout/test_backup_layout.xml | 8 + .../src/net/osmand/AndroidNetworkUtils.java | 214 ++++++- .../src/net/osmand/plus/AnalyticsHelper.java | 3 +- .../src/net/osmand/plus/AppInitializer.java | 2 + .../net/osmand/plus/FavouritesDbHelper.java | 8 + OsmAnd/src/net/osmand/plus/GPXDatabase.java | 76 ++- OsmAnd/src/net/osmand/plus/GpxDbHelper.java | 6 + .../net/osmand/plus/OsmandApplication.java | 6 + .../osmand/plus/ProgressImplementation.java | 4 +- .../net/osmand/plus/backup/BackupHelper.java | 581 ++++++++++++++++++ .../net/osmand/plus/backup/BackupTask.java | 335 ++++++++++ .../net/osmand/plus/backup/GpxFileInfo.java | 65 ++ .../osmand/plus/backup/PrepareBackupTask.java | 211 +++++++ .../src/net/osmand/plus/backup/UserFile.java | 102 +++ .../backup/UserNotRegisteredException.java | 9 + .../plus/development/TestBackupActivity.java | 575 +++++------------ .../plus/importfiles/FavoritesImportTask.java | 12 +- .../plus/inapp/InAppPurchaseHelper.java | 11 +- .../plus/settings/backend/OsmandSettings.java | 3 + 19 files changed, 1736 insertions(+), 495 deletions(-) create mode 100644 OsmAnd/src/net/osmand/plus/backup/BackupHelper.java create mode 100644 OsmAnd/src/net/osmand/plus/backup/BackupTask.java create mode 100644 OsmAnd/src/net/osmand/plus/backup/GpxFileInfo.java create mode 100644 OsmAnd/src/net/osmand/plus/backup/PrepareBackupTask.java create mode 100644 OsmAnd/src/net/osmand/plus/backup/UserFile.java create mode 100644 OsmAnd/src/net/osmand/plus/backup/UserNotRegisteredException.java diff --git a/OsmAnd/res/layout/test_backup_layout.xml b/OsmAnd/res/layout/test_backup_layout.xml index 216820a9bf..d24c50eddc 100644 --- a/OsmAnd/res/layout/test_backup_layout.xml +++ b/OsmAnd/res/layout/test_backup_layout.xml @@ -42,6 +42,7 @@ @@ -121,6 +122,13 @@ android:textColor="?android:textColorPrimary" android:textSize="@dimen/default_list_text_size" /> + + errors); } + public interface OnFilesDownloadCallback { + @Nullable + Map getAdditionalParams(@NonNull File file); + void onFileDownloadProgress(@NonNull File file, int percent); + @WorkerThread + void onFileDownloadedAsync(@NonNull File file); + void onFilesDownloadDone(@NonNull Map errors); + } + public static class RequestResponse { private Request request; private String response; @@ -74,35 +84,46 @@ public class AndroidNetworkUtils { } } - public interface OnRequestsResultListener { - void onResult(@NonNull List results); + public interface OnSendRequestsListener { + void onRequestSent(@NonNull RequestResponse response); + void onRequestsSent(@NonNull List results); } - public static void sendRequestsAsync(final OsmandApplication ctx, - final List requests, - final OnRequestsResultListener listener) { + public static void sendRequestsAsync(@Nullable final OsmandApplication ctx, + @NonNull final List requests, + @Nullable final OnSendRequestsListener listener) { - new AsyncTask>() { + new AsyncTask>() { @Override protected List doInBackground(Void... params) { List responses = new ArrayList<>(); for (Request request : requests) { + RequestResponse requestResponse; try { String response = sendRequest(ctx, request.getUrl(), request.getParameters(), request.getUserOperation(), request.isToastAllowed(), request.isPost()); - responses.add(new RequestResponse(request, response)); + requestResponse = new RequestResponse(request, response); } catch (Exception e) { - responses.add(new RequestResponse(request, null)); + requestResponse = new RequestResponse(request, null); } + responses.add(requestResponse); + publishProgress(requestResponse); } return responses; } + @Override + protected void onProgressUpdate(RequestResponse... values) { + if (listener != null) { + listener.onRequestSent(values[0]); + } + } + @Override protected void onPostExecute(@NonNull List results) { if (listener != null) { - listener.onResult(results); + listener.onRequestsSent(results); } } @@ -146,7 +167,7 @@ public class AndroidNetworkUtils { @Override protected String doInBackground(Void... params) { - return downloadFile(url, fileToSave); + return downloadFile(url, fileToSave, false, null); } @Override @@ -158,8 +179,80 @@ public class AndroidNetworkUtils { }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void) null); } - public static String sendRequest(OsmandApplication ctx, String url, Map parameters, - String userOperation, boolean toastAllowed, boolean post) { + public static void downloadFilesAsync(final @NonNull String url, + final @NonNull List files, + final @NonNull Map parameters, + final @Nullable OnFilesDownloadCallback callback) { + + new AsyncTask>() { + + @Override + @NonNull + protected Map doInBackground(Void... v) { + Map errors = new HashMap<>(); + for (final File file : files) { + final int[] progressValue = {0}; + publishProgress(file, 0); + IProgress progress = null; + if (callback != null) { + progress = new NetworkProgress() { + @Override + public void progress(int deltaWork) { + progressValue[0] += deltaWork; + publishProgress(file, progressValue[0]); + } + }; + } + try { + Map params = new HashMap<>(parameters); + if (callback != null) { + Map additionalParams = callback.getAdditionalParams(file); + if (additionalParams != null) { + params.putAll(additionalParams); + } + } + boolean firstPrm = !url.contains("?"); + StringBuilder sb = new StringBuilder(url); + for (Entry entry : params.entrySet()) { + sb.append(firstPrm ? "?" : "&").append(entry.getKey()).append("=").append(URLEncoder.encode(entry.getValue(), "UTF-8")); + firstPrm = false; + } + String res = downloadFile(sb.toString(), file, true, progress); + if (res != null) { + errors.put(file, res); + } else { + if (callback != null) { + callback.onFileDownloadedAsync(file); + } + } + } catch (Exception e) { + errors.put(file, e.getMessage()); + } + publishProgress(file, Integer.MAX_VALUE); + } + return errors; + } + + @Override + protected void onProgressUpdate(Object... objects) { + if (callback != null) { + callback.onFileDownloadProgress((File) objects[0], (Integer) objects[1]); + } + } + + @Override + protected void onPostExecute(@NonNull Map errors) { + if (callback != null) { + callback.onFilesDownloadDone(errors); + } + } + + }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void) null); + } + + public static String sendRequest(@Nullable OsmandApplication ctx, @NonNull String url, + @Nullable Map parameters, + @Nullable String userOperation, boolean toastAllowed, boolean post) { HttpURLConnection connection = null; try { @@ -177,7 +270,7 @@ public class AndroidNetworkUtils { String paramsSeparator = url.indexOf('?') == -1 ? "?" : "&"; connection = NetworkUtils.getHttpURLConnection(params == null || post ? url : url + paramsSeparator + params); connection.setRequestProperty("Accept-Charset", "UTF-8"); - connection.setRequestProperty("User-Agent", Version.getFullVersion(ctx)); + connection.setRequestProperty("User-Agent", ctx != null ? Version.getFullVersion(ctx) : "OsmAnd"); connection.setConnectTimeout(15000); if (params != null && post) { connection.setDoInput(true); @@ -200,9 +293,10 @@ public class AndroidNetworkUtils { } if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) { - if (toastAllowed) { - String msg = userOperation - + " " + ctx.getString(R.string.failed_op) + ": " + connection.getResponseMessage(); + if (toastAllowed && ctx != null) { + String msg = (!Algorithms.isEmpty(userOperation) ? userOperation + " " : "") + + ctx.getString(R.string.failed_op) + ": " + + connection.getResponseMessage(); showToast(ctx, msg); } } else { @@ -233,17 +327,17 @@ public class AndroidNetworkUtils { } catch (NullPointerException e) { // that's tricky case why NPE is thrown to fix that problem httpClient could be used - if (toastAllowed) { + if (toastAllowed && ctx != null) { String msg = ctx.getString(R.string.auth_failed); showToast(ctx, msg); } } catch (MalformedURLException e) { - if (toastAllowed) { + if (toastAllowed && ctx != null) { showToast(ctx, MessageFormat.format(ctx.getResources().getString(R.string.shared_string_action_template) + ": " + ctx.getResources().getString(R.string.shared_string_unexpected_error), userOperation)); } } catch (IOException e) { - if (toastAllowed) { + if (toastAllowed && ctx != null) { showToast(ctx, MessageFormat.format(ctx.getResources().getString(R.string.shared_string_action_template) + ": " + ctx.getResources().getString(R.string.shared_string_io_error), userOperation)); } @@ -277,18 +371,23 @@ public class AndroidNetworkUtils { return res; } - public static String downloadFile(@NonNull String url, @NonNull File fileToSave) { + public static String downloadFile(@NonNull String url, @NonNull File fileToSave, boolean gzip, @Nullable IProgress progress) { String error = null; try { URLConnection connection = NetworkUtils.getHttpURLConnection(url); connection.setConnectTimeout(CONNECTION_TIMEOUT); connection.setReadTimeout(CONNECTION_TIMEOUT); - BufferedInputStream inputStream = new BufferedInputStream(connection.getInputStream(), 8 * 1024); + if (gzip) { + connection.setRequestProperty("Accept-Encoding", "deflate, gzip"); + } + InputStream inputStream = gzip + ? new GZIPInputStream(connection.getInputStream()) + : new BufferedInputStream(connection.getInputStream(), 8 * 1024); fileToSave.getParentFile().mkdirs(); OutputStream stream = null; try { stream = new FileOutputStream(fileToSave); - Algorithms.streamCopy(inputStream, stream); + Algorithms.streamCopy(inputStream, stream, progress, 1024); stream.flush(); } finally { Algorithms.closeStream(inputStream); @@ -307,12 +406,17 @@ public class AndroidNetworkUtils { private static final String BOUNDARY = "CowMooCowMooCowCowCow"; public static String uploadFile(@NonNull String urlText, @NonNull File file, boolean gzip, - @NonNull Map additionalParams, @Nullable Map headers) throws IOException { - return uploadFile(urlText, new FileInputStream(file), file.getName(), gzip, additionalParams, headers); + @NonNull Map additionalParams, + @Nullable Map headers, + @Nullable IProgress progress) throws IOException { + return uploadFile(urlText, new FileInputStream(file), file.getName(), gzip, additionalParams, headers, progress); } - public static String uploadFile(@NonNull String urlText, @NonNull InputStream inputStream, @NonNull String fileName, boolean gzip, - Map additionalParams, @Nullable Map headers) { + public static String uploadFile(@NonNull String urlText, @NonNull InputStream inputStream, + @NonNull String fileName, boolean gzip, + @NonNull Map additionalParams, + @Nullable Map headers, + @Nullable IProgress progress) { URL url; try { boolean firstPrm = !urlText.contains("?"); @@ -350,11 +454,11 @@ public class AndroidNetworkUtils { ous.flush(); if (gzip) { GZIPOutputStream gous = new GZIPOutputStream(ous, 1024); - Algorithms.streamCopy(bis, gous); + Algorithms.streamCopy(bis, gous, progress, 1024); gous.flush(); gous.finish(); } else { - Algorithms.streamCopy(bis, ous); + Algorithms.streamCopy(bis, ous, progress, 1024); } ous.write(("\r\n--" + BOUNDARY + "--\r\n").getBytes()); @@ -406,8 +510,19 @@ public class AndroidNetworkUtils { @NonNull protected Map doInBackground(Void... v) { Map errors = new HashMap<>(); - for (File file : files) { + for (final File file : files) { + final int[] progressValue = {0}; publishProgress(file, 0); + IProgress progress = null; + if (callback != null) { + progress = new NetworkProgress() { + @Override + public void progress(int deltaWork) { + progressValue[0] += deltaWork; + publishProgress(file, progressValue[0]); + } + }; + } try { Map params = new HashMap<>(parameters); if (callback != null) { @@ -416,14 +531,14 @@ public class AndroidNetworkUtils { params.putAll(additionalParams); } } - String res = uploadFile(url, file, gzip, params, headers); + String res = uploadFile(url, file, gzip, params, headers, progress); if (res != null) { errors.put(file, res); } } catch (Exception e) { errors.put(file, e.getMessage()); } - publishProgress(file, 100); + publishProgress(file, Integer.MAX_VALUE); } return errors; } @@ -484,4 +599,39 @@ public class AndroidNetworkUtils { return post; } } + + private abstract static class NetworkProgress implements IProgress { + @Override + public void startTask(String taskName, int work) { + } + + @Override + public void startWork(int work) { + } + + @Override + public abstract void progress(int deltaWork); + + @Override + public void remaining(int remainingWork) { + } + + @Override + public void finishTask() { + } + + @Override + public boolean isIndeterminate() { + return false; + } + + @Override + public boolean isInterrupted() { + return false; + } + + @Override + public void setGeneralProgress(String genProgress) { + } + } } diff --git a/OsmAnd/src/net/osmand/plus/AnalyticsHelper.java b/OsmAnd/src/net/osmand/plus/AnalyticsHelper.java index e8d04254c3..52dff8e6d0 100644 --- a/OsmAnd/src/net/osmand/plus/AnalyticsHelper.java +++ b/OsmAnd/src/net/osmand/plus/AnalyticsHelper.java @@ -184,7 +184,8 @@ public class AnalyticsHelper extends SQLiteOpenHelper { String jsonStr = json.toString(); InputStream inputStream = new ByteArrayInputStream(jsonStr.getBytes()); - String res = AndroidNetworkUtils.uploadFile(ANALYTICS_UPLOAD_URL, inputStream, ANALYTICS_FILE_NAME, true, additionalData, null); + String res = AndroidNetworkUtils.uploadFile(ANALYTICS_UPLOAD_URL, inputStream, + ANALYTICS_FILE_NAME, true, additionalData, null, null); if (res != null) { return; } diff --git a/OsmAnd/src/net/osmand/plus/AppInitializer.java b/OsmAnd/src/net/osmand/plus/AppInitializer.java index af3ea4f475..d5afd31937 100644 --- a/OsmAnd/src/net/osmand/plus/AppInitializer.java +++ b/OsmAnd/src/net/osmand/plus/AppInitializer.java @@ -31,6 +31,7 @@ import net.osmand.osm.MapPoiTypes; import net.osmand.plus.activities.LocalIndexHelper; import net.osmand.plus.activities.LocalIndexInfo; import net.osmand.plus.activities.SavingTrackHelper; +import net.osmand.plus.backup.BackupHelper; import net.osmand.plus.base.MapViewTrackingUtilities; import net.osmand.plus.download.DownloadActivity; import net.osmand.plus.download.ui.AbstractLoadLocalIndexTask; @@ -473,6 +474,7 @@ public class AppInitializer implements IProgress { app.oprAuthHelper = startupInit(new OprAuthHelper(app), OprAuthHelper.class); app.onlineRoutingHelper = startupInit(new OnlineRoutingHelper(app), OnlineRoutingHelper.class); app.itineraryHelper = startupInit(new ItineraryHelper(app), ItineraryHelper.class); + app.backupHelper = startupInit(new BackupHelper(app), BackupHelper.class); initOpeningHoursParser(); } diff --git a/OsmAnd/src/net/osmand/plus/FavouritesDbHelper.java b/OsmAnd/src/net/osmand/plus/FavouritesDbHelper.java index c90ecccc02..be36c5197d 100644 --- a/OsmAnd/src/net/osmand/plus/FavouritesDbHelper.java +++ b/OsmAnd/src/net/osmand/plus/FavouritesDbHelper.java @@ -146,6 +146,14 @@ public class FavouritesDbHelper { } } + public long getLastUploadedTime() { + return context.getSettings().FAVORITES_LAST_UPLOADED_TIME.get(); + } + + public void setLastUploadedTime(long time) { + context.getSettings().FAVORITES_LAST_UPLOADED_TIME.set(time); + } + @Nullable public Drawable getColoredIconForGroup(String groupName) { String groupIdName = FavoriteGroup.convertDisplayNameToGroupIdName(context, groupName); diff --git a/OsmAnd/src/net/osmand/plus/GPXDatabase.java b/OsmAnd/src/net/osmand/plus/GPXDatabase.java index 385ec79484..f1e5f64099 100644 --- a/OsmAnd/src/net/osmand/plus/GPXDatabase.java +++ b/OsmAnd/src/net/osmand/plus/GPXDatabase.java @@ -18,7 +18,7 @@ import androidx.annotation.Nullable; public class GPXDatabase { - private static final int DB_VERSION = 11; + private static final int DB_VERSION = 12; private static final String DB_NAME = "gpx_database"; private static final String GPX_TABLE_NAME = "gpxTable"; @@ -48,6 +48,7 @@ public class GPXDatabase { private static final String GPX_COL_COLOR = "color"; private static final String GPX_COL_FILE_LAST_MODIFIED_TIME = "fileLastModifiedTime"; + private static final String GPX_COL_FILE_LAST_UPLOADED_TIME = "fileLastUploadedTime"; private static final String GPX_COL_SPLIT_TYPE = "splitType"; private static final String GPX_COL_SPLIT_INTERVAL = "splitInterval"; @@ -98,6 +99,7 @@ public class GPXDatabase { GPX_COL_WPT_POINTS + " int, " + GPX_COL_COLOR + " TEXT, " + GPX_COL_FILE_LAST_MODIFIED_TIME + " long, " + + GPX_COL_FILE_LAST_UPLOADED_TIME + " long, " + GPX_COL_SPLIT_TYPE + " int, " + GPX_COL_SPLIT_INTERVAL + " double, " + GPX_COL_API_IMPORTED + " int, " + // 1 = true, 0 = false @@ -133,6 +135,7 @@ public class GPXDatabase { GPX_COL_WPT_POINTS + ", " + GPX_COL_COLOR + ", " + GPX_COL_FILE_LAST_MODIFIED_TIME + ", " + + GPX_COL_FILE_LAST_UPLOADED_TIME + ", " + GPX_COL_SPLIT_TYPE + ", " + GPX_COL_SPLIT_INTERVAL + ", " + GPX_COL_API_IMPORTED + ", " + @@ -184,6 +187,7 @@ public class GPXDatabase { private int splitType; private double splitInterval; private long fileLastModifiedTime; + private long fileLastUploadedTime; private boolean apiImported; private boolean showAsMarkers; private boolean joinSegments; @@ -200,6 +204,11 @@ public class GPXDatabase { this.color = color; } + public GpxDataItem(File file, long fileLastUploadedTime) { + this.file = file; + this.fileLastUploadedTime = fileLastUploadedTime; + } + public GpxDataItem(File file, @NonNull GPXFile gpxFile) { this.file = file; readGpxParams(gpxFile); @@ -263,6 +272,10 @@ public class GPXDatabase { return fileLastModifiedTime; } + public long getFileLastUploadedTime() { + return fileLastUploadedTime; + } + public int getSplitType() { return splitType; } @@ -441,10 +454,13 @@ public class GPXDatabase { db.execSQL("UPDATE " + GPX_TABLE_NAME + " SET " + GPX_COL_SHOW_START_FINISH + " = ? " + "WHERE " + GPX_COL_SHOW_START_FINISH + " IS NULL", new Object[]{1}); } + if (oldVersion < 12) { + db.execSQL("ALTER TABLE " + GPX_TABLE_NAME + " ADD " + GPX_COL_FILE_LAST_UPLOADED_TIME + " long"); + } db.execSQL("CREATE INDEX IF NOT EXISTS " + GPX_INDEX_NAME_DIR + " ON " + GPX_TABLE_NAME + " (" + GPX_COL_NAME + ", " + GPX_COL_DIR + ");"); } - private boolean updateLastModifiedTime(GpxDataItem item) { + private boolean updateLastModifiedTime(@NonNull GpxDataItem item) { SQLiteConnection db = openConnection(false); if (db != null) { try { @@ -464,6 +480,25 @@ public class GPXDatabase { return false; } + public boolean updateLastUploadedTime(@NonNull GpxDataItem item, long fileLastUploadedTime) { + SQLiteConnection db = openConnection(false); + if (db != null) { + try { + String fileName = getFileName(item.file); + String fileDir = getFileDir(item.file); + db.execSQL("UPDATE " + GPX_TABLE_NAME + " SET " + + GPX_COL_FILE_LAST_UPLOADED_TIME + " = ? " + + " WHERE " + GPX_COL_NAME + " = ? AND " + GPX_COL_DIR + " = ?", + new Object[] { fileLastUploadedTime, fileName, fileDir }); + item.fileLastUploadedTime = fileLastUploadedTime; + } finally { + db.close(); + } + return true; + } + return false; + } + public boolean rename(@Nullable GpxDataItem item, File currentFile, File newFile) { SQLiteConnection db = openConnection(false); if (db != null){ @@ -721,11 +756,11 @@ public class GPXDatabase { String gradientScaleType = item.gradientScaleType != null ? item.gradientScaleType.getTypeName() : null; if (a != null) { db.execSQL( - "INSERT INTO " + GPX_TABLE_NAME + " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + "INSERT INTO " + GPX_TABLE_NAME + " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", new Object[] {fileName, fileDir, a.totalDistance, a.totalTracks, a.startTime, a.endTime, a.timeSpan, a.timeMoving, a.totalDistanceMoving, a.diffElevationUp, a.diffElevationDown, a.avgElevation, a.minElevation, a.maxElevation, a.maxSpeed, a.avgSpeed, a.points, a.wptPoints, - color, item.file.lastModified(), item.splitType, item.splitInterval, item.apiImported ? 1 : 0, + color, item.file.lastModified(), item.fileLastUploadedTime, item.splitType, item.splitInterval, item.apiImported ? 1 : 0, Algorithms.encodeStringSet(item.analysis.wptCategoryNames), item.showAsMarkers ? 1 : 0, item.joinSegments ? 1 : 0, item.showArrows ? 1 : 0, item.showStartFinish ? 1 : 0, item.width, item.gradientSpeedPalette, item.gradientAltitudePalette, item.gradientSlopePalette, gradientScaleType}); @@ -735,6 +770,7 @@ public class GPXDatabase { GPX_COL_DIR + ", " + GPX_COL_COLOR + ", " + GPX_COL_FILE_LAST_MODIFIED_TIME + ", " + + GPX_COL_FILE_LAST_UPLOADED_TIME + ", " + GPX_COL_SPLIT_TYPE + ", " + GPX_COL_SPLIT_INTERVAL + ", " + GPX_COL_API_IMPORTED + ", " + @@ -747,8 +783,8 @@ public class GPXDatabase { GPX_COL_GRADIENT_ALTITUDE_COLOR + ", " + GPX_COL_GRADIENT_SLOPE_COLOR + ", " + GPX_COL_GRADIENT_SCALE_TYPE + - ") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", - new Object[] {fileName, fileDir, color, 0, item.splitType, item.splitInterval, + ") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + new Object[] {fileName, fileDir, color, 0, item.fileLastUploadedTime, item.splitType, item.splitInterval, item.apiImported ? 1 : 0, item.showAsMarkers ? 1 : 0, item.joinSegments ? 1 : 0, item.showArrows ? 1 : 0, item.showStartFinish ? 1 : 0, item.width, Algorithms.gradientPaletteToString(item.gradientSpeedPalette), @@ -828,19 +864,20 @@ public class GPXDatabase { int wptPoints = (int)query.getInt(17); String color = query.getString(18); long fileLastModifiedTime = query.getLong(19); - int splitType = (int)query.getInt(20); - double splitInterval = query.getDouble(21); - boolean apiImported = query.getInt(22) == 1; - String wptCategoryNames = query.getString(23); - boolean showAsMarkers = query.getInt(24) == 1; - boolean joinSegments = query.getInt(25) == 1; - boolean showArrows = query.getInt(26) == 1; - boolean showStartFinish = query.getInt(27) == 1; - String width = query.getString(28); - String gradientSpeedPalette = query.getString(29); - String gradientAltitudePalette = query.getString(30); - String gradientSlopePalette = query.getString(31); - String gradientScaleType = query.getString(32); + long fileLastUploadedTime = query.getLong(20); + int splitType = (int)query.getInt(21); + double splitInterval = query.getDouble(22); + boolean apiImported = query.getInt(23) == 1; + String wptCategoryNames = query.getString(24); + boolean showAsMarkers = query.getInt(25) == 1; + boolean joinSegments = query.getInt(26) == 1; + boolean showArrows = query.getInt(27) == 1; + boolean showStartFinish = query.getInt(28) == 1; + String width = query.getString(29); + String gradientSpeedPalette = query.getString(30); + String gradientAltitudePalette = query.getString(31); + String gradientSlopePalette = query.getString(32); + String gradientScaleType = query.getString(33); GPXTrackAnalysis a = new GPXTrackAnalysis(); a.totalDistance = totalDistance; @@ -873,6 +910,7 @@ public class GPXDatabase { GpxDataItem item = new GpxDataItem(new File(dir, fileName), a); item.color = parseColor(color); item.fileLastModifiedTime = fileLastModifiedTime; + item.fileLastUploadedTime = fileLastUploadedTime; item.splitType = splitType; item.splitInterval = splitInterval; item.apiImported = apiImported; diff --git a/OsmAnd/src/net/osmand/plus/GpxDbHelper.java b/OsmAnd/src/net/osmand/plus/GpxDbHelper.java index 21154d5819..6bfc639f70 100644 --- a/OsmAnd/src/net/osmand/plus/GpxDbHelper.java +++ b/OsmAnd/src/net/osmand/plus/GpxDbHelper.java @@ -78,6 +78,12 @@ public class GpxDbHelper { return res; } + public boolean updateLastUploadedTime(GpxDataItem item, long fileLastUploadedTime) { + boolean res = db.updateLastUploadedTime(item, fileLastUploadedTime); + putToCache(item); + return res; + } + public boolean updateGradientScalePalette(@NonNull GpxDataItem item, @NonNull GradientScaleType gradientScaleType, int[] palette) { boolean res = db.updateGradientScaleColor(item, gradientScaleType, palette); putToCache(item); diff --git a/OsmAnd/src/net/osmand/plus/OsmandApplication.java b/OsmAnd/src/net/osmand/plus/OsmandApplication.java index 72e4266f35..ddbbd7087e 100644 --- a/OsmAnd/src/net/osmand/plus/OsmandApplication.java +++ b/OsmAnd/src/net/osmand/plus/OsmandApplication.java @@ -52,6 +52,7 @@ import net.osmand.plus.activities.SavingTrackHelper; import net.osmand.plus.activities.actions.OsmAndDialogs; import net.osmand.plus.api.SQLiteAPI; import net.osmand.plus.api.SQLiteAPIImpl; +import net.osmand.plus.backup.BackupHelper; import net.osmand.plus.base.MapViewTrackingUtilities; import net.osmand.plus.download.DownloadIndexesThread; import net.osmand.plus.download.DownloadService; @@ -169,6 +170,7 @@ public class OsmandApplication extends MultiDexApplication { MeasurementEditingContext measurementEditingContext; OnlineRoutingHelper onlineRoutingHelper; ItineraryHelper itineraryHelper; + BackupHelper backupHelper; private Map customRoutingConfigs = new ConcurrentHashMap<>(); private File externalStorageDirectory; @@ -474,6 +476,10 @@ public class OsmandApplication extends MultiDexApplication { return itineraryHelper; } + public BackupHelper getBackupHelper() { + return backupHelper; + } + public TransportRoutingHelper getTransportRoutingHelper() { return transportRoutingHelper; } diff --git a/OsmAnd/src/net/osmand/plus/ProgressImplementation.java b/OsmAnd/src/net/osmand/plus/ProgressImplementation.java index 03d2114c7b..9e673bacdc 100644 --- a/OsmAnd/src/net/osmand/plus/ProgressImplementation.java +++ b/OsmAnd/src/net/osmand/plus/ProgressImplementation.java @@ -5,6 +5,7 @@ import android.app.ProgressDialog; import android.content.Context; import android.content.DialogInterface; import android.content.DialogInterface.OnCancelListener; +import android.content.res.Resources; import android.os.Handler; import android.os.Message; import android.widget.ProgressBar; @@ -204,7 +205,8 @@ public class ProgressImplementation implements IProgress { work = -1; progress = 0; if (taskName != null) { - message = context.getResources().getString(R.string.finished_task) +" : "+ taskName; //$NON-NLS-1$ + Resources resources = context.getResources(); + message = resources.getString(R.string.ltr_or_rtl_combine_via_colon, resources.getString(R.string.finished_task), taskName); mViewUpdateHandler.sendEmptyMessage(HANDLER_START_TASK); } } diff --git a/OsmAnd/src/net/osmand/plus/backup/BackupHelper.java b/OsmAnd/src/net/osmand/plus/backup/BackupHelper.java new file mode 100644 index 0000000000..be81017d42 --- /dev/null +++ b/OsmAnd/src/net/osmand/plus/backup/BackupHelper.java @@ -0,0 +1,581 @@ +package net.osmand.plus.backup; + +import android.annotation.SuppressLint; +import android.os.AsyncTask; +import android.provider.Settings; +import android.util.Pair; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.WorkerThread; + +import net.osmand.AndroidNetworkUtils; +import net.osmand.AndroidNetworkUtils.OnFilesDownloadCallback; +import net.osmand.AndroidNetworkUtils.OnFilesUploadCallback; +import net.osmand.AndroidNetworkUtils.OnRequestResultListener; +import net.osmand.AndroidNetworkUtils.OnSendRequestsListener; +import net.osmand.AndroidNetworkUtils.Request; +import net.osmand.AndroidNetworkUtils.RequestResponse; +import net.osmand.AndroidUtils; +import net.osmand.IndexConstants; +import net.osmand.plus.FavouritesDbHelper; +import net.osmand.plus.GPXDatabase.GpxDataItem; +import net.osmand.plus.GpxDbHelper; +import net.osmand.plus.OsmandApplication; +import net.osmand.plus.inapp.InAppPurchaseHelper; +import net.osmand.plus.inapp.InAppPurchases.InAppSubscription; +import net.osmand.plus.settings.backend.OsmandSettings; +import net.osmand.util.Algorithms; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.File; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class BackupHelper { + + private final OsmandApplication app; + private final OsmandSettings settings; + private final FavouritesDbHelper favouritesHelper; + private final GpxDbHelper gpxHelper; + + private static final String SERVER_URL = "https://osmand.net"; + + private static final String USER_REGISTER_URL = SERVER_URL + "/userdata/user-register"; + private static final String DEVICE_REGISTER_URL = SERVER_URL + "/userdata/device-register"; + private static final String UPLOAD_FILE_URL = SERVER_URL + "/userdata/upload-file"; + private static final String LIST_FILES_URL = SERVER_URL + "/userdata/list-files"; + private static final String DOWNLOAD_FILE_URL = SERVER_URL + "/userdata/download-file"; + private static final String DELETE_FILE_URL = SERVER_URL + "/userdata/delete-file"; + + public final static int STATUS_SUCCESS = 0; + public final static int STATUS_PARSE_JSON_ERROR = 1; + public final static int STATUS_EMPTY_RESPONSE_ERROR = 2; + public final static int STATUS_SERVER_ERROR = 3; + + public interface OnResultListener { + void onResult(int status, @Nullable String message, @Nullable JSONObject json); + } + + public interface OnRegisterUserListener { + void onRegisterUser(int status, @Nullable String message); + } + + public interface OnRegisterDeviceListener { + void onRegisterDevice(int status, @Nullable String message); + } + + public interface OnDownloadFileListListener { + void onDownloadFileList(int status, @Nullable String message, @NonNull List userFiles); + } + + public interface OnCollectLocalFilesListener { + void onFileCollected(@NonNull GpxFileInfo fileInfo); + + void onFilesCollected(@NonNull List fileInfos); + } + + public interface OnGenerateBackupInfoListener { + void onBackupInfoGenerated(@Nullable BackupInfo backupInfo, @Nullable String error); + } + + public interface OnUploadFilesListener { + void onFileUploadProgress(@NonNull File file, int progress); + + void onFilesUploadDone(@NonNull Map errors); + } + + public interface OnDeleteFilesListener { + void onFileDeleteProgress(@NonNull UserFile file); + + void onFilesDeleteDone(@NonNull Map errors); + } + + public interface OnDownloadFileListener { + void onFileDownloadProgress(@NonNull UserFile userFile, int progress); + @WorkerThread + void onFileDownloadedAsync(@NonNull File file); + void onFilesDownloadDone(@NonNull Map errors); + } + + public static class BackupInfo { + public List filesToDownload = new ArrayList<>(); + public List filesToUpload = new ArrayList<>(); + public List filesToDelete = new ArrayList<>(); + public List> filesToMerge = new ArrayList<>(); + } + + public BackupHelper(@NonNull OsmandApplication app) { + this.app = app; + this.settings = app.getSettings(); + this.favouritesHelper = app.getFavorites(); + this.gpxHelper = app.getGpxDbHelper(); + } + + @SuppressLint("HardwareIds") + private String getAndroidId() { + try { + return Settings.Secure.getString(app.getContentResolver(), Settings.Secure.ANDROID_ID); + } catch (Exception e) { + return null; + } + } + + public static boolean isTokenValid(@NonNull String token) { + return token.matches("[0-9]+"); + } + + public boolean hasOsmLiveUpdates() { + return InAppPurchaseHelper.isSubscribedToLiveUpdates(app); + } + + @Nullable + public String getOrderId() { + InAppPurchaseHelper purchaseHelper = app.getInAppPurchaseHelper(); + InAppSubscription purchasedSubscription = purchaseHelper.getAnyPurchasedSubscription(); + return purchasedSubscription != null ? purchasedSubscription.getOrderId() : null; + } + + public String getDeviceId() { + return settings.BACKUP_DEVICE_ID.get(); + } + + public String getAccessToken() { + return settings.BACKUP_ACCESS_TOKEN.get(); + } + + public String getEmail() { + return settings.BACKUP_USER_EMAIL.get(); + } + + public boolean isRegistered() { + return !Algorithms.isEmpty(getDeviceId()) && !Algorithms.isEmpty(getAccessToken()); + } + + private void checkRegistered() throws UserNotRegisteredException { + if (Algorithms.isEmpty(getDeviceId()) || Algorithms.isEmpty(getAccessToken())) { + throw new UserNotRegisteredException(); + } + } + + public void registerUser(@NonNull String email, @Nullable final OnRegisterUserListener listener) { + Map params = new HashMap<>(); + params.put("email", email); + params.put("orderid", getOrderId()); + params.put("deviceid", app.getUserAndroidId()); + AndroidNetworkUtils.sendRequestAsync(app, USER_REGISTER_URL, params, "Register user", true, true, new OnRequestResultListener() { + @Override + public void onResult(String resultJson) { + int status; + String message; + if (!Algorithms.isEmpty(resultJson)) { + try { + JSONObject result = new JSONObject(resultJson); + String statusStr = result.getString("status"); + if (statusStr.equals("ok")) { + message = "You have been registered successfully. Please check for email with activation code."; + status = STATUS_SUCCESS; + } else { + message = "User registration error: " + statusStr; + status = STATUS_SERVER_ERROR; + } + } catch (JSONException e) { + message = "User registration error: json parsing"; + status = STATUS_PARSE_JSON_ERROR; + } + } else { + message = "User registration error: empty response"; + status = STATUS_EMPTY_RESPONSE_ERROR; + } + if (listener != null) { + listener.onRegisterUser(status, message); + } + } + }); + } + + public void registerDevice(String token, @Nullable final OnRegisterDeviceListener listener) { + Map params = new HashMap<>(); + params.put("email", getEmail()); + String orderId = getOrderId(); + if (orderId != null) { + params.put("orderid", orderId); + } + String androidId = getAndroidId(); + if (!Algorithms.isEmpty(androidId)) { + params.put("deviceid", androidId); + } + params.put("token", token); + AndroidNetworkUtils.sendRequestAsync(app, DEVICE_REGISTER_URL, params, "Register device", true, true, new OnRequestResultListener() { + @Override + public void onResult(String resultJson) { + int status; + String message; + if (!Algorithms.isEmpty(resultJson)) { + try { + JSONObject result = new JSONObject(resultJson); + settings.BACKUP_DEVICE_ID.set(result.getString("id")); + settings.BACKUP_USER_ID.set(result.getString("userid")); + settings.BACKUP_NATIVE_DEVICE_ID.set(result.getString("deviceid")); + settings.BACKUP_ACCESS_TOKEN.set(result.getString("accesstoken")); + settings.BACKUP_ACCESS_TOKEN_UPDATE_TIME.set(result.getString("udpatetime")); + status = STATUS_SUCCESS; + message = "Device have been registered successfully"; + } catch (JSONException e) { + message = "Device registration error: json parsing"; + status = STATUS_PARSE_JSON_ERROR; + } + } else { + message = "Device registration error: empty response"; + status = STATUS_EMPTY_RESPONSE_ERROR; + } + if (listener != null) { + listener.onRegisterDevice(status, message); + } + } + }); + } + + public void uploadFiles(@NonNull List gpxFiles, @Nullable final OnUploadFilesListener listener) throws UserNotRegisteredException { + checkRegistered(); + + Map params = new HashMap<>(); + params.put("deviceid", getDeviceId()); + params.put("accessToken", getAccessToken()); + Map headers = new HashMap<>(); + headers.put("Accept-Encoding", "deflate, gzip"); + + final Map gpxInfos = new HashMap<>(); + for (GpxFileInfo gpxFile : gpxFiles) { + gpxInfos.put(gpxFile.file, gpxFile); + } + final File favoritesFile = favouritesHelper.getExternalFile(); + AndroidNetworkUtils.uploadFilesAsync(UPLOAD_FILE_URL, new ArrayList<>(gpxInfos.keySet()), true, params, headers, new OnFilesUploadCallback() { + @Nullable + @Override + public Map getAdditionalParams(@NonNull File file) { + Map additionaParams = new HashMap<>(); + GpxFileInfo gpxFileInfo = gpxInfos.get(file); + if (gpxFileInfo != null) { + additionaParams.put("name", gpxFileInfo.getFileName(true)); + additionaParams.put("type", Algorithms.getFileExtension(file)); + gpxFileInfo.uploadTime = System.currentTimeMillis(); + if (file.equals(favoritesFile)) { + favouritesHelper.setLastUploadedTime(gpxFileInfo.uploadTime); + } else { + GpxDataItem gpxItem = gpxHelper.getItem(file); + if (gpxItem != null) { + gpxHelper.updateLastUploadedTime(gpxItem, gpxFileInfo.uploadTime); + } + } + additionaParams.put("clienttime", String.valueOf(gpxFileInfo.uploadTime)); + } + return additionaParams; + } + + @Override + public void onFileUploadProgress(@NonNull File file, int progress) { + if (listener != null) { + listener.onFileUploadProgress(file, progress); + } + } + + @Override + public void onFilesUploadDone(@NonNull Map errors) { + if (errors.isEmpty()) { + settings.BACKUP_LAST_UPLOADED_TIME.set(System.currentTimeMillis() + 1); + } + if (listener != null) { + listener.onFilesUploadDone(errors); + } + } + }); + } + + public void deleteFiles(@NonNull List userFiles, @Nullable final OnDeleteFilesListener listener) throws UserNotRegisteredException { + checkRegistered(); + + Map commonParameters = new HashMap<>(); + commonParameters.put("deviceid", getDeviceId()); + commonParameters.put("accessToken", getAccessToken()); + + final List requests = new ArrayList<>(); + final Map filesMap = new HashMap<>(); + for (UserFile userFile : userFiles) { + Map parameters = new HashMap<>(commonParameters); + parameters.put("name", userFile.getName()); + parameters.put("type", userFile.getType()); + Request r = new Request(DELETE_FILE_URL, parameters, null, false, true); + requests.add(r); + filesMap.put(r, userFile); + } + AndroidNetworkUtils.sendRequestsAsync(null, requests, new OnSendRequestsListener() { + @Override + public void onRequestSent(@NonNull RequestResponse response) { + if (listener != null) { + UserFile userFile = filesMap.get(response.getRequest()); + if (userFile != null) { + listener.onFileDeleteProgress(userFile); + } + } + } + + @Override + public void onRequestsSent(@NonNull List results) { + if (listener != null) { + Map errors = new HashMap<>(); + for (RequestResponse response : results) { + UserFile userFile = filesMap.get(response.getRequest()); + if (userFile != null) { + String responseStr = response.getResponse(); + boolean success; + try { + JSONObject json = new JSONObject(responseStr); + String status = json.getString("status"); + success = status.equalsIgnoreCase("ok"); + } catch (JSONException e) { + success = false; + } + if (!success) { + errors.put(userFile, responseStr); + } + } + } + listener.onFilesDeleteDone(errors); + } + } + }); + } + + public void downloadFileList(@Nullable final OnDownloadFileListListener listener) throws UserNotRegisteredException { + checkRegistered(); + + Map params = new HashMap<>(); + params.put("deviceid", getDeviceId()); + params.put("accessToken", getAccessToken()); + AndroidNetworkUtils.sendRequestAsync(app, LIST_FILES_URL, params, "Download file list", true, false, new OnRequestResultListener() { + @Override + public void onResult(String resultJson) { + int status; + String message; + List userFiles = new ArrayList<>(); + if (!Algorithms.isEmpty(resultJson)) { + try { + JSONObject result = new JSONObject(resultJson); + String totalZipSize = result.getString("totalZipSize"); + String totalFiles = result.getString("totalFiles"); + String totalFileVersions = result.getString("totalFileVersions"); + JSONArray files = result.getJSONArray("uniqueFiles"); + for (int i = 0; i < files.length(); i++) { + userFiles.add(new UserFile(files.getJSONObject(i))); + } + + status = STATUS_SUCCESS; + message = "Total files: " + totalFiles + "\n" + + "Total zip size: " + AndroidUtils.formatSize(app, Long.parseLong(totalZipSize)) + "\n" + + "Total file versions: " + totalFileVersions; + } catch (JSONException | ParseException e) { + status = STATUS_PARSE_JSON_ERROR; + message = "Download file list error: json parsing"; + } + } else { + status = STATUS_EMPTY_RESPONSE_ERROR; + message = "Download file list error: empty response"; + } + if (listener != null) { + listener.onDownloadFileList(status, message, userFiles); + } + } + }); + } + + public void downloadFiles(@NonNull final Map filesMap, @Nullable final OnDownloadFileListener listener) throws UserNotRegisteredException { + checkRegistered(); + + Map params = new HashMap<>(); + params.put("deviceid", getDeviceId()); + params.put("accessToken", getAccessToken()); + AndroidNetworkUtils.downloadFilesAsync(DOWNLOAD_FILE_URL, + new ArrayList<>(filesMap.keySet()), params, new OnFilesDownloadCallback() { + @Nullable + @Override + public Map getAdditionalParams(@NonNull File file) { + UserFile userFile = filesMap.get(file); + Map additionaParams = new HashMap<>(); + additionaParams.put("name", userFile.getName()); + additionaParams.put("type", userFile.getType()); + return additionaParams; + } + + @Override + public void onFileDownloadProgress(@NonNull File file, int percent) { + if (listener != null) { + listener.onFileDownloadProgress(filesMap.get(file), percent); + } + } + + @Override + public void onFileDownloadedAsync(@NonNull File file) { + if (listener != null) { + listener.onFileDownloadedAsync(file); + } + } + + @Override + public void onFilesDownloadDone(@NonNull Map errors) { + if (listener != null) { + listener.onFilesDownloadDone(errors); + } + } + }); + } + + @SuppressLint("StaticFieldLeak") + public void collectLocalFiles(@Nullable final OnCollectLocalFilesListener listener) { + AsyncTask> task = new AsyncTask>() { + + private final OnCollectLocalFilesListener internalListener = new OnCollectLocalFilesListener() { + @Override + public void onFileCollected(@NonNull GpxFileInfo fileInfo) { + publishProgress(fileInfo); + } + + @Override + public void onFilesCollected(@NonNull List fileInfos) { + } + }; + + private void loadGPXData(@NonNull File mapPath, @NonNull List result, + @Nullable OnCollectLocalFilesListener listener) { + if (mapPath.canRead()) { + loadGPXFolder(mapPath, result, "", listener); + } + } + + private void loadGPXFolder(@NonNull File mapPath, @NonNull List result, + @NonNull String gpxSubfolder, @Nullable OnCollectLocalFilesListener listener) { + File[] listFiles = mapPath.listFiles(); + if (listFiles != null) { + for (File gpxFile : listFiles) { + if (gpxFile.isDirectory()) { + String sub = gpxSubfolder.length() == 0 ? gpxFile.getName() : gpxSubfolder + "/" + + gpxFile.getName(); + loadGPXFolder(gpxFile, result, sub, listener); + } else if (gpxFile.isFile() && gpxFile.getName().toLowerCase().endsWith(IndexConstants.GPX_FILE_EXT)) { + GpxFileInfo info = new GpxFileInfo(); + info.subfolder = gpxSubfolder; + info.file = gpxFile; + GpxDataItem gpxItem = gpxHelper.getItem(gpxFile); + if (gpxItem != null) { + info.uploadTime = gpxItem.getFileLastUploadedTime(); + } + result.add(info); + if (listener != null) { + listener.onFileCollected(info); + } + } + } + } + } + + @Override + protected List doInBackground(Void... voids) { + List result = new ArrayList<>(); + + GpxFileInfo favInfo = new GpxFileInfo(); + favInfo.subfolder = ""; + favInfo.file = favouritesHelper.getExternalFile(); + favInfo.uploadTime = favouritesHelper.getLastUploadedTime(); + result.add(favInfo); + if (listener != null) { + listener.onFileCollected(favInfo); + } + + loadGPXData(app.getAppPath(IndexConstants.GPX_INDEX_DIR), result, internalListener); + return result; + } + + @Override + protected void onProgressUpdate(GpxFileInfo... fileInfos) { + if (listener != null) { + listener.onFileCollected(fileInfos[0]); + } + } + + @Override + protected void onPostExecute(List fileInfos) { + if (listener != null) { + listener.onFilesCollected(fileInfos); + } + } + }; + task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + + @SuppressLint("StaticFieldLeak") + public void generateBackupInfo(@NonNull final List localFiles, @NonNull final List remoteFiles, + @Nullable final OnGenerateBackupInfoListener listener) { + + final long backupLastUploadedTime = settings.BACKUP_LAST_UPLOADED_TIME.get(); + + AsyncTask task = new AsyncTask() { + @Override + protected BackupInfo doInBackground(Void... voids) { + BackupInfo info = new BackupInfo(); + for (UserFile remoteFile : remoteFiles) { + boolean hasLocalFile = false; + for (GpxFileInfo localFile : localFiles) { + if (remoteFile.getName().equals(localFile.getFileName(true))) { + hasLocalFile = true; + long remoteUploadTime = remoteFile.getClienttimems(); + long localUploadTime = localFile.uploadTime; + long localModifiedTime = localFile.file.lastModified(); + if (remoteUploadTime == localUploadTime) { + if (localUploadTime < localModifiedTime) { + info.filesToUpload.add(localFile); + } + } else { + info.filesToMerge.add(new Pair<>(localFile, remoteFile)); + } + break; + } + } + if (!hasLocalFile) { + if (backupLastUploadedTime > 0 && backupLastUploadedTime >= remoteFile.getClienttimems()) { + info.filesToDelete.add(remoteFile); + } else { + info.filesToDownload.add(remoteFile); + } + } + } + for (GpxFileInfo localFile : localFiles) { + boolean hasRemoteFile = false; + for (UserFile remoteFile : remoteFiles) { + if (localFile.getFileName(true).equals(remoteFile.getName())) { + hasRemoteFile = true; + break; + } + } + if (!hasRemoteFile) { + info.filesToUpload.add(localFile); + } + } + return info; + } + + @Override + protected void onPostExecute(BackupInfo backupInfo) { + if (listener != null) { + listener.onBackupInfoGenerated(backupInfo, null); + } + } + }; + task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } +} diff --git a/OsmAnd/src/net/osmand/plus/backup/BackupTask.java b/OsmAnd/src/net/osmand/plus/backup/BackupTask.java new file mode 100644 index 0000000000..a2fc299463 --- /dev/null +++ b/OsmAnd/src/net/osmand/plus/backup/BackupTask.java @@ -0,0 +1,335 @@ +package net.osmand.plus.backup; + +import android.app.Activity; +import android.app.ProgressDialog; +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import net.osmand.AndroidUtils; +import net.osmand.GPXUtilities; +import net.osmand.GPXUtilities.GPXFile; +import net.osmand.plus.GPXDatabase.GpxDataItem; +import net.osmand.plus.GpxDbHelper; +import net.osmand.plus.OsmandApplication; +import net.osmand.plus.ProgressImplementation; +import net.osmand.plus.backup.BackupHelper.BackupInfo; +import net.osmand.plus.backup.BackupHelper.OnDeleteFilesListener; +import net.osmand.plus.backup.BackupHelper.OnDownloadFileListener; +import net.osmand.plus.backup.BackupHelper.OnUploadFilesListener; +import net.osmand.plus.importfiles.FavoritesImportTask; +import net.osmand.util.Algorithms; + +import java.io.File; +import java.lang.ref.WeakReference; +import java.util.HashMap; +import java.util.Map; +import java.util.Stack; + +import static net.osmand.IndexConstants.GPX_INDEX_DIR; +import static net.osmand.IndexConstants.TEMP_DIR; + +public class BackupTask { + + private final OsmandApplication app; + private final BackupHelper backupHelper; + + private final OnBackupListener listener; + private final WeakReference contextRef; + private ProgressImplementation progress; + + private final BackupInfo backupInfo; + private Map uploadErrors; + private Map downloadErrors; + private Map deleteErrors; + private String error; + + private final TaskType[] backupTasks = {TaskType.UPLOAD_FILES, TaskType.DELETE_FILES}; + private final TaskType[] restoreTasks = {TaskType.DOWNLOAD_FILES}; + + private Stack runningTasks = new Stack<>(); + + private enum TaskType { + UPLOAD_FILES, + DOWNLOAD_FILES, + DELETE_FILES + } + + public interface OnBackupListener { + void onBackupDone(@Nullable Map uploadErrors, + @Nullable Map downloadErrors, + @Nullable Map deleteErrors, @Nullable String error); + } + + public BackupTask(@NonNull BackupInfo backupInfo, @NonNull Context context, @Nullable OnBackupListener listener) { + this.contextRef = new WeakReference<>(context); + this.app = (OsmandApplication) context.getApplicationContext(); + this.backupHelper = app.getBackupHelper(); + this.backupInfo = backupInfo; + this.listener = listener; + } + + public BackupInfo getBackupInfo() { + return backupInfo; + } + + public Map getUploadErrors() { + return uploadErrors; + } + + public Map getDownloadErrors() { + return downloadErrors; + } + + public Map getDeleteErrors() { + return deleteErrors; + } + + public String getError() { + return error; + } + + public boolean runBackup() { + if (!runningTasks.empty()) { + return false; + } + initBackupTasks(); + return runTasks(); + } + + public boolean runRestore() { + if (!runningTasks.empty()) { + return false; + } + initRestoreTasks(); + return runTasks(); + } + + private void initBackupTasks() { + initData(); + Stack tasks = new Stack<>(); + for (int i = backupTasks.length - 1; i >= 0; i--) { + tasks.push(backupTasks[i]); + } + this.runningTasks = tasks; + onTasksInit(); + } + + private void initRestoreTasks() { + initData(); + Stack tasks = new Stack<>(); + for (int i = restoreTasks.length - 1; i >= 0; i--) { + tasks.push(restoreTasks[i]); + } + this.runningTasks = tasks; + onTasksInit(); + } + + private void initData() { + uploadErrors = null; + downloadErrors = null; + deleteErrors = null; + error = null; + } + + private boolean runTasks() { + if (runningTasks.empty()) { + return false; + } else { + TaskType taskType = runningTasks.pop(); + runTask(taskType); + return true; + } + } + + private void runTask(@NonNull TaskType taskType) { + switch (taskType) { + case UPLOAD_FILES: + doUploadFiles(); + break; + case DOWNLOAD_FILES: + doDownloadFiles(); + break; + case DELETE_FILES: + doDeleteFiles(); + break; + } + } + + private void onTaskFinished(@NonNull TaskType taskType) { + if (!runTasks()) { + onTasksDone(); + } + } + + private void doUploadFiles() { + if (Algorithms.isEmpty(backupInfo.filesToUpload)) { + onTaskFinished(TaskType.UPLOAD_FILES); + return; + } + onTaskProgressUpdate("Upload files..."); + try { + backupHelper.uploadFiles(backupInfo.filesToUpload, new OnUploadFilesListener() { + @Override + public void onFileUploadProgress(@NonNull File file, int progress) { + if (progress == 0) { + onTaskProgressUpdate(file.getName(), (int) (file.length() / 1024)); + } else { + onTaskProgressUpdate(progress); + } + } + + @Override + public void onFilesUploadDone(@NonNull Map errors) { + uploadErrors = errors; + onTaskFinished(TaskType.UPLOAD_FILES); + } + }); + } catch (UserNotRegisteredException e) { + onError("User is not registered"); + } + } + + private void doDownloadFiles() { + if (Algorithms.isEmpty(backupInfo.filesToDownload)) { + onTaskFinished(TaskType.DOWNLOAD_FILES); + return; + } + onTaskProgressUpdate("Download files..."); + File favoritesFile = app.getFavorites().getExternalFile(); + String favoritesFileName = favoritesFile.getName(); + File tempFavoritesFile = null; + final Map filesMap = new HashMap<>(); + for (UserFile userFile : backupInfo.filesToDownload) { + File file; + String fileName = userFile.getName(); + if (favoritesFileName.equals(fileName)) { + file = new File(app.getAppPath(TEMP_DIR), fileName); + tempFavoritesFile = file; + } else { + file = new File(app.getAppPath(GPX_INDEX_DIR), fileName); + } + filesMap.put(file, userFile); + } + final File finalTempFavoritesFile = tempFavoritesFile; + try { + backupHelper.downloadFiles(filesMap, new OnDownloadFileListener() { + @Override + public void onFileDownloadProgress(@NonNull UserFile userFile, int progress) { + if (progress == 0) { + onTaskProgressUpdate(new File(userFile.getName()).getName(), userFile.getFilesize() / 1024); + } else { + onTaskProgressUpdate(progress); + } + } + + @Override + public void onFileDownloadedAsync(@NonNull File file) { + UserFile userFile = filesMap.get(file); + long userFileTime = userFile.getClienttimems(); + if (file.equals(finalTempFavoritesFile)) { + GPXFile gpxFile = GPXUtilities.loadGPXFile(finalTempFavoritesFile); + FavoritesImportTask.mergeFavorites(app, gpxFile, "", false); + finalTempFavoritesFile.delete(); + app.getFavorites().getExternalFile().setLastModified(userFileTime); + } else { + file.setLastModified(userFileTime); + GpxDataItem item = new GpxDataItem(file, userFileTime); + app.getGpxDbHelper().add(item); + } + } + + @Override + public void onFilesDownloadDone(@NonNull Map errors) { + downloadErrors = errors; + onTaskFinished(TaskType.DOWNLOAD_FILES); + } + }); + } catch (UserNotRegisteredException e) { + onError("User is not registered"); + } + } + + private void doDeleteFiles() { + if (Algorithms.isEmpty(backupInfo.filesToDelete)) { + onTaskFinished(TaskType.DELETE_FILES); + return; + } + onTaskProgressUpdate("Delete files..."); + try { + backupHelper.deleteFiles(backupInfo.filesToDelete, new OnDeleteFilesListener() { + @Override + public void onFileDeleteProgress(@NonNull UserFile userFile) { + onTaskProgressUpdate(userFile.getName()); + } + + @Override + public void onFilesDeleteDone(@NonNull Map errors) { + deleteErrors = errors; + onTaskFinished(TaskType.DELETE_FILES); + } + }); + } catch (UserNotRegisteredException e) { + onError("User is not registered"); + } + } + + private void onTasksInit() { + Context ctx = contextRef.get(); + if (ctx instanceof Activity && AndroidUtils.isActivityNotDestroyed((Activity) ctx) && progress != null) { + progress = ProgressImplementation.createProgressDialog(ctx, + "Backup data", "Initializing...", ProgressDialog.STYLE_HORIZONTAL); + } + } + + private void onTaskProgressUpdate(Object... objects) { + Context ctx = contextRef.get(); + if (ctx instanceof Activity && AndroidUtils.isActivityNotDestroyed((Activity) ctx) && progress != null) { + if (objects != null) { + if (objects.length == 1) { + if (objects[0] instanceof String) { + progress.startTask((String) objects[0], -1); + } else if (objects[0] instanceof Integer) { + int progressValue = (Integer) objects[0]; + if (progressValue < Integer.MAX_VALUE) { + progress.progress(progressValue); + } else { + progress.finishTask(); + } + } + } else if (objects.length == 2) { + progress.startTask((String) objects[0], (Integer) objects[1]); + } + } + } + } + + private void onError(@NonNull String message) { + this.error = message; + runningTasks.clear(); + onTasksDone(); + } + + private void onTasksDone() { + if (listener != null) { + listener.onBackupDone(uploadErrors, downloadErrors, deleteErrors, error); + } + Context ctx = contextRef.get(); + if (ctx instanceof Activity && AndroidUtils.isActivityNotDestroyed((Activity) ctx) && progress != null) { + progress.finishTask(); + app.runInUIThread(new Runnable() { + @Override + public void run() { + try { + if (progress.getDialog().isShowing()) { + progress.getDialog().dismiss(); + } + } catch (Exception e) { + //ignored + } + } + }, 300); + } + } +} diff --git a/OsmAnd/src/net/osmand/plus/backup/GpxFileInfo.java b/OsmAnd/src/net/osmand/plus/backup/GpxFileInfo.java new file mode 100644 index 0000000000..228e119fd6 --- /dev/null +++ b/OsmAnd/src/net/osmand/plus/backup/GpxFileInfo.java @@ -0,0 +1,65 @@ +package net.osmand.plus.backup; + +import net.osmand.util.Algorithms; + +import java.io.File; + +public class GpxFileInfo { + public File file; + public String subfolder; + public long uploadTime = 0; + + private String name = null; + private int sz = -1; + private String fileName = null; + + public String getName() { + if (name == null) { + name = formatName(file.getName()); + } + return name; + } + + private String formatName(String name) { + int ext = name.lastIndexOf('.'); + if (ext != -1) { + name = name.substring(0, ext); + } + return name.replace('_', ' '); + } + + // Usage: AndroidUtils.formatSize(v.getContext(), getSize() * 1024l); + public int getSize() { + if (sz == -1) { + if (file == null) { + return -1; + } + sz = (int) ((file.length() + 512) >> 10); + } + return sz; + } + + public long getFileDate() { + if (file == null) { + return 0; + } + return file.lastModified(); + } + + public String getFileName(boolean includeSubfolder) { + String result; + if (fileName != null) { + result = fileName; + } else { + if (file == null) { + result = ""; + } else { + result = fileName = file.getName(); + } + } + if (includeSubfolder && !Algorithms.isEmpty(subfolder)) { + result = subfolder + "/" + result; + } + return result; + } +} diff --git a/OsmAnd/src/net/osmand/plus/backup/PrepareBackupTask.java b/OsmAnd/src/net/osmand/plus/backup/PrepareBackupTask.java new file mode 100644 index 0000000000..d3932b5ba8 --- /dev/null +++ b/OsmAnd/src/net/osmand/plus/backup/PrepareBackupTask.java @@ -0,0 +1,211 @@ +package net.osmand.plus.backup; + +import android.app.Activity; +import android.app.ProgressDialog; +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import net.osmand.AndroidUtils; +import net.osmand.plus.OsmandApplication; +import net.osmand.plus.ProgressImplementation; +import net.osmand.plus.backup.BackupHelper.BackupInfo; +import net.osmand.plus.backup.BackupHelper.OnCollectLocalFilesListener; +import net.osmand.plus.backup.BackupHelper.OnDownloadFileListListener; +import net.osmand.plus.backup.BackupHelper.OnGenerateBackupInfoListener; +import net.osmand.util.Algorithms; + +import java.lang.ref.WeakReference; +import java.util.List; +import java.util.Stack; + +public class PrepareBackupTask { + + private final OsmandApplication app; + private final BackupHelper backupHelper; + + private final OnPrepareBackupListener listener; + private final WeakReference contextRef; + private ProgressImplementation progress; + + private BackupInfo result; + private List userFiles; + private List fileInfos; + private String error; + + private Stack runningTasks = new Stack<>(); + + private enum TaskType { + COLLECT_LOCAL_FILES, + COLLECT_REMOTE_FILES, + GENERATE_BACKUP_INFO + } + + public interface OnPrepareBackupListener { + void onBackupPrepared(@Nullable BackupInfo backupInfo, @Nullable String error); + } + + public PrepareBackupTask(@NonNull Context context, @Nullable OnPrepareBackupListener listener) { + this.contextRef = new WeakReference<>(context); + this.app = (OsmandApplication) context.getApplicationContext(); + this.backupHelper = app.getBackupHelper(); + this.listener = listener; + } + + public BackupInfo getResult() { + return result; + } + + public String getError() { + return error; + } + + public boolean prepare() { + if (!runningTasks.empty()) { + return false; + } + initTasks(); + return runTasks(); + } + + private void initTasks() { + result = null; + userFiles = null; + fileInfos = null; + error = null; + Stack tasks = new Stack<>(); + TaskType[] types = TaskType.values(); + for (int i = types.length - 1; i >= 0; i--) { + tasks.push(types[i]); + } + this.runningTasks = tasks; + onTasksInit(); + } + + private boolean runTasks() { + if (runningTasks.empty()) { + return false; + } else { + TaskType taskType = runningTasks.pop(); + runTask(taskType); + return true; + } + } + + private void runTask(@NonNull TaskType taskType) { + switch (taskType) { + case COLLECT_LOCAL_FILES: + doCollectLocalFiles(); + break; + case COLLECT_REMOTE_FILES: + doCollectRemoteFiles(); + break; + case GENERATE_BACKUP_INFO: + doGenerateBackupInfo(); + break; + } + } + + private void onTaskFinished(@NonNull TaskType taskType) { + if (!runTasks()) { + onTasksDone(); + } + } + + private void doCollectLocalFiles() { + onTaskProgressUpdate("Collecting local info..."); + backupHelper.collectLocalFiles(new OnCollectLocalFilesListener() { + @Override + public void onFileCollected(@NonNull GpxFileInfo fileInfo) { + } + + @Override + public void onFilesCollected(@NonNull List fileInfos) { + PrepareBackupTask.this.fileInfos = fileInfos; + onTaskFinished(TaskType.COLLECT_LOCAL_FILES); + } + }); + } + + private void doCollectRemoteFiles() { + onTaskProgressUpdate("Downloading remote info..."); + try { + backupHelper.downloadFileList(new OnDownloadFileListListener() { + @Override + public void onDownloadFileList(int status, @Nullable String message, @NonNull List userFiles) { + if (status == BackupHelper.STATUS_SUCCESS) { + PrepareBackupTask.this.userFiles = userFiles; + } else { + onError(!Algorithms.isEmpty(message) ? message : "Download file list error: " + status); + } + onTaskFinished(TaskType.COLLECT_REMOTE_FILES); + } + }); + } catch (UserNotRegisteredException e) { + onError("User is not registered"); + } + } + + private void doGenerateBackupInfo() { + if (fileInfos == null || userFiles == null) { + onTaskFinished(TaskType.GENERATE_BACKUP_INFO); + return; + } + onTaskProgressUpdate("Generating backup info..."); + backupHelper.generateBackupInfo(fileInfos, userFiles, new OnGenerateBackupInfoListener() { + @Override + public void onBackupInfoGenerated(@Nullable BackupInfo backupInfo, @Nullable String error) { + if (Algorithms.isEmpty(error)) { + PrepareBackupTask.this.result = backupInfo; + } else { + onError(error); + } + onTaskFinished(TaskType.GENERATE_BACKUP_INFO); + } + }); + } + + private void onTasksInit() { + Context ctx = contextRef.get(); + if (ctx instanceof Activity && AndroidUtils.isActivityNotDestroyed((Activity) ctx)) { + progress = ProgressImplementation.createProgressDialog(ctx, + "Prepare backup", "Initializing...", ProgressDialog.STYLE_HORIZONTAL); + } + } + + private void onTaskProgressUpdate(String message) { + Context ctx = contextRef.get(); + if (ctx instanceof Activity && AndroidUtils.isActivityNotDestroyed((Activity) ctx) && progress != null) { + progress.startTask(message, -1); + } + } + + private void onError(@NonNull String message) { + this.error = message; + runningTasks.clear(); + onTasksDone(); + } + + private void onTasksDone() { + if (listener != null) { + listener.onBackupPrepared(result, error); + } + Context ctx = contextRef.get(); + if (ctx instanceof Activity && AndroidUtils.isActivityNotDestroyed((Activity) ctx) && progress != null) { + progress.finishTask(); + app.runInUIThread(new Runnable() { + @Override + public void run() { + try { + if (progress.getDialog().isShowing()) { + progress.getDialog().dismiss(); + } + } catch (Exception e) { + //ignored + } + } + }, 300); + } + } +} diff --git a/OsmAnd/src/net/osmand/plus/backup/UserFile.java b/OsmAnd/src/net/osmand/plus/backup/UserFile.java new file mode 100644 index 0000000000..437c46963b --- /dev/null +++ b/OsmAnd/src/net/osmand/plus/backup/UserFile.java @@ -0,0 +1,102 @@ +package net.osmand.plus.backup; + +import androidx.annotation.NonNull; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.text.DateFormat; +import java.text.ParseException; +import java.util.Date; +import java.util.Locale; + +public class UserFile { + + private int userid; + private long id; + private int deviceid; + private int filesize; + private String type; + private String name; + private Date updatetime; + private long updatetimems; + private Date clienttime; + private long clienttimems; + private int zipSize; + + public UserFile(@NonNull JSONObject json) throws JSONException, ParseException { + if (json.has("userid")) { + userid = json.getInt("userid"); + } + if (json.has("id")) { + id = json.getLong("id"); + } + if (json.has("deviceid")) { + deviceid = json.getInt("deviceid"); + } + if (json.has("filesize")) { + filesize = json.getInt("filesize"); + } + if (json.has("type")) { + type = json.getString("type"); + } + if (json.has("name")) { + name = json.getString("name"); + } + if (json.has("updatetimems")) { + updatetimems = json.getLong("updatetimems"); + updatetime = new Date(updatetimems); + } + if (json.has("clienttimems")) { + clienttimems = json.getLong("clienttimems"); + clienttime = new Date(clienttimems); + } + if (json.has("zipSize")) { + zipSize = json.getInt("zipSize"); + } + } + + public int getUserid() { + return userid; + } + + public long getId() { + return id; + } + + public int getDeviceid() { + return deviceid; + } + + public int getFilesize() { + return filesize; + } + + public String getType() { + return type; + } + + public String getName() { + return name; + } + + public Date getUpdatetime() { + return updatetime; + } + + public long getUpdatetimems() { + return updatetimems; + } + + public Date getClienttime() { + return clienttime; + } + + public long getClienttimems() { + return clienttimems; + } + + public int getZipSize() { + return zipSize; + } +} diff --git a/OsmAnd/src/net/osmand/plus/backup/UserNotRegisteredException.java b/OsmAnd/src/net/osmand/plus/backup/UserNotRegisteredException.java new file mode 100644 index 0000000000..a414022b91 --- /dev/null +++ b/OsmAnd/src/net/osmand/plus/backup/UserNotRegisteredException.java @@ -0,0 +1,9 @@ +package net.osmand.plus.backup; + +public class UserNotRegisteredException extends Exception { + private static final long serialVersionUID = -8005954380280822845L; + + public UserNotRegisteredException() { + super("User is not resistered"); + } +} diff --git a/OsmAnd/src/net/osmand/plus/development/TestBackupActivity.java b/OsmAnd/src/net/osmand/plus/development/TestBackupActivity.java index 0f1d1775bd..516e1cc581 100644 --- a/OsmAnd/src/net/osmand/plus/development/TestBackupActivity.java +++ b/OsmAnd/src/net/osmand/plus/development/TestBackupActivity.java @@ -1,72 +1,64 @@ package net.osmand.plus.development; import android.app.Activity; -import android.app.ProgressDialog; import android.graphics.drawable.Drawable; -import android.os.AsyncTask; import android.os.Bundle; -import android.text.TextUtils; -import android.util.Patterns; import android.util.TypedValue; import android.view.View; import android.widget.EditText; import android.widget.ProgressBar; import android.widget.TextView; -import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.widget.Toolbar; -import net.osmand.AndroidNetworkUtils; -import net.osmand.AndroidNetworkUtils.OnFilesUploadCallback; -import net.osmand.AndroidNetworkUtils.OnRequestResultListener; import net.osmand.AndroidUtils; -import net.osmand.IndexConstants; import net.osmand.plus.OsmandApplication; -import net.osmand.plus.ProgressImplementation; import net.osmand.plus.R; import net.osmand.plus.UiUtilities; import net.osmand.plus.UiUtilities.DialogButtonType; import net.osmand.plus.activities.OsmandActionBarActivity; +import net.osmand.plus.backup.BackupHelper; +import net.osmand.plus.backup.BackupHelper.BackupInfo; +import net.osmand.plus.backup.BackupHelper.OnRegisterUserListener; +import net.osmand.plus.backup.BackupTask; +import net.osmand.plus.backup.BackupTask.OnBackupListener; +import net.osmand.plus.backup.PrepareBackupTask; +import net.osmand.plus.backup.PrepareBackupTask.OnPrepareBackupListener; +import net.osmand.plus.backup.UserFile; import net.osmand.plus.settings.backend.OsmandSettings; +import net.osmand.plus.widgets.OsmandTextFieldBoxes; import net.osmand.util.Algorithms; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - import java.io.File; import java.lang.ref.WeakReference; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; import java.util.Map; +import java.util.Map.Entry; public class TestBackupActivity extends OsmandActionBarActivity { - // TODO pass actual sub order id! - private static final String TEST_ORDER_ID = ""; - private OsmandApplication app; private OsmandSettings settings; + private BackupHelper backupHelper; private ProgressBar progressBar; private View buttonRegister; private View buttonVerify; + private View buttonRefresh; private View buttonBackup; private View buttonRestore; private EditText emailEditText; + private OsmandTextFieldBoxes tokenEdit; private EditText tokenEditText; private TextView infoView; - public interface OnResultListener { - void onResult(boolean success, @Nullable String result); - } + private BackupInfo backupInfo; @Override public void onCreate(Bundle savedInstanceState) { app = getMyApplication(); settings = app.getSettings(); + backupHelper = app.getBackupHelper(); final WeakReference activityRef = new WeakReference<>(this); boolean nightMode = !app.getSettings().isLightContent(); @@ -90,15 +82,23 @@ public class TestBackupActivity extends OsmandActionBarActivity { } }); + if (!backupHelper.hasOsmLiveUpdates()) { + findViewById(R.id.main_view).setVisibility(View.GONE); + return; + } + buttonRegister = findViewById(R.id.btn_register); UiUtilities.setupDialogButton(nightMode, buttonRegister, DialogButtonType.PRIMARY, "Register"); buttonVerify = findViewById(R.id.btn_verify); UiUtilities.setupDialogButton(nightMode, buttonVerify, DialogButtonType.PRIMARY, "Verify"); + buttonRefresh = findViewById(R.id.btn_refresh); + UiUtilities.setupDialogButton(nightMode, buttonRefresh, DialogButtonType.PRIMARY, "Refresh"); buttonBackup = findViewById(R.id.btn_backup); UiUtilities.setupDialogButton(nightMode, buttonBackup, DialogButtonType.PRIMARY, "Backup"); buttonRestore = findViewById(R.id.btn_restore); UiUtilities.setupDialogButton(nightMode, buttonRestore, DialogButtonType.PRIMARY, "Restore"); + tokenEdit = findViewById(R.id.edit_token_label); tokenEditText = findViewById(R.id.edit_token); infoView = findViewById(R.id.text_info); progressBar = findViewById(R.id.progress_bar); @@ -109,22 +109,31 @@ public class TestBackupActivity extends OsmandActionBarActivity { if (!Algorithms.isEmpty(email)) { emailEditText.setText(email); } + if (backupHelper.isRegistered()) { + tokenEdit.setVisibility(View.GONE); + buttonVerify.setVisibility(View.GONE); + } else { + tokenEdit.setVisibility(View.VISIBLE); + buttonVerify.setVisibility(View.VISIBLE); + } buttonRegister.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { String email = emailEditText.getText().toString(); - if (isEmailValid(email)) { + if (AndroidUtils.isValidEmail(email)) { buttonRegister.setEnabled(false); settings.BACKUP_USER_EMAIL.set(email); progressBar.setVisibility(View.VISIBLE); - registerUser(email, new OnResultListener() { + backupHelper.registerUser(email, new OnRegisterUserListener() { @Override - public void onResult(boolean success, @Nullable String result) { + public void onRegisterUser(int status, @Nullable String message) { TestBackupActivity a = activityRef.get(); if (AndroidUtils.isActivityNotDestroyed(a)) { a.progressBar.setVisibility(View.GONE); - a.buttonRegister.setEnabled(!success); - a.buttonVerify.setEnabled(success); + a.buttonRegister.setEnabled(status != BackupHelper.STATUS_SUCCESS); + a.tokenEdit.setVisibility(View.VISIBLE); + a.buttonVerify.setVisibility(View.VISIBLE); + a.buttonVerify.setEnabled(status == BackupHelper.STATUS_SUCCESS); a.tokenEditText.requestFocus(); } } @@ -139,17 +148,22 @@ public class TestBackupActivity extends OsmandActionBarActivity { @Override public void onClick(View v) { String token = tokenEditText.getText().toString(); - if (isTokenValid(token)) { + if (BackupHelper.isTokenValid(token)) { buttonVerify.setEnabled(false); progressBar.setVisibility(View.VISIBLE); - registerDevice(token, new OnResultListener() { + backupHelper.registerDevice(token, new BackupHelper.OnRegisterDeviceListener() { + @Override - public void onResult(boolean success, @Nullable String result) { + public void onRegisterDevice(int status, @Nullable String message) { TestBackupActivity a = activityRef.get(); if (AndroidUtils.isActivityNotDestroyed(a)) { a.progressBar.setVisibility(View.GONE); - a.buttonVerify.setEnabled(!success); - a.loadBackupInfo(); + a.buttonVerify.setEnabled(status != BackupHelper.STATUS_SUCCESS); + if (status == BackupHelper.STATUS_SUCCESS) { + tokenEdit.setVisibility(View.GONE); + buttonVerify.setVisibility(View.GONE); + } + a.prepareBackup(); } } }); @@ -160,262 +174,120 @@ public class TestBackupActivity extends OsmandActionBarActivity { } } }); + buttonRefresh.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + prepareBackup(); + } + }); buttonBackup.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - uploadFiles(); + if (backupInfo != null) { + BackupTask task = new BackupTask(backupInfo, TestBackupActivity.this, new OnBackupListener() { + @Override + public void onBackupDone(@Nullable Map uploadErrors, @Nullable Map downloadErrors, + @Nullable Map deleteErrors, @Nullable String error) { + TestBackupActivity a = activityRef.get(); + if (AndroidUtils.isActivityNotDestroyed(a)) { + String description; + if (error != null) { + description = error; + } else if (uploadErrors == null && downloadErrors == null) { + description = "No data"; + } else { + description = getBackupDescription(uploadErrors, downloadErrors, deleteErrors, error); + } + a.infoView.setText(description); + a.infoView.requestFocus(); + a.prepareBackup(); + } + } + }); + task.runBackup(); + } } }); buttonRestore.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - } - }); - - loadBackupInfo(); - } - - private void loadBackupInfo() { - if (!Algorithms.isEmpty(getDeviceId()) && !Algorithms.isEmpty(getAccessToken())) { - final WeakReference activityRef = new WeakReference<>(this); - progressBar.setVisibility(View.VISIBLE); - loadBackupInfo(new OnResultListener() { - @Override - public void onResult(boolean success, @Nullable String result) { - TestBackupActivity a = activityRef.get(); - if (AndroidUtils.isActivityNotDestroyed(a)) { - a.progressBar.setVisibility(View.GONE); - a.infoView.setText(result); - a.infoView.requestFocus(); - } - } - }); - } - } - - private boolean isEmailValid(CharSequence target) { - return (!TextUtils.isEmpty(target) && Patterns.EMAIL_ADDRESS.matcher(target).matches()); - } - - private String getOrderId() { - return TEST_ORDER_ID; - } - - private String getDeviceId() { - return settings.BACKUP_DEVICE_ID.get(); - } - - private String getAccessToken() { - return settings.BACKUP_ACCESS_TOKEN.get(); - } - - private void registerUser(@NonNull String email, @Nullable final OnResultListener listener) { - Map params = new HashMap<>(); - params.put("email", email); - params.put("orderid", getOrderId()); - params.put("deviceid", app.getUserAndroidId()); - AndroidNetworkUtils.sendRequestAsync(app, "https://osmand.net/userdata/user-register", params, "Register user", true, true, new OnRequestResultListener() { - @Override - public void onResult(String resultJson) { - boolean success = false; - if (!Algorithms.isEmpty(resultJson)) { - try { - // {"status":"ok"} - JSONObject result = new JSONObject(resultJson); - String status = result.getString("status"); - success = status.equals("ok"); - app.showToastMessage(success - ? "You have been registered successfully. Please check for email with activation code." - : "User registration error: " + status); - } catch (JSONException e) { - app.showToastMessage("User registration error: json parsing"); - } - } else { - app.showToastMessage("User registration error: empty response"); - } - if (listener != null) { - listener.onResult(success, resultJson); - } - } - }); - } - - private void registerDevice(String token, @Nullable final OnResultListener listener) { - Map params = new HashMap<>(); - params.put("email", settings.BACKUP_USER_EMAIL.get()); - params.put("orderid", getOrderId()); - params.put("deviceid", app.getUserAndroidId()); - params.put("token", token); - AndroidNetworkUtils.sendRequestAsync(app, "https://osmand.net/userdata/device-register", params, "Register device", true, true, new OnRequestResultListener() { - @Override - public void onResult(String resultJson) { - boolean success = false; - if (!Algorithms.isEmpty(resultJson)) { - try { - /* - { - "id": 1034, - "userid": 1033, - "deviceid": "2fa8080d2985a777", - "orderid": "460000687003939", - "accesstoken": "4bc0a61f-397a-4c3e-9ffc-db382ec00372", - "udpatetime": "Apr 11, 2021, 11:32:20 AM" - } - */ - JSONObject result = new JSONObject(resultJson); - settings.BACKUP_DEVICE_ID.set(result.getString("id")); - settings.BACKUP_USER_ID.set(result.getString("userid")); - settings.BACKUP_NATIVE_DEVICE_ID.set(result.getString("deviceid")); - settings.BACKUP_ACCESS_TOKEN.set(result.getString("accesstoken")); - settings.BACKUP_ACCESS_TOKEN_UPDATE_TIME.set(result.getString("udpatetime")); - success = true; - app.showToastMessage("Device have been registered successfully"); - } catch (JSONException e) { - app.showToastMessage("Device registration error: json parsing"); - } - } else { - app.showToastMessage("Device registration error: empty response"); - } - if (listener != null) { - listener.onResult(success, resultJson); - } - } - }); - } - - private void uploadFiles() { - LoadGpxTask loadGpxTask = new LoadGpxTask(this, new LoadGpxTask.OnLoadGpxListener() { - @Override - public void onLoadGpxDone(@NonNull List result) { - uploadFiles(result); - } - }); - loadGpxTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, this); - } - - private void uploadFiles(List gpxFiles) { - //{"status":"ok"} - final WeakReference activityRef = new WeakReference<>(this); - - Map params = new HashMap<>(); - params.put("deviceid", getDeviceId()); - params.put("accessToken", getAccessToken()); - Map headers = new HashMap<>(); - headers.put("Accept-Encoding", "deflate, gzip"); - - final Map gpxInfos = new HashMap<>(); - for (GpxInfo gpxFile : gpxFiles) { - gpxInfos.put(gpxFile.file, gpxFile); - } - final List files = new ArrayList<>(gpxInfos.keySet()); - File favoritesFile = app.getFavorites().getExternalFile(); - files.add(favoritesFile); - - final ProgressImplementation progress = ProgressImplementation.createProgressDialog(this, - "Create backup", "Uploading " + files.size() + " file(s) to server", ProgressDialog.STYLE_HORIZONTAL); - - AndroidNetworkUtils.uploadFilesAsync("https://osmand.net/userdata/upload-file", files, true, params, headers, new OnFilesUploadCallback() { - @Nullable - @Override - public Map getAdditionalParams(@NonNull File file) { - GpxInfo gpxInfo = gpxInfos.get(file); - Map additionaParams = new HashMap<>(); - additionaParams.put("name", gpxInfo == null ? file.getName() : gpxInfo.getFileName(true)); - additionaParams.put("type", Algorithms.getFileExtension(file)); - return additionaParams; - } - - @Override - public void onFileUploadProgress(@NonNull File file, int percent) { - Activity a = activityRef.get(); - if (AndroidUtils.isActivityNotDestroyed(a)) { - if (percent < 100) { - progress.startTask(file.getName(), percent); - } else { - progress.finishTask(); - } - } - } - - @Override - public void onFilesUploadDone(@NonNull Map errors) { - Activity a = activityRef.get(); - if (AndroidUtils.isActivityNotDestroyed(a)) { - app.runInUIThread(new Runnable() { + if (backupInfo != null) { + BackupTask task = new BackupTask(backupInfo, TestBackupActivity.this, new OnBackupListener() { @Override - public void run() { - try { - if (progress.getDialog().isShowing()) { - progress.getDialog().dismiss(); + public void onBackupDone(@Nullable Map uploadErrors, @Nullable Map downloadErrors, + @Nullable Map deleteErrors, @Nullable String error) { + TestBackupActivity a = activityRef.get(); + if (AndroidUtils.isActivityNotDestroyed(a)) { + String description; + if (error != null) { + description = error; + } else if (uploadErrors == null && downloadErrors == null) { + description = "No data"; + } else { + description = getBackupDescription(uploadErrors, downloadErrors, deleteErrors, error); } - } catch (Exception e) { - //ignored + a.infoView.setText(description); + a.infoView.requestFocus(); + a.prepareBackup(); } } - }, 300); - app.showToastMessage("Uploaded " + (files.size() - errors.size() + " files" + - (errors.size() > 0 ? ". Errors: " + errors.size() : ""))); - loadBackupInfo(); + }); + task.runRestore(); } } }); + + prepareBackup(); } - private void loadBackupInfo(@Nullable final OnResultListener listener) { - Map params = new HashMap<>(); - params.put("deviceid", getDeviceId()); - params.put("accessToken", getAccessToken()); - AndroidNetworkUtils.sendRequestAsync(app, "https://osmand.net/userdata/list-files", params, "Get backup info", true, false, new OnRequestResultListener() { + private String getBackupDescription(@Nullable Map uploadErrors, @Nullable Map downloadErrors, @Nullable Map deleteErrors, @Nullable String error) { + StringBuilder sb = new StringBuilder(); + if (!Algorithms.isEmpty(uploadErrors)) { + sb.append("--- Upload errors ---").append("\n"); + for (Entry uploadEntry : uploadErrors.entrySet()) { + sb.append(uploadEntry.getKey().getName()).append(": ").append(uploadEntry.getValue()).append("\n"); + } + } + if (!Algorithms.isEmpty(downloadErrors)) { + sb.append("--- Download errors ---").append("\n"); + for (Entry downloadEntry : downloadErrors.entrySet()) { + sb.append(downloadEntry.getKey().getName()).append(": ").append(downloadEntry.getValue()).append("\n"); + } + } + if (!Algorithms.isEmpty(deleteErrors)) { + sb.append("--- Delete errors ---").append("\n"); + for (Entry deleteEntry : deleteErrors.entrySet()) { + sb.append(deleteEntry.getKey().getName()).append(": ").append(deleteEntry.getValue()).append("\n"); + } + } + return sb.length() == 0 ? "OK" : sb.toString(); + } + + private void prepareBackup() { + final WeakReference activityRef = new WeakReference<>(this); + PrepareBackupTask prepareBackupTask = new PrepareBackupTask(this, new OnPrepareBackupListener() { @Override - public void onResult(String resultJson) { - boolean success = false; - StringBuilder resultString = new StringBuilder(); - if (!Algorithms.isEmpty(resultJson)) { - try { - /* - { - "totalZipSize": 21792, - "totalFileSize": 185920, - "totalFiles": 1, - "totalFileVersions": 2, - "uniqueFiles": [ - { - "userid": 1033, - "id": 7, - "deviceid": 1034, - "filesize": 92960, - "type": "gpx", - "name": "test/Day 2.gpx", - "updatetime": "Apr 11, 2021, 1:49:01 PM", - "updatetimems": 1618141741822, - "zipSize": 10896 - } - ], - "deviceid": 1034 - } - */ - JSONObject result = new JSONObject(resultJson); - String totalZipSize = result.getString("totalZipSize"); - String totalFiles = result.getString("totalFiles"); - String totalFileVersions = result.getString("totalFileVersions"); - JSONArray files = result.getJSONArray("uniqueFiles"); - resultString.append("Total files: ").append(totalFiles).append("\n"); - resultString.append("Total zip size: ").append(AndroidUtils.formatSize(app, Long.parseLong(totalZipSize))).append("\n"); - resultString.append("Total file versions: ").append(totalFileVersions); - - success = true; - } catch (JSONException e) { + public void onBackupPrepared(@Nullable BackupInfo backupInfo, @Nullable String error) { + TestBackupActivity.this.backupInfo = backupInfo; + TestBackupActivity a = activityRef.get(); + if (AndroidUtils.isActivityNotDestroyed(a)) { + String description; + if (error != null) { + description = error; + } else if (backupInfo == null) { + description = "No data"; + } else { + description = "Files to upload: " + backupInfo.filesToUpload.size() + + "\nFiles to download: " + backupInfo.filesToDownload.size() + + "\nFiles to delete: " + backupInfo.filesToDelete.size() + + "\nConflicts: " + backupInfo.filesToMerge.size(); } - } - if (listener != null) { - listener.onResult(success, resultString.toString()); + a.infoView.setText(description); + a.infoView.requestFocus(); } } }); - } - - private boolean isTokenValid(String token) { - return token.matches("[0-9]+"); + prepareBackupTask.prepare(); } private int resolveResourceId(final Activity activity, final int attr) { @@ -423,173 +295,4 @@ public class TestBackupActivity extends OsmandActionBarActivity { activity.getTheme().resolveAttribute(attr, typedvalueattr, true); return typedvalueattr.resourceId; } - - private static class LoadGpxTask extends AsyncTask> { - - private final OsmandApplication app; - private final OnLoadGpxListener listener; - private final WeakReference activityRef; - private List result; - private ProgressImplementation progress; - - interface OnLoadGpxListener { - void onLoadGpxDone(@NonNull List result); - } - - LoadGpxTask(@NonNull Activity activity, @Nullable OnLoadGpxListener listener) { - this.activityRef = new WeakReference<>(activity); - this.app = (OsmandApplication) activity.getApplication(); - this.listener = listener; - } - - public List getResult() { - return result; - } - - @NonNull - @Override - protected List doInBackground(Activity... params) { - List result = new ArrayList<>(); - loadGPXData(app.getAppPath(IndexConstants.GPX_INDEX_DIR), result, this); - return result; - } - - public void loadFile(GpxInfo... loaded) { - publishProgress(loaded); - } - - @Override - protected void onPreExecute() { - Activity a = activityRef.get(); - if (AndroidUtils.isActivityNotDestroyed(a)) { - progress = ProgressImplementation.createProgressDialog(a, - "Create backup", "Collecting gpx files...", ProgressDialog.STYLE_HORIZONTAL); - } - } - - @Override - protected void onProgressUpdate(GpxInfo... values) { - Activity a = activityRef.get(); - if (AndroidUtils.isActivityNotDestroyed(a)) { - progress.startTask(values[0].getFileName(true), -1); - } - } - - @Override - protected void onPostExecute(@NonNull List result) { - this.result = result; - if (listener != null) { - listener.onLoadGpxDone(result); - } - Activity a = activityRef.get(); - if (AndroidUtils.isActivityNotDestroyed(a)) { - progress.finishTask(); - app.runInUIThread(new Runnable() { - @Override - public void run() { - try { - if (progress.getDialog().isShowing()) { - progress.getDialog().dismiss(); - } - } catch (Exception e) { - //ignored - } - } - }, 300); - } - } - - private void loadGPXData(File mapPath, List result, LoadGpxTask loadTask) { - if (mapPath.canRead()) { - List progress = new ArrayList<>(); - loadGPXFolder(mapPath, result, loadTask, progress, ""); - if (!progress.isEmpty()) { - loadTask.loadFile(progress.toArray(new GpxInfo[0])); - } - } - } - - private void loadGPXFolder(File mapPath, List result, LoadGpxTask loadTask, List progress, - String gpxSubfolder) { - File[] listFiles = mapPath.listFiles(); - if (listFiles != null) { - for (File gpxFile : listFiles) { - if (gpxFile.isDirectory()) { - String sub = gpxSubfolder.length() == 0 ? gpxFile.getName() : gpxSubfolder + "/" - + gpxFile.getName(); - loadGPXFolder(gpxFile, result, loadTask, progress, sub); - } else if (gpxFile.isFile() && gpxFile.getName().toLowerCase().endsWith(IndexConstants.GPX_FILE_EXT)) { - GpxInfo info = new GpxInfo(); - info.subfolder = gpxSubfolder; - info.file = gpxFile; - result.add(info); - progress.add(info); - if (progress.size() > 7) { - loadTask.loadFile(progress.toArray(new GpxInfo[0])); - progress.clear(); - } - } - } - } - } - } - - private static class GpxInfo { - public File file; - public String subfolder; - - private String name = null; - private int sz = -1; - private String fileName = null; - - public String getName() { - if (name == null) { - name = formatName(file.getName()); - } - return name; - } - - private String formatName(String name) { - int ext = name.lastIndexOf('.'); - if (ext != -1) { - name = name.substring(0, ext); - } - return name.replace('_', ' '); - } - - // Usage: AndroidUtils.formatSize(v.getContext(), getSize() * 1024l); - public int getSize() { - if (sz == -1) { - if (file == null) { - return -1; - } - sz = (int) ((file.length() + 512) >> 10); - } - return sz; - } - - public long getFileDate() { - if (file == null) { - return 0; - } - return file.lastModified(); - } - - public String getFileName(boolean includeSubfolder) { - String result; - if (fileName != null) { - result = fileName; - } else { - if (file == null) { - result = ""; - } else { - result = fileName = file.getName(); - } - } - if (includeSubfolder && !Algorithms.isEmpty(subfolder)) { - result = subfolder + "/" + result; - } - return result; - } - } } diff --git a/OsmAnd/src/net/osmand/plus/importfiles/FavoritesImportTask.java b/OsmAnd/src/net/osmand/plus/importfiles/FavoritesImportTask.java index b62de4923c..703e920abc 100644 --- a/OsmAnd/src/net/osmand/plus/importfiles/FavoritesImportTask.java +++ b/OsmAnd/src/net/osmand/plus/importfiles/FavoritesImportTask.java @@ -8,6 +8,7 @@ import androidx.fragment.app.FragmentActivity; import net.osmand.GPXUtilities.GPXFile; import net.osmand.data.FavouritePoint; import net.osmand.plus.FavouritesDbHelper; +import net.osmand.plus.OsmandApplication; import net.osmand.plus.R; import net.osmand.plus.base.BaseLoadAsyncTask; @@ -17,7 +18,7 @@ import static net.osmand.plus.importfiles.ImportHelper.asFavourites; import static net.osmand.plus.myplaces.FavoritesActivity.FAV_TAB; import static net.osmand.plus.myplaces.FavoritesActivity.TAB_ID; -class FavoritesImportTask extends BaseLoadAsyncTask { +public class FavoritesImportTask extends BaseLoadAsyncTask { private GPXFile gpxFile; private String fileName; @@ -33,6 +34,12 @@ class FavoritesImportTask extends BaseLoadAsyncTask { @Override protected GPXFile doInBackground(Void... nothing) { + mergeFavorites(app, gpxFile, fileName, forceImportFavourites); + return null; + } + + public static void mergeFavorites(@NonNull OsmandApplication app, @NonNull GPXFile gpxFile, + @NonNull String fileName, boolean forceImportFavourites) { List favourites = asFavourites(app, gpxFile.getPoints(), fileName, forceImportFavourites); FavouritesDbHelper favoritesHelper = app.getFavorites(); checkDuplicateNames(favourites); @@ -42,10 +49,9 @@ class FavoritesImportTask extends BaseLoadAsyncTask { } favoritesHelper.sortAll(); favoritesHelper.saveCurrentPointsIntoFile(); - return null; } - public void checkDuplicateNames(List favourites) { + public static void checkDuplicateNames(List favourites) { for (FavouritePoint fp : favourites) { int number = 1; String index; diff --git a/OsmAnd/src/net/osmand/plus/inapp/InAppPurchaseHelper.java b/OsmAnd/src/net/osmand/plus/inapp/InAppPurchaseHelper.java index 1a36558b87..8a54df1b86 100644 --- a/OsmAnd/src/net/osmand/plus/inapp/InAppPurchaseHelper.java +++ b/OsmAnd/src/net/osmand/plus/inapp/InAppPurchaseHelper.java @@ -10,7 +10,7 @@ import android.util.Log; import net.osmand.AndroidNetworkUtils; import net.osmand.AndroidNetworkUtils.OnRequestResultListener; -import net.osmand.AndroidNetworkUtils.OnRequestsResultListener; +import net.osmand.AndroidNetworkUtils.OnSendRequestsListener; import net.osmand.AndroidNetworkUtils.RequestResponse; import net.osmand.PlatformUtil; import net.osmand.plus.OsmandApplication; @@ -608,9 +608,14 @@ public abstract class InAppPurchaseHelper { addUserInfo(parameters); requests.add(new AndroidNetworkUtils.Request(url, parameters, userOperation, true, true)); } - AndroidNetworkUtils.sendRequestsAsync(ctx, requests, new OnRequestsResultListener() { + AndroidNetworkUtils.sendRequestsAsync(ctx, requests, new OnSendRequestsListener() { + @Override - public void onResult(@NonNull List results) { + public void onRequestSent(@NonNull RequestResponse response) { + } + + @Override + public void onRequestsSent(@NonNull List results) { for (RequestResponse rr : results) { String sku = rr.getRequest().getParameters().get("sku"); PurchaseInfo info = getPurchaseInfo(sku); diff --git a/OsmAnd/src/net/osmand/plus/settings/backend/OsmandSettings.java b/OsmAnd/src/net/osmand/plus/settings/backend/OsmandSettings.java index 1a77de50fb..5b770bafda 100644 --- a/OsmAnd/src/net/osmand/plus/settings/backend/OsmandSettings.java +++ b/OsmAnd/src/net/osmand/plus/settings/backend/OsmandSettings.java @@ -1172,6 +1172,9 @@ public class OsmandSettings { public final OsmandPreference BACKUP_ACCESS_TOKEN = new StringPreference(this, "backup_access_token", "").makeGlobal(); public final OsmandPreference BACKUP_ACCESS_TOKEN_UPDATE_TIME = new StringPreference(this, "backup_access_token_update_time", "").makeGlobal(); + public final OsmandPreference FAVORITES_LAST_UPLOADED_TIME = new LongPreference(this, "favorites_last_uploaded_time", 0L).makeGlobal(); + public final OsmandPreference BACKUP_LAST_UPLOADED_TIME = new LongPreference(this, "backup_last_uploaded_time", 0L).makeGlobal(); + // this value string is synchronized with settings_pref.xml preference name public final OsmandPreference USER_OSM_BUG_NAME = new StringPreference(this, "user_osm_bug_name", "NoName/OsmAnd").makeGlobal().makeShared(); From 40c060af267ed72d4116fed3ab0fc572a4fdc182 Mon Sep 17 00:00:00 2001 From: nazar-kutz Date: Tue, 20 Apr 2021 23:49:03 +0300 Subject: [PATCH 3/8] Remove srtm file of other type after new downloaded --- .../plus/download/DownloadFileHelper.java | 54 +++++++++++++------ .../plus/download/MultipleDownloadItem.java | 2 +- 2 files changed, 38 insertions(+), 18 deletions(-) diff --git a/OsmAnd/src/net/osmand/plus/download/DownloadFileHelper.java b/OsmAnd/src/net/osmand/plus/download/DownloadFileHelper.java index c85d63361a..257e46db00 100644 --- a/OsmAnd/src/net/osmand/plus/download/DownloadFileHelper.java +++ b/OsmAnd/src/net/osmand/plus/download/DownloadFileHelper.java @@ -7,6 +7,7 @@ import net.osmand.osm.io.NetworkUtils; import net.osmand.plus.OsmandApplication; import net.osmand.plus.R; import net.osmand.plus.Version; +import net.osmand.plus.download.IndexItem.DownloadEntry; import net.osmand.plus.helpers.FileNameTranslationHelper; import net.osmand.util.Algorithms; @@ -199,33 +200,35 @@ public class DownloadFileHelper { public boolean isWifiConnected(){ return ctx.getSettings().isWifiConnected(); } - - public boolean downloadFile(IndexItem.DownloadEntry de, IProgress progress, - List toReIndex, DownloadFileShowWarning showWarningCallback, boolean forceWifi) throws InterruptedException { + + public boolean downloadFile(IndexItem.DownloadEntry de, IProgress progress, + List toReIndex, DownloadFileShowWarning showWarningCallback, boolean forceWifi) throws InterruptedException { try { final List downloadInputStreams = new ArrayList(); URL url = new URL(de.urlToDownload); //$NON-NLS-1$ log.info("Url downloading " + de.urlToDownload); downloadInputStreams.add(getInputStreamToDownload(url, forceWifi)); de.fileToDownload = de.targetFile; - if(!de.unzipFolder) { - de.fileToDownload = new File(de.targetFile.getParentFile(), de.targetFile.getName() +".download"); + if (!de.unzipFolder) { + de.fileToDownload = new File(de.targetFile.getParentFile(), de.targetFile.getName() + ".download"); } unzipFile(de, progress, downloadInputStreams); - if(!de.targetFile.getAbsolutePath().equals(de.fileToDownload.getAbsolutePath())){ - boolean successfull = Algorithms.removeAllFiles(de.targetFile); - if (successfull) { + if (!de.targetFile.getAbsolutePath().equals(de.fileToDownload.getAbsolutePath())) { + boolean successful = Algorithms.removeAllFiles(de.targetFile); + if (successful) { ctx.getResourceManager().closeFile(de.targetFile.getName()); } - + boolean renamed = de.fileToDownload.renameTo(de.targetFile); - if(!renamed) { + if (!renamed) { showWarningCallback.showWarning(ctx.getString(R.string.shared_string_io_error) + ": old file can't be deleted"); return false; } } - if (de.type == DownloadActivityType.VOICE_FILE){ + if (de.type == DownloadActivityType.VOICE_FILE) { copyVoiceConfig(de); + } else if (de.type == DownloadActivityType.SRTM_COUNTRY_FILE) { + removePreviousSrtmFile(de); } toReIndex.add(de.targetFile); return true; @@ -238,6 +241,26 @@ public class DownloadFileHelper { } } + private void removePreviousSrtmFile(DownloadEntry entry) { + String meterExt = IndexConstants.BINARY_SRTM_MAP_INDEX_EXT; + String feetExt = IndexConstants.BINARY_SRTM_FEET_MAP_INDEX_EXT; + + String fileName = entry.targetFile.getAbsolutePath(); + if (fileName.endsWith(meterExt)) { + fileName = fileName.replace(meterExt, feetExt); + } else if (fileName.endsWith(feetExt)) { + fileName = fileName.replace(feetExt, meterExt); + } + + File previous = new File(fileName); + if (previous != null && previous.exists()) { + boolean successful = Algorithms.removeAllFiles(previous); + if (successful) { + ctx.getResourceManager().closeFile(previous.getName()); + } + } + } + private void copyVoiceConfig(IndexItem.DownloadEntry de) { File f = ctx.getAppPath("/voice/" + de.baseName + "/_config.p"); if (f.exists()) try { @@ -386,23 +409,20 @@ public class DownloadFileHelper { } return r; } - + @Override public int available() throws IOException { int av = 0; - for(int i = currentRead; i < delegate.length; i++) { + for (int i = currentRead; i < delegate.length; i++) { av += delegate[i].available(); } return av; } - + public int getAndClearReadCount() { int last = count; count = 0; return last; } - - - } } diff --git a/OsmAnd/src/net/osmand/plus/download/MultipleDownloadItem.java b/OsmAnd/src/net/osmand/plus/download/MultipleDownloadItem.java index eaa3b4b92a..a9cfd1e1af 100644 --- a/OsmAnd/src/net/osmand/plus/download/MultipleDownloadItem.java +++ b/OsmAnd/src/net/osmand/plus/download/MultipleDownloadItem.java @@ -139,7 +139,7 @@ public class MultipleDownloadItem extends DownloadItem { if (obj instanceof IndexItem) { return (IndexItem) obj; } else if (obj instanceof SrtmDownloadItem) { - return ((SrtmDownloadItem) obj).getIndexItem(); + return ((SrtmDownloadItem) obj).getDefaultIndexItem(); } return null; } From 396f9354c18c614b12f7158937c2b70cab4aec96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Morais?= Date: Tue, 20 Apr 2021 19:31:21 +0000 Subject: [PATCH 4/8] Translated using Weblate (Portuguese) Currently translated at 100.0% (3717 of 3717 strings) --- OsmAnd/res/values-pt/strings.xml | 34 ++++++++++++++++---------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/OsmAnd/res/values-pt/strings.xml b/OsmAnd/res/values-pt/strings.xml index 9b9f73d7a6..f6e6b35339 100644 --- a/OsmAnd/res/values-pt/strings.xml +++ b/OsmAnd/res/values-pt/strings.xml @@ -371,7 +371,7 @@ Erro de entrada/saída na execução da ação {0}. As informações sobre o objeto não foram carregadas Aberto - Comentário + Comentar Alterar POI Todas as outras etiquetas são preservadas Enviar @@ -920,7 +920,7 @@ Curvas de nível Outros mapas Curvas de nível - Limites + Fronteiras regionais Ocultar a visualização de limites regionais (níveis de administração 5 – 9). Ver desmarcado @@ -1018,7 +1018,7 @@ Focagem automática Foco hiperfocal Profundidade de campo alargada (EDOF) - O foco está definido como infinito + Focar infinito Focagem macro (close-up) A câmara tenta focar continuadamente Reproduzir o som do obturador da câmara @@ -1435,7 +1435,7 @@ Colorir edifícios por tipo Escala de montanhismo (SAC) Camada superior de símbolos de montanhismo - Mostrar rotas para bicicletas + Rotas para bicicletas Edifícios Rotas de troleicarros Por favor, use um nome de categoria que ainda não exista. @@ -1513,7 +1513,7 @@ Edições OSM partilhadas via OsmAnd Ler mais Novidades - Objetos planeados + Elementos com construção planeada Enviar POI OSM adicionado Mapa base mundial (cobrindo o mundo inteiro em baixo nível de ampliação) ausente ou ultrapassado. Por favor, considere descarregá-lo para uma visão global. @@ -1754,7 +1754,7 @@ Marcadores Marcador de mapa É recomendável desativar a renderização de polígono. - Mostrar trilhos de bicicletas de montanha + Trilhos de bicicletas BTT Mostrar polígonos Encontrar estacionamento Situação @@ -1768,7 +1768,7 @@ Estrada bloqueada Selecionar Inverter ponto de partida e destino - Ícones POI + Ícones dos POI Item removido Itens removidos Desfazer tudo @@ -1776,7 +1776,7 @@ Ponto de partida Divisão das gravações Usar divisão das gravações - Duração do recorte + Duração da divisão Limite de tempo máximo para clipes gravados. Macedónio Servo-Croata @@ -2026,7 +2026,7 @@ Espessura das curvas de nível Espessura das curvas de nível Água - Ocultar água + Corpos de água largos Utilizar autoestradas Permitir autoestradas. Artigos da Wikipédia próximos @@ -2127,7 +2127,7 @@ Adquira o OsmAnd Live para desbloquear todas as funcionalidades: atualizações diárias de mapas com descarregamentos ilimitados, todas as extensões pagas e gratuitas, Wikipédia, Wikivoyage e muito mais. Alteração do estilo padrão para aumentar o contraste de caminhos pedestres e ciclovias. Usa cores clássicas do Mapnik. Favorito - Esconder descrição completa + Ocultar descrição completa Mostrar a descrição completa Obrigado pelos seus comentários Procurar rua @@ -2261,7 +2261,7 @@ Adicionar paragem inicial Mover destino para cima e criar destino Mostrar notas fechadas - Mostrar ou ocultar notas do OpenStreetMap no mapa. + Mostrar notas do OpenStreetMap. GPX - adequado para exportar para o JOSM ou outros editores do OSM. OSC - adequado para exportar para o OSM. Ficheiro GPX @@ -3293,7 +3293,7 @@ O nome do ficheiro está vazio Reverter Um botão para centrar o ecrã no ponto de partida. Em seguida, solicitará para definir o destino ou acionar o cálculo da rota. - Mostrar nós da rede de ciclovias + Nós da rede de ciclovias Limpar %1$s\? Diálogo de descarregar mapas Diálogos e notificações @@ -3442,7 +3442,7 @@ Autorização bem sucedida Som do obturador da câmara Usar aplicação do sistema - Divisão de gravação + Dividir gravações Repor configurações originais da extensão Deslocamento mínimo Precisão mínima @@ -3454,7 +3454,7 @@ Memória intermédia Recomendação: uma configuração de 5 metros pode funcionar bem se não precisar capturar detalhes mais refinados do que isso e não quer capturar dados explicitamente enquanto estiver parado. Efeitos colaterais: os períodos em que está parado não são registados em absoluto ou em apenas um ponto cada. Pequenos movimentos (no mundo real, por exemplo de lado, para marcar um possível desvio na sua viagem) podem ser filtrados. O seu ficheiro contém menos informações para pós-processamento e possui estatísticas piores ao filtrar pontos obviamente redundantes no tempo de gravação, mantendo potencialmente os artefactos causados por má receção ou efeitos do chipset GPS. - Este filtro evita que sejam gravados pontos duplicados onde ocorrer muito pouco movimento real, cria uma aparência espacial mais agradável dos trilhos que não são processados posteriormente. + Este filtro evita que sejam gravados pontos duplicados quando houver muito pouco movimento real e cria uma aparência espacial mais agradável dos trilhos que não são processados posteriormente. Observação: se o GPS estava desligado imediatamente antes de uma gravação, o primeiro ponto medido pode ter uma precisão diminuída; portanto, no nosso código, podemos esperar um segundo antes da gravação de um ponto (ou gravar o melhor de três pontos consecutivos, etc.), mas isso ainda não foi implementado. Recomendação: é difícil prever o que será gravado e o que não será, talvez seja melhor desativar este filtro. Efeito colateral: como resultado da filtragem por precisão, os pontos podem estar totalmente ausentes por ex. debaixo de pontes, sob árvores, entre prédios altos ou com certas condições climáticas. @@ -3501,7 +3501,7 @@ Não foi possível importar de \'%1$s\'. Personalize a quantidade de itens em \"Gaveta\", \"Configurar Mapa\" e \"Menu de Contexto\". \n -\nDesative as extensões não utilizados para ocultar todos os seus controlos. %1$s. +\nDesative as extensões não utilizadas para ocultar todos os seus controlos. %1$s. Itens da gaveta, menu de contexto Personalização da interface Gaveta @@ -3704,7 +3704,7 @@ Adicionar a um trilho O ponto adicionado não será visível no mapa, já que o grupo selecionado está escondido, pode encontrá-lo em \"%s\". Mostrar ícones de início e fim - Selecionar espessura da linha do trilho + Espessura da linha do trilho Selecione o intervalo em que as marcas com distância ou tempo no trilho serão mostradas. Selecione a opção de divisão desejada: por tempo ou por distância. Personalizado @@ -4025,7 +4025,7 @@ Não tem compras Se tiver alguma dúvida, contacte-nos em %1$s. Intervalos de tempo e distância - Distância por toque + Distância com 2 dedos Contacte o suporte Por favor siga este link se tiver algum problema com assinaturas. Atualizar todos os mapas para %1$s\? From 2968aad4389f68b9e9467c451d25e15d58bb2536 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Tue, 20 Apr 2021 09:25:47 +0000 Subject: [PATCH 5/8] Translated using Weblate (Danish) Currently translated at 90.9% (3379 of 3717 strings) --- OsmAnd/res/values-da/strings.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/OsmAnd/res/values-da/strings.xml b/OsmAnd/res/values-da/strings.xml index 7a6398afca..4272306e50 100644 --- a/OsmAnd/res/values-da/strings.xml +++ b/OsmAnd/res/values-da/strings.xml @@ -3837,4 +3837,8 @@ Sporet indeholder ikke højdedata. Sporet indeholder ikke hastighedsdata. Vælg en anden type farvelægning. + Udgangsnummer + Meddelelse ved overskridelse + Bruger points + Resultat \ No newline at end of file From f1f7db7842118caf57d0dbf8ae385622f6b86cea Mon Sep 17 00:00:00 2001 From: iman Date: Mon, 19 Apr 2021 21:27:47 +0000 Subject: [PATCH 6/8] Translated using Weblate (Persian) Currently translated at 36.6% (1438 of 3927 strings) --- OsmAnd/res/values-fa/phrases.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/OsmAnd/res/values-fa/phrases.xml b/OsmAnd/res/values-fa/phrases.xml index 7dc383d9c1..63d5c094fa 100644 --- a/OsmAnd/res/values-fa/phrases.xml +++ b/OsmAnd/res/values-fa/phrases.xml @@ -1460,4 +1460,5 @@ بانوان بانوان لباس کار + باغداری گلخانه‌ای \ No newline at end of file From fa17db8756676094b97d90c30288354eab5bc821 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Morais?= Date: Mon, 19 Apr 2021 15:42:21 +0000 Subject: [PATCH 7/8] Translated using Weblate (Portuguese) Currently translated at 100.0% (3927 of 3927 strings) --- OsmAnd/res/values-pt/phrases.xml | 52 ++++++++++++++++---------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/OsmAnd/res/values-pt/phrases.xml b/OsmAnd/res/values-pt/phrases.xml index 294787f8f3..5ae2a5c536 100644 --- a/OsmAnd/res/values-pt/phrases.xml +++ b/OsmAnd/res/values-pt/phrases.xml @@ -411,10 +411,10 @@ Reservatório elevado Comporta de eclusa Ponto de viragem fluvial - Represa;Açude + Represa/açude;Represa;Açude Barragem Moinho de água - Quebra-mar;Molhe + Quebra-mar/molhe;Quebra-mar;Molhe Espigão marítimo Subestação Transformador @@ -565,14 +565,14 @@ Sim Sede de concelho Sede de freguesia - Aldeia;Lugar + Aldeia/lugar;Aldeia;Lugar Moradia isolada Subúrbio Zona de cidade Bairro Localidade Horta comunitária - Quinta;Fazenda + Quinta/fazenda;Quinta;Fazenda Farmácia Hospital Consultório médico @@ -694,7 +694,7 @@ Marco de fronteira Canhão histórico Castelo - Portão da cidade + Portão/arco de cidade Forte Chafariz Ruínas históricas @@ -721,7 +721,7 @@ Tobogã aquático Alojamento Hotel - Pensão;Albergaria;Hospedaria;Estalagem;Residencial + Pensão/albergaria/residenial;Estalagem;Residencial;Albergaria;Hospedaria;Casa de hóspedes Hostel Hotel estrada Abrigo de montanha @@ -890,7 +890,7 @@ Circo Galeria de arte Pista de dança - Discoteca;Danceteria + Discoteca/danceteria;Discoteca;Danceteria Clube de striptease Resort de esqui Resort de praia @@ -912,9 +912,9 @@ Restaurante Comida rápida Bar - Taberna + Taberna/pub/tasca;Taberna;Tasco;Tasca;Pub;Boteco;Buteco;Botequim Praça de alimentação - Bebedouro (água potável para beber) + Bebedouro (água potável) Churrasqueira Máquinas agrícolas Cesteiro @@ -977,8 +977,8 @@ Ponto de boleia solidária de carro Ponto de barcos partilhados Doca - Linha de corte florestal;Atalhada;Linha corta-fogo - Casa de banho;Banheiros + Linha de corte florestal/atalhada;Atalhada;Linha corta-fogo;Linha de corte florestal + Casa de banho;Banheiros;WC Chuveiros públicos Sauna Bordel @@ -1002,7 +1002,7 @@ Cumeeira Glaciar Sumidouro - Queda de água;Cascata;Salto;Catarata + Queda de água/cascata/catarata/salto;Cascata;Salto;Catarata;Queda de água Rio Ribeiro(a) Rápidos @@ -1246,8 +1246,8 @@ Não Sim Não - Vigiado - Não vigiado + Vigiado: sim + Vigiado: não Sim Não Estação seca @@ -1433,7 +1433,7 @@ Centro equestre Área de lazer comum Jardim - Charneca;Mato de vegetação rasteira + Charneca/mato de vegetação rasteira;Charneca;Mato de vegetação rasteira Relvado Pradaria Matagal @@ -1771,7 +1771,7 @@ Brinquedos Gelados Cartões de telemóvel (SIM) - Filial;Sucursal + Filial/sucursal;Filial;Sucursal Memorial de guerra Placa comemorativa Estátua @@ -2242,7 +2242,7 @@ Condição dos degraus: regular Condição dos degraus: irregular Condição dos degraus: acidentada - Moledro;Moledo;Melédro;Mariola + Moledro/mariola;Moledo;Melédro;Mariola;Moledro Desfibrilhador Desfibrilhador: sim Tipo: túmulo de guerra @@ -2360,7 +2360,7 @@ Tipo de fortificação: arandela Tipo de fortificação: vala circular Pa (assentamento fortificado maori) - Quinta histórica;Fazenda histórica + Quinta/fazenda histórica;Quinta histórica;Fazenda histórica Estação ferroviária histórica Eira histórica Forca histórica @@ -2476,7 +2476,7 @@ Comportamental Medicina paliativa Tipo de edifício: pirâmide - Ginásio;Academia desportiva + Ginásio/academia desportiva;Ginásio;Academia desportiva Exercício físico Bilhar Forno microondas: sim @@ -3082,7 +3082,7 @@ Suplementos alimentares Estúdio de fotografia Penhasco - Cativeiro de animais;Refúgio de animais + Refúgio de animais;Cativeiro de animais Cativeiro de animais: cavalos Cativeiro de animais: ovelhas Tipo: cercado @@ -3118,7 +3118,7 @@ Estandes Vendas Vendas: não - Vendas: sim; usados + Vendas: sim, usados Vendas: usados Aluguer Aluguer: não @@ -3203,7 +3203,7 @@ Instituição governamental de transportes Instituição legislativa governamental Canal VHF - Desfiladeiro;Canhão + Desfiladeiro/canhão;Desfiladeiro;Canhão Ravina Área montanhosa Argila @@ -3572,7 +3572,7 @@ Mesa muda-fraldas: sim Mesa muda-fraldas: não Mesa muda-fraldas: limitada - Mesa muda-fraldas; sala + Mesa muda-fraldas: sala Local da mesa muda-fraldas: WC masculino Local da mesa muda-fraldas: WC feminino Local da mesa muda-fraldas: WC unissexo @@ -3810,7 +3810,7 @@ Vibração Quarteirão Município - Caixa livre;Caixa de donativos;Give-box + Caixa livre/de donativos;Give-box;Caixa livre;Caixa de donativos Seta: não Elevador Horário @@ -3917,8 +3917,8 @@ Posição de paragem da biblioteca itinerante Estado da pista: fechada Estado da pista: aberta - Vigiado: não - Vigiado: sim + Supervisionado: não + Supervisionado: sim Nome da pista Salto com esqui Passagem de vida selvagem From 1e2399351453b6fceb250b004996262d214fc64a Mon Sep 17 00:00:00 2001 From: max-klaus Date: Wed, 21 Apr 2021 09:22:49 +0300 Subject: [PATCH 8/8] Added backup test description --- .../plus/development/TestBackupActivity.java | 66 ++++++++++++++++--- 1 file changed, 58 insertions(+), 8 deletions(-) diff --git a/OsmAnd/src/net/osmand/plus/development/TestBackupActivity.java b/OsmAnd/src/net/osmand/plus/development/TestBackupActivity.java index 516e1cc581..a5d0daddf5 100644 --- a/OsmAnd/src/net/osmand/plus/development/TestBackupActivity.java +++ b/OsmAnd/src/net/osmand/plus/development/TestBackupActivity.java @@ -3,12 +3,14 @@ package net.osmand.plus.development; import android.app.Activity; import android.graphics.drawable.Drawable; import android.os.Bundle; +import android.util.Pair; import android.util.TypedValue; import android.view.View; import android.widget.EditText; import android.widget.ProgressBar; import android.widget.TextView; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.widget.Toolbar; @@ -23,6 +25,7 @@ import net.osmand.plus.backup.BackupHelper.BackupInfo; import net.osmand.plus.backup.BackupHelper.OnRegisterUserListener; import net.osmand.plus.backup.BackupTask; import net.osmand.plus.backup.BackupTask.OnBackupListener; +import net.osmand.plus.backup.GpxFileInfo; import net.osmand.plus.backup.PrepareBackupTask; import net.osmand.plus.backup.PrepareBackupTask.OnPrepareBackupListener; import net.osmand.plus.backup.UserFile; @@ -32,11 +35,15 @@ import net.osmand.util.Algorithms; import java.io.File; import java.lang.ref.WeakReference; +import java.text.DateFormat; +import java.util.Date; import java.util.Map; import java.util.Map.Entry; public class TestBackupActivity extends OsmandActionBarActivity { + private static final DateFormat DF = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM); + private OsmandApplication app; private OsmandSettings settings; private BackupHelper backupHelper; @@ -196,7 +203,7 @@ public class TestBackupActivity extends OsmandActionBarActivity { } else if (uploadErrors == null && downloadErrors == null) { description = "No data"; } else { - description = getBackupDescription(uploadErrors, downloadErrors, deleteErrors, error); + description = getBackupErrorsDescription(uploadErrors, downloadErrors, deleteErrors, error); } a.infoView.setText(description); a.infoView.requestFocus(); @@ -224,7 +231,7 @@ public class TestBackupActivity extends OsmandActionBarActivity { } else if (uploadErrors == null && downloadErrors == null) { description = "No data"; } else { - description = getBackupDescription(uploadErrors, downloadErrors, deleteErrors, error); + description = getBackupErrorsDescription(uploadErrors, downloadErrors, deleteErrors, error); } a.infoView.setText(description); a.infoView.requestFocus(); @@ -240,7 +247,7 @@ public class TestBackupActivity extends OsmandActionBarActivity { prepareBackup(); } - private String getBackupDescription(@Nullable Map uploadErrors, @Nullable Map downloadErrors, @Nullable Map deleteErrors, @Nullable String error) { + private String getBackupErrorsDescription(@Nullable Map uploadErrors, @Nullable Map downloadErrors, @Nullable Map deleteErrors, @Nullable String error) { StringBuilder sb = new StringBuilder(); if (!Algorithms.isEmpty(uploadErrors)) { sb.append("--- Upload errors ---").append("\n"); @@ -263,6 +270,48 @@ public class TestBackupActivity extends OsmandActionBarActivity { return sb.length() == 0 ? "OK" : sb.toString(); } + private String getBackupDescription(@NonNull BackupInfo backupInfo) { + StringBuilder sb = new StringBuilder(); + if (!Algorithms.isEmpty(backupInfo.filesToUpload)) { + sb.append("\n").append("--- Upload ---").append("\n"); + for (GpxFileInfo info : backupInfo.filesToUpload) { + sb.append(info.getFileName(true)) + .append(" L: ").append(DF.format(new Date(info.getFileDate()))) + .append(" U: ").append(DF.format(new Date(info.uploadTime))) + .append("\n"); + } + } + if (!Algorithms.isEmpty(backupInfo.filesToDownload)) { + sb.append("\n").append("--- Download ---").append("\n"); + for (UserFile userFile : backupInfo.filesToDownload) { + sb.append(userFile.getName()) + .append(" R: ").append(DF.format(new Date(userFile.getClienttimems()))) + .append("\n"); + } + } + if (!Algorithms.isEmpty(backupInfo.filesToDelete)) { + sb.append("\n").append("--- Delete ---").append("\n"); + for (UserFile userFile : backupInfo.filesToDelete) { + sb.append(userFile.getName()) + .append(" R: ").append(DF.format(new Date(userFile.getClienttimems()))) + .append("\n"); + } + } + if (!Algorithms.isEmpty(backupInfo.filesToMerge)) { + sb.append("\n").append("--- Conflicts ---").append("\n"); + for (Pair localRemote : backupInfo.filesToMerge) { + GpxFileInfo local = localRemote.first; + UserFile remote = localRemote.second; + sb.append(local.getFileName(true)) + .append(" L: ").append(DF.format(new Date(local.getFileDate()))) + .append(" U: ").append(DF.format(new Date(local.uploadTime))) + .append(" R: ").append(DF.format(new Date(remote.getClienttimems()))) + .append("\n"); + } + } + return sb.toString(); + } + private void prepareBackup() { final WeakReference activityRef = new WeakReference<>(this); PrepareBackupTask prepareBackupTask = new PrepareBackupTask(this, new OnPrepareBackupListener() { @@ -271,16 +320,17 @@ public class TestBackupActivity extends OsmandActionBarActivity { TestBackupActivity.this.backupInfo = backupInfo; TestBackupActivity a = activityRef.get(); if (AndroidUtils.isActivityNotDestroyed(a)) { - String description; + String description = "Last uploaded: " + DF.format(new Date(settings.BACKUP_LAST_UPLOADED_TIME.get())) + "\n\n"; if (error != null) { - description = error; + description += error; } else if (backupInfo == null) { - description = "No data"; + description += "No data"; } else { - description = "Files to upload: " + backupInfo.filesToUpload.size() + description += "Files to upload: " + backupInfo.filesToUpload.size() + "\nFiles to download: " + backupInfo.filesToDownload.size() + "\nFiles to delete: " + backupInfo.filesToDelete.size() - + "\nConflicts: " + backupInfo.filesToMerge.size(); + + "\nConflicts: " + backupInfo.filesToMerge.size() + + "\n" + getBackupDescription(backupInfo); } a.infoView.setText(description); a.infoView.requestFocus();