diff --git a/OsmAnd/AndroidManifest.xml b/OsmAnd/AndroidManifest.xml index 5cc90e170d..7ca98ddfc8 100644 --- a/OsmAnd/AndroidManifest.xml +++ b/OsmAnd/AndroidManifest.xml @@ -1026,6 +1026,7 @@ + diff --git a/OsmAnd/res/layout/test_backup_layout.xml b/OsmAnd/res/layout/test_backup_layout.xml new file mode 100644 index 0000000000..216820a9bf --- /dev/null +++ b/OsmAnd/res/layout/test_backup_layout.xml @@ -0,0 +1,151 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/OsmAnd/res/xml/development_settings.xml b/OsmAnd/res/xml/development_settings.xml index 231bd469bc..75079dd7ed 100644 --- a/OsmAnd/res/xml/development_settings.xml +++ b/OsmAnd/res/xml/development_settings.xml @@ -70,6 +70,13 @@ android:summary="@string/play_commands_of_currently_selected_voice" android:title="@string/test_voice_prompts" /> + + getAdditionalParams(@NonNull File file); + void onFileUploadProgress(@NonNull File file, int percent); + void onFilesUploadDone(@NonNull Map errors); + } + public static class RequestResponse { private Request request; private String response; @@ -156,7 +166,7 @@ public class AndroidNetworkUtils { String params = null; if (parameters != null && parameters.size() > 0) { StringBuilder sb = new StringBuilder(); - for (Map.Entry entry : parameters.entrySet()) { + for (Entry entry : parameters.entrySet()) { if (sb.length() > 0) { sb.append("&"); } @@ -296,16 +306,18 @@ public class AndroidNetworkUtils { private static final String BOUNDARY = "CowMooCowMooCowCowCow"; - public static String uploadFile(String urlText, File file, boolean gzip, Map additionalParams) throws IOException { - return uploadFile(urlText, new FileInputStream(file), file.getName(), gzip, additionalParams); + 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); } - public static String uploadFile(String urlText, InputStream inputStream, String fileName, boolean gzip, Map additionalParams) { + public static String uploadFile(@NonNull String urlText, @NonNull InputStream inputStream, @NonNull String fileName, boolean gzip, + Map additionalParams, @Nullable Map headers) { URL url; try { boolean firstPrm = !urlText.contains("?"); StringBuilder sb = new StringBuilder(urlText); - for (Map.Entry entry : additionalParams.entrySet()) { + for (Entry entry : additionalParams.entrySet()) { sb.append(firstPrm ? "?" : "&").append(entry.getKey()).append("=").append(URLEncoder.encode(entry.getValue(), "UTF-8")); firstPrm = false; } @@ -320,6 +332,11 @@ public class AndroidNetworkUtils { conn.setRequestMethod("POST"); conn.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + BOUNDARY); conn.setRequestProperty("User-Agent", "OsmAnd"); + if (headers != null) { + for (Entry header : headers.entrySet()) { + conn.setRequestProperty(header.getKey(), header.getValue()); + } + } OutputStream ous = conn.getOutputStream(); ous.write(("--" + BOUNDARY + "\r\n").getBytes()); @@ -376,6 +393,58 @@ public class AndroidNetworkUtils { } } + public static void uploadFilesAsync(final @NonNull String url, + final @NonNull List files, + final boolean gzip, + final @NonNull Map parameters, + final @Nullable Map headers, + final OnFilesUploadCallback callback) { + + new AsyncTask>() { + + @Override + @NonNull + protected Map doInBackground(Void... v) { + Map errors = new HashMap<>(); + for (File file : files) { + publishProgress(file, 0); + try { + Map params = new HashMap<>(parameters); + if (callback != null) { + Map additionalParams = callback.getAdditionalParams(file); + if (additionalParams != null) { + params.putAll(additionalParams); + } + } + String res = uploadFile(url, file, gzip, params, headers); + if (res != null) { + errors.put(file, res); + } + } catch (Exception e) { + errors.put(file, e.getMessage()); + } + publishProgress(file, 100); + } + return errors; + } + + @Override + protected void onProgressUpdate(Object... objects) { + if (callback != null) { + callback.onFileUploadProgress((File) objects[0], (Integer) objects[1]); + } + } + + @Override + protected void onPostExecute(@NonNull Map errors) { + if (callback != null) { + callback.onFilesUploadDone(errors); + } + } + + }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void) null); + } + private static void showToast(OsmandApplication ctx, String message) { ctx.showToastMessage(message); } diff --git a/OsmAnd/src/net/osmand/plus/AnalyticsHelper.java b/OsmAnd/src/net/osmand/plus/AnalyticsHelper.java index 2fd12a11ac..e8d04254c3 100644 --- a/OsmAnd/src/net/osmand/plus/AnalyticsHelper.java +++ b/OsmAnd/src/net/osmand/plus/AnalyticsHelper.java @@ -184,7 +184,7 @@ 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); + String res = AndroidNetworkUtils.uploadFile(ANALYTICS_UPLOAD_URL, inputStream, ANALYTICS_FILE_NAME, true, additionalData, null); if (res != null) { return; } diff --git a/OsmAnd/src/net/osmand/plus/development/DevelopmentSettingsFragment.java b/OsmAnd/src/net/osmand/plus/development/DevelopmentSettingsFragment.java index d34a306b79..9ffe2d12bf 100644 --- a/OsmAnd/src/net/osmand/plus/development/DevelopmentSettingsFragment.java +++ b/OsmAnd/src/net/osmand/plus/development/DevelopmentSettingsFragment.java @@ -48,6 +48,7 @@ public class DevelopmentSettingsFragment extends BaseSettingsFragment { setupSimulateInitialStartupPref(); setupShouldShowFreeVersionBannerPref(); setupTestVoiceCommandsPref(); + setupTestBackupsPref(); setupLogcatBufferPref(); Preference info = findPreference("info"); @@ -117,6 +118,12 @@ public class DevelopmentSettingsFragment extends BaseSettingsFragment { testVoiceCommands.setIconSpaceReserved(false); } + private void setupTestBackupsPref() { + Preference testBackups = findPreference("test_backup"); + testBackups.setIntent(new Intent(getActivity(), TestBackupActivity.class)); + testBackups.setIconSpaceReserved(false); + } + private void setupLogcatBufferPref() { Preference logcatBuffer = findPreference("logcat_buffer"); logcatBuffer.setIntent(new Intent(getActivity(), LogcatActivity.class)); diff --git a/OsmAnd/src/net/osmand/plus/development/TestBackupActivity.java b/OsmAnd/src/net/osmand/plus/development/TestBackupActivity.java new file mode 100644 index 0000000000..85bc84cd95 --- /dev/null +++ b/OsmAnd/src/net/osmand/plus/development/TestBackupActivity.java @@ -0,0 +1,400 @@ +package net.osmand.plus.development; + +import android.app.Activity; +import android.app.ProgressDialog; +import android.graphics.drawable.Drawable; +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.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.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.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class TestBackupActivity extends OsmandActionBarActivity { + + private static final String TEST_ORDER_ID = "460000687003939"; + + private OsmandApplication app; + private OsmandSettings settings; + + private ProgressBar progressBar; + private View buttonRegister; + private View buttonVerify; + private View buttonBackup; + private View buttonRestore; + private EditText emailEditText; + private EditText tokenEditText; + private TextView infoView; + + public interface OnResultListener { + void onResult(boolean success, @Nullable String result); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + app = getMyApplication(); + settings = app.getSettings(); + final WeakReference activityRef = new WeakReference<>(this); + + boolean nightMode = !app.getSettings().isLightContent(); + int themeId = nightMode ? R.style.OsmandDarkTheme_NoActionbar : R.style.OsmandLightTheme_NoActionbar; + setTheme(themeId); + super.onCreate(savedInstanceState); + setContentView(R.layout.test_backup_layout); + Toolbar tb = findViewById(R.id.toolbar); + tb.setTitle("Backup test"); + + tb.setClickable(true); + Drawable icBack = ((OsmandApplication) getApplication()).getUIUtilities().getIcon(AndroidUtils.getNavigationIconResId(app)); + tb.setNavigationIcon(icBack); + tb.setNavigationContentDescription(R.string.access_shared_string_navigate_up); + tb.setBackgroundColor(getResources().getColor(resolveResourceId(this, R.attr.pstsTabBackground))); + tb.setTitleTextColor(getResources().getColor(resolveResourceId(this, R.attr.pstsTextColor))); + tb.setNavigationOnClickListener(new View.OnClickListener() { + @Override + public void onClick(final View v) { + finish(); + } + }); + + 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"); + 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"); + + tokenEditText = findViewById(R.id.edit_token); + infoView = findViewById(R.id.text_info); + progressBar = findViewById(R.id.progress_bar); + + buttonVerify.setEnabled(false); + emailEditText = findViewById(R.id.edit_email); + String email = settings.BACKUP_USER_EMAIL.get(); + if (!Algorithms.isEmpty(email)) { + emailEditText.setText(email); + } + buttonRegister.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + String email = emailEditText.getText().toString(); + if (isEmailValid(email)) { + buttonRegister.setEnabled(false); + settings.BACKUP_USER_EMAIL.set(email); + progressBar.setVisibility(View.VISIBLE); + registerUser(email, 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.buttonRegister.setEnabled(!success); + a.buttonVerify.setEnabled(success); + a.tokenEditText.requestFocus(); + } + } + }); + } else { + emailEditText.requestFocus(); + emailEditText.setError(getString(R.string.osm_live_enter_email)); + } + } + }); + buttonVerify.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + String token = tokenEditText.getText().toString(); + if (isTokenValid(token)) { + buttonVerify.setEnabled(false); + progressBar.setVisibility(View.VISIBLE); + registerDevice(token, 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.buttonVerify.setEnabled(!success); + a.loadBackupInfo(); + } + } + }); + } else { + tokenEditText.requestFocus(); + tokenEditText.setError("Token is not valid"); + buttonVerify.setEnabled(true); + } + } + }); + buttonBackup.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + uploadFiles(); + } + }); + 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() { + //{"status":"ok"} + Map params = new HashMap<>(); + params.put("deviceid", getDeviceId()); + params.put("accessToken", getAccessToken()); + Map headers = new HashMap<>(); + headers.put("Accept-Encoding", "deflate, gzip"); + final List files = new ArrayList<>(); + + final ProgressImplementation progress = ProgressImplementation.createProgressDialog(this, + "Uploading files to server", "Files count: " + files.size(), ProgressDialog.STYLE_HORIZONTAL); + + File favoritesFile = app.getFavorites().getExternalFile(); + files.add(favoritesFile); + + AndroidNetworkUtils.uploadFilesAsync("https://osmand.net/userdata/upload-file", files, true, params, headers, new OnFilesUploadCallback() { + @Nullable + @Override + public Map getAdditionalParams(@NonNull File file) { + Map additionaParams = new HashMap<>(); + additionaParams.put("name", file.getName()); + additionaParams.put("type", Algorithms.getFileExtension(file)); + return additionaParams; + } + + @Override + public void onFileUploadProgress(@NonNull File file, int percent) { + if (percent < 100) { + progress.startTask(file.getName(), percent); + } else { + progress.finishTask(); + } + } + + @Override + public void onFilesUploadDone(@NonNull Map errors) { + app.runInUIThread(new Runnable() { + @Override + public void run() { + try { + if (progress.getDialog().isShowing()) { + progress.getDialog().dismiss(); + } + } catch (Exception e) { + //ignored + } + } + }, 300); + app.showToastMessage("Uploaded " + (files.size() - errors.size() + " files" + + (errors.size() > 0 ? ". Errors: " + errors.size() : ""))); + loadBackupInfo(); + } + }); + } + + 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() { + @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) { + } + } + if (listener != null) { + listener.onResult(success, resultString.toString()); + } + } + }); + } + + private boolean isTokenValid(String token) { + return token.matches("[0-9]+"); + } + + private int resolveResourceId(final Activity activity, final int attr) { + final TypedValue typedvalueattr = new TypedValue(); + activity.getTheme().resolveAttribute(attr, typedvalueattr, true); + return typedvalueattr.resourceId; + } +} diff --git a/OsmAnd/src/net/osmand/plus/settings/backend/OsmandSettings.java b/OsmAnd/src/net/osmand/plus/settings/backend/OsmandSettings.java index 501d84d608..1a77de50fb 100644 --- a/OsmAnd/src/net/osmand/plus/settings/backend/OsmandSettings.java +++ b/OsmAnd/src/net/osmand/plus/settings/backend/OsmandSettings.java @@ -1165,6 +1165,13 @@ public class OsmandSettings { public final OsmandPreference DISCOUNT_TOTAL_SHOW = new IntPreference(this, "discount_total_show", 0).makeGlobal(); public final OsmandPreference DISCOUNT_SHOW_DATETIME_MS = new LongPreference(this, "show_discount_datetime_ms", 0).makeGlobal(); + public final OsmandPreference BACKUP_USER_EMAIL = new StringPreference(this, "backup_user_email", "").makeGlobal(); + public final OsmandPreference BACKUP_USER_ID = new StringPreference(this, "backup_user_id", "").makeGlobal(); + public final OsmandPreference BACKUP_DEVICE_ID = new StringPreference(this, "backup_device_id", "").makeGlobal(); + public final OsmandPreference BACKUP_NATIVE_DEVICE_ID = new StringPreference(this, "backup_native_device_id", "").makeGlobal(); + 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(); + // 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(); diff --git a/build.gradle b/build.gradle index 48bcc21248..9c0feb1c3d 100644 --- a/build.gradle +++ b/build.gradle @@ -10,7 +10,7 @@ buildscript { } dependencies { //classpath 'com.android.tools.build:gradle:2.+' - classpath 'com.android.tools.build:gradle:4.1.2' + classpath 'com.android.tools.build:gradle:4.1.3' classpath 'com.google.gms:google-services:3.0.0' classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"