diff --git a/.gitignore b/.gitignore index 5798e73f27..33e746a3d6 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,10 @@ OsmAndCore_*.aar .project out/ +# Huawei +agconnect-services.json +OsmAndHms.jks + # Android Studio /.idea *.iml diff --git a/OsmAnd-api/src/net/osmand/aidlapi/IOsmAndAidlInterface.aidl b/OsmAnd-api/src/net/osmand/aidlapi/IOsmAndAidlInterface.aidl index 3edd8c94da..72a3035b60 100644 --- a/OsmAnd-api/src/net/osmand/aidlapi/IOsmAndAidlInterface.aidl +++ b/OsmAnd-api/src/net/osmand/aidlapi/IOsmAndAidlInterface.aidl @@ -20,6 +20,8 @@ import net.osmand.aidlapi.mapmarker.UpdateMapMarkerParams; import net.osmand.aidlapi.calculateroute.CalculateRouteParams; +import net.osmand.aidlapi.profile.ExportProfileParams; + import net.osmand.aidlapi.gpx.ImportGpxParams; import net.osmand.aidlapi.gpx.ShowGpxParams; import net.osmand.aidlapi.gpx.StartGpxRecordingParams; @@ -103,6 +105,8 @@ import net.osmand.aidlapi.events.AKeyEventsParams; import net.osmand.aidlapi.info.AppInfoParams; +import net.osmand.aidlapi.profile.ExportProfileParams; + // NOTE: Add new methods at the end of file!!! interface IOsmAndAidlInterface { @@ -867,4 +871,6 @@ interface IOsmAndAidlInterface { AppInfoParams getAppInfo(); boolean setMapMargins(in MapMarginsParams params); + + boolean exportProfile(in ExportProfileParams params); } \ No newline at end of file diff --git a/OsmAnd-api/src/net/osmand/aidlapi/OsmAndCustomizationConstants.java b/OsmAnd-api/src/net/osmand/aidlapi/OsmAndCustomizationConstants.java index 2e1543321a..af48552bc4 100644 --- a/OsmAnd-api/src/net/osmand/aidlapi/OsmAndCustomizationConstants.java +++ b/OsmAnd-api/src/net/osmand/aidlapi/OsmAndCustomizationConstants.java @@ -4,6 +4,8 @@ public interface OsmAndCustomizationConstants { // Navigation Drawer: String DRAWER_ITEM_ID_SCHEME = "drawer.action."; + String DRAWER_SWITCH_PROFILE_ID = DRAWER_ITEM_ID_SCHEME + "switch_profile"; + String DRAWER_CONFIGURE_PROFILE_ID = DRAWER_ITEM_ID_SCHEME + "configure_profile"; String DRAWER_DASHBOARD_ID = DRAWER_ITEM_ID_SCHEME + "dashboard"; String DRAWER_MAP_MARKERS_ID = DRAWER_ITEM_ID_SCHEME + "map_markers"; String DRAWER_MY_PLACES_ID = DRAWER_ITEM_ID_SCHEME + "my_places"; diff --git a/OsmAnd-api/src/net/osmand/aidlapi/copyfile/CopyFileParams.java b/OsmAnd-api/src/net/osmand/aidlapi/copyfile/CopyFileParams.java index 1118a17f5c..ad122f60c6 100644 --- a/OsmAnd-api/src/net/osmand/aidlapi/copyfile/CopyFileParams.java +++ b/OsmAnd-api/src/net/osmand/aidlapi/copyfile/CopyFileParams.java @@ -9,12 +9,21 @@ import net.osmand.aidlapi.AidlParams; public class CopyFileParams extends AidlParams { + public static final String DESTINATION_DIR_KEY = "destinationDir"; + public static final String FILE_NAME_KEY = "fileName"; + public static final String FILE_PART_DATA_KEY = "filePartData"; + public static final String START_TIME_KEY = "startTime"; + public static final String DONE_KEY = "done"; + private String destinationDir; private String fileName; private byte[] filePartData; private long startTime; private boolean done; - public CopyFileParams(@NonNull String fileName, @NonNull byte[] filePartData, long startTime, boolean done) { + public CopyFileParams(@NonNull String destinationDir, @NonNull String fileName, @NonNull byte[] filePartData, + long startTime, boolean done) { + + this.destinationDir = destinationDir; this.fileName = fileName; this.filePartData = filePartData; this.startTime = startTime; @@ -37,6 +46,10 @@ public class CopyFileParams extends AidlParams { } }; + public String getDestinationDir() { + return destinationDir; + } + public String getFileName() { return fileName; } @@ -55,23 +68,26 @@ public class CopyFileParams extends AidlParams { @Override public void writeToBundle(Bundle bundle) { - bundle.putString("fileName", fileName); - bundle.putByteArray("filePartData", filePartData); - bundle.putLong("startTime", startTime); - bundle.putBoolean("done", done); + bundle.putString(DESTINATION_DIR_KEY, destinationDir); + bundle.putString(FILE_NAME_KEY, fileName); + bundle.putByteArray(FILE_PART_DATA_KEY, filePartData); + bundle.putLong(START_TIME_KEY, startTime); + bundle.putBoolean(DONE_KEY, done); } @Override protected void readFromBundle(Bundle bundle) { - fileName = bundle.getString("fileName"); - filePartData = bundle.getByteArray("filePartData"); - startTime = bundle.getLong("startTime"); - done = bundle.getBoolean("done"); + destinationDir = bundle.getString(DESTINATION_DIR_KEY); + fileName = bundle.getString(FILE_NAME_KEY); + filePartData = bundle.getByteArray(FILE_PART_DATA_KEY); + startTime = bundle.getLong(START_TIME_KEY); + done = bundle.getBoolean(DONE_KEY); } @Override public String toString() { return "CopyFileParams {" + + " destinationDir=" + destinationDir + " fileName=" + fileName + ", filePartData size=" + filePartData.length + ", startTime=" + startTime + diff --git a/OsmAnd-api/src/net/osmand/aidlapi/customization/ProfileSettingsParams.java b/OsmAnd-api/src/net/osmand/aidlapi/customization/ProfileSettingsParams.java index 36959ef776..00c68851e7 100644 --- a/OsmAnd-api/src/net/osmand/aidlapi/customization/ProfileSettingsParams.java +++ b/OsmAnd-api/src/net/osmand/aidlapi/customization/ProfileSettingsParams.java @@ -5,15 +5,31 @@ import android.os.Bundle; import android.os.Parcel; import net.osmand.aidlapi.AidlParams; +import net.osmand.aidlapi.profile.AExportSettingsType; + +import java.util.ArrayList; + +import static net.osmand.aidlapi.profile.ExportProfileParams.SETTINGS_TYPE_KEY; public class ProfileSettingsParams extends AidlParams { + public static final String VERSION_KEY = "version"; + public static final String REPLACE_KEY = "replace"; + public static final String LATEST_CHANGES_KEY = "latestChanges"; + public static final String PROFILE_SETTINGS_URI_KEY = "profileSettingsUri"; private Uri profileSettingsUri; private String latestChanges; private int version; + private ArrayList settingsTypeKeyList = new ArrayList<>(); + boolean replace; - public ProfileSettingsParams(Uri profileSettingsUri, String latestChanges, int version) { + public ProfileSettingsParams(Uri profileSettingsUri, ArrayList settingsTypeList, boolean replace, + String latestChanges, int version) { this.profileSettingsUri = profileSettingsUri; + for (AExportSettingsType settingsType : settingsTypeList) { + settingsTypeKeyList.add(settingsType.name()); + } + this.replace = replace; this.latestChanges = latestChanges; this.version = version; } @@ -46,17 +62,29 @@ public class ProfileSettingsParams extends AidlParams { return profileSettingsUri; } + public ArrayList getSettingsTypeKeys() { + return settingsTypeKeyList; + } + + public boolean isReplace() { + return replace; + } + @Override public void writeToBundle(Bundle bundle) { - bundle.putInt("version", version); - bundle.putString("latestChanges", latestChanges); - bundle.putParcelable("profileSettingsUri", profileSettingsUri); + bundle.putInt(VERSION_KEY, version); + bundle.putString(LATEST_CHANGES_KEY, latestChanges); + bundle.putParcelable(PROFILE_SETTINGS_URI_KEY, profileSettingsUri); + bundle.putStringArrayList(SETTINGS_TYPE_KEY, settingsTypeKeyList); + bundle.putBoolean(REPLACE_KEY, replace); } @Override protected void readFromBundle(Bundle bundle) { - version = bundle.getInt("version"); - latestChanges = bundle.getString("latestChanges"); - profileSettingsUri = bundle.getParcelable("profileSettingsUri"); + version = bundle.getInt(VERSION_KEY); + latestChanges = bundle.getString(LATEST_CHANGES_KEY); + profileSettingsUri = bundle.getParcelable(PROFILE_SETTINGS_URI_KEY); + settingsTypeKeyList = bundle.getStringArrayList(SETTINGS_TYPE_KEY); + replace = bundle.getBoolean(REPLACE_KEY); } } \ No newline at end of file diff --git a/OsmAnd-api/src/net/osmand/aidlapi/profile/AExportSettingsType.aidl b/OsmAnd-api/src/net/osmand/aidlapi/profile/AExportSettingsType.aidl new file mode 100644 index 0000000000..99c59bba2a --- /dev/null +++ b/OsmAnd-api/src/net/osmand/aidlapi/profile/AExportSettingsType.aidl @@ -0,0 +1,3 @@ +package net.osmand.aidlapi.profile; + +parcelable AExportSettingsType; \ No newline at end of file diff --git a/OsmAnd-api/src/net/osmand/aidlapi/profile/AExportSettingsType.java b/OsmAnd-api/src/net/osmand/aidlapi/profile/AExportSettingsType.java new file mode 100644 index 0000000000..23c0189615 --- /dev/null +++ b/OsmAnd-api/src/net/osmand/aidlapi/profile/AExportSettingsType.java @@ -0,0 +1,11 @@ +package net.osmand.aidlapi.profile; + +public enum AExportSettingsType { + PROFILE, + QUICK_ACTIONS, + POI_TYPES, + MAP_SOURCES, + CUSTOM_RENDER_STYLE, + CUSTOM_ROUTING, + AVOID_ROADS; +} diff --git a/OsmAnd-api/src/net/osmand/aidlapi/profile/ExportProfileParams.aidl b/OsmAnd-api/src/net/osmand/aidlapi/profile/ExportProfileParams.aidl new file mode 100644 index 0000000000..0dfefce6be --- /dev/null +++ b/OsmAnd-api/src/net/osmand/aidlapi/profile/ExportProfileParams.aidl @@ -0,0 +1,3 @@ +package net.osmand.aidlapi.profile; + +parcelable ExportProfileParams; \ No newline at end of file diff --git a/OsmAnd-api/src/net/osmand/aidlapi/profile/ExportProfileParams.java b/OsmAnd-api/src/net/osmand/aidlapi/profile/ExportProfileParams.java new file mode 100644 index 0000000000..931f52eeb8 --- /dev/null +++ b/OsmAnd-api/src/net/osmand/aidlapi/profile/ExportProfileParams.java @@ -0,0 +1,61 @@ +package net.osmand.aidlapi.profile; + +import android.os.Bundle; +import android.os.Parcel; + +import net.osmand.aidlapi.AidlParams; + +import java.util.ArrayList; +import java.util.List; + +public class ExportProfileParams extends AidlParams { + + public static final String PROFILE_KEY = "profile"; + public static final String SETTINGS_TYPE_KEY = "settings_type"; + private String profile; + private ArrayList settingsTypeKeyList = new ArrayList<>(); + + public ExportProfileParams(String profile, ArrayList settingsTypeList) { + + this.profile = profile; + for (AExportSettingsType settingsType : settingsTypeList) { + settingsTypeKeyList.add(settingsType.name()); + } + } + + public ExportProfileParams(Parcel in) { + readFromParcel(in); + } + + public static final Creator CREATOR = new Creator() { + @Override + public ExportProfileParams createFromParcel(Parcel in) { + return new ExportProfileParams(in); + } + + @Override + public ExportProfileParams[] newArray(int size) { + return new ExportProfileParams[size]; + } + }; + + public String getProfile() { + return profile; + } + + public List getSettingsTypeKeys() { + return settingsTypeKeyList; + } + + @Override + public void writeToBundle(Bundle bundle) { + bundle.putString(PROFILE_KEY, profile); + bundle.putStringArrayList(SETTINGS_TYPE_KEY, settingsTypeKeyList); + } + + @Override + protected void readFromBundle(Bundle bundle) { + profile = bundle.getString(PROFILE_KEY); + settingsTypeKeyList = bundle.getStringArrayList(SETTINGS_TYPE_KEY); + } +} \ No newline at end of file diff --git a/OsmAnd-java/src/main/java/net/osmand/router/RoutePlannerFrontEnd.java b/OsmAnd-java/src/main/java/net/osmand/router/RoutePlannerFrontEnd.java index 54464afc32..bff7db3a6d 100644 --- a/OsmAnd-java/src/main/java/net/osmand/router/RoutePlannerFrontEnd.java +++ b/OsmAnd-java/src/main/java/net/osmand/router/RoutePlannerFrontEnd.java @@ -739,7 +739,7 @@ public class RoutePlannerFrontEnd { res = searchRouteImpl(ctx, points, routeDirection); } if (ctx.calculationProgress != null) { - ctx.calculationProgress.timeToCalculate += (System.nanoTime() - timeToCalculate); + ctx.calculationProgress.timeToCalculate = (System.nanoTime() - timeToCalculate); } BinaryRoutePlanner.printDebugMemoryInformation(ctx); if (res != null) { diff --git a/OsmAnd-telegram/AndroidManifest.xml b/OsmAnd-telegram/AndroidManifest.xml index 7b2a96c236..73e2e856ca 100644 --- a/OsmAnd-telegram/AndroidManifest.xml +++ b/OsmAnd-telegram/AndroidManifest.xml @@ -20,7 +20,7 @@ android:screenOrientation="unspecified" android:supportsRtl="true" android:theme="@style/AppTheme"> - + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/OsmAnd-telegram/res/layout/fragement_settings_dialog.xml b/OsmAnd-telegram/res/layout/fragement_settings_dialog.xml index 928ad6f319..1c5738a313 100644 --- a/OsmAnd-telegram/res/layout/fragement_settings_dialog.xml +++ b/OsmAnd-telegram/res/layout/fragement_settings_dialog.xml @@ -447,6 +447,50 @@ + + + + + + + + + + + + + + diff --git a/OsmAnd-telegram/res/layout/item_description_long.xml b/OsmAnd-telegram/res/layout/item_description_long.xml new file mode 100644 index 0000000000..6face5220f --- /dev/null +++ b/OsmAnd-telegram/res/layout/item_description_long.xml @@ -0,0 +1,18 @@ + + diff --git a/OsmAnd-telegram/res/values-de/strings.xml b/OsmAnd-telegram/res/values-de/strings.xml index 8c12570da5..0767c598d8 100644 --- a/OsmAnd-telegram/res/values-de/strings.xml +++ b/OsmAnd-telegram/res/values-de/strings.xml @@ -267,4 +267,8 @@ Letzte Antwort: vor %1$s vor %1$s ERR + Export + Logcat-Puffer + Protokolle der Anwendung einsehen und freigeben + Bericht senden \ No newline at end of file diff --git a/OsmAnd-telegram/res/values-et/strings.xml b/OsmAnd-telegram/res/values-et/strings.xml index 126490e593..0ddb7950a7 100644 --- a/OsmAnd-telegram/res/values-et/strings.xml +++ b/OsmAnd-telegram/res/values-et/strings.xml @@ -267,4 +267,8 @@ Viimane vastus: %1$s tagasi %1$s tagasi ERR + Ekspordi + Logcati puhver + Vaata ja jaga rakenduse detailseid logisid + Saada ettekanne \ No newline at end of file diff --git a/OsmAnd-telegram/res/values-fr/strings.xml b/OsmAnd-telegram/res/values-fr/strings.xml index 4397948508..4b9d45edfb 100644 --- a/OsmAnd-telegram/res/values-fr/strings.xml +++ b/OsmAnd-telegram/res/values-fr/strings.xml @@ -267,4 +267,8 @@ Définissez l\'heure à laquelle les contacts et groupes sélectionnés verront votre position en temps réel. OsmAnd connect depuis + Buffer Logcat + Vérifier et partager les logs détaillés de l\'application + Exporter + Envoyer le rapport \ No newline at end of file diff --git a/OsmAnd-telegram/res/values-he/strings.xml b/OsmAnd-telegram/res/values-he/strings.xml index 99c18d4cb2..65da2f52a1 100644 --- a/OsmAnd-telegram/res/values-he/strings.xml +++ b/OsmAnd-telegram/res/values-he/strings.xml @@ -268,4 +268,8 @@ תגובה אחרונה: לפני %1$s לפני %1$s שגיאה + ייצוא + מכלא Logcat + בדיקה ושיתוף יומני תיעוד מפורטים של היישומים + שליחת דיווח \ No newline at end of file diff --git a/OsmAnd-telegram/res/values-hu/strings.xml b/OsmAnd-telegram/res/values-hu/strings.xml index 8c257bb040..95d655b5cb 100644 --- a/OsmAnd-telegram/res/values-hu/strings.xml +++ b/OsmAnd-telegram/res/values-hu/strings.xml @@ -268,4 +268,8 @@ Utolsó válasz: %1$s Ennyivel ezelőtt: %1$s HIBA + Exportálás + Logcat-puffer (hibanapló) + Az alkalmazás részletes naplóinak ellenőrzése és megosztása + Jelentés küldése \ No newline at end of file diff --git a/OsmAnd-telegram/res/values-pt-rBR/strings.xml b/OsmAnd-telegram/res/values-pt-rBR/strings.xml index c1aa3b8e55..d792f9748f 100644 --- a/OsmAnd-telegram/res/values-pt-rBR/strings.xml +++ b/OsmAnd-telegram/res/values-pt-rBR/strings.xml @@ -267,4 +267,8 @@ Última resposta: %1$s atrás %1$s atrás ERR + Exportar + Buffer de Logcat + Verifique e compartilhe registros detalhados do aplicativo + Enviar o relatório \ No newline at end of file diff --git a/OsmAnd-telegram/res/values-pt/strings.xml b/OsmAnd-telegram/res/values-pt/strings.xml index 6076bfee91..21dfc6b4e9 100644 --- a/OsmAnd-telegram/res/values-pt/strings.xml +++ b/OsmAnd-telegram/res/values-pt/strings.xml @@ -108,7 +108,7 @@ Ainda não encontrado Reenvie o local Última localização disponível - Status de compartilhamento + Estado de compartilhamento Compartilhamento: %1$s Ativado Sem conexão GPS diff --git a/OsmAnd-telegram/res/values-ru/strings.xml b/OsmAnd-telegram/res/values-ru/strings.xml index daff0e950b..e12c0245b5 100644 --- a/OsmAnd-telegram/res/values-ru/strings.xml +++ b/OsmAnd-telegram/res/values-ru/strings.xml @@ -75,7 +75,7 @@ По расстоянию По имени По группе - Сортировать + Сортировка Сортировать по Отстановить все Выход @@ -267,4 +267,8 @@ Последнее обновление от Telegram: %1$s назад Последний ответ: %1$s Последнее обновление от Telegram: %1$s + Экспорт + Буфер Logcat + Проверьте и поделитесь подробными журналами приложения + Отправить отчёт \ No newline at end of file diff --git a/OsmAnd-telegram/res/values-sc/strings.xml b/OsmAnd-telegram/res/values-sc/strings.xml index 9846061b82..82a4e8aade 100644 --- a/OsmAnd-telegram/res/values-sc/strings.xml +++ b/OsmAnd-telegram/res/values-sc/strings.xml @@ -268,4 +268,8 @@ Ùrtima risposta: %1$s a como %1$s a como ERR + Esporta + Buffer de Logcat + Verìfica e cumpartzi sos registros de s\'aplicatzione fatos a sa minuda + Imbia resumu \ No newline at end of file diff --git a/OsmAnd-telegram/res/values-tr/strings.xml b/OsmAnd-telegram/res/values-tr/strings.xml index 086306b609..87ba56717d 100644 --- a/OsmAnd-telegram/res/values-tr/strings.xml +++ b/OsmAnd-telegram/res/values-tr/strings.xml @@ -267,4 +267,8 @@ Son cevap: %1$s önce %1$s önce HATA + Dışa aktar + Logcat tamponu + Uygulamanın ayrıntılı günlük kayıtlarına göz atın ve paylaşın + Rapor gönder \ No newline at end of file diff --git a/OsmAnd-telegram/res/values-uk/strings.xml b/OsmAnd-telegram/res/values-uk/strings.xml index 1ff2bebb09..e370c05360 100644 --- a/OsmAnd-telegram/res/values-uk/strings.xml +++ b/OsmAnd-telegram/res/values-uk/strings.xml @@ -118,7 +118,7 @@ Налаштування Застосунок не має дозволу до отримання даних позиціювання. Будь ласка, увімкніть «Позиціювання» у системних налаштуваннях - Фоновий режим + Режим тла OsmAnd Tracker працює у фоновому режимі з вимкненим екраном. Відстань Поділитися позицією @@ -267,4 +267,8 @@ Остання відповідь: %1$s тому %1$s тому ПМЛК + Експорт + Буфер logcat + Переглянути та надіслати докладний журнал застосунку + Надіслати звіт \ No newline at end of file diff --git a/OsmAnd-telegram/res/values-zh-rTW/strings.xml b/OsmAnd-telegram/res/values-zh-rTW/strings.xml index f1f06a0daa..7739c65184 100644 --- a/OsmAnd-telegram/res/values-zh-rTW/strings.xml +++ b/OsmAnd-telegram/res/values-zh-rTW/strings.xml @@ -270,4 +270,8 @@ 最後回應:%1$s 前 %1$s 前 ERR + 傳送報告 + 匯出 + Logcat 緩衝 + 檢查及分享應用程式的詳細紀錄 \ No newline at end of file diff --git a/OsmAnd-telegram/res/values/dimens.xml b/OsmAnd-telegram/res/values/dimens.xml index 7f4942dc3e..60976ff822 100644 --- a/OsmAnd-telegram/res/values/dimens.xml +++ b/OsmAnd-telegram/res/values/dimens.xml @@ -27,6 +27,7 @@ 89dp 48dp + 44dp 42dp 56dp diff --git a/OsmAnd-telegram/res/values/strings.xml b/OsmAnd-telegram/res/values/strings.xml index 2b5eca334e..8bccf957ce 100644 --- a/OsmAnd-telegram/res/values/strings.xml +++ b/OsmAnd-telegram/res/values/strings.xml @@ -1,5 +1,9 @@ + Send report + Check and share detailed logs of the app + Logcat buffer + Export ERR Last update from Telegram: %1$s Last response: %1$s diff --git a/OsmAnd-telegram/src/net/osmand/telegram/TelegramApplication.kt b/OsmAnd-telegram/src/net/osmand/telegram/TelegramApplication.kt index ee6bbb29d4..b6927dc610 100644 --- a/OsmAnd-telegram/src/net/osmand/telegram/TelegramApplication.kt +++ b/OsmAnd-telegram/src/net/osmand/telegram/TelegramApplication.kt @@ -3,16 +3,20 @@ package net.osmand.telegram import android.app.Application import android.content.Context import android.content.Intent +import android.content.pm.PackageManager import android.net.ConnectivityManager import android.net.NetworkInfo import android.os.Build import android.os.Handler +import net.osmand.PlatformUtil +import net.osmand.telegram.ui.TrackerLogcatActivity import net.osmand.telegram.helpers.* import net.osmand.telegram.helpers.OsmandAidlHelper.OsmandHelperListener import net.osmand.telegram.helpers.OsmandAidlHelper.UpdatesListener import net.osmand.telegram.notifications.NotificationHelper import net.osmand.telegram.utils.AndroidUtils import net.osmand.telegram.utils.UiUtils +import java.io.File class TelegramApplication : Application() { @@ -200,4 +204,33 @@ class TelegramApplication : Application() { fun runInUIThread(action: (() -> Unit), delay: Long) { uiHandler.postDelayed(action, delay) } + + fun sendCrashLog(file: File) { + val intent = Intent(Intent.ACTION_SEND) + intent.putExtra(Intent.EXTRA_EMAIL, arrayOf("crash@osmand.net")) + intent.putExtra(Intent.EXTRA_STREAM, AndroidUtils.getUriForFile(this, file)) + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + intent.type = "vnd.android.cursor.dir/email" + intent.putExtra(Intent.EXTRA_SUBJECT, "OsmAnd bug") + val text = StringBuilder() + text.append("\nDevice : ").append(Build.DEVICE) + text.append("\nBrand : ").append(Build.BRAND) + text.append("\nModel : ").append(Build.MODEL) + text.append("\nProduct : ").append(Build.PRODUCT) + text.append("\nBuild : ").append(Build.DISPLAY) + text.append("\nVersion : ").append(Build.VERSION.RELEASE) + text.append("\nApp : ").append(getString(R.string.app_name_short)) + try { + val info = packageManager.getPackageInfo(packageName, 0) + if (info != null) { + text.append("\nApk Version : ").append(info.versionName).append(" ").append(info.versionCode) + } + } catch (e: PackageManager.NameNotFoundException) { + PlatformUtil.getLog(TrackerLogcatActivity::class.java).error("", e) + } + intent.putExtra(Intent.EXTRA_TEXT, text.toString()) + val chooserIntent = Intent.createChooser(intent, getString(R.string.send_report)) + chooserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + startActivity(chooserIntent) + } } diff --git a/OsmAnd-telegram/src/net/osmand/telegram/TelegramSettings.kt b/OsmAnd-telegram/src/net/osmand/telegram/TelegramSettings.kt index d8ec28a9c0..d9ec335a0e 100644 --- a/OsmAnd-telegram/src/net/osmand/telegram/TelegramSettings.kt +++ b/OsmAnd-telegram/src/net/osmand/telegram/TelegramSettings.kt @@ -110,7 +110,7 @@ private const val PROXY_ENABLED = "proxy_enabled" private const val PROXY_PREFERENCES_KEY = "proxy_preferences" private const val SHARING_INITIALIZATION_TIME = 60 * 2L // 2 minutes -private const val WAITING_TDLIB_TIME = 3 // 3 seconds +private const val WAITING_TDLIB_TIME = 7 // 7 seconds private const val GPS_UPDATE_EXPIRED_TIME = 60 * 3L // 3 minutes @@ -540,14 +540,24 @@ class TelegramSettings(private val app: TelegramApplication) { if (initTime && initSending) { initializing = true } else { + var waitingTimeError = false val maxWaitingTime = WAITING_TDLIB_TIME * MAX_MESSAGES_IN_TDLIB_PER_CHAT * max(1, chatsCount) - val textSharingError = !shareInfo.lastTextMessageHandled && currentTime - shareInfo.lastSendTextMessageTime > maxWaitingTime - val mapSharingError = !shareInfo.lastMapMessageHandled && currentTime - shareInfo.lastSendMapMessageTime > maxWaitingTime - if (shareInfo.hasSharingError - || (shareTypeValue == SHARE_TYPE_MAP_AND_TEXT && (textSharingError || mapSharingError)) - || textSharingError && (shareTypeValue == SHARE_TYPE_TEXT) - || mapSharingError && (shareTypeValue == SHARE_TYPE_MAP) - ) { + val textSharingWaitingTime = currentTime - shareInfo.lastSendTextMessageTime + val mapSharingWaitingTime = currentTime - shareInfo.lastSendMapMessageTime + val textSharingError = !shareInfo.lastTextMessageHandled && textSharingWaitingTime > maxWaitingTime + val mapSharingError = !shareInfo.lastMapMessageHandled && mapSharingWaitingTime > maxWaitingTime + if ((shareTypeValue == SHARE_TYPE_MAP_AND_TEXT && (textSharingError || mapSharingError)) + || textSharingError && (shareTypeValue == SHARE_TYPE_TEXT) + || mapSharingError && (shareTypeValue == SHARE_TYPE_MAP)) { + waitingTimeError = true + log.debug("Send chats error for share type \"$shareTypeValue\"" + + "\nMax waiting time: ${maxWaitingTime}s" + + "\nLast text message handled: ${shareInfo.lastTextMessageHandled}" + + "\nText sharing waiting time: ${textSharingWaitingTime}s" + + "\nLast map message handled: ${shareInfo.lastMapMessageHandled}" + + "\nMap sharing waiting time: ${mapSharingWaitingTime}s") + } + if (shareInfo.hasSharingError || waitingTimeError) { sendChatsErrors = true locationTime = max(shareInfo.lastTextSuccessfulSendTime, shareInfo.lastMapSuccessfulSendTime) chatsIds.add(shareInfo.chatId) diff --git a/OsmAnd-telegram/src/net/osmand/telegram/helpers/TelegramHelper.kt b/OsmAnd-telegram/src/net/osmand/telegram/helpers/TelegramHelper.kt index e4d5188a2b..704a797af2 100644 --- a/OsmAnd-telegram/src/net/osmand/telegram/helpers/TelegramHelper.kt +++ b/OsmAnd-telegram/src/net/osmand/telegram/helpers/TelegramHelper.kt @@ -776,6 +776,7 @@ class TelegramHelper private constructor() { client?.send(TdApi.CreatePrivateChat(userId, false)) { obj -> when (obj.constructor) { TdApi.Error.CONSTRUCTOR -> { + log.debug("createPrivateChatWithUser ERROR $obj") val error = obj as TdApi.Error if (error.code != IGNORED_ERROR_CODE) { shareInfo.hasSharingError = true @@ -969,7 +970,7 @@ class TelegramHelper private constructor() { val messageType = if (isBot) MESSAGE_TYPE_BOT else MESSAGE_TYPE_TEXT when (obj.constructor) { TdApi.Error.CONSTRUCTOR -> { - log.debug("handleTextLocationMessageUpdate - ERROR") + log.debug("handleTextLocationMessageUpdate - ERROR $obj") val error = obj as TdApi.Error if (error.code != IGNORED_ERROR_CODE) { shareInfo.hasSharingError = true diff --git a/OsmAnd-telegram/src/net/osmand/telegram/ui/SettingsDialogFragment.kt b/OsmAnd-telegram/src/net/osmand/telegram/ui/SettingsDialogFragment.kt index a5bf5993e9..447275815e 100644 --- a/OsmAnd-telegram/src/net/osmand/telegram/ui/SettingsDialogFragment.kt +++ b/OsmAnd-telegram/src/net/osmand/telegram/ui/SettingsDialogFragment.kt @@ -213,6 +213,12 @@ class SettingsDialogFragment : BaseDialogFragment() { DisconnectTelegramBottomSheet.showInstance(childFragmentManager) } + mainView.findViewById(R.id.logcat_row).setOnClickListener { + val intent = Intent(activity, TrackerLogcatActivity::class.java) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + app.startActivity(intent) + } + return mainView } diff --git a/OsmAnd-telegram/src/net/osmand/telegram/ui/TrackerLogcatActivity.kt b/OsmAnd-telegram/src/net/osmand/telegram/ui/TrackerLogcatActivity.kt new file mode 100644 index 0000000000..ee62ab4407 --- /dev/null +++ b/OsmAnd-telegram/src/net/osmand/telegram/ui/TrackerLogcatActivity.kt @@ -0,0 +1,271 @@ +package net.osmand.telegram.ui + +import android.os.AsyncTask +import android.os.Bundle +import android.view.* +import android.widget.ProgressBar +import android.widget.TextView +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.widget.Toolbar +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import net.osmand.PlatformUtil +import net.osmand.telegram.R +import net.osmand.telegram.TelegramApplication +import java.io.* +import java.lang.ref.WeakReference +import java.util.* + +class TrackerLogcatActivity : AppCompatActivity() { + private var logcatAsyncTask: LogcatAsyncTask? = null + private val logs: MutableList = ArrayList() + private var adapter: LogcatAdapter? = null + private val LEVELS = arrayOf("D", "I", "W", "E") + private var filterLevel = 1 + private lateinit var recyclerView: RecyclerView + + override fun onCreate(savedInstanceState: Bundle?) { + val app: TelegramApplication = getApplication() as TelegramApplication + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_tracker_logcat) + + val toolbar = findViewById(R.id.toolbar).apply { + navigationIcon = app.uiUtils.getThemedIcon(R.drawable.ic_arrow_back) + setNavigationOnClickListener { onBackPressed() } + } + setSupportActionBar(toolbar) + setupIntermediateProgressBar() + + adapter = LogcatAdapter() + recyclerView = findViewById(R.id.recycler_view) as RecyclerView + recyclerView!!.layoutManager = LinearLayoutManager(this) + recyclerView!!.adapter = adapter + } + + protected fun setupIntermediateProgressBar() { + val progressBar = ProgressBar(this) + progressBar.visibility = View.GONE + progressBar.isIndeterminate = true + val supportActionBar = supportActionBar + if (supportActionBar != null) { + supportActionBar.setDisplayShowCustomEnabled(true) + supportActionBar.customView = progressBar + setSupportProgressBarIndeterminateVisibility(false) + } + } + + override fun setSupportProgressBarIndeterminateVisibility(visible: Boolean) { + val supportActionBar = supportActionBar + if (supportActionBar != null) { + supportActionBar.customView.visibility = if (visible) View.VISIBLE else View.GONE + } + } + + override fun onResume() { + super.onResume() + startLogcatAsyncTask() + } + + override fun onPause() { + super.onPause() + stopLogcatAsyncTask() + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + val app: TelegramApplication = applicationContext as TelegramApplication + val share: MenuItem = menu.add(0, SHARE_ID, 0, R.string.shared_string_export) + share.icon = app.uiUtils.getThemedIcon(R.drawable.ic_action_share) + share.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS) + val level = menu.add(0, LEVEL_ID, 0, "") + level.title = getFilterLevel() + level.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS) + return super.onCreateOptionsMenu(menu) + } + + private fun getFilterLevel(): String { + return "*:" + LEVELS[filterLevel] + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + val itemId = item.itemId + when (itemId) { + android.R.id.home -> { + finish() + return true + } + LEVEL_ID -> { + filterLevel++ + if (filterLevel >= LEVELS.size) { + filterLevel = 0 + } + item.title = getFilterLevel() + stopLogcatAsyncTask() + logs.clear() + adapter!!.notifyDataSetChanged() + startLogcatAsyncTask() + return true + } + SHARE_ID -> { + startSaveLogsAsyncTask() + return true + } + } + return false + } + + private fun startSaveLogsAsyncTask() { + val saveLogsAsyncTask = SaveLogsAsyncTask(this, logs) + saveLogsAsyncTask.execute() + } + + private fun startLogcatAsyncTask() { + logcatAsyncTask = LogcatAsyncTask(this, getFilterLevel()) + logcatAsyncTask!!.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR) + } + + private fun stopLogcatAsyncTask() { + if (logcatAsyncTask != null && logcatAsyncTask!!.status == AsyncTask.Status.RUNNING) { + logcatAsyncTask!!.cancel(false) + logcatAsyncTask!!.stopLogging() + } + } + + private inner class LogcatAdapter : RecyclerView.Adapter() { + override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + val inflater = LayoutInflater.from(viewGroup.context) + val itemView = inflater.inflate(R.layout.item_description_long, viewGroup, false) as TextView + itemView.gravity = Gravity.CENTER_VERTICAL + return LogViewHolder(itemView) + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + if (holder is LogViewHolder) { + val log = getLog(position) + holder.logTextView.text = log + } + } + + override fun getItemCount(): Int { + return logs.size + } + + private fun getLog(position: Int): String { + return logs[position] + } + + private inner class LogViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + val logTextView: TextView = itemView.findViewById(R.id.description) + } + } + + class SaveLogsAsyncTask internal constructor(logcatActivity: TrackerLogcatActivity, logs: Collection) : AsyncTask() { + private val logcatActivity: WeakReference + private val logs: Collection + + override fun onPreExecute() { + val activity = logcatActivity.get() + activity?.setSupportProgressBarIndeterminateVisibility(true) + } + + override fun doInBackground(vararg voids: Void?): File { + val app: TelegramApplication = logcatActivity.get()?.applicationContext as TelegramApplication + val file = File(app.getExternalFilesDir(null), LOGCAT_PATH) + try { + if (file.exists()) { + file.delete() + } + val stringBuilder = StringBuilder() + for (log in logs) { + stringBuilder.append(log) + stringBuilder.append("\n") + } + if (file.parentFile.canWrite()) { + val writer = BufferedWriter(FileWriter(file, true)) + writer.write(stringBuilder.toString()) + writer.close() + } + } catch (e: Exception) { + log.error(e) + } + return file + } + + override fun onPostExecute(file: File?) { + val activity = logcatActivity.get() + if (activity != null && file != null) { + val app: TelegramApplication = activity.applicationContext as TelegramApplication + activity.setSupportProgressBarIndeterminateVisibility(false) + app.sendCrashLog(file) + } + } + + init { + this.logcatActivity = WeakReference(logcatActivity) + this.logs = logs + } + } + + class LogcatAsyncTask internal constructor(logcatActivity: TrackerLogcatActivity?, filterLevel: String) : AsyncTask() { + private var processLogcat: Process? = null + private val logcatActivity: WeakReference + private val filterLevel: String + + override fun doInBackground(vararg voids: Void?): Void? { + try { + val filter = android.os.Process.myPid().toString() + val command = arrayOf("logcat", filterLevel, "--pid=$filter", "-T", MAX_BUFFER_LOG.toString()) + processLogcat = Runtime.getRuntime().exec(command) + val bufferedReader = BufferedReader(InputStreamReader(processLogcat?.inputStream)) + var line: String? + while (bufferedReader.readLine().also { line = it } != null && logcatActivity.get() != null) { + if (isCancelled) { + break + } + publishProgress(line) + } + stopLogging() + } catch (e: IOException) { // ignore + } catch (e: Exception) { + log.error(e) + } + return null + } + + override fun onProgressUpdate(vararg values: String?) { + if (values.size > 0 && !isCancelled) { + val activity = logcatActivity.get() + if (activity != null) { + val autoscroll = !activity.recyclerView!!.canScrollVertically(1) + for (s in values) { + if (s != null) { + activity.logs.add(s) + } + } + activity.adapter!!.notifyDataSetChanged() + if (autoscroll) { + activity.recyclerView!!.scrollToPosition(activity.logs.size - 1) + } + } + } + } + + fun stopLogging() { + if (processLogcat != null) { + processLogcat!!.destroy() + } + } + + init { + this.logcatActivity = WeakReference(logcatActivity) + this.filterLevel = filterLevel + } + } + + companion object { + private const val LOGCAT_PATH = "logcat.log" + private const val MAX_BUFFER_LOG = 10000 + private const val SHARE_ID = 0 + private const val LEVEL_ID = 1 + private val log = PlatformUtil.getLog(TrackerLogcatActivity::class.java) + } +} diff --git a/OsmAnd/.gitignore b/OsmAnd/.gitignore index e3071e5fbf..8bfbd5b688 100644 --- a/OsmAnd/.gitignore +++ b/OsmAnd/.gitignore @@ -13,10 +13,13 @@ libs/it.unibo.alice.tuprolog-tuprolog-3.2.1.jar libs/commons-codec-commons-codec-1.11.jar libs/OsmAndCore_android-0.1-SNAPSHOT.jar +# Huawei libs/huawei-*.jar huaweidrmlib/ HwDRM_SDK_* drm_strings.xml +agconnect-services.json +OsmAndHms.jks # copy_widget_icons.sh res/drawable-large/map_* diff --git a/OsmAnd/AndroidManifest-freehuawei.xml b/OsmAnd/AndroidManifest-freehuawei.xml index e96bb7fe47..c557d5fe2a 100644 --- a/OsmAnd/AndroidManifest-freehuawei.xml +++ b/OsmAnd/AndroidManifest-freehuawei.xml @@ -2,24 +2,31 @@ - - - - + + + + + + + + - + tools:replace="android:authorities" + android:authorities="net.osmand.huawei.fileprovider"/> - \ No newline at end of file diff --git a/OsmAnd/AndroidManifest-huawei.xml b/OsmAnd/AndroidManifest-huawei.xml deleted file mode 100644 index bc847980cd..0000000000 --- a/OsmAnd/AndroidManifest-huawei.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/OsmAnd/build.gradle b/OsmAnd/build.gradle index ddac6aee41..ee9d4dd684 100644 --- a/OsmAnd/build.gradle +++ b/OsmAnd/build.gradle @@ -40,6 +40,15 @@ android { keyAlias "osmand" keyPassword System.getenv("OSMAND_APK_PASSWORD") } + + publishingHuawei { + storeFile file("/var/lib/jenkins/osmand_hw_key") + storePassword System.getenv("OSMAND_HW_APK_PASSWORD") + keyAlias "osmand" + keyPassword System.getenv("OSMAND_HW_APK_PASSWORD") + v1SigningEnabled true + v2SigningEnabled true + } } defaultConfig { @@ -107,19 +116,26 @@ android { debug { manifest.srcFile "AndroidManifest-debug.xml" } + full { + java.srcDirs = ["src-google"] + } + fulldev { + java.srcDirs = ["src-google"] + } free { + java.srcDirs = ["src-google"] manifest.srcFile "AndroidManifest-free.xml" } freedev { + java.srcDirs = ["src-google"] manifest.srcFile "AndroidManifest-freedev.xml" } freecustom { + java.srcDirs = ["src-google"] manifest.srcFile "AndroidManifest-freecustom.xml" } - huawei { - manifest.srcFile "AndroidManifest-huawei.xml" - } freehuawei { + java.srcDirs = ["src-huawei"] manifest.srcFile "AndroidManifest-freehuawei.xml" } @@ -185,21 +201,16 @@ android { dimension "version" applicationId "net.osmand.plus" } - fulldev { - dimension "version" - applicationId "net.osmand.plus" - resConfig "en" - //resConfigs "xxhdpi", "nodpi" - } - huawei { + fulldev { dimension "version" - applicationId "net.osmand.plus.huawei" + applicationId "net.osmand.plus" + resConfig "en" + // resConfigs "xxhdpi", "nodpi" } freehuawei { dimension "version" applicationId "net.osmand.huawei" } - // CoreVersion legacy { dimension "coreversion" @@ -223,7 +234,11 @@ android { release { buildConfigField "String", "OSM_OAUTH_CONSUMER_KEY", "\"Ti2qq3fo4i4Wmuox3SiWRIGq3obZisBHnxmcM05y\"" buildConfigField "String", "OSM_OAUTH_CONSUMER_SECRET", "\"lxulb3HYoMmd2cC4xxNe1dyfRMAY8dS0eNihJ0DM\"" - signingConfig signingConfigs.publishing + if (gradle.startParameter.taskNames.toString().contains("huawei")) { + signingConfig signingConfigs.publishingHuawei + } else { + signingConfig signingConfigs.publishing + } } } @@ -280,46 +295,6 @@ task downloadWorldMiniBasemap { } } -task downloadHuaweiDrmZip { - doLast { - ant.get(src: 'https://obs.cn-north-2.myhwclouds.com/hms-ds-wf/sdk/HwDRM_SDK_2.5.2.300_ADT.zip', dest: 'HwDRM_SDK_2.5.2.300_ADT.zip', skipexisting: 'true') - ant.unzip(src: 'HwDRM_SDK_2.5.2.300_ADT.zip', dest: 'huaweidrmlib/') - } -} - -task copyHuaweiDrmLibs(type: Copy) { - dependsOn downloadHuaweiDrmZip - from "huaweidrmlib/HwDRM_SDK_2.5.2.300_ADT/libs" - into "libs" -} - -task copyHuaweiDrmValues(type: Copy) { - dependsOn downloadHuaweiDrmZip - from "huaweidrmlib/HwDRM_SDK_2.5.2.300_ADT/res" - into "res" -} - -task downloadPrebuiltHuaweiDrm { - dependsOn copyHuaweiDrmLibs, copyHuaweiDrmValues -} - -task cleanHuaweiDrmLibs(type: Delete) { - delete "huaweidrmlib" - delete fileTree("libs").matching { - include "**/huawei-*.jar" - } -} - -task cleanHuaweiDrmValues(type: Delete) { - delete fileTree("res").matching { - include "**/drm_strings.xml" - } -} - -task cleanPrebuiltHuaweiDrm { - dependsOn cleanHuaweiDrmLibs, cleanHuaweiDrmValues -} - task collectVoiceAssets(type: Sync) { from "../../resources/voice" into "assets/voice" @@ -401,8 +376,6 @@ task copyLargePOIIcons(type: Sync) { } } - - task copyWidgetIconsXhdpi(type: Sync) { from "res/drawable-xxhdpi/" into "res/drawable-large-xhdpi/" @@ -448,15 +421,6 @@ task collectExternalResources { copyWidgetIconsXhdpi, copyPoiCategories, downloadWorldMiniBasemap - - Gradle gradle = getGradle() - String tskReqStr = gradle.getStartParameter().getTaskRequests().toString().toLowerCase() - // Use Drm SDK only for huawei build - if (tskReqStr.contains("huawei")) { - dependsOn downloadPrebuiltHuaweiDrm - } else { - dependsOn cleanPrebuiltHuaweiDrm - } } // Legacy core build @@ -507,10 +471,16 @@ task cleanupDuplicatesInCore() { file("libs/x86_64/libc++_shared.so").renameTo(file("libc++/x86_64/libc++_shared.so")) } } + afterEvaluate { android.applicationVariants.all { variant -> variant.javaCompiler.dependsOn(collectExternalResources, buildOsmAndCore, cleanupDuplicatesInCore) } + Gradle gradle = getGradle() + String tskReqStr = gradle.getStartParameter().getTaskRequests().toString().toLowerCase() + if (tskReqStr.contains("huawei")) { + apply plugin: 'com.huawei.agconnect' + } } task appStart(type: Exec) { @@ -520,7 +490,6 @@ task appStart(type: Exec) { // commandLine 'cmd', '/c', 'adb', 'shell', 'am', 'start', '-n', 'net.osmand.plus/net.osmand.plus.activities.MapActivity' } - dependencies { implementation project(path: ':OsmAnd-java', configuration: 'android') implementation project(':OsmAnd-api') @@ -572,6 +541,5 @@ dependencies { } implementation 'com.jaredrummler:colorpicker:1.1.0' - huaweiImplementation files('libs/huawei-android-drm_v2.5.2.300.jar') - freehuaweiImplementation files('libs/huawei-android-drm_v2.5.2.300.jar') + freehuaweiImplementation 'com.huawei.hms:iap:5.0.2.300' } diff --git a/OsmAnd/res/drawable/ic_action_direction_arrow.xml b/OsmAnd/res/drawable/ic_action_direction_arrow.xml index 5e97fe8338..49bec48f22 100644 --- a/OsmAnd/res/drawable/ic_action_direction_arrow.xml +++ b/OsmAnd/res/drawable/ic_action_direction_arrow.xml @@ -1,9 +1,9 @@ + android:viewportHeight="12"> diff --git a/OsmAnd/res/layout/bottom_buttons_vertical.xml b/OsmAnd/res/layout/bottom_buttons_vertical.xml new file mode 100644 index 0000000000..0de7d48389 --- /dev/null +++ b/OsmAnd/res/layout/bottom_buttons_vertical.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/OsmAnd/res/layout/bottom_sheet_item_with_switch.xml b/OsmAnd/res/layout/bottom_sheet_item_with_switch.xml index 955c2937b2..53b21954ee 100644 --- a/OsmAnd/res/layout/bottom_sheet_item_with_switch.xml +++ b/OsmAnd/res/layout/bottom_sheet_item_with_switch.xml @@ -3,7 +3,7 @@ xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" - android:layout_height="@dimen/bottom_sheet_list_item_height" + android:layout_height="wrap_content" android:background="?attr/selectableItemBackground" android:gravity="center_vertical" android:minHeight="@dimen/bottom_sheet_list_item_height" @@ -28,7 +28,7 @@ android:layout_marginRight="@dimen/content_padding" android:layout_weight="1" android:ellipsize="end" - android:maxLines="1" + android:maxLines="4" android:textAppearance="@style/TextAppearance.ListItemTitle" tools:text="Some Title"/> diff --git a/OsmAnd/res/layout/bottom_sheet_menu_base.xml b/OsmAnd/res/layout/bottom_sheet_menu_base.xml index 5110455a7c..d265ba938c 100644 --- a/OsmAnd/res/layout/bottom_sheet_menu_base.xml +++ b/OsmAnd/res/layout/bottom_sheet_menu_base.xml @@ -35,10 +35,8 @@ android:layout_width="match_parent" android:layout_height="10dp" android:layout_gravity="bottom" - android:visibility="gone" - android:background="@drawable/bg_contextmenu_shadow_top_light" /> + android:background="@drawable/bg_contextmenu_shadow_top_light" + android:visibility="gone" /> - - - + \ No newline at end of file diff --git a/OsmAnd/res/values-ar/strings.xml b/OsmAnd/res/values-ar/strings.xml index f3bbf28ac6..c1426b8483 100644 --- a/OsmAnd/res/values-ar/strings.xml +++ b/OsmAnd/res/values-ar/strings.xml @@ -721,9 +721,9 @@ عكس اتجاه المسار استخدم الوجهة الحالية يمر على طول المسار باكمله - خريطة التنقل متوفرة حالياً لهذا الموقع. + خريطة التنقل متوفرة لهذا الموقع فعلها عبر \n -\nلتفعليها \'القائمة\' ← \'ضبط الخريطة\' ← \'مصدر الخريطة\' ← \'الخريطة المحملة\'. +\n\'القائمة\' ← \'ضبط الخريطة\' ← \'مصدر الخريطة\' ← \'الخريطة المحملة\'. مصدر التوجيه الصوتي اختيار قناة لتشغيل التوجيه الصوتي. صوت المكالمة الهاتفية ( كما يحاول قطع ستريو بلوتوث السيارة ) @@ -1645,7 +1645,7 @@ الطريق محظور تحديد اعكس نقطة الانطلاق والوصول - أيقونات POI + أيقونات نقاط الاهتمام النوع غير محدد قسم مسجل @@ -1972,7 +1972,7 @@ بطاقة الذاكرة غير متاحة. \nلن تكون قادرا على رؤية الخرائط أو العثور على أماكن. بطاقة الذاكرة في وضع القراءة فقط. -\n يمكنك فقط مشاهدة الخريطة المحملة مسبقا ولا يمكنك التحميل من الإنترنت. +\n يمكنك فقط مشاهدة الخريطة المحملة مسبقاً ولا يمكنك التحميل من الإنترنت. انعطف يميناً بشكل حاد انعطف يساراً بشكل حاد قم بالدوران وواصل @@ -3891,4 +3891,6 @@ آخر تعديل الاسم: أ – ي الاسم: أ – ي + رموز البدء/الانتهاء + شكرا لشرائك \"خطوط الكنتور\" \ No newline at end of file diff --git a/OsmAnd/res/values-cs/phrases.xml b/OsmAnd/res/values-cs/phrases.xml index e63732cdc6..8d64234b3a 100644 --- a/OsmAnd/res/values-cs/phrases.xml +++ b/OsmAnd/res/values-cs/phrases.xml @@ -3844,4 +3844,5 @@ Šipka Vibrace Tlak + Zkapalněný zemní plyn \ No newline at end of file diff --git a/OsmAnd/res/values-cs/strings.xml b/OsmAnd/res/values-cs/strings.xml index 64f1dc8949..59b0baf68d 100644 --- a/OsmAnd/res/values-cs/strings.xml +++ b/OsmAnd/res/values-cs/strings.xml @@ -3525,4 +3525,57 @@ Zobrazená oblast: %1$s x %2$s Smazat následující cílový bod Umožní ovládat úroveň přiblížení mapy pomocí tlačítek hlasitosti zařízení. Tlačítka hlasitosti pro přibližování + Mezní vzdálenost + Navigační profil + Vyberte soubor stopy, do níž se nový úsek přidá. + Snímky z úrovně ulice + Opravdu chcete zavřít plánovanou trasu a zahodit tak všechny změny\? + V případě opačného směru + Uložit jako nový soubor stopy + Přidat do souboru stopy + Stopy + Stopy + Stopy + "Ukládat trasu do GPX souboru" + Trasa ze stopy + Přidat soubory stop + Zjednodušená trasa + Uloží se pouze linie trasy, mezicíle budou odstraněny. + Název souboru + %s vybraných souborů stop + REC + Pozastaví záznam trasy, pokud je aplikace ukončena (přes nedávné aplikace). (Ikona režimu na pozadí zmizí z notifikační oblasti Androidu.) + Pozastavit záznam trasy + Pokračovat v záznamu trasy + Výchozí + Všechny následující úseky + Přecházející úsek + Všechny předcházející úseky + Pouze vybraný úsek bude přepočítán pomocí vybraného profilu. + Všechny následující úseky budou přepočítány pomocí vybraného profilu. + Všechny předcházející úseky budou přepočítány pomocí vybraného profilu. + Otevřít uloženou trasu + je uloženo + Přidejte prosím alespoň dva body. + Znovu + • Vylepšená funkce plánování trasy: umožňuje použít různé typy navigace pro úseky trasy a přidává stopy +\n +\n • Nové menu pro vzhled stop: zvolte barvu, tloušťku, směrové šipky, ikony startu a cíle +\n +\n • Vylepšená viditelnost bodů pro cyklistiku. +\n +\n • Stopy je nyní možné aktivovat, nabízejí kontextové menu se základními údaji. +\n +\n • Vylepšený algoritmus vyhledávání +\n +\n • Vylepšené možnosti následování stopy v navigaci +\n +\n • Opravené problémy s importem a exportem nastavení profilů +\n +\n + Naposledy změněno + Název: Z – A + Název: A – Z + Ikony startu/cíle + Děkujeme za zakoupení modulu \'Vrstevnice\' \ No newline at end of file diff --git a/OsmAnd/res/values-da/phrases.xml b/OsmAnd/res/values-da/phrases.xml index 3e1f8c0d81..9f26ab3b43 100644 --- a/OsmAnd/res/values-da/phrases.xml +++ b/OsmAnd/res/values-da/phrases.xml @@ -3842,4 +3842,14 @@ Donationsboks Pil: nej Elevator + Nøddebutik + Bikube + Tidsplan + Realtid + Forsinkelse + Ja + Afgangstavle: nej + Små elektriske apparater + Afgangstavle + Genopfyldning af drikkevand \ No newline at end of file diff --git a/OsmAnd/res/values-da/strings.xml b/OsmAnd/res/values-da/strings.xml index 058b72d771..0e05949cf0 100644 --- a/OsmAnd/res/values-da/strings.xml +++ b/OsmAnd/res/values-da/strings.xml @@ -3243,7 +3243,7 @@ Advarsler vises nederst til venstre under navigationen. Skift profil Sprog og output - Gendan standardværdi + Nulstil til standard Opret, importer, rediger profiler Administrer programprofiler… Påvirker hele programmet @@ -3756,8 +3756,8 @@ Slet adresse Tilføj adresse Angiv adresse - Trim før - Trim efter + Trimme før + Trimme efter Skift rutetype før Skift rutetype efter Forenklet spor @@ -3771,4 +3771,21 @@ er gemt Tilføj mindst to punkter. Omgøre + Navigere fra position til sporet + Punkt på sporet for at navigere + %s sporfiler valgt + Sidst ændret + Navn: Z – A + Navn: A – Z + Vælg en sporfil, der skal åbnes. + Vælg en sporfil, som det nye segment skal føjes til. + Kasser alle ændringer i den planlagte rute ved at lukke den\? + Føj til en sporfil + Vælg sporfil, der skal følges eller importeres fra enheden. + Kun rutelinjen gemmes, rutepunkter slettes. + Alle efterfølgende segmenter + Kun det valgte segment genberegnes igen ved hjælp af den valgte profil. + Alle efterfølgende segmenter genberegnes ved hjælp af den valgte profil. + Alle tidligere segmenter genberegnes ved hjælp af den valgte profil. + Start-/slutikoner \ No newline at end of file diff --git a/OsmAnd/res/values-de/phrases.xml b/OsmAnd/res/values-de/phrases.xml index d091dc7fd5..d40597ca63 100644 --- a/OsmAnd/res/values-de/phrases.xml +++ b/OsmAnd/res/values-de/phrases.xml @@ -3847,4 +3847,5 @@ Bienenstock Kleine Elektrogeräte Nussladen + Flüssigerdgas \ No newline at end of file diff --git a/OsmAnd/res/values-de/strings.xml b/OsmAnd/res/values-de/strings.xml index 319e1c4f6f..1fcf32f445 100644 --- a/OsmAnd/res/values-de/strings.xml +++ b/OsmAnd/res/values-de/strings.xml @@ -3876,9 +3876,7 @@ Track Datei zum Folgen auswählen, oder vom Gerät importieren. Die GPX-Aufzeichnung wird angehalten, wenn OsmAnd beendet wird (über „zuletzt verwendete Apps“). (Die Hintergrunddienst-Anzeige verschwindet aus der Android-Benachrichtigungsleiste.) Aufzeichnungsintervall für die generelle Track-Aufzeichnung festlegen (via Schaltfläche \'GPX\' auf dem Kartenbildschirm). - Um diese Option nutzen zu können, muss OsmAnd den Track auf die Straßen der Karte einrasten. -\n -\n Wählen Sie im nächsten Schritt ein Navigationsprofil um festzulegen, welche Straßentypen verwendet werden sollen, und wählen Sie einen Wert für die maximal zulässige Entfernung zwischen Track und Straße. + Als nächstes können Sie Ihren Track mit einem Ihrer Navigationsprofile auf die nächstgelegene erlaubte Straße einrasten lassen, um diese Option zu nutzen. Track-Wegpunkt hinzufügen %s Track Dateien ausgewählt Nur die Routenlinie wird gespeichert, die Wegpunkte werden gelöscht. @@ -3910,4 +3908,6 @@ Zuletzt geändert Name: Z – A Name: A – Z + Start-/Ziel-Symbole + Vielen Dank für den Kauf von \'Höhenlinien\' \ No newline at end of file diff --git a/OsmAnd/res/values-es-rAR/phrases.xml b/OsmAnd/res/values-es-rAR/phrases.xml index 4019b950b2..88c35ef6f9 100644 --- a/OsmAnd/res/values-es-rAR/phrases.xml +++ b/OsmAnd/res/values-es-rAR/phrases.xml @@ -3851,4 +3851,5 @@ Pequeños electrodomésticos Panal de abejas Frutos secos + Gas natural licuado \ No newline at end of file diff --git a/OsmAnd/res/values-es-rAR/strings.xml b/OsmAnd/res/values-es-rAR/strings.xml index 99f789d362..fcdccf360a 100644 --- a/OsmAnd/res/values-es-rAR/strings.xml +++ b/OsmAnd/res/values-es-rAR/strings.xml @@ -385,15 +385,15 @@ \nCualquiera de estos mapas puede usarse como el mapa predefinido que se mostrará, o como una superposición o subyacencia de otro mapa base (como los mapas estándar de OsmAnd en línea). Ciertos elementos de los mapas vectoriales de OsmAnd pueden ocultarse a través del menú «Configurar mapa» para hacer cualquier subyacencia mas visible. \n \nDescarga las teselas de los mapas directamente en línea, o prepáralo para su uso sin conexión (copiar manualmente en la carpeta de datos OsmAnd) como una base de datos SQLite que puede ser producida por una variedad de herramientas de preparación de mapas de terceros. - Este complemento activa la funcionalidad para registrar y guardar tus trazas manualmente pulsando el widget de grabación GPX en el mapa, o automáticamente registrando todas tus rutas navegadas en un archivo GPX. -\n + Activa la funcionalidad para registrar y guardar tus trazas manualmente pulsando el widget de grabación GPX en el mapa, o automáticamente registrando todas tus rutas navegadas en un archivo GPX. +\n \nLas trazas grabadas pueden ser compartidas con tus amigos o ser usadas para contribuir a OSM. Los atletas pueden usar las trazas grabadas para seguir sus entrenamientos. Algunos análisis básicos de trazas se pueden realizar directamente en OsmAnd, como tiempos por vuelta, velocidad media, etc., y por supuesto las trazas pueden analizarse posteriormente con herramientas de análisis de terceros. Complemento de curvas de nivel - Este complemento proporciona una capa superpuesta de curvas de nivel y una capa (de relieve) sombreada, que se pueden visualizar sobre los mapas descargados de OsmAnd. Esta funcionalidad será muy apreciada por atletas, caminantes, excursionistas, y cualquiera interesado en la estructura de relieve de un paisaje. -\n + Proporciona una capa superpuesta de curvas de nivel y una capa (de relieve) sombreada, que se pueden visualizar sobre los mapas descargados de OsmAnd. Esta funcionalidad será muy apreciada por atletas, caminantes, excursionistas, y cualquiera interesado en la estructura de relieve de un paisaje. +\n \nLos datos globales (entre 70° norte y 70° sur) se basan en mediciones de SRTM (Shuttle Radar Topography Mission, o en español Misión de Topografía por Radar desde Transbordador) y ASTER (Advanced Spaceborne Thermal Emission and Reflection Radiometer, o en español Radiómetro Espacial Avanzado de Emisión Térmica y Reflexión), un instrumento de captura de imágenes a bordo de Terra, el satélite insignia del Sistema de Observación de la Tierra de la NASA. ASTER es un esfuerzo cooperativo entre la NASA, el Ministerio de Economía, Comercio e Industria de Japón (METI) y Sistemas Espaciales de Japón (J-spacesystems). - Este complemento proporciona tanto una capa superpuesta de curvas de nivel y una capa (de relieve) sombreada, que se pueden visualizar sobre los mapas descargados de OsmAnd. Esta funcionalidad será muy apreciada por atletas, caminantes, excursionistas, y cualquiera interesado en la estructura de relieve de un paisaje. (Note que las curvas de nivel y/o los datos de relieve están disponibles en descargas adicionales separadas luego de activar el complemento.) -\n + Proporciona tanto una capa superpuesta de curvas de nivel y una capa (de relieve) sombreada, que se pueden visualizar sobre los mapas descargados de OsmAnd. Esta funcionalidad será muy apreciada por atletas, caminantes, excursionistas, y cualquiera interesado en la estructura de relieve de un paisaje. (Note que las curvas de nivel y/o los datos de relieve están disponibles en descargas adicionales separadas luego de activar el complemento.) +\n \nLos datos globales (entre 70° norte y 70° sur) se basan en mediciones de SRTM (Shuttle Radar Topography Mission, o en español Misión de Topografía por Radar Shuffle) y ASTER (Advanced Spaceborne Thermal Emission and Reflection Radiometer, o en español Radiómetro Espacial Avanzado de Emisión Térmica y Reflexión), un instrumento de captura de imágenes a bordo de Terra, el satélite insignia del Sistema de Observación de la Tierra de la NASA. ASTER es un esfuerzo cooperativo entre la NASA, el Ministerio de Economía, Comercio e Industria de Japón (METI) y Sistemas Espaciales de Japón (J-spacesystems). Activando esta vista cambia el estilo del mapa OsmAnd a la «Vista turística», que es una vista de alto detalle especial para viajeros y conductores profesionales. \n @@ -406,13 +406,13 @@ \nNo es necesario descargar un mapa especial, la vista es creada a partir de nuestros mapas estándar. \n \nEsta vista puede ser revertida desactivando de nuevo aquí, o cambiando el «Estilo del mapa» desde «Configurar mapa» cuando lo desees. - Este complemento enriquece el mapa y la navegación de OsmAnd al producir también mapas náuticos para el canotaje, vela y otros tipos de deportes acuáticos. -\n -\nUn mapa especial complementado para OsmAnd proporcionará toda las marcas de navegación náutica y símbolos cartográficos para el interior, así como para la navegación cerca de la costa. La descripción de cada marca de navegación proporciona los datos necesarios para su identificación y su significado (categoría, forma, color, número, referencia, etc.). -\n + Enriquece el mapa y la navegación de OsmAnd al producir también mapas náuticos para el canotaje, vela y otros tipos de deportes acuáticos. +\n +\nUn mapa especial complementado para OsmAnd proporcionará toda las marcas de navegación náutica y símbolos cartográficos para el interior, así como para la navegación cerca de la costa. La descripción de cada marca de navegación proporciona los datos necesarios para su identificación y su significado (categoría, forma, color, número, referencia, etc.). +\n \nPara volver a uno de los estilos del mapas convencionales de OsmAnd, simplemente desactiva este complemento de nuevo, o cambia el «Estilo del mapa» en «Configurar mapa» cuando lo desees. - Este complemento para OsmAnd pone a tu alcance detalles sobre pistas de esquí de descenso, de travesía, rutas de esquí alpino, teleféricos y remontes a nivel mundial. Las rutas y pistas se muestran por código de color en función de su dificultad y representados con un estilo del mapa especial «Invierno» que lo asemeja a un paisaje invernal nevado. -\n + Detalles sobre pistas de esquí de descenso, de travesía, rutas de esquí alpino, teleféricos y remontes a nivel mundial. Las rutas y pistas se muestran por código de color en función de su dificultad y representados con un estilo del mapa especial «Invierno» que lo asemeja a un paisaje invernal nevado. +\n \nActivando esta vista, cambia el estilo del mapa a «Invierno y esquí», mostrando las características del terreno en condiciones invernales. Esta vista se puede revertir desactivando de nuevo aquí o cambiando el «Estilo del mapa» en «Configurar mapa» cuando lo desees. Toma notas de audio, fotografía y/o video durante un viaje, usando un botón en el mapa o el menú contextual de la ubicación. Registra dónde se ha estacionado el automóvil, incluyendo cuánto tiempo queda. @@ -3907,4 +3907,6 @@ Último modificado Nombre: Z – A Nombre: A – Z + Iconos de inicio/fin + Gracias por comprar las «Curvas de nivel» \ No newline at end of file diff --git a/OsmAnd/res/values-es-rUS/phrases.xml b/OsmAnd/res/values-es-rUS/phrases.xml index 25786f2488..8c0dbc2414 100644 --- a/OsmAnd/res/values-es-rUS/phrases.xml +++ b/OsmAnd/res/values-es-rUS/phrases.xml @@ -3851,4 +3851,5 @@ Tablero de partidas Frutos secos Panal de abejas + Gas natural licuado \ No newline at end of file diff --git a/OsmAnd/res/values-es-rUS/strings.xml b/OsmAnd/res/values-es-rUS/strings.xml index a7750a3b93..6ba4dcc29e 100644 --- a/OsmAnd/res/values-es-rUS/strings.xml +++ b/OsmAnd/res/values-es-rUS/strings.xml @@ -3904,4 +3904,5 @@ Nombre: Z – A Nombre: A – Z Último modificado + Iconos de inicio/fin \ No newline at end of file diff --git a/OsmAnd/res/values-es/phrases.xml b/OsmAnd/res/values-es/phrases.xml index d892b94f7e..cac7b4a36d 100644 --- a/OsmAnd/res/values-es/phrases.xml +++ b/OsmAnd/res/values-es/phrases.xml @@ -1588,7 +1588,7 @@ Koshinto Placa azul Jizo - Crucero (monumento) + Cruz Cantera histórica Agregado Antimonio @@ -1825,7 +1825,7 @@ Capacitación: artes marciales Capacitación: aviación Capacitación: peluquería - Monumento + Objeto monumental Tipo: industria petrolera Tipo: Área de pozos Tipo: fábrica @@ -1868,7 +1868,7 @@ 100LL (con plomo, para aviones) Autogas (Etanol libre de plomo) Jet A-1 (diésel) - AdBlue + Líquido de escape de diesel Combustible: madera Combustible: carbón vegetal Combustible: carbón @@ -3798,7 +3798,7 @@ Tubo Red de recarga de agua potable Recarga de agua potable: no - Recarga de agua potable: sí + Obstrucción Nivel de agua: por debajo del nivel medio del agua Nivel de agua: por encima del nivel medio del agua diff --git a/OsmAnd/res/values-es/strings.xml b/OsmAnd/res/values-es/strings.xml index caf2e2ec40..6041f2a95e 100644 --- a/OsmAnd/res/values-es/strings.xml +++ b/OsmAnd/res/values-es/strings.xml @@ -23,7 +23,7 @@ Borrar edición Edición asíncrona OSM: PDI/Notas de OSM guardados en el dispositivo - Muestra y gestiona PDI/notas de OSM guardadas en la base de datos del dispositivo. + Muestra y gestiona PDI/Notas de OSM en la base de datos de tu dispositivo. Indica el intervalo del seguimiento en línea. Intervalo del seguimiento en línea Indica la dirección web con sintaxis de parámetros : lat={0}, lon={1}, timestamp={2}, hdop={3}, altitude={4}, speed={5}, bearing={6}. @@ -163,7 +163,7 @@ El idioma elegido es incompatible con el motor TTS (texto a voz) instalado en Android, se usará el idioma TTS predefinido. ¿Buscar otro motor TTS en la tienda de aplicaciones\? Faltan datos ¿Ir a la tienda de aplicaciones para descargar el idioma elegido? - Invertir la dirección GPX + Invertir la dirección de la traza Usar destino actual Pasar a lo largo de la traza completa Mapa vectorial presente para esta ubicación. @@ -265,10 +265,7 @@ Todos los datos sin conexión en la versión vieja de OsmAnd son compatibles con la nueva versión, pero los puntos de Favoritos deben exportarse desde la versión vieja y luego, importarse en la nueva. Compilación {0} instalada ({1}). Descargando compilación… - ¿Instalar OsmAnd\? -\nVersión: {0} -\nFecha: {1} -\nTamaño: {2} MB + ¿Instalar OsmAnd - {0} de {1} {2} MB\? Error al recuperar la lista de compilaciones de OsmAnd Cargando compilaciones de OsmAnd… Instalar compilación de OsmAnd @@ -441,7 +438,7 @@ Búsqueda sin conexión Búsqueda en línea Máximo zoom en línea - No buscar en las teselas de mapas en línea para niveles de zoom más allá de esto. + No busca en los mapas en línea niveles de zoom más allá de éste. Distancia total %1$s, tiempo de viaje %2$d h %3$d min. Servicios de navegación con o sin conexión. Servicio de navegación @@ -606,7 +603,7 @@ Para países donde la gente conduce por el lado izquierdo del camino. Punto de partida aún no determinado. ¿Cancelar la descarga\? - El mapa base necesario para proporcionar la funcionalidad básica, está en la cola de descarga. + El mapa base necesario para proporcionar funcionalidad básica está en la cola de descargas. Activa el complemento «Mapas en línea», para elegir diferentes fuentes de mapas Mapas en línea y teselas Usa mapas en línea (descarga y guarda teselas en la tarjeta de memoria). @@ -729,8 +726,8 @@ PM AM Lugar de aparcamiento - Este complemento, registra dónde se ha aparcado el automóvil y cuánto tiempo queda (si hay un límite de tiempo). -\nTanto la ubicación como el tiempo del aparcamiento se muestran en el menú principal y en un widget sobre el mapa. Puedes añadir una notificación al calendario, en el caso de que desees tener un recordatorio al respecto. + Te permite egistrar dónde has aparcado el automóvil y cuánto tiempo queda (si hay un límite de tiempo). +\nTanto la ubicación como el tiempo del aparcamiento se muestran en el menú principal y en un control sobre el mapa. Puedes añadir un recordatorio al calendario, de Android. Aparcamiento Marcar como aparcamiento Quitar marcador de aparcamiento @@ -890,7 +887,7 @@ Curvas de nivel Otros mapas Curvas de nivel - Este complemento, proporciona la funcionalidad para tomar notas de audio, fotografía y/o video durante un viaje, usando un botón en el mapa, o directamente en el menú contextual para cualquier ubicación en el mapa. + Tomar notas de audio/foto/video durante un viaje, usando un botón de mapa o un menú contextual de la ubicación. Notas audio/vídeo Complemento OsmAnd para curvas de nivel sin conexión Este complemento proporciona una capa superpuesta de curvas de nivel y una capa (de relieve) sombreada, que se pueden visualizar sobre los mapas descargados de OsmAnd. Esta funcionalidad será muy apreciada por atletas, caminantes, excursionistas, y cualquiera interesado en la estructura de relieve de un paisaje. @@ -930,7 +927,7 @@ Grabando %1$s %3$s %2$s Por favor, considera pagar por el complemento «Curvas de nivel» para apoyar desarrollos adicionales. Complemento de curvas de nivel - El complemento de Dropbox, permite sincronizar trazas y notas multimedia con tu cuenta de Dropbox. + Sincroniza trazas y notas multimedia con tu cuenta de Dropbox. Complemento Dropbox Cambiar orden Mostrar @@ -1003,8 +1000,8 @@ Punto Nombre del archivo GPX Archivo GPX guardado en {0} - Este complemento proporciona un widget en el mapa, permitiendo crear caminos pulsando el mapa, usando o modificando archivos GPX existentes, para planificar un viaje y medir la distancia entre puntos. Los resultados pueden guardarse como un archivo GPX y usarse luego para la orientación. - Distancias y planificación + Crea caminos pulsando en el mapa, o usando o modificando archivos GPX existentes, para planificar un viaje y medir la distancia entre puntos. El resultado puede guardarse como un archivo GPX y usarse luego para la orientación. + Calculadora de distancias y herramienta de planificación * Pulse para marcar un punto. \n * Mantenga pulsado el mapa para quitar el punto anterior. \n * Mantenga pulsado en un punto para ver e incluir la descripción. @@ -1307,9 +1304,9 @@ Duración Distancia Grabación de viaje - Este complemento activa la funcionalidad para registrar y guardar tus trazas manualmente pulsando el widget de grabación GPX en el mapa, o automáticamente registrando todas tus rutas navegadas en un archivo GPX. -\n -\nLas trazas grabadas pueden ser compartidas con sus amigos o ser usadas para contribuir a OSM. Los atletas pueden usar las trazas grabadas para seguir sus entrenamientos. Algunos análisis básicos de trazas se pueden realizar directamente en OsmAnd, como tiempos por vuelta, velocidad media, etc., y por supuesto las trazas pueden analizarse posteriormente con herramientas de análisis de terceros. + Este complemento activa la funcionalidad para registrar y guardar tus trazas manualmente pulsando el widget de grabación GPX en el mapa, o automáticamente registrando todas tus rutas navegadas en un archivo GPX. +\n +\nLas trazas grabadas pueden ser compartidas con tus amigos o ser usadas para contribuir a OSM. Los atletas pueden usar las trazas grabadas para monitorizar sus entrenamientos. Algunos análisis básicos de trazas pueden realizarse directamente en OsmAnd, como tiempos por vuelta, velocidad media, etc., y por supuesto las trazas pueden analizarse posteriormente con herramientas de análisis de terceros. Rutas de autobús, trolebús y lanzadera Intervalo de registro Registra la ubicación en un archivo GPX, pudiendo des/activarlo usando el widget de grabación GPX en el mapa. @@ -1433,7 +1430,7 @@ Pista de entrenamiento Sólo caminos Compartir nota - Notas + Notas A/V Mapa en línea Exportar Audio @@ -1781,7 +1778,7 @@ Se ofrece la opción de controlar la aplicación principalmente a través del panel de control flexible o de un menú estático. Se puede cambiar esto luego, en los ajustes del panel. Usar panel de control Usar menú - El botón del menú, muestra el panel de control en lugar del menú + El botón del menú muestra el panel de control en lugar del menú Acceso desde el mapa Indica el tipo de PDI correcto u omítelo. Sin escaleras @@ -1880,7 +1877,7 @@ \nSe utiliza {3} MB temporalmente, {1} MB constantemente. (De {2} MB.) Elegir marcador del mapa Otros marcadores - Subir notas de OSM anónimas o usar el perfil de OpenStreetMap.org. + Sube tu nota de OSM de forma anónima o usando tu perfil de OpenStreetMap.org. Subir nota(s) de OSM Subir anónimamente Barra superior @@ -1910,7 +1907,7 @@ \nParte de los ingresos vuelven a la comunidad de OSM y se paga por cada contribución OSM. \nSi amas a OsmAnd, OSM y quieres apoyarlos y ser apoyado por ellos, esta es una perfecta manera de hacerlo. Mostrar barra de transparencia en el mapa - Se ha cambiado a la memoria interna, porque la carpeta de almacenamiento de datos elegida es de sólo lectura. Elige un directorio de almacenamiento válido. + Se ha cambiado a la memoria interna, porque la carpeta de almacenamiento de datos elegida está protegida de escritura. Elige un directorio de almacenamiento válido. Memoria compartida La aplicación ya permite escribir en el almacenamiento externo, pero se debe reiniciar la aplicación. Subir ↑ @@ -1918,7 +1915,7 @@ Finalizar navegación Evitar camino Informe completo - Nombre de usuario y contraseña de OpenStreetMap + Nombre de usuario y contraseña de OSM Informe Añade marcadores a través del mapa No se encontraron puntos de referencia @@ -2009,16 +2006,16 @@ Necesario para descargar mapas. Buscando la ubicación… Espacio libre - Almacenamiento de datos de OsmAnd (para mapas, archivos GPX, etc.): %1$s. + Almacenamiento de datos de OsmAnd (para mapas, trazas, etc.): %1$s. Conceder permiso Permitir el acceso a la ubicación Obtenga direcciones y descubra lugares nuevos, sin una conexión a Internet Encontrar mi ubicación Omite la búsqueda de nuevas versiones o descuentos relacionados con OsmAnd. Ocultar nuevas versiones - Suscripción mensual. Puede cancelarlo en cualquier momento en Google Play. - Donaciones a la comunidad de OpenStreetMap - Parte de tu donación se envía a usuarios que realicen cambios en OpenStreetMap. El costo de la suscripción sigue siendo la misma. + Suscripción mensual. Puedes cancelarla en cualquier momento en Google Play. + Donación a la comunidad de OSM + Parte de tu donación se envía a los contribuidores a OSM. El coste de la suscripción sigue siendo el mismo. La suscripción permite actualizaciones cada hora, día o semana y descargas ilimitadas para los mapas de todo el mundo. Obtener Obtener por %1$s @@ -2032,8 +2029,8 @@ Subir PDI Cálculo de la ruta Ciudad o región - Sin archivos GPX aún - También puedes añadir archivos GPX a la carpeta + No tienes archivos de trazas aún + También puedes añadir archivos de trazas a la carpeta Añadir más… Aspecto Notificaciones @@ -2047,7 +2044,7 @@ Usar autopistas Permite usar autopistas. Activar la grabación rápida - Muestra una notificación del sistema que permite la grabación del viaje. + Muestra una notificación del sistema que permite empezar la grabación del viaje. Viaje Grabado Grabar @@ -2121,7 +2118,7 @@ Un botón que añade una nota fotográfica en el centro de la pantalla. Un botón que añade una nota de OSM en el centro de la pantalla. Un botón que añade un PDI en el centro de la pantalla. - Un botón que des/activa las indicaciones por voz durante la navegación. + Un botón que activa o desactiva las indicaciones por voz durante la navegación. Un botón que añade la ubicación del aparcamiento en el centro de la pantalla. Mostrar un diálogo temporal " guardado como " @@ -2188,7 +2185,7 @@ \nProporciona un código completo OLC completo y válido. \nÁrea representada: %1$s x %2$s - Un botón que muestra la siguiente lista. + Un botón para paginar a través de la lista de abajo. Mapa superpuesto cambiado a «%s». Mapa subyacente cambiado a «%s». Pendiente @@ -2286,15 +2283,15 @@ \n ¡Más países alrededor del globo están disponibles para descargar! \n Obtén un navegador confiable en tu país - ya sea Francia, Alemania, México, Reino Unido, España, Países bajos, Estados Unidos, Rusia, Brasil o cualquier otro. Alternar zoom automático del mapa - Un botón que des/activa el zoom automático del mapa de acuerdo a la velocidad. - Activar zoom automático del mapa - Desactivar zoom automático del mapa + Botón para activar o desactivar el zoom automático controlado por la velocidad. + Activar zoom automático + Desactivar zoom automático Definir destino Reemplazar destino Añadir primer destino intermedio - Un botón que añade el destino de la ruta en el centro de la pantalla, cualquier destino previamente elegido se convierte en el último destino intermedio. - Este botón de acción, añade un nuevo destino de ruta en el centro de la pantalla, reemplazando el anterior destino (si existe). - Un botón que añade el primer destino intermedio en el centro de la pantalla. + Un botón que añade el centro de la pantalla como destino de la ruta, cualquier destino previamente elegido se convierte en el último destino intermedio. + Un botón que añade el centro de la pantalla como destino de la nueva ruta, reemplazando el anterior destino (si existe). + Un botón que añade el centro de la pantalla como el primer destino intermedio. Sin superposición Sin subyacencia Error @@ -2327,13 +2324,14 @@ Ubicación propia animada Activa el desplazamiento animado del mapa para «Mi ubicación» durante la navegación. Resumen - Navegación GPS + Navegación \n • Funciona en línea (rápido) o sin conexión (sin cargos de roaming al viajar al extranjero) \n • Guía por voz giro-a-giro (voces grabadas y sintetizadas) -\n • (Opcional) Guía de carriles, nombres de calles y tiempo estimado al destino -\n • Soporta puntos intermedios en el itinerario +\n • (Opcional) Guía de carriles, nombres de calles y tiempo estimado de llegada +\n • Soporta puntos intermedios en tu itinerario \n • La ruta se recalcula al salirse de la misma -\n • Busca destinos por dirección, por tipo (por ejemplo: Restaurantes, hoteles, gasolineras, museos), o por coordenada geográfica +\n • Busca lugares por dirección, por tipo (por ejemplo: Restaurantes, hoteles, gasolineras, museos), o por coordenadas geográficas +\n Vista del mapa \n • Muestra tu ubicación y orientación \n • (Opcional) Ajusta el mapa a la dirección del movimiento (o la brújula) @@ -2397,7 +2395,7 @@ Mostrar u ocultar notas de OSM Mostrar notas de OSM Ocultar notas de OSM - Un botón que muestra u oculta las notas de OSM en el mapa. + Botón para mostrar u ocultar las notas de OSM en el mapa. Ordenados por distancia Buscar en Favoritos Ocultar desde el nivel de zoom @@ -2419,7 +2417,7 @@ Abrir Mapillary Instalar Mejorar cobertura de fotos con Mapillary - Instala Mapillary para añadir una o más fotos a esta ubicación del mapa. + Instala Mapillary para añadir fotos a esta ubicación del mapa. Fotos en línea Imagen de Mapillary Permisos @@ -2449,10 +2447,10 @@ Min/Máx Rosa translúcido Pausar/reanudar navegación - Este botón de acción, pausa o reanuda la navegación. + Botón para pausar o reanudar navegación. Mostrar diálogo «Navegación finalizada» Iniciar/parar navegación - Este botón de acción, inicia o para la navegación. + Botón para iniciar o terminar la navegación. Tiempo del búfer para el seguimiento en línea Indica el tiempo que el búfer mantendrá los lugares para enviar sin conexión Añadir al menos un punto. @@ -2468,7 +2466,7 @@ Punto de ruta 1 Punto de referencia 1 Sin animaciones - Desactiva las animaciones en la aplicación. + Desactiva las animaciones de los mapas. Mantener en el mapa ¿Salir sin guardar? Línea @@ -2492,9 +2490,9 @@ Mover todo al historial Indicación de distancia Ordenar por - Elige cómo se indica la distancia y dirección a los marcadores del mapa en la pantalla del mapa: + Elige cómo se indica la distancia y dirección a los marcadores del mapa en el mapa: Umbral de orientación del mapa - Velocidad a partir de la cual la orientación del mapa cambia de «Dirección del movimiento» a «Dirección de la brújula». + Selecciona la velocidad a partir de la que la orientación del mapa cambia de «Dirección del movimiento» a «Dirección de la brújula». Todos los marcadores del mapa movidos al historial Marcador del mapa movido al historial Marcador del mapa movido a los activos @@ -2603,8 +2601,8 @@ Pulsa un marcador en el mapa para moverlo al primer lugar de los marcadores activos, sin abrir el menú contextual. Activar «Una pulsación» ¡Toma notas! - Añade una nota de audio, vídeo o foto para cada punto del mapa, utilizando el widget o el menú contextual. - Notas por fecha + Añade una nota de audio, vídeo o foto a cualquier punto del mapa, utilizando el control o el menú contextual. + Notas A/V por fecha Por fecha Por tipo Aquí hay: @@ -2852,7 +2850,7 @@ Guaraní Está utilizando el mapa «{0}» que funciona con OsmAnd. ¿Quiere ejecutar la versión completa de OsmAnd\? ¿Ejecutar OsmAnd\? - Un botón que alterna entre el modo diurno y nocturno para OsmAnd. + Un botón que alterna entre los modos diurno y nocturno para OsmAnd. Modo diurno Modo nocturno Alternar modos diurno/nocturno @@ -2956,7 +2954,7 @@ Autopista Carretera/ruta estatal Carretera principal - Calle residencial + Calle Vía de servicio Acera Camino rural @@ -3103,7 +3101,7 @@ Elegir el tipo de navegación Automóvil, camión, motocicleta Bicicleta de montaña, ciclomotor, caballo - Caminata, senderismo, correr + Caminata, senderismo, carrera Tipos de transporte público Barco, remo, vela Avión, ala delta @@ -3139,7 +3137,7 @@ Dificultad preferida Preferir rutas de esta dificultad, aunque el trazado sobre pistas más duras o más fáciles sigue siendo posible si son más cortas. Fuera de pista - Los senderos libres y fuera de pista son rutas y pasajes no oficiales. Típicamente descuidados, no mantenidos por los oficiales y no controlados por la noche. Entrar bajo su propio riesgo. + Los senderos libres y fuera de pista son rutas y pasajes no oficiales. Típicamente descuidados, no mantenidos y no controlados por la noche. Entra bajo tu propio riesgo. Geocodificación Error Todo terreno @@ -3361,7 +3359,7 @@ El nombre de archivo está vacío Traza guardada Revertir - Un botón para centrar en la pantalla el punto de partida y calcular la ruta hacia el destino o abre un cuadro de diálogo para elegir el destino si el marcador no está en el mapa. + Un botón que añade el centro de la pantalla como punto de partida. Pedirá luego que se fije el destino o iniciará el cálculo de la ruta. Mostrar nodo de la red de rutas ciclistas ¿Borrar %1$s\? Diálogo de descarga del mapa @@ -3394,7 +3392,7 @@ Elegir el color Los perfiles predefinidos de OsmAnd no pueden borrarse , pero sí desactivarse (en la pantalla anterior), o moverse a la parte inferior. Editar perfiles - El \'tipo de navegación\' domina como se calculan las ruta. + El \'Tipo de navegación\' determina cómo se calculan las rutas. Apariencia del perfil Icono, color y nombre Editar lista de perfiles @@ -3679,7 +3677,7 @@ Buscar tipos de PDI Acción %1$s no admitida Mapa general del mundo (detallado) - Tipo no admitido + Tipo no soportado Proporciona la anchura de tu vehículo, algunas restricciones de ruta pueden aplicarse para vehículos anchos. Proporciona la altura de tu vehículo, algunas restricciones de ruta pueden aplicarse para vehículos altos. Proporciona el peso de tu vehículo, algunas restricciones de ruta pueden aplicarse para vehículos pesados. @@ -3715,7 +3713,7 @@ Elige cómo se guardarán las teselas descargadas. Puede exportar o importar acciones rápidas con perfiles de aplicación. ¿Eliminar todo\? - ¿Estás seguro de que deseas eliminar irrevocablemente% d acciones rápidas\? + ¿Estás seguro deseas eliminar de forma irreversible %d acciones rápidas\? Tiempo de apagado de la pantalla Si \"%1$s\" está encendido, el tiempo de actividad dependerá de ello. metros @@ -3740,7 +3738,7 @@ Urdu Tayiko Bávaro - Rastreador OsmAnd + Trazador OsmAnd La guía para la simbología del mapa. Posiciones de estacionamiento Deshabilitado. Requiere \'Mantener la pantalla encendida\' dentro de \'Tiempo de espera después de la activación\'. @@ -3849,7 +3847,7 @@ Distancia umbral Perfil de navegación Selecciona un archivo de traza al que agregar el nuevo segmento. - Imágenes a pie de calle + Fotos a pie de calle ¿Estás seguro de que quieres descartar todos los cambios en la ruta planeada cerrándola\? En caso de dirección contraria Guardar como nuevo archivo de traza @@ -3897,4 +3895,4 @@ Último modificado Nombre: Z – A Nombre: A - Z - + \ No newline at end of file diff --git a/OsmAnd/res/values-et/phrases.xml b/OsmAnd/res/values-et/phrases.xml index 6d72d50576..4cb42eee67 100644 --- a/OsmAnd/res/values-et/phrases.xml +++ b/OsmAnd/res/values-et/phrases.xml @@ -524,7 +524,7 @@ Oktaan 95 Oktaan 98 Oktaan 100 - CNG + Surugaas 1:25 kütus 1:50 kütus Etanool @@ -3826,4 +3826,5 @@ Väikesed elektriseadmed Mesitaru Pähklipood + Veeldatud maagaas \ No newline at end of file diff --git a/OsmAnd/res/values-et/strings.xml b/OsmAnd/res/values-et/strings.xml index e219936c08..8e58fdad52 100644 --- a/OsmAnd/res/values-et/strings.xml +++ b/OsmAnd/res/values-et/strings.xml @@ -40,7 +40,7 @@ Start Stop Import - Eksport + Ekspordi Veel… Veel tegevusi Ära enam näita @@ -910,7 +910,7 @@ Seadista teekonna parameetrid Teekonna parameetrid Rakenduse profiiliks muudetud \"%s\" - Logcat puhver + Logcati puhver Laienduse seaded Vaikimisi Selle ala vaatlemiseks lae alla üksikasjalik %s kaart. @@ -3364,7 +3364,7 @@ OSM Ikooni kuvatakse vaid navigeerimise või liikumise ajal. Peatumisel näidatav ikoon. - Kontrolli ja jaga rakenduse detailseid logisid + Vaata ja jaga rakenduse detailseid logisid Geokavatsuse väärtusest \'%s\' ei saanud aru. Selle valiku kasutamine vajab luba. See on madala kiirusega väljalülitusfilter, et mitte salvestada punkte, mis jäävad alla teatud kiiruse. See võib muuta salvestatud rajad kaardil vaadates sujuvamaks. @@ -3497,7 +3497,7 @@ Kohandatud värv Jätkamiseks vali tööpäevad Teekond punktide vahel - Kavanda teekonda + Kavanda teekond Lisa rajale Näita alguse ja lõpu ikoone Vali laius @@ -3762,4 +3762,6 @@ Viimati muudetud Nimi: Z – A Nimi: A – Z + Ekraani väljalülitamine + Ratastool edasi \ No newline at end of file diff --git a/OsmAnd/res/values-fa/phrases.xml b/OsmAnd/res/values-fa/phrases.xml index eba643a4f9..dd6c374db3 100644 --- a/OsmAnd/res/values-fa/phrases.xml +++ b/OsmAnd/res/values-fa/phrases.xml @@ -9,7 +9,7 @@ فروشگاه گوشت بقالی فروشگاه محصولات دامی - سبزی فروشی + میوه و سبزی‌فروشی فروشگاه غذاهای دریایی شیرینی و آجیل فروشی بستنی فروشی diff --git a/OsmAnd/res/values-fa/strings.xml b/OsmAnd/res/values-fa/strings.xml index 4d8d47c875..68b4a4511d 100644 --- a/OsmAnd/res/values-fa/strings.xml +++ b/OsmAnd/res/values-fa/strings.xml @@ -3278,7 +3278,7 @@ تور اسکی مسیرها برای تور اسکی. کمپر - ون کمپر + ون کمپر (RV) واگن کامیون پیک‌آپ تحلیل‌ها @@ -3553,7 +3553,7 @@ پروفایل سفارشی زاویه: ‎%s° زاویه - تا مسیریابی مجدد انجام شود، از موقعیت من تا مسیر محاسبه‌شده پاره‌خط مستقیمی نمایش داده می‌شود + تا مسیریابی مجدد انجام نشده، از موقعیت من تا مسیر محاسبه‌شده پاره‌خط مستقیمی نمایش داده می‌شود کمترین زاویه میان موقعیت من و مسیر آماده‌سازی چیزی انتخاب نشده @@ -3873,7 +3873,7 @@ ذخیره به‌عنوان رد جدید برعکس‌کردن مسیر تمام رد با استفاده از پروفایل انتخابی بازمحاسبه خواهد شد. - با استفاده از پروفایل انتخابی فقط پارهٔ بعدی بازمحاسبه خواهد شد. + فقط پارهٔ بعدی با استفاده از پروفایل انتخابی بازمحاسبه خواهد شد. همهٔ پاره‌های بعدی پارهٔ قبلی همهٔ پاره‌های قبلی @@ -3928,4 +3928,9 @@ بازهٔ زمانی برای ضبط رد را انتخاب کنید (که از طریق ابزار ضبط سفر روی نقشه فعال می‌شود). نگه‌داشتن ضبط سفر ازسرگیری ضبط سفر + اسکیت این‌لاین + موتور پرشی + اسکوتر موتوری + ویلچر رو به جلو + فاصله آستانه \ No newline at end of file diff --git a/OsmAnd/res/values-fr/phrases.xml b/OsmAnd/res/values-fr/phrases.xml index 638995c1f2..91dc180070 100644 --- a/OsmAnd/res/values-fr/phrases.xml +++ b/OsmAnd/res/values-fr/phrases.xml @@ -1762,7 +1762,7 @@ Fournitures de plomberie Fournitures de bois Ancres pour vélo - Râtelier pour vélo + Arceaux pour vélo Terminal d\'informations Carte tactile Tableau d\'affichage @@ -3380,7 +3380,7 @@ Parc animalier Enceinte Parc safari - Râtelier pour vélo + Arceaux pour vélo Vélo de sport Hachoir Hors route diff --git a/OsmAnd/res/values-fr/strings.xml b/OsmAnd/res/values-fr/strings.xml index 91daebce7f..41007cc7eb 100644 --- a/OsmAnd/res/values-fr/strings.xml +++ b/OsmAnd/res/values-fr/strings.xml @@ -3412,7 +3412,7 @@ OSM Icône affiché pendant la navigation ou en déplacement. Icône affiché à l\'arrêt. - Consultez et partagez les journaux de l\'application (pour debug) + Vérifier et partager les logs détaillés de l\'application Vous n\'êtes pas autorisés à utiliser cette option. Intervalle de suivi Adresse web @@ -3883,4 +3883,6 @@ Dernière modification Nom : Z – A Nom : A – Z + Icônes de départ / arrivée + Merci pour votre achat de \'Courbes de niveaux\' \ No newline at end of file diff --git a/OsmAnd/res/values-hu/phrases.xml b/OsmAnd/res/values-hu/phrases.xml index 4e0cce87d3..0234cd76a2 100644 --- a/OsmAnd/res/values-hu/phrases.xml +++ b/OsmAnd/res/values-hu/phrases.xml @@ -294,7 +294,7 @@ Forgalomlassító útszűkület Gázolaj Biodízel - Cseppfolyós gáz (LPG) + LPG (cseppfolyósított PB-gáz) 80-as oktánszámú 91-es oktánszámú 92-es oktánszámú @@ -3835,4 +3835,5 @@ Ivóvíz-utántöltés Mag- és aszaltgyümölcsbolt Méhkaptár + LNG (cseppfolyósított földgáz) \ No newline at end of file diff --git a/OsmAnd/res/values-hu/strings.xml b/OsmAnd/res/values-hu/strings.xml index 97f76c0623..b903ccb8da 100644 --- a/OsmAnd/res/values-hu/strings.xml +++ b/OsmAnd/res/values-hu/strings.xml @@ -3277,7 +3277,7 @@ Térképnézet Alapértelmezés visszaállítása Másolás egy másik profilból - Logcat-puffer + Logcat-puffer (hibanapló) Alapértelmezés szerint Hópark Lovas szán @@ -3424,8 +3424,8 @@ OSM-szerkesztés Az összes még fel nem töltött szerkesztés vagy OSM-hiba megtalálható a %1$s helyen. A már feltöltött pontok nem láthatók az OsmAndban. OSM - Az ikon navigáció vagy haladás közben jelenik meg. - Az ikon álló helyzetben jelenik meg. + Navigáció vagy haladás közben megjelenő ikon. + Álló helyzetben megjelenő ikon. Az alkalmazás részletes naplóinak ellenőrzése és megosztása A beállítás használatához engedélyre van szükség. Kategóriák átrendezése @@ -3896,4 +3896,6 @@ Utolsó módosítás Név: Z–A Név: A–Z + Kiindulás/érkezés ikonjai + Köszönjük, hogy megvásárolta a szintvonalbővítményt (Contour lines) \ No newline at end of file diff --git a/OsmAnd/res/values-it/strings.xml b/OsmAnd/res/values-it/strings.xml index e806eff554..919b6123a8 100644 --- a/OsmAnd/res/values-it/strings.xml +++ b/OsmAnd/res/values-it/strings.xml @@ -1954,7 +1954,7 @@ Navigazione OsmAnd Live Livello della batteria Ungherese (formale) - Tracciato attuale + Traccia attuale Cambia posizione del marcatore Spagnolo americano Asturiano @@ -3620,7 +3620,7 @@ Mappa mondiale generale (dettagliata) Mappe extra Azione non supportata %1$s - OsmAnd tracker + Tracker OsmAnd OsmAnd + Mapillary Azione veloce Righello radiale @@ -3812,7 +3812,7 @@ Aggiungi ad una Traccia Seleziona l\'intervallo a cui i segnaposti con distanza o orario sulla traccia verranno mostrati. Seleziona l\'opzione desiderata per la divisione: per tempo o per distanza. - Frecce delle direzioni + Frecce della direzione Seleziona larghezza Ultima modificata Importa una traccia @@ -3900,7 +3900,9 @@ \n \n Traccia semplificata - Ultimo modificato + Cronologico Nome: Z – A Nome: A – Z + Icona Partenza/Arrivo + Grazie per l\'acquisto del \'Plugin delle curve di livello\' \ No newline at end of file diff --git a/OsmAnd/res/values-iw/phrases.xml b/OsmAnd/res/values-iw/phrases.xml index c625ca9fdd..d593ffb9d0 100644 --- a/OsmAnd/res/values-iw/phrases.xml +++ b/OsmAnd/res/values-iw/phrases.xml @@ -1390,7 +1390,7 @@ שיתוף רכב שיתוף סירות מעגן - בית שימוש + בית שימוש;אסלה;טואלט מקלחת סאונה בית בושת @@ -2043,7 +2043,7 @@ אגרה למשאיות כן כן - לא + שמע: לא רק כאשר מותר לחצות תחנת הצלה אזור שירות @@ -2119,7 +2119,7 @@ דלק 100LL גפ״מ דלק מטוסים A-1 - תוסף AdBlue + נוזל מפלט לדיזל כספומט מקום בנסיעה שיתופית וסליאן @@ -2159,4 +2159,19 @@ כן מצוק טיפוס קטגוריית קושי + צינור + רשת מילוי מי שתייה + מילוי מי שתייה: אין + כן + תיבת תרומה + חץ: אין + כן + כן + מעלית + זמן אמת + השהיה + כן + לוח זמנים + כוורת דבורים + חנות אגוזים \ No newline at end of file diff --git a/OsmAnd/res/values-iw/strings.xml b/OsmAnd/res/values-iw/strings.xml index 7f556c9926..42a840fa87 100644 --- a/OsmAnd/res/values-iw/strings.xml +++ b/OsmAnd/res/values-iw/strings.xml @@ -2103,7 +2103,7 @@ ביטול הבחירה למחוק הכול שיתוף - יצוא + ייצוא עוד… שמירת הבחירה שגיאה בלתי צפויה @@ -3248,7 +3248,7 @@ התצורה הנבחרת תחול בכל רחבי היישומון. הגדרה זו נבחרת כבררת מחדל על הפרופילים: %s החלפת הגדרה - זיכרון זמני של Logcat + מכלא Logcat כבררת מחדל הגדרות תוסף הורדת המפה המפורטת %s, כדי לצפות באזור זה. @@ -3428,7 +3428,7 @@ סמל שמופיע רק בעת ניווט או תזוזה. סמל שמופיע במנוחה. דירוג - לבדוק ולשתף תיעוד מפורט של יומני היישומון + בדיקה ושיתוף יומני תיעוד מפורטים של היישומים נדרשת הרשאה כדי להשתמש באפשרות הזו. סידור הקטגוריות מחדש ניתן להחליף את סדר הרשימה ולהסתיר קטגוריות בלתי נחוצות. אפשר לייבא או לייצא את כל השינויים עם פרופילים. @@ -3909,4 +3909,6 @@ שינוי אחרון שם: ת – א שם: א – ת + סמלי התחלה/סיום + תודה לך על רכישת ‚קווי מתאר’ \ No newline at end of file diff --git a/OsmAnd/res/values-ja/phrases.xml b/OsmAnd/res/values-ja/phrases.xml index 680da76205..66c5cc2a31 100644 --- a/OsmAnd/res/values-ja/phrases.xml +++ b/OsmAnd/res/values-ja/phrases.xml @@ -1258,7 +1258,7 @@ 有り 掃除機:無し 掃除機 - アドブルー・尿素水還元剤 + ディーゼル排気用液(AdBlue・尿素水) ドライブスルー 有り 無し @@ -3822,4 +3822,16 @@ 吸引 加圧 地下水 + ナッツ専門店 + 養蜂箱 + リアルタイム時刻表 + 一般的な時刻表 + 大まかな時刻表 + 有り + 時刻表:無し + エレベーター + 街区 + 行政区 + ギブボックス(提供品置場) + 簡易給水栓 \ No newline at end of file diff --git a/OsmAnd/res/values-ja/strings.xml b/OsmAnd/res/values-ja/strings.xml index 80f0b62286..35f9146fc7 100644 --- a/OsmAnd/res/values-ja/strings.xml +++ b/OsmAnd/res/values-ja/strings.xml @@ -1190,7 +1190,7 @@ POIの更新は利用できません "出発時刻: %1$tF, %1$tT " "到着時刻: %1$tF, %1$tT " "平均速度: %1$s " - "最高速度: %1$s " + 最高速度: %1$s 平均標高: %1$s 標高差: %1$s 上り/下り: %1$s @@ -1350,7 +1350,7 @@ POIの更新は利用できません 画面の電源オン設定 方向転換地点に近づいたらデバイスの画面を(オフの場合指定時間)オンにします しない - 除外する道の指定… + 避ける道の指定… 電車でのルート 路面電車でのルート タクシーのルート共有 @@ -1625,7 +1625,7 @@ POIの更新は利用できません 所属ネットワークに応じたカラー変更 OSMCのハイキングシンボルカラー 被災域 - 太線 + 輪郭強調 更新はありません ライブ更新 ユーザーからの意見やフィードバックを大切にしています。 @@ -1816,7 +1816,7 @@ POIの更新は利用できません 上に移動 下に移動 ナビゲーションの終了 - 使用しない道路として指定 + 避ける道を指定 選択したデータ保存フォルダーが書き込み保護されているため、内部メモリに切り替えました。書き込み可能な保存用ディレクトリを選択してください。 共有記憶域 より詳細なレポートは以下サイトにて @@ -2496,7 +2496,7 @@ POIの更新は利用できません 今年 全て履歴に移動 距離表示 - 並び順の変更 + 並び順: マップ上に表示し続ける 保存せずに終了しますか? @@ -3010,9 +3010,9 @@ POIの更新は利用できません アイコン選択 基本プロファイル アイコン - 最低速度 - 最高速度 - 標準移動速度 + 予想最低速度 + 予想最高速度 + 予想標準速度 すべての道路の移動速度を制限し、種別や制限速度が不明な道路が多い場合の到着時間予測に役立ちます(ルート計算に影響します) オフロード プロファイルの個別設定 @@ -3590,7 +3590,7 @@ POIの更新は利用できません POIの作成/編集 駐車位置 お気に入りの追加/編集 - アイテム順序をデフォルトに戻す + アイテム順序を初期状態に戻します 編集に戻る 選択したプロファイルを切り替えるボタンです。 プロファイルの追加 @@ -3667,12 +3667,12 @@ POIの更新は利用できません コンテキストメニュー 項目の並べ替えや非表示するものを指定できます。 分割 - 分割線で区切られた部分より下にある項目が適用されます。 + ここで指定された項目は、区切り線より下に配置されます。 非表示 これらの項目はメニューに表示されなくなりますが、オプションやプラグインはそのまま機能します。 項目 設定を非表示にすると、元の状態にリセットされます。 - ボタンは4つしかありません。 + ボタンの数は4つ固定で変更できません。 主要機能 “%1$s”ボタンをタップすると、これらの機能にアクセスできます。 アイテムはこのカテゴリ内でのみ移動できます。 @@ -3794,7 +3794,7 @@ POIの更新は利用できません \n \n国の法律に基づいて、使用を望むかどうかを決定する必要があります。 \n -\n%1$sを選択すると、スピードカメラに関するアラートと警告が表示されます。 +\n%1$sを選択すると、スピードカメラに関するアラートと警告機能を使用できます。 \n \n%2$sを選択すると、スピードカメラに関するすべてのデータ(警告、通知、POI)が、OsmAndの再インストールを行うまで削除されます。 機能を維持 diff --git a/OsmAnd/res/values-nb/strings.xml b/OsmAnd/res/values-nb/strings.xml index 32c34dd961..9c24c2aa7c 100644 --- a/OsmAnd/res/values-nb/strings.xml +++ b/OsmAnd/res/values-nb/strings.xml @@ -751,7 +751,7 @@ Sikker modus Programmet kjører i sikker modus (skru det av i \'Innstillinger\'). Talemeldinger stopper midlertidig musikkavspilling. - Avbryt musikk + Sett musikk på pause Offentlig Optimaliser kart for Vis fra zoom-nivå (krever kotedata): @@ -1650,7 +1650,7 @@ Valgt valgte FJERN MERKELAPPEN - Last ned nattlige utviklerversjoner. + Last ned aktuelle utviklingsversjoner. Byggversjoner Spesifiser en mellomtjener. Innlogget som %1$s @@ -2142,7 +2142,7 @@ Vis/skjul OSM-notater Vis OSM-notater Skjul OSM-notater - Å trykke denne handlingsknappen viser eller skjuler OSM-notater på kartet. + Knapp til å vise eller skjule OSM-notater på kartet. Takk for at du kjøpte \'Havdybdekonturer\' Havdybdekonturer Havdybdekonturer @@ -2257,17 +2257,17 @@ Rediger handling Slett handling Navneforvalg - Trykk på denne handlingsknappen legger til en kartmarkør i skjermsenteret. - Trykking på denne handlingsknappen legger til et GPX-rutepunkt i midten av skjermen. - Trykking på denne handlingsknappen legger til et lydnotat i midten av skjermen. - Trykking på denne handlingsknappen legger til et videonotat i midten av skjermen. - Trykking på denne handlingsknappen legger til et bildenotat i midten av skjermen. - Trykking på denne handlingsknappen legger til et OSM-notat i midten av skjermen. - Trykking på denne handlingsknappen slår av eller på taleveiledning under navigering. - Trykking på denne handlingsknappen legger til en parkeringsplass i midten av skjermen. + En knapp for å legge til en kartmarkør i skjermsenteret. + En knapp for å legge til et GPX-rutepunkt i midten av skjermen. + En knapp for å legge til et lydnotat i midten av skjermen. + En knapp for å legge til et videonotat i midten av skjermen. + En knapp for å legge til et bildenotat i midten av skjermen. + En knapp for å legge til et OSM-notat i midten av skjermen. + En knapp til å slå av eller på taleveiledning under navigering. + En knapp for å legge til en parkeringsplass i midten av skjermen. Vis en midlertidig dialog " lagret i " - Trykking på denne handlingsknappen viser eller skjuler favorittpunktene på kartet. + En knapp til å vise eller skjule favorittpunktene på kartet. Opprett elementer La stå tomt for å bruke adressen eller stedsnavnet. Denne meldingen inkluderes i kommentarfeltet. @@ -2341,10 +2341,10 @@ Et trykk på kartet skjuler/viser kontrollknappene og miniprogrammene. Marker som passert Kunne ikke endre notatet. - Trykking på denne handlingsknappen slår av/på automatisk kartforstørrelse i henhold til hastigheten din. - Trykking på denne handlingsknappen gjør skjermsenteret til rutemål, ethvert tidligere valgt reisemål blir det siste mellomliggende målet. - Trykking på denne handlingsknappen gjør skjermsenteret det nye rutemålet, og erstatter det tidligere valgte reisemålet (hvis noe). - Trykking på denne handlingsknappen gjør skjermsenteret til det første mellomliggende reisemålet. + Knapp til å slå av eller på hastighetsbasert auto-zoom. + En knapp for å gjøre skjermsenteret til rutemålet, et tidligere valgt reisemål blir det siste mellomliggende målet. + En knapp for å gjøre skjermsenteret til det nye rutemålet, erstatter det tidligere valgte reisemålet (hvis noe). + En knapp for å gjøre skjermsenteret til det første mellomliggende reisemålet. Abonner på vår e-postliste om programrabatter og få tre kartnedlastinger til! Havdybdepunkter for sørlige halvkule Havdybdepunkter for nordlige halvkule @@ -2382,7 +2382,7 @@ Aktiver \"sjøkartvisning\" -tillegget Navnet inneholder for mange store bokstaver. Fortsett\? Legg til interessepunkt - Trykking på denne handlingsknappen viser eller skjuler interessepunkter på kartet. + En knapp til å vise eller skjule interessepunkter på kartet. En knapp til å bla gjennom listen nedenfor. Fyll ut alle parametere Ved å trykke lenge og dra knappen endres dens plassering på skjermen. @@ -2408,7 +2408,7 @@ Alle punkter i gruppen GPX-fil med de valgte notatenes koordinater og data. GPX-fil med alle notaters koordinater og data. - Trykking på denne handlingsknappen legger til et interessepunkt i midten av skjermen. + En knapp for å legge til et interessepunkt i midten av skjermen. Åpen fra Åpen til Stenger @@ -2859,10 +2859,10 @@ Du bruker {0} kart levert av OsmAnd. Vil du starte fullversjonen av OsmAnd\? Kjør OsmAnd\? Guaraní - Vekselvender mellom dag- og nattmodus i OsmAnd + En knapp til å skifte mellom dag- og nattmodus i OsmAnd. Dagmodus Nattmodus - Veksle dag-/nattmodus + Bytt dag/natt-modus Offentlig transport Sett reisemål Legg til mellomliggende @@ -3256,7 +3256,7 @@ OsmAnd-profil: %1$s Profil-import Hvit - Brukes til å estimere ankomsttid for ukjente veityper, og for å begrense farten på alle veier (kan endre rute) + Estimerer ankomsttid for ukjente veityper og begrenser farten for alle veier (kan påvirke ruting) Spor-lagringsmappe Spor kan lagres i \'rec\'-mappen, månedlige eller daglige mapper. Ta opp spor til \'rec\'-mappen @@ -3286,13 +3286,13 @@ \n \n Du kan bruke denne endringen på alle eller bare på den valgte profilen. - En bryter for å vise eller skjule koter på kartet. + Knapp som viser eller skjuler koter på kartet. Eksporter profil \"%1$s\" finnes allerede. Overskriv\? Kunne ikke eksportere profil. Legg til en profil ved å åpne profilfilen med OsmAnd. - %1$s feil under import: %2$s - %1$s ble importert. + %1$s importfeil: %2$s + %1$s importert. Bytt %1$s og %2$s Startpunkt Isvei @@ -3345,20 +3345,20 @@ Åpen plasseringskode (OLC) Valgt format vil benyttes i programmet. Denne innstillingen er valgt som forvalg for profiler: %s - Bruk kun for «%1$s» - Bruk for alle profiler + Bruk kun for \"%1$s\" + Bruk på alle profiler Analyseinstrumenter Vis kart på låseskjermen under navigering. - Innstillinger for ruting i valgt profil «%1$s». + Innstillinger for ruting i den valgte profilen \"%1$s\". Tidsavbrudd etter oppvåkning Varsler vises nede til venstre under navigering. Kart under navigasjon Kart under navigasjon Stemmekunngjøringer finner kun sted under navigasjon. Navigasjonsinstruks og kunngjøringer - Stemmekunngjøringer + Talemeldinger Sett opp ruteparameter - Ruteparameter + Ruteparametere Last ned detaljert %s-kart for å vise dette området. Slede Akebrett @@ -3398,13 +3398,13 @@ Personlig Laster ned %s Tykk - For ørkener og andre tynt befolkede områder. Høyere detaljnivå. + For ørkener og andre tynt befolkede områder. Mer detaljert. Posisjonsikon under bevegelse Posisjonsikon i hviletilstand Trykk på \'Bruk\' sletter fjernede profiler permanent. Hovedprofil Velg farge - Du kan ikke slette forvalgsprofilene, men du kan skru dem av før dette steget, eller flytte dem til bunnen. + OsmAnd-Standardprofiler kan ikke slettes, men deaktiveres (på forrige skjermbilde), eller bli sortert til bunnen. Rediger profiler Navigasjonstype har innvirkning på regler for ruteberegning. Profilutseende @@ -3418,8 +3418,8 @@ %1$s %2$s Importer profil OSM - \"%1$s\"-filen inneholder ingen ruteplanleggingsregler, velg en annen fil. - Ustøttet filtype. Du må velge en fil med %1$s-filendelse. + Ingen rutingsregler i \'%1$s\'. Velg en annen fil. + Velg en støttet fil med %1$s-endelse isteden. Importer fra fil Importer ruteplanleggingsfil Navigasjon, loggingsnøyaktighet @@ -3429,7 +3429,7 @@ Tillater deg å dele nåværende plassering ved bruk av turopptak. Nettbasert sporing Loggingsnøyaktighet - Du kan finne alle dine innspilte spor i %1$s, eller i OsmAnd-mappen. + Dine innspilte spor er i %1$s, eller i OsmAnd-mappen. Du finner alle dine OSM-notater i %1$s. Videonotater Bildenotater @@ -3601,7 +3601,7 @@ Sjekk og del detaljert loggføring fra programmet Bruk systemets skjermtidsavbrudd Programtillegg av - Ingen omregning + Ingen ny beregning Angi et navn for profilen Velg data å importere. Du kan lese mer om løyper i %1$s. @@ -3627,7 +3627,7 @@ \nSkru av ubrukte programtillegg for å skjule alle deres styringskontroller. %1$s. Disse elementene er skjult fra menyen, men de representerte valgene eller programtilleggene vil fortsette å virke. Velg språkene Wikipedia-artikler skal vises på i kartet. Du kan bytte mellom alle tilgjengelige språk mens du leser artikkelen. - Veiledning til kartets merking. + Veiledning til kartets symbolbruk. Ruteplanlegging Minsteavstand for å beregne rute på nytt OsmAnd har allerede elementer med samme navn som de i importen. @@ -3662,7 +3662,7 @@ Fotoboks-interessepunkter Avinstaller Du kan sette fartøyhøyde for å unngå lave broer. Hvis broen endrer høyde, brukes høyden i åpen tilstand. - Slett neste målpunkt + Slett nærmeste målpunkt Navngi punktet Vis/skjul Mapillary Skjul Mapillary @@ -3675,19 +3675,19 @@ Navigasjonsinstruks - + Avinstaller og start på nytt Rullestol Gokart Planlegg en rute Du får tilgang til disse handlingene ved å trykke på knappen “%1$s”. - Slå på for å stille inn zoomnivået på kartet med enhetens volumknapper. + Styr zoomnivået på kartet med enhetens volumknapper. Det tillagte punktet vil ikke være synlig på kartet, siden den valgte gruppen er skjult, du kan finne det i \"%s\". Scooter Rullestol framover - Du må definere arbeidsdagene for å fortsette + Still inn arbeidsdager for å fortsette Legg til i et spor - Vis ikoner for start-mål + Vis ikoner for start og mål Velg bredde Velg intervallet hvor markeringer med avstand eller tid på sporet vil vises. Velg det ønskede oppdelingsalternativet: etter tid eller etter avstand. @@ -3702,7 +3702,7 @@ Sist redigert Importer spor Åpne eksisterende spor - Velg en sporfil for åpning. + Velg en sporfil for å åpne. Snu rute Overskriv spor Hele sporet blir beregnet på nytt med den valgte profilen. @@ -3722,7 +3722,7 @@ Importer eller ta opp sporfiler Følg spor Velg sporfil å følge - Velg sporfil å følge, eller importer en. + Velg sporfil å følge eller importer fra enheten din. Velg et annet spor Starten av sporet Nærmeste punkt @@ -3747,4 +3747,15 @@ Sist endret Navn: Å - A Navn: A - Å + Start/mål-ikoner + Lagre som ny sporfil + Legg til i en sporfil + Spor + Spor + Spor + Turopptak + Lagre som sporfil + %s sporfiler valgt + Sett turopptak på pause + Gjenoppta turopptak \ No newline at end of file diff --git a/OsmAnd/res/values-pl/phrases.xml b/OsmAnd/res/values-pl/phrases.xml index 4b3efcc10c..faec609ff4 100644 --- a/OsmAnd/res/values-pl/phrases.xml +++ b/OsmAnd/res/values-pl/phrases.xml @@ -3812,7 +3812,7 @@ Skontrastowane Uzupełnianie wody pitnej: woda z sieci Uzupełnianie wody pitnej: nie - Uzupełnianie wody pitnej: tak + Tak Poziom wody: utrzymujący się na powierzchni Poziom wody: poniżej średniego poziomu wody Poziom wody: obmywający falami @@ -3834,4 +3834,7 @@ Stan pompy: brak wiązki Strzałka: nie Winda + Małogabarytowe urządzenia elektryczne + Tablica odjazdów/odlotów + Uzupełnianie wody pitnej \ No newline at end of file diff --git a/OsmAnd/res/values-pt-rBR/phrases.xml b/OsmAnd/res/values-pt-rBR/phrases.xml index dd7c249f87..07c80c478d 100644 --- a/OsmAnd/res/values-pt-rBR/phrases.xml +++ b/OsmAnd/res/values-pt-rBR/phrases.xml @@ -3844,4 +3844,5 @@ Pequenos aparelhos elétricos Colmeia Loja de nozes + GNL \ No newline at end of file diff --git a/OsmAnd/res/values-pt-rBR/strings.xml b/OsmAnd/res/values-pt-rBR/strings.xml index a2c4186817..1358007216 100644 --- a/OsmAnd/res/values-pt-rBR/strings.xml +++ b/OsmAnd/res/values-pt-rBR/strings.xml @@ -3427,7 +3427,7 @@ OSM Ícone mostrado ao navegar ou mover. Ícone mostrado em repouso. - Verifique e compartilhe logs detalhados do aplicativo + Verifique e compartilhe registros detalhados do aplicativo Não foi possível analisar a intenção geográfica \'%s\'. É necessária permissão para usar esta opção. Este é um filtro de corte de baixa velocidade para não registrar pontos abaixo de uma determinada velocidade. Isso pode fazer com que as faixas gravadas pareçam mais suaves quando visualizadas no mapa. @@ -3899,4 +3899,6 @@ Última modificação Nome: Z – A Nome: A – Z + Ícones de início/término + Obrigado por adquirir \'curvas de nível\' \ No newline at end of file diff --git a/OsmAnd/res/values-pt/phrases.xml b/OsmAnd/res/values-pt/phrases.xml index 8d22b73dbe..fc6f81cb62 100644 --- a/OsmAnd/res/values-pt/phrases.xml +++ b/OsmAnd/res/values-pt/phrases.xml @@ -436,7 +436,7 @@ Central telefônica Reciclagem Centro de reciclagem - Contêiner + Contentor Vidro Papel Roupas @@ -1775,7 +1775,7 @@ Brinquedos Sorvete Cartão SIM - Seção + Secção Memorial de guerra Placa comemorativa Estátua @@ -2400,7 +2400,7 @@ Passageiros Veículos Bicicletas - Contêineres + Contentor Veículos pesados Academia ao ar livre Hackerspace @@ -2808,9 +2808,9 @@ Tipo de bomba: gravidade Estilo de bomba: moderno Estilo de bomba: histórico - Status da bomba: ok - Status da bomba: quebrado - Status da bomba: bloqueado + Estado da bomba: ok + Estado da bomba: quebrado + Estado da bomba: bloqueado Troika Cartão Troika não aceito Telescópio @@ -3563,7 +3563,7 @@ 3B 3B* Explosão de gás;Queimador de gás - Objeto excluído + Objeto apagado Caixa de resgate Sim Reddit @@ -3829,4 +3829,5 @@ Pequenos aparelhos elétricos Colmeia Loja de nozes + GNL \ No newline at end of file diff --git a/OsmAnd/res/values-pt/strings.xml b/OsmAnd/res/values-pt/strings.xml index fe6c5d23b1..08c39f206e 100644 --- a/OsmAnd/res/values-pt/strings.xml +++ b/OsmAnd/res/values-pt/strings.xml @@ -239,7 +239,7 @@ Meio de transporte: Por favor, define o destino primeiro Navegação - A aplicação do estado do GPS não está instalada. Pesquisar na loja de aplicações\? + A app do estado do GPS não está instalada. Pesquisar na loja de apps\? Horas de abertura Abrindo conjunto de alterações … Fechando conjunto de alterações… @@ -294,7 +294,7 @@ Adicionar aos \'Favoritos\' Escolher entre os nomes nativos e inglês. Usar nomes em inglês - Configurações da aplicação + Configurações da app Pesquisar endereço Escolher edifício Escolher rua @@ -486,7 +486,7 @@ Usar cores fluorescentes para mostrar trajetos e rotas. Edição offline Usar sempre a edição offline. - As alterações de POI dentro da aplicação não afetam os ficheiros de mapas descarregados; essas alterações são guardadas num ficheiro separado no seu aparelho. + As alterações de POI dentro da app não afetam os ficheiros de mapas descarregados; essas alterações são guardadas num ficheiro separado no seu aparelho. A enviar… {0} POI/anotações enviados Enviar todos @@ -523,7 +523,7 @@ Já existe um ficheiro de favoritos exportados anteriormente. Quer substitui-lo\? Configurações específicas de Perfil Configurações Globais - Configurações globais da aplicação + Configurações globais da app Espaço livre insuficiente, precisa de %1$s MB (só tem: %2$s disponíveis). Descarregar {0} ficheiro(s)\? \n {1} MB (de {2} MB) será utilizado. @@ -554,7 +554,7 @@ O ficheiro POI \'%1$s\' é redundante e pode ser eliminado. Não foi encontrado (e não pôde ser criado) o ficheiro local para guardar as mudanças de POI. Upgrade para OsmAnd+ - Descarregue a nova versão da aplicação para poder usar os novos ficheiros de mapas. + Descarregue a nova versão da app para poder usar os novos ficheiros de mapas. Mudar o nome Online Nomeação Procurando posição… @@ -624,7 +624,7 @@ Áudio de chamada telefónica (para interromper os aparelhos de som Bluetooth do carro) Áudio de Notificação Áudio de mídia/navegação - A aplicação não conseguiu descarregar a camada do mapa %1$s, se a tornar a instalar pode resolver o problema. + A app não conseguiu descarregar a camada do mapa %1$s, se a tornar a instalar pode resolver o problema. Ajustar a transparência da sobreposição. Transparência da Sobreposição Ajustar a transparência do mapa base. @@ -646,7 +646,7 @@ Não foi possível executar a pesquisa offline. Pesquisa por localização geográfica Sistema - Idioma de exibição da aplicação (usado após OsmAnd ser reiniciado). + Idioma de exibição da app (usado após OsmAnd ser reiniciado). Linguagem Próximo Anterior @@ -671,7 +671,7 @@ \nO serviço de navegação está temporariamente mudado para CloudMade on-line. Não foi possível encontrar a pasta especificada. Local de armazenamento - Todos os dados offline na aplicação instalada antiga serão suportados pela nova aplicação, mas os pontos Favoritos devem ser exportados da aplicação antiga e depois importados na nova aplicação. + Todos os dados offline na app instalada antiga serão suportados pela nova, mas os pontos Favoritos devem ser exportados da app antiga e depois importados na nova. Build {0} foi instalado ({1}). Descarregando construção… Instalar OsmAnd - {0} de {1} {2} MB \? @@ -787,27 +787,27 @@ Widgets transparentes Contínuo e-mail - OsmAnd (direções automatizadas de navegação OSM) -\n -\nO OsmAnd é uma aplicação de navegação livre, com acesso a uma ampla variedade de dados globais do OSM. Todos os dados dos mapas (mapas vetoriais ou imagens raster) podem ser armazenados no cartão de memória do telemóvel para usar desligado da Internet. O OsmAnd também permite roteamento, tanto ligado como desligado da Internet, incluindo a funcionalidade de roteamento curva a curva com orientação por voz. -\n -\nAlgumas das características principais: -\n- Funcionalidade totalmente desligado da Internet (guarda os mapas obtidos, sejam eles vetoriais ou imagens, numa pasta selecionável). -\n- Mapas vetoriais compactados do mundo inteiro disponíveis. -\n- Descarregar mapas de países ou regiões diretamente na aplicação. -\n- Sobreposição de mapas diversos, como GPX ou trajetos de navegação, pontos de interesse (POI), favoritos, curvas de nível, paragens de transportes públicos, mapas adicionais com transparência personalizável. -\n- Pesquisa desligado da Internet para endereços e locais (POIs). -\n- Encaminhamento desligado da Internet para distâncias médias. -\n- Modo de carro, bicicleta e pedestre. -\n- Vista de dia/noite, com alteração automática (opcional). -\n- Ampliação do mapa dependente da velocidade. -\n- Orientação do mapa de acordo com bússola ou direção do movimento. -\n- Orientação de faixas de rodagem, aviso de limite de velocidade, vozes gravadas e vozes para a conversão de texto para voz. -\n -\nLimitações desta versão gratuita do OsmAnd: -\n- Quantidade de descarregamentos de mapas limitado. -\n- Sem acesso aos POIs da Wikipédia no modo desligado da Internet. -\n + OsmAnd (direções automatizadas de navegação OSM) +\n +\nO OsmAnd é uma app de navegação livre, com acesso a uma ampla variedade de dados globais do OSM. Todos os dados dos mapas (mapas vetoriais ou imagens raster) podem ser armazenados no cartão de memória do telemóvel para usar desligado da Internet. O OsmAnd também permite roteamento, tanto ligado como desligado da Internet, incluindo a funcionalidade de roteamento curva a curva com orientação por voz. +\n +\nAlgumas das características principais: +\n- Funcionalidade totalmente desligado da Internet (guarda os mapas obtidos, sejam eles vetoriais ou imagens, numa pasta selecionável). +\n- Mapas vetoriais compactados do mundo inteiro disponíveis. +\n- Descarregar mapas de países ou regiões diretamente na app. +\n- Sobreposição de mapas diversos, como GPX ou trajetos de navegação, pontos de interesse (POI), favoritos, curvas de nível, paragens de transportes públicos, mapas adicionais com transparência personalizável. +\n- Pesquisa desligado da Internet para endereços e locais (POIs). +\n- Encaminhamento desligado da Internet para distâncias médias. +\n- Modo de carro, bicicleta e pedestre. +\n- Vista de dia/noite, com alteração automática (opcional). +\n- Ampliação do mapa dependente da velocidade. +\n- Orientação do mapa de acordo com bússola ou direção do movimento. +\n- Orientação de faixas de rodagem, aviso de limite de velocidade, vozes gravadas e vozes para a conversão de texto para voz. +\n +\nLimitações desta versão gratuita do OsmAnd: +\n- Quantidade de descarregamentos de mapas limitado. +\n- Sem acesso aos POIs da Wikipédia no modo desligado da Internet. +\n \nO OsmAnd está em desenvolvimento ativo, mas o nosso projeto e o seu progresso ainda depende de contribuições financeiras para o desenvolvimento e testes de novas funcionalidades. Por favor, considere comprar o OsmAnd+, ou a ajudar a financiar novas funcionalidades específicas, ou fazer um donativo no osmand.net. Selecione um esquema de cores de estrada: Esquema de cores @@ -846,9 +846,9 @@ Só Estradas Mapa padrão Mapas só de estradas - Executar a aplicação no modo de segurança (usando o código do Android mais lento em vez do nativo). + Executar a app no modo de segurança (usando o código do Android mais lento em vez do nativo). Modo seguro - A aplicação está a ser executada no modo de segurança (desligue-a em \"Definições\"). + A app está a ser executada no modo de segurança (desligue-a em \"Definições\"). O serviço de segundo plano OsmAnd ainda está em execução. Tambẽm pará-lo\? Fechar conjunto de alterações Pesquisar mais povoações/códigos postais @@ -893,7 +893,7 @@ Sob demanda\? Formato de saída de vídeo: Usar gravador do sistema para vídeo. - Utilize aplicação do sistema para fotos. + Utilize a app do sistema para fotos. Usar aplicação da câmara A tocar o áudio da gravação. \n%1$s Indisponível @@ -927,14 +927,14 @@ desmarcado Limite de Velocidade Nenhum edifício encontrado. - A aplicação do leitor de código de barras ZXing não está instalada. Procurar no Google Play\? + A app do leitor de código de barras ZXing não está instalada. Procurar no Google Play\? Faça uma doação para ver novas funcionalidades implementadas nesta aplicação. incompleto Nome da rua Número de casa Gravação de viagem - Personalizar a aparência da aplicação. - Tema da aplicação + Personalizar a aparência da app. + Tema da app Opções de acessibilidade Especifique um endereço Selecione favorito @@ -1047,14 +1047,14 @@ Mapa mundial OsmAnd+ (Direções de Navegação Automatizada do OSM) \n -\n OsmAnd+ é uma aplicação de navegação livre, com acesso a uma ampla variedade de dados globais do OSM. Todos os dados dos mapas (mapas vetoriais ou imagens raster) podem ser armazenados no cartão de memória do telemóvel para usar desligado da Internet. O OsmAnd também permite roteamento, tanto ligado como desligado da Internet, incluindo a funcionalidade de roteamento curva a curva com orientação por voz. +\n OsmAnd+ é uma app de navegação livre, com acesso a uma ampla variedade de dados globais do OSM. Todos os dados dos mapas (mapas vetoriais ou imagens raster) podem ser armazenados no cartão de memória do telemóvel para usar desligado da Internet. O OsmAnd também permite roteamento, tanto ligado como desligado da Internet, incluindo a funcionalidade de roteamento curva a curva com orientação por voz. \n -\n OsmAnd+ é a versão paga da aplicação, ao comprá-lo está a apoiar o projeto, a financiar o desenvolvimento de novas funcionalidades e a receber as últimas atualizações. +\n OsmAnd+ é a versão paga da app, ao comprá-lo está a apoiar o projeto, a financiar o desenvolvimento de novas funcionalidades e a receber as últimas atualizações. \n \n Algumas das características principais: \n - Funcionalidade totalmente desligado da Internet (guarda os mapas obtidos, sejam eles vetoriais ou imagens, numa pasta selecionável). \n - Mapas vetoriais compactados do mundo inteiro disponíveis. -\n - Descarregamento de mapas de países ou regiões diretamente na aplicação. +\n - Descarregamento de mapas de países ou regiões diretamente na app. \n - Recurso Wikipédia desligado da Internet (descarregamento de POIs da Wikipédia), ótimo para passeios turísticos. \n - Possibilidade de sobreposição de várias camadas de mapas, como trilhos GPX ou navegação, pontos de Interesse, favoritos, curvas de nível, paragens de transporte público, mapas adicionais com transparência personalizável. \n @@ -1086,7 +1086,7 @@ Wikipédia (off-line) Marca Marítima Escolha perfis visíveis. - Perfis da aplicação + Perfis da app Destino Rosa Castanho @@ -1341,7 +1341,7 @@ Ver Norte Leste - Memória interna da aplicação + Memória interna da app Ir Configurações de navegação Configurações gerais @@ -1521,8 +1521,8 @@ O mapa %1$s está pronto para ser usado. Mapa descarregado Mostrar mapa - Define o sinalizador que indica a primeira inicialização da aplicação, mantém todas as outras configurações inalteradas. - Simular arranque inicial da aplicação + Define o sinalizador que indica a primeira inicialização da app, mantém todas as outras configurações inalteradas. + Simular arranque inicial da app geo: Partilhar Localização Enviar @@ -1572,7 +1572,7 @@ Escolher orientação por voz Escolher ou descarregar a orientação por voz para o seu idioma. Noite - Há uma nova opção para controlar principalmente a aplicação através do painel de controlo flexível ou um menu estático. A sua escolha pode ser alterada nas configurações do painel. + Há uma nova opção para controlar principalmente a app através do painel de controlo flexível ou um menu estático. A sua escolha pode ser alterada nas configurações do painel. Painel de controlo ou menu de controlo Atualizar Apenas descarregar com Wi-Fi @@ -1661,7 +1661,7 @@ Fino Média Negrito - Agora a aplicação está autorizada a escrever no armazenamento externo, mas primeiro é necessário reiniciar a aplicação. + Agora a app está autorizada a escrever no armazenamento externo, mas primeiro é necessário reiniciar a app. Mover ↑ Mover ↓ Terminar a navegação @@ -1959,7 +1959,7 @@ Sem sobreposição Sem subposição Erro - Assine a nossa lista de e-mail sobre descontos da aplicação e ganhe mais 3 descarregamentos de mapas! + Assine a nossa lista de e-mail sobre descontos da app e ganhe mais 3 descarregamentos de mapas! Curvas de nível de profundidade marítima e seamarks. Muito obrigado por comprar \'Contornos de profundidade náutica\' Contornos de profundidade náutica @@ -1973,10 +1973,10 @@ Fontes do mapa Circulação pela direita Automático - Não envie estatísticas anónimas de utilização da aplicação - OsmAnd recolhe informação sobre as secções da aplicação que abriu. Não são enviadas: a sua localização; a informação que introduz na aplicação; detalhes de áreas que veja, procure ou descarregue. + Não envie estatísticas anónimas de utilização da app + OsmAnd recolhe informação sobre as secções da app que abriu. Não são enviadas: a sua localização; a informação que introduz na app; detalhes de áreas que veja, procure ou descarregue. Não mostrar mensagens ao iniciar - Nâo mostrar descontos da aplicação e mensagens de eventos locais especiais. + Nâo mostrar descontos da app e mensagens de eventos locais especiais. Opções de estacionamento Muito obrigado por comprar a versão paga de OsmAnd. Inclinado @@ -2208,7 +2208,7 @@ Como abrir a hiperligação\? Ler a Wikipédia desligado da Internet Descarregar tudo - Reiniciar a aplicação + Reiniciar a app Mostrar imagens Cancelou a sua assinatura do OsmAnd Live Renovar assinatura para continuar a utilizar todas as funcionalidades: @@ -2232,7 +2232,7 @@ Guias para os lugares mais interessantes do mundo dentro do OsmAnd, sem uma conexão com a Internet. Atualizações de mapa mensais Atualizações de mapa a cada hora - Compra na aplicação + Compra na app Pagamento de uma só vez Uma vez comprado, estará sempre disponível para si. Comprar - %1$s @@ -2475,7 +2475,7 @@ \nRepresenta área: %1$s x %2$s Tolerância do limite de velocidade Selecione a margem de tolerância de limite de velocidade, acima do qual receberá um aviso de voz. - O nome do Favorito foi modificado para %1$s para facilitar gravar corretamente a sequência de caracteres com emoticons para um ficheiro. + O nome do favorito foi modificado para %1$s para facilitar gravar corretamente a cadeia de caracteres com emoticons num ficheiro. Imprimir rota Nome de favorito duplicado Nome favorito especificado já está em uso, foi alterado para %1$s para evitar a duplicação. @@ -2551,8 +2551,8 @@ Renderizar caminhos de acordo com a escala de SAC. Renderizar caminhos de acordo com traços OSMC. Hora intermediária - OsmAnd (sigla em inglês de direções de navegação automatizada do OSM) é uma aplicação de mapas e navegação com acesso a dados livres, mundiais e de alta qualidade do OSM. -\n + OsmAnd (sigla em inglês de direções de navegação automatizada do OSM) é uma app de mapas e navegação com acesso a dados livres, mundiais e de alta qualidade do OSM. +\n \nPoderá usar o navegador visual e por voz, ver POIs (pontos de interesse), criar e gerir trilhos GPX, usar (através de um suplemento) curvas de nível e dados de altitude, escolher entre os modos motorista, ciclista e pedestre, editar o OpenStreetMap e muito mais. Navegação GPS \n• Escolha entre modos off-line (sem tarifa de roaming quando estiver no exterior) ou on-line (mais rápido) @@ -2595,7 +2595,7 @@ \n • Envie trilhos GPX para o OpenStretMap diretamente da aplicação \n • Adicione POIs e envie-os diretamente para o OpenStretMap (ou mais tarde se estiver desconectado da Internet) \n - OsmAnd é um programa de fonte aberta desenvolvido ativamente. Todos podem contribuir para a aplicação reportando erros, melhorando as traduções ou programando novas funcionalidades. Além disso, o projeto conta com contribuições financeiras para financiar a programação e testes de novas funcionalidades. + OsmAnd é um programa de fonte aberta desenvolvido ativamente. Todos podem contribuir para a app reportando erros, melhorando as traduções ou programando novas funcionalidades. Além disso, o projeto conta com contribuições financeiras para financiar a programação e testes de novas funcionalidades. \n Cobertura de mapa e qualidade aproximada: \n • Europa Ocidental: **** \n • Europa Oriental: *** @@ -2609,11 +2609,11 @@ \n • Antártida: * \n A maioria dos países ao redor do globo está disponível para descarregar! \n Obtenha um navegador confiável no seu país - seja em França, Alemanha, México, Reino Unido, Espanha, Holanda, EUA, Rússia, Brasil ou qualquer outro. - OsmAnd+ (direções de navegação automatizada do OSM) é uma aplicação de mapas e navegação com acesso a dados livres do OSM, de todo o mundo e de alta qualidade. -\nDesfrute da navegação visual ou por voz, vendo POIs (pontos de interesse), criando e gerindo trilhos GPX, usando informação de altitude e curvas de nível, escolher entre modos dirigir, andar de bicicleta e pedestre, editar o OpenStreetMap e muito mais. -\n -\nOsmAnd+ é a versão paga da aplicação. Ao comprá-lo, está a apoiar o projeto, a financiar o desenvolvimento de novas funcionalidades e a receber as últimas atualizações. -\n + OsmAnd+ (direções de navegação automatizada do OSM) é uma app de mapas e navegação com acesso a dados livres do OSM, de todo o mundo e de alta qualidade. +\nDesfrute da navegação visual ou por voz, ver POIs (pontos de interesse), criando e gerindo trilhos GPX, usando informação de altitude e curvas de nível, escolher entre modos dirigir, andar de bicicleta e pedestre, editar o OpenStreetMap e muito mais. +\n +\nOsmAnd+ é a versão paga da app. Ao comprá-lo, está a apoiar o projeto, a financiar o desenvolvimento de novas funcionalidades e a receber as últimas atualizações. +\n \nAlgumas das características principais: Navegação \n• Funciona on-line (rápido) ou off-line (sem custos de roaming quando estiver no estrangeiro) @@ -2654,10 +2654,10 @@ \n• Visualização de curvas de nível e sombreamento de relevo (via suplemento adicional) Contribua diretamente para o OpenStreetMap \n • Envie relatórios de erros. -\n • Envie trilhos GPX para o OpenStretMap diretamente da aplicação. +\n • Envie trilhos GPX para o OpenStretMap diretamente da app. \n • Adicione POIs e envie-os diretamente para o OpenStretMap (ou mais tarde se estiver desconectado da Internet). \n • Gravação de viagem opcional também em plano de fundo (enquanto o aparelho está no modo adormecido). -\n OsmAnd é um programa de fonte aberta desenvolvido ativamente. Todos podem contribuir para a aplicação reportando erros, melhorando as traduções ou programando novas funcionalidades. Além disso, o projeto conta com contribuições financeiras para financiar a programação e testes de novas funcionalidades. +\n OsmAnd é um programa de fonte aberta desenvolvido ativamente. Todos podem contribuir para a app por reportar erros, a melhorar as traduções ou a programar novas funcionalidades. Além disso, o projeto conta com contribuições financeiras para financiar a programação e testes de novas funcionalidades. \n Cobertura de mapa e qualidade aproximada: \n• Europa Ocidental: **** @@ -3358,7 +3358,7 @@ Tocar em \'Aplicar\' apagará os perfis removidos permanentemente. Perfil principal Selecione a cor - Perfis padrão do OsmAnd não podem ser apagados, mas desativados (na tela anterior) ou classificados na parte inferior. + Perfis predefinidos do OsmAnd não podem ser apagados, mas desativados (no ecrã anterior) ou classificados na parte inferior. Editar perfis O \'Tipo de navegação\' controla como as rotas são calculadas. Aspeto do perfil @@ -3887,7 +3887,7 @@ Apenas a linha da rota será gravada, os pontos de passagem serão apagados. Nome do ficheiro %s ficheiros de faixa selecionados - Vai pausar o registo de faixas quando a aplicação for morta (através de aplicações recentes). (indicação de fundo de OsmAnd desaparece da barra de notificação do Android.) + Vai pausar o registo de faixas quando a app for morta (através de apps recentes). (indicação de fundo de OsmAnd desaparece da barra de notificação do Android.) - Função atualizada de Planear uma rota: permite utilizar diferentes tipos de navegação por segmento e a inclusão de faixas \n \n - Novo menu Aparência para trilhos: selecionar cor, espessura, setas de direção de visualização, ícones de início/fim @@ -3903,4 +3903,9 @@ \n - Problemas com as configurações de importação/exportação de perfis resolvidos \n \n + Última modificação + Nome: Z – A + Nome: A – Z + Ícones de início/fim + Obrigado por comprar \'Curvas de nível\' \ No newline at end of file diff --git a/OsmAnd/res/values-ru/phrases.xml b/OsmAnd/res/values-ru/phrases.xml index f67e31fb94..d87b1f7c05 100644 --- a/OsmAnd/res/values-ru/phrases.xml +++ b/OsmAnd/res/values-ru/phrases.xml @@ -3832,4 +3832,5 @@ Малые электроприборы Магазин орехов Улей + СПГ \ No newline at end of file diff --git a/OsmAnd/res/values-ru/strings.xml b/OsmAnd/res/values-ru/strings.xml index 701641a64d..b6dd46c4e9 100644 --- a/OsmAnd/res/values-ru/strings.xml +++ b/OsmAnd/res/values-ru/strings.xml @@ -52,7 +52,7 @@ Плагин Приобретите и установите плагин «Контурные линии» для отображения градуированных вертикальных областей. Цветовая схема - Показывать, начиная с уровня масштабирования + Показывать начиная с масштаба Анимация моего положения Включить анимацию прокрутки карты с моим положением во время навигации. Масштаб: %1$s @@ -60,7 +60,7 @@ Выбрать цвет Задать имя Для больших расстояний: добавьте промежуточные пункты, если маршрут не построен в течение 10 минут. - Разрешить частный доступ + Разрешить частные зоны Разрешить доступ на частную территорию. Обзор Выберите улицу @@ -71,7 +71,7 @@ Ближайшие города Выберите город Поиск почтового индекса - Аудио⁣заметка + Запись аудио⁣ Записать видео Фотозаметка OSM-заметка @@ -86,7 +86,7 @@ Видимые Восстановить покупки Шрифты карты - Посмотреть на карте + Анализ на карте Морские карты Контуры морских глубин Контуры морских глубин @@ -105,7 +105,7 @@ Добавление нового пункта назначения в центре экрана. Ранее выбранный пункт назначения станет последним промежуточным пунктом. Кнопка для установки центра экрана пунктом отправления. Затем нужно будет выбрать пункт назначения или запустить расчёт маршрута. Кнопка для установки центра экрана пунктом назначения с заменой предыдущего (если был задан). - Установка центра экрана первой промежуточной точкой маршрута. + Установка центра экрана местом первой остановки на маршруте. Нет покрытия Нет подложки Гористый @@ -115,7 +115,7 @@ Сбалансированный Предпочитать переулки Выберите предпочтительный рельеф. - Склон + Уклон Добавить новую папку Точки удалены. Вы уверены, что хотите удалить %1$d точки\? @@ -171,7 +171,7 @@ Добавить источник карты Источник карты изменён на «%s». Удерживайте кнопку для перемещения её по экрану. - Показывать контуры и точки глубины. + Показывать контуры и точки глубин. Контуры морских глубин Частота горизонталей Частота горизонталей @@ -202,7 +202,7 @@ Продолжить Пауза Поездка - Записано + Записано в трек Запись Нет данных Минимальная скорость для записи @@ -233,7 +233,7 @@ Сохранить фильтр Удалить фильтр Новый фильтр - Изменить позицию + Изменение позиции Текущий путь Навигация OsmAnd Live Уровень заряда батареи @@ -317,18 +317,18 @@ Открыть внешний проигрыватель Удалить эту запись? недоступно - Аудиозаметка - Видеозаметка - Слой аудиозаписей - Запись не может быть воспроизведена. + Запись аудио + Запись видео + Слой медиазаписей + Не удаётся воспроизвести запись. Удалить запись Проиграть Запись %1$s %3$s %2$s Запись - Аудиозаметки + Медиазаметки OsmAnd-плагин для линий высот Измерение расстояний - Нажмите «Использовать местоположение…» чтобы добавить заметку к данному местоположению. + Нажмите «Использовать местоположение…» для добавления заметки к месту. Аудиозаметки Создавайте аудио-, видео- и фотозаметки в поездке, используя виджет или контекстное меню. Аудио/видеозаметки @@ -368,9 +368,9 @@ Использовать онлайн-карты (загрузка и кеширование на SD-карте). Онлайн-карты Выберите источник онлайн или кешированных растровых карт. - Доступ ко множеству онлайн-карт (т. н. тайловых или растровых): от встроенных OSM (как Mapnik), до спутниковых снимков и слоёв специального назначения, таких как карты погоды, климатические, геологические карты, затенения рельефа и др. + Доступ ко множеству онлайн-карт (т. н. тайловых или растровых): от встроенных OSM (как Mapnik) до спутниковых снимков и слоёв специального назначения, таких как карты погоды, климатические, геологические карты, затенения рельефа и др. \n -\n Любая из этих карт может быть использована в качестве базовой либо как наложение или подложка к другой базовой карте (например стандартной локальной карте OsmAnd). Некоторые элементы векторной карты OsmAnd можно скрыть в меню «Настройки карты». +\n Любая из этих карт может быть использована как основная или в качестве подложки к другой карте (например стандартной локальной карте OsmAnd). Некоторые элементы векторной карты OsmAnd можно скрыть в меню «Настройки карты». \n \n Карты можно загрузить непосредственно из интернета или подготовить для использования в автономном режиме (и вручную скопировать в папку данных OsmAnd) в виде базы данных sqlite, которая может быть создана с помощью различных инструментов подготовки карт сторонних производителей. Показывает настройки для включения фонового отслеживания и навигации путём периодического пробуждения устройства GPS (с выключенным экраном). @@ -505,7 +505,7 @@ Голосовые подсказки (TTS) Голосовые подсказки (записанные) Данные POI - Голос TTS + TTS Новый поиск Размер текста для названий на карте: Размер текста @@ -518,14 +518,14 @@ Обратное направление трека Использовать текущий пункт назначения Пройти весь путь - Для этого региона доступны локальные векторные карты. -\n\t -\n\tДля их использования выберите в \"Меню\" → \"Настройка карты\" → \"Источник карты…\" → \"Векторные карты\". - Голосовые инструкции + Для этого региона есть локальные векторные карты. +\n\t +\n\tДля использования выберите их в качестве источника (Меню → Настройка карты → Источник карты → Локальные векторные карты). + Аудиоканал голосовых инструкций Выберите канал вывода голосовых подсказок. - Канал голосовых звонков (прерывает автомобильную Bluetooth стереосистему) - Канал уведомлений - Канал медиа/навигации + Голосовые звонки (для прерывания автомобильной стереосистемы Bluetooth) + Уведомления + Мультимедиа, навигация Приложение не может загрузить слой карты %1$s, переустановка может решить проблему. Отрегулируйте прозрачность наложения. Прозрачность наложения @@ -605,8 +605,8 @@ Чтение кешированных растровых карт… Недостаточно памяти для локальной карты «{0}» Версия локальной карты «{0}» не поддерживается - Локальная навигация OsmAnd является экспериментальной функцией и не работает на длинные расстояния более 20 километров. -\n + Офлайн-навигация OsmAnd — это экспериментальная функция, и она не работает на дистанциях больше 20 км. +\n \nНавигация временно переключена на онлайн-сервис CloudMade. Невозможно найти указанную папку. Папка хранилища данных @@ -616,7 +616,7 @@ Установить OsmAnd — {0} из {1} {2} МБ\? Не удалось получить список сборок OsmAnd Загружаются сборки OsmAnd… - Выберите сборку OsmAnd для установки + Выберите сборку для установки Голосовая навигация недоступна. Перейдите в «Настройки» → «Настройки навигации», выберите профиль → «Голосовые данные» и выберите или загрузите пакет голосовых подсказок. Выберите пакет голосовых подсказок Показывать производительность отрисовки. @@ -638,7 +638,7 @@ Самый быстрый маршрут Расчёт скоростного маршрута вместо кратчайшего. На масштабе {0} загрузить {1} тайлов ({2} МБ) - Загрузить карту + Скачать карту Наибольший масштаб для предварительной загрузки Выбранная карта не может быть загружена Непрерывная отрисовка @@ -669,9 +669,9 @@ Локальные векторные карты Редактировать POI Удалить POI - По направлению компаса - По направлению движения - Не вращать (север сверху) + по направлению компаса + по направлению движения + не вращать (север сверху) Выравнивание карты: Ориентация карты Детали маршрута @@ -715,10 +715,10 @@ Фильтр Звук Без звука - Выберите язык голосовых инструкции для навигации. + Выберите голосовое сопровождение для навигации. Голосовые данные Инициализируются голосовые данные… - Голосовые данные не поддерживаются текущей версией приложения + Неподдерживаемая версия голосовых данных Выбранные голосовые данные не правильного формата Выбранный пакет голосовых подсказок не доступен SD-карта недоступна. @@ -726,13 +726,13 @@ Карта памяти доступна только для чтения. \nТеперь можно только просматривать предварительно загруженную карту, а не загружать новые области. Файл распаковывается… - Направо и прямо - Резко направо и прямо - Плавно направо и прямо - Налево и прямо - Резко налево и прямо - Плавно налево и прямо - Выполните разворот, затем прямо + Направо + Резко направо + Плавно направо + Налево + Резко налево + Плавно налево + Выполните разворот Двигайтесь прямо Продолжить Загрузить детальные карты регионов @@ -844,7 +844,7 @@ Сохранить текущий трек Укажите интервал фиксирования точек для записи трека во время навигации Интервал записи во время навигации - Во время навигации GPX треки будут автоматически сохранены в папку с треками. + Во время навигации GPX-треки будут автоматически сохранены в папку с треками. Автозапись трека во время навигации Обновить карту Обновить часть карты @@ -864,7 +864,7 @@ 3D вид Показать последние использованные POI на карте. Показывать POI - Выберите источник онлайн или кешированных тайлов карты + Выберите источник онлайн- или кешированных тайлов карт. Растровые карты Источник карты Использовать интернет @@ -901,7 +901,7 @@ Дом Пересечение улиц Обновить карту - Создать POI + Добавление POI Да Отмена Нет @@ -1160,10 +1160,10 @@ \n — подсказки полосы движения, отображение ограничения скорости, предварительно записанные и синтезированные голосовые подсказки \n Без автомагистралей - Привязываться к дорогам во время навигации. + Привязывать позицию к дороге во время навигации. Привязка к дороге Промежуточный пункт %1$s слишком далеко от ближайшей дороги. - Достигнут промежуточный пункт + Вы прибыли в промежуточный пункт Промежуточный пункт Промежуточный пункт Конец маршрута слишком далеко от ближайшей дороги. @@ -1177,11 +1177,11 @@ Рестораны Достопримечательности Последний промежуточный пункт - Первый промежуточный пункт - Добавить последним промежуточным пунктом - Добавить первым промежуточным пунктом + Сделать начальной остановкой + Сделать последней остановкой + Сделать начальной остановкой Заменить пункт назначения - Пункт назначения уже задан: + Пункт назначения уже задан Пункт %1$s Точки маршрута Промежуточный пункт %1$s @@ -1201,12 +1201,12 @@ Настройки аудио и видео Изменить порядок Просмотр - Сделать фото + Снимок Сделать фото Синхронизация треков и медиазаметок с вашим аккаунтом Dropbox. Плагин Dropbox - Плагин обеспечивает наложение контурных линии и (рельефа) затемняющего слоя, которые будут отображаться поверх стандартных карт OsmAnd. Эта функция высоко оценится спортсменами, туристами, путешественниками и всеми, кто заинтересован в рельефной структуре ландшафта. -\n + Плагин обеспечивает наложение контурных линии и затемняющего слоя (рельефа), которые будут отображаться поверх стандартных карт OsmAnd. Эту функцию оценят спортсмены, туристы, путешественники и все, для кого рельеф местности имеет значение. +\n \nГлобальные данные (между 70° на севере и 70° на юге) основываются на измерениях SRTM (Shuttle Radar Topography Mission) и ASTER (Advanced Spaceborne Thermal Emission and Reflection Radiometer), инструментом визуализации Terra, флагманского спутника Земли системы наблюдения NASA. ASTER является результатом совместных усилий NASA, министерства экономики Японии, торговли и промышленности (METI), космических систем Японии (J-spacesystems). Фото %1$s %2$s Медиаданные @@ -1227,7 +1227,7 @@ Время прибытия Информация GPS нет - Слой рельефа местности + Слой затенения рельефа Название улицы Номер дома Нет соединения по Wi-Fi. Использовать текущее интернет-соединение для загрузки\? @@ -1239,7 +1239,7 @@ Укажите адрес Выбор избранной Модификации OSM - Выбирать + Выбрать OsmAnd карты и навигация OsmAnd+ карты и навигация Уменьшает «шум» компаса, но добавляет инерцию. @@ -1258,7 +1258,7 @@ Отменить маршрут Очистить пункт назначения Искать улицу в ближайших населённых пунктах - В порядке следования домов + Расположить в оптимальном порядке Доступно %1$d файлов для скачивания осталось %1$d файлов Подождите, пока завершится текущая операция @@ -1272,10 +1272,10 @@ Резервное копирование как правка OSM высота OsmChange-файл создан за %1$s - * Нажмите, чтобы отметить точку. -\n* Удерживайте нажатие на карте, чтобы удалить предыдущую точку. -\n* Удерживайте нажатие на точке, чтобы просмотреть и добавить описание. -\n* Нажмите на виджет измерения, чтобы увидеть больше действий. + * Нажмите, чтобы отметить точку. +\n* Нажмите и удерживайте карту, чтобы удалить предыдущую точку. +\n* Удерживайте точку для просмотра и добавления описания. +\n* Нажмите на виджет измерения для других действий. Использовать магнитный датчик вместо датчика ориентации. Другие Контурные линии @@ -1301,9 +1301,9 @@ Дорожные предупреждения Очистить промежуточные пункты Оставить промежуточные пункты - К: + Назначение: Через: - От: + Отправление: Промежуточные пункты уже заданы. Названия улиц (TTS) Объявлять… @@ -1345,9 +1345,9 @@ Симуляция использования рассчитанного маршрута Симуляция использования трека GPX Без автомасштаба - Ближний план - Средний план - Дальний план + К ближнему плану + К среднему плану + К дальнему плану Избегать автомагистралей Без автомагистралей Предпочитать автомагистрали @@ -1358,7 +1358,7 @@ Избегать грунтовых дорог Без паромов Исключить паромные переправы - Максимальная масса + Предельная масса Укажите допустимый предел массы автомобиля для учёта при построении маршрута. Отображение карты Навигационные знаки (водоёмы) @@ -1386,7 +1386,7 @@ Задать пункт назначения Предпочтения маршрута Информация про маршрут - Добавить как новый пункт назначения + Добавить пункт назначения Использовать показанный путь для навигации? Рассчитать сегмент маршрута OsmAnd без интернета Рассчитать маршрут OsmAnd для первого и последнего сегмента маршрута @@ -1445,15 +1445,15 @@ Голос Разное Локализация - Голосовые подсказки приостанавливают воспроизведение музыки. - Приостановить музыку + Приостановка воспроизведения во время подсказок. + Прерывать музыку Поделиться маршрутом используя файл GPX Неправильный формат: %s Маршрут предоставленный через OsmAnd - Только вручную (нажатием «стрелочки») + При нажатии на стрелку (вручную) Повторять навигационные инструкции с регулярными интервалами. Повторять навигационные инструкции - Объявление прибытия + Объявление о прибытии Как скоро следует сообщать о прибытии? Места, отправленные в OsmAnd Рассчитать маршрут между точками @@ -1492,7 +1492,7 @@ %1$s точек Точка %1$s %1$s \nМаршрутных точек %2$s - Показывать кнопки изменения масштаба во время навигации. + Показывать кнопки масштаба во время навигации. Кнопки масштаба Сортировать по расстоянию Сортировать по имени @@ -1536,14 +1536,14 @@ Сербский (кириллица) Китайский (упрощённый) Китайский (традиционный) - Маршруты метро + Линии метро Продолжить навигацию Приостановить навигацию - Визуализация пути по шкале SAC. - Визуализация пути согласно трассам OSMC. - Пораньше - Как обычно - Попозже + Отрисовка дорог cогласно шкале SAC. + Отрисовка дорог согласно трассам OSMC. + Раннее + По умолчанию + Позднее На последних метрах Пеший горный туризм по шкале (SAC) Наложение туристических меток @@ -1581,7 +1581,7 @@ Снизьте скорость Камера скорости Дорожные предупреждения - Выберите допустимое значение превышения скорости выше которого вы получите голосовое предупреждение. + Выберите значение скорости, при превышении которого вы получите голосовое предупреждение. Допустимое превышение скорости Избранная точка переименована на «%1$s», чтобы сохранить строку, содержащую эмотикон в файл. Печать маршрута @@ -1739,7 +1739,7 @@ Отменить выбор всех Поделиться Мои места - Точки + Избранные Треки Текущий трек Поделиться заметкой @@ -1780,7 +1780,7 @@ \n \nВ случае активации этого вида, стиль карты меняется на «Зимний/лыжный», показывая все детали пейзажа так, как они выглядят зимой. Такой (зимний) вид может быть отменён либо путём деактивации здесь, либо если вы поменяете «Стиль карты» в меню «Настройки карты» на желаемый вид. Текущий маршрут - Скачать карты + Загрузка карт Для правильного отображения дорожных знаков и правил выберите свой регион вождения: Добро пожаловать Отметить для удаления @@ -1815,8 +1815,8 @@ Переместить файлы данных Osmand в новое место назначения\? Напечатайте для поиска Номера домов - Избегать перехода границы - Максимальная высота + Избегать пересечения границ + Предельная высота Укажите высоту транспортного средства для учёта при построении маршрута. Умный пересчёт маршрута Для больших маршрутов пересчитывать только начало. @@ -1905,8 +1905,8 @@ Пропустить OsmAnd Плагины - Локальные карты -\nи Навигация + Офлайн-карты +\nи навигация Номер дома Тип лыжной трассы Автообновления @@ -2030,13 +2030,13 @@ Изменить путевую точку GPX Без лестниц Избегать ступеней и лестниц - Без пересечений границ + Без перехода границы Обновлять Загружать только по Wi-Fi Обновить сейчас Приложение не имеет разрешения на использование SD-карты Доступные карты - Начальный пункт + Пункт отправления Не выбрано Размер хранилища Звук @@ -2065,7 +2065,7 @@ Разбиение на клипы Использовать разбиение на клипы Циклическая перезапись клипов при превышении заданного объёма хранилища. - Поменять местами пункты отправления и назначения + Поменять местами пункты отправления и назначения Удалить Подземные объекты Данные недоступны @@ -2110,7 +2110,7 @@ Запись удалена элементы удалены Автообновления - Выберите или скачайте голосовые подсказки для вашего языка. + Выберите или скачайте голосовое сопровождение для вашего языка. Полный отчёт Пересчёт маршрута Имя пользователя и пароль OSM @@ -2235,7 +2235,7 @@ Получить Получайте неограниченное количество загрузок карт, вдобавок к еженедельным, ежедневным и даже почасовым обновлениям. Неограниченный доступ к картам, обновлениям и плагину «Википедия». - Голосовое сопровождение + Голосовые инструкции Абонентская плата взимается за выбранный период. Отменить подписку можно в Google Play в любой момент. Пожертвование для сообщества OSM Часть вашего пожертвования будет отправлена участникам OSM. Стоимость подписки при этом остаётся прежней. @@ -2281,16 +2281,16 @@ Изменить положение кнопки Название действия Сербский (латиница) - Голос вкл/выкл - Включить голос - Выключить голос + Голосовая навигация + Включить подсказки + Выключить подсказки Не удалось переместить файл. Благодарим вас за покупку контуров морских глубин Добавить фото Разрешения Онлайн-фото - Здесь нет фотографий. - Поделитесь вашим просмотром улиц через Mapillary. + Здесь нет фото. + Поделитесь своими уличными видами через Mapillary. Виджет Mapillary Позволяет быстро внести свой вклад в Mapillary. Фото с улиц онлайн для каждого. Открывайте места, взаимодействуйте, запечатлейте весь мир. @@ -2337,7 +2337,7 @@ Измерить расстояние Добавьте хотя бы одну точку. Фотография Mapillary - Улучшить фотопокрытие через Mapillary + Улучшить фотопокрытие в Mapillary Скрыть, начиная с уровня масштабирования Прозрачно-розовый Берберский @@ -2380,7 +2380,7 @@ Избегать ледовых дорог и бродов. Моё местоположение Финиш - Сортировать + Сортировка Экспорт маркеров в следующий файл GPX: Маркеры Изменить заметку @@ -2392,7 +2392,7 @@ Критерий сортировки: Выберите способ указания расстояния и направления до маркеров на карте: Смена ориентации карты - Выберите скорость, при которой переключается ориентация карты с «По направлению движения» на «По направлению компаса». + Выберите скорость, при которой ориентация по направлению движения переключится на ориентацию по компасу. Все маркеры перемещены в историю Маркер перемещён в историю Маркер перемещён в действующие @@ -2440,7 +2440,7 @@ Кнопка для добавления фотозаметки в центре экрана. Добавление в центре экрана OSM-заметки. Добавление POI в центре экрана. - Переключатель, чтобы включить или выключить голосовые подсказки во время навигации. + Включение/отключение голосового сопровождения при навигации. Добавление в центре экрана места парковки. Показывать промежуточный диалог Фотографии Mapillary доступны только онлайн. @@ -2470,7 +2470,7 @@ Полноэкранный режим Отметить пройденным Файл %1$s не содержит путевых точек, импортировать его как трек? - Выберите трек, чтобы добавить в маркеры его точки. + Выберите трек, чтобы добавить в маркеры его точки. Трек путевых точек Направо Налево @@ -2520,14 +2520,14 @@ \n• Поддержка промежуточных точек маршрута \n• Запись собственного или отправка GPX трека и следование ему \n - Карта -\n• Отображает POI (точки интереса) около вас -\n• Адаптирует карту в направлении вашего движения (или компаса) -\n• Показывает, где вы находитесь и куда вы смотрите -\n• Делитесь своим расположением, чтобы друзья смогли найти вас -\n• Сохраняет ваши самые важные места в избранных -\n• Позволяет вам выбрать как отображать названия на карте: на английском, местным или с фонетическим написанием -\n• Отображает специальные онлайн-тайлы, спутниковые снимки (с Bing), различные метки, как туристические/навигационные треки GPX и дополнительные слои с настраиваемой прозрачностью + Карта +\n• Отображает POI (точки интереса) вокруг вас +\n• Поворачивает карту по направлению движения (или компаса) +\n• Показывает вашу позицию и направление взгляда +\n• Делитесь вашим местоположением, чтобы вас могли найти друзья +\n• Сохраняет важные для вас места в избранных +\n• Позволяет выбрать способ отображения названий на карте: на английском, местное или фонетическое написание. +\n• Отображает специальные онлайн-тайлы, спутниковые снимки (Bing), различные метки, как туристические/навигационные треки GPX и дополнительные слои с настраиваемой прозрачностью \n Катание на лыжах \n• OsmAnd-плагин лыжные карты позволяет видеть лыжные трассы с уровнем сложности и некоторой дополнительной информацией, как расположение подъёмников и других объектов. @@ -2668,9 +2668,9 @@ Копировать местоположение/название POI Место без названия Текущий - Добавляет промежуточную остановку - Добавляет начальную остановку - Перемещает пункт назначения и создаёт промежуточную точку + Добавить последним промежуточным пунктом + Добавить первым промежуточным пунктом + Ранее выбранный пункт назначения станет последним промежуточным пунктом Показать закрытые заметки Показать/скрыть заметки OSM на карте. GPX — подходит для экспорта в JOSM и другие OSM редакторы. @@ -2683,7 +2683,7 @@ OSM-заметки Впереди туннель Туннели - Сделать отправной точкой + Сделать пунктом отправления Введите имя файла. Ошибка импорта карты Карта импортирована @@ -2764,7 +2764,7 @@ Начать редактирование Получить неограниченный доступ Добро пожаловать на открытое бета-тестирование - Карты горизонталей и карты с отмывкой рельефа + Карты горизонталей и затенение рельефа Скачать статьи Википедии для %1$s, чтобы читать их в автономном режиме. Загрузка данных Википедии Открыть статью в интернете @@ -2788,7 +2788,7 @@ Для катания на лыжах. Выделяет горнолыжные трассы, подъёмники, трассы для беговых лыж и прочее. Меньше отвлекающих второстепенных объектов на карте. Скачать все Простой стиль для вождения. Мягкий ночной режим, контурные линии, контрастные дороги в оранжевом стиле, тусклые второстепенные объекты карты. - Для пеших походов, трекинга и велосипедных прогулок на природе. Читабельный на открытом воздухе и при сложном освещении. Контрастные дороги и природные объекты, различные типы маршрутов, контурные линии с расширенными настройками, дополнительные детали. Функция «Качество дорожного покрытия» позволяет различать дороги с различным качеством поверхности. Нет ночного режима. + Для пеших походов, трекинга и велопоездок на природе. Хорошо читается при сложном освещении. Контрастные дороги и природные объекты, различные типы маршрутов, контурные линии с расширенными настройками, дополнительные детали. Параметр «Дорожное покрытие» позволяет различать поверхность и качество дорог. Ночной режим отсутствует. Старый стиль по умолчанию «Mapnik». Похожие цвета на «Mapnik». Стиль общего назначения. Густонаселённые города показаны упрощённо. Выделяет контурные линии, маршруты, качество поверхности, ограничения доступа, дорожные щиты, визуализация пешеходных маршрутов по шкале SAC, объекты спортивных сплавов. Открыть ссылку Википедии в онлайн @@ -2796,13 +2796,13 @@ Получите подписку на OsmAnd Live, чтобы читать статьи в Википедии и Викигиде в автономном режиме. Как открыть ссылку? Читать Википедию в автономном режиме - Туристический стиль с высоким контрастом и максимальной детализацией. Включает все функции стиля OsmAnd по умолчанию, также отображая как можно больше деталей, в частности дороги, тропы и другие пути для передвижения. Чёткое различие между типами дорог, как во многих туристических атласах. Подходит для дневного, ночного и уличного использования. + Туристический стиль с высоким контрастом и максимальной детализацией. Включает все функции стиля OsmAnd по умолчанию, отображая максимальное количество деталей, в частности дороги, тропы и другие пути передвижения. Чёткое различие между типами дорог (как в туристических атласах). Подходит для использования днём, ночью и при ярком освещении. Сохранить Для езды по бездорожью, основано на топографическом стиле (англ. «Topo»), можно использовать с зелёными спутниковыми снимками в качестве подложки. Уменьшенная толщина основных дорог, увеличенная толщина путей, дорожек, велосипедных и других маршрутов. Модификация стиля по умолчанию для увеличения контраста пешеходных и велосипедных дорог. Использует старые цвета Mapnik. Получите OsmAnd Live, чтобы разблокировать все функции: ежедневные обновления карт с неограниченной загрузкой, все платные и бесплатные плагины, Википедия, Викигид и многое другое. Промежуточное время прибытия - Промежуточное время + Прибытие в промежуточный пункт Редактировать действие Пожалуйста, пришлите скриншот этого уведомления на support@osmand.net Редактировать точку @@ -2855,7 +2855,7 @@ Для продолжения дайте OsmAnd разрешение на определение местоположения. Чёрный Поиск улицы - Сначала выберите город/населённый пункт/местность + Укажите город/место/район Восстановить Маркеры, добавленные как группа избранных или путевых точек GPX и отмеченные как пройденные, останутся на карте. Если группа не активна, маркеры исчезнут с карты. Оставить пройденные маркеры на карте @@ -2891,7 +2891,7 @@ Точки интереса (POI) Расчёт маршрута… Общественный транспорт - Выберите дорогу на карте или из списка ниже, которую вы хотите избежать во время навигации: + Выберите на карте или в списке ниже дорогу, которой хотите избежать при навигации: Моделировать навигацию Выберите файл трека для следования Голосовые подсказки @@ -2908,7 +2908,7 @@ пешком Показывать вдоль маршрута Покрытие - Тип дороги + Класс дороги Крутизна Добавить дом Добавить работу @@ -2923,7 +2923,7 @@ Скрыть треки Показать треки Время суток - Поворот за поворотом + По шагам Типы дорог Переключатель, чтобы показать или скрыть выбранные треки на карте. На %1$s @@ -2987,7 +2987,7 @@ Ступеньки Тропа Велодорожка - Неопределённая + Не определено Узнайте больше о маршрутизации OsmAnd в нашем блоге. Навигация на общественном транспорте в настоящее время проходит бета-тестирование, возможны ошибки и неточности. Добавить промежуточную точку @@ -3066,7 +3066,7 @@ Метро Лошадь Вертолёт - Вы можете добавить собственную модифицированную версию routing.xml в ..osmand/routing + Вы можете добавить свою модифицированную версию файла routing.xml в ..osmand/routing Выберите значок Лыжи Тип: %s @@ -3101,20 +3101,20 @@ Использовать WunderLINQ для контроля Значок Собранные данные - Нажмите ещё раз, чтобы изменить ориентацию карты + Нажмите ещё раз для смены ориентации карты Последний запуск OsmAnd завершился ошибкой. Пожалуйста, помогите нам улучшить OsmAnd, отправив нам отчёт об ошибке. Режим: %s Режим пользователя, полученный из: %s Повторяющееся имя BRouter (локально) - Альпийские/горные лыжи + Горнолыжные спуски Склоны для катания и спуска на горных лыжах и доступ к подъёмникам. Лыжные туры Сани Склоны для катания на санях. Более сложные маршруты с крутыми участками дороги. В целом, препятствия, которых следует избегать. Сложные маршруты, с опасными препятствиями и крутыми участками. - Предпочитаемая сложность + Предпочтительная сложность Служба скачивания OsmAnd Пурпурный Оценить @@ -3158,7 +3158,7 @@ Сбой Внедорожник Выбор настроек карты для профиля - Выбор настроек экрана для профиля + Настройка элементов экрана для профиля Выбор настроек навигации для профиля Выбор верхней границы изменений Использовать бесконтактный датчик (сенсорный выключатель) @@ -3187,8 +3187,8 @@ \nРасчёт: %.1f с, %d дорог, %d тайлов) Добавьте хотя бы один элемент в список «Быстрые действия» в настройках Маршруты для горнолыжного туризма. - Офпист - Фрирайды и офписты являются неофициальными неадаптированными трассами. Обычно неухоженные, неразмеченные и неосвещённые вечером. Вход на свой страх и риск. + Вне трассы + Фрирайды и внетрассовые маршруты — это неофициальные неподготовленные трассы. Обычно неухоженные, неразмеченные и неосвещённые вечером. Вход на свой страх и риск. Открыть трек Соединить разрывы (исключить пробелы) Кемпер @@ -3233,11 +3233,11 @@ Карта во время навигации Скорость движения, размеры, масса транспортного средства Параметры транспортного средства - Голосовые оповещения происходят только во время навигации. + Голосовые инструкции работают только при навигации. Навигационные инструкции и объявления Голосовые подсказки Экранные оповещения - Настройка параметров маршрута + Настройки маршрутизации Параметры маршрута Буфер Logcat Настройки плагинов @@ -3319,7 +3319,7 @@ «%1$s» уже существует. Перезаписать\? Не удалось экспортировать профиль. Импорт профиля - Чтобы добавить профиль, откройте его с помощью OsmAnd. + Для добавления профиля откройте файл профиля с помощью OsmAnd. %1$s импортирован. Белый Невозможно запустить механизм преобразования текста в речь. @@ -3365,8 +3365,8 @@ Разрешить только маршруты для катания на коньках Маршруты, подготовленные для фристайла или катания только на коньках без классических треков. Разрешить только классические маршруты - Маршруты, подготовленные только для классического стиля без конькобежных трасс. Сюда входят маршруты, подготовленные небольшим снегоходом с более свободной лыжнёй и трассами, подготовленные вручную лыжниками. - Предпочитать маршруты заданной сложности, хотя прокладка маршрута по более сложным или лёгким трассам всё же возможна, если они короче. + Маршруты только для классического стиля, без конькобежных трасс. Сюда входят маршруты, подготовленные небольшим снегоходом с более свободной лыжнёй и трассами, подготовленные вручную лыжниками. + Предпочитаемый уровень сложности маршрутов. Более сложные или лёгкие трассы могут использоваться, если они короче. Включать на повороте Класс 1 Класс 2 @@ -3390,7 +3390,7 @@ Эксперт Фрирайд Экстрим - Неопределённо + Неопределённая Канатная дорога Соединение Симулировать свою позицию используя записанный GPX трек. @@ -3534,12 +3534,12 @@ Сохранение нового профиля Не удалось создать резервную копию профиля. Очистить записанные данные\? - Побочный эффект: в записи трека будут отсутствовать все участки, где критерий минимальной скорости не был соблюдён (например, когда вы толкаете велосипед вверх по крутому склону). Также не будет информации о периодах покоя, например, во время отдыха. Это влияет на любой анализ или последующую обработку, например, при попытке определить общую продолжительность поездки, время в движении или среднюю скорость. + Побочный эффект: в треке будут отсутствовать все участки, где не соблюдён критерий минимальной скорости (например где вы толкаете велосипед вверх по крутому склону). Также не будет информации о периодах покоя, например во время отдыха. Это влияет на любой анализ или последующую обработку, например при попытке определить общую продолжительность поездки, время в движении или среднюю скорость. Побочный эффект: в результате фильтрации по точности, точки могут быть полностью пропущены, например, под мостами, под деревьями, между высокими зданиями или при определённых погодных условиях. Примечание: при включении GPS непосредственно перед записью точность определения первой точки может быть снижена, поэтому мы рассматриваем добавление секундной задержки перед записью точки (либо записи лучшей из трёх последовательных точек и т. д.), но пока это не реализовано. Фильтр предотвращает запись точек при отсутствии фактического перемещения и улучшает вид треков без обработки. Побочные эффекты: периоды в состоянии покоя не записываются вообще или только по одной точке каждый. Небольшие (в реальности) перемещения (например, в сторону, указывающие возможное изменение направления движения) могут быть отфильтрованы. Файл содержит меньше информации для последующей обработки и имеет худшую статистику, отфильтровывая явно избыточные точки во время записи, при этом потенциально сохраняя артефакты, вызванные плохим приёмом или эффектами модуля GPS. - Рекомендация: настройка 5 метров может должна вас устроить, если нет необходимости учитывать более короткие перемещения, и вы точное не хотите записывать данные в состоянии покоя. + Рекомендация: настройка 5 метров может подойти вам, если нет необходимости учитывать более короткие перемещения, и вы точно не хотите записывать данные в состоянии покоя. Указанные %1$s уже существуют в OsmAnd. Импорт данных из %1$s Импортирование @@ -3553,9 +3553,9 @@ Отклонение, при котором маршрут будет пересчитан. Легенда Невозможно разобрать геоссылку «%s». - Для отображения затенения рельефа на карте необходимы дополнительные карты. + Для отображения затенения рельефа требуются дополнительные карты. Мин. - Отображение затенения рельефа или карты уклонов. Подробнее об этих типах карт вы можете прочитать на нашем сайте. + Способы отображения рельефа местности: посредством теней (затенение рельефа) или цветов (карта уклонов). Подробнее об этих типах карт вы можете прочитать на нашем сайте. Прозрачность Уровни масштаба Пересчитывать маршрут в случае отклонения @@ -3580,14 +3580,15 @@ Примечание: проверка скорости > 0: большинство модулей GPS сообщают значение скорости только в том случае, если алгоритм определяет, что вы движетесь, и ничего, если вы не перемещаетесь. Следовательно, использование параметра > 0 в этом фильтре в некотором смысле приводит к обнаружению факта перемещения модуля GPS. Но даже если мы не производим данную фильтрацию во время записи, то всё равно эта функция используется при анализе GPX для определения скорректированного расстояния, то есть значение, отображаемое в этом поле, является расстоянием, записанным во время движения. Разделение записи Укажите веб-адрес со следующими параметрами: lat={0}, lon={1}, timestamp={2}, hdop={3}, altitude={4}, speed={5}, bearing={6}. - В этом случае будут записываться только точки, измеренные с минимальной точностью (в метрах/футах согласно настройкам устройства). Точность — это близость измерений к истинному местоположению и не имеет прямого отношения к точности, подразумевающейся под разбросом повторных замеров. + "Будут записываться только точки, отвечающие +\n в минимальной точностью (в метрах/футах —зависит от настроек системы). Точность — это близость измерений к истинному положению, и она не связана напрямую с точностью, которая представляет собой разброс повторных измерений." Рекомендация: попробуйте сначала воспользоваться детектором движения через фильтр минимального смещения (B), что может дать лучшие результаты и вы потеряете меньше данных. Если треки остаются шумными на низких скоростях, попробуйте использовать ненулевые значения. Обратите внимание, что некоторые измерения могут вообще не указывать значения скорости (некоторые сетевые методы), и в этом случае ничего не будет записываться. - Уклон использует цвета для визуализации крутизны рельефа. + Для визуализации крутизны рельефа используются цвета. Подробнее об уклонах можно прочитать в %1$s. Затенение рельефа - Затенение рельефа использует тёмные оттенки для отображения склонов, вершин и низменностей. - Для отображения склонов на карте необходимы дополнительные карты. - Уклоны + Для отображения склонов, вершин и низменностей используются тёмные тени. + Для отображения уклонов требуются дополнительные карты. + Карта уклонов Заменить этой точкой другую. Изменения применены к профилю «%1$s». Невозможно прочитать из «%1$s». @@ -3789,7 +3790,7 @@ Изменение масштаба карты кнопками громкости. Масштабирование кнопками громкости Укажите длину автомобиля, для длинных транспортных средств могут применяться ограничения на маршруте. - Удалить следующий пункт + Удалить ближайший пункт Задайте название точки Следующая точка маршрута будет удалена. Если это конечный пункт, навигация завершится. Информация о достопримечательностях из Википедии. Ваш карманный офлайн-путеводитель — просто включите плагин Википедии и читайте об объектах вокруг вас. @@ -3803,7 +3804,7 @@ Добавить к треку Для продолжения задайте рабочие дни Маршрут между точками - Составить маршрут + Составление маршрута Выберите способ разбиения: по времени или по расстоянию. Интервал между метками расстояния или времени на треке. Своё @@ -3883,17 +3884,17 @@ сохранен Добавьте хотя бы две точки. ПОВТОРИТЬ - • Обновлённый режим планирования маршрута позволяет использовать разные типы навигации для каждого сегмента и прикрепляет любой трек к дорогам + • Обновлённая функция планирования маршрута позволяет применять к сегментам разные режимы навигации и настраивать привязку к дорогам \n -\n • Новые параметры внешнего вида для треков: можно выбрать цвет, толщину, включите стрелки направления и отметки начала/окончания +\n • Новые настройки вида треков: выбор цвета и толщины линии, указатели направления, метки начала и конца маршрута \n -\n • Улучшена видимость велосипедных узлов +\n • Повышенная видимость велосипедных узлов \n -\n • Контекстное меню для треков с основной информацией +\n • Контекстное меню с основной информацией для треков \n \n • Улучшенные алгоритмы поиска \n -\n • Улучшены параметры следования по треку в навигации +\n • Улучшенные настройки следования по треку в Навигации \n \n • Исправлены проблемы с импортом/экспортом настроек профиля \n @@ -3901,4 +3902,6 @@ Последнее изменение Имя: Я - А Имя: А - Я + Значки старта и финиша + Спасибо за покупку \'Контурных линий\' \ No newline at end of file diff --git a/OsmAnd/res/values-sc/phrases.xml b/OsmAnd/res/values-sc/phrases.xml index df52122c1e..62f4c1ece8 100644 --- a/OsmAnd/res/values-sc/phrases.xml +++ b/OsmAnd/res/values-sc/phrases.xml @@ -3840,4 +3840,5 @@ Eletrodomèsticos minores Tabellone de sas tzucadas Ricàrriga de abba potàbile + GNL (LNG) \ No newline at end of file diff --git a/OsmAnd/res/values-sc/strings.xml b/OsmAnd/res/values-sc/strings.xml index accd1282b4..57221ff561 100644 --- a/OsmAnd/res/values-sc/strings.xml +++ b/OsmAnd/res/values-sc/strings.xml @@ -3903,4 +3903,6 @@ Ùrtima modìfica Nùmene: Z – A Nùmene: A – Z + Iconas de incumintzu/fine + Gràtzias pro àere comporadu \'Curvas de livellu\' \ No newline at end of file diff --git a/OsmAnd/res/values-sk/phrases.xml b/OsmAnd/res/values-sk/phrases.xml index 14f1a5e6be..28ac2c4297 100644 --- a/OsmAnd/res/values-sk/phrases.xml +++ b/OsmAnd/res/values-sk/phrases.xml @@ -3274,9 +3274,9 @@ Mahájana Zamrznutí Turistická/trasová značka - - - + Ko-Shintō + Jizō + Prasat Apoštolská cirkev Radiačná onkológia Nebezpečenstvo @@ -3567,4 +3567,89 @@ Šípka Vibrácie Tlak + Skvapalnený zemný plyn + Obchod s orechmi + Včelí úľ + Cestovný poriadok + Odjazdy v reálnom čase + Intervaly + Áno + Tabuľa odjazdov: nie + Výťah + Mestský blok + Mestský obvod + Šípka: nie + Nie + Áno + Vibrovanie: nie + Výber hotovosti: cudzie karty + Výber hotovosti: minimálny nákup + Poplatok za výber hotovosti: nie + Poplatok za výber hotovosti: áno + Výber hotovosti: nie je vyžadovaný nákup + Výber hotovosti: vyžadovaný nákup + Mena výberu hotovosti + Limit výberu hotovosti + Typ výberu hotovosti: samoobslužný výber + Typ výberu hotovosti: pri pokladni + Operátor výberu hotovosti + Výber hotovosti + Výber hotovosti: áno + Zmiešané: nie + Zmiešané: áno + Ľad: nie + Ľad: áno + Trvanlivosť vodného zdroja: núdzový + Trvanlivosť vodného zdroja: trvalý + Ordinácia pôrodnej asistentky + Opatrovateľská služba + Ordinácia psychológa + Ordinácia liečiteľa + Ordinácia terapeuta + Ordinácia lekára + Zdravotná služba: testovanie: nie + Zdravotná služba: testovanie: áno + Zdravotná služba: podpora: nie + Zdravotná služba: podpora: áno + Zdravotná služba: očkovanie: nie + Zdravotná služba: očkovanie: áno + Zdravotná služba: prevencia: nie + Zdravotná služba: prevencia: áno + Zdravotná služba: starostlivosť o deti: nie + Zdravotná služba: starostlivosť o deti: áno + Zdravotná služba: vyšetrenie: nie + Zdravotná služba: vyšetrenie: áno + Zdravotná služba: poradenstvo: nie + Zdravotná služba: poradenstvo: áno + Zdravotná služba: opatrovateľstvo: áno + Zdravotná služba: opatrovateľstvo: nie + Správanie + Športová medicína + Malé elektrické spotrebiče + Tabuľa odjazdov/cestovný poriadok + Doplnenie pitnej vody + Nasávanie + Pod tlakom + Spodná voda + Potrubie + Sieť doplnenia pitnej vody + Doplnenie pitnej vody: nie + Áno + Prekážka + Výška vody: pod strednou hladinou + Výška vody: nad strednou hladinou + Výška vody: prekrýva + Výška vody: suché + Výška vody: ponorené + Výška vody: čiastočne ponorené + Nesprávne + Primitívne + Kontrastné + Len keď je povolená chôdza + Nie + Áno + Typ búdky + Búdka + Nie + Áno \ No newline at end of file diff --git a/OsmAnd/res/values-sk/strings.xml b/OsmAnd/res/values-sk/strings.xml index a9fbf672f4..6ce4134d80 100644 --- a/OsmAnd/res/values-sk/strings.xml +++ b/OsmAnd/res/values-sk/strings.xml @@ -224,7 +224,7 @@ Aktualizovať OsmAnd+ Stiahnite novú verziu aplikácie, aby ste mohli použiť nové mapové súbory. Online filter Nominatim - Hľadám umiestnenie… + Hľadám polohu… Moja poloha (nájdená) Adresa… Obľúbené miesta… @@ -325,11 +325,11 @@ ft mph mi - Zdieľať umiestnenie cez + Zdieľať polohu cez Pozícia: %1$s\n%2$s Na zobrazenie umiestnenia nasledujte webový odkaz %1$s alebo odkaz androidovského obsahu (intent link) %2$s - Poslať umiestnenie - Zdieľať umiestnenie + Poslať polohu + Zdieľať polohu GPX bod (waypoint) \"{0}\" pridaný Pridať waypoint do nahranej GPX stopy Administratíva @@ -419,7 +419,7 @@ sem zadajte čo chcete nájsť Mapa s vysokým rozlíšením Nerozťahovať (a nerozmazať) mapové dlaždice na obrazovkách s vysokou hustotou bodov. - Umiestnenie zatiaľ nenájdené. + Poloha zatiaľ nezistená. Hľadať hromadnú dopravu Hľadá sa preprava (bez cieľa): Hľadá sa preprava ({0} ako cieľ): @@ -743,7 +743,7 @@ Pridať pripomienku do aplikácie Kalendár Časovo obmedzené parkovanie Časovo neobmedzené parkovanie - Umiestnenie vášho zaparkovaného vozidla. %1$s + Poloha vášho zaparkovaného vozidla. %1$s Vyzdvihnúť vozidlo o: popoludní dopoludnia @@ -754,7 +754,7 @@ Označiť ako parkovacie miesto Odstrániť parkovaciu značku Východzí bod je príliš ďaleko od najbližšej cesty. - Zdieľané umiestnenie + Zdieľaná poloha Pridelená pamäť %1$s MB (Android limit %2$s MB, Dalvik %3$s MB). Pridelená pamäť Celková natívna pamäť pridelená aplikácii %1$s MB (Dalvik %2$s MB, iné %3$s MB). @@ -886,7 +886,7 @@ \n \nGlobálne údaje (medzi 70° severne a 70° južne) sú založené na meraní SRTM (Shuttle Radar Topography Mission) a ASTER (Advanced Spaceborne Thermal Emission and Reflection Radiometer), zobrazovacieho nástroja na palube Terra, vlajkového satelitu NASA Earth Observing System. ASTER je kooperatívne úsilie medzi NASA, Japonským ministerstvom hospodárstva, obchodu a priemyslu (METI) a Japonských vesmírnych systémov (J-spacesystems). Meranie vzdialenosti - Stlačte \"Použiť umiestnenie…\" pre pridanie poznámky k polohe. + Stlačte \"Použiť polohu…\" pre pridanie poznámky k polohe. Zvukové poznámky Vytvárajte obrazové/zvukové/video poznámky počas výletu, buď tlačidlom na mape alebo v kontextovom menu polohy na mape. Audio/video poznámky @@ -1393,7 +1393,9 @@ \n \nTento pohľad môže byť vypnutý jeho deaktivovaním tu alebo zmenou hodnoty v \"Štýl vykresľovania\" v \"Nastaviť mapu\". Cestovný mapový pohľad - Umiestnenie:\n Šírka %1$s\n Dĺžka %2$s + Poloha: +\n Šírka %1$s +\n Dĺžka %2$s Zobraziť dní pozadu Premenovanie zlyhalo. @@ -2632,7 +2634,7 @@ Cestovný pohľad Námorný Kopírovať názov bodu/umiestnenia - Nepomenované umiestnenie + Nepomenované miesto Zobraziť uzavreté poznámky Zobraziť/skryť OSM poznámky na mape. GPX - vhodné na export do JOSM a iných editorov OSM. @@ -3890,16 +3892,18 @@ \n \n • Zlepšená viditeľnosť bodov pre bicykle. \n -\n • Stopy je teraz možné aktivovať, pre kontextové menu sú základnými údajmi. +\n • Stopy je teraz možné aktivovať a majú kontextové menu so základnými údajmi. \n \n • Zlepšený algoritmus vyhľadávania \n \n • Zlepšené možnosti nasledovania stopy v navigácii \n -\n • Opravené problému s importom a exportom nastavení profilov +\n • Opravené problémy s importom a exportom nastavení profilov \n \n Naposledy zmenené Názov: Z – A Názov: A – Z + Ikony štartu/cieľa + Ďakujeme za zakúpenie modulu \'Vrstevnice\' \ No newline at end of file diff --git a/OsmAnd/res/values-tr/phrases.xml b/OsmAnd/res/values-tr/phrases.xml index ed3292f096..16005d8a23 100644 --- a/OsmAnd/res/values-tr/phrases.xml +++ b/OsmAnd/res/values-tr/phrases.xml @@ -45,7 +45,7 @@ Kullanıcı tanımlı Paleontolojik alan Fırın - Likör dükkanı + İçki dükkanı Peynir dükkanı Çikolata dükkanı Kahve dükkanı @@ -326,7 +326,7 @@ Düşük enerji Ampüller Floresan tüpler Metal - Elektrik öğeleri + Elektrik ögeleri Beyaz eşya Yemeklik yağ Motor yağı @@ -362,7 +362,7 @@ Çocuk bezi Araba aküsü Arabalar - Bisiklet + Bisikletler Depolama Çöp bertaraf Çöp tenekesi @@ -766,7 +766,7 @@ Tarımsal motorlar Demirci Bira fabrikası - Boat builder + Gemi yapımcısı Ciltçi Marangoz Halı satıcısı @@ -866,7 +866,7 @@ Gün işareti Mesafe işareti Havuz - Lezbiyen + Su seti Simgesel Yapı Deniz işareti, ışık Deniz işareti, büyük ışık @@ -1740,8 +1740,8 @@ Açıklık genişliği 0.5$ madeni para Satıcı - Onarım - Onarım yok + Tamir + Tamir yok Elektrikli araçların tamiri Motosiklet tamiri Evet @@ -2043,8 +2043,8 @@ Bisiklet kiralama: hayır Pompa Bisiklet pompası: hayır - Kendin-Yap onarım için araçlar - Kendin-Yap onarım için bisiklet araçları: hayır + Kendin-Yap tamir için araçlar + Kendin-Yap tamir için bisiklet araçları: hayır Temizleme Bisiklet temizleme: hayır Zincir aleti @@ -2148,7 +2148,7 @@ Kostüm Geleneksel Takım elbise - Hamile + Hamilelik Nostalji Büyük beden Okul @@ -2201,8 +2201,8 @@ Karavan: hayır Hazırlıksız: evet Hazırlıksız: hayır - Tuvalet atığı istasyonu: evet - Tuvalet atığı istasyonu: hayır + Sıhhi atık boşaltma istasyonu: evet + Sıhhi atık boşaltma istasyonu: hayır Evet Güç kaynağı: hayır Güç kaynağı (soket): CEE 17 mavi @@ -2443,4 +2443,476 @@ Tarihi tren istasyonu Tarihi çiftlik Pa (müstahkem maori yerleşimi) + Futsal (salon futbolu) + ATM: hayır + ATM: evet + Boks + Lakros + Disk iteleme oyunu + Squash (duvar tenisi) + Uzaktan kumandalı araba yarışı + Disk golf + Judo + Badminton + Karting + Netbol + Koşma + Gal oyunları + Dojo + Resmi adı + Araç park çatısı + Garaj bölmeleri + Tür: yüzey + Su ısıtıcısı: hayır + Su ısıtıcısı: evet + Mikrodalga fırın: hayır + Mikrodalga fırın: evet + Bilardo + Yüzme: hayır + Yüzme: evet + Tekne depolama + Referans numarası + Tünel numarası + Köprü numarası + Taşıma: evet + Uzunluk + Havai fişek mağazası + Elektronik tamiri + Hackerspace + Fitness istasyonu + Bina türü: piramit + Palyatif tıp + Davranış + Derinlik psikolojisi + Naturopati + Kayropraktik + Bitkisel tıp + Reiki + Geleneksel Çin tıbbı + Homeopati + Akupunktur + Yetişkin psikiyatrisi + Podoloji + Spor hekimliği + Manuel terapi + Konuşma terapisi + Klinik patoloji + Optometri + Bağımlılık tedavisi + Sağlık uzmanlığı: obstetrik (sezaryen): hayır + Obstetrik (sezaryen) + Sağlık uzmanlığı: sosyal pediatri: hayır + Sosyal pediatri + Sağlık uzmanlığı: obstetrik (doğum öncesi): hayır + Obstetrik (doğum öncesi) + Sağlık uzmanlığı: obstetrik (doğum sonrası): hayır + Obstetrik (doğum sonrası) + Sağlık uzmanlığı: tropikal tıp: hayır + Tropikal tıp + Onkoloji + Patolojik anatomi + Nükleer tıp + Endokrinoloji + Nöropsikiyatri + Beyin ve sinir cerrahisi + Nefroloji (böbrek hastalıkları) + Diş hekimliği + Gastroenteroloji + Tıbbi görüntüleme + Çene-yüz cerrahisi + Fiziksel tıp ve rehabilitasyon + Çocuk psikiyatrisi + İş terapisi + Biyokimya + Fizyoterapi + Ortodonti + Plastik cerrahi + Sağlık uzmanlığı: kaza ve acil tıp: hayır + Kaza ve acil tıp + Hamilelik + Diş, ağız ve çene-yüz cerrahisi + Göğüs hastalıkları + Anesteziyoloji + Osteopati + Klinik biyoloji + Travmatoloji + Kardiyoloji + Dermato-venereoloji + Nöroloji + Psikiyatri + Radyoterapi + Radyoloji + Genel cerrahi + Üroloji + Dermatoloji + Sağlık uzmanlığı: pediatri: hayır + Pediatri + Kulak burun boğaz + Ortopedi + İç hastalıkları + Jinekoloji + Oftalmoloji + Pratisyen doktor + Ağır yük araçları + Bisikletler + Konteynerler + Araçlar + Yolcular + Apartman + Ok: hayır + Evet + Evet + Titreşim: hayır + Evet + Evet + Hayır + Evet + Göl + Irmak + Hamam + Konum + Feribot + Tramvay + Otobüs + Tren + Evet + Çocuk + Akademik + Din + Kan bağışı + Toptancı + Yalnızca + Evet + Ayakkabı tamiri + Kaya + Tekerlek + Araba tamiri + Tür: tarım + Posta kodu + Ev numarası + Posta kutusu + Park ücreti + Park ücreti: hayır + Park ücreti: evet + Tır: hayır + Tır: evet + Bisiklet: hayır + Bisiklet: evet + Araba: hayır + Araba: evet + Kısıtlı + Kapalı + Açık + Görünürlük: sokak + Görünürlük: ev + Konum: duvar + Konum: yerüstü + Konum: sualtı + Konum: yeraltı + Metal ızgara + Yapay çimen + Kil + Hazine + Kamu hizmeti + Bakanlık + Arşiv + + Yatak + Yön: her + Yön: çıkış + Yön: giriş + Yön: aşağı + Yön: yukarı + Yön: saat yönünün tersi + Yön: saat yönü + Yön: geri + Yön: ileri + Yön: kuzey-kuzeybatı + Yön: kuzeybatı + Yön: batı-kuzeybatı + Yön: batı + Yön: batı-güneybatı + Yön: güneybatı + Yön: güney-güneybatı + Gün: güney + Yön güney-güneydoğu + Yön: güneydoğu + Yön: doğu-güneydoğu + Yön: doğu + Yön: doğu-kuzeydoğu + Yön: kuzeydoğu + Yön: kuzey-kuzeydoğu + Yön: kuzey + Sürüngen + Kuşhane + Kuşlar + Elektronik tamir: televizyon + Elektronik tamir: telefon + Elektronik tamir: bilgisayar + Şarap: perakende + Şarap: evet + Evet + Çilingir + Elektrik mağazası + Tibet + Bulgar + Yahudi + Senegal + Mısır + Avustralya + Suriye + Hollanda + Tayvan + Pakistan + İngiliz + Hawaii + Ermeni + Jamaika + İsveç + Kanton + İsviçre + Bask + Belçika + Afgan + Ukrayna + Orta Doğu + Moğol + Nepal + Latin Amerika + Britanya + Küba + Çek + Özbek + Avrupa + Lao + Macar + Etiyopya + İrlanda + Malezya + Avusturya + Fas + Madagaskar + Fars + Bolivya + Hırvat + Peru + Balkan + Arjantin + Karayip + Afrika + Endonez + Dan + Arap + Brezilya + Leh + Gürcü + Portekiz + Filipin + Rus + Lübnan + Bavyera + Akdeniz + Kore + Vietnam + İspanyol + Türk + Uluslararası + Tay + Yunan + Fransız + Asya + Amerikan + Hint + Alman + Japon + Meksika + Çin + İtalyan + Bölgesel + Patates + Şarap + Çikolata + Kanat + Et + Yoğurt + Tatlı + Salata + Çorba + Çay + Makarna + Biftek + Kahvaltı + Mangal + Suşi + Dondurma + Tavuk + Döner + Kebap + Sandviç + Pizza + Hamburger + Kahve + Noel ağacı + Noel + Vikipedi + Uzunluk + Su deposu + Delegasyon + Başkonsolosluk + Konsolosluk + Yalnızca + Evet + Su kültürü: midye + Su kültürü: balık + Su kültürü: karides + Su kültürü + Termometre: hayır + Termometre + Barometre: hayır + Barometre + Teleskop + Özel + Askeri/kamusal + Askeri + Kamusal + Bölgesel + Uluslararası + Yaz kampı + Tuz: hayır + Tuz + Derinlik + Otizm: hayır + Otizm: evet + Ebola: hayır + Ebola: evet + Sıtma: hayır + Sıtma: evet + Evet + Acil: hayır + Acil: evet + Danışma: hayır + Danışma: evet + Kapasite (yatak) + Danışma (evsiz): hayır + Danışma (evsiz): evet + Danışma (aile): hayır + Danışma (aile): evet + Danışma (uyuşturucu): hayır + Danışma (uyuşturucu): evet + Danışma (bağımlılık): evet + Danışma (bağımlılık): evet + Sağlık çalışanının işi: psikolog + İlk yardım çantası + Duvar + Yeraltı + Sağlık merkezi türü: sahra hastanesi + Sağlık merkezi türü: laboratuvar + Sağlık hizmeti: aşılama: hayır + Sağlık hizmeti: aşılama: evet + Sağlık hizmeti: danışma: hayır + Sağlık hizmeti: danışma: evet + Sağlık hizmeti: hasta bakıcılık: hayır + Sağlık hizmeti: hasta bakıcılık: evet + Geleneksel Tibet + Geleneksel Moğol + Geleneksel Çin + Batılı + Solaryum + Çeşme + Elektrikli süpürtge: hayır + Elektrikli süpürge + Evet + Evet + Evet + Ev adı + Patlama zamanı (UTC) + Patlama tarihi (UTC) + Patlama türü: yeraltı + Patlama türü: yeraltı, tünel + Patlama: ülke + Korunan nesne: su + Korunan nesne: yaşam alanı + Korunan nesne: doğa + Korunan nesne: tarihi + Koruma alanı + Yapım aşamasında + Fast food + Pankek + Pasta + Izgara + Sosis + Sosisli sandviç + Köri + Krep + Erişte (ramen) + Çörek (donut) + Erişte (noodle) + Deniz ürünleri + Balık ve patates kızartması + Noel: web sitesi + Noel: konum + Noel: çalışma saatleri + Noel: not + Noel: etkinlik dönemi + Ağaç dükkanı + Noel dükkanı + Noel piramidi + Noel marketi + Noel etkinliği + Yolcu bilgilendirme ekranı: hayır + Yolcu bilgilendirme ekranı: evet + Higrometre: hayır + Higrometre + Kullanım: casusluk + Kullanım: araştırma + Kullanım: casusluk + Kullanım: eğitim + Ev ziyareti: hayır + Yunani + Sidda + Kampo + Ayurveda + Sıhhi atık boşaltma istasyonu + Yalnızca adil ticaret ürünleri + Adil ticaret: hayır + Adil ticaret: evet + Basınçlı hava: hayır + Bitki fidanlığı + Motorlu tekneler: hayır + Motorlu tekneler: evet + Tekne kiralama + Konum: giriş + Yön tabelası: orman tahsisi + Yön tabelası: orman bölmesi + Motosiklet giysileri: hayır + Motosiklet giysileri + Lastik: hayır + Lastik + Yedek parça: hayır + Yedek parça + Tamir: hayır + Tamir + Kiralama: hayır + Kiralama + Müzik okulu + Şarap: servis + Parti malzemeleri + Cajun + Tex-mex + Baget + Waffle + Krep + Falafel + Taco + Kantin + Tuzlu krep + Kızarmış tavuk + Kuskus + Fırın + Bistro + Kızarmış yiyecekler + Dondurulmuş yoğurt + Şarküteri + Turta + Çay dükkanı \ No newline at end of file diff --git a/OsmAnd/res/values-tr/strings.xml b/OsmAnd/res/values-tr/strings.xml index 9713a69b10..50a41c8562 100644 --- a/OsmAnd/res/values-tr/strings.xml +++ b/OsmAnd/res/values-tr/strings.xml @@ -892,7 +892,7 @@ Güzergahı göster Yönlendirmeyi başlatın Lütfen önce hedefi ayarlayın - Açılış saatleri + Çalışma saatleri Yetkilendirme başarısız Sokaklar/binalar yükleniyor… Sokaklar yükleniyor… @@ -1374,7 +1374,7 @@ Açılış Kapanış İletişim Bilgileri - Açılış saatleri ekle + Çalışma saatleri ekle POI Türü Dash %1$s satır sayısı POI türü belirtiniz. @@ -3859,4 +3859,6 @@ Son değiştirme İsim: Z – A İsim: A – Z + Başlangıç/bitiş simgeleri + \'Eş yükselti eğrileri\'ni satın aldığınız için teşekkürler \ No newline at end of file diff --git a/OsmAnd/res/values-uk/phrases.xml b/OsmAnd/res/values-uk/phrases.xml index 4375b43720..493e631c02 100644 --- a/OsmAnd/res/values-uk/phrases.xml +++ b/OsmAnd/res/values-uk/phrases.xml @@ -3832,4 +3832,5 @@ Невеликі електроприлади Вулик Насіннєвий магазин + СПГ \ No newline at end of file diff --git a/OsmAnd/res/values-uk/strings.xml b/OsmAnd/res/values-uk/strings.xml index 905af29b00..fda15847c5 100644 --- a/OsmAnd/res/values-uk/strings.xml +++ b/OsmAnd/res/values-uk/strings.xml @@ -42,7 +42,7 @@ \nБудь яка з цих мап може використовуватись як основна мапа в OsmAnd, або як покриття чи підкладка до іншої основної мапи (наприклад усталена безмережева мапа OsmAnd). Для того, щоб зробити більш помітною будь-яку мапу-підкладку, певні елементи векторної мапи OsmAnd можна легко сховати через меню „Налаштування мапи“ за бажанням, щоб зробити будь-яку підкладку мапи помітнішою.. \n \nКвадрати мап можна отримувати безпосередньо з мережевих джерел або підготувати їх для безмережевого використання (та вручну скопіювати в теку даних OsmAnd) у вигляді бази даних SQLite, яку можна створити за допомогою різноманітних сторонніх знаряддь підготовки мап. - Показує налаштування для увімкнення фонового трекінгу та навігації шляхом періодичного пробудження GPS-передавача (з вимкненим екраном). + Показує налаштування для увімкнення відстеження у тлі та навігації шляхом періодичного пробудження GPS-передавача (з вимкненим екраном). Робить спеціальні можливості пристрою доступними безпосередньо в OsmAnd. Це полегшує, наприклад, налаштування частоти мовлення для голосу синтезу мовлення, настроювання D-Pad навігації, за допомогою трекбола для контролю масштабу або зворотного зв\'язку синтезу мовлення, наприклад, щоб автоматично оголосити свою позицію. Налаштування функцій розробки та налагодження, як-от навігаційне моделювання, дієвість відмальовування чи голосові підказки. Призначений для розробників, не потрібний для звичайного використання застосунку. Втулки @@ -158,7 +158,7 @@ Файл з раніше імпортованими Закладками вже існує. Замінити його? Налаштування профілю Усталений профіль - Вид мапи й налаштування навігації зберігаються для кожного окремого профілю. Встановіть ваш типовий профіль. + Вид мапи й налаштування навігації зберігаються для кожного окремого профілю. Встановіть ваш усталений профіль. Навігація Визначити налаштування навігації. Загальні налаштування @@ -169,8 +169,8 @@ Ім\'я користувача OSM Потрібно для входу на openstreetmap.org. Пароль - Фоновий режим - OsmAnd працює у фоновому режимі з вимкненим екраном. + Режим тла + OsmAnd працює у режимі тла з вимкненим екраном. Не вистачає місця на диску для завантаження %1$s MB (вільно: %2$s). Завантажити {0} файл(ів)\? \n{1} МБ (із {2} МБ) буде використано. @@ -281,8 +281,8 @@ Прозорість Змінити прозорість основної мапи. Прозорість мапи - Фонова мапа… - Фонова мапа + Мапа підкладки… + Мапа підкладки Виберіть тлову мапу Верхній шар… Нема @@ -428,7 +428,7 @@ Відсутні точки Закладок для збереження Імпортувати Не вдалося завантажити GPX. - Відправити звіт + Надіслати звіт Не знайдено завантаженої мапи на карті пам\'яті. Почніть вводити текст для пошуку POI Всі @@ -456,7 +456,7 @@ Виберіть метод позиціонування, що використовується тловою службою: Джерело позиціювання Відслідковує Ваше місцерозташування, поки екран вимкнено. - Запустити OsmAnd у фоні + Запустити OsmAnd у тлі Службі навігації у тлі необхідно, аби постачальник позиціювання був активним. Сховати фільтр Показати фільтр @@ -759,8 +759,9 @@ Торкніться значка блокування, щоб розблокувати Розблокувати Запустити -\n застосунок у фоновому режимі - Вимкнути\nфоновий режим +\n застосунок у режимі тла + Вимкнути +\nрежим тла Час прибуття не відмічено Мапа @@ -846,7 +847,7 @@ Безпечний режим Програму запущено в безпечному режимі (вимкніть його в \'Налаштуваннях\'). Оберіть \"Використати місцезнаходження...\" для прив\'язки нотатки до даного місцезнаходження. - Фоновий режим OsmAnd досі запущений. Зупинити його роботу також? + Службу OsmAnd у тлі досі запущено. Зупинити її роботу також\? Закрити набір змін Програма \'ZXing Barcode Scanner\' не встановлена. Шукати в Google Play? Виберіть кольорову схему доріг: @@ -1078,9 +1079,9 @@ Колір Продовжувати Зупинити - Ввімкнути фоновий режим GPS + Увімкнути режим GPS у тлі Інтервал вмикання GPS - Зупинити фоновий режим GPS? + Зупинити режим GPS у тлі\? Бажана мова для підписів на мапі (якщо вона недоступна, будуть використані англійська чи місцева мови). Бажана мова мапи Назви місцевою мовою @@ -1686,7 +1687,7 @@ м/с Запис подорожі Навігація - Працює у фоновому режимі + Працює у тлі Відомості про закладки Зупинити симуляцію Вашої позиції. Пошук адреси @@ -2390,12 +2391,12 @@ \n• Опціональний запис подорожі в локальний GPX-файл чи онлайн-сервіс \n• Опціональне відображення швидкості та висотного розташування \n• Відображення горизонталей та рельєфу (через додатковий втулок) - Безпосередній вклад у OSM -\n• Звітуйте про помилки в даних -\n• Вивантажуйте GPX-треки в OSM безпосередньо з програми -\n• Додавайте POI (цікаві точки) та безпосередньо вивантажуйте їх в OSM (чи пізніше, якщо зараз Ви в офлайні) -\n• Опція запису подорожі також і у фоновому режимі (в той час як пристрій знаходиться в сплячому режимі) -\nOsmAnd — вільне й відкрите програмне забезпечення, що активно розвивається. Кожен може внести свій вклад, звітуючи про помилки, поліпшуючи переклад чи розробляючи нові функції. Також проєкт покладається на фінансові внески для оплати розробки та тестування нових функціональних можливостей. + Безпосередній вклад у OSM +\n• Звітуйте про помилки в даних +\n• Вивантажуйте GPX-треки в OSM безпосередньо з програми +\n• Додавайте POI (цікаві точки) та безпосередньо вивантажуйте їх в OSM (чи пізніше, якщо зараз Ви в офлайні) +\n• Опція запису подорожі також і в режимі тла (в той час як пристрій знаходиться в сплячому режимі) +\nOsmAnd — вільне й відкрите програмне забезпечення, що активно розвивається. Кожен може внести свій вклад, звітуючи про помилки, поліпшуючи переклад чи розробляючи нові функції. Також проєкт покладається на фінансові внески для оплати розробки та тестування нових функціональних можливостей. \n Приблизне охоплення мап та якість: \n • Західна Європа: **** @@ -3043,7 +3044,7 @@ Лижі Показати компас-лінійку Приховати компас-лінійку - Оберіть піктограму + Оберіть значок Режим: %s Користувацький режим, похідний від: %s Лижі @@ -3068,31 +3069,31 @@ Натисніть ще раз для зміни орієнтації мапи Мінімальна швидкість Максимальна швидкість - Типова швидкість + Усталена швидкість Змінити налаштування усталеної швидкості Встановити мінімальну/максимальну швидкість Новий профіль Збій Під час останнього запуску OsmAnd сталася помилка. Допоможіть нам покращити OsmAnd - надішліть повідомлення про помилку. НЛО - • Профілі застосунку: створюйте власний профіль з довільною піктограмою та кольором для ваших особистих потреб -\n -\n• Налаштуйте типову мінімальну/максимальну швидкості профілю -\n -\n• Додано віджет з поточними координатами -\n + • Профілі застосунку: створюйте власний профіль з довільним значком та кольором для ваших особистих потреб +\n +\n• Налаштуйте усталену найменшу/найбільшу швидкості профілю +\n +\n• Додано віджет з поточними координатами +\n \n• Додано можливість показувати на мапі компас і радіус-лінійку -\n +\n \n• Виправлено записування шляху у тлі -\n -\n• Покращено завантаження мап у тлі +\n +\n• Покращено завантаження мап у тлі \n \n• Повернено параметр \'Увімкнути екран\' \n -\n• Виправлено вибір мови Wikipedia -\n +\n• Виправлено вибір мови Wikipedia +\n \n• Виправлено поведінку кнопки компаса під час навігації -\n +\n \n• Інші виправлення помилок \n \n @@ -3430,7 +3431,7 @@ OSM Значок відображається під час навігації чи переміщення. Значок показано в спокої. - Перевіряти та обмінюватися докладними журналами застосунку + Переглянути та надіслати докладний журнал застосунку Не вдалося розібрати метод \'%s\'. Для використання цього параметра потрібен дозвіл. Це низькошвидкісний відсічний фільтр, щоб не записувати точки нижче певної швидкості. Це може призвести до плавнішого вигляду записаних треків при перегляді на мапі. @@ -3670,7 +3671,7 @@ Профілі навігації • Нові автономні мапи схилів \n -\n• Налаштування вибраних та GPX шляхових точок - спеціальні кольори, піктограми, форми +\n• Налаштування вибраних та GPX шляхових точок - спеціальні кольори, значки, форми \n \n• Налаштування порядку елементів меню \"Контекстне меню\", \"Налаштувати мапу\" та \"Скринька\" \n @@ -3694,7 +3695,7 @@ Кнопка показу або приховування громадського транспорту на мапі. Створити / змінити POI Додати / правити вибране - Відновити типовий порядок елементів + Відновити усталене впорядкування Повернутися до редагування Ви можете отримати доступ до цих дій, торкнувшись кнопки “%1$s”. Продовжити @@ -3741,7 +3742,7 @@ \nОдин тиждень - 10 080 хвилин. \nОдин місяць - 43 829 хвилин. Виберіть спосіб зберігання завантажених плиток. - Типовий час до вимкнення екрану + Усталений час до вимкнення екрану Ви можете експортувати або імпортувати швидкі дії з профілями застосунку. Видалити все\? Ви дійсно бажаєте безповоротно видалити %d швидких дій\? @@ -3805,7 +3806,7 @@ Маршрут між точками Планування маршруту Додати до треку - Показувати піктограми старт та фініш + Показувати значки початку та завершення Встановити ширину Виберіть інтервал показу міток часу або відстані для показу поверх треку. Виберіть власний варіант поділу: за часом чи відстанню. @@ -3863,7 +3864,7 @@ Змінити вид маршруту раніше Змінити вид маршруту після Вкажіть інтервал для загального запису поїздки (включається через віджет запису подорожі на мапі). - Типова системи + Усталена системна Всі наступні сегменти Попередній сегмент Усі попередні сегменти @@ -3876,14 +3877,14 @@ Відкрити збережений трек збережено Додайте принаймні дві точки. - Запис треку буде зупинено після припинення роботи застосунку через меню з переліком нещодавно запущених застосунків. (Індикатор, який інформує про роботу OsmAnd у фоні зникне з панелі сповіщень Android) + Запис треку буде зупинено після припинення роботи застосунку через меню з переліком нещодавно запущених застосунків. (Індикатор, що повідомляє про роботу OsmAnd у тлі зникне з панелі сповіщень Android) Спрощений трек Буде збережено лише лінію маршруту, а проміжні точки буде видалено. Назва файлу Повторити • Оновлено функції планування маршруту: дозволено застосувати різні типи переходів для кожного сегмента і прив\'язати будь-який трек до доріг \n -\n• Нове меню вигляду треків: вибір кольору, товщина, вигляд стрілки напрямку, піктограми початку/завершення +\n• Нове меню вигляду треків: вибір кольору, товщина, вигляд стрілки напрямку, значки початку/завершення \n \n• Покращено оглядовість велосипедних вузлів \n @@ -3899,4 +3900,6 @@ Востаннє змінено За назвою: Я — А За назвою: А — Я + Значки початку/завершення + Дякуємо за придбання «Горизонталей» \ No newline at end of file diff --git a/OsmAnd/res/values-zh-rTW/phrases.xml b/OsmAnd/res/values-zh-rTW/phrases.xml index 25766f928e..e36aa4414d 100644 --- a/OsmAnd/res/values-zh-rTW/phrases.xml +++ b/OsmAnd/res/values-zh-rTW/phrases.xml @@ -3843,4 +3843,5 @@ 小電器 蜂箱 堅果店 + LNG \ No newline at end of file diff --git a/OsmAnd/res/values-zh-rTW/strings.xml b/OsmAnd/res/values-zh-rTW/strings.xml index b12cc12a51..cf0cc3fd99 100644 --- a/OsmAnd/res/values-zh-rTW/strings.xml +++ b/OsmAnd/res/values-zh-rTW/strings.xml @@ -3425,7 +3425,7 @@ OSM 在導航或移動時顯示的圖示。 靜止時顯示的圖示。 - 檢視及分享應用程式的紀錄檔 + 檢查及分享應用程式的詳細紀錄 無法解析地理含義「%s」。 您已紀錄的軌跡位於 %1$s,或是 OsmAnd 資料夾。 您的 OSM 註記位於 %1$s。 @@ -3899,4 +3899,6 @@ 最後修改時間 名稱:Z – A 名稱:A – Z + 開始/結束圖示 + 感謝您購買 \'Contour lines\' \ No newline at end of file diff --git a/OsmAnd/res/values/phrases.xml b/OsmAnd/res/values/phrases.xml index 3f4394578a..b58d25a781 100644 --- a/OsmAnd/res/values/phrases.xml +++ b/OsmAnd/res/values/phrases.xml @@ -4257,5 +4257,7 @@ Nut store + LNG + diff --git a/OsmAnd/res/values/strings.xml b/OsmAnd/res/values/strings.xml index a8b3d34fbe..6093d106d4 100644 --- a/OsmAnd/res/values/strings.xml +++ b/OsmAnd/res/values/strings.xml @@ -15,6 +15,12 @@ Clear OpenStreetMap OAuth token Log in via OAuth Perform an OAuth Login to use osmedit features + Avoid footways + Avoid footways + Payment will be charged to your AppGallery account at the confirmation of purchase.\n\nSubscription automatically renews unless it is canceled before the renewal date. Your account will be charged for renewal period(month/three month/year) only on the renewal date.\n\nYou can manage and cancel your subscriptions by going to your AppGallery settings. + Subscription charged per selected period. Cancel it on AppGallery at any time. + Thank you for purchasing \'Contour lines\' + Start/finish icons Name: A – Z Name: Z – A Last modified diff --git a/OsmAnd/src-google/net/osmand/plus/inapp/InAppPurchaseHelperImpl.java b/OsmAnd/src-google/net/osmand/plus/inapp/InAppPurchaseHelperImpl.java new file mode 100644 index 0000000000..c6925a5e63 --- /dev/null +++ b/OsmAnd/src-google/net/osmand/plus/inapp/InAppPurchaseHelperImpl.java @@ -0,0 +1,583 @@ +package net.osmand.plus.inapp; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.billingclient.api.BillingClient; +import com.android.billingclient.api.BillingResult; +import com.android.billingclient.api.Purchase; +import com.android.billingclient.api.SkuDetails; +import com.android.billingclient.api.SkuDetailsResponseListener; + +import net.osmand.AndroidUtils; +import net.osmand.plus.OsmandApplication; +import net.osmand.plus.OsmandPlugin; +import net.osmand.plus.R; +import net.osmand.plus.inapp.InAppPurchases.InAppPurchase; +import net.osmand.plus.inapp.InAppPurchases.InAppSubscription; +import net.osmand.plus.inapp.InAppPurchasesImpl.InAppPurchaseLiveUpdatesOldSubscription; +import net.osmand.plus.inapp.util.BillingManager; +import net.osmand.plus.settings.backend.OsmandSettings; +import net.osmand.plus.srtmplugin.SRTMPlugin; +import net.osmand.util.Algorithms; + +import java.lang.ref.WeakReference; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class InAppPurchaseHelperImpl extends InAppPurchaseHelper { + + // The helper object + private BillingManager billingManager; + private List skuDetailsList; + + /* base64EncodedPublicKey should be YOUR APPLICATION'S PUBLIC KEY + * (that you got from the Google Play developer console). This is not your + * developer public key, it's the *app-specific* public key. + * + * Instead of just storing the entire literal string here embedded in the + * program, construct the key at runtime from pieces or + * use bit manipulation (for example, XOR with some other string) to hide + * the actual key. The key itself is not secret information, but we don't + * want to make it easy for an attacker to replace the public key with one + * of their own and then fake messages from the server. + */ + private static final String BASE64_ENCODED_PUBLIC_KEY = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAgk8cEx" + + "UO4mfEwWFLkQnX1Tkzehr4SnXLXcm2Osxs5FTJPEgyTckTh0POKVMrxeGLn0KoTY2NTgp1U/inp" + + "wccWisPhVPEmw9bAVvWsOkzlyg1kv03fJdnAXRBSqDDPV6X8Z3MtkPVqZkupBsxyIllEILKHK06" + + "OCw49JLTsMR3oTRifGzma79I71X0spw0fM+cIRlkS2tsXN8GPbdkJwHofZKPOXS51pgC1zU8uWX" + + "I+ftJO46a1XkNh1dO2anUiQ8P/H4yOTqnMsXF7biyYuiwjXPOcy0OMhEHi54Dq6Mr3u5ZALOAkc" + + "YTjh1H/ZgqIHy5ZluahINuDE76qdLYMXrDMQIDAQAB"; + + public InAppPurchaseHelperImpl(OsmandApplication ctx) { + super(ctx); + purchases = new InAppPurchasesImpl(ctx); + } + + @Override + public void isInAppPurchaseSupported(@NonNull final Activity activity, @Nullable final InAppPurchaseInitCallback callback) { + if (callback != null) { + callback.onSuccess(); + } + } + + private BillingManager getBillingManager() { + return billingManager; + } + + protected void execImpl(@NonNull final InAppPurchaseTaskType taskType, @NonNull final InAppCommand runnable) { + billingManager = new BillingManager(ctx, BASE64_ENCODED_PUBLIC_KEY, new BillingManager.BillingUpdatesListener() { + + @Override + public void onBillingClientSetupFinished() { + logDebug("Setup finished."); + + BillingManager billingManager = getBillingManager(); + // Have we been disposed of in the meantime? If so, quit. + if (billingManager == null) { + stop(true); + return; + } + + if (!billingManager.isServiceConnected()) { + // Oh noes, there was a problem. + //complain("Problem setting up in-app billing: " + result); + notifyError(taskType, billingManager.getBillingClientResponseMessage()); + stop(true); + return; + } + + runnable.run(InAppPurchaseHelperImpl.this); + } + + @Override + public void onConsumeFinished(String token, BillingResult billingResult) { + } + + @Override + public void onPurchasesUpdated(final List purchases) { + + BillingManager billingManager = getBillingManager(); + // Have we been disposed of in the meantime? If so, quit. + if (billingManager == null) { + stop(true); + return; + } + + if (activeTask == InAppPurchaseTaskType.REQUEST_INVENTORY) { + List skuInApps = new ArrayList<>(); + for (InAppPurchase purchase : getInAppPurchases().getAllInAppPurchases(false)) { + skuInApps.add(purchase.getSku()); + } + for (Purchase p : purchases) { + skuInApps.add(p.getSku()); + } + billingManager.querySkuDetailsAsync(BillingClient.SkuType.INAPP, skuInApps, new SkuDetailsResponseListener() { + @Override + public void onSkuDetailsResponse(BillingResult billingResult, final List skuDetailsListInApps) { + // Is it a failure? + if (billingResult.getResponseCode() != BillingClient.BillingResponseCode.OK) { + logError("Failed to query inapps sku details: " + billingResult.getResponseCode()); + notifyError(InAppPurchaseTaskType.REQUEST_INVENTORY, billingResult.getDebugMessage()); + stop(true); + return; + } + + List skuSubscriptions = new ArrayList<>(); + for (InAppSubscription subscription : getInAppPurchases().getAllInAppSubscriptions()) { + skuSubscriptions.add(subscription.getSku()); + } + for (Purchase p : purchases) { + skuSubscriptions.add(p.getSku()); + } + + BillingManager billingManager = getBillingManager(); + // Have we been disposed of in the meantime? If so, quit. + if (billingManager == null) { + stop(true); + return; + } + + billingManager.querySkuDetailsAsync(BillingClient.SkuType.SUBS, skuSubscriptions, new SkuDetailsResponseListener() { + @Override + public void onSkuDetailsResponse(BillingResult billingResult, final List skuDetailsListSubscriptions) { + // Is it a failure? + if (billingResult.getResponseCode() != BillingClient.BillingResponseCode.OK) { + logError("Failed to query subscriptipons sku details: " + billingResult.getResponseCode()); + notifyError(InAppPurchaseTaskType.REQUEST_INVENTORY, billingResult.getDebugMessage()); + stop(true); + return; + } + + List skuDetailsList = new ArrayList<>(skuDetailsListInApps); + skuDetailsList.addAll(skuDetailsListSubscriptions); + InAppPurchaseHelperImpl.this.skuDetailsList = skuDetailsList; + + mSkuDetailsResponseListener.onSkuDetailsResponse(billingResult, skuDetailsList); + } + }); + } + }); + } + for (Purchase purchase : purchases) { + if (!purchase.isAcknowledged()) { + onPurchaseFinished(purchase); + } + } + } + + @Override + public void onPurchaseCanceled() { + stop(true); + } + }); + } + + @Override + public void purchaseFullVersion(@NonNull final Activity activity) { + notifyShowProgress(InAppPurchaseTaskType.PURCHASE_FULL_VERSION); + exec(InAppPurchaseTaskType.PURCHASE_FULL_VERSION, new InAppCommand() { + @Override + public void run(InAppPurchaseHelper helper) { + try { + SkuDetails skuDetails = getSkuDetails(getFullVersion().getSku()); + if (skuDetails == null) { + throw new IllegalArgumentException("Cannot find sku details"); + } + BillingManager billingManager = getBillingManager(); + if (billingManager != null) { + billingManager.initiatePurchaseFlow(activity, skuDetails); + } else { + throw new IllegalStateException("BillingManager disposed"); + } + commandDone(); + } catch (Exception e) { + complain("Cannot launch full version purchase!"); + logError("purchaseFullVersion Error", e); + stop(true); + } + } + }); + } + + @Override + public void purchaseDepthContours(@NonNull final Activity activity) { + notifyShowProgress(InAppPurchaseTaskType.PURCHASE_DEPTH_CONTOURS); + exec(InAppPurchaseTaskType.PURCHASE_DEPTH_CONTOURS, new InAppCommand() { + @Override + public void run(InAppPurchaseHelper helper) { + try { + SkuDetails skuDetails = getSkuDetails(getDepthContours().getSku()); + if (skuDetails == null) { + throw new IllegalArgumentException("Cannot find sku details"); + } + BillingManager billingManager = getBillingManager(); + if (billingManager != null) { + billingManager.initiatePurchaseFlow(activity, skuDetails); + } else { + throw new IllegalStateException("BillingManager disposed"); + } + commandDone(); + } catch (Exception e) { + complain("Cannot launch depth contours purchase!"); + logError("purchaseDepthContours Error", e); + stop(true); + } + } + }); + } + + @Override + public void purchaseContourLines(@NonNull Activity activity) throws UnsupportedOperationException { + OsmandPlugin plugin = OsmandPlugin.getPlugin(SRTMPlugin.class); + if(plugin == null || plugin.getInstallURL() == null) { + Toast.makeText(activity.getApplicationContext(), + activity.getString(R.string.activate_srtm_plugin), Toast.LENGTH_LONG).show(); + } else { + activity.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(plugin.getInstallURL()))); + } + } + + @Override + public void manageSubscription(@NonNull Context ctx, @Nullable String sku) { + String url = "https://play.google.com/store/account/subscriptions?package=" + ctx.getPackageName(); + if (!Algorithms.isEmpty(sku)) { + url += "&sku=" + sku; + } + Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); + ctx.startActivity(intent); + } + + @Nullable + private SkuDetails getSkuDetails(@NonNull String sku) { + List skuDetailsList = this.skuDetailsList; + if (skuDetailsList != null) { + for (SkuDetails details : skuDetailsList) { + if (details.getSku().equals(sku)) { + return details; + } + } + } + return null; + } + + private boolean hasDetails(@NonNull String sku) { + return getSkuDetails(sku) != null; + } + + @Nullable + private Purchase getPurchase(@NonNull String sku) { + BillingManager billingManager = getBillingManager(); + if (billingManager != null) { + List purchases = billingManager.getPurchases(); + if (purchases != null) { + for (Purchase p : purchases) { + if (p.getSku().equals(sku)) { + return p; + } + } + } + } + return null; + } + + // Listener that's called when we finish querying the items and subscriptions we own + private SkuDetailsResponseListener mSkuDetailsResponseListener = new SkuDetailsResponseListener() { + + @NonNull + private List getAllOwnedSubscriptionSkus() { + List result = new ArrayList<>(); + BillingManager billingManager = getBillingManager(); + if (billingManager != null) { + for (Purchase p : billingManager.getPurchases()) { + if (getInAppPurchases().getInAppSubscriptionBySku(p.getSku()) != null) { + result.add(p.getSku()); + } + } + } + return result; + } + + @Override + public void onSkuDetailsResponse(BillingResult billingResult, List skuDetailsList) { + + logDebug("Query sku details finished."); + + // Have we been disposed of in the meantime? If so, quit. + if (getBillingManager() == null) { + stop(true); + return; + } + + // Is it a failure? + if (billingResult.getResponseCode() != BillingClient.BillingResponseCode.OK) { + logError("Failed to query inventory: " + billingResult.getResponseCode()); + notifyError(InAppPurchaseTaskType.REQUEST_INVENTORY, billingResult.getDebugMessage()); + stop(true); + return; + } + + logDebug("Query sku details was successful."); + + /* + * Check for items we own. Notice that for each purchase, we check + * the developer payload to see if it's correct! See + * verifyDeveloperPayload(). + */ + + List allOwnedSubscriptionSkus = getAllOwnedSubscriptionSkus(); + for (InAppSubscription s : getLiveUpdates().getAllSubscriptions()) { + if (hasDetails(s.getSku())) { + Purchase purchase = getPurchase(s.getSku()); + SkuDetails liveUpdatesDetails = getSkuDetails(s.getSku()); + if (liveUpdatesDetails != null) { + fetchInAppPurchase(s, liveUpdatesDetails, purchase); + } + allOwnedSubscriptionSkus.remove(s.getSku()); + } + } + for (String sku : allOwnedSubscriptionSkus) { + Purchase purchase = getPurchase(sku); + SkuDetails liveUpdatesDetails = getSkuDetails(sku); + if (liveUpdatesDetails != null) { + InAppSubscription s = getLiveUpdates().upgradeSubscription(sku); + if (s == null) { + s = new InAppPurchaseLiveUpdatesOldSubscription(liveUpdatesDetails); + } + fetchInAppPurchase(s, liveUpdatesDetails, purchase); + } + } + + InAppPurchase fullVersion = getFullVersion(); + if (hasDetails(fullVersion.getSku())) { + Purchase purchase = getPurchase(fullVersion.getSku()); + SkuDetails fullPriceDetails = getSkuDetails(fullVersion.getSku()); + if (fullPriceDetails != null) { + fetchInAppPurchase(fullVersion, fullPriceDetails, purchase); + } + } + + InAppPurchase depthContours = getDepthContours(); + if (hasDetails(depthContours.getSku())) { + Purchase purchase = getPurchase(depthContours.getSku()); + SkuDetails depthContoursDetails = getSkuDetails(depthContours.getSku()); + if (depthContoursDetails != null) { + fetchInAppPurchase(depthContours, depthContoursDetails, purchase); + } + } + + InAppPurchase contourLines = getContourLines(); + if (hasDetails(contourLines.getSku())) { + Purchase purchase = getPurchase(contourLines.getSku()); + SkuDetails contourLinesDetails = getSkuDetails(contourLines.getSku()); + if (contourLinesDetails != null) { + fetchInAppPurchase(contourLines, contourLinesDetails, purchase); + } + } + + Purchase fullVersionPurchase = getPurchase(fullVersion.getSku()); + boolean fullVersionPurchased = fullVersionPurchase != null; + if (fullVersionPurchased) { + ctx.getSettings().FULL_VERSION_PURCHASED.set(true); + } + + Purchase depthContoursPurchase = getPurchase(depthContours.getSku()); + boolean depthContoursPurchased = depthContoursPurchase != null; + if (depthContoursPurchased) { + ctx.getSettings().DEPTH_CONTOURS_PURCHASED.set(true); + } + + // Do we have the live updates? + boolean subscribedToLiveUpdates = false; + List liveUpdatesPurchases = new ArrayList<>(); + for (InAppPurchase p : getLiveUpdates().getAllSubscriptions()) { + Purchase purchase = getPurchase(p.getSku()); + if (purchase != null) { + liveUpdatesPurchases.add(purchase); + if (!subscribedToLiveUpdates) { + subscribedToLiveUpdates = true; + } + } + } + OsmandSettings.OsmandPreference subscriptionCancelledTime = ctx.getSettings().LIVE_UPDATES_PURCHASE_CANCELLED_TIME; + if (!subscribedToLiveUpdates && ctx.getSettings().LIVE_UPDATES_PURCHASED.get()) { + if (subscriptionCancelledTime.get() == 0) { + subscriptionCancelledTime.set(System.currentTimeMillis()); + ctx.getSettings().LIVE_UPDATES_PURCHASE_CANCELLED_FIRST_DLG_SHOWN.set(false); + ctx.getSettings().LIVE_UPDATES_PURCHASE_CANCELLED_SECOND_DLG_SHOWN.set(false); + } else if (System.currentTimeMillis() - subscriptionCancelledTime.get() > SUBSCRIPTION_HOLDING_TIME_MSEC) { + ctx.getSettings().LIVE_UPDATES_PURCHASED.set(false); + if (!isDepthContoursPurchased(ctx)) { + ctx.getSettings().getCustomRenderBooleanProperty("depthContours").set(false); + } + } + } else if (subscribedToLiveUpdates) { + subscriptionCancelledTime.set(0L); + ctx.getSettings().LIVE_UPDATES_PURCHASED.set(true); + } + + lastValidationCheckTime = System.currentTimeMillis(); + logDebug("User " + (subscribedToLiveUpdates ? "HAS" : "DOES NOT HAVE") + + " live updates purchased."); + + OsmandSettings settings = ctx.getSettings(); + settings.INAPPS_READ.set(true); + + List tokensToSend = new ArrayList<>(); + if (liveUpdatesPurchases.size() > 0) { + List tokensSent = Arrays.asList(settings.BILLING_PURCHASE_TOKENS_SENT.get().split(";")); + for (Purchase purchase : liveUpdatesPurchases) { + if ((Algorithms.isEmpty(settings.BILLING_USER_ID.get()) || Algorithms.isEmpty(settings.BILLING_USER_TOKEN.get())) + && !Algorithms.isEmpty(purchase.getDeveloperPayload())) { + String payload = purchase.getDeveloperPayload(); + if (!Algorithms.isEmpty(payload)) { + String[] arr = payload.split(" "); + if (arr.length > 0) { + settings.BILLING_USER_ID.set(arr[0]); + } + if (arr.length > 1) { + token = arr[1]; + settings.BILLING_USER_TOKEN.set(token); + } + } + } + if (!tokensSent.contains(purchase.getSku())) { + tokensToSend.add(purchase); + } + } + } + List purchaseInfoList = new ArrayList<>(); + for (Purchase purchase : tokensToSend) { + purchaseInfoList.add(getPurchaseInfo(purchase)); + } + onSkuDetailsResponseDone(purchaseInfoList); + } + }; + + private PurchaseInfo getPurchaseInfo(Purchase purchase) { + return new PurchaseInfo(purchase.getSku(), purchase.getOrderId(), purchase.getPurchaseToken()); + } + + private void fetchInAppPurchase(@NonNull InAppPurchase inAppPurchase, @NonNull SkuDetails skuDetails, @Nullable Purchase purchase) { + if (purchase != null) { + inAppPurchase.setPurchaseState(InAppPurchase.PurchaseState.PURCHASED); + inAppPurchase.setPurchaseTime(purchase.getPurchaseTime()); + } else { + inAppPurchase.setPurchaseState(InAppPurchase.PurchaseState.NOT_PURCHASED); + } + inAppPurchase.setPrice(skuDetails.getPrice()); + inAppPurchase.setPriceCurrencyCode(skuDetails.getPriceCurrencyCode()); + if (skuDetails.getPriceAmountMicros() > 0) { + inAppPurchase.setPriceValue(skuDetails.getPriceAmountMicros() / 1000000d); + } + String subscriptionPeriod = skuDetails.getSubscriptionPeriod(); + if (!Algorithms.isEmpty(subscriptionPeriod)) { + if (inAppPurchase instanceof InAppSubscription) { + try { + ((InAppSubscription) inAppPurchase).setSubscriptionPeriodString(subscriptionPeriod); + } catch (ParseException e) { + LOG.error(e); + } + } + } + if (inAppPurchase instanceof InAppSubscription) { + String introductoryPrice = skuDetails.getIntroductoryPrice(); + String introductoryPricePeriod = skuDetails.getIntroductoryPricePeriod(); + String introductoryPriceCycles = skuDetails.getIntroductoryPriceCycles(); + long introductoryPriceAmountMicros = skuDetails.getIntroductoryPriceAmountMicros(); + if (!Algorithms.isEmpty(introductoryPrice)) { + InAppSubscription s = (InAppSubscription) inAppPurchase; + try { + s.setIntroductoryInfo(new InAppPurchases.InAppSubscriptionIntroductoryInfo(s, introductoryPrice, + introductoryPriceAmountMicros, introductoryPricePeriod, introductoryPriceCycles)); + } catch (ParseException e) { + LOG.error(e); + } + } + } + } + + protected InAppCommand getPurchaseLiveUpdatesCommand(final WeakReference activity, final String sku, final String payload) { + return new InAppCommand() { + @Override + public void run(InAppPurchaseHelper helper) { + try { + Activity a = activity.get(); + SkuDetails skuDetails = getSkuDetails(sku); + if (AndroidUtils.isActivityNotDestroyed(a) && skuDetails != null) { + BillingManager billingManager = getBillingManager(); + if (billingManager != null) { + billingManager.setPayload(payload); + billingManager.initiatePurchaseFlow(a, skuDetails); + } else { + throw new IllegalStateException("BillingManager disposed"); + } + commandDone(); + } else { + stop(true); + } + } catch (Exception e) { + logError("launchPurchaseFlow Error", e); + stop(true); + } + } + }; + } + + protected InAppCommand getRequestInventoryCommand() { + return new InAppCommand() { + @Override + public void run(InAppPurchaseHelper helper) { + logDebug("Setup successful. Querying inventory."); + try { + BillingManager billingManager = getBillingManager(); + if (billingManager != null) { + billingManager.queryPurchases(); + } else { + throw new IllegalStateException("BillingManager disposed"); + } + commandDone(); + } catch (Exception e) { + logError("queryInventoryAsync Error", e); + notifyDismissProgress(InAppPurchaseTaskType.REQUEST_INVENTORY); + stop(true); + } + } + }; + } + + // Call when a purchase is finished + private void onPurchaseFinished(Purchase purchase) { + logDebug("Purchase finished: " + purchase); + + // if we were disposed of in the meantime, quit. + if (getBillingManager() == null) { + stop(true); + return; + } + + onPurchaseDone(getPurchaseInfo(purchase)); + } + + @Override + protected boolean isBillingManagerExists() { + return getBillingManager() != null; + } + + @Override + protected void destroyBillingManager() { + BillingManager billingManager = getBillingManager(); + if (billingManager != null) { + billingManager.destroy(); + this.billingManager = null; + } + } +} diff --git a/OsmAnd/src-google/net/osmand/plus/inapp/InAppPurchasesImpl.java b/OsmAnd/src-google/net/osmand/plus/inapp/InAppPurchasesImpl.java new file mode 100644 index 0000000000..a2a0f8d680 --- /dev/null +++ b/OsmAnd/src-google/net/osmand/plus/inapp/InAppPurchasesImpl.java @@ -0,0 +1,323 @@ +package net.osmand.plus.inapp; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.billingclient.api.SkuDetails; + +import net.osmand.plus.OsmandApplication; +import net.osmand.plus.R; +import net.osmand.plus.Version; + +public class InAppPurchasesImpl extends InAppPurchases { + + private static final InAppPurchase FULL_VERSION = new InAppPurchaseFullVersion(); + private static final InAppPurchaseDepthContoursFull DEPTH_CONTOURS_FULL = new InAppPurchaseDepthContoursFull(); + private static final InAppPurchaseDepthContoursFree DEPTH_CONTOURS_FREE = new InAppPurchaseDepthContoursFree(); + private static final InAppPurchaseContourLinesFull CONTOUR_LINES_FULL = new InAppPurchaseContourLinesFull(); + private static final InAppPurchaseContourLinesFree CONTOUR_LINES_FREE = new InAppPurchaseContourLinesFree(); + + private static final InAppSubscription[] LIVE_UPDATES_FULL = new InAppSubscription[]{ + new InAppPurchaseLiveUpdatesOldMonthlyFull(), + new InAppPurchaseLiveUpdatesMonthlyFull(), + new InAppPurchaseLiveUpdates3MonthsFull(), + new InAppPurchaseLiveUpdatesAnnualFull() + }; + + private static final InAppSubscription[] LIVE_UPDATES_FREE = new InAppSubscription[]{ + new InAppPurchaseLiveUpdatesOldMonthlyFree(), + new InAppPurchaseLiveUpdatesMonthlyFree(), + new InAppPurchaseLiveUpdates3MonthsFree(), + new InAppPurchaseLiveUpdatesAnnualFree() + }; + + public InAppPurchasesImpl(OsmandApplication ctx) { + super(ctx); + fullVersion = FULL_VERSION; + if (Version.isFreeVersion(ctx)) { + liveUpdates = new LiveUpdatesInAppPurchasesFree(); + } else { + liveUpdates = new LiveUpdatesInAppPurchasesFull(); + } + for (InAppSubscription s : liveUpdates.getAllSubscriptions()) { + if (s instanceof InAppPurchaseLiveUpdatesMonthly) { + if (s.isDiscounted()) { + discountedMonthlyLiveUpdates = s; + } else { + monthlyLiveUpdates = s; + } + } + } + if (Version.isFreeVersion(ctx)) { + depthContours = DEPTH_CONTOURS_FREE; + } else { + depthContours = DEPTH_CONTOURS_FULL; + } + if (Version.isFreeVersion(ctx)) { + contourLines = CONTOUR_LINES_FREE; + } else { + contourLines = CONTOUR_LINES_FULL; + } + + inAppPurchases = new InAppPurchase[] { fullVersion, depthContours, contourLines }; + } + + @Override + public boolean isFullVersion(String sku) { + return FULL_VERSION.getSku().equals(sku); + } + + @Override + public boolean isDepthContours(String sku) { + return DEPTH_CONTOURS_FULL.getSku().equals(sku) || DEPTH_CONTOURS_FREE.getSku().equals(sku); + } + + @Override + public boolean isContourLines(String sku) { + return CONTOUR_LINES_FULL.getSku().equals(sku) || CONTOUR_LINES_FREE.getSku().equals(sku); + } + + @Override + public boolean isLiveUpdates(String sku) { + for (InAppPurchase p : LIVE_UPDATES_FULL) { + if (p.getSku().equals(sku)) { + return true; + } + } + for (InAppPurchase p : LIVE_UPDATES_FREE) { + if (p.getSku().equals(sku)) { + return true; + } + } + return false; + } + + private static class InAppPurchaseFullVersion extends InAppPurchase { + + private static final String SKU_FULL_VERSION_PRICE = "osmand_full_version_price"; + + InAppPurchaseFullVersion() { + super(SKU_FULL_VERSION_PRICE); + } + + @Override + public String getDefaultPrice(Context ctx) { + return ctx.getString(R.string.full_version_price); + } + } + + private static class InAppPurchaseDepthContoursFull extends InAppPurchaseDepthContours { + + private static final String SKU_DEPTH_CONTOURS_FULL = "net.osmand.seadepth_plus"; + + InAppPurchaseDepthContoursFull() { + super(SKU_DEPTH_CONTOURS_FULL); + } + } + + private static class InAppPurchaseDepthContoursFree extends InAppPurchaseDepthContours { + + private static final String SKU_DEPTH_CONTOURS_FREE = "net.osmand.seadepth"; + + InAppPurchaseDepthContoursFree() { + super(SKU_DEPTH_CONTOURS_FREE); + } + } + + private static class InAppPurchaseContourLinesFull extends InAppPurchaseContourLines { + + private static final String SKU_CONTOUR_LINES_FULL = "net.osmand.contourlines_plus"; + + InAppPurchaseContourLinesFull() { + super(SKU_CONTOUR_LINES_FULL); + } + } + + private static class InAppPurchaseContourLinesFree extends InAppPurchaseContourLines { + + private static final String SKU_CONTOUR_LINES_FREE = "net.osmand.contourlines"; + + InAppPurchaseContourLinesFree() { + super(SKU_CONTOUR_LINES_FREE); + } + } + + private static class InAppPurchaseLiveUpdatesMonthlyFull extends InAppPurchaseLiveUpdatesMonthly { + + private static final String SKU_LIVE_UPDATES_MONTHLY_FULL = "osm_live_subscription_monthly_full"; + + InAppPurchaseLiveUpdatesMonthlyFull() { + super(SKU_LIVE_UPDATES_MONTHLY_FULL, 1); + } + + private InAppPurchaseLiveUpdatesMonthlyFull(@NonNull String sku) { + super(sku); + } + + @Nullable + @Override + protected InAppSubscription newInstance(@NonNull String sku) { + return sku.startsWith(getSkuNoVersion()) ? new InAppPurchaseLiveUpdatesMonthlyFull(sku) : null; + } + } + + private static class InAppPurchaseLiveUpdatesMonthlyFree extends InAppPurchaseLiveUpdatesMonthly { + + private static final String SKU_LIVE_UPDATES_MONTHLY_FREE = "osm_live_subscription_monthly_free"; + + InAppPurchaseLiveUpdatesMonthlyFree() { + super(SKU_LIVE_UPDATES_MONTHLY_FREE, 1); + } + + private InAppPurchaseLiveUpdatesMonthlyFree(@NonNull String sku) { + super(sku); + } + + @Nullable + @Override + protected InAppSubscription newInstance(@NonNull String sku) { + return sku.startsWith(getSkuNoVersion()) ? new InAppPurchaseLiveUpdatesMonthlyFree(sku) : null; + } + } + + private static class InAppPurchaseLiveUpdates3MonthsFull extends InAppPurchaseLiveUpdates3Months { + + private static final String SKU_LIVE_UPDATES_3_MONTHS_FULL = "osm_live_subscription_3_months_full"; + + InAppPurchaseLiveUpdates3MonthsFull() { + super(SKU_LIVE_UPDATES_3_MONTHS_FULL, 1); + } + + private InAppPurchaseLiveUpdates3MonthsFull(@NonNull String sku) { + super(sku); + } + + @Nullable + @Override + protected InAppSubscription newInstance(@NonNull String sku) { + return sku.startsWith(getSkuNoVersion()) ? new InAppPurchaseLiveUpdates3MonthsFull(sku) : null; + } + } + + private static class InAppPurchaseLiveUpdates3MonthsFree extends InAppPurchaseLiveUpdates3Months { + + private static final String SKU_LIVE_UPDATES_3_MONTHS_FREE = "osm_live_subscription_3_months_free"; + + InAppPurchaseLiveUpdates3MonthsFree() { + super(SKU_LIVE_UPDATES_3_MONTHS_FREE, 1); + } + + private InAppPurchaseLiveUpdates3MonthsFree(@NonNull String sku) { + super(sku); + } + + @Nullable + @Override + protected InAppSubscription newInstance(@NonNull String sku) { + return sku.startsWith(getSkuNoVersion()) ? new InAppPurchaseLiveUpdates3MonthsFree(sku) : null; + } + } + + private static class InAppPurchaseLiveUpdatesAnnualFull extends InAppPurchaseLiveUpdatesAnnual { + + private static final String SKU_LIVE_UPDATES_ANNUAL_FULL = "osm_live_subscription_annual_full"; + + InAppPurchaseLiveUpdatesAnnualFull() { + super(SKU_LIVE_UPDATES_ANNUAL_FULL, 1); + } + + private InAppPurchaseLiveUpdatesAnnualFull(@NonNull String sku) { + super(sku); + } + + @Nullable + @Override + protected InAppSubscription newInstance(@NonNull String sku) { + return sku.startsWith(getSkuNoVersion()) ? new InAppPurchaseLiveUpdatesAnnualFull(sku) : null; + } + } + + private static class InAppPurchaseLiveUpdatesAnnualFree extends InAppPurchaseLiveUpdatesAnnual { + + private static final String SKU_LIVE_UPDATES_ANNUAL_FREE = "osm_live_subscription_annual_free"; + + InAppPurchaseLiveUpdatesAnnualFree() { + super(SKU_LIVE_UPDATES_ANNUAL_FREE, 1); + } + + private InAppPurchaseLiveUpdatesAnnualFree(@NonNull String sku) { + super(sku); + } + + @Nullable + @Override + protected InAppSubscription newInstance(@NonNull String sku) { + return sku.startsWith(getSkuNoVersion()) ? new InAppPurchaseLiveUpdatesAnnualFree(sku) : null; + } + } + + private static class InAppPurchaseLiveUpdatesOldMonthlyFull extends InAppPurchaseLiveUpdatesOldMonthly { + + private static final String SKU_LIVE_UPDATES_OLD_MONTHLY_FULL = "osm_live_subscription_2"; + + InAppPurchaseLiveUpdatesOldMonthlyFull() { + super(SKU_LIVE_UPDATES_OLD_MONTHLY_FULL); + } + } + + private static class InAppPurchaseLiveUpdatesOldMonthlyFree extends InAppPurchaseLiveUpdatesOldMonthly { + + private static final String SKU_LIVE_UPDATES_OLD_MONTHLY_FREE = "osm_free_live_subscription_2"; + + InAppPurchaseLiveUpdatesOldMonthlyFree() { + super(SKU_LIVE_UPDATES_OLD_MONTHLY_FREE); + } + } + + public static class InAppPurchaseLiveUpdatesOldSubscription extends InAppSubscription { + + private SkuDetails details; + + InAppPurchaseLiveUpdatesOldSubscription(@NonNull SkuDetails details) { + super(details.getSku(), true); + this.details = details; + } + + @Override + public String getDefaultPrice(Context ctx) { + return ""; + } + + @Override + public CharSequence getTitle(Context ctx) { + return details.getTitle(); + } + + @Override + public CharSequence getDescription(@NonNull Context ctx) { + return details.getDescription(); + } + + @Nullable + @Override + protected InAppSubscription newInstance(@NonNull String sku) { + return null; + } + } + + private static class LiveUpdatesInAppPurchasesFree extends InAppSubscriptionList { + + public LiveUpdatesInAppPurchasesFree() { + super(LIVE_UPDATES_FREE); + } + } + + private static class LiveUpdatesInAppPurchasesFull extends InAppSubscriptionList { + + public LiveUpdatesInAppPurchasesFull() { + super(LIVE_UPDATES_FULL); + } + } +} diff --git a/OsmAnd/src-huawei/net/osmand/plus/inapp/CipherUtil.java b/OsmAnd/src-huawei/net/osmand/plus/inapp/CipherUtil.java new file mode 100755 index 0000000000..c7e6cd11be --- /dev/null +++ b/OsmAnd/src-huawei/net/osmand/plus/inapp/CipherUtil.java @@ -0,0 +1,96 @@ +/** + * Copyright 2020. Huawei Technologies Co., Ltd. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.osmand.plus.inapp; + +import android.text.TextUtils; +import android.util.Base64; +import android.util.Log; + +import java.io.UnsupportedEncodingException; +import java.security.InvalidKeyException; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.SignatureException; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.X509EncodedKeySpec; + +/** + * Signature related tools. + * + * @since 2019/12/9 + */ +public class CipherUtil { + private static final String TAG = "CipherUtil"; + private static final String SIGN_ALGORITHMS = "SHA256WithRSA"; + private static final String PUBLIC_KEY = "MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAsB+oH8rYQncwpTqGa0kS/5E725HJrq2sW1ThAZtmorYVi52Yt9PmZvNDz7284ol9C2skrKQR34eIer8Tr7Qqq3mlNo+/LVUpq9sa++kB2glaG6jj5NNjM3w4nVYHFIYkd5AQhodJgmqFvnp2s7r7YmyQVXZSehei5bA1G70Bs+El9cSv9shNNGTCaU3ARUu2hy3Ltkc/ov7/ZYYpiwjbyD3cmoMh9jO1++zztXb2phjv1h9zeJOp1i6HsotZll+c9J4jjV3GhrF+ZJm5WrSzGLDLtwSldRpMZFxrSvAJJstjzhDz3LpUM+nPV3HZ5VQ/xosmwWYmiibo89E1gw8p73NTBXHzuQMJcTJ6vTjD8LeMskpXHZUAGhifmFLGN1LbNP9662ulCV12kIbXuzWCwwi/h0DWqmnjKmLvzc88e4BrGrp2zZUnHz7m15voPG+4cQ3z9+cwS4gEI3SUTiFyQGE539SO/11VkkQAJ8P7du1JFNqQw5ZEW3AoE1iUsp5XAgMBAAE="; + + /** + * the method to check the signature for the data returned from the interface + * @param content Unsigned data + * @param sign the signature for content + * @param publicKey the public of the application + * @return boolean + */ + public static boolean doCheck(String content, String sign, String publicKey) { + if (TextUtils.isEmpty(publicKey)) { + Log.e(TAG, "publicKey is null"); + return false; + } + + if (TextUtils.isEmpty(content) || TextUtils.isEmpty(sign)) { + Log.e(TAG, "data is error"); + return false; + } + + try { + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + byte[] encodedKey = Base64.decode(publicKey, Base64.DEFAULT); + PublicKey pubKey = keyFactory.generatePublic(new X509EncodedKeySpec(encodedKey)); + + java.security.Signature signature = java.security.Signature.getInstance(SIGN_ALGORITHMS); + + signature.initVerify(pubKey); + signature.update(content.getBytes("UTF-8")); + + boolean bverify = signature.verify(Base64.decode(sign, Base64.DEFAULT)); + return bverify; + + } catch (NoSuchAlgorithmException e) { + Log.e(TAG, "doCheck NoSuchAlgorithmException" + e); + } catch (InvalidKeySpecException e) { + Log.e(TAG, "doCheck InvalidKeySpecException" + e); + } catch (InvalidKeyException e) { + Log.e(TAG, "doCheck InvalidKeyException" + e); + } catch (SignatureException e) { + Log.e(TAG, "doCheck SignatureException" + e); + } catch (UnsupportedEncodingException e) { + Log.e(TAG, "doCheck UnsupportedEncodingException" + e); + } + return false; + } + + /** + * get the publicKey of the application + * During the encoding process, avoid storing the public key in clear text. + * @return publickey + */ + public static String getPublicKey(){ + return PUBLIC_KEY; + } + +} diff --git a/OsmAnd/src-huawei/net/osmand/plus/inapp/Constants.java b/OsmAnd/src-huawei/net/osmand/plus/inapp/Constants.java new file mode 100755 index 0000000000..fba4db210a --- /dev/null +++ b/OsmAnd/src-huawei/net/osmand/plus/inapp/Constants.java @@ -0,0 +1,33 @@ +/** + * Copyright 2020. Huawei Technologies Co., Ltd. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.osmand.plus.inapp; + +/** + * Constants Class. + * + * @since 2019/12/9 + */ +public class Constants { + + /** requestCode for pull up the pmsPay page */ + public static final int REQ_CODE_BUY_SUB = 4002; + public static final int REQ_CODE_BUY_INAPP = 4003; + + /** requestCode for pull up the login page for isEnvReady interface */ + public static final int REQ_CODE_LOGIN = 2001; + +} diff --git a/OsmAnd/src-huawei/net/osmand/plus/inapp/ExceptionHandle.java b/OsmAnd/src-huawei/net/osmand/plus/inapp/ExceptionHandle.java new file mode 100755 index 0000000000..585c6b8404 --- /dev/null +++ b/OsmAnd/src-huawei/net/osmand/plus/inapp/ExceptionHandle.java @@ -0,0 +1,103 @@ +/** + * Copyright 2020. Huawei Technologies Co., Ltd. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.osmand.plus.inapp; + +import android.app.Activity; +import android.widget.Toast; + +import androidx.annotation.Nullable; + +import com.huawei.hms.iap.IapApiException; +import com.huawei.hms.iap.entity.OrderStatusCode; + +import net.osmand.AndroidUtils; +import net.osmand.PlatformUtil; + +/** + * Handles the exception returned from the iap api. + * + * @since 2019/12/9 + */ +public class ExceptionHandle { + + protected static final org.apache.commons.logging.Log LOG = PlatformUtil.getLog(ExceptionHandle.class); + + /** + * The exception is solved. + */ + public static final int SOLVED = 0; + + /** + * Handles the exception returned from the iap api. + * @param activity The Activity to call the iap api. + * @param e The exception returned from the iap api. + * @return int + */ + public static int handle(@Nullable Activity activity, Exception e) { + + if (e instanceof IapApiException) { + IapApiException iapApiException = (IapApiException) e; + LOG.info("returnCode: " + iapApiException.getStatusCode()); + switch (iapApiException.getStatusCode()) { + case OrderStatusCode.ORDER_STATE_CANCEL: + showToast(activity, "Order has been canceled!"); + return SOLVED; + case OrderStatusCode.ORDER_STATE_PARAM_ERROR: + showToast(activity, "Order state param error!"); + return SOLVED; + case OrderStatusCode.ORDER_STATE_NET_ERROR: + showToast(activity, "Order state net error!"); + return SOLVED; + case OrderStatusCode.ORDER_VR_UNINSTALL_ERROR: + showToast(activity, "Order vr uninstall error!"); + return SOLVED; + case OrderStatusCode.ORDER_HWID_NOT_LOGIN: + IapRequestHelper.startResolutionForResult(activity, iapApiException.getStatus(), Constants.REQ_CODE_LOGIN); + return SOLVED; + case OrderStatusCode.ORDER_PRODUCT_OWNED: + showToast(activity, "Product already owned error!"); + return OrderStatusCode.ORDER_PRODUCT_OWNED; + case OrderStatusCode.ORDER_PRODUCT_NOT_OWNED: + showToast(activity, "Product not owned error!"); + return SOLVED; + case OrderStatusCode.ORDER_PRODUCT_CONSUMED: + showToast(activity, "Product consumed error!"); + return SOLVED; + case OrderStatusCode.ORDER_ACCOUNT_AREA_NOT_SUPPORTED: + showToast(activity, "Order account area not supported error!"); + return SOLVED; + case OrderStatusCode.ORDER_NOT_ACCEPT_AGREEMENT: + showToast(activity, "User does not agree the agreement"); + return SOLVED; + default: + // handle other error scenarios + showToast(activity, "Order unknown error (" + iapApiException.getStatusCode() + ")"); + return SOLVED; + } + } else { + showToast(activity, "External error"); + LOG.error(e.getMessage(), e); + return SOLVED; + } + } + + private static void showToast(@Nullable Activity activity, String s) { + if (AndroidUtils.isActivityNotDestroyed(activity)) { + Toast.makeText(activity, s, Toast.LENGTH_SHORT).show(); + } + } +} \ No newline at end of file diff --git a/OsmAnd/src-huawei/net/osmand/plus/inapp/IapApiCallback.java b/OsmAnd/src-huawei/net/osmand/plus/inapp/IapApiCallback.java new file mode 100755 index 0000000000..d8fb908093 --- /dev/null +++ b/OsmAnd/src-huawei/net/osmand/plus/inapp/IapApiCallback.java @@ -0,0 +1,37 @@ +/** + * Copyright 2020. Huawei Technologies Co., Ltd. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.osmand.plus.inapp; + +/** + * Used to callback the result from iap api. + * + * @since 2019/12/9 + */ +public interface IapApiCallback { + + /** + * The request is successful. + * @param result The result of a successful response. + */ + void onSuccess(T result); + + /** + * Callback fail. + * @param e An Exception from IAPSDK. + */ + void onFail(Exception e); +} diff --git a/OsmAnd/src-huawei/net/osmand/plus/inapp/IapRequestHelper.java b/OsmAnd/src-huawei/net/osmand/plus/inapp/IapRequestHelper.java new file mode 100755 index 0000000000..5c1b7838a5 --- /dev/null +++ b/OsmAnd/src-huawei/net/osmand/plus/inapp/IapRequestHelper.java @@ -0,0 +1,351 @@ +/** + * Copyright 2020. Huawei Technologies Co., Ltd. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.osmand.plus.inapp; + +import android.app.Activity; +import android.content.IntentSender; +import android.text.TextUtils; +import android.util.Log; + +import com.huawei.hmf.tasks.OnFailureListener; +import com.huawei.hmf.tasks.OnSuccessListener; +import com.huawei.hmf.tasks.Task; +import com.huawei.hms.iap.Iap; +import com.huawei.hms.iap.IapApiException; +import com.huawei.hms.iap.IapClient; +import com.huawei.hms.iap.entity.ConsumeOwnedPurchaseReq; +import com.huawei.hms.iap.entity.ConsumeOwnedPurchaseResult; +import com.huawei.hms.iap.entity.IsEnvReadyResult; +import com.huawei.hms.iap.entity.OwnedPurchasesReq; +import com.huawei.hms.iap.entity.OwnedPurchasesResult; +import com.huawei.hms.iap.entity.ProductInfoReq; +import com.huawei.hms.iap.entity.ProductInfoResult; +import com.huawei.hms.iap.entity.PurchaseIntentReq; +import com.huawei.hms.iap.entity.PurchaseIntentResult; +import com.huawei.hms.iap.entity.StartIapActivityReq; +import com.huawei.hms.iap.entity.StartIapActivityResult; +import com.huawei.hms.support.api.client.Status; + +import java.util.List; + +/** + * The tool class of Iap interface. + * + * @since 2019/12/9 + */ +public class IapRequestHelper { + private final static String TAG = "IapRequestHelper"; + + /** + * Create a PurchaseIntentReq object. + * @param type In-app product type. + * The value contains: 0: consumable 1: non-consumable 2 auto-renewable subscription + * @param productId ID of the in-app product to be paid. + * The in-app product ID is the product ID you set during in-app product configuration in AppGallery Connect. + * @return PurchaseIntentReq + */ + private static PurchaseIntentReq createPurchaseIntentReq(int type, String productId) { + PurchaseIntentReq req = new PurchaseIntentReq(); + req.setPriceType(type); + req.setProductId(productId); + req.setDeveloperPayload("testPurchase"); + return req; + } + + /** + * Create a ConsumeOwnedPurchaseReq object. + * @param purchaseToken which is generated by the Huawei payment server during product payment and returned to the app through InAppPurchaseData. + * The app transfers this parameter for the Huawei payment server to update the order status and then deliver the in-app product. + * @return ConsumeOwnedPurchaseReq + */ + private static ConsumeOwnedPurchaseReq createConsumeOwnedPurchaseReq(String purchaseToken) { + ConsumeOwnedPurchaseReq req = new ConsumeOwnedPurchaseReq(); + req.setPurchaseToken(purchaseToken); + req.setDeveloperChallenge("testConsume"); + return req; + } + + /** + * Create a OwnedPurchasesReq object. + * @param type type In-app product type. + * The value contains: 0: consumable 1: non-consumable 2 auto-renewable subscription + * @param continuationToken A data location flag which returns from obtainOwnedPurchases api or obtainOwnedPurchaseRecord api. + * @return OwnedPurchasesReq + */ + private static OwnedPurchasesReq createOwnedPurchasesReq(int type, String continuationToken) { + OwnedPurchasesReq req = new OwnedPurchasesReq(); + req.setPriceType(type); + req.setContinuationToken(continuationToken); + return req; + } + + /** + * Create a ProductInfoReq object. + * @param type In-app product type. + * The value contains: 0: consumable 1: non-consumable 2 auto-renewable subscription + * @param productIds ID list of products to be queried. Each product ID must exist and be unique in the current app. + * @return ProductInfoReq + */ + private static ProductInfoReq createProductInfoReq(int type, List productIds) { + ProductInfoReq req = new ProductInfoReq(); + req.setPriceType(type); + req.setProductIds(productIds); + return req; + } + + /** + * To check whether the country or region of the logged in HUAWEI ID is included in the countries or regions supported by HUAWEI IAP. + * @param mClient IapClient instance to call the isEnvReady API. + * @param callback IapApiCallback. + */ + public static void isEnvReady(IapClient mClient, final IapApiCallback callback) { + Log.i(TAG, "call isEnvReady"); + Task task = mClient.isEnvReady(); + task.addOnSuccessListener(new OnSuccessListener() { + @Override + public void onSuccess(IsEnvReadyResult result) { + Log.i(TAG, "isEnvReady, success"); + callback.onSuccess(result); + } + }).addOnFailureListener(new OnFailureListener() { + @Override + public void onFailure(Exception e) { + Log.e(TAG, "isEnvReady, fail"); + callback.onFail(e); + } + }); + } + + /** + * Obtain in-app product details configured in AppGallery Connect. + * @param iapClient IapClient instance to call the obtainProductInfo API. + * @param productIds ID list of products to be queried. Each product ID must exist and be unique in the current app. + * @param type In-app product type. + * The value contains: 0: consumable 1: non-consumable 2 auto-renewable subscription + * @param callback IapApiCallback + */ + public static void obtainProductInfo(IapClient iapClient, final List productIds, int type, final IapApiCallback callback) { + Log.i(TAG, "call obtainProductInfo"); + + Task task = iapClient.obtainProductInfo(createProductInfoReq(type, productIds)); + task.addOnSuccessListener(new OnSuccessListener() { + @Override + public void onSuccess(ProductInfoResult result) { + Log.i(TAG, "obtainProductInfo, success"); + callback.onSuccess(result); + } + }).addOnFailureListener(new OnFailureListener() { + @Override + public void onFailure(Exception e) { + Log.e(TAG, "obtainProductInfo, fail"); + callback.onFail(e); + } + }); + } + + /** + * create orders for in-app products in the PMS + * @param iapClient IapClient instance to call the createPurchaseIntent API. + * @param productId ID of the in-app product to be paid. + * The in-app product ID is the product ID you set during in-app product configuration in AppGallery Connect. + * @param type In-app product type. + * The value contains: 0: consumable 1: non-consumable 2 auto-renewable subscription + * @param callback IapApiCallback + */ + public static void createPurchaseIntent(final IapClient iapClient, String productId, int type, final IapApiCallback callback) { + Log.i(TAG, "call createPurchaseIntent"); + Task task = iapClient.createPurchaseIntent(createPurchaseIntentReq(type, productId)); + task.addOnSuccessListener(new OnSuccessListener() { + @Override + public void onSuccess(PurchaseIntentResult result) { + Log.i(TAG, "createPurchaseIntent, success"); + callback.onSuccess(result); + } + }).addOnFailureListener(new OnFailureListener() { + @Override + public void onFailure(Exception e) { + Log.e(TAG, "createPurchaseIntent, fail"); + callback.onFail(e); + + } + }); + } + + public static void createPurchaseIntent(final IapClient iapClient, String productId, int type, String payload, final IapApiCallback callback) { + Log.i(TAG, "call createPurchaseIntent"); + PurchaseIntentReq req = createPurchaseIntentReq(type, productId); + req.setDeveloperPayload(payload); + Task task = iapClient.createPurchaseIntent(req); + task.addOnSuccessListener(new OnSuccessListener() { + @Override + public void onSuccess(PurchaseIntentResult result) { + Log.i(TAG, "createPurchaseIntent, success"); + callback.onSuccess(result); + } + }).addOnFailureListener(new OnFailureListener() { + @Override + public void onFailure(Exception e) { + Log.e(TAG, "createPurchaseIntent, fail"); + callback.onFail(e); + + } + }); + } + + /** + * to start an activity. + * @param activity the activity to launch a new page. + * @param status This parameter contains the pendingIntent object of the payment page. + * @param reqCode Result code. + */ + public static void startResolutionForResult(Activity activity, Status status, int reqCode) { + if (status == null) { + Log.e(TAG, "status is null"); + return; + } + if (status.hasResolution()) { + try { + status.startResolutionForResult(activity, reqCode); + } catch (IntentSender.SendIntentException exp) { + Log.e(TAG, exp.getMessage()); + } + } else { + Log.e(TAG, "intent is null"); + } + } + + /** + * query information about all subscribed in-app products, including consumables, non-consumables, and auto-renewable subscriptions.
+ * If consumables are returned, the system needs to deliver them and calls the consumeOwnedPurchase API to consume the products. + * If non-consumables are returned, the in-app products do not need to be consumed. + * If subscriptions are returned, all existing subscription relationships of the user under the app are returned. + * @param mClient IapClient instance to call the obtainOwnedPurchases API. + * @param type In-app product type. + * The value contains: 0: consumable 1: non-consumable 2 auto-renewable subscription + * @param callback IapApiCallback + */ + public static void obtainOwnedPurchases(IapClient mClient, final int type, String continuationToken, final IapApiCallback callback) { + Log.i(TAG, "call obtainOwnedPurchases"); + Task task = mClient.obtainOwnedPurchases(IapRequestHelper.createOwnedPurchasesReq(type, continuationToken)); + task.addOnSuccessListener(new OnSuccessListener() { + @Override + public void onSuccess(OwnedPurchasesResult result) { + Log.i(TAG, "obtainOwnedPurchases, success"); + callback.onSuccess(result); + + } + }).addOnFailureListener(new OnFailureListener() { + @Override + public void onFailure(Exception e) { + Log.e(TAG, "obtainOwnedPurchases, fail"); + callback.onFail(e); + } + }); + + } + + /** + * obtain the historical consumption information about a consumable in-app product or all subscription receipts of a subscription. + * @param iapClient IapClient instance to call the obtainOwnedPurchaseRecord API. + * @param priceType In-app product type. + * The value contains: 0: consumable 1: non-consumable 2 auto-renewable subscription. + * @param continuationToken Data locating flag for supporting query in pagination mode. + * @param callback IapApiCallback + */ + public static void obtainOwnedPurchaseRecord(IapClient iapClient, int priceType, String continuationToken, final IapApiCallback callback) { + Log.i(TAG, "call obtainOwnedPurchaseRecord"); + Task task = iapClient.obtainOwnedPurchaseRecord(createOwnedPurchasesReq(priceType, continuationToken)); + task.addOnSuccessListener(new OnSuccessListener() { + @Override + public void onSuccess(OwnedPurchasesResult result) { + Log.i(TAG, "obtainOwnedPurchaseRecord, success"); + callback.onSuccess(result); + + } + }).addOnFailureListener(new OnFailureListener() { + @Override + public void onFailure(Exception e) { + Log.e(TAG, "obtainOwnedPurchaseRecord, fail"); + callback.onFail(e); + } + }); + } + + /** + * Consume all the unconsumed purchases with priceType 0. + * @param iapClient IapClient instance to call the consumeOwnedPurchase API. + * @param purchaseToken which is generated by the Huawei payment server during product payment and returned to the app through InAppPurchaseData. + */ + public static void consumeOwnedPurchase(IapClient iapClient, String purchaseToken) { + Log.i(TAG, "call consumeOwnedPurchase"); + Task task = iapClient.consumeOwnedPurchase(createConsumeOwnedPurchaseReq(purchaseToken)); + task.addOnSuccessListener(new OnSuccessListener() { + @Override + public void onSuccess(ConsumeOwnedPurchaseResult result) { + // Consume success. + Log.i(TAG, "consumeOwnedPurchase success"); + } + }).addOnFailureListener(new OnFailureListener() { + @Override + public void onFailure(Exception e) { + if (e instanceof IapApiException) { + IapApiException apiException = (IapApiException)e; + int returnCode = apiException.getStatusCode(); + Log.e(TAG, "consumeOwnedPurchase fail, IapApiException returnCode: " + returnCode); + } else { + // Other external errors + Log.e(TAG, e.getMessage()); + } + + } + }); + + } + + /** + * link to subscription manager page + * @param activity activity + * @param productId the productId of the subscription product + */ + public static void showSubscription(final Activity activity, String productId) { + StartIapActivityReq req = new StartIapActivityReq(); + if (TextUtils.isEmpty(productId)) { + req.setType(StartIapActivityReq.TYPE_SUBSCRIBE_MANAGER_ACTIVITY); + } else { + req.setType(StartIapActivityReq.TYPE_SUBSCRIBE_EDIT_ACTIVITY); + req.setSubscribeProductId(productId); + } + + IapClient iapClient = Iap.getIapClient(activity); + Task task = iapClient.startIapActivity(req); + + task.addOnSuccessListener(new OnSuccessListener() { + @Override + public void onSuccess(StartIapActivityResult result) { + if(result != null) { + result.startActivity(activity); + } + } + }).addOnFailureListener(new OnFailureListener() { + @Override + public void onFailure(Exception e) { + ExceptionHandle.handle(activity, e); + } + }); + } + +} diff --git a/OsmAnd/src-huawei/net/osmand/plus/inapp/InAppPurchaseHelperImpl.java b/OsmAnd/src-huawei/net/osmand/plus/inapp/InAppPurchaseHelperImpl.java new file mode 100644 index 0000000000..c3d6fc193c --- /dev/null +++ b/OsmAnd/src-huawei/net/osmand/plus/inapp/InAppPurchaseHelperImpl.java @@ -0,0 +1,703 @@ +package net.osmand.plus.inapp; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.text.TextUtils; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.huawei.hmf.tasks.OnFailureListener; +import com.huawei.hmf.tasks.OnSuccessListener; +import com.huawei.hmf.tasks.Task; +import com.huawei.hms.iap.Iap; +import com.huawei.hms.iap.IapClient; +import com.huawei.hms.iap.entity.InAppPurchaseData; +import com.huawei.hms.iap.entity.IsEnvReadyResult; +import com.huawei.hms.iap.entity.OrderStatusCode; +import com.huawei.hms.iap.entity.OwnedPurchasesResult; +import com.huawei.hms.iap.entity.ProductInfo; +import com.huawei.hms.iap.entity.ProductInfoResult; +import com.huawei.hms.iap.entity.PurchaseIntentResult; +import com.huawei.hms.iap.entity.PurchaseResultInfo; +import com.huawei.hms.iap.entity.StartIapActivityReq; +import com.huawei.hms.iap.entity.StartIapActivityResult; + +import net.osmand.AndroidUtils; +import net.osmand.plus.OsmandApplication; +import net.osmand.plus.inapp.InAppPurchases.InAppPurchase; +import net.osmand.plus.inapp.InAppPurchases.InAppSubscription; +import net.osmand.plus.inapp.InAppPurchases.InAppSubscriptionIntroductoryInfo; +import net.osmand.plus.inapp.InAppPurchasesImpl.InAppPurchaseLiveUpdatesOldSubscription; +import net.osmand.plus.settings.backend.OsmandSettings; +import net.osmand.plus.settings.backend.OsmandSettings.OsmandPreference; +import net.osmand.util.Algorithms; + +import java.lang.ref.WeakReference; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class InAppPurchaseHelperImpl extends InAppPurchaseHelper { + + private boolean envReady = false; + private boolean purchaseSupported = false; + + private List productInfos; + private OwnedPurchasesResult ownedSubscriptions; + private List ownedInApps = new ArrayList<>(); + + public InAppPurchaseHelperImpl(OsmandApplication ctx) { + super(ctx); + purchases = new InAppPurchasesImpl(ctx); + } + + @Override + public void isInAppPurchaseSupported(@NonNull final Activity activity, @Nullable final InAppPurchaseInitCallback callback) { + if (envReady) { + if (callback != null) { + if (purchaseSupported) { + callback.onSuccess(); + } else { + callback.onFail(); + } + } + } else { + // Initiating an isEnvReady request when entering the app. + // Check if the account service country supports IAP. + IapClient mClient = Iap.getIapClient(activity); + final WeakReference activityRef = new WeakReference<>(activity); + IapRequestHelper.isEnvReady(mClient, new IapApiCallback() { + + private void onReady(boolean succeed) { + logDebug("Setup finished."); + envReady = true; + purchaseSupported = succeed; + if (callback != null) { + if (succeed) { + callback.onSuccess(); + } else { + callback.onFail(); + } + } + } + + @Override + public void onSuccess(IsEnvReadyResult result) { + onReady(true); + } + + @Override + public void onFail(Exception e) { + onReady(false); + LOG.error("isEnvReady fail, " + e.getMessage(), e); + ExceptionHandle.handle(activityRef.get(), e); + } + }); + } + } + + protected void execImpl(@NonNull final InAppPurchaseTaskType taskType, @NonNull final InAppCommand command) { + if (envReady) { + command.run(this); + } else { + command.commandDone(); + } + } + + private InAppCommand getPurchaseInAppCommand(@NonNull final Activity activity, @NonNull final String productId) throws UnsupportedOperationException { + return new InAppCommand() { + @Override + public void run(InAppPurchaseHelper helper) { + try { + ProductInfo productInfo = getProductInfo(productId); + if (productInfo != null) { + IapRequestHelper.createPurchaseIntent(getIapClient(), productInfo.getProductId(), + IapClient.PriceType.IN_APP_NONCONSUMABLE, new IapApiCallback() { + @Override + public void onSuccess(PurchaseIntentResult result) { + if (result == null) { + logError("result is null"); + } else { + // you should pull up the page to complete the payment process + IapRequestHelper.startResolutionForResult(activity, result.getStatus(), Constants.REQ_CODE_BUY_INAPP); + } + commandDone(); + } + + @Override + public void onFail(Exception e) { + int errorCode = ExceptionHandle.handle(activity, e); + if (errorCode != ExceptionHandle.SOLVED) { + logDebug("createPurchaseIntent, returnCode: " + errorCode); + if (OrderStatusCode.ORDER_PRODUCT_OWNED == errorCode) { + logError("already own this product"); + } else { + logError("unknown error"); + } + } + commandDone(); + } + }); + } else { + commandDone(); + } + } catch (Exception e) { + complain("Cannot launch full version purchase!"); + logError("purchaseFullVersion Error", e); + stop(true); + } + } + }; + } + + @Override + public void purchaseFullVersion(@NonNull final Activity activity) throws UnsupportedOperationException { + notifyShowProgress(InAppPurchaseTaskType.PURCHASE_FULL_VERSION); + exec(InAppPurchaseTaskType.PURCHASE_FULL_VERSION, getPurchaseInAppCommand(activity, purchases.getFullVersion().getSku())); + } + + @Override + public void purchaseDepthContours(@NonNull final Activity activity) throws UnsupportedOperationException { + notifyShowProgress(InAppPurchaseTaskType.PURCHASE_DEPTH_CONTOURS); + exec(InAppPurchaseTaskType.PURCHASE_DEPTH_CONTOURS, getPurchaseInAppCommand(activity, purchases.getDepthContours().getSku())); + } + + @Override + public void purchaseContourLines(@NonNull Activity activity) throws UnsupportedOperationException { + notifyShowProgress(InAppPurchaseTaskType.PURCHASE_CONTOUR_LINES); + exec(InAppPurchaseTaskType.PURCHASE_CONTOUR_LINES, getPurchaseInAppCommand(activity, purchases.getContourLines().getSku())); + } + + @Override + public void manageSubscription(@NonNull Context ctx, @Nullable String sku) { + if (uiActivity != null) { + StartIapActivityReq req = new StartIapActivityReq(); + if (!Algorithms.isEmpty(sku)) { + req.setSubscribeProductId(sku); + req.setType(StartIapActivityReq.TYPE_SUBSCRIBE_EDIT_ACTIVITY); + } else { + req.setType(StartIapActivityReq.TYPE_SUBSCRIBE_MANAGER_ACTIVITY); + } + Task task = getIapClient().startIapActivity(req); + task.addOnSuccessListener(new OnSuccessListener() { + @Override + public void onSuccess(StartIapActivityResult result) { + logDebug("startIapActivity: onSuccess"); + Activity activity = (Activity) uiActivity; + if (result != null && AndroidUtils.isActivityNotDestroyed(activity)) { + result.startActivity(activity); + } + } + }).addOnFailureListener(new OnFailureListener() { + @Override + public void onFailure(Exception e) { + logDebug("startIapActivity: onFailure"); + } + }); + } + } + + @Nullable + private ProductInfo getProductInfo(@NonNull String productId) { + List productInfos = this.productInfos; + if (productInfos != null) { + for (ProductInfo info : productInfos) { + if (info.getProductId().equals(productId)) { + return info; + } + } + } + return null; + } + + private boolean hasDetails(@NonNull String productId) { + return getProductInfo(productId) != null; + } + + @Nullable + private InAppPurchaseData getPurchaseData(@NonNull String productId) { + InAppPurchaseData data = SubscriptionUtils.getPurchaseData(ownedSubscriptions, productId); + if (data == null) { + for (OwnedPurchasesResult result : ownedInApps) { + data = InAppUtils.getPurchaseData(result, productId); + if (data != null) { + break; + } + } + } + return data; + } + + private PurchaseInfo getPurchaseInfo(InAppPurchaseData purchase) { + return new PurchaseInfo(purchase.getProductId(), purchase.getOrderID(), purchase.getPurchaseToken()); + } + + private void fetchInAppPurchase(@NonNull InAppPurchase inAppPurchase, @NonNull ProductInfo productInfo, @Nullable InAppPurchaseData purchaseData) { + if (purchaseData != null) { + inAppPurchase.setPurchaseState(InAppPurchase.PurchaseState.PURCHASED); + inAppPurchase.setPurchaseTime(purchaseData.getPurchaseTime()); + } else { + inAppPurchase.setPurchaseState(InAppPurchase.PurchaseState.NOT_PURCHASED); + } + inAppPurchase.setPrice(productInfo.getPrice()); + inAppPurchase.setPriceCurrencyCode(productInfo.getCurrency()); + if (productInfo.getMicrosPrice() > 0) { + inAppPurchase.setPriceValue(productInfo.getMicrosPrice() / 1000000d); + } + String subscriptionPeriod = productInfo.getSubPeriod(); + if (!Algorithms.isEmpty(subscriptionPeriod)) { + if (inAppPurchase instanceof InAppSubscription) { + try { + ((InAppSubscription) inAppPurchase).setSubscriptionPeriodString(subscriptionPeriod); + } catch (ParseException e) { + LOG.error(e); + } + } + } + if (inAppPurchase instanceof InAppSubscription) { + String introductoryPrice = productInfo.getSubSpecialPrice(); + String introductoryPricePeriod = productInfo.getSubPeriod(); + int introductoryPriceCycles = productInfo.getSubSpecialPeriodCycles(); + long introductoryPriceAmountMicros = productInfo.getSubSpecialPriceMicros(); + if (!Algorithms.isEmpty(introductoryPrice)) { + InAppSubscription s = (InAppSubscription) inAppPurchase; + try { + s.setIntroductoryInfo(new InAppSubscriptionIntroductoryInfo(s, introductoryPrice, + introductoryPriceAmountMicros, introductoryPricePeriod, String.valueOf(introductoryPriceCycles))); + } catch (ParseException e) { + LOG.error(e); + } + } + } + } + + protected InAppCommand getPurchaseLiveUpdatesCommand(final WeakReference activity, final String sku, final String payload) { + return new InAppCommand() { + @Override + public void run(InAppPurchaseHelper helper) { + try { + Activity a = activity.get(); + ProductInfo productInfo = getProductInfo(sku); + if (AndroidUtils.isActivityNotDestroyed(a) && productInfo != null) { + IapRequestHelper.createPurchaseIntent(getIapClient(), sku, + IapClient.PriceType.IN_APP_SUBSCRIPTION, payload, new IapApiCallback() { + @Override + public void onSuccess(PurchaseIntentResult result) { + if (result == null) { + logError("GetBuyIntentResult is null"); + } else { + Activity a = activity.get(); + if (AndroidUtils.isActivityNotDestroyed(a)) { + IapRequestHelper.startResolutionForResult(a, result.getStatus(), Constants.REQ_CODE_BUY_SUB); + } else { + logError("startResolutionForResult on destroyed activity"); + } + } + commandDone(); + } + + @Override + public void onFail(Exception e) { + int errorCode = ExceptionHandle.handle(activity.get(), e); + if (ExceptionHandle.SOLVED != errorCode) { + logError("createPurchaseIntent, returnCode: " + errorCode); + if (OrderStatusCode.ORDER_PRODUCT_OWNED == errorCode) { + logError("already own this product"); + } else { + logError("unknown error"); + } + } + commandDone(); + } + }); + } else { + stop(true); + } + } catch (Exception e) { + logError("launchPurchaseFlow Error", e); + stop(true); + } + } + }; + } + + @Override + protected InAppCommand getRequestInventoryCommand() { + return new InAppCommand() { + + @Override + protected void commandDone() { + super.commandDone(); + inventoryRequested = false; + } + + @Override + public void run(InAppPurchaseHelper helper) { + logDebug("Setup successful. Querying inventory."); + try { + productInfos = new ArrayList<>(); + obtainOwnedSubscriptions(); + } catch (Exception e) { + logError("queryInventoryAsync Error", e); + notifyDismissProgress(InAppPurchaseTaskType.REQUEST_INVENTORY); + stop(true); + commandDone(); + } + } + + private void obtainOwnedSubscriptions() { + if (uiActivity != null) { + IapRequestHelper.obtainOwnedPurchases(getIapClient(), IapClient.PriceType.IN_APP_SUBSCRIPTION, + null, new IapApiCallback() { + @Override + public void onSuccess(OwnedPurchasesResult result) { + ownedSubscriptions = result; + obtainOwnedInApps(null); + } + + @Override + public void onFail(Exception e) { + logError("obtainOwnedSubscriptions exception", e); + ExceptionHandle.handle((Activity) uiActivity, e); + commandDone(); + } + }); + } else { + commandDone(); + } + } + + private void obtainOwnedInApps(final String continuationToken) { + if (uiActivity != null) { + // Query users' purchased non-consumable products. + IapRequestHelper.obtainOwnedPurchases(getIapClient(), IapClient.PriceType.IN_APP_NONCONSUMABLE, + continuationToken, new IapApiCallback() { + @Override + public void onSuccess(OwnedPurchasesResult result) { + ownedInApps.add(result); + if (result != null && !TextUtils.isEmpty(result.getContinuationToken())) { + obtainOwnedInApps(result.getContinuationToken()); + } else { + obtainSubscriptionsInfo(); + } + } + + @Override + public void onFail(Exception e) { + logError("obtainOwnedInApps exception", e); + ExceptionHandle.handle((Activity) uiActivity, e); + commandDone(); + } + }); + } else { + commandDone(); + } + } + + private void obtainSubscriptionsInfo() { + if (uiActivity != null) { + Set productIds = new HashSet<>(); + List subscriptions = purchases.getLiveUpdates().getAllSubscriptions(); + for (InAppSubscription s : subscriptions) { + productIds.add(s.getSku()); + } + productIds.addAll(ownedSubscriptions.getItemList()); + IapRequestHelper.obtainProductInfo(getIapClient(), new ArrayList<>(productIds), + IapClient.PriceType.IN_APP_SUBSCRIPTION, new IapApiCallback() { + @Override + public void onSuccess(final ProductInfoResult result) { + if (result != null && result.getProductInfoList() != null) { + productInfos.addAll(result.getProductInfoList()); + } + obtainInAppsInfo(); + } + + @Override + public void onFail(Exception e) { + int errorCode = ExceptionHandle.handle((Activity) uiActivity, e); + if (ExceptionHandle.SOLVED != errorCode) { + LOG.error("Unknown error"); + } + commandDone(); + } + }); + } else { + commandDone(); + } + } + + private void obtainInAppsInfo() { + if (uiActivity != null) { + Set productIds = new HashSet<>(); + for (InAppPurchase purchase : getInAppPurchases().getAllInAppPurchases(false)) { + productIds.add(purchase.getSku()); + } + for (OwnedPurchasesResult result : ownedInApps) { + productIds.addAll(result.getItemList()); + } + IapRequestHelper.obtainProductInfo(getIapClient(), new ArrayList<>(productIds), + IapClient.PriceType.IN_APP_NONCONSUMABLE, new IapApiCallback() { + @Override + public void onSuccess(ProductInfoResult result) { + if (result != null && result.getProductInfoList() != null) { + productInfos.addAll(result.getProductInfoList()); + } + processInventory(); + } + + @Override + public void onFail(Exception e) { + int errorCode = ExceptionHandle.handle((Activity) uiActivity, e); + if (ExceptionHandle.SOLVED != errorCode) { + LOG.error("Unknown error"); + } + commandDone(); + } + }); + } else { + commandDone(); + } + } + + private void processInventory() { + logDebug("Query sku details was successful."); + + /* + * Check for items we own. Notice that for each purchase, we check + * the developer payload to see if it's correct! + */ + + List allOwnedSubscriptionSkus = ownedSubscriptions.getItemList(); + for (InAppSubscription s : getLiveUpdates().getAllSubscriptions()) { + if (hasDetails(s.getSku())) { + InAppPurchaseData purchaseData = getPurchaseData(s.getSku()); + ProductInfo liveUpdatesInfo = getProductInfo(s.getSku()); + if (liveUpdatesInfo != null) { + fetchInAppPurchase(s, liveUpdatesInfo, purchaseData); + } + allOwnedSubscriptionSkus.remove(s.getSku()); + } + } + for (String sku : allOwnedSubscriptionSkus) { + InAppPurchaseData purchaseData = getPurchaseData(sku); + ProductInfo liveUpdatesInfo = getProductInfo(sku); + if (liveUpdatesInfo != null) { + InAppSubscription s = getLiveUpdates().upgradeSubscription(sku); + if (s == null) { + s = new InAppPurchaseLiveUpdatesOldSubscription(liveUpdatesInfo); + } + fetchInAppPurchase(s, liveUpdatesInfo, purchaseData); + } + } + + InAppPurchase fullVersion = getFullVersion(); + if (hasDetails(fullVersion.getSku())) { + InAppPurchaseData purchaseData = getPurchaseData(fullVersion.getSku()); + ProductInfo fullPriceDetails = getProductInfo(fullVersion.getSku()); + if (fullPriceDetails != null) { + fetchInAppPurchase(fullVersion, fullPriceDetails, purchaseData); + } + } + InAppPurchase depthContours = getDepthContours(); + if (hasDetails(depthContours.getSku())) { + InAppPurchaseData purchaseData = getPurchaseData(depthContours.getSku()); + ProductInfo depthContoursDetails = getProductInfo(depthContours.getSku()); + if (depthContoursDetails != null) { + fetchInAppPurchase(depthContours, depthContoursDetails, purchaseData); + } + } + InAppPurchase contourLines = getContourLines(); + if (hasDetails(contourLines.getSku())) { + InAppPurchaseData purchaseData = getPurchaseData(contourLines.getSku()); + ProductInfo contourLinesDetails = getProductInfo(contourLines.getSku()); + if (contourLinesDetails != null) { + fetchInAppPurchase(contourLines, contourLinesDetails, purchaseData); + } + } + + if (getPurchaseData(fullVersion.getSku()) != null) { + ctx.getSettings().FULL_VERSION_PURCHASED.set(true); + } + if (getPurchaseData(depthContours.getSku()) != null) { + ctx.getSettings().DEPTH_CONTOURS_PURCHASED.set(true); + } + if (getPurchaseData(contourLines.getSku()) != null) { + ctx.getSettings().CONTOUR_LINES_PURCHASED.set(true); + } + + // Do we have the live updates? + boolean subscribedToLiveUpdates = false; + List liveUpdatesPurchases = new ArrayList<>(); + for (InAppPurchase p : getLiveUpdates().getAllSubscriptions()) { + InAppPurchaseData purchaseData = getPurchaseData(p.getSku()); + if (purchaseData != null) { + liveUpdatesPurchases.add(purchaseData); + if (!subscribedToLiveUpdates) { + subscribedToLiveUpdates = true; + } + } + } + OsmandPreference subscriptionCancelledTime = ctx.getSettings().LIVE_UPDATES_PURCHASE_CANCELLED_TIME; + if (!subscribedToLiveUpdates && ctx.getSettings().LIVE_UPDATES_PURCHASED.get()) { + if (subscriptionCancelledTime.get() == 0) { + subscriptionCancelledTime.set(System.currentTimeMillis()); + ctx.getSettings().LIVE_UPDATES_PURCHASE_CANCELLED_FIRST_DLG_SHOWN.set(false); + ctx.getSettings().LIVE_UPDATES_PURCHASE_CANCELLED_SECOND_DLG_SHOWN.set(false); + } else if (System.currentTimeMillis() - subscriptionCancelledTime.get() > SUBSCRIPTION_HOLDING_TIME_MSEC) { + ctx.getSettings().LIVE_UPDATES_PURCHASED.set(false); + if (!isDepthContoursPurchased(ctx)) { + ctx.getSettings().getCustomRenderBooleanProperty("depthContours").set(false); + } + } + } else if (subscribedToLiveUpdates) { + subscriptionCancelledTime.set(0L); + ctx.getSettings().LIVE_UPDATES_PURCHASED.set(true); + } + + lastValidationCheckTime = System.currentTimeMillis(); + logDebug("User " + (subscribedToLiveUpdates ? "HAS" : "DOES NOT HAVE") + + " live updates purchased."); + + OsmandSettings settings = ctx.getSettings(); + settings.INAPPS_READ.set(true); + + List tokensToSend = new ArrayList<>(); + if (liveUpdatesPurchases.size() > 0) { + List tokensSent = Arrays.asList(settings.BILLING_PURCHASE_TOKENS_SENT.get().split(";")); + for (InAppPurchaseData purchase : liveUpdatesPurchases) { + if ((Algorithms.isEmpty(settings.BILLING_USER_ID.get()) || Algorithms.isEmpty(settings.BILLING_USER_TOKEN.get())) + && !Algorithms.isEmpty(purchase.getDeveloperPayload())) { + String payload = purchase.getDeveloperPayload(); + if (!Algorithms.isEmpty(payload)) { + String[] arr = payload.split(" "); + if (arr.length > 0) { + settings.BILLING_USER_ID.set(arr[0]); + } + if (arr.length > 1) { + token = arr[1]; + settings.BILLING_USER_TOKEN.set(token); + } + } + } + if (!tokensSent.contains(purchase.getProductId())) { + tokensToSend.add(purchase); + } + } + } + List purchaseInfoList = new ArrayList<>(); + for (InAppPurchaseData purchase : tokensToSend) { + purchaseInfoList.add(getPurchaseInfo(purchase)); + } + onSkuDetailsResponseDone(purchaseInfoList); + } + }; + } + + private IapClient getIapClient() { + return Iap.getIapClient((Activity) uiActivity); + } + + // Call when a purchase is finished + private void onPurchaseFinished(InAppPurchaseData purchase) { + logDebug("Purchase finished: " + purchase.getProductId()); + onPurchaseDone(getPurchaseInfo(purchase)); + } + + @Override + protected boolean isBillingManagerExists() { + return false; + } + + @Override + protected void destroyBillingManager() { + // non implemented + } + + @Override + public boolean onActivityResult(@NonNull Activity activity, int requestCode, int resultCode, Intent data) { + if (requestCode == Constants.REQ_CODE_BUY_SUB) { + boolean succeed = false; + if (resultCode == Activity.RESULT_OK) { + PurchaseResultInfo result = SubscriptionUtils.getPurchaseResult(activity, data); + if (result != null) { + switch (result.getReturnCode()) { + case OrderStatusCode.ORDER_STATE_CANCEL: + logDebug("Purchase cancelled"); + break; + case OrderStatusCode.ORDER_STATE_FAILED: + inventoryRequestPending = true; + logDebug("Purchase failed"); + break; + case OrderStatusCode.ORDER_PRODUCT_OWNED: + inventoryRequestPending = true; + logDebug("Product already owned"); + break; + case OrderStatusCode.ORDER_STATE_SUCCESS: + inventoryRequestPending = true; + InAppPurchaseData purchaseData = SubscriptionUtils.getInAppPurchaseData(null, + result.getInAppPurchaseData(), result.getInAppDataSignature()); + if (purchaseData != null) { + onPurchaseFinished(purchaseData); + succeed = true; + } else { + logDebug("Purchase failed"); + } + break; + default: + break; + } + } else { + logDebug("Purchase failed"); + } + } else { + logDebug("Purchase cancelled"); + } + if (!succeed) { + stop(true); + } + return true; + } else if (requestCode == Constants.REQ_CODE_BUY_INAPP) { + boolean succeed = false; + if (data == null) { + logDebug("data is null"); + } else { + PurchaseResultInfo buyResultInfo = Iap.getIapClient(activity).parsePurchaseResultInfoFromIntent(data); + switch (buyResultInfo.getReturnCode()) { + case OrderStatusCode.ORDER_STATE_CANCEL: + logDebug("Order has been canceled"); + break; + case OrderStatusCode.ORDER_STATE_FAILED: + inventoryRequestPending = true; + logDebug("Order has been failed"); + break; + case OrderStatusCode.ORDER_PRODUCT_OWNED: + inventoryRequestPending = true; + logDebug("Product already owned"); + break; + case OrderStatusCode.ORDER_STATE_SUCCESS: + InAppPurchaseData purchaseData = InAppUtils.getInAppPurchaseData(null, + buyResultInfo.getInAppPurchaseData(), buyResultInfo.getInAppDataSignature()); + if (purchaseData != null) { + onPurchaseFinished(purchaseData); + succeed = true; + } else { + logDebug("Purchase failed"); + } + break; + default: + break; + } + } + if (!succeed) { + stop(true); + } + return true; + } + return false; + } +} diff --git a/OsmAnd/src-huawei/net/osmand/plus/inapp/InAppPurchasesImpl.java b/OsmAnd/src-huawei/net/osmand/plus/inapp/InAppPurchasesImpl.java new file mode 100644 index 0000000000..4ed0021b6f --- /dev/null +++ b/OsmAnd/src-huawei/net/osmand/plus/inapp/InAppPurchasesImpl.java @@ -0,0 +1,196 @@ +package net.osmand.plus.inapp; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.huawei.hms.iap.entity.ProductInfo; + +import net.osmand.plus.OsmandApplication; +import net.osmand.plus.R; + +public class InAppPurchasesImpl extends InAppPurchases { + + private static final InAppPurchase FULL_VERSION = new InAppPurchaseFullVersion(); + private static final InAppPurchaseDepthContoursFree DEPTH_CONTOURS_FREE = new InAppPurchaseDepthContoursFree(); + private static final InAppPurchaseContourLinesFree CONTOUR_LINES_FREE = new InAppPurchaseContourLinesFree(); + + + private static final InAppSubscription[] LIVE_UPDATES_FREE = new InAppSubscription[]{ + new InAppPurchaseLiveUpdatesMonthlyFree(), + new InAppPurchaseLiveUpdates3MonthsFree(), + new InAppPurchaseLiveUpdatesAnnualFree() + }; + + public InAppPurchasesImpl(OsmandApplication ctx) { + super(ctx); + fullVersion = FULL_VERSION; + depthContours = DEPTH_CONTOURS_FREE; + contourLines = CONTOUR_LINES_FREE; + inAppPurchases = new InAppPurchase[] { fullVersion, depthContours, contourLines }; + + liveUpdates = new LiveUpdatesInAppPurchasesFree(); + for (InAppSubscription s : liveUpdates.getAllSubscriptions()) { + if (s instanceof InAppPurchaseLiveUpdatesMonthly) { + if (s.isDiscounted()) { + discountedMonthlyLiveUpdates = s; + } else { + monthlyLiveUpdates = s; + } + } + } + } + + @Override + public boolean isFullVersion(String sku) { + return FULL_VERSION.getSku().equals(sku); + } + + @Override + public boolean isDepthContours(String sku) { + return DEPTH_CONTOURS_FREE.getSku().equals(sku); + } + + @Override + public boolean isContourLines(String sku) { + return CONTOUR_LINES_FREE.getSku().equals(sku); + } + + @Override + public boolean isLiveUpdates(String sku) { + for (InAppPurchase p : LIVE_UPDATES_FREE) { + if (p.getSku().equals(sku)) { + return true; + } + } + return false; + } + + private static class InAppPurchaseFullVersion extends InAppPurchase { + + private static final String SKU_FULL_VERSION_PRICE = "net.osmand.huawei.full"; + + InAppPurchaseFullVersion() { + super(SKU_FULL_VERSION_PRICE); + } + + @Override + public String getDefaultPrice(Context ctx) { + return ctx.getString(R.string.full_version_price); + } + } + + private static class InAppPurchaseDepthContoursFree extends InAppPurchaseDepthContours { + + private static final String SKU_DEPTH_CONTOURS_FREE = "net.osmand.huawei.seadepth"; + + InAppPurchaseDepthContoursFree() { + super(SKU_DEPTH_CONTOURS_FREE); + } + } + + private static class InAppPurchaseContourLinesFree extends InAppPurchaseContourLines { + + private static final String SKU_CONTOUR_LINES_FREE = "net.osmand.huawei.contourlines"; + + InAppPurchaseContourLinesFree() { + super(SKU_CONTOUR_LINES_FREE); + } + } + + private static class InAppPurchaseLiveUpdatesMonthlyFree extends InAppPurchaseLiveUpdatesMonthly { + + private static final String SKU_LIVE_UPDATES_MONTHLY_HW_FREE = "net.osmand.huawei.monthly"; + + InAppPurchaseLiveUpdatesMonthlyFree() { + super(SKU_LIVE_UPDATES_MONTHLY_HW_FREE, 1); + } + + private InAppPurchaseLiveUpdatesMonthlyFree(@NonNull String sku) { + super(sku); + } + + @Nullable + @Override + protected InAppSubscription newInstance(@NonNull String sku) { + return sku.startsWith(getSkuNoVersion()) ? new InAppPurchaseLiveUpdatesMonthlyFree(sku) : null; + } + } + + private static class InAppPurchaseLiveUpdates3MonthsFree extends InAppPurchaseLiveUpdates3Months { + + private static final String SKU_LIVE_UPDATES_3_MONTHS_HW_FREE = "net.osmand.huawei.3months"; + + InAppPurchaseLiveUpdates3MonthsFree() { + super(SKU_LIVE_UPDATES_3_MONTHS_HW_FREE, 1); + } + + private InAppPurchaseLiveUpdates3MonthsFree(@NonNull String sku) { + super(sku); + } + + @Nullable + @Override + protected InAppSubscription newInstance(@NonNull String sku) { + return sku.startsWith(getSkuNoVersion()) ? new InAppPurchaseLiveUpdates3MonthsFree(sku) : null; + } + } + + private static class InAppPurchaseLiveUpdatesAnnualFree extends InAppPurchaseLiveUpdatesAnnual { + + private static final String SKU_LIVE_UPDATES_ANNUAL_HW_FREE = "net.osmand.huawei.annual"; + + InAppPurchaseLiveUpdatesAnnualFree() { + super(SKU_LIVE_UPDATES_ANNUAL_HW_FREE, 1); + } + + private InAppPurchaseLiveUpdatesAnnualFree(@NonNull String sku) { + super(sku); + } + + @Nullable + @Override + protected InAppSubscription newInstance(@NonNull String sku) { + return sku.startsWith(getSkuNoVersion()) ? new InAppPurchaseLiveUpdatesAnnualFree(sku) : null; + } + } + + public static class InAppPurchaseLiveUpdatesOldSubscription extends InAppSubscription { + + private ProductInfo info; + + InAppPurchaseLiveUpdatesOldSubscription(@NonNull ProductInfo info) { + super(info.getProductId(), true); + this.info = info; + } + + @Override + public String getDefaultPrice(Context ctx) { + return ""; + } + + @Override + public CharSequence getTitle(Context ctx) { + return info.getProductName(); + } + + @Override + public CharSequence getDescription(@NonNull Context ctx) { + return info.getProductDesc(); + } + + @Nullable + @Override + protected InAppSubscription newInstance(@NonNull String sku) { + return null; + } + } + + private static class LiveUpdatesInAppPurchasesFree extends InAppSubscriptionList { + + public LiveUpdatesInAppPurchasesFree() { + super(LIVE_UPDATES_FREE); + } + } +} diff --git a/OsmAnd/src-huawei/net/osmand/plus/inapp/InAppUtils.java b/OsmAnd/src-huawei/net/osmand/plus/inapp/InAppUtils.java new file mode 100644 index 0000000000..445727de96 --- /dev/null +++ b/OsmAnd/src-huawei/net/osmand/plus/inapp/InAppUtils.java @@ -0,0 +1,49 @@ +package net.osmand.plus.inapp; + +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.huawei.hms.iap.entity.InAppPurchaseData; +import com.huawei.hms.iap.entity.OwnedPurchasesResult; + +import org.json.JSONException; + +public class InAppUtils { + private static final String TAG = "InAppUtils"; + + @Nullable + public static InAppPurchaseData getPurchaseData(OwnedPurchasesResult result, String productId) { + if (result == null || result.getInAppPurchaseDataList() == null) { + Log.i(TAG, "result is null"); + return null; + } + int index = result.getItemList().indexOf(productId); + if (index != -1) { + String data = result.getInAppPurchaseDataList().get(index); + String signature = result.getInAppSignature().get(index); + return getInAppPurchaseData(productId, data, signature); + } + return null; + } + + @Nullable + public static InAppPurchaseData getInAppPurchaseData(@Nullable String productId, @NonNull String data, @NonNull String signature) { + if (CipherUtil.doCheck(data, signature, CipherUtil.getPublicKey())) { + try { + InAppPurchaseData purchaseData = new InAppPurchaseData(data); + if (purchaseData.getPurchaseState() == InAppPurchaseData.PurchaseState.PURCHASED) { + if (productId == null || productId.equals(purchaseData.getProductId())) { + return purchaseData; + } + } + } catch (JSONException e) { + Log.e(TAG, "delivery: " + e.getMessage()); + } + } else { + Log.e(TAG, "delivery: verify signature error"); + } + return null; + } +} \ No newline at end of file diff --git a/OsmAnd/src-huawei/net/osmand/plus/inapp/SubscriptionUtils.java b/OsmAnd/src-huawei/net/osmand/plus/inapp/SubscriptionUtils.java new file mode 100755 index 0000000000..3b249cb300 --- /dev/null +++ b/OsmAnd/src-huawei/net/osmand/plus/inapp/SubscriptionUtils.java @@ -0,0 +1,138 @@ +/** + * Copyright 2020. Huawei Technologies Co., Ltd. All rights reserved. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.osmand.plus.inapp; + +import android.app.Activity; +import android.content.Intent; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.huawei.hms.iap.Iap; +import com.huawei.hms.iap.entity.InAppPurchaseData; +import com.huawei.hms.iap.entity.OrderStatusCode; +import com.huawei.hms.iap.entity.OwnedPurchasesResult; +import com.huawei.hms.iap.entity.PurchaseResultInfo; + +import org.json.JSONException; + +import java.util.List; + +/** + * Util for Subscription function. + * + * @since 2019/12/9 + */ +public class SubscriptionUtils { + private static final String TAG = "SubscriptionUtils"; + + /** + * Decide whether to offer subscription service + * + * @param result the OwnedPurchasesResult from IapClient.obtainOwnedPurchases + * @param productId subscription product id + * @return decision result + */ + @Nullable + public static InAppPurchaseData getPurchaseData(OwnedPurchasesResult result, String productId) { + if (null == result) { + Log.e(TAG, "OwnedPurchasesResult is null"); + return null; + } + List dataList = result.getInAppPurchaseDataList(); + List signatureList = result.getInAppSignature(); + for (int i = 0; i < dataList.size(); i++) { + String data = dataList.get(i); + String signature = signatureList.get(i); + InAppPurchaseData purchaseData = getInAppPurchaseData(productId, data, signature); + if (purchaseData != null) { + return purchaseData; + } + } + return null; + } + + @Nullable + public static InAppPurchaseData getInAppPurchaseData(@Nullable String productId, @NonNull String data, @NonNull String signature) { + try { + InAppPurchaseData purchaseData = new InAppPurchaseData(data); + if (productId == null || productId.equals(purchaseData.getProductId())) { + boolean credible = CipherUtil.doCheck(data, signature, CipherUtil.getPublicKey()); + if (credible) { + return purchaseData.isSubValid() ? purchaseData : null; + } else { + Log.e(TAG, "check the data signature fail"); + return null; + } + } + } catch (JSONException e) { + Log.e(TAG, "parse InAppPurchaseData JSONException", e); + return null; + } + return null; + } + + /** + * Parse PurchaseResult data from intent + * + * @param activity Activity + * @param data the intent from onActivityResult + * @return PurchaseResultInfo + */ + public static PurchaseResultInfo getPurchaseResult(Activity activity, Intent data) { + PurchaseResultInfo purchaseResultInfo = Iap.getIapClient(activity).parsePurchaseResultInfoFromIntent(data); + if (null == purchaseResultInfo) { + Log.e(TAG, "PurchaseResultInfo is null"); + } else { + int returnCode = purchaseResultInfo.getReturnCode(); + String errMsg = purchaseResultInfo.getErrMsg(); + switch (returnCode) { + case OrderStatusCode.ORDER_PRODUCT_OWNED: + Log.w(TAG, "you have owned this product"); + break; + case OrderStatusCode.ORDER_STATE_SUCCESS: + boolean credible = CipherUtil.doCheck(purchaseResultInfo.getInAppPurchaseData(), purchaseResultInfo.getInAppDataSignature(), CipherUtil + .getPublicKey()); + if (credible) { + try { + InAppPurchaseData inAppPurchaseData = new InAppPurchaseData(purchaseResultInfo.getInAppPurchaseData()); + if (!inAppPurchaseData.isSubValid()) { + return getFailedPurchaseResultInfo(); + } + } catch (JSONException e) { + Log.e(TAG, "parse InAppPurchaseData JSONException", e); + return getFailedPurchaseResultInfo(); + } + } else { + Log.e(TAG, "check the data signature fail"); + return getFailedPurchaseResultInfo(); + } + default: + Log.e(TAG, "returnCode: " + returnCode + " , errMsg: " + errMsg); + break; + } + } + return purchaseResultInfo; + } + + private static PurchaseResultInfo getFailedPurchaseResultInfo() { + PurchaseResultInfo info = new PurchaseResultInfo(); + info.setReturnCode(OrderStatusCode.ORDER_STATE_FAILED); + return info; + } +} diff --git a/OsmAnd/src/net/osmand/FileUtils.java b/OsmAnd/src/net/osmand/FileUtils.java index 426c5c10c2..65ea676356 100644 --- a/OsmAnd/src/net/osmand/FileUtils.java +++ b/OsmAnd/src/net/osmand/FileUtils.java @@ -205,10 +205,7 @@ public class FileUtils { if (!src.exists()) { return null; } - File tempDir = app.getAppPath(IndexConstants.TEMP_DIR); - if (!tempDir.exists()) { - tempDir.mkdirs(); - } + File tempDir = getTempDir(app); File dest = new File(tempDir, src.getName()); try { Algorithms.fileCopy(src, dest); @@ -218,6 +215,14 @@ public class FileUtils { return dest; } + public static File getTempDir(OsmandApplication app) { + File tempDir = app.getAppPath(IndexConstants.TEMP_DIR); + if (!tempDir.exists()) { + tempDir.mkdirs(); + } + return tempDir; + } + public interface RenameCallback { void renamedTo(File file); } diff --git a/OsmAnd/src/net/osmand/aidl/OsmandAidlApi.java b/OsmAnd/src/net/osmand/aidl/OsmandAidlApi.java index 07c545a424..6329d5db2e 100644 --- a/OsmAnd/src/net/osmand/aidl/OsmandAidlApi.java +++ b/OsmAnd/src/net/osmand/aidl/OsmandAidlApi.java @@ -26,9 +26,11 @@ import com.google.gson.reflect.TypeToken; import net.osmand.AndroidUtils; import net.osmand.CallbackWithObject; +import net.osmand.FileUtils; import net.osmand.GPXUtilities; import net.osmand.GPXUtilities.GPXFile; import net.osmand.GPXUtilities.GPXTrackAnalysis; +import net.osmand.IProgress; import net.osmand.IndexConstants; import net.osmand.Location; import net.osmand.PlatformUtil; @@ -80,6 +82,7 @@ import net.osmand.plus.settings.backend.ApplicationMode; import net.osmand.plus.settings.backend.OsmAndAppCustomization; import net.osmand.plus.settings.backend.OsmandSettings; import net.osmand.plus.settings.backend.SettingsHelper; +import net.osmand.plus.settings.backend.ExportSettingsType; import net.osmand.plus.views.OsmandMapLayer; import net.osmand.plus.views.OsmandMapTileView; import net.osmand.plus.views.layers.AidlMapLayer; @@ -129,11 +132,13 @@ import static net.osmand.aidlapi.OsmandAidlConstants.COPY_FILE_PART_SIZE_LIMIT_E import static net.osmand.aidlapi.OsmandAidlConstants.COPY_FILE_UNSUPPORTED_FILE_TYPE_ERROR; import static net.osmand.aidlapi.OsmandAidlConstants.COPY_FILE_WRITE_LOCK_ERROR; import static net.osmand.aidlapi.OsmandAidlConstants.OK_RESPONSE; +import static net.osmand.plus.FavouritesDbHelper.FILE_TO_SAVE; import static net.osmand.plus.helpers.ExternalApiHelper.PARAM_NT_DIRECTION_LANES; import static net.osmand.plus.helpers.ExternalApiHelper.PARAM_NT_DIRECTION_NAME; import static net.osmand.plus.helpers.ExternalApiHelper.PARAM_NT_DIRECTION_TURN; import static net.osmand.plus.helpers.ExternalApiHelper.PARAM_NT_DISTANCE; import static net.osmand.plus.helpers.ExternalApiHelper.PARAM_NT_IMMINENT; +import static net.osmand.plus.settings.backend.SettingsHelper.REPLACE_KEY; public class OsmandAidlApi { @@ -204,7 +209,7 @@ public class OsmandAidlApi { private static final ApplicationMode DEFAULT_PROFILE = ApplicationMode.CAR; - private static final ApplicationMode[] VALID_PROFILES = new ApplicationMode[] { + private static final ApplicationMode[] VALID_PROFILES = new ApplicationMode[]{ ApplicationMode.CAR, ApplicationMode.BICYCLE, ApplicationMode.PEDESTRIAN @@ -284,7 +289,7 @@ public class OsmandAidlApi { } private void initOsmandTelegram() { - String[] packages = new String[] {"net.osmand.telegram", "net.osmand.telegram.debug"}; + String[] packages = new String[]{"net.osmand.telegram", "net.osmand.telegram.debug"}; Intent intent = new Intent("net.osmand.telegram.InitApp"); for (String pack : packages) { intent.setComponent(new ComponentName(pack, "net.osmand.telegram.InitAppBroadcastReceiver")); @@ -1015,7 +1020,7 @@ public class OsmandAidlApi { } if (!newName.equals(f.getName()) || !newDescription.equals(f.getDescription()) || !newCategory.equals(f.getCategory()) || !newAddress.equals(f.getAddress())) { - favoritesHelper.editFavouriteName(f, newName, newCategory, newDescription,newAddress); + favoritesHelper.editFavouriteName(f, newName, newCategory, newDescription, newAddress); } refreshMap(); return true; @@ -2260,6 +2265,21 @@ public class OsmandAidlApi { return false; } + public boolean importProfileV2(final Uri profileUri, ArrayList settingsTypeKeys, boolean replace, + String latestChanges, int version) { + if (profileUri != null) { + Bundle bundle = new Bundle(); + bundle.putStringArrayList(SettingsHelper.SETTINGS_TYPE_LIST_KEY, settingsTypeKeys); + bundle.putBoolean(REPLACE_KEY, replace); + bundle.putString(SettingsHelper.SETTINGS_LATEST_CHANGES_KEY, latestChanges); + bundle.putInt(SettingsHelper.SETTINGS_VERSION_KEY, version); + + MapActivity.launchMapActivityMoveToTop(app, null, profileUri, bundle); + return true; + } + return false; + } + public void registerLayerContextMenu(ContextMenuAdapter adapter, MapActivity mapActivity) { for (ConnectedApp connectedApp : getConnectedApps()) { if (!connectedApp.getLayers().isEmpty()) { @@ -2323,6 +2343,25 @@ public class OsmandAidlApi { return true; } + public boolean exportProfile(String appModeKey, List settingsTypesKeys) { + ApplicationMode appMode = ApplicationMode.valueOfStringKey(appModeKey, null); + if (app != null && appMode != null) { + List settingsTypes = new ArrayList<>(); + for (String key : settingsTypesKeys) { + settingsTypes.add(ExportSettingsType.valueOf(key)); + } + List settingsItems = new ArrayList<>(); + settingsItems.add(new SettingsHelper.ProfileSettingsItem(app, appMode)); + File exportDir = app.getSettings().getExternalStorageDirectory(); + String fileName = appMode.toHumanString(); + SettingsHelper settingsHelper = app.getSettingsHelper(); + settingsItems.addAll(settingsHelper.getFilteredSettingsItems(settingsHelper.getAdditionalData(), settingsTypes)); + settingsHelper.exportSettings(exportDir, fileName, null, settingsItems, true); + return true; + } + return false; + } + private static class FileCopyInfo { long startTime; long lastAccessTime; @@ -2349,13 +2388,35 @@ public class OsmandAidlApi { } } - private int copyFileImpl(String fileName, byte[] filePartData, long startTime, boolean done, String destinationDir) { - File file = app.getAppPath(IndexConstants.TEMP_DIR + fileName); - File tempDir = app.getAppPath(IndexConstants.TEMP_DIR); - if (!tempDir.exists()) { - tempDir.mkdirs(); + int copyFileV2(String destinationDir, String fileName, byte[] filePartData, long startTime, boolean done) { + if (Algorithms.isEmpty(fileName) || filePartData == null) { + return COPY_FILE_PARAMS_ERROR; } - File destFile = app.getAppPath(destinationDir + fileName); + if (filePartData.length > COPY_FILE_PART_SIZE_LIMIT) { + return COPY_FILE_PART_SIZE_LIMIT_ERROR; + } + int result = copyFileImpl(fileName, filePartData, startTime, done, destinationDir); + if (done) { + if (fileName.endsWith(IndexConstants.BINARY_MAP_INDEX_EXT) && IndexConstants.MAPS_PATH.equals(destinationDir)) { + app.getResourceManager().reloadIndexes(IProgress.EMPTY_PROGRESS, new ArrayList()); + app.getDownloadThread().updateLoadedFiles(); + } else if (fileName.endsWith(IndexConstants.GPX_FILE_EXT)) { + if (destinationDir.startsWith(IndexConstants.GPX_INDEX_DIR) + && !FILE_TO_SAVE.equals(fileName)) { + destinationDir = destinationDir.replaceFirst(IndexConstants.GPX_INDEX_DIR, ""); + showGpx(new File(destinationDir, fileName).getPath()); + } else if (destinationDir.isEmpty() && FILE_TO_SAVE.equals(fileName)) { + app.getFavorites().loadFavorites(); + } + } + } + return result; + } + + private int copyFileImpl(String fileName, byte[] filePartData, long startTime, boolean done, String destinationDir) { + File tempDir = FileUtils.getTempDir(app); + File file = new File(tempDir, fileName); + File destFile = app.getAppPath(new File(destinationDir, fileName).getPath()); long currentTime = System.currentTimeMillis(); try { FileCopyInfo info = copyFilesCache.get(fileName); diff --git a/OsmAnd/src/net/osmand/aidl/OsmandAidlService.java b/OsmAnd/src/net/osmand/aidl/OsmandAidlService.java index f1c9493fc8..99e888973f 100644 --- a/OsmAnd/src/net/osmand/aidl/OsmandAidlService.java +++ b/OsmAnd/src/net/osmand/aidl/OsmandAidlService.java @@ -1299,7 +1299,8 @@ public class OsmandAidlService extends Service implements AidlCallbackListener { public boolean importProfile(ProfileSettingsParams params) { try { OsmandAidlApi api = getApi("importProfile"); - return api != null && api.importProfile(params.getProfileSettingsUri(), params.getLatestChanges(), params.getVersion()); + return api != null && api.importProfile(params.getProfileSettingsUri(), params.getLatestChanges(), + params.getVersion()); } catch (Exception e) { handleException(e); return false; diff --git a/OsmAnd/src/net/osmand/aidl/OsmandAidlServiceV2.java b/OsmAnd/src/net/osmand/aidl/OsmandAidlServiceV2.java index 333fd01895..7c69be1e94 100644 --- a/OsmAnd/src/net/osmand/aidl/OsmandAidlServiceV2.java +++ b/OsmAnd/src/net/osmand/aidl/OsmandAidlServiceV2.java @@ -85,6 +85,7 @@ import net.osmand.aidlapi.note.StartVideoRecordingParams; import net.osmand.aidlapi.note.StopRecordingParams; import net.osmand.aidlapi.note.TakePhotoNoteParams; import net.osmand.aidlapi.plugins.PluginParams; +import net.osmand.aidlapi.profile.ExportProfileParams; import net.osmand.aidlapi.quickaction.QuickActionInfoParams; import net.osmand.aidlapi.quickaction.QuickActionParams; import net.osmand.aidlapi.search.SearchParams; @@ -1091,7 +1092,8 @@ public class OsmandAidlServiceV2 extends Service implements AidlCallbackListener if (api == null) { return CANNOT_ACCESS_API_ERROR; } - return api.copyFile(params.getFileName(), params.getFilePartData(), params.getStartTime(), params.isDone()); + return api.copyFileV2(params.getDestinationDir(), params.getFileName(), params.getFilePartData(), + params.getStartTime(), params.isDone()); } catch (Exception e) { handleException(e); return UNKNOWN_API_ERROR; @@ -1258,7 +1260,19 @@ public class OsmandAidlServiceV2 extends Service implements AidlCallbackListener public boolean importProfile(ProfileSettingsParams params) { try { OsmandAidlApi api = getApi("importProfile"); - return api != null && api.importProfile(params.getProfileSettingsUri(), params.getLatestChanges(), params.getVersion()); + return api != null && api.importProfileV2(params.getProfileSettingsUri(), params.getSettingsTypeKeys(), + params.isReplace(), params.getLatestChanges(), params.getVersion()); + } catch (Exception e) { + handleException(e); + return false; + } + } + + @Override + public boolean exportProfile(ExportProfileParams params) { + try { + OsmandAidlApi api = getApi("exportProfile"); + return api != null && api.exportProfile(params.getProfile(), params.getSettingsTypeKeys()); } catch (Exception e) { handleException(e); return false; diff --git a/OsmAnd/src/net/osmand/plus/AppInitializer.java b/OsmAnd/src/net/osmand/plus/AppInitializer.java index 2837bff6f8..0ae24b7c5f 100644 --- a/OsmAnd/src/net/osmand/plus/AppInitializer.java +++ b/OsmAnd/src/net/osmand/plus/AppInitializer.java @@ -38,7 +38,7 @@ import net.osmand.plus.download.ui.AbstractLoadLocalIndexTask; import net.osmand.plus.helpers.AvoidSpecificRoads; import net.osmand.plus.helpers.LockHelper; import net.osmand.plus.helpers.WaypointHelper; -import net.osmand.plus.inapp.InAppPurchaseHelper; +import net.osmand.plus.inapp.InAppPurchaseHelperImpl; import net.osmand.plus.liveupdates.LiveUpdatesHelper; import net.osmand.plus.mapmarkers.MapMarkersDbHelper; import net.osmand.plus.monitoring.LiveMonitoringHelper; @@ -428,7 +428,7 @@ public class AppInitializer implements IProgress { } getLazyRoutingConfig(); app.applyTheme(app); - app.inAppPurchaseHelper = startupInit(new InAppPurchaseHelper(app), InAppPurchaseHelper.class); + app.inAppPurchaseHelper = startupInit(new InAppPurchaseHelperImpl(app), InAppPurchaseHelperImpl.class); app.poiTypes = startupInit(MapPoiTypes.getDefaultNoInit(), MapPoiTypes.class); app.transportRoutingHelper = startupInit(new TransportRoutingHelper(app), TransportRoutingHelper.class); app.routingHelper = startupInit(new RoutingHelper(app), RoutingHelper.class); diff --git a/OsmAnd/src/net/osmand/plus/ContextMenuItem.java b/OsmAnd/src/net/osmand/plus/ContextMenuItem.java index 3a5b391a67..3718169532 100644 --- a/OsmAnd/src/net/osmand/plus/ContextMenuItem.java +++ b/OsmAnd/src/net/osmand/plus/ContextMenuItem.java @@ -34,6 +34,7 @@ public class ContextMenuItem { private boolean hidden; private int order; private String description; + private final OnUpdateCallback onUpdateCallback; private final ContextMenuAdapter.ItemClickListener itemClickListener; private final ContextMenuAdapter.OnIntegerValueChangedListener integerListener; private final ContextMenuAdapter.ProgressListener progressListener; @@ -58,6 +59,7 @@ public class ContextMenuItem { boolean skipPaintingWithoutColor, int order, String description, + OnUpdateCallback onUpdateCallback, ContextMenuAdapter.ItemClickListener itemClickListener, ContextMenuAdapter.OnIntegerValueChangedListener integerListener, ContextMenuAdapter.ProgressListener progressListener, @@ -81,6 +83,7 @@ public class ContextMenuItem { this.skipPaintingWithoutColor = skipPaintingWithoutColor; this.order = order; this.description = description; + this.onUpdateCallback = onUpdateCallback; this.itemClickListener = itemClickListener; this.integerListener = integerListener; this.progressListener = progressListener; @@ -245,6 +248,16 @@ public class ContextMenuItem { return id; } + public void update() { + if (onUpdateCallback != null) { + onUpdateCallback.onUpdateMenuItem(this); + } + } + + public interface OnUpdateCallback { + void onUpdateMenuItem(ContextMenuItem item); + } + public static ItemBuilder createBuilder(String title) { return new ItemBuilder().setTitle(title); } @@ -268,6 +281,7 @@ public class ContextMenuItem { private boolean mIsClickable = true; private int mOrder = 0; private String mDescription = null; + private OnUpdateCallback mOnUpdateCallback = null; private ContextMenuAdapter.ItemClickListener mItemClickListener = null; private ContextMenuAdapter.OnIntegerValueChangedListener mIntegerListener = null; private ContextMenuAdapter.ProgressListener mProgressListener = null; @@ -348,6 +362,11 @@ public class ContextMenuItem { return this; } + public ItemBuilder setOnUpdateCallback(OnUpdateCallback onUpdateCallback) { + mOnUpdateCallback = onUpdateCallback; + return this; + } + public ItemBuilder setListener(ContextMenuAdapter.ItemClickListener checkBoxListener) { mItemClickListener = checkBoxListener; return this; @@ -403,10 +422,12 @@ public class ContextMenuItem { } public ContextMenuItem createItem() { - return new ContextMenuItem(mTitleId, mTitle, mIcon, mColorRes, mSecondaryIcon, + ContextMenuItem item = new ContextMenuItem(mTitleId, mTitle, mIcon, mColorRes, mSecondaryIcon, mSelected, mProgress, mLayout, mLoading, mIsCategory, mIsClickable, mSkipPaintingWithoutColor, - mOrder, mDescription, mItemClickListener, mIntegerListener, mProgressListener, mItemDeleteAction, - mHideDivider, mHideCompoundButton, mMinHeight, mTag, mId); + mOrder, mDescription, mOnUpdateCallback, mItemClickListener, mIntegerListener, mProgressListener, + mItemDeleteAction, mHideDivider, mHideCompoundButton, mMinHeight, mTag, mId); + item.update(); + return item; } } } diff --git a/OsmAnd/src/net/osmand/plus/HuaweiDrmHelper.java b/OsmAnd/src/net/osmand/plus/HuaweiDrmHelper.java deleted file mode 100644 index 7cc2f2798e..0000000000 --- a/OsmAnd/src/net/osmand/plus/HuaweiDrmHelper.java +++ /dev/null @@ -1,66 +0,0 @@ -package net.osmand.plus; - -import android.app.Activity; -import android.util.Log; - -import java.lang.ref.WeakReference; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; - -public class HuaweiDrmHelper { - private static final String TAG = HuaweiDrmHelper.class.getSimpleName(); - - //Copyright protection id - private static final String DRM_ID = "101117397"; - //Copyright protection public key - private static final String DRM_PUBLIC_KEY = "9d6f861e7d46be167809a6a62302749a6753b3c1bd02c9729efb3973e268091d"; - - public static void check(Activity activity) { - boolean succeed = false; - try { - final WeakReference activityRef = new WeakReference<>(activity); - Class drmCheckCallbackClass = Class.forName("com.huawei.android.sdk.drm.DrmCheckCallback"); - Object callback = java.lang.reflect.Proxy.newProxyInstance( - drmCheckCallbackClass.getClassLoader(), - new java.lang.Class[]{drmCheckCallbackClass}, - new java.lang.reflect.InvocationHandler() { - - @Override - public Object invoke(Object proxy, java.lang.reflect.Method method, Object[] args) { - Activity a = activityRef.get(); - if (a != null && !a.isFinishing()) { - String method_name = method.getName(); - if (method_name.equals("onCheckSuccess")) { - // skip now - } else if (method_name.equals("onCheckFailed")) { - closeApplication(a); - } - } - return null; - } - }); - - Class drmClass = Class.forName("com.huawei.android.sdk.drm.Drm"); - Class[] partypes = new Class[]{Activity.class, String.class, String.class, String.class, drmCheckCallbackClass}; - Method check = drmClass.getMethod("check", partypes); - check.invoke(null, activity, activity.getPackageName(), DRM_ID, DRM_PUBLIC_KEY, callback); - succeed = true; - - } catch (ClassNotFoundException e) { - Log.e(TAG, "check: ", e); - } catch (NoSuchMethodException e) { - Log.e(TAG, "check: ", e); - } catch (IllegalAccessException e) { - Log.e(TAG, "check: ", e); - } catch (InvocationTargetException e) { - Log.e(TAG, "check: ", e); - } - if (!succeed) { - closeApplication(activity); - } - } - - private static void closeApplication(Activity activity) { - ((OsmandApplication) activity.getApplication()).closeApplicationAnywayImpl(activity, true); - } -} diff --git a/OsmAnd/src/net/osmand/plus/Version.java b/OsmAnd/src/net/osmand/plus/Version.java index 14ed68b100..86d26ec954 100644 --- a/OsmAnd/src/net/osmand/plus/Version.java +++ b/OsmAnd/src/net/osmand/plus/Version.java @@ -121,8 +121,8 @@ public class Version { public static boolean isFreeVersion(OsmandApplication ctx){ return ctx.getPackageName().equals(FREE_VERSION_NAME) || ctx.getPackageName().equals(FREE_DEV_VERSION_NAME) || - ctx.getPackageName().equals(FREE_CUSTOM_VERSION_NAME) - ; + ctx.getPackageName().equals(FREE_CUSTOM_VERSION_NAME) || + isHuawei(ctx); } public static boolean isPaidVersion(OsmandApplication ctx) { diff --git a/OsmAnd/src/net/osmand/plus/activities/MapActivity.java b/OsmAnd/src/net/osmand/plus/activities/MapActivity.java index e23e596dcf..3baa33cb32 100644 --- a/OsmAnd/src/net/osmand/plus/activities/MapActivity.java +++ b/OsmAnd/src/net/osmand/plus/activities/MapActivity.java @@ -67,7 +67,6 @@ import net.osmand.plus.AppInitializer; import net.osmand.plus.AppInitializer.AppInitializeListener; import net.osmand.plus.AppInitializer.InitEvents; import net.osmand.plus.GpxSelectionHelper.GpxDisplayItem; -import net.osmand.plus.HuaweiDrmHelper; import net.osmand.plus.MapMarkersHelper.MapMarker; import net.osmand.plus.MapMarkersHelper.MapMarkerChangedListener; import net.osmand.plus.OnDismissDialogFragmentListener; @@ -276,9 +275,6 @@ public class MapActivity extends OsmandActionBarActivity implements DownloadEven super.onCreate(savedInstanceState); - if (Version.isHuawei(getMyApplication())) { - HuaweiDrmHelper.check(this); - } // Full screen is not used here // getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); setContentView(R.layout.main); @@ -1466,6 +1462,10 @@ public class MapActivity extends OsmandActionBarActivity implements DownloadEven } protected void onPostExecute(Void result) { + DashboardOnMap dashboard = getDashboard(); + if (dashboard != null) { + dashboard.onMapSettingsUpdated(); + } } }.executeOnExecutor(singleThreadExecutor, (Void) null); diff --git a/OsmAnd/src/net/osmand/plus/activities/MapActivityActions.java b/OsmAnd/src/net/osmand/plus/activities/MapActivityActions.java index dbd0f133fa..d9137ee2d6 100644 --- a/OsmAnd/src/net/osmand/plus/activities/MapActivityActions.java +++ b/OsmAnd/src/net/osmand/plus/activities/MapActivityActions.java @@ -91,6 +91,7 @@ import java.util.Map; import static net.osmand.IndexConstants.GPX_FILE_EXT; import static net.osmand.aidlapi.OsmAndCustomizationConstants.DRAWER_CONFIGURE_MAP_ID; +import static net.osmand.aidlapi.OsmAndCustomizationConstants.DRAWER_CONFIGURE_PROFILE_ID; import static net.osmand.aidlapi.OsmAndCustomizationConstants.DRAWER_CONFIGURE_SCREEN_ID; import static net.osmand.aidlapi.OsmAndCustomizationConstants.DRAWER_DASHBOARD_ID; import static net.osmand.aidlapi.OsmAndCustomizationConstants.DRAWER_DIRECTIONS_ID; @@ -104,6 +105,7 @@ import static net.osmand.aidlapi.OsmAndCustomizationConstants.DRAWER_OSMAND_LIVE import static net.osmand.aidlapi.OsmAndCustomizationConstants.DRAWER_PLUGINS_ID; import static net.osmand.aidlapi.OsmAndCustomizationConstants.DRAWER_SEARCH_ID; import static net.osmand.aidlapi.OsmAndCustomizationConstants.DRAWER_SETTINGS_ID; +import static net.osmand.aidlapi.OsmAndCustomizationConstants.DRAWER_SWITCH_PROFILE_ID; import static net.osmand.aidlapi.OsmAndCustomizationConstants.DRAWER_TRAVEL_GUIDES_ID; import static net.osmand.aidlapi.OsmAndCustomizationConstants.MAP_CONTEXT_MENU_ADD_GPX_WAYPOINT; import static net.osmand.aidlapi.OsmAndCustomizationConstants.MAP_CONTEXT_MENU_ADD_ID; @@ -145,7 +147,7 @@ public class MapActivityActions implements DialogProvider { private static final int DIALOG_RELOAD_TITLE = 103; private static final int DIALOG_SAVE_DIRECTIONS = 106; - + private static final int DRAWER_MODE_NORMAL = 0; private static final int DRAWER_MODE_SWITCH_PROFILE = 1; @@ -476,7 +478,7 @@ public class MapActivityActions implements DialogProvider { mapActivity.showQuickSearch(latitude, longitude); } else if (standardId == R.string.context_menu_item_directions_from) { //if (OsmAndLocationProvider.isLocationPermissionAvailable(mapActivity)) { - enterDirectionsFromPoint(latitude, longitude); + enterDirectionsFromPoint(latitude, longitude); //} else { // ActivityCompat.requestPermissions(mapActivity, // new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, @@ -535,17 +537,17 @@ public class MapActivityActions implements DialogProvider { } public void enterRoutePlanningModeGivenGpx(GPXFile gpxFile, LatLon from, PointDescription fromName, - boolean useIntermediatePointsByDefault, boolean showMenu) { + boolean useIntermediatePointsByDefault, boolean showMenu) { enterRoutePlanningModeGivenGpx(gpxFile, from, fromName, useIntermediatePointsByDefault, showMenu, MapRouteInfoMenu.DEFAULT_MENU_STATE); } public void enterRoutePlanningModeGivenGpx(GPXFile gpxFile, LatLon from, PointDescription fromName, - boolean useIntermediatePointsByDefault, boolean showMenu, int menuState) { + boolean useIntermediatePointsByDefault, boolean showMenu, int menuState) { enterRoutePlanningModeGivenGpx(gpxFile, null, from, fromName, useIntermediatePointsByDefault, showMenu, menuState); } public void enterRoutePlanningModeGivenGpx(GPXFile gpxFile, ApplicationMode appMode, LatLon from, PointDescription fromName, - boolean useIntermediatePointsByDefault, boolean showMenu, int menuState) { + boolean useIntermediatePointsByDefault, boolean showMenu, int menuState) { settings.USE_INTERMEDIATE_POINTS_NAVIGATION.set(useIntermediatePointsByDefault); OsmandApplication app = mapActivity.getMyApplication(); TargetPointsHelper targets = app.getTargetPointsHelper(); @@ -727,7 +729,7 @@ public class MapActivityActions implements DialogProvider { private ContextMenuAdapter createSwitchProfileOptionsMenu(final OsmandApplication app, ContextMenuAdapter optionsMenuHelper, boolean nightMode) { drawerMode = DRAWER_MODE_NORMAL; createProfilesController(app, optionsMenuHelper, nightMode, true); - + List activeModes = ApplicationMode.values(app); ApplicationMode currentMode = app.getSettings().APPLICATION_MODE.get(); @@ -759,7 +761,7 @@ public class MapActivityActions implements DialogProvider { }) .createItem()); } - + int activeColorPrimaryResId = nightMode ? R.color.active_color_primary_dark : R.color.active_color_primary_light; optionsMenuHelper.addItem(new ItemBuilder().setLayout(R.layout.profile_list_item) .setColor(activeColorPrimaryResId) @@ -778,7 +780,7 @@ public class MapActivityActions implements DialogProvider { } private ContextMenuAdapter createNormalOptionsMenu(final OsmandApplication app, ContextMenuAdapter optionsMenuHelper, boolean nightMode) { - + createProfilesController(app, optionsMenuHelper, nightMode, false); optionsMenuHelper.addItem(new ItemBuilder().setTitleId(R.string.home, mapActivity) @@ -899,7 +901,7 @@ public class MapActivityActions implements DialogProvider { } }).createItem()); - if (Version.isGooglePlayEnabled(app) || Version.isDeveloperVersion(app)) { + if (Version.isGooglePlayEnabled(app) || Version.isHuawei(app) || Version.isDeveloperVersion(app)) { optionsMenuHelper.addItem(new ItemBuilder().setTitleId(R.string.osm_live, mapActivity) .setId(DRAWER_OSMAND_LIVE_ID) .setIcon(R.drawable.ic_action_osm_live) @@ -1055,6 +1057,7 @@ public class MapActivityActions implements DialogProvider { int icArrowResId = listExpanded ? R.drawable.ic_action_arrow_drop_up : R.drawable.ic_action_arrow_drop_down; final int nextMode = listExpanded ? DRAWER_MODE_NORMAL : DRAWER_MODE_SWITCH_PROFILE; optionsMenuHelper.addItem(new ItemBuilder().setLayout(R.layout.main_menu_drawer_btn_switch_profile) + .setId(DRAWER_SWITCH_PROFILE_ID) .setIcon(currentMode.getIconRes()) .setSecondaryIcon(icArrowResId) .setColor(currentMode.getIconColorInfo().getColor(nightMode)) @@ -1070,6 +1073,7 @@ public class MapActivityActions implements DialogProvider { }) .createItem()); optionsMenuHelper.addItem(new ItemBuilder().setLayout(R.layout.main_menu_drawer_btn_configure_profile) + .setId(DRAWER_CONFIGURE_PROFILE_ID) .setColor(currentMode.getIconColorInfo().getColor(nightMode)) .setTitle(getString(R.string.configure_profile)) .setListener(new ItemClickListener() { @@ -1084,8 +1088,8 @@ public class MapActivityActions implements DialogProvider { } private String getProfileDescription(OsmandApplication app, ApplicationMode mode, - Map profilesObjects, String defaultDescription){ - String description = defaultDescription; + Map profilesObjects, String defaultDescription) { + String description = defaultDescription; String routingProfileKey = mode.getRoutingProfile(); if (!Algorithms.isEmpty(routingProfileKey)) { diff --git a/OsmAnd/src/net/osmand/plus/activities/OsmandInAppPurchaseActivity.java b/OsmAnd/src/net/osmand/plus/activities/OsmandInAppPurchaseActivity.java index cb91f7d167..47f7a17444 100644 --- a/OsmAnd/src/net/osmand/plus/activities/OsmandInAppPurchaseActivity.java +++ b/OsmAnd/src/net/osmand/plus/activities/OsmandInAppPurchaseActivity.java @@ -5,7 +5,6 @@ import android.app.Activity; import android.content.ActivityNotFoundException; import android.content.Intent; import android.net.Uri; -import android.os.Bundle; import android.widget.Toast; import androidx.annotation.NonNull; @@ -14,12 +13,15 @@ import androidx.appcompat.app.AppCompatActivity; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; +import net.osmand.AndroidUtils; import net.osmand.PlatformUtil; import net.osmand.plus.OsmandApplication; import net.osmand.plus.OsmandPlugin; import net.osmand.plus.R; import net.osmand.plus.Version; +import net.osmand.plus.download.DownloadActivity; import net.osmand.plus.inapp.InAppPurchaseHelper; +import net.osmand.plus.inapp.InAppPurchaseHelper.InAppPurchaseInitCallback; import net.osmand.plus.inapp.InAppPurchaseHelper.InAppPurchaseListener; import net.osmand.plus.inapp.InAppPurchaseHelper.InAppPurchaseTaskType; import net.osmand.plus.liveupdates.OsmLiveRestartBottomSheetDialogFragment; @@ -27,6 +29,7 @@ import net.osmand.plus.srtmplugin.SRTMPlugin; import org.apache.commons.logging.Log; +import java.lang.ref.WeakReference; import java.util.List; @SuppressLint("Registered") @@ -34,14 +37,7 @@ public class OsmandInAppPurchaseActivity extends AppCompatActivity implements In private static final Log LOG = PlatformUtil.getLog(OsmandInAppPurchaseActivity.class); private InAppPurchaseHelper purchaseHelper; - - @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - if (isInAppPurchaseAllowed() && isInAppPurchaseSupported()) { - purchaseHelper = getMyApplication().getInAppPurchaseHelper(); - } - } + private boolean activityDestroyed; @Override protected void onResume() { @@ -53,17 +49,39 @@ public class OsmandInAppPurchaseActivity extends AppCompatActivity implements In protected void onDestroy() { super.onDestroy(); deinitInAppPurchaseHelper(); + activityDestroyed = true; } private void initInAppPurchaseHelper() { deinitInAppPurchaseHelper(); - - if (purchaseHelper != null) { - purchaseHelper.setUiActivity(this); - if (purchaseHelper.needRequestInventory()) { - purchaseHelper.requestInventory(); + if (purchaseHelper == null) { + OsmandApplication app = getMyApplication(); + InAppPurchaseHelper purchaseHelper = app.getInAppPurchaseHelper(); + if (app.getSettings().isInternetConnectionAvailable() + && isInAppPurchaseAllowed() + && isInAppPurchaseSupported(purchaseHelper)) { + this.purchaseHelper = purchaseHelper; } } + if (purchaseHelper != null) { + final WeakReference activityRef = new WeakReference<>(this); + purchaseHelper.isInAppPurchaseSupported(this, new InAppPurchaseInitCallback() { + @Override + public void onSuccess() { + OsmandInAppPurchaseActivity activity = activityRef.get(); + if (!activityDestroyed && AndroidUtils.isActivityNotDestroyed(activity)) { + purchaseHelper.setUiActivity(activity); + if (purchaseHelper.needRequestInventory()) { + purchaseHelper.requestInventory(); + } + } + } + + @Override + public void onFail() { + } + }); + } } private void deinitInAppPurchaseHelper() { @@ -80,7 +98,11 @@ public class OsmandInAppPurchaseActivity extends AppCompatActivity implements In InAppPurchaseHelper purchaseHelper = app.getInAppPurchaseHelper(); if (purchaseHelper != null) { app.logEvent("in_app_purchase_redirect"); - purchaseHelper.purchaseFullVersion(activity); + try { + purchaseHelper.purchaseFullVersion(activity); + } catch (UnsupportedOperationException e) { + LOG.error("purchaseFullVersion is not supported", e); + } } } else { app.logEvent("paid_version_redirect"); @@ -101,18 +123,27 @@ public class OsmandInAppPurchaseActivity extends AppCompatActivity implements In InAppPurchaseHelper purchaseHelper = app.getInAppPurchaseHelper(); if (purchaseHelper != null) { app.logEvent("depth_contours_purchase_redirect"); - purchaseHelper.purchaseDepthContours(activity); + try { + purchaseHelper.purchaseDepthContours(activity); + } catch (UnsupportedOperationException e) { + LOG.error("purchaseDepthContours is not supported", e); + } } } } - public static void purchaseSrtmPlugin(@NonNull final Activity activity) { - OsmandPlugin plugin = OsmandPlugin.getPlugin(SRTMPlugin.class); - if(plugin == null || plugin.getInstallURL() == null) { - Toast.makeText(activity.getApplicationContext(), - activity.getString(R.string.activate_srtm_plugin), Toast.LENGTH_LONG).show(); - } else { - activity.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(plugin.getInstallURL()))); + public static void purchaseContourLines(@NonNull final Activity activity) { + OsmandApplication app = (OsmandApplication) activity.getApplication(); + if (app != null) { + InAppPurchaseHelper purchaseHelper = app.getInAppPurchaseHelper(); + if (purchaseHelper != null) { + app.logEvent("contour_lines_purchase_redirect"); + try { + purchaseHelper.purchaseContourLines(activity); + } catch (UnsupportedOperationException e) { + LOG.error("purchaseContourLines is not supported", e); + } + } } } @@ -129,8 +160,9 @@ public class OsmandInAppPurchaseActivity extends AppCompatActivity implements In return false; } - public boolean isInAppPurchaseSupported() { - return Version.isGooglePlayEnabled(getMyApplication()); + public boolean isInAppPurchaseSupported(InAppPurchaseHelper purchaseHelper) { + OsmandApplication app = getMyApplication(); + return Version.isGooglePlayEnabled(app) || Version.isHuawei(app); } @Override @@ -178,6 +210,11 @@ public class OsmandInAppPurchaseActivity extends AppCompatActivity implements In } onInAppPurchaseItemPurchased(sku); fireInAppPurchaseItemPurchasedOnFragments(fragmentManager, sku, active); + if (purchaseHelper != null && purchaseHelper.getContourLines().getSku().equals(sku)) { + if (!(this instanceof MapActivity)) { + finish(); + } + } } public void fireInAppPurchaseItemPurchasedOnFragments(@NonNull FragmentManager fragmentManager, @@ -222,6 +259,17 @@ public class OsmandInAppPurchaseActivity extends AppCompatActivity implements In } } + @Override + protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { + boolean handled = false; + if (purchaseHelper != null) { + handled = purchaseHelper.onActivityResult(this, requestCode, resultCode, data); + } + if (!handled) { + super.onActivityResult(requestCode, resultCode, data); + } + } + public void onInAppPurchaseError(InAppPurchaseTaskType taskType, String error) { // not implemented } diff --git a/OsmAnd/src/net/osmand/plus/base/MenuBottomSheetDialogFragment.java b/OsmAnd/src/net/osmand/plus/base/MenuBottomSheetDialogFragment.java index 7625e5de20..307cfb3bbe 100644 --- a/OsmAnd/src/net/osmand/plus/base/MenuBottomSheetDialogFragment.java +++ b/OsmAnd/src/net/osmand/plus/base/MenuBottomSheetDialogFragment.java @@ -8,11 +8,11 @@ import android.graphics.drawable.Drawable; import android.graphics.drawable.LayerDrawable; import android.os.Build; import android.os.Bundle; -import android.view.ContextThemeWrapper; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; +import android.view.ViewTreeObserver.OnScrollChangedListener; import android.view.Window; import android.view.WindowManager; import android.widget.LinearLayout; @@ -50,8 +50,11 @@ public abstract class MenuBottomSheetDialogFragment extends BottomSheetDialogFra protected int themeRes; protected View dismissButton; protected View rightButton; + protected View thirdButton; + private View buttonsShadow; private LinearLayout itemsContainer; + private LinearLayout buttonsContainer; @StringRes protected int dismissButtonStringRes = R.string.shared_string_cancel; @@ -74,45 +77,21 @@ public abstract class MenuBottomSheetDialogFragment extends BottomSheetDialogFra @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) { createMenuItems(savedInstanceState); - Context ctx = requireContext(); - View mainView = View.inflate(new ContextThemeWrapper(ctx, themeRes), R.layout.bottom_sheet_menu_base, null); + Activity activity = requireActivity(); + LayoutInflater themedInflater = UiUtilities.getInflater(activity, nightMode); + View mainView = themedInflater.inflate(R.layout.bottom_sheet_menu_base, null); if (useScrollableItemsContainer()) { - itemsContainer = (LinearLayout) mainView.findViewById(R.id.scrollable_items_container); + itemsContainer = mainView.findViewById(R.id.scrollable_items_container); } else { mainView.findViewById(R.id.scroll_view).setVisibility(View.GONE); - itemsContainer = (LinearLayout) mainView.findViewById(R.id.non_scrollable_items_container); + itemsContainer = mainView.findViewById(R.id.non_scrollable_items_container); itemsContainer.setVisibility(View.VISIBLE); } + buttonsShadow = mainView.findViewById(R.id.buttons_shadow); inflateMenuItems(); - - dismissButton = mainView.findViewById(R.id.dismiss_button); - UiUtilities.setupDialogButton(nightMode, dismissButton, getDismissButtonType(), getDismissButtonTextId()); - dismissButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - onDismissButtonClickAction(); - dismiss(); - } - }); - if (hideButtonsContainer()) { - mainView.findViewById(R.id.buttons_container).setVisibility(View.GONE); - } else { - int rightBottomButtonTextId = getRightBottomButtonTextId(); - if (rightBottomButtonTextId != DEFAULT_VALUE) { - mainView.findViewById(R.id.buttons_divider).setVisibility(View.VISIBLE); - rightButton = mainView.findViewById(R.id.right_bottom_button); - UiUtilities.setupDialogButton(nightMode, rightButton, getRightBottomButtonType(), rightBottomButtonTextId); - rightButton.setVisibility(View.VISIBLE); - rightButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - onRightBottomButtonClick(); - } - }); - } - } - updateBottomButtons(); + setupScrollShadow(mainView); + setupBottomButtons((ViewGroup) mainView); setupHeightAndBackground(mainView); return mainView; } @@ -199,7 +178,7 @@ public abstract class MenuBottomSheetDialogFragment extends BottomSheetDialogFra if (contentView.getHeight() > contentHeight) { if (useScrollableItemsContainer() || useExpandableList()) { contentView.getLayoutParams().height = contentHeight; - mainView.findViewById(R.id.buttons_shadow).setVisibility(View.VISIBLE); + buttonsShadow.setVisibility(View.VISIBLE); } else { contentView.getLayoutParams().height = ViewGroup.LayoutParams.WRAP_CONTENT; } @@ -222,7 +201,18 @@ public abstract class MenuBottomSheetDialogFragment extends BottomSheetDialogFra private int getContentHeight(int availableScreenHeight) { int customHeight = getCustomHeight(); - int maxHeight = availableScreenHeight - getResources().getDimensionPixelSize(R.dimen.dialog_button_ex_height); + int buttonsHeight; + if (useVerticalButtons()) { + int padding = getResources().getDimensionPixelSize(R.dimen.content_padding_small); + int buttonHeight = getResources().getDimensionPixelSize(R.dimen.dialog_button_height); + buttonsHeight = (buttonHeight + padding) * 2 + getFirstDividerHeight(); + if (getThirdBottomButtonTextId() != DEFAULT_VALUE) { + buttonsHeight += buttonHeight + getSecondDividerHeight(); + } + } else { + buttonsHeight = getResources().getDimensionPixelSize(R.dimen.dialog_button_ex_height); + } + int maxHeight = availableScreenHeight - buttonsHeight; if (customHeight != DEFAULT_VALUE && customHeight <= maxHeight) { return customHeight; } @@ -280,6 +270,18 @@ public abstract class MenuBottomSheetDialogFragment extends BottomSheetDialogFra } + protected int getThirdBottomButtonTextId() { + return DEFAULT_VALUE; + } + + protected DialogButtonType getThirdBottomButtonType() { + return DialogButtonType.PRIMARY; + } + + protected void onThirdBottomButtonClick() { + + } + protected boolean isDismissButtonEnabled() { return true; } @@ -288,6 +290,42 @@ public abstract class MenuBottomSheetDialogFragment extends BottomSheetDialogFra return true; } + protected void setupBottomButtons(ViewGroup view) { + Activity activity = requireActivity(); + LayoutInflater themedInflater = UiUtilities.getInflater(activity, nightMode); + if (!hideButtonsContainer()) { + if (useVerticalButtons()) { + buttonsContainer = (LinearLayout) themedInflater.inflate(R.layout.bottom_buttons_vertical, view); + setupThirdButton(); + } else { + buttonsContainer = (LinearLayout) themedInflater.inflate(R.layout.bottom_buttons, view); + } + setupRightButton(); + setupDismissButton(); + updateBottomButtons(); + } + } + + boolean useVerticalButtons() { + Activity activity = requireActivity(); + int rightBottomButtonTextId = getRightBottomButtonTextId(); + if (getDismissButtonTextId() != DEFAULT_VALUE && rightBottomButtonTextId != DEFAULT_VALUE) { + if (getThirdBottomButtonTextId() != DEFAULT_VALUE) { + return true; + } + String rightButtonText = getString(rightBottomButtonTextId); + boolean portrait = AndroidUiHelper.isOrientationPortrait(activity); + int outerPadding = getResources().getDimensionPixelSize(R.dimen.content_padding); + int innerPadding = getResources().getDimensionPixelSize(R.dimen.content_padding_small); + int dialogWidth = portrait ? AndroidUtils.getScreenWidth(activity) : getResources().getDimensionPixelSize(R.dimen.landscape_bottom_sheet_dialog_fragment_width); + int availableTextWidth = (dialogWidth - (outerPadding * 3 + innerPadding * 4)) / 2; + + int measuredTextWidth = AndroidUtils.getTextWidth(getResources().getDimensionPixelSize(R.dimen.default_desc_text_size), rightButtonText); + return measuredTextWidth > availableTextWidth; + } + return false; + } + protected void updateBottomButtons() { if (dismissButton != null) { boolean enabled = isDismissButtonEnabled(); @@ -301,6 +339,66 @@ public abstract class MenuBottomSheetDialogFragment extends BottomSheetDialogFra } } + private void setupDismissButton() { + dismissButton = buttonsContainer.findViewById(R.id.dismiss_button); + int buttonTextId = getDismissButtonTextId(); + if (buttonTextId != DEFAULT_VALUE) { + UiUtilities.setupDialogButton(nightMode, dismissButton, getDismissButtonType(), buttonTextId); + dismissButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + onDismissButtonClickAction(); + dismiss(); + } + }); + } + AndroidUiHelper.updateVisibility(dismissButton, buttonTextId != DEFAULT_VALUE); + } + + private void setupRightButton() { + rightButton = buttonsContainer.findViewById(R.id.right_bottom_button); + int buttonTextId = getRightBottomButtonTextId(); + if (buttonTextId != DEFAULT_VALUE) { + UiUtilities.setupDialogButton(nightMode, rightButton, getRightBottomButtonType(), buttonTextId); + rightButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + onRightBottomButtonClick(); + } + }); + } + View divider = buttonsContainer.findViewById(R.id.buttons_divider); + divider.getLayoutParams().height = getFirstDividerHeight(); + AndroidUiHelper.updateVisibility(rightButton, buttonTextId != DEFAULT_VALUE); + AndroidUiHelper.updateVisibility(divider, buttonTextId != DEFAULT_VALUE); + } + + protected int getFirstDividerHeight() { + return getResources().getDimensionPixelSize(R.dimen.content_padding); + } + + private void setupThirdButton() { + thirdButton = buttonsContainer.findViewById(R.id.third_button); + int buttonTextId = getThirdBottomButtonTextId(); + if (buttonTextId != DEFAULT_VALUE) { + UiUtilities.setupDialogButton(nightMode, thirdButton, getThirdBottomButtonType(), buttonTextId); + thirdButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + onThirdBottomButtonClick(); + } + }); + } + View divider = buttonsContainer.findViewById(R.id.buttons_divider_top); + divider.getLayoutParams().height = getSecondDividerHeight(); + AndroidUiHelper.updateVisibility(thirdButton, buttonTextId != DEFAULT_VALUE); + AndroidUiHelper.updateVisibility(divider, buttonTextId != DEFAULT_VALUE); + } + + protected int getSecondDividerHeight() { + return getResources().getDimensionPixelSize(R.dimen.content_padding); + } + @ColorRes protected int getBgColorId() { return nightMode ? R.color.list_background_color_dark : R.color.list_background_color_light; @@ -325,7 +423,7 @@ public abstract class MenuBottomSheetDialogFragment extends BottomSheetDialogFra private LayerDrawable createBackgroundDrawable(@NonNull Context ctx, @DrawableRes int shadowDrawableResId) { Drawable shadowDrawable = ContextCompat.getDrawable(ctx, shadowDrawableResId); - Drawable[] layers = new Drawable[]{shadowDrawable, getColoredBg(ctx)}; + Drawable[] layers = new Drawable[] {shadowDrawable, getColoredBg(ctx)}; return new LayerDrawable(layers); } @@ -335,4 +433,21 @@ public abstract class MenuBottomSheetDialogFragment extends BottomSheetDialogFra } return !app.getSettings().isLightContent(); } -} + + private void setupScrollShadow(View view) { + final View scrollView; + if (useScrollableItemsContainer()) { + scrollView = view.findViewById(R.id.scroll_view); + } else { + scrollView = itemsContainer; + } + scrollView.getViewTreeObserver().addOnScrollChangedListener(new OnScrollChangedListener() { + + @Override + public void onScrollChanged() { + boolean scrollToBottomAvailable = scrollView.canScrollVertically(1); + AndroidUiHelper.updateVisibility(buttonsShadow, scrollToBottomAvailable); + } + }); + } +} \ No newline at end of file diff --git a/OsmAnd/src/net/osmand/plus/chooseplan/ChoosePlanDialogFragment.java b/OsmAnd/src/net/osmand/plus/chooseplan/ChoosePlanDialogFragment.java index 1e3b650a8f..9a7215867f 100644 --- a/OsmAnd/src/net/osmand/plus/chooseplan/ChoosePlanDialogFragment.java +++ b/OsmAnd/src/net/osmand/plus/chooseplan/ChoosePlanDialogFragment.java @@ -215,6 +215,9 @@ public abstract class ChoosePlanDialogFragment extends BaseOsmAndDialogFragment if (!TextUtils.isEmpty(getInfoDescription())) { infoDescription.setText(getInfoDescription()); } + TextViewEx planInfoDescription = (TextViewEx) view.findViewById(R.id.plan_info_description); + planInfoDescription.setText(Version.isHuawei(app) + ? R.string.osm_live_payment_subscription_management_hw : R.string.osm_live_payment_subscription_management); ViewGroup osmLiveCard = buildOsmLiveCard(ctx, cardsContainer); if (osmLiveCard != null) { cardsContainer.addView(osmLiveCard); @@ -428,7 +431,7 @@ public abstract class ChoosePlanDialogFragment extends BaseOsmAndDialogFragment buttonCancelView.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { - manageSubscription(ctx, s.getSku()); + purchaseHelper.manageSubscription(ctx, s.getSku()); } }); div.setVisibility(View.VISIBLE); @@ -538,15 +541,6 @@ public abstract class ChoosePlanDialogFragment extends BaseOsmAndDialogFragment } } - private void manageSubscription(@NonNull Context ctx, @Nullable String sku) { - String url = "https://play.google.com/store/account/subscriptions?package=" + ctx.getPackageName(); - if (!Algorithms.isEmpty(sku)) { - url += "&sku=" + sku; - } - Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); - startActivity(intent); - } - private ViewGroup buildPlanTypeCard(@NonNull Context ctx, ViewGroup container) { if (getPlanTypeFeatures().length == 0) { return null; diff --git a/OsmAnd/src/net/osmand/plus/chooseplan/ChoosePlanHillshadeSrtmDialogFragment.java b/OsmAnd/src/net/osmand/plus/chooseplan/ChoosePlanHillshadeSrtmDialogFragment.java index b2858b53b8..d3a48a5a15 100644 --- a/OsmAnd/src/net/osmand/plus/chooseplan/ChoosePlanHillshadeSrtmDialogFragment.java +++ b/OsmAnd/src/net/osmand/plus/chooseplan/ChoosePlanHillshadeSrtmDialogFragment.java @@ -79,7 +79,7 @@ public class ChoosePlanHillshadeSrtmDialogFragment extends ChoosePlanDialogFragm public void onClick(View v) { Activity activity = getActivity(); if (activity != null) { - OsmandInAppPurchaseActivity.purchaseSrtmPlugin(activity); + OsmandInAppPurchaseActivity.purchaseContourLines(activity); dismiss(); } } diff --git a/OsmAnd/src/net/osmand/plus/chooseplan/OsmLiveCancelledDialog.java b/OsmAnd/src/net/osmand/plus/chooseplan/OsmLiveCancelledDialog.java index 788b605eec..43d95c266c 100644 --- a/OsmAnd/src/net/osmand/plus/chooseplan/OsmLiveCancelledDialog.java +++ b/OsmAnd/src/net/osmand/plus/chooseplan/OsmLiveCancelledDialog.java @@ -23,6 +23,7 @@ import androidx.fragment.app.FragmentManager; import net.osmand.PlatformUtil; import net.osmand.plus.OsmandApplication; +import net.osmand.plus.Version; import net.osmand.plus.settings.backend.OsmandSettings; import net.osmand.plus.settings.backend.OsmandSettings.OsmandPreference; import net.osmand.plus.R; @@ -110,6 +111,8 @@ public class OsmLiveCancelledDialog extends BaseOsmAndDialogFragment implements descr.append("\n").append("— ").append(feature.toHumanString(ctx)); } infoDescr.setText(descr); + TextViewEx inappDescr = (TextViewEx) view.findViewById(R.id.inapp_descr); + inappDescr.setText(Version.isHuawei(app) ? R.string.osm_live_payment_desc_hw : R.string.osm_live_payment_desc); osmLiveButton = view.findViewById(R.id.card_button); diff --git a/OsmAnd/src/net/osmand/plus/dashboard/DashboardOnMap.java b/OsmAnd/src/net/osmand/plus/dashboard/DashboardOnMap.java index 4c9b2a2c2b..bb2143c7eb 100644 --- a/OsmAnd/src/net/osmand/plus/dashboard/DashboardOnMap.java +++ b/OsmAnd/src/net/osmand/plus/dashboard/DashboardOnMap.java @@ -568,7 +568,7 @@ public class DashboardOnMap implements ObservableScrollViewCallbacks, IRouteInfo boolean refresh = this.visibleType == type && !appModeChanged; previousAppMode = currentAppMode; - this.visibleType = type; + visibleType = type; DashboardOnMap.staticVisible = visible; DashboardOnMap.staticVisibleType = type; mapActivity.enableDrawer(); @@ -1032,6 +1032,24 @@ public class DashboardOnMap implements ObservableScrollViewCallbacks, IRouteInfo } } + public void onMapSettingsUpdated() { + if (DashboardType.CONFIGURE_MAP.equals(visibleType)) { + updateMenuItems(); + } + } + + public void updateMenuItems() { + if (listAdapter != null) { + for (int i = 0; i < listAdapter.getCount(); i++) { + Object o = listAdapter.getItem(i); + if (o instanceof ContextMenuItem) { + ((ContextMenuItem) o).update(); + } + } + listAdapter.notifyDataSetChanged(); + } + } + public void updateLocation(final boolean centerChanged, final boolean locationChanged, final boolean compassChanged) { if (inLocationUpdate) { diff --git a/OsmAnd/src/net/osmand/plus/dialogs/ConfigureMapMenu.java b/OsmAnd/src/net/osmand/plus/dialogs/ConfigureMapMenu.java index 647eebeb4c..83c4e1942d 100644 --- a/OsmAnd/src/net/osmand/plus/dialogs/ConfigureMapMenu.java +++ b/OsmAnd/src/net/osmand/plus/dialogs/ConfigureMapMenu.java @@ -277,10 +277,12 @@ public class ConfigureMapMenu { adapter.addItem(new ContextMenuItem.ItemBuilder().setTitleId(R.string.map_widget_map_rendering, activity) .setId(MAP_RENDERING_CATEGORY_ID) .setCategory(true).setLayout(R.layout.list_group_title_with_switch).createItem()); - adapter.addItem(new ContextMenuItem.ItemBuilder().setTitleId(R.string.map_widget_renderer, activity) + adapter.addItem(new ContextMenuItem.ItemBuilder() .setId(MAP_STYLE_ID) - .setDescription(getRenderDescr(activity)).setLayout(R.layout.list_item_single_line_descrition_narrow) - .setIcon(R.drawable.ic_map).setListener(new ContextMenuAdapter.ItemClickListener() { + .setTitleId(R.string.map_widget_renderer, activity) + .setLayout(R.layout.list_item_single_line_descrition_narrow) + .setIcon(R.drawable.ic_map) + .setListener(new ContextMenuAdapter.ItemClickListener() { @Override public boolean onContextMenuClick(final ArrayAdapter ad, int itemId, final int pos, boolean isChecked, int[] viewCoordinates) { @@ -290,6 +292,13 @@ public class ConfigureMapMenu { } }) .setItemDeleteAction(makeDeleteAction(settings.RENDERER)) + .setOnUpdateCallback(new ContextMenuItem.OnUpdateCallback() { + @Override + public void onUpdateMenuItem(ContextMenuItem item) { + String renderDescr = getRenderDescr(app); + item.setDescription(renderDescr); + } + }) .createItem()); String description = ""; @@ -942,13 +951,13 @@ public class ConfigureMapMenu { dialog.show(); } - protected String getRenderDescr(final MapActivity activity) { - RendererRegistry rr = activity.getMyApplication().getRendererRegistry(); + protected String getRenderDescr(OsmandApplication app) { + RendererRegistry rr = app.getRendererRegistry(); RenderingRulesStorage storage = rr.getCurrentSelectedRenderer(); if (storage == null) { return ""; } - String translation = RendererRegistry.getTranslatedRendererName(activity, storage.getName()); + String translation = RendererRegistry.getTranslatedRendererName(app, storage.getName()); return translation == null ? storage.getName() : translation; } diff --git a/OsmAnd/src/net/osmand/plus/dialogs/GpxAppearanceAdapter.java b/OsmAnd/src/net/osmand/plus/dialogs/GpxAppearanceAdapter.java index 1a1e873b95..87af46891a 100644 --- a/OsmAnd/src/net/osmand/plus/dialogs/GpxAppearanceAdapter.java +++ b/OsmAnd/src/net/osmand/plus/dialogs/GpxAppearanceAdapter.java @@ -25,10 +25,13 @@ public class GpxAppearanceAdapter extends ArrayAdapter getAppearanceItems(OsmandApplication app, GpxAppearanceAdapterType adapterType) { + return getAppearanceItems(app, adapterType, false); + } + + public static List getAppearanceItems(OsmandApplication app, GpxAppearanceAdapterType adapterType, + boolean showStartFinishIcons) { List items = new ArrayList<>(); RenderingRuleProperty trackWidthProp = null; RenderingRuleProperty trackColorProp = null; @@ -118,11 +126,19 @@ public class GpxAppearanceAdapter extends ArrayAdapter 0) { for (Map.Entry entry : gpxAppearanceParams.entrySet()) { - final OsmandSettings.CommonPreference pref - = app.getSettings().getCustomRenderProperty(entry.getKey()); - pref.set(entry.getValue()); + if (SHOW_START_FINISH_ATTR.equals(entry.getKey())) { + app.getSettings().SHOW_START_FINISH_ICONS.set("true".equals(entry.getValue())); + } else { + final OsmandSettings.CommonPreference pref + = app.getSettings().getCustomRenderProperty(entry.getKey()); + pref.set(entry.getValue()); + } } if (activity instanceof MapActivity) { ConfigureMapMenu.refreshMapComplete((MapActivity) activity); diff --git a/OsmAnd/src/net/osmand/plus/helpers/ImportHelper.java b/OsmAnd/src/net/osmand/plus/helpers/ImportHelper.java index c57f53a0fa..686a56233f 100644 --- a/OsmAnd/src/net/osmand/plus/helpers/ImportHelper.java +++ b/OsmAnd/src/net/osmand/plus/helpers/ImportHelper.java @@ -21,6 +21,7 @@ import androidx.fragment.app.FragmentManager; import net.osmand.AndroidUtils; import net.osmand.CallbackWithObject; +import net.osmand.FileUtils; import net.osmand.GPXUtilities; import net.osmand.GPXUtilities.GPXFile; import net.osmand.GPXUtilities.WptPt; @@ -29,6 +30,7 @@ import net.osmand.IndexConstants; import net.osmand.PlatformUtil; import net.osmand.data.FavouritePoint; import net.osmand.data.FavouritePoint.BackgroundType; +import net.osmand.map.ITileSource; import net.osmand.plus.AppInitializer; import net.osmand.plus.CustomOsmandPlugin; import net.osmand.plus.FavouritesDbHelper; @@ -41,7 +43,11 @@ import net.osmand.plus.activities.MapActivity; import net.osmand.plus.activities.TrackActivity; import net.osmand.plus.dialogs.ImportGpxBottomSheetDialogFragment; import net.osmand.plus.measurementtool.MeasurementToolFragment; +import net.osmand.plus.poi.PoiUIFilter; +import net.osmand.plus.quickaction.QuickAction; import net.osmand.plus.rastermaps.OsmandRasterMapsPlugin; +import net.osmand.plus.settings.backend.ApplicationMode; +import net.osmand.plus.settings.backend.ExportSettingsType; import net.osmand.plus.settings.backend.SettingsHelper; import net.osmand.plus.settings.backend.SettingsHelper.CheckDuplicatesListener; import net.osmand.plus.settings.backend.SettingsHelper.PluginSettingsItem; @@ -49,6 +55,7 @@ import net.osmand.plus.settings.backend.SettingsHelper.ProfileSettingsItem; import net.osmand.plus.settings.backend.SettingsHelper.SettingsCollectListener; import net.osmand.plus.settings.backend.SettingsHelper.SettingsImportListener; import net.osmand.plus.settings.backend.SettingsHelper.SettingsItem; +import net.osmand.plus.settings.fragments.ImportCompleteFragment; import net.osmand.plus.settings.fragments.ImportSettingsFragment; import net.osmand.plus.views.OsmandMapTileView; import net.osmand.router.RoutingConfiguration; @@ -69,8 +76,10 @@ import java.io.UnsupportedEncodingException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; +import java.util.HashMap; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.zip.ZipInputStream; import static android.app.Activity.RESULT_OK; @@ -86,6 +95,7 @@ import static net.osmand.plus.AppInitializer.loadRoutingFiles; import static net.osmand.plus.myplaces.FavoritesActivity.FAV_TAB; import static net.osmand.plus.myplaces.FavoritesActivity.GPX_TAB; import static net.osmand.plus.myplaces.FavoritesActivity.TAB_ID; +import static net.osmand.plus.settings.backend.SettingsHelper.*; /** * @author Koen Rabaey @@ -690,17 +700,29 @@ public class ImportHelper { } private void handleOsmAndSettingsImport(Uri intentUri, String fileName, Bundle extras, CallbackWithObject> callback) { - if (extras != null && extras.containsKey(SettingsHelper.SETTINGS_VERSION_KEY) && extras.containsKey(SettingsHelper.SETTINGS_LATEST_CHANGES_KEY)) { - int version = extras.getInt(SettingsHelper.SETTINGS_VERSION_KEY, -1); - String latestChanges = extras.getString(SettingsHelper.SETTINGS_LATEST_CHANGES_KEY); - handleOsmAndSettingsImport(intentUri, fileName, latestChanges, version, callback); + if (extras != null && extras.containsKey(SETTINGS_VERSION_KEY) + && extras.containsKey(SETTINGS_LATEST_CHANGES_KEY)) { + int version = extras.getInt(SETTINGS_VERSION_KEY, -1); + String latestChanges = extras.getString(SETTINGS_LATEST_CHANGES_KEY); + boolean replace = extras.getBoolean(REPLACE_KEY); + ArrayList settingsTypeKeys = extras.getStringArrayList(SETTINGS_TYPE_LIST_KEY); + List settingsTypes = new ArrayList<>(); + if (settingsTypeKeys != null) { + for (String key : settingsTypeKeys) { + settingsTypes.add(ExportSettingsType.valueOf(key)); + } + } + handleOsmAndSettingsImport(intentUri, fileName, settingsTypes, replace, latestChanges, version, callback); } else { - handleOsmAndSettingsImport(intentUri, fileName, null, -1, callback); + handleOsmAndSettingsImport(intentUri, fileName, null, false, null, -1, callback); } } @SuppressLint("StaticFieldLeak") - private void handleOsmAndSettingsImport(final Uri uri, final String name, final String latestChanges, final int version, + private void handleOsmAndSettingsImport(final Uri uri, final String name, + final List settingsTypes, + final boolean replace, + final String latestChanges, final int version, final CallbackWithObject> callback) { final AsyncTask settingsImportTask = new AsyncTask() { @@ -715,20 +737,18 @@ public class ImportHelper { @Override protected String doInBackground(Void... voids) { - File tempDir = app.getAppPath(IndexConstants.TEMP_DIR); - if (!tempDir.exists()) { - tempDir.mkdirs(); - } + File tempDir = FileUtils.getTempDir(app); File dest = new File(tempDir, name); return copyFile(app, dest, uri, true); } @Override protected void onPostExecute(String error) { - File tempDir = app.getAppPath(IndexConstants.TEMP_DIR); + File tempDir = FileUtils.getTempDir(app); final File file = new File(tempDir, name); if (error == null && file.exists()) { - app.getSettingsHelper().collectSettings(file, latestChanges, version, new SettingsCollectListener() { + final SettingsHelper settingsHelper = app.getSettingsHelper(); + settingsHelper.collectSettings(file, latestChanges, version, new SettingsCollectListener() { @Override public void onSettingsCollectFinished(boolean succeed, boolean empty, @NonNull List items) { if (progress != null && AndroidUtils.isActivityNotDestroyed(activity)) { @@ -748,8 +768,14 @@ public class ImportHelper { handlePluginImport(pluginItem, file); } if (!pluginIndependentItems.isEmpty()) { - FragmentManager fragmentManager = activity.getSupportFragmentManager(); - ImportSettingsFragment.showInstance(fragmentManager, pluginIndependentItems, file); + if (settingsTypes == null) { + FragmentManager fragmentManager = activity.getSupportFragmentManager(); + ImportSettingsFragment.showInstance(fragmentManager, pluginIndependentItems, file); + } else { + Map> allSettingsList = getSettingsToOperate(pluginIndependentItems, false); + List settingsList = settingsHelper.getFilteredSettingsItems(allSettingsList, settingsTypes); + settingsHelper.checkDuplicates(file, settingsList, settingsList, getDuplicatesListener(file, replace)); + } } } else if (empty) { app.showShortToastMessage(app.getString(R.string.file_import_error, name, app.getString(R.string.shared_string_unexpected_error))); @@ -767,6 +793,120 @@ public class ImportHelper { executeImportTask(settingsImportTask); } + private CheckDuplicatesListener getDuplicatesListener(final File file, final boolean replace) { + return new CheckDuplicatesListener() { + @Override + public void onDuplicatesChecked(@NonNull List duplicates, List items) { + if (replace) { + for (SettingsItem item : items) { + item.setShouldReplace(true); + } + } + app.getSettingsHelper().importSettings(file, items, "", 1, getImportListener(file)); + } + }; + } + + private SettingsImportListener getImportListener(final File file) { + return new SettingsImportListener() { + @Override + public void onSettingsImportFinished(boolean succeed, @NonNull List items) { + MapActivity mapActivity = getMapActivity(); + if (mapActivity != null && succeed) { + FragmentManager fm = mapActivity.getSupportFragmentManager(); + app.getRendererRegistry().updateExternalRenderers(); + AppInitializer.loadRoutingFiles(app, null); + if (file != null) { + ImportCompleteFragment.showInstance(fm, items, file.getName()); + } + } + } + }; + } + + public static Map> getSettingsToOperate(List settingsItems, boolean importComplete) { + Map> settingsToOperate = new HashMap<>(); + List profiles = new ArrayList<>(); + List quickActions = new ArrayList<>(); + List poiUIFilters = new ArrayList<>(); + List tileSourceTemplates = new ArrayList<>(); + List routingFilesList = new ArrayList<>(); + List renderFilesList = new ArrayList<>(); + List avoidRoads = new ArrayList<>(); + for (SettingsItem item : settingsItems) { + switch (item.getType()) { + case PROFILE: + profiles.add(((ProfileSettingsItem) item).getModeBean()); + break; + case FILE: + FileSettingsItem fileItem = (FileSettingsItem) item; + if (fileItem.getSubtype() == FileSettingsItem.FileSubtype.RENDERING_STYLE) { + renderFilesList.add(fileItem.getFile()); + } else if (fileItem.getSubtype() == FileSettingsItem.FileSubtype.ROUTING_CONFIG) { + routingFilesList.add(fileItem.getFile()); + } + break; + case QUICK_ACTIONS: + QuickActionsSettingsItem quickActionsItem = (QuickActionsSettingsItem) item; + if (importComplete) { + quickActions.addAll(quickActionsItem.getAppliedItems()); + } else { + quickActions.addAll(quickActionsItem.getItems()); + } + break; + case POI_UI_FILTERS: + PoiUiFiltersSettingsItem poiUiFilterItem = (PoiUiFiltersSettingsItem) item; + if (importComplete) { + poiUIFilters.addAll(poiUiFilterItem.getAppliedItems()); + } else { + poiUIFilters.addAll(poiUiFilterItem.getItems()); + } + break; + case MAP_SOURCES: + MapSourcesSettingsItem mapSourcesItem = (MapSourcesSettingsItem) item; + if (importComplete) { + tileSourceTemplates.addAll(mapSourcesItem.getAppliedItems()); + } else { + tileSourceTemplates.addAll(mapSourcesItem.getItems()); + } + break; + case AVOID_ROADS: + AvoidRoadsSettingsItem avoidRoadsItem = (AvoidRoadsSettingsItem) item; + if (importComplete) { + avoidRoads.addAll(avoidRoadsItem.getAppliedItems()); + } else { + avoidRoads.addAll(avoidRoadsItem.getItems()); + } + break; + default: + break; + } + } + + if (!profiles.isEmpty()) { + settingsToOperate.put(ExportSettingsType.PROFILE, profiles); + } + if (!quickActions.isEmpty()) { + settingsToOperate.put(ExportSettingsType.QUICK_ACTIONS, quickActions); + } + if (!poiUIFilters.isEmpty()) { + settingsToOperate.put(ExportSettingsType.POI_TYPES, poiUIFilters); + } + if (!tileSourceTemplates.isEmpty()) { + settingsToOperate.put(ExportSettingsType.MAP_SOURCES, tileSourceTemplates); + } + if (!renderFilesList.isEmpty()) { + settingsToOperate.put(ExportSettingsType.CUSTOM_RENDER_STYLE, renderFilesList); + } + if (!routingFilesList.isEmpty()) { + settingsToOperate.put(ExportSettingsType.CUSTOM_ROUTING, routingFilesList); + } + if (!avoidRoads.isEmpty()) { + settingsToOperate.put(ExportSettingsType.AVOID_ROADS, avoidRoads); + } + return settingsToOperate; + } + private void handlePluginImport(final PluginSettingsItem pluginItem, final File file) { final ProgressDialog progress = new ProgressDialog(activity); progress.setTitle(app.getString(R.string.loading_smth, "")); diff --git a/OsmAnd/src/net/osmand/plus/inapp/InAppPurchaseHelper.java b/OsmAnd/src/net/osmand/plus/inapp/InAppPurchaseHelper.java index ac7e2c77fb..1364d7b154 100644 --- a/OsmAnd/src/net/osmand/plus/inapp/InAppPurchaseHelper.java +++ b/OsmAnd/src/net/osmand/plus/inapp/InAppPurchaseHelper.java @@ -2,6 +2,8 @@ package net.osmand.plus.inapp; import android.annotation.SuppressLint; import android.app.Activity; +import android.content.Context; +import android.content.Intent; import android.os.AsyncTask; import android.text.TextUtils; import android.util.Log; @@ -9,33 +11,21 @@ import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import com.android.billingclient.api.BillingClient.BillingResponseCode; -import com.android.billingclient.api.BillingClient.SkuType; -import com.android.billingclient.api.BillingResult; -import com.android.billingclient.api.Purchase; -import com.android.billingclient.api.SkuDetails; -import com.android.billingclient.api.SkuDetailsResponseListener; - import net.osmand.AndroidNetworkUtils; import net.osmand.AndroidNetworkUtils.OnRequestResultListener; import net.osmand.AndroidNetworkUtils.OnRequestsResultListener; import net.osmand.AndroidNetworkUtils.RequestResponse; import net.osmand.PlatformUtil; import net.osmand.plus.OsmandApplication; -import net.osmand.plus.settings.backend.OsmandSettings; -import net.osmand.plus.settings.backend.OsmandSettings.OsmandPreference; import net.osmand.plus.R; import net.osmand.plus.Version; import net.osmand.plus.inapp.InAppPurchases.InAppPurchase; import net.osmand.plus.inapp.InAppPurchases.InAppPurchase.PurchaseState; -import net.osmand.plus.inapp.InAppPurchases.InAppPurchaseLiveUpdatesOldSubscription; import net.osmand.plus.inapp.InAppPurchases.InAppSubscription; -import net.osmand.plus.inapp.InAppPurchases.InAppSubscriptionIntroductoryInfo; import net.osmand.plus.inapp.InAppPurchases.InAppSubscriptionList; -import net.osmand.plus.inapp.util.BillingManager; -import net.osmand.plus.inapp.util.BillingManager.BillingUpdatesListener; import net.osmand.plus.liveupdates.CountrySelectionFragment; import net.osmand.plus.liveupdates.CountrySelectionFragment.CountryItem; +import net.osmand.plus.settings.backend.OsmandSettings; import net.osmand.util.Algorithms; import org.json.JSONArray; @@ -43,7 +33,6 @@ import org.json.JSONException; import org.json.JSONObject; import java.lang.ref.WeakReference; -import java.text.ParseException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -53,54 +42,31 @@ import java.util.List; import java.util.Map; import java.util.Set; -public class InAppPurchaseHelper { +public abstract class InAppPurchaseHelper { // Debug tag, for logging - private static final org.apache.commons.logging.Log LOG = PlatformUtil.getLog(InAppPurchaseHelper.class); + protected static final org.apache.commons.logging.Log LOG = PlatformUtil.getLog(InAppPurchaseHelper.class); private static final String TAG = InAppPurchaseHelper.class.getSimpleName(); private boolean mDebugLog = false; public static final long SUBSCRIPTION_HOLDING_TIME_MSEC = 1000 * 60 * 60 * 24 * 3; // 3 days - private InAppPurchases purchases; - private long lastValidationCheckTime; - private boolean inventoryRequested; + protected InAppPurchases purchases; + protected long lastValidationCheckTime; + protected boolean inventoryRequested; private static final long PURCHASE_VALIDATION_PERIOD_MSEC = 1000 * 60 * 60 * 24; // daily - // (arbitrary) request code for the purchase flow - private static final int RC_REQUEST = 10001; - // The helper object - private BillingManager billingManager; - private List skuDetailsList; + protected boolean isDeveloperVersion; + protected String token = ""; + protected InAppPurchaseTaskType activeTask; + protected boolean processingTask = false; + protected boolean inventoryRequestPending = false; - private boolean isDeveloperVersion; - private String token = ""; - private InAppPurchaseTaskType activeTask; - private boolean processingTask = false; - private boolean inventoryRequestPending = false; - - private OsmandApplication ctx; - private InAppPurchaseListener uiActivity = null; - - /* base64EncodedPublicKey should be YOUR APPLICATION'S PUBLIC KEY - * (that you got from the Google Play developer console). This is not your - * developer public key, it's the *app-specific* public key. - * - * Instead of just storing the entire literal string here embedded in the - * program, construct the key at runtime from pieces or - * use bit manipulation (for example, XOR with some other string) to hide - * the actual key. The key itself is not secret information, but we don't - * want to make it easy for an attacker to replace the public key with one - * of their own and then fake messages from the server. - */ - private static final String BASE64_ENCODED_PUBLIC_KEY = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAgk8cEx" + - "UO4mfEwWFLkQnX1Tkzehr4SnXLXcm2Osxs5FTJPEgyTckTh0POKVMrxeGLn0KoTY2NTgp1U/inp" + - "wccWisPhVPEmw9bAVvWsOkzlyg1kv03fJdnAXRBSqDDPV6X8Z3MtkPVqZkupBsxyIllEILKHK06" + - "OCw49JLTsMR3oTRifGzma79I71X0spw0fM+cIRlkS2tsXN8GPbdkJwHofZKPOXS51pgC1zU8uWX" + - "I+ftJO46a1XkNh1dO2anUiQ8P/H4yOTqnMsXF7biyYuiwjXPOcy0OMhEHi54Dq6Mr3u5ZALOAkc" + - "YTjh1H/ZgqIHy5ZluahINuDE76qdLYMXrDMQIDAQAB"; + protected OsmandApplication ctx; + protected InAppPurchaseListener uiActivity = null; public interface InAppPurchaseListener { + void onError(InAppPurchaseTaskType taskType, String error); void onGetItems(); @@ -112,16 +78,62 @@ public class InAppPurchaseHelper { void dismissProgress(InAppPurchaseTaskType taskType); } + public interface InAppPurchaseInitCallback { + + void onSuccess(); + + void onFail(); + } + public enum InAppPurchaseTaskType { REQUEST_INVENTORY, PURCHASE_FULL_VERSION, PURCHASE_LIVE_UPDATES, - PURCHASE_DEPTH_CONTOURS + PURCHASE_DEPTH_CONTOURS, + PURCHASE_CONTOUR_LINES } - public interface InAppRunnable { + public abstract class InAppCommand { + + InAppCommandResultHandler resultHandler; + // return true if done and false if async task started - boolean run(InAppPurchaseHelper helper); + abstract void run(InAppPurchaseHelper helper); + + protected void commandDone() { + InAppCommandResultHandler resultHandler = this.resultHandler; + if (resultHandler != null) { + resultHandler.onCommandDone(this); + } + } + } + + public interface InAppCommandResultHandler { + void onCommandDone(@NonNull InAppCommand command); + } + + public static class PurchaseInfo { + private String sku; + private String orderId; + private String purchaseToken; + + public PurchaseInfo(String sku, String orderId, String purchaseToken) { + this.sku = sku; + this.orderId = orderId; + this.purchaseToken = purchaseToken; + } + + public String getSku() { + return sku; + } + + public String getOrderId() { + return orderId; + } + + public String getPurchaseToken() { + return purchaseToken; + } } public String getToken() { @@ -144,6 +156,10 @@ public class InAppPurchaseHelper { return Version.isDeveloperBuild(ctx) || ctx.getSettings().DEPTH_CONTOURS_PURCHASED.get(); } + public static boolean isContourLinesPurchased(@NonNull OsmandApplication ctx) { + return Version.isDeveloperBuild(ctx) || ctx.getSettings().CONTOUR_LINES_PURCHASED.get(); + } + public InAppPurchases getInAppPurchases() { return purchases; } @@ -176,9 +192,10 @@ public class InAppPurchaseHelper { public InAppPurchaseHelper(OsmandApplication ctx) { this.ctx = ctx; isDeveloperVersion = Version.isDeveloperVersion(ctx); - purchases = new InAppPurchases(ctx); } + public abstract void isInAppPurchaseSupported(@NonNull final Activity activity, @Nullable final InAppPurchaseInitCallback callback); + public boolean hasInventory() { return lastValidationCheckTime != 0; } @@ -194,12 +211,8 @@ public class InAppPurchaseHelper { return false; } - private BillingManager getBillingManager() { - return billingManager; - } - - private void exec(final @NonNull InAppPurchaseTaskType taskType, final @NonNull InAppRunnable runnable) { - if (isDeveloperVersion || !Version.isGooglePlayEnabled(ctx)) { + protected void exec(final @NonNull InAppPurchaseTaskType taskType, final @NonNull InAppCommand command) { + if (isDeveloperVersion || (!Version.isGooglePlayEnabled(ctx) && !Version.isHuawei(ctx))) { notifyDismissProgress(taskType); stop(true); return; @@ -222,117 +235,21 @@ public class InAppPurchaseHelper { try { processingTask = true; activeTask = taskType; - billingManager = new BillingManager(ctx, BASE64_ENCODED_PUBLIC_KEY, new BillingUpdatesListener() { - + command.resultHandler = new InAppCommandResultHandler() { @Override - public void onBillingClientSetupFinished() { - logDebug("Setup finished."); - - BillingManager billingManager = getBillingManager(); - // Have we been disposed of in the meantime? If so, quit. - if (billingManager == null) { - stop(true); - return; - } - - if (!billingManager.isServiceConnected()) { - // Oh noes, there was a problem. - //complain("Problem setting up in-app billing: " + result); - notifyError(taskType, billingManager.getBillingClientResponseMessage()); - stop(true); - return; - } - - processingTask = !runnable.run(InAppPurchaseHelper.this); + public void onCommandDone(@NonNull InAppCommand command) { + processingTask = false; } - - @Override - public void onConsumeFinished(String token, BillingResult billingResult) { - } - - @Override - public void onPurchasesUpdated(final List purchases) { - - BillingManager billingManager = getBillingManager(); - // Have we been disposed of in the meantime? If so, quit. - if (billingManager == null) { - stop(true); - return; - } - - if (activeTask == InAppPurchaseTaskType.REQUEST_INVENTORY) { - List skuInApps = new ArrayList<>(); - for (InAppPurchase purchase : getInAppPurchases().getAllInAppPurchases(false)) { - skuInApps.add(purchase.getSku()); - } - for (Purchase p : purchases) { - skuInApps.add(p.getSku()); - } - billingManager.querySkuDetailsAsync(SkuType.INAPP, skuInApps, new SkuDetailsResponseListener() { - @Override - public void onSkuDetailsResponse(BillingResult billingResult, final List skuDetailsListInApps) { - // Is it a failure? - if (billingResult.getResponseCode() != BillingResponseCode.OK) { - logError("Failed to query inapps sku details: " + billingResult.getResponseCode()); - notifyError(InAppPurchaseTaskType.REQUEST_INVENTORY, billingResult.getDebugMessage()); - stop(true); - return; - } - - List skuSubscriptions = new ArrayList<>(); - for (InAppSubscription subscription : getInAppPurchases().getAllInAppSubscriptions()) { - skuSubscriptions.add(subscription.getSku()); - } - for (Purchase p : purchases) { - skuSubscriptions.add(p.getSku()); - } - - BillingManager billingManager = getBillingManager(); - // Have we been disposed of in the meantime? If so, quit. - if (billingManager == null) { - stop(true); - return; - } - - billingManager.querySkuDetailsAsync(SkuType.SUBS, skuSubscriptions, new SkuDetailsResponseListener() { - @Override - public void onSkuDetailsResponse(BillingResult billingResult, final List skuDetailsListSubscriptions) { - // Is it a failure? - if (billingResult.getResponseCode() != BillingResponseCode.OK) { - logError("Failed to query subscriptipons sku details: " + billingResult.getResponseCode()); - notifyError(InAppPurchaseTaskType.REQUEST_INVENTORY, billingResult.getDebugMessage()); - stop(true); - return; - } - - List skuDetailsList = new ArrayList<>(skuDetailsListInApps); - skuDetailsList.addAll(skuDetailsListSubscriptions); - InAppPurchaseHelper.this.skuDetailsList = skuDetailsList; - - mSkuDetailsResponseListener.onSkuDetailsResponse(billingResult, skuDetailsList); - } - }); - } - }); - } - for (Purchase purchase : purchases) { - if (!purchase.isAcknowledged()) { - onPurchaseFinished(purchase); - } - } - } - - @Override - public void onPurchaseCanceled() { - stop(true); - } - }); + }; + execImpl(taskType, command); } catch (Exception e) { logError("exec Error", e); stop(true); } } + protected abstract void execImpl(@NonNull final InAppPurchaseTaskType taskType, @NonNull final InAppCommand command); + public boolean needRequestInventory() { return !inventoryRequested && ((isSubscribedToLiveUpdates(ctx) && Algorithms.isEmpty(ctx.getSettings().BILLING_PURCHASE_TOKENS_SENT.get())) || System.currentTimeMillis() - lastValidationCheckTime > PURCHASE_VALIDATION_PERIOD_MSEC); @@ -343,322 +260,20 @@ public class InAppPurchaseHelper { new RequestInventoryTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void) null); } - public void purchaseFullVersion(final Activity activity) { - notifyShowProgress(InAppPurchaseTaskType.PURCHASE_FULL_VERSION); - exec(InAppPurchaseTaskType.PURCHASE_FULL_VERSION, new InAppRunnable() { - @Override - public boolean run(InAppPurchaseHelper helper) { - try { - SkuDetails skuDetails = getSkuDetails(getFullVersion().getSku()); - if (skuDetails == null) { - throw new IllegalArgumentException("Cannot find sku details"); - } - BillingManager billingManager = getBillingManager(); - if (billingManager != null) { - billingManager.initiatePurchaseFlow(activity, skuDetails); - } else { - throw new IllegalStateException("BillingManager disposed"); - } - return false; - } catch (Exception e) { - complain("Cannot launch full version purchase!"); - logError("purchaseFullVersion Error", e); - stop(true); - } - return true; - } - }); - } + public abstract void purchaseFullVersion(@NonNull final Activity activity) throws UnsupportedOperationException; - public void purchaseLiveUpdates(Activity activity, String sku, String email, String userName, + public void purchaseLiveUpdates(@NonNull Activity activity, String sku, String email, String userName, String countryDownloadName, boolean hideUserName) { notifyShowProgress(InAppPurchaseTaskType.PURCHASE_LIVE_UPDATES); new LiveUpdatesPurchaseTask(activity, sku, email, userName, countryDownloadName, hideUserName) .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void) null); } - public void purchaseDepthContours(final Activity activity) { - notifyShowProgress(InAppPurchaseTaskType.PURCHASE_DEPTH_CONTOURS); - exec(InAppPurchaseTaskType.PURCHASE_DEPTH_CONTOURS, new InAppRunnable() { - @Override - public boolean run(InAppPurchaseHelper helper) { - try { - SkuDetails skuDetails = getSkuDetails(getDepthContours().getSku()); - if (skuDetails == null) { - throw new IllegalArgumentException("Cannot find sku details"); - } - BillingManager billingManager = getBillingManager(); - if (billingManager != null) { - billingManager.initiatePurchaseFlow(activity, skuDetails); - } else { - throw new IllegalStateException("BillingManager disposed"); - } - return false; - } catch (Exception e) { - complain("Cannot launch depth contours purchase!"); - logError("purchaseDepthContours Error", e); - stop(true); - } - return true; - } - }); - } + public abstract void purchaseDepthContours(@NonNull final Activity activity) throws UnsupportedOperationException; - @Nullable - private SkuDetails getSkuDetails(@NonNull String sku) { - List skuDetailsList = this.skuDetailsList; - if (skuDetailsList != null) { - for (SkuDetails details : skuDetailsList) { - if (details.getSku().equals(sku)) { - return details; - } - } - } - return null; - } + public abstract void purchaseContourLines(@NonNull final Activity activity) throws UnsupportedOperationException; - private boolean hasDetails(@NonNull String sku) { - return getSkuDetails(sku) != null; - } - - @Nullable - private Purchase getPurchase(@NonNull String sku) { - BillingManager billingManager = getBillingManager(); - if (billingManager != null) { - List purchases = billingManager.getPurchases(); - if (purchases != null) { - for (Purchase p : purchases) { - if (p.getSku().equals(sku)) { - return p; - } - } - } - } - return null; - } - - // Listener that's called when we finish querying the items and subscriptions we own - private SkuDetailsResponseListener mSkuDetailsResponseListener = new SkuDetailsResponseListener() { - - @NonNull - private List getAllOwnedSubscriptionSkus() { - List result = new ArrayList<>(); - BillingManager billingManager = getBillingManager(); - if (billingManager != null) { - for (Purchase p : billingManager.getPurchases()) { - if (getInAppPurchases().getInAppSubscriptionBySku(p.getSku()) != null) { - result.add(p.getSku()); - } - } - } - return result; - } - - @Override - public void onSkuDetailsResponse(BillingResult billingResult, List skuDetailsList) { - - logDebug("Query sku details finished."); - - // Have we been disposed of in the meantime? If so, quit. - if (getBillingManager() == null) { - stop(true); - return; - } - - // Is it a failure? - if (billingResult.getResponseCode() != BillingResponseCode.OK) { - logError("Failed to query inventory: " + billingResult.getResponseCode()); - notifyError(InAppPurchaseTaskType.REQUEST_INVENTORY, billingResult.getDebugMessage()); - stop(true); - return; - } - - logDebug("Query sku details was successful."); - - /* - * Check for items we own. Notice that for each purchase, we check - * the developer payload to see if it's correct! See - * verifyDeveloperPayload(). - */ - - List allOwnedSubscriptionSkus = getAllOwnedSubscriptionSkus(); - for (InAppSubscription s : getLiveUpdates().getAllSubscriptions()) { - if (hasDetails(s.getSku())) { - Purchase purchase = getPurchase(s.getSku()); - SkuDetails liveUpdatesDetails = getSkuDetails(s.getSku()); - if (liveUpdatesDetails != null) { - fetchInAppPurchase(s, liveUpdatesDetails, purchase); - } - allOwnedSubscriptionSkus.remove(s.getSku()); - } - } - for (String sku : allOwnedSubscriptionSkus) { - Purchase purchase = getPurchase(sku); - SkuDetails liveUpdatesDetails = getSkuDetails(sku); - if (liveUpdatesDetails != null) { - InAppSubscription s = getLiveUpdates().upgradeSubscription(sku); - if (s == null) { - s = new InAppPurchaseLiveUpdatesOldSubscription(liveUpdatesDetails); - } - fetchInAppPurchase(s, liveUpdatesDetails, purchase); - } - } - - InAppPurchase fullVersion = getFullVersion(); - if (hasDetails(fullVersion.getSku())) { - Purchase purchase = getPurchase(fullVersion.getSku()); - SkuDetails fullPriceDetails = getSkuDetails(fullVersion.getSku()); - if (fullPriceDetails != null) { - fetchInAppPurchase(fullVersion, fullPriceDetails, purchase); - } - } - - InAppPurchase depthContours = getDepthContours(); - if (hasDetails(depthContours.getSku())) { - Purchase purchase = getPurchase(depthContours.getSku()); - SkuDetails depthContoursDetails = getSkuDetails(depthContours.getSku()); - if (depthContoursDetails != null) { - fetchInAppPurchase(depthContours, depthContoursDetails, purchase); - } - } - - InAppPurchase contourLines = getContourLines(); - if (hasDetails(contourLines.getSku())) { - Purchase purchase = getPurchase(contourLines.getSku()); - SkuDetails contourLinesDetails = getSkuDetails(contourLines.getSku()); - if (contourLinesDetails != null) { - fetchInAppPurchase(contourLines, contourLinesDetails, purchase); - } - } - - Purchase fullVersionPurchase = getPurchase(fullVersion.getSku()); - boolean fullVersionPurchased = fullVersionPurchase != null; - if (fullVersionPurchased) { - ctx.getSettings().FULL_VERSION_PURCHASED.set(true); - } - - Purchase depthContoursPurchase = getPurchase(depthContours.getSku()); - boolean depthContoursPurchased = depthContoursPurchase != null; - if (depthContoursPurchased) { - ctx.getSettings().DEPTH_CONTOURS_PURCHASED.set(true); - } - - // Do we have the live updates? - boolean subscribedToLiveUpdates = false; - List liveUpdatesPurchases = new ArrayList<>(); - for (InAppPurchase p : getLiveUpdates().getAllSubscriptions()) { - Purchase purchase = getPurchase(p.getSku()); - if (purchase != null) { - liveUpdatesPurchases.add(purchase); - if (!subscribedToLiveUpdates) { - subscribedToLiveUpdates = true; - } - } - } - OsmandPreference subscriptionCancelledTime = ctx.getSettings().LIVE_UPDATES_PURCHASE_CANCELLED_TIME; - if (!subscribedToLiveUpdates && ctx.getSettings().LIVE_UPDATES_PURCHASED.get()) { - if (subscriptionCancelledTime.get() == 0) { - subscriptionCancelledTime.set(System.currentTimeMillis()); - ctx.getSettings().LIVE_UPDATES_PURCHASE_CANCELLED_FIRST_DLG_SHOWN.set(false); - ctx.getSettings().LIVE_UPDATES_PURCHASE_CANCELLED_SECOND_DLG_SHOWN.set(false); - } else if (System.currentTimeMillis() - subscriptionCancelledTime.get() > SUBSCRIPTION_HOLDING_TIME_MSEC) { - ctx.getSettings().LIVE_UPDATES_PURCHASED.set(false); - if (!isDepthContoursPurchased(ctx)) { - ctx.getSettings().getCustomRenderBooleanProperty("depthContours").set(false); - } - } - } else if (subscribedToLiveUpdates) { - subscriptionCancelledTime.set(0L); - ctx.getSettings().LIVE_UPDATES_PURCHASED.set(true); - } - - lastValidationCheckTime = System.currentTimeMillis(); - logDebug("User " + (subscribedToLiveUpdates ? "HAS" : "DOES NOT HAVE") - + " live updates purchased."); - - OsmandSettings settings = ctx.getSettings(); - settings.INAPPS_READ.set(true); - - List tokensToSend = new ArrayList<>(); - if (liveUpdatesPurchases.size() > 0) { - List tokensSent = Arrays.asList(settings.BILLING_PURCHASE_TOKENS_SENT.get().split(";")); - for (Purchase purchase : liveUpdatesPurchases) { - if ((Algorithms.isEmpty(settings.BILLING_USER_ID.get()) || Algorithms.isEmpty(settings.BILLING_USER_TOKEN.get())) - && !Algorithms.isEmpty(purchase.getDeveloperPayload())) { - String payload = purchase.getDeveloperPayload(); - if (!Algorithms.isEmpty(payload)) { - String[] arr = payload.split(" "); - if (arr.length > 0) { - settings.BILLING_USER_ID.set(arr[0]); - } - if (arr.length > 1) { - token = arr[1]; - settings.BILLING_USER_TOKEN.set(token); - } - } - } - if (!tokensSent.contains(purchase.getSku())) { - tokensToSend.add(purchase); - } - } - } - - final OnRequestResultListener listener = new OnRequestResultListener() { - @Override - public void onResult(String result) { - notifyDismissProgress(InAppPurchaseTaskType.REQUEST_INVENTORY); - notifyGetItems(); - stop(true); - logDebug("Initial inapp query finished"); - } - }; - - if (tokensToSend.size() > 0) { - sendTokens(tokensToSend, listener); - } else { - listener.onResult("OK"); - } - } - }; - - private void fetchInAppPurchase(@NonNull InAppPurchase inAppPurchase, @NonNull SkuDetails skuDetails, @Nullable Purchase purchase) { - if (purchase != null) { - inAppPurchase.setPurchaseState(PurchaseState.PURCHASED); - inAppPurchase.setPurchaseTime(purchase.getPurchaseTime()); - } else { - inAppPurchase.setPurchaseState(PurchaseState.NOT_PURCHASED); - } - inAppPurchase.setPrice(skuDetails.getPrice()); - inAppPurchase.setPriceCurrencyCode(skuDetails.getPriceCurrencyCode()); - if (skuDetails.getPriceAmountMicros() > 0) { - inAppPurchase.setPriceValue(skuDetails.getPriceAmountMicros() / 1000000d); - } - String subscriptionPeriod = skuDetails.getSubscriptionPeriod(); - if (!Algorithms.isEmpty(subscriptionPeriod)) { - if (inAppPurchase instanceof InAppSubscription) { - try { - ((InAppSubscription) inAppPurchase).setSubscriptionPeriodString(subscriptionPeriod); - } catch (ParseException e) { - LOG.error(e); - } - } - } - if (inAppPurchase instanceof InAppSubscription) { - String introductoryPrice = skuDetails.getIntroductoryPrice(); - String introductoryPricePeriod = skuDetails.getIntroductoryPricePeriod(); - String introductoryPriceCycles = skuDetails.getIntroductoryPriceCycles(); - long introductoryPriceAmountMicros = skuDetails.getIntroductoryPriceAmountMicros(); - if (!Algorithms.isEmpty(introductoryPrice)) { - InAppSubscription s = (InAppSubscription) inAppPurchase; - try { - s.setIntroductoryInfo(new InAppSubscriptionIntroductoryInfo(s, introductoryPrice, - introductoryPriceAmountMicros, introductoryPricePeriod, introductoryPriceCycles)); - } catch (ParseException e) { - LOG.error(e); - } - } - } - } + public abstract void manageSubscription(@NonNull Context ctx, @Nullable String sku); @SuppressLint("StaticFieldLeak") private class LiveUpdatesPurchaseTask extends AsyncTask { @@ -746,31 +361,7 @@ public class InAppPurchaseHelper { if (!Algorithms.isEmpty(userId) && !Algorithms.isEmpty(token)) { logDebug("Launching purchase flow for live updates subscription for userId=" + userId); final String payload = userId + " " + token; - exec(InAppPurchaseTaskType.PURCHASE_LIVE_UPDATES, new InAppRunnable() { - @Override - public boolean run(InAppPurchaseHelper helper) { - try { - Activity a = activity.get(); - SkuDetails skuDetails = getSkuDetails(sku); - if (a != null && skuDetails != null) { - BillingManager billingManager = getBillingManager(); - if (billingManager != null) { - billingManager.setPayload(payload); - billingManager.initiatePurchaseFlow(a, skuDetails); - } else { - throw new IllegalStateException("BillingManager disposed"); - } - return false; - } else { - stop(true); - } - } catch (Exception e) { - logError("launchPurchaseFlow Error", e); - stop(true); - } - return true; - } - }); + exec(InAppPurchaseTaskType.PURCHASE_LIVE_UPDATES, getPurchaseLiveUpdatesCommand(activity, sku, payload)); } else { notifyError(InAppPurchaseTaskType.PURCHASE_LIVE_UPDATES, "Empty userId"); stop(true); @@ -778,6 +369,9 @@ public class InAppPurchaseHelper { } } + protected abstract InAppCommand getPurchaseLiveUpdatesCommand(final WeakReference activity, + final String sku, final String payload) throws UnsupportedOperationException; + @SuppressLint("StaticFieldLeak") private class RequestInventoryTask extends AsyncTask { @@ -808,38 +402,41 @@ public class InAppPurchaseHelper { try { JSONObject obj = new JSONObject(response); JSONArray names = obj.names(); - for (int i = 0; i < names.length(); i++) { - String skuType = names.getString(i); - JSONObject subObj = obj.getJSONObject(skuType); - String sku = subObj.getString("sku"); - if (!Algorithms.isEmpty(sku)) { - getLiveUpdates().upgradeSubscription(sku); + if (names != null) { + for (int i = 0; i < names.length(); i++) { + String skuType = names.getString(i); + JSONObject subObj = obj.getJSONObject(skuType); + String sku = subObj.getString("sku"); + if (!Algorithms.isEmpty(sku)) { + getLiveUpdates().upgradeSubscription(sku); + } } } } catch (JSONException e) { logError("Json parsing error", e); } } - exec(InAppPurchaseTaskType.REQUEST_INVENTORY, new InAppRunnable() { - @Override - public boolean run(InAppPurchaseHelper helper) { - logDebug("Setup successful. Querying inventory."); - try { - BillingManager billingManager = getBillingManager(); - if (billingManager != null) { - billingManager.queryPurchases(); - } else { - throw new IllegalStateException("BillingManager disposed"); - } - return false; - } catch (Exception e) { - logError("queryInventoryAsync Error", e); - notifyDismissProgress(InAppPurchaseTaskType.REQUEST_INVENTORY); - stop(true); - } - return true; - } - }); + exec(InAppPurchaseTaskType.REQUEST_INVENTORY, getRequestInventoryCommand()); + } + } + + protected abstract InAppCommand getRequestInventoryCommand() throws UnsupportedOperationException; + + protected void onSkuDetailsResponseDone(List purchaseInfoList) { + final AndroidNetworkUtils.OnRequestResultListener listener = new AndroidNetworkUtils.OnRequestResultListener() { + @Override + public void onResult(String result) { + notifyDismissProgress(InAppPurchaseTaskType.REQUEST_INVENTORY); + notifyGetItems(); + stop(true); + logDebug("Initial inapp query finished"); + } + }; + + if (purchaseInfoList.size() > 0) { + sendTokens(purchaseInfoList, listener); + } else { + listener.onResult("OK"); } } @@ -852,25 +449,16 @@ public class InAppPurchaseHelper { parameters.put("aid", ctx.getUserAndroidId()); } - // Call when a purchase is finished - private void onPurchaseFinished(Purchase purchase) { - logDebug("Purchase finished: " + purchase); - - // if we were disposed of in the meantime, quit. - if (getBillingManager() == null) { - stop(true); - return; - } - + protected void onPurchaseDone(PurchaseInfo info) { logDebug("Purchase successful."); - InAppPurchase liveUpdatesPurchase = getLiveUpdates().getSubscriptionBySku(purchase.getSku()); + InAppPurchase liveUpdatesPurchase = getLiveUpdates().getSubscriptionBySku(info.getSku()); if (liveUpdatesPurchase != null) { // bought live updates logDebug("Live updates subscription purchased."); final String sku = liveUpdatesPurchase.getSku(); liveUpdatesPurchase.setPurchaseState(PurchaseState.PURCHASED); - sendTokens(Collections.singletonList(purchase), new OnRequestResultListener() { + sendTokens(Collections.singletonList(info), new OnRequestResultListener() { @Override public void onResult(String result) { boolean active = ctx.getSettings().LIVE_UPDATES_PURCHASED.get(); @@ -887,7 +475,7 @@ public class InAppPurchaseHelper { } }); - } else if (purchase.getSku().equals(getFullVersion().getSku())) { + } else if (info.getSku().equals(getFullVersion().getSku())) { // bought full version getFullVersion().setPurchaseState(PurchaseState.PURCHASED); logDebug("Full version purchased."); @@ -898,7 +486,7 @@ public class InAppPurchaseHelper { notifyItemPurchased(getFullVersion().getSku(), false); stop(true); - } else if (purchase.getSku().equals(getDepthContours().getSku())) { + } else if (info.getSku().equals(getDepthContours().getSku())) { // bought sea depth contours getDepthContours().setPurchaseState(PurchaseState.PURCHASED); logDebug("Sea depth contours purchased."); @@ -910,6 +498,17 @@ public class InAppPurchaseHelper { notifyItemPurchased(getDepthContours().getSku(), false); stop(true); + } else if (info.getSku().equals(getContourLines().getSku())) { + // bought contour lines + getContourLines().setPurchaseState(PurchaseState.PURCHASED); + logDebug("Contours lines purchased."); + showToast(ctx.getString(R.string.contour_lines_thanks)); + ctx.getSettings().CONTOUR_LINES_PURCHASED.set(true); + + notifyDismissProgress(InAppPurchaseTaskType.PURCHASE_CONTOUR_LINES); + notifyItemPurchased(getContourLines().getSku(), false); + stop(true); + } else { notifyDismissProgress(activeTask); stop(true); @@ -921,17 +520,19 @@ public class InAppPurchaseHelper { stop(false); } - private void stop(boolean taskDone) { + protected abstract boolean isBillingManagerExists(); + + protected abstract void destroyBillingManager(); + + protected void stop(boolean taskDone) { logDebug("Destroying helper."); - BillingManager billingManager = getBillingManager(); - if (billingManager != null) { + if (isBillingManagerExists()) { if (taskDone) { processingTask = false; } if (!processingTask) { activeTask = null; - billingManager.destroy(); - this.billingManager = null; + destroyBillingManager(); } } else { processingTask = false; @@ -943,7 +544,7 @@ public class InAppPurchaseHelper { } } - private void sendTokens(@NonNull final List purchases, final OnRequestResultListener listener) { + protected void sendTokens(@NonNull final List purchaseInfoList, final OnRequestResultListener listener) { final String userId = ctx.getSettings().BILLING_USER_ID.get(); final String token = ctx.getSettings().BILLING_USER_TOKEN.get(); final String email = ctx.getSettings().BILLING_USER_EMAIL.get(); @@ -951,12 +552,12 @@ public class InAppPurchaseHelper { String url = "https://osmand.net/subscription/purchased"; String userOperation = "Sending purchase info..."; final List requests = new ArrayList<>(); - for (Purchase purchase : purchases) { + for (PurchaseInfo info : purchaseInfoList) { Map parameters = new HashMap<>(); parameters.put("userid", userId); - parameters.put("sku", purchase.getSku()); - parameters.put("orderId", purchase.getOrderId()); - parameters.put("purchaseToken", purchase.getPurchaseToken()); + parameters.put("sku", info.getSku()); + parameters.put("orderId", info.getOrderId()); + parameters.put("purchaseToken", info.getPurchaseToken()); parameters.put("email", email); parameters.put("token", token); addUserInfo(parameters); @@ -967,9 +568,9 @@ public class InAppPurchaseHelper { public void onResult(@NonNull List results) { for (RequestResponse rr : results) { String sku = rr.getRequest().getParameters().get("sku"); - Purchase purchase = getPurchase(sku); - if (purchase != null) { - updateSentTokens(purchase); + PurchaseInfo info = getPurchaseInfo(sku); + if (info != null) { + updateSentTokens(info); String result = rr.getResponse(); if (result != null) { try { @@ -979,13 +580,13 @@ public class InAppPurchaseHelper { } else { complain("SendToken Error: " + obj.getString("error") - + " (userId=" + userId + " token=" + token + " response=" + result + " google=" + purchase.toString() + ")"); + + " (userId=" + userId + " token=" + token + " response=" + result + " google=" + info.toString() + ")"); } } catch (JSONException e) { logError("SendToken", e); complain("SendToken Error: " + (e.getMessage() != null ? e.getMessage() : "JSONException") - + " (userId=" + userId + " token=" + token + " response=" + result + " google=" + purchase.toString() + ")"); + + " (userId=" + userId + " token=" + token + " response=" + result + " google=" + info.toString() + ")"); } } } @@ -995,10 +596,10 @@ public class InAppPurchaseHelper { } } - private void updateSentTokens(@NonNull Purchase purchase) { + private void updateSentTokens(@NonNull PurchaseInfo info) { String tokensSentStr = ctx.getSettings().BILLING_PURCHASE_TOKENS_SENT.get(); Set tokensSent = new HashSet<>(Arrays.asList(tokensSentStr.split(";"))); - tokensSent.add(purchase.getSku()); + tokensSent.add(info.getSku()); ctx.getSettings().BILLING_PURCHASE_TOKENS_SENT.set(TextUtils.join(";", tokensSent)); } @@ -1032,10 +633,10 @@ public class InAppPurchaseHelper { } @Nullable - private Purchase getPurchase(String sku) { - for (Purchase purchase : purchases) { - if (purchase.getSku().equals(sku)) { - return purchase; + private PurchaseInfo getPurchaseInfo(String sku) { + for (PurchaseInfo info : purchaseInfoList) { + if (info.getSku().equals(sku)) { + return info; } } return null; @@ -1049,31 +650,35 @@ public class InAppPurchaseHelper { } } - private void notifyError(InAppPurchaseTaskType taskType, String message) { + public boolean onActivityResult(@NonNull Activity activity, int requestCode, int resultCode, Intent data) { + return false; + } + + protected void notifyError(InAppPurchaseTaskType taskType, String message) { if (uiActivity != null) { uiActivity.onError(taskType, message); } } - private void notifyGetItems() { + protected void notifyGetItems() { if (uiActivity != null) { uiActivity.onGetItems(); } } - private void notifyItemPurchased(String sku, boolean active) { + protected void notifyItemPurchased(String sku, boolean active) { if (uiActivity != null) { uiActivity.onItemPurchased(sku, active); } } - private void notifyShowProgress(InAppPurchaseTaskType taskType) { + protected void notifyShowProgress(InAppPurchaseTaskType taskType) { if (uiActivity != null) { uiActivity.showProgress(taskType); } } - private void notifyDismissProgress(InAppPurchaseTaskType taskType) { + protected void notifyDismissProgress(InAppPurchaseTaskType taskType) { if (uiActivity != null) { uiActivity.dismissProgress(taskType); } @@ -1090,26 +695,26 @@ public class InAppPurchaseHelper { } } - private void complain(String message) { + protected void complain(String message) { logError("**** InAppPurchaseHelper Error: " + message); showToast(message); } - private void showToast(final String message) { + protected void showToast(final String message) { ctx.showToastMessage(message); } - private void logDebug(String msg) { + protected void logDebug(String msg) { if (mDebugLog) { Log.d(TAG, msg); } } - private void logError(String msg) { + protected void logError(String msg) { Log.e(TAG, msg); } - private void logError(String msg, Throwable e) { + protected void logError(String msg, Throwable e) { Log.e(TAG, "Error: " + msg, e); } diff --git a/OsmAnd/src/net/osmand/plus/inapp/InAppPurchases.java b/OsmAnd/src/net/osmand/plus/inapp/InAppPurchases.java index b42b57f045..5004e97165 100644 --- a/OsmAnd/src/net/osmand/plus/inapp/InAppPurchases.java +++ b/OsmAnd/src/net/osmand/plus/inapp/InAppPurchases.java @@ -11,14 +11,11 @@ import androidx.annotation.ColorInt; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import com.android.billingclient.api.SkuDetails; - import net.osmand.AndroidUtils; import net.osmand.Period; import net.osmand.Period.PeriodUnit; import net.osmand.plus.OsmandApplication; import net.osmand.plus.R; -import net.osmand.plus.Version; import net.osmand.plus.helpers.FontCache; import net.osmand.plus.widgets.style.CustomTypefaceSpan; import net.osmand.util.Algorithms; @@ -33,64 +30,17 @@ import java.util.Locale; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; -public class InAppPurchases { +public abstract class InAppPurchases { - private static final InAppPurchase FULL_VERSION = new InAppPurchaseFullVersion(); - private static final InAppPurchaseDepthContoursFull DEPTH_CONTOURS_FULL = new InAppPurchaseDepthContoursFull(); - private static final InAppPurchaseDepthContoursFree DEPTH_CONTOURS_FREE = new InAppPurchaseDepthContoursFree(); - private static final InAppPurchaseContourLinesFull CONTOUR_LINES_FULL = new InAppPurchaseContourLinesFull(); - private static final InAppPurchaseContourLinesFree CONTOUR_LINES_FREE = new InAppPurchaseContourLinesFree(); + protected InAppPurchase fullVersion; + protected InAppPurchase depthContours; + protected InAppPurchase contourLines; + protected InAppSubscription monthlyLiveUpdates; + protected InAppSubscription discountedMonthlyLiveUpdates; + protected InAppSubscriptionList liveUpdates; + protected InAppPurchase[] inAppPurchases; - private static final InAppSubscription[] LIVE_UPDATES_FULL = new InAppSubscription[]{ - new InAppPurchaseLiveUpdatesOldMonthlyFull(), - new InAppPurchaseLiveUpdatesMonthlyFull(), - new InAppPurchaseLiveUpdates3MonthsFull(), - new InAppPurchaseLiveUpdatesAnnualFull() - }; - - private static final InAppSubscription[] LIVE_UPDATES_FREE = new InAppSubscription[]{ - new InAppPurchaseLiveUpdatesOldMonthlyFree(), - new InAppPurchaseLiveUpdatesMonthlyFree(), - new InAppPurchaseLiveUpdates3MonthsFree(), - new InAppPurchaseLiveUpdatesAnnualFree() - }; - - private InAppPurchase fullVersion; - private InAppPurchase depthContours; - private InAppPurchase contourLines; - private InAppSubscription monthlyLiveUpdates; - private InAppSubscription discountedMonthlyLiveUpdates; - private InAppSubscriptionList liveUpdates; - private InAppPurchase[] inAppPurchases; - - InAppPurchases(OsmandApplication ctx) { - fullVersion = FULL_VERSION; - if (Version.isFreeVersion(ctx)) { - liveUpdates = new LiveUpdatesInAppPurchasesFree(); - } else { - liveUpdates = new LiveUpdatesInAppPurchasesFull(); - } - for (InAppSubscription s : liveUpdates.getAllSubscriptions()) { - if (s instanceof InAppPurchaseLiveUpdatesMonthly) { - if (s.isDiscounted()) { - discountedMonthlyLiveUpdates = s; - } else { - monthlyLiveUpdates = s; - } - } - } - if (Version.isFreeVersion(ctx)) { - depthContours = DEPTH_CONTOURS_FREE; - } else { - depthContours = DEPTH_CONTOURS_FULL; - } - if (Version.isFreeVersion(ctx)) { - contourLines = CONTOUR_LINES_FREE; - } else { - contourLines = CONTOUR_LINES_FULL; - } - - inAppPurchases = new InAppPurchase[] { fullVersion, depthContours, contourLines }; + protected InAppPurchases(OsmandApplication ctx) { } public InAppPurchase getFullVersion() { @@ -123,7 +73,7 @@ public class InAppPurchases { public InAppSubscription getPurchasedMonthlyLiveUpdates() { if (monthlyLiveUpdates.isAnyPurchased()) { return monthlyLiveUpdates; - } else if (discountedMonthlyLiveUpdates.isAnyPurchased()) { + } else if (discountedMonthlyLiveUpdates != null && discountedMonthlyLiveUpdates.isAnyPurchased()) { return discountedMonthlyLiveUpdates; } return null; @@ -158,31 +108,13 @@ public class InAppPurchases { return null; } - public boolean isFullVersion(String sku) { - return FULL_VERSION.getSku().equals(sku); - } + public abstract boolean isFullVersion(String sku); - public boolean isDepthContours(String sku) { - return DEPTH_CONTOURS_FULL.getSku().equals(sku) || DEPTH_CONTOURS_FREE.getSku().equals(sku); - } + public abstract boolean isDepthContours(String sku); - public boolean isContourLines(String sku) { - return CONTOUR_LINES_FULL.getSku().equals(sku) || CONTOUR_LINES_FREE.getSku().equals(sku); - } + public abstract boolean isContourLines(String sku); - public boolean isLiveUpdates(String sku) { - for (InAppPurchase p : LIVE_UPDATES_FULL) { - if (p.getSku().equals(sku)) { - return true; - } - } - for (InAppPurchase p : LIVE_UPDATES_FREE) { - if (p.getSku().equals(sku)) { - return true; - } - } - return false; - } + public abstract boolean isLiveUpdates(String sku); public abstract static class InAppSubscriptionList { @@ -260,20 +192,6 @@ public class InAppPurchases { } } - public static class LiveUpdatesInAppPurchasesFree extends InAppSubscriptionList { - - public LiveUpdatesInAppPurchasesFree() { - super(LIVE_UPDATES_FREE); - } - } - - public static class LiveUpdatesInAppPurchasesFull extends InAppSubscriptionList { - - public LiveUpdatesInAppPurchasesFull() { - super(LIVE_UPDATES_FULL); - } - } - public abstract static class InAppPurchase { public enum PurchaseState { @@ -295,11 +213,11 @@ public class InAppPurchases { private NumberFormat currencyFormatter; - private InAppPurchase(@NonNull String sku) { + protected InAppPurchase(@NonNull String sku) { this.sku = sku; } - private InAppPurchase(@NonNull String sku, boolean discounted) { + protected InAppPurchase(@NonNull String sku, boolean discounted) { this(sku); this.discounted = discounted; } @@ -777,23 +695,9 @@ public class InAppPurchases { } } - public static class InAppPurchaseFullVersion extends InAppPurchase { - - private static final String SKU_FULL_VERSION_PRICE = "osmand_full_version_price"; - - InAppPurchaseFullVersion() { - super(SKU_FULL_VERSION_PRICE); - } - - @Override - public String getDefaultPrice(Context ctx) { - return ctx.getString(R.string.full_version_price); - } - } - public static class InAppPurchaseDepthContours extends InAppPurchase { - private InAppPurchaseDepthContours(String sku) { + protected InAppPurchaseDepthContours(String sku) { super(sku); } @@ -803,27 +707,9 @@ public class InAppPurchases { } } - public static class InAppPurchaseDepthContoursFull extends InAppPurchaseDepthContours { - - private static final String SKU_DEPTH_CONTOURS_FULL = "net.osmand.seadepth_plus"; - - InAppPurchaseDepthContoursFull() { - super(SKU_DEPTH_CONTOURS_FULL); - } - } - - public static class InAppPurchaseDepthContoursFree extends InAppPurchaseDepthContours { - - private static final String SKU_DEPTH_CONTOURS_FREE = "net.osmand.seadepth"; - - InAppPurchaseDepthContoursFree() { - super(SKU_DEPTH_CONTOURS_FREE); - } - } - public static class InAppPurchaseContourLines extends InAppPurchase { - private InAppPurchaseContourLines(String sku) { + protected InAppPurchaseContourLines(String sku) { super(sku); } @@ -833,25 +719,7 @@ public class InAppPurchases { } } - public static class InAppPurchaseContourLinesFull extends InAppPurchaseContourLines { - - private static final String SKU_CONTOUR_LINES_FULL = "net.osmand.contourlines_plus"; - - InAppPurchaseContourLinesFull() { - super(SKU_CONTOUR_LINES_FULL); - } - } - - public static class InAppPurchaseContourLinesFree extends InAppPurchaseContourLines { - - private static final String SKU_CONTOUR_LINES_FREE = "net.osmand.contourlines"; - - InAppPurchaseContourLinesFree() { - super(SKU_CONTOUR_LINES_FREE); - } - } - - public static abstract class InAppPurchaseLiveUpdatesMonthly extends InAppSubscription { + protected static abstract class InAppPurchaseLiveUpdatesMonthly extends InAppSubscription { InAppPurchaseLiveUpdatesMonthly(String skuNoVersion, int version) { super(skuNoVersion, version); @@ -905,45 +773,7 @@ public class InAppPurchases { } } - public static class InAppPurchaseLiveUpdatesMonthlyFull extends InAppPurchaseLiveUpdatesMonthly { - - private static final String SKU_LIVE_UPDATES_MONTHLY_FULL = "osm_live_subscription_monthly_full"; - - InAppPurchaseLiveUpdatesMonthlyFull() { - super(SKU_LIVE_UPDATES_MONTHLY_FULL, 1); - } - - private InAppPurchaseLiveUpdatesMonthlyFull(@NonNull String sku) { - super(sku); - } - - @Nullable - @Override - protected InAppSubscription newInstance(@NonNull String sku) { - return sku.startsWith(getSkuNoVersion()) ? new InAppPurchaseLiveUpdatesMonthlyFull(sku) : null; - } - } - - public static class InAppPurchaseLiveUpdatesMonthlyFree extends InAppPurchaseLiveUpdatesMonthly { - - private static final String SKU_LIVE_UPDATES_MONTHLY_FREE = "osm_live_subscription_monthly_free"; - - InAppPurchaseLiveUpdatesMonthlyFree() { - super(SKU_LIVE_UPDATES_MONTHLY_FREE, 1); - } - - private InAppPurchaseLiveUpdatesMonthlyFree(@NonNull String sku) { - super(sku); - } - - @Nullable - @Override - protected InAppSubscription newInstance(@NonNull String sku) { - return sku.startsWith(getSkuNoVersion()) ? new InAppPurchaseLiveUpdatesMonthlyFree(sku) : null; - } - } - - public static abstract class InAppPurchaseLiveUpdates3Months extends InAppSubscription { + protected static abstract class InAppPurchaseLiveUpdates3Months extends InAppSubscription { InAppPurchaseLiveUpdates3Months(String skuNoVersion, int version) { super(skuNoVersion, version); @@ -986,45 +816,7 @@ public class InAppPurchases { } } - public static class InAppPurchaseLiveUpdates3MonthsFull extends InAppPurchaseLiveUpdates3Months { - - private static final String SKU_LIVE_UPDATES_3_MONTHS_FULL = "osm_live_subscription_3_months_full"; - - InAppPurchaseLiveUpdates3MonthsFull() { - super(SKU_LIVE_UPDATES_3_MONTHS_FULL, 1); - } - - private InAppPurchaseLiveUpdates3MonthsFull(@NonNull String sku) { - super(sku); - } - - @Nullable - @Override - protected InAppSubscription newInstance(@NonNull String sku) { - return sku.startsWith(getSkuNoVersion()) ? new InAppPurchaseLiveUpdates3MonthsFull(sku) : null; - } - } - - public static class InAppPurchaseLiveUpdates3MonthsFree extends InAppPurchaseLiveUpdates3Months { - - private static final String SKU_LIVE_UPDATES_3_MONTHS_FREE = "osm_live_subscription_3_months_free"; - - InAppPurchaseLiveUpdates3MonthsFree() { - super(SKU_LIVE_UPDATES_3_MONTHS_FREE, 1); - } - - private InAppPurchaseLiveUpdates3MonthsFree(@NonNull String sku) { - super(sku); - } - - @Nullable - @Override - protected InAppSubscription newInstance(@NonNull String sku) { - return sku.startsWith(getSkuNoVersion()) ? new InAppPurchaseLiveUpdates3MonthsFree(sku) : null; - } - } - - public static abstract class InAppPurchaseLiveUpdatesAnnual extends InAppSubscription { + protected static abstract class InAppPurchaseLiveUpdatesAnnual extends InAppSubscription { InAppPurchaseLiveUpdatesAnnual(String skuNoVersion, int version) { super(skuNoVersion, version); @@ -1067,44 +859,6 @@ public class InAppPurchases { } } - public static class InAppPurchaseLiveUpdatesAnnualFull extends InAppPurchaseLiveUpdatesAnnual { - - private static final String SKU_LIVE_UPDATES_ANNUAL_FULL = "osm_live_subscription_annual_full"; - - InAppPurchaseLiveUpdatesAnnualFull() { - super(SKU_LIVE_UPDATES_ANNUAL_FULL, 1); - } - - private InAppPurchaseLiveUpdatesAnnualFull(@NonNull String sku) { - super(sku); - } - - @Nullable - @Override - protected InAppSubscription newInstance(@NonNull String sku) { - return sku.startsWith(getSkuNoVersion()) ? new InAppPurchaseLiveUpdatesAnnualFull(sku) : null; - } - } - - public static class InAppPurchaseLiveUpdatesAnnualFree extends InAppPurchaseLiveUpdatesAnnual { - - private static final String SKU_LIVE_UPDATES_ANNUAL_FREE = "osm_live_subscription_annual_free"; - - InAppPurchaseLiveUpdatesAnnualFree() { - super(SKU_LIVE_UPDATES_ANNUAL_FREE, 1); - } - - private InAppPurchaseLiveUpdatesAnnualFree(@NonNull String sku) { - super(sku); - } - - @Nullable - @Override - protected InAppSubscription newInstance(@NonNull String sku) { - return sku.startsWith(getSkuNoVersion()) ? new InAppPurchaseLiveUpdatesAnnualFree(sku) : null; - } - } - public static class InAppPurchaseLiveUpdatesOldMonthly extends InAppPurchaseLiveUpdatesMonthly { InAppPurchaseLiveUpdatesOldMonthly(String sku) { @@ -1127,54 +881,5 @@ public class InAppPurchases { return null; } } - - public static class InAppPurchaseLiveUpdatesOldMonthlyFull extends InAppPurchaseLiveUpdatesOldMonthly { - - private static final String SKU_LIVE_UPDATES_OLD_MONTHLY_FULL = "osm_live_subscription_2"; - - InAppPurchaseLiveUpdatesOldMonthlyFull() { - super(SKU_LIVE_UPDATES_OLD_MONTHLY_FULL); - } - } - - public static class InAppPurchaseLiveUpdatesOldMonthlyFree extends InAppPurchaseLiveUpdatesOldMonthly { - - private static final String SKU_LIVE_UPDATES_OLD_MONTHLY_FREE = "osm_free_live_subscription_2"; - - InAppPurchaseLiveUpdatesOldMonthlyFree() { - super(SKU_LIVE_UPDATES_OLD_MONTHLY_FREE); - } - } - - public static class InAppPurchaseLiveUpdatesOldSubscription extends InAppSubscription { - - private SkuDetails details; - - InAppPurchaseLiveUpdatesOldSubscription(@NonNull SkuDetails details) { - super(details.getSku(), true); - this.details = details; - } - - @Override - public String getDefaultPrice(Context ctx) { - return ""; - } - - @Override - public CharSequence getTitle(Context ctx) { - return details.getTitle(); - } - - @Override - public CharSequence getDescription(@NonNull Context ctx) { - return details.getDescription(); - } - - @Nullable - @Override - protected InAppSubscription newInstance(@NonNull String sku) { - return null; - } - } } diff --git a/OsmAnd/src/net/osmand/plus/measurementtool/ExitBottomSheetDialogFragment.java b/OsmAnd/src/net/osmand/plus/measurementtool/ExitBottomSheetDialogFragment.java index df7b4ae9b7..1bdd3ffdf2 100644 --- a/OsmAnd/src/net/osmand/plus/measurementtool/ExitBottomSheetDialogFragment.java +++ b/OsmAnd/src/net/osmand/plus/measurementtool/ExitBottomSheetDialogFragment.java @@ -1,7 +1,6 @@ package net.osmand.plus.measurementtool; import android.os.Bundle; -import android.view.View; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -9,9 +8,8 @@ import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import net.osmand.plus.R; -import net.osmand.plus.UiUtilities; +import net.osmand.plus.UiUtilities.DialogButtonType; import net.osmand.plus.base.MenuBottomSheetDialogFragment; -import net.osmand.plus.base.bottomsheetmenu.BottomSheetItemButton; import net.osmand.plus.base.bottomsheetmenu.simpleitems.DividerSpaceItem; import net.osmand.plus.base.bottomsheetmenu.simpleitems.ShortDescriptionItem; @@ -35,39 +33,6 @@ public class ExitBottomSheetDialogFragment extends MenuBottomSheetDialogFragment items.add(new DividerSpaceItem(getContext(), getResources().getDimensionPixelSize(R.dimen.bottom_sheet_exit_button_margin))); - items.add(new BottomSheetItemButton.Builder() - .setButtonType(UiUtilities.DialogButtonType.SECONDARY) - .setTitle(getString(R.string.shared_string_exit)) - .setLayoutId(R.layout.bottom_sheet_button) - .setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - Fragment targetFragment = getTargetFragment(); - if (targetFragment != null) { - targetFragment.onActivityResult(REQUEST_CODE, EXIT_RESULT_CODE, null); - } - dismiss(); - } - }) - .create()); - - items.add(new DividerSpaceItem(getContext(), - getResources().getDimensionPixelSize(R.dimen.bottom_sheet_icon_margin))); - - items.add(new BottomSheetItemButton.Builder() - .setTitle(getString(R.string.shared_string_save)) - .setLayoutId(R.layout.bottom_sheet_button) - .setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - Fragment targetFragment = getTargetFragment(); - if (targetFragment != null) { - targetFragment.onActivityResult(REQUEST_CODE, SAVE_RESULT_CODE, null); - } - dismiss(); - } - }) - .create()); } @Override @@ -75,6 +40,44 @@ public class ExitBottomSheetDialogFragment extends MenuBottomSheetDialogFragment return R.string.shared_string_cancel; } + @Override + protected int getRightBottomButtonTextId() { + return R.string.shared_string_save; + } + + @Override + protected int getThirdBottomButtonTextId() { + return R.string.shared_string_exit; + } + + @Override + public int getSecondDividerHeight() { + return getResources().getDimensionPixelSize(R.dimen.bottom_sheet_icon_margin); + } + + @Override + protected void onRightBottomButtonClick() { + Fragment targetFragment = getTargetFragment(); + if (targetFragment != null) { + targetFragment.onActivityResult(REQUEST_CODE, SAVE_RESULT_CODE, null); + } + dismiss(); + } + + @Override + protected void onThirdBottomButtonClick() { + Fragment targetFragment = getTargetFragment(); + if (targetFragment != null) { + targetFragment.onActivityResult(REQUEST_CODE, EXIT_RESULT_CODE, null); + } + dismiss(); + } + + @Override + protected DialogButtonType getThirdBottomButtonType() { + return (DialogButtonType.SECONDARY); + } + public static void showInstance(@NonNull FragmentManager fragmentManager, @Nullable Fragment targetFragment) { if (!fragmentManager.isStateSaved()) { ExitBottomSheetDialogFragment fragment = new ExitBottomSheetDialogFragment(); @@ -82,4 +85,4 @@ public class ExitBottomSheetDialogFragment extends MenuBottomSheetDialogFragment fragment.show(fragmentManager, TAG); } } -} +} \ No newline at end of file diff --git a/OsmAnd/src/net/osmand/plus/measurementtool/MeasurementToolFragment.java b/OsmAnd/src/net/osmand/plus/measurementtool/MeasurementToolFragment.java index 7d6818a9d6..83ef171fda 100644 --- a/OsmAnd/src/net/osmand/plus/measurementtool/MeasurementToolFragment.java +++ b/OsmAnd/src/net/osmand/plus/measurementtool/MeasurementToolFragment.java @@ -2,7 +2,6 @@ package net.osmand.plus.measurementtool; import android.annotation.SuppressLint; import android.app.Activity; -import android.app.ProgressDialog; import android.content.Context; import android.content.Intent; import android.graphics.drawable.Drawable; @@ -38,7 +37,6 @@ import net.osmand.AndroidUtils; import net.osmand.FileUtils; import net.osmand.GPXUtilities; import net.osmand.GPXUtilities.GPXFile; -import net.osmand.GPXUtilities.Track; import net.osmand.GPXUtilities.TrkSegment; import net.osmand.GPXUtilities.WptPt; import net.osmand.LocationsHolder; @@ -83,13 +81,9 @@ import net.osmand.plus.views.mapwidgets.MapInfoWidgetsFactory.TopToolbarControll import net.osmand.plus.views.mapwidgets.MapInfoWidgetsFactory.TopToolbarControllerType; import net.osmand.plus.views.mapwidgets.MapInfoWidgetsFactory.TopToolbarView; import net.osmand.router.RoutePlannerFrontEnd.GpxRouteApproximation; -import net.osmand.util.Algorithms; import java.io.File; -import java.lang.ref.WeakReference; -import java.text.MessageFormat; import java.text.SimpleDateFormat; -import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Locale; @@ -151,13 +145,16 @@ public class MeasurementToolFragment extends BaseOsmAndFragment implements Route private MeasurementEditingContext editingCtx = new MeasurementEditingContext(); private LatLon initialPoint; + private OsmandApplication app; + private MapActivity mapActivity; + private MeasurementToolLayer measurementToolLayer; - private enum SaveType { + enum SaveType { ROUTE_POINT, LINE } - private enum FinalSaveAction { + enum FinalSaveAction { SHOW_SNACK_BAR_AND_CLOSE, SHOW_TOAST, SHOW_IS_SAVED_FRAGMENT @@ -171,7 +168,7 @@ public class MeasurementToolFragment extends BaseOsmAndFragment implements Route this.initialPoint = initialPoint; } - private void setMode(int mode, boolean on) { + void setMode(int mode, boolean on) { int modes = this.modes; if (on) { modes |= mode; @@ -181,7 +178,7 @@ public class MeasurementToolFragment extends BaseOsmAndFragment implements Route this.modes = modes; } - private boolean isPlanRouteMode() { + boolean isPlanRouteMode() { return (this.modes & PLAN_ROUTE_MODE) == PLAN_ROUTE_MODE; } @@ -1062,7 +1059,7 @@ public class MeasurementToolFragment extends BaseOsmAndFragment implements Route } @Nullable - private GpxData setupGpxData(@Nullable GPXFile gpxFile) { + GpxData setupGpxData(@Nullable GPXFile gpxFile) { GpxData gpxData = null; if (gpxFile != null) { QuadRect rect = gpxFile.getRect(); @@ -1284,7 +1281,7 @@ public class MeasurementToolFragment extends BaseOsmAndFragment implements Route editingCtx.splitSegments(editingCtx.getBeforePoints().size() + editingCtx.getAfterPoints().size()); } - private void cancelModes() { + void cancelModes() { if (editingCtx.getOriginalPointToMove() != null) { cancelMovePointMode(); } else if (editingCtx.isInAddPointMode()) { @@ -1545,247 +1542,7 @@ public class MeasurementToolFragment extends BaseOsmAndFragment implements Route final SaveType saveType, final FinalSaveAction finalSaveAction) { - new AsyncTask() { - - private ProgressDialog progressDialog; - private File backupFile; - private File outFile; - private GPXFile savedGpxFile; - - @Override - protected void onPreExecute() { - cancelModes(); - MapActivity activity = getMapActivity(); - if (activity != null) { - progressDialog = new ProgressDialog(activity); - progressDialog.setMessage(getString(R.string.saving_gpx_tracks)); - progressDialog.show(); - } - } - - @Override - protected Exception doInBackground(Void... voids) { - MeasurementToolLayer measurementLayer = getMeasurementLayer(); - OsmandApplication app = getMyApplication(); - if (app == null) { - return null; - } - List points = editingCtx.getPoints(); - TrkSegment before = editingCtx.getBeforeTrkSegmentLine(); - TrkSegment after = editingCtx.getAfterTrkSegmentLine(); - if (gpxFile == null) { - outFile = new File(dir, fileName); - String trackName = fileName.substring(0, fileName.length() - GPX_FILE_EXT.length()); - GPXFile gpx = new GPXFile(Version.getFullVersion(app)); - if (measurementLayer != null) { - if (saveType == SaveType.LINE) { - TrkSegment segment = new TrkSegment(); - if (editingCtx.hasRoute()) { - segment.points.addAll(editingCtx.getRoutePoints()); - } else { - segment.points.addAll(before.points); - segment.points.addAll(after.points); - } - Track track = new Track(); - track.name = trackName; - track.segments.add(segment); - gpx.tracks.add(track); - } else if (saveType == SaveType.ROUTE_POINT) { - if (editingCtx.hasRoute()) { - GPXFile newGpx = editingCtx.exportRouteAsGpx(trackName); - if (newGpx != null) { - gpx = newGpx; - } - } - gpx.addRoutePoints(points); - } - } - Exception res = GPXUtilities.writeGpxFile(outFile, gpx); - gpx.path = outFile.getAbsolutePath(); - savedGpxFile = gpx; - if (showOnMap) { - showGpxOnMap(app, gpx, true); - } - return res; - } else { - GPXFile gpx = gpxFile; - outFile = new File(gpx.path); - backupFile = FileUtils.backupFile(app, outFile); - String trackName = Algorithms.getFileNameWithoutExtension(outFile); - if (measurementLayer != null) { - if (isPlanRouteMode()) { - if (saveType == SaveType.LINE) { - TrkSegment segment = new TrkSegment(); - if (editingCtx.hasRoute()) { - segment.points.addAll(editingCtx.getRoutePoints()); - } else { - segment.points.addAll(before.points); - segment.points.addAll(after.points); - } - Track track = new Track(); - track.name = trackName; - track.segments.add(segment); - gpx.tracks.add(track); - } else if (saveType == SaveType.ROUTE_POINT) { - if (editingCtx.hasRoute()) { - GPXFile newGpx = editingCtx.exportRouteAsGpx(trackName); - if (newGpx != null) { - gpx = newGpx; - } - } - gpx.addRoutePoints(points); - } - } else if (actionType != null) { - GpxData gpxData = editingCtx.getGpxData(); - switch (actionType) { - case ADD_SEGMENT: { - List snappedPoints = new ArrayList<>(); - snappedPoints.addAll(before.points); - snappedPoints.addAll(after.points); - gpx.addTrkSegment(snappedPoints); - break; - } - case ADD_ROUTE_POINTS: { - gpx.replaceRoutePoints(points); - break; - } - case EDIT_SEGMENT: { - if (gpxData != null) { - TrkSegment segment = new TrkSegment(); - segment.points.addAll(points); - gpx.replaceSegment(gpxData.getTrkSegment(), segment); - } - break; - } - case OVERWRITE_SEGMENT: { - if (gpxData != null) { - List snappedPoints = new ArrayList<>(); - snappedPoints.addAll(before.points); - snappedPoints.addAll(after.points); - TrkSegment segment = new TrkSegment(); - segment.points.addAll(snappedPoints); - gpx.replaceSegment(gpxData.getTrkSegment(), segment); - } - break; - } - } - } else { - gpx.addRoutePoints(points); - } - } - Exception res = null; - if (!gpx.showCurrentTrack) { - res = GPXUtilities.writeGpxFile(outFile, gpx); - } - savedGpxFile = gpx; - if (showOnMap) { - showGpxOnMap(app, gpx, false); - } - return res; - } - } - - private void showGpxOnMap(OsmandApplication app, GPXFile gpx, boolean isNewGpx) { - SelectedGpxFile sf = app.getSelectedGpxHelper().selectGpxFile(gpx, true, false); - if (sf != null && !isNewGpx) { - if (actionType == ActionType.ADD_SEGMENT || actionType == ActionType.EDIT_SEGMENT) { - sf.processPoints(getMyApplication()); - } - } - } - - @Override - protected void onPostExecute(Exception warning) { - onGpxSaved(warning); - } - - private void onGpxSaved(Exception warning) { - MapActivity mapActivity = getMapActivity(); - if (mapActivity == null) { - return; - } - if (progressDialog != null && progressDialog.isShowing()) { - progressDialog.dismiss(); - } - mapActivity.refreshMap(); - if (warning == null) { - if (editingCtx.isNewData() && savedGpxFile != null) { - QuadRect rect = savedGpxFile.getRect(); - TrkSegment segment = savedGpxFile.getNonEmptyTrkSegment(); - GpxData gpxData = new GpxData(savedGpxFile, rect, ActionType.EDIT_SEGMENT, segment); - editingCtx.setGpxData(gpxData); - updateToolbar(); - } - if (isInEditMode()) { - editingCtx.setChangesSaved(); - dismiss(mapActivity); - } else { - switch (finalSaveAction) { - case SHOW_SNACK_BAR_AND_CLOSE: - final WeakReference mapActivityRef = new WeakReference<>(mapActivity); - snackbar = Snackbar.make(mapActivity.getLayout(), - MessageFormat.format(getString(R.string.gpx_saved_sucessfully), outFile.getName()), - Snackbar.LENGTH_LONG) - .setAction(R.string.shared_string_undo, new OnClickListener() { - @Override - public void onClick(View view) { - MapActivity mapActivity = mapActivityRef.get(); - if (mapActivity != null) { - if (outFile != null) { - OsmandApplication app = mapActivity.getMyApplication(); - FileUtils.removeGpxFile(app, outFile); - if (backupFile != null) { - FileUtils.renameGpxFile(app, backupFile, outFile); - GPXFile gpx = GPXUtilities.loadGPXFile(outFile); - setupGpxData(gpx); - if (showOnMap) { - showGpxOnMap(app, gpx, false); - } - } else { - setupGpxData(null); - } - } - setMode(UNDO_MODE, true); - MeasurementToolFragment.showInstance(mapActivity.getSupportFragmentManager(), - editingCtx, modes); - } - } - }) - .addCallback(new Snackbar.Callback() { - @Override - public void onDismissed(Snackbar transientBottomBar, int event) { - if (event != DISMISS_EVENT_ACTION) { - editingCtx.setChangesSaved(); - } - super.onDismissed(transientBottomBar, event); - } - }); - snackbar.getView().findViewById(com.google.android.material.R.id.snackbar_action) - .setAllCaps(false); - UiUtilities.setupSnackbar(snackbar, nightMode); - snackbar.show(); - dismiss(mapActivity, false); - break; - case SHOW_IS_SAVED_FRAGMENT: - editingCtx.setChangesSaved(); - SavedTrackBottomSheetDialogFragment.showInstance(mapActivity.getSupportFragmentManager(), - outFile.getAbsolutePath()); - dismiss(mapActivity); - break; - case SHOW_TOAST: - editingCtx.setChangesSaved(); - if (!savedGpxFile.showCurrentTrack) { - Toast.makeText(mapActivity, - MessageFormat.format(getString(R.string.gpx_saved_sucessfully), outFile.getAbsolutePath()), - Toast.LENGTH_LONG).show(); - } - } - } - } else { - Toast.makeText(mapActivity, warning.getMessage(), Toast.LENGTH_LONG).show(); - } - } - }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + new SaveGPX(this, gpxFile, dir, fileName, saveType, showOnMap, actionType, finalSaveAction, app, mapActivity, measurementToolLayer, nightMode).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } private void updateUndoRedoButton(boolean enable, View view) { @@ -1818,7 +1575,7 @@ public class MeasurementToolFragment extends BaseOsmAndFragment implements Route updateToolbar(); } - private void updateToolbar() { + void updateToolbar() { MapActivity mapActivity = getMapActivity(); if (mapActivity == null) { return; @@ -1949,7 +1706,7 @@ public class MeasurementToolFragment extends BaseOsmAndFragment implements Route } } - private void dismiss(@NonNull MapActivity mapActivity) { + void dismiss(@NonNull MapActivity mapActivity) { dismiss(mapActivity, true); } @@ -2171,4 +1928,5 @@ public class MeasurementToolFragment extends BaseOsmAndFragment implements Route public boolean isNightModeForMapControls() { return nightMode; } + } diff --git a/OsmAnd/src/net/osmand/plus/measurementtool/SaveGPX.java b/OsmAnd/src/net/osmand/plus/measurementtool/SaveGPX.java new file mode 100644 index 0000000000..de47191ab5 --- /dev/null +++ b/OsmAnd/src/net/osmand/plus/measurementtool/SaveGPX.java @@ -0,0 +1,297 @@ +package net.osmand.plus.measurementtool; + +import android.annotation.SuppressLint; +import android.app.ProgressDialog; +import android.os.AsyncTask; +import android.view.View; +import android.widget.TextView; +import android.widget.Toast; + +import com.google.android.material.snackbar.Snackbar; + +import net.osmand.FileUtils; +import net.osmand.GPXUtilities; +import net.osmand.data.QuadRect; +import net.osmand.plus.GpxSelectionHelper; +import net.osmand.plus.OsmandApplication; +import net.osmand.plus.R; +import net.osmand.plus.UiUtilities; +import net.osmand.plus.Version; +import net.osmand.plus.activities.MapActivity; +import net.osmand.util.Algorithms; + +import java.io.File; +import java.lang.ref.WeakReference; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.List; + +import static net.osmand.IndexConstants.GPX_FILE_EXT; + +class SaveGPX extends AsyncTask { + + private final MeasurementToolFragment measurementToolFragment; + private final GPXUtilities.GPXFile gpxFile; + private final File dir; + private final String fileName; + private final MeasurementToolFragment.SaveType saveType; + private final boolean showOnMap; + private final GpxData.ActionType actionType; + private final MeasurementToolFragment.FinalSaveAction finalSaveAction; + private final OsmandApplication app; + @SuppressLint("StaticFieldLeak") + private final MapActivity mapActivity; + private MeasurementToolLayer measurementToolLayer; + private boolean nightMode; + private ProgressDialog progressDialog; + private File backupFile; + private File outFile; + private GPXUtilities.GPXFile savedGpxFile; + public MeasurementEditingContext editingCtx = new MeasurementEditingContext(); + private static final int UNDO_MODE = 0x8; + + public SaveGPX(MeasurementToolFragment measurementToolFragment, GPXUtilities.GPXFile gpxFile, File dir, String fileName, MeasurementToolFragment.SaveType saveType, boolean showOnMap, GpxData.ActionType actionType, MeasurementToolFragment.FinalSaveAction finalSaveAction, OsmandApplication app, MapActivity mapActivity, MeasurementToolLayer measurementToolLayer, boolean nightMode) { + this.measurementToolFragment = measurementToolFragment; + this.gpxFile = gpxFile; + this.dir = dir; + this.fileName = fileName; + this.saveType = saveType; + this.showOnMap = showOnMap; + this.actionType = actionType; + this.finalSaveAction = finalSaveAction; + this.app = app; + this.mapActivity = mapActivity; + this.measurementToolLayer = measurementToolLayer; + this.nightMode = nightMode; + } + + @Override + protected void onPreExecute() { + measurementToolFragment.cancelModes(); + if (mapActivity != null) { + progressDialog = new ProgressDialog(mapActivity); + progressDialog.setMessage(measurementToolFragment.getString(R.string.saving_gpx_tracks)); + progressDialog.show(); + } + } + + @Override + protected Exception doInBackground(Void... voids) { + if (app == null) { + return null; + } + List points = editingCtx.getPoints(); + GPXUtilities.TrkSegment before = editingCtx.getBeforeTrkSegmentLine(); + GPXUtilities.TrkSegment after = editingCtx.getAfterTrkSegmentLine(); + if (gpxFile == null) { + outFile = new File(dir, fileName); + String trackName = fileName.substring(0, fileName.length() - GPX_FILE_EXT.length()); + GPXUtilities.GPXFile gpx = new GPXUtilities.GPXFile(Version.getFullVersion(app)); + if (measurementToolLayer != null) { + if (saveType == MeasurementToolFragment.SaveType.LINE) { + GPXUtilities.TrkSegment segment = new GPXUtilities.TrkSegment(); + if (editingCtx.hasRoute()) { + segment.points.addAll(editingCtx.getRoutePoints()); + } else { + segment.points.addAll(before.points); + segment.points.addAll(after.points); + } + GPXUtilities.Track track = new GPXUtilities.Track(); + track.name = trackName; + track.segments.add(segment); + gpx.tracks.add(track); + } else if (saveType == MeasurementToolFragment.SaveType.ROUTE_POINT) { + if (editingCtx.hasRoute()) { + GPXUtilities.GPXFile newGpx = editingCtx.exportRouteAsGpx(trackName); + if (newGpx != null) { + gpx = newGpx; + } + } + gpx.addRoutePoints(points); + } + } + Exception res = GPXUtilities.writeGpxFile(outFile, gpx); + gpx.path = outFile.getAbsolutePath(); + savedGpxFile = gpx; + if (showOnMap) { + showGpxOnMap(app, gpx, true); + } + return res; + } else { + GPXUtilities.GPXFile gpx = gpxFile; + outFile = new File(gpx.path); + backupFile = FileUtils.backupFile(app, outFile); + String trackName = Algorithms.getFileNameWithoutExtension(outFile); + if (measurementToolLayer != null) { + if (measurementToolFragment.isPlanRouteMode()) { + if (saveType == MeasurementToolFragment.SaveType.LINE) { + GPXUtilities.TrkSegment segment = new GPXUtilities.TrkSegment(); + if (editingCtx.hasRoute()) { + segment.points.addAll(editingCtx.getRoutePoints()); + } else { + segment.points.addAll(before.points); + segment.points.addAll(after.points); + } + GPXUtilities.Track track = new GPXUtilities.Track(); + track.name = trackName; + track.segments.add(segment); + gpx.tracks.add(track); + } else if (saveType == MeasurementToolFragment.SaveType.ROUTE_POINT) { + if (editingCtx.hasRoute()) { + GPXUtilities.GPXFile newGpx = editingCtx.exportRouteAsGpx(trackName); + if (newGpx != null) { + gpx = newGpx; + } + } + gpx.addRoutePoints(points); + } + } else if (actionType != null) { + GpxData gpxData = editingCtx.getGpxData(); + switch (actionType) { + case ADD_SEGMENT: { + List snappedPoints = new ArrayList<>(); + snappedPoints.addAll(before.points); + snappedPoints.addAll(after.points); + gpx.addTrkSegment(snappedPoints); + break; + } + case ADD_ROUTE_POINTS: { + gpx.replaceRoutePoints(points); + break; + } + case EDIT_SEGMENT: { + if (gpxData != null) { + GPXUtilities.TrkSegment segment = new GPXUtilities.TrkSegment(); + segment.points.addAll(points); + gpx.replaceSegment(gpxData.getTrkSegment(), segment); + } + break; + } + case OVERWRITE_SEGMENT: { + if (gpxData != null) { + List snappedPoints = new ArrayList<>(); + snappedPoints.addAll(before.points); + snappedPoints.addAll(after.points); + GPXUtilities.TrkSegment segment = new GPXUtilities.TrkSegment(); + segment.points.addAll(snappedPoints); + gpx.replaceSegment(gpxData.getTrkSegment(), segment); + } + break; + } + } + } else { + gpx.addRoutePoints(points); + } + } + Exception res = null; + if (!gpx.showCurrentTrack) { + res = GPXUtilities.writeGpxFile(outFile, gpx); + } + savedGpxFile = gpx; + if (showOnMap) { + showGpxOnMap(app, gpx, false); + } + return res; + } + } + + private void showGpxOnMap(OsmandApplication app, GPXUtilities.GPXFile gpx, boolean isNewGpx) { + GpxSelectionHelper.SelectedGpxFile sf = app.getSelectedGpxHelper().selectGpxFile(gpx, true, false); + if (sf != null && !isNewGpx) { + if (actionType == GpxData.ActionType.ADD_SEGMENT || actionType == GpxData.ActionType.EDIT_SEGMENT) { + sf.processPoints(app); + } + } + } + + @Override + protected void onPostExecute(Exception warning) { + onGpxSaved(warning); + } + + private void onGpxSaved(Exception warning) { + if (mapActivity == null) { + return; + } + if (progressDialog != null && progressDialog.isShowing()) { + progressDialog.dismiss(); + } + mapActivity.refreshMap(); + if (warning == null) { + if (editingCtx.isNewData() && savedGpxFile != null) { + QuadRect rect = savedGpxFile.getRect(); + GPXUtilities.TrkSegment segment = savedGpxFile.getNonEmptyTrkSegment(); + GpxData gpxData = new GpxData(savedGpxFile, rect, GpxData.ActionType.EDIT_SEGMENT, segment); + editingCtx.setGpxData(gpxData); + measurementToolFragment.updateToolbar(); + } + if (measurementToolFragment.isInEditMode()) { + editingCtx.setChangesSaved(); + measurementToolFragment.dismiss(mapActivity); + } else { + switch (finalSaveAction) { + case SHOW_SNACK_BAR_AND_CLOSE: + final WeakReference mapActivityRef = new WeakReference<>(mapActivity); + Snackbar snackbar = Snackbar.make(mapActivity.getLayout(), + MessageFormat.format(measurementToolFragment.getString(R.string.gpx_saved_sucessfully), outFile.getName()), + Snackbar.LENGTH_LONG) + .setAction(R.string.shared_string_undo, new View.OnClickListener() { + @Override + public void onClick(View view) { + MapActivity mapActivity = mapActivityRef.get(); + if (mapActivity != null) { + if (outFile != null) { + OsmandApplication app = mapActivity.getMyApplication(); + FileUtils.removeGpxFile(app, outFile); + if (backupFile != null) { + FileUtils.renameGpxFile(app, backupFile, outFile); + GPXUtilities.GPXFile gpx = GPXUtilities.loadGPXFile(outFile); + measurementToolFragment.setupGpxData(gpx); + if (showOnMap) { + showGpxOnMap(app, gpx, false); + } + } else { + measurementToolFragment.setupGpxData(null); + } + } + measurementToolFragment.setMode(UNDO_MODE, true); + MeasurementToolFragment.showInstance(mapActivity.getSupportFragmentManager() + ); + } + } + }) + .addCallback(new Snackbar.Callback() { + @Override + public void onDismissed(Snackbar transientBottomBar, int event) { + if (event != DISMISS_EVENT_ACTION) { + editingCtx.setChangesSaved(); + } + super.onDismissed(transientBottomBar, event); + } + }); + snackbar.getView().findViewById(com.google.android.material.R.id.snackbar_action) + .setAllCaps(false); + UiUtilities.setupSnackbar(snackbar, nightMode); + snackbar.show(); + measurementToolFragment.dismiss(mapActivity); + break; + case SHOW_IS_SAVED_FRAGMENT: + editingCtx.setChangesSaved(); + SavedTrackBottomSheetDialogFragment.showInstance(mapActivity.getSupportFragmentManager(), + outFile.getAbsolutePath()); + measurementToolFragment.dismiss(mapActivity); + break; + case SHOW_TOAST: + editingCtx.setChangesSaved(); + if (!savedGpxFile.showCurrentTrack) { + Toast.makeText(mapActivity, + MessageFormat.format(measurementToolFragment.getString(R.string.gpx_saved_sucessfully), outFile.getAbsolutePath()), + Toast.LENGTH_LONG).show(); + } + } + } + } else { + Toast.makeText(mapActivity, warning.getMessage(), Toast.LENGTH_LONG).show(); + } + } +} diff --git a/OsmAnd/src/net/osmand/plus/measurementtool/SelectFileBottomSheet.java b/OsmAnd/src/net/osmand/plus/measurementtool/SelectFileBottomSheet.java index 31502bea57..24d3ec8282 100644 --- a/OsmAnd/src/net/osmand/plus/measurementtool/SelectFileBottomSheet.java +++ b/OsmAnd/src/net/osmand/plus/measurementtool/SelectFileBottomSheet.java @@ -14,7 +14,9 @@ import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import net.osmand.AndroidUtils; +import net.osmand.Collator; import net.osmand.IndexConstants; +import net.osmand.OsmAndCollator; import net.osmand.plus.OsmandApplication; import net.osmand.plus.R; import net.osmand.plus.UiUtilities; @@ -41,6 +43,10 @@ import static net.osmand.util.Algorithms.collectDirs; public class SelectFileBottomSheet extends BottomSheetBehaviourDialogFragment { + private List folders; + private HorizontalSelectionAdapter folderAdapter; + private GPXInfo currentlyRecording; + enum Mode { OPEN_TRACK(R.string.shared_string_gpx_tracks, R.string.sort_by), ADD_TO_TRACK(R.string.add_to_a_track, R.string.route_between_points_add_track_desc); @@ -98,55 +104,52 @@ public class SelectFileBottomSheet extends BottomSheetBehaviourDialogFragment { if (fragmentMode == Mode.OPEN_TRACK) { titleView.setText(AndroidUtils.addColon(app, fragmentMode.title)); updateDescription(descriptionView); - final ImageButton sortButton = mainView.findViewById(R.id.sort_button); - Drawable background = app.getUIUtilities().getIcon(R.drawable.bg_dash_line_dark, - nightMode - ? R.color.inactive_buttons_and_links_bg_dark - : R.color.inactive_buttons_and_links_bg_light); - AndroidUtils.setBackground(sortButton, background); - sortButton.setImageResource(sortByMode.getIconId()); - sortButton.setVisibility(View.VISIBLE); - sortButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - final List items = new ArrayList<>(); - for (final TracksSortByMode mode : TracksSortByMode.values()) { - items.add(new SimplePopUpMenuItem( - getString(mode.getNameId()), - app.getUIUtilities().getThemedIcon(mode.getIconId()), - new View.OnClickListener() { - @Override - public void onClick(View v) { - sortByMode = mode; - sortButton.setImageResource(mode.getIconId()); - updateDescription(descriptionView); - sortFileList(); - adapter.notifyDataSetChanged(); - } - }, sortByMode == mode - )); - } - UiUtilities.showPopUpMenu(v, items); - } - }); } + final ImageButton sortButton = mainView.findViewById(R.id.sort_button); + Drawable background = app.getUIUtilities().getIcon(R.drawable.bg_dash_line_dark, + nightMode + ? R.color.inactive_buttons_and_links_bg_dark + : R.color.inactive_buttons_and_links_bg_light); + AndroidUtils.setBackground(sortButton, background); + sortButton.setImageResource(sortByMode.getIconId()); + sortButton.setVisibility(View.VISIBLE); + sortButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + final List items = new ArrayList<>(); + for (final TracksSortByMode mode : TracksSortByMode.values()) { + items.add(new SimplePopUpMenuItem( + getString(mode.getNameId()), + app.getUIUtilities().getThemedIcon(mode.getIconId()), + new View.OnClickListener() { + @Override + public void onClick(View v) { + sortByMode = mode; + sortButton.setImageResource(mode.getIconId()); + updateDescription(descriptionView); + sortFolderList(); + folderAdapter.setItems(getFolderNames()); + folderAdapter.notifyDataSetChanged(); + sortFileList(); + adapter.notifyDataSetChanged(); + } + }, sortByMode == mode + )); + } + UiUtilities.showPopUpMenu(v, items); + } + }); - List dirs = new ArrayList<>(); final File gpxDir = app.getAppPath(IndexConstants.GPX_INDEX_DIR); - collectDirs(gpxDir, dirs); - List dirItems = new ArrayList<>(); + allFilesFolder = context.getString(R.string.shared_string_all); if (savedInstanceState == null) { selectedFolder = allFilesFolder; } - dirItems.add(allFilesFolder); - for (File dir : dirs) { - dirItems.add(dir.getName()); - } - final List allGpxList = getSortedGPXFilesInfo(gpxDir, null, false); + currentlyRecording = new GPXInfo(getString(R.string.shared_string_currently_recording_track), 0, 0); if (isShowCurrentGpx()) { - allGpxList.add(0, new GPXInfo(getString(R.string.shared_string_currently_recording_track), 0, 0)); + allGpxList.add(0, currentlyRecording); } gpxInfoMap = new HashMap<>(); gpxInfoMap.put(allFilesFolder, allGpxList); @@ -184,8 +187,11 @@ public class SelectFileBottomSheet extends BottomSheetBehaviourDialogFragment { final RecyclerView foldersRecyclerView = mainView.findViewById(R.id.folder_list); foldersRecyclerView.setLayoutManager(new LinearLayoutManager(context, RecyclerView.HORIZONTAL, false)); - final HorizontalSelectionAdapter folderAdapter = new HorizontalSelectionAdapter(app, nightMode); - folderAdapter.setItems(dirItems); + folderAdapter = new HorizontalSelectionAdapter(app, nightMode); + folders = new ArrayList<>(); + collectDirs(gpxDir, folders); + sortFolderList(); + folderAdapter.setItems(getFolderNames()); folderAdapter.setSelectedItem(selectedFolder); foldersRecyclerView.setAdapter(folderAdapter); folderAdapter.setListener(new HorizontalSelectionAdapterListener() { @@ -199,11 +205,22 @@ public class SelectFileBottomSheet extends BottomSheetBehaviourDialogFragment { updateFileList(folderAdapter); } + private List getFolderNames() { + List folderNames = new ArrayList<>(); + folderNames.add(allFilesFolder); + for (File folder : folders) { + folderNames.add(folder.getName()); + } + return folderNames; + } + private void updateDescription(TextView descriptionView) { - String string = getString(sortByMode.getNameId()); - descriptionView.setText(String.format(getString(R.string.ltr_or_rtl_combine_via_space), - getString(fragmentMode.description), - Character.toLowerCase(string.charAt(0)) + string.substring(1))); + if (fragmentMode == Mode.OPEN_TRACK) { + String string = getString(sortByMode.getNameId()); + descriptionView.setText(String.format(getString(R.string.ltr_or_rtl_combine_via_space), + getString(fragmentMode.description), + Character.toLowerCase(string.charAt(0)) + string.substring(1))); + } } private void updateFileList(HorizontalSelectionAdapter folderAdapter) { @@ -213,6 +230,27 @@ public class SelectFileBottomSheet extends BottomSheetBehaviourDialogFragment { folderAdapter.notifyDataSetChanged(); } + private void sortFolderList() { + final Collator collator = OsmAndCollator.primaryCollator(); + Collections.sort(folders, new Comparator() { + @Override + public int compare(File i1, File i2) { + if (sortByMode == TracksSortByMode.BY_NAME_ASCENDING) { + return collator.compare(i1.getName(), i2.getName()); + } else if (sortByMode == TracksSortByMode.BY_NAME_DESCENDING) { + return -collator.compare(i1.getName(), i2.getName()); + } else { + long time1 = i1.lastModified(); + long time2 = i2.lastModified(); + if (time1 == time2) { + return collator.compare(i1.getName(), i2.getName()); + } + return -((time1 < time2) ? -1 : ((time1 == time2) ? 0 : 1)); + } + } + }); + } + private void sortFileList() { List gpxInfoList = gpxInfoMap.get(selectedFolder); if (gpxInfoList != null) { @@ -222,23 +260,28 @@ public class SelectFileBottomSheet extends BottomSheetBehaviourDialogFragment { } public void sortSelected(List gpxInfoList) { + boolean hasRecording = gpxInfoList.remove(currentlyRecording); + final Collator collator = OsmAndCollator.primaryCollator(); Collections.sort(gpxInfoList, new Comparator() { @Override public int compare(GPXInfo i1, GPXInfo i2) { if (sortByMode == TracksSortByMode.BY_NAME_ASCENDING) { - return i1.getFileName().toLowerCase().compareTo(i2.getFileName().toLowerCase()); + return collator.compare(i1.getFileName(), i2.getFileName()); } else if (sortByMode == TracksSortByMode.BY_NAME_DESCENDING) { - return -i1.getFileName().toLowerCase().compareTo(i2.getFileName().toLowerCase()); + return -collator.compare(i1.getFileName(), i2.getFileName()); } else { long time1 = i1.getLastModified(); long time2 = i2.getLastModified(); if (time1 == time2) { - return i1.getFileName().toLowerCase().compareTo(i2.getFileName().toLowerCase()); + return collator.compare(i1.getFileName(), i2.getFileName()); } return -((time1 < time2) ? -1 : ((time1 == time2) ? 0 : 1)); } } }); + if (hasRecording) { + gpxInfoList.add(0, currentlyRecording); + } } private boolean showFoldersName() { diff --git a/OsmAnd/src/net/osmand/plus/myplaces/AvailableGPXFragment.java b/OsmAnd/src/net/osmand/plus/myplaces/AvailableGPXFragment.java index 9596bafb89..fae190ea01 100644 --- a/OsmAnd/src/net/osmand/plus/myplaces/AvailableGPXFragment.java +++ b/OsmAnd/src/net/osmand/plus/myplaces/AvailableGPXFragment.java @@ -45,6 +45,7 @@ import androidx.appcompat.widget.SearchView; import androidx.core.content.ContextCompat; import net.osmand.AndroidUtils; +import net.osmand.Collator; import net.osmand.FileUtils; import net.osmand.FileUtils.RenameCallback; import net.osmand.GPXUtilities; @@ -53,6 +54,7 @@ import net.osmand.GPXUtilities.GPXTrackAnalysis; import net.osmand.GPXUtilities.Track; import net.osmand.GPXUtilities.WptPt; import net.osmand.IndexConstants; +import net.osmand.OsmAndCollator; import net.osmand.data.PointDescription; import net.osmand.plus.ContextMenuAdapter; import net.osmand.plus.ContextMenuAdapter.ItemClickListener; @@ -85,7 +87,6 @@ import net.osmand.plus.settings.backend.OsmandSettings; import net.osmand.plus.settings.backend.OsmandSettings.TracksSortByMode; import java.io.File; -import java.text.Collator; import java.text.DateFormat; import java.util.ArrayList; import java.util.Arrays; @@ -165,8 +166,6 @@ public class AvailableGPXFragment extends OsmandExpandableListFragment implement super.onAttach(activity); this.app = (OsmandApplication) getActivity().getApplication(); sortByMode = app.getSettings().TRACKS_SORT_BY_MODE.get(); - final Collator collator = Collator.getInstance(); - collator.setStrength(Collator.SECONDARY); currentRecording = new GpxInfo(app.getSavingTrackHelper().getCurrentGpx(), getString(R.string.shared_string_currently_recording_track)); currentRecording.currentlyRecordingTrack = true; asyncLoader = new LoadGpxTask(); @@ -950,26 +949,9 @@ public class AvailableGPXFragment extends OsmandExpandableListFragment implement for (GpxInfo v : values) { allGpxAdapter.addLocalIndexInfo(v); } - // disable sort - // allGpxAdapter.sort(); allGpxAdapter.notifyDataSetChanged(); } - public void setResult(List result) { - this.result = result; - allGpxAdapter.clear(); - if (result != null) { - for (GpxInfo v : result) { - allGpxAdapter.addLocalIndexInfo(v); - } - // disable sort - // allGpxAdapter.sort(); - allGpxAdapter.refreshSelected(); - allGpxAdapter.notifyDataSetChanged(); - onPostExecute(result); - } - } - @Override protected void onPostExecute(List result) { this.result = result; @@ -989,17 +971,18 @@ public class AvailableGPXFragment extends OsmandExpandableListFragment implement } // This file could be sorted in different way for folders // now folders are also sorted by last modified date + final Collator collator = OsmAndCollator.primaryCollator(); Arrays.sort(listFiles, new Comparator() { @Override public int compare(File f1, File f2) { if (sortByMode == TracksSortByMode.BY_NAME_ASCENDING) { - return f1.getName().compareTo(f2.getName()); + return collator.compare(f1.getName(), (f2.getName())); } else if (sortByMode == TracksSortByMode.BY_NAME_DESCENDING) { - return -f1.getName().compareTo(f2.getName()); + return -collator.compare(f1.getName(), (f2.getName())); } else { // here we could guess date from file name '2017-08-30 ...' - first part date if (f1.lastModified() == f2.lastModified()) { - return -f1.getName().compareTo(f2.getName()); + return -collator.compare(f1.getName(), (f2.getName())); } return -((f1.lastModified() < f2.lastModified()) ? -1 : ((f1.lastModified() == f2.lastModified()) ? 0 : 1)); } @@ -1096,21 +1079,22 @@ public class AvailableGPXFragment extends OsmandExpandableListFragment implement public void refreshSelected() { selected.clear(); selected.addAll(getSelectedGpx()); + final Collator collator = OsmAndCollator.primaryCollator(); Collections.sort(selected, new Comparator() { @Override public int compare(GpxInfo i1, GpxInfo i2) { if (sortByMode == TracksSortByMode.BY_NAME_ASCENDING) { - return i1.getName().toLowerCase().compareTo(i2.getName().toLowerCase()); + return collator.compare(i1.getName(), i2.getName()); } else if (sortByMode == TracksSortByMode.BY_NAME_DESCENDING) { - return -i1.getName().toLowerCase().compareTo(i2.getName().toLowerCase()); + return -collator.compare(i1.getName(), i2.getName()); } else { if (i1.file == null || i2.file == null) { - return i1.getName().toLowerCase().compareTo(i2.getName().toLowerCase()); + return collator.compare(i1.getName(), i2.getName()); } long time1 = i1.file.lastModified(); long time2 = i2.file.lastModified(); if (time1 == time2) { - return i1.getName().toLowerCase().compareTo(i2.getName().toLowerCase()); + return collator.compare(i1.getName(), i2.getName()); } return -((time1 < time2) ? -1 : ((time1 == time2) ? 0 : 1)); } @@ -1175,15 +1159,6 @@ public class AvailableGPXFragment extends OsmandExpandableListFragment implement data.get(category.get(found)).add(info); } - public void sort() { - Collections.sort(category, new Comparator() { - @Override - public int compare(String lhs, String rhs) { - return lhs.toLowerCase().compareTo(rhs.toLowerCase()); - } - }); - } - @Override public GpxInfo getChild(int groupPosition, int childPosition) { if (isSelectedGroup(groupPosition)) { diff --git a/OsmAnd/src/net/osmand/plus/render/RendererRegistry.java b/OsmAnd/src/net/osmand/plus/render/RendererRegistry.java index 17dc572bc4..3f8ed81ba7 100644 --- a/OsmAnd/src/net/osmand/plus/render/RendererRegistry.java +++ b/OsmAnd/src/net/osmand/plus/render/RendererRegistry.java @@ -49,7 +49,7 @@ public class RendererRegistry { private RenderingRulesStorage defaultRender = null; private RenderingRulesStorage currentSelectedRender = null; - + private Map externalRenderers = new LinkedHashMap(); private Map internalRenderers = new LinkedHashMap(); diff --git a/OsmAnd/src/net/osmand/plus/settings/backend/ExportSettingsType.java b/OsmAnd/src/net/osmand/plus/settings/backend/ExportSettingsType.java new file mode 100644 index 0000000000..bda48d389f --- /dev/null +++ b/OsmAnd/src/net/osmand/plus/settings/backend/ExportSettingsType.java @@ -0,0 +1,11 @@ +package net.osmand.plus.settings.backend; + +public enum ExportSettingsType { + PROFILE, + QUICK_ACTIONS, + POI_TYPES, + MAP_SOURCES, + CUSTOM_RENDER_STYLE, + CUSTOM_ROUTING, + AVOID_ROADS +} diff --git a/OsmAnd/src/net/osmand/plus/settings/backend/OsmandSettings.java b/OsmAnd/src/net/osmand/plus/settings/backend/OsmandSettings.java index a3e5876a0b..9635acaec0 100644 --- a/OsmAnd/src/net/osmand/plus/settings/backend/OsmandSettings.java +++ b/OsmAnd/src/net/osmand/plus/settings/backend/OsmandSettings.java @@ -2008,6 +2008,7 @@ public class OsmandSettings { public final OsmandPreference LIVE_UPDATES_PURCHASE_CANCELLED_SECOND_DLG_SHOWN = new BooleanPreference("live_updates_purchase_cancelled_second_dlg_shown", false).makeGlobal(); public final OsmandPreference FULL_VERSION_PURCHASED = new BooleanPreference("billing_full_version_purchased", false).makeGlobal(); public final OsmandPreference DEPTH_CONTOURS_PURCHASED = new BooleanPreference("billing_sea_depth_purchased", false).makeGlobal(); + public final OsmandPreference CONTOUR_LINES_PURCHASED = new BooleanPreference("billing_srtm_purchased", false).makeGlobal(); public final OsmandPreference EMAIL_SUBSCRIBED = new BooleanPreference("email_subscribed", false).makeGlobal(); public final OsmandPreference DISCOUNT_ID = new IntPreference("discount_id", 0).makeGlobal(); @@ -2220,6 +2221,8 @@ public class OsmandSettings { } }.makeProfile().cache(); + public final OsmandPreference SHOW_START_FINISH_ICONS = new BooleanPreference("show_start_finish_icons", true).makeGlobal().cache(); + public final OsmandPreference GPX_ROUTE_CALC_OSMAND_PARTS = new BooleanPreference("gpx_routing_calculate_osmand_route", true).makeGlobal().cache(); // public final OsmandPreference GPX_CALCULATE_RTEPT = new BooleanPreference("gpx_routing_calculate_rtept", true).makeGlobal().cache(); public final OsmandPreference GPX_ROUTE_CALC = new BooleanPreference("calc_gpx_route", false).makeGlobal().cache(); diff --git a/OsmAnd/src/net/osmand/plus/settings/backend/SettingsHelper.java b/OsmAnd/src/net/osmand/plus/settings/backend/SettingsHelper.java index 2b5b4282d4..0172531139 100644 --- a/OsmAnd/src/net/osmand/plus/settings/backend/SettingsHelper.java +++ b/OsmAnd/src/net/osmand/plus/settings/backend/SettingsHelper.java @@ -100,6 +100,8 @@ public class SettingsHelper { public static final int VERSION = 1; + public static final String SETTINGS_TYPE_LIST_KEY = "settings_type_list_key"; + public static final String REPLACE_KEY = "replace"; public static final String SETTINGS_LATEST_CHANGES_KEY = "settings_latest_changes"; public static final String SETTINGS_VERSION_KEY = "settings_version"; @@ -2928,4 +2930,108 @@ public class SettingsHelper { CHECK_DUPLICATES, IMPORT } + + public List getFilteredSettingsItems(Map> additionalData, + List settingsTypes) { + List settingsItems = new ArrayList<>(); + for (ExportSettingsType settingsType : settingsTypes) { + List settingsDataObjects = additionalData.get(settingsType); + if (settingsDataObjects != null) { + settingsItems.addAll(prepareAdditionalSettingsItems(new ArrayList<>(settingsDataObjects))); + } + } + return settingsItems; + } + + public Map> getAdditionalData() { + Map> dataList = new HashMap<>(); + + QuickActionRegistry registry = app.getQuickActionRegistry(); + List actionsList = registry.getQuickActions(); + if (!actionsList.isEmpty()) { + dataList.put(ExportSettingsType.QUICK_ACTIONS, actionsList); + } + + List poiList = app.getPoiFilters().getUserDefinedPoiFilters(false); + if (!poiList.isEmpty()) { + dataList.put(ExportSettingsType.POI_TYPES, poiList); + } + + List iTileSources = new ArrayList<>(); + Set tileSourceNames = app.getSettings().getTileSourceEntries(true).keySet(); + for (String name : tileSourceNames) { + File f = app.getAppPath(IndexConstants.TILES_INDEX_DIR + name); + if (f != null) { + ITileSource template; + if (f.getName().endsWith(SQLiteTileSource.EXT)) { + template = new SQLiteTileSource(app, f, TileSourceManager.getKnownSourceTemplates()); + } else { + template = TileSourceManager.createTileSourceTemplate(f); + } + if (template.getUrlTemplate() != null) { + iTileSources.add(template); + } + } + } + if (!iTileSources.isEmpty()) { + dataList.put(ExportSettingsType.MAP_SOURCES, iTileSources); + } + + Map externalRenderers = app.getRendererRegistry().getExternalRenderers(); + if (!externalRenderers.isEmpty()) { + dataList.put(ExportSettingsType.CUSTOM_RENDER_STYLE, new ArrayList<>(externalRenderers.values())); + } + + File routingProfilesFolder = app.getAppPath(IndexConstants.ROUTING_PROFILES_DIR); + if (routingProfilesFolder.exists() && routingProfilesFolder.isDirectory()) { + File[] fl = routingProfilesFolder.listFiles(); + if (fl != null && fl.length > 0) { + dataList.put(ExportSettingsType.CUSTOM_ROUTING, Arrays.asList(fl)); + } + } + + Map impassableRoads = app.getAvoidSpecificRoads().getImpassableRoads(); + if (!impassableRoads.isEmpty()) { + dataList.put(ExportSettingsType.AVOID_ROADS, new ArrayList<>(impassableRoads.values())); + } + return dataList; + } + + public List prepareAdditionalSettingsItems(List data) { + List settingsItems = new ArrayList<>(); + List quickActions = new ArrayList<>(); + List poiUIFilters = new ArrayList<>(); + List tileSourceTemplates = new ArrayList<>(); + List avoidRoads = new ArrayList<>(); + for (Object object : data) { + if (object instanceof QuickAction) { + quickActions.add((QuickAction) object); + } else if (object instanceof PoiUIFilter) { + poiUIFilters.add((PoiUIFilter) object); + } else if (object instanceof TileSourceTemplate || object instanceof SQLiteTileSource) { + tileSourceTemplates.add((ITileSource) object); + } else if (object instanceof File) { + try { + settingsItems.add(new FileSettingsItem(app, (File) object)); + } catch (IllegalArgumentException e) { + LOG.warn("Trying to export unsuported file type", e); + } + } else if (object instanceof AvoidRoadInfo) { + avoidRoads.add((AvoidRoadInfo) object); + } + } + if (!quickActions.isEmpty()) { + settingsItems.add(new QuickActionsSettingsItem(app, quickActions)); + } + if (!poiUIFilters.isEmpty()) { + settingsItems.add(new PoiUiFiltersSettingsItem(app, poiUIFilters)); + } + if (!tileSourceTemplates.isEmpty()) { + settingsItems.add(new MapSourcesSettingsItem(app, tileSourceTemplates)); + } + if (!avoidRoads.isEmpty()) { + settingsItems.add(new AvoidRoadsSettingsItem(app, avoidRoads)); + } + return settingsItems; + } } \ No newline at end of file diff --git a/OsmAnd/src/net/osmand/plus/settings/fragments/ExportImportSettingsAdapter.java b/OsmAnd/src/net/osmand/plus/settings/fragments/ExportImportSettingsAdapter.java index 18e8900203..01199ccff2 100644 --- a/OsmAnd/src/net/osmand/plus/settings/fragments/ExportImportSettingsAdapter.java +++ b/OsmAnd/src/net/osmand/plus/settings/fragments/ExportImportSettingsAdapter.java @@ -16,6 +16,7 @@ import net.osmand.AndroidUtils; import net.osmand.IndexConstants; import net.osmand.PlatformUtil; import net.osmand.map.ITileSource; +import net.osmand.plus.settings.backend.ExportSettingsType; import net.osmand.plus.settings.backend.ApplicationMode.ApplicationModeBean; import net.osmand.plus.settings.backend.ApplicationMode; import net.osmand.plus.OsmandApplication; @@ -50,8 +51,8 @@ class ExportImportSettingsAdapter extends OsmandBaseExpandableListAdapter { private OsmandApplication app; private UiUtilities uiUtilities; private List data; - private Map> itemsMap; - private List itemsTypes; + private Map> itemsMap; + private List itemsTypes; private boolean nightMode; private boolean importState; private int activeColorRes; @@ -82,7 +83,7 @@ class ExportImportSettingsAdapter extends OsmandBaseExpandableListAdapter { } boolean isLastGroup = groupPosition == getGroupCount() - 1; - final Type type = itemsTypes.get(groupPosition); + final ExportSettingsType type = itemsTypes.get(groupPosition); TextView titleTv = group.findViewById(R.id.title_tv); TextView subTextTv = group.findViewById(R.id.sub_text_tv); @@ -146,7 +147,7 @@ class ExportImportSettingsAdapter extends OsmandBaseExpandableListAdapter { boolean isLastGroup = groupPosition == getGroupCount() - 1; boolean itemSelected = data.contains(currentItem); - final Type type = itemsTypes.get(groupPosition); + final ExportSettingsType type = itemsTypes.get(groupPosition); TextView title = child.findViewById(R.id.title_tv); TextView subText = child.findViewById(R.id.sub_title_tv); @@ -299,7 +300,7 @@ class ExportImportSettingsAdapter extends OsmandBaseExpandableListAdapter { return app.getString(R.string.n_items_of_z, String.valueOf(amount), String.valueOf(listItems.size())); } - private int getGroupTitle(Type type) { + private int getGroupTitle(ExportSettingsType type) { switch (type) { case PROFILE: return R.string.shared_string_profiles; @@ -320,15 +321,15 @@ class ExportImportSettingsAdapter extends OsmandBaseExpandableListAdapter { } } - private void setupIcon(ImageView icon, int iconRes, boolean itemSelected) { - if (itemSelected) { - icon.setImageDrawable(uiUtilities.getIcon(iconRes, activeColorRes)); - } else { - icon.setImageDrawable(uiUtilities.getIcon(iconRes, nightMode)); - } - } + private void setupIcon(ImageView icon, int iconRes, boolean itemSelected) { + if (itemSelected) { + icon.setImageDrawable(uiUtilities.getIcon(iconRes, activeColorRes)); + } else { + icon.setImageDrawable(uiUtilities.getIcon(iconRes, nightMode)); + } + } - public void updateSettingsList(Map> itemsMap) { + public void updateSettingsList(Map> itemsMap) { this.itemsMap = itemsMap; this.itemsTypes = new ArrayList<>(itemsMap.keySet()); Collections.sort(itemsTypes); @@ -354,14 +355,4 @@ class ExportImportSettingsAdapter extends OsmandBaseExpandableListAdapter { List getData() { return this.data; } - - public enum Type { - PROFILE, - QUICK_ACTIONS, - POI_TYPES, - MAP_SOURCES, - CUSTOM_RENDER_STYLE, - CUSTOM_ROUTING, - AVOID_ROADS - } } diff --git a/OsmAnd/src/net/osmand/plus/settings/fragments/ExportProfileBottomSheet.java b/OsmAnd/src/net/osmand/plus/settings/fragments/ExportProfileBottomSheet.java index a09a8ba435..c95f7d578c 100644 --- a/OsmAnd/src/net/osmand/plus/settings/fragments/ExportProfileBottomSheet.java +++ b/OsmAnd/src/net/osmand/plus/settings/fragments/ExportProfileBottomSheet.java @@ -21,45 +21,30 @@ import androidx.fragment.app.FragmentActivity; import androidx.fragment.app.FragmentManager; import net.osmand.AndroidUtils; +import net.osmand.FileUtils; import net.osmand.IndexConstants; import net.osmand.PlatformUtil; -import net.osmand.data.LatLon; -import net.osmand.map.ITileSource; -import net.osmand.map.TileSourceManager; -import net.osmand.map.TileSourceManager.TileSourceTemplate; import net.osmand.plus.OsmandApplication; import net.osmand.plus.R; -import net.osmand.plus.SQLiteTileSource; import net.osmand.plus.UiUtilities; import net.osmand.plus.base.bottomsheetmenu.BaseBottomSheetItem; import net.osmand.plus.base.bottomsheetmenu.BottomSheetItemWithCompoundButton; import net.osmand.plus.base.bottomsheetmenu.SimpleBottomSheetItem; import net.osmand.plus.base.bottomsheetmenu.simpleitems.TitleItem; -import net.osmand.plus.helpers.AvoidSpecificRoads.AvoidRoadInfo; -import net.osmand.plus.poi.PoiUIFilter; -import net.osmand.plus.quickaction.QuickAction; -import net.osmand.plus.quickaction.QuickActionRegistry; +import net.osmand.plus.settings.backend.ExportSettingsType; import net.osmand.plus.settings.backend.ApplicationMode; import net.osmand.plus.settings.backend.SettingsHelper; -import net.osmand.plus.settings.backend.SettingsHelper.AvoidRoadsSettingsItem; -import net.osmand.plus.settings.backend.SettingsHelper.FileSettingsItem; -import net.osmand.plus.settings.backend.SettingsHelper.MapSourcesSettingsItem; -import net.osmand.plus.settings.backend.SettingsHelper.PoiUiFiltersSettingsItem; import net.osmand.plus.settings.backend.SettingsHelper.ProfileSettingsItem; -import net.osmand.plus.settings.backend.SettingsHelper.QuickActionsSettingsItem; import net.osmand.plus.settings.backend.SettingsHelper.SettingsItem; import net.osmand.plus.settings.bottomsheets.BasePreferenceBottomSheet; -import net.osmand.plus.settings.fragments.ExportImportSettingsAdapter.Type; import org.apache.commons.logging.Log; import java.io.File; import java.util.ArrayList; -import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Set; public class ExportProfileBottomSheet extends BasePreferenceBottomSheet { @@ -72,7 +57,7 @@ public class ExportProfileBottomSheet extends BasePreferenceBottomSheet { private OsmandApplication app; private ApplicationMode profile; - private Map> dataList = new HashMap<>(); + private Map> dataList = new HashMap<>(); private ExportImportSettingsAdapter adapter; private SettingsHelper.SettingsExportListener exportListener; @@ -86,7 +71,7 @@ public class ExportProfileBottomSheet extends BasePreferenceBottomSheet { super.onCreate(savedInstanceState); app = requiredMyApplication(); profile = getAppMode(); - dataList = getAdditionalData(); + dataList = app.getSettingsHelper().getAdditionalData(); if (savedInstanceState != null) { includeAdditionalData = savedInstanceState.getBoolean(INCLUDE_ADDITIONAL_DATA_KEY); exportingProfile = savedInstanceState.getBoolean(EXPORTING_PROFILE_KEY); @@ -145,7 +130,7 @@ public class ExportProfileBottomSheet extends BasePreferenceBottomSheet { topSwitchDivider.setVisibility(includeAdditionalData ? View.VISIBLE : View.GONE); bottomSwitchDivider.setVisibility(includeAdditionalData ? View.VISIBLE : View.GONE); if (includeAdditionalData) { - adapter.updateSettingsList(getAdditionalData()); + adapter.updateSettingsList(app.getSettingsHelper().getAdditionalData()); adapter.selectAll(true); } else { adapter.selectAll(false); @@ -223,104 +208,11 @@ public class ExportProfileBottomSheet extends BasePreferenceBottomSheet { } } - private Map> getAdditionalData() { - Map> dataList = new HashMap<>(); - - - QuickActionRegistry registry = app.getQuickActionRegistry(); - List actionsList = registry.getQuickActions(); - if (!actionsList.isEmpty()) { - dataList.put(Type.QUICK_ACTIONS, actionsList); - } - - List poiList = app.getPoiFilters().getUserDefinedPoiFilters(false); - if (!poiList.isEmpty()) { - dataList.put(Type.POI_TYPES, poiList); - } - - List iTileSources = new ArrayList<>(); - Set tileSourceNames = app.getSettings().getTileSourceEntries(true).keySet(); - for (String name : tileSourceNames) { - File f = app.getAppPath(IndexConstants.TILES_INDEX_DIR + name); - if (f != null) { - ITileSource template; - if (f.getName().endsWith(SQLiteTileSource.EXT)) { - template = new SQLiteTileSource(app, f, TileSourceManager.getKnownSourceTemplates()); - } else { - template = TileSourceManager.createTileSourceTemplate(f); - } - if (template.getUrlTemplate() != null) { - iTileSources.add(template); - } - } - } - if (!iTileSources.isEmpty()) { - dataList.put(Type.MAP_SOURCES, iTileSources); - } - - Map externalRenderers = app.getRendererRegistry().getExternalRenderers(); - if (!externalRenderers.isEmpty()) { - dataList.put(Type.CUSTOM_RENDER_STYLE, new ArrayList<>(externalRenderers.values())); - } - - File routingProfilesFolder = app.getAppPath(IndexConstants.ROUTING_PROFILES_DIR); - if (routingProfilesFolder.exists() && routingProfilesFolder.isDirectory()) { - File[] fl = routingProfilesFolder.listFiles(); - if (fl != null && fl.length > 0) { - dataList.put(Type.CUSTOM_ROUTING, Arrays.asList(fl)); - } - } - - Map impassableRoads = app.getAvoidSpecificRoads().getImpassableRoads(); - if (!impassableRoads.isEmpty()) { - dataList.put(Type.AVOID_ROADS, new ArrayList<>(impassableRoads.values())); - } - return dataList; - } - private List prepareSettingsItemsForExport() { List settingsItems = new ArrayList<>(); settingsItems.add(new ProfileSettingsItem(app, profile)); if (includeAdditionalData) { - settingsItems.addAll(prepareAdditionalSettingsItems()); - } - return settingsItems; - } - - private List prepareAdditionalSettingsItems() { - List settingsItems = new ArrayList<>(); - List quickActions = new ArrayList<>(); - List poiUIFilters = new ArrayList<>(); - List tileSourceTemplates = new ArrayList<>(); - List avoidRoads = new ArrayList<>(); - for (Object object : adapter.getData()) { - if (object instanceof QuickAction) { - quickActions.add((QuickAction) object); - } else if (object instanceof PoiUIFilter) { - poiUIFilters.add((PoiUIFilter) object); - } else if (object instanceof TileSourceTemplate || object instanceof SQLiteTileSource) { - tileSourceTemplates.add((ITileSource) object); - } else if (object instanceof File) { - try { - settingsItems.add(new FileSettingsItem(app, (File) object)); - } catch (IllegalArgumentException e) { - LOG.warn("Trying to export unsuported file type", e); - } - } else if (object instanceof AvoidRoadInfo) { - avoidRoads.add((AvoidRoadInfo) object); - } - } - if (!quickActions.isEmpty()) { - settingsItems.add(new QuickActionsSettingsItem(app, quickActions)); - } - if (!poiUIFilters.isEmpty()) { - settingsItems.add(new PoiUiFiltersSettingsItem(app, poiUIFilters)); - } - if (!tileSourceTemplates.isEmpty()) { - settingsItems.add(new MapSourcesSettingsItem(app, tileSourceTemplates)); - } - if (!avoidRoads.isEmpty()) { - settingsItems.add(new AvoidRoadsSettingsItem(app, avoidRoads)); + settingsItems.addAll(app.getSettingsHelper().prepareAdditionalSettingsItems(adapter.getData())); } return settingsItems; } @@ -329,7 +221,7 @@ public class ExportProfileBottomSheet extends BasePreferenceBottomSheet { if (app != null) { exportingProfile = true; showExportProgressDialog(); - File tempDir = getTempDir(); + File tempDir = FileUtils.getTempDir(app); String fileName = profile.toHumanString(); app.getSettingsHelper().exportSettings(tempDir, fileName, getSettingsExportListener(), prepareSettingsItemsForExport(), true); } @@ -391,19 +283,11 @@ public class ExportProfileBottomSheet extends BasePreferenceBottomSheet { } private File getExportFile() { - File tempDir = getTempDir(); + File tempDir = FileUtils.getTempDir(app); String fileName = profile.toHumanString(); return new File(tempDir, fileName + IndexConstants.OSMAND_SETTINGS_FILE_EXT); } - private File getTempDir() { - File tempDir = app.getAppPath(IndexConstants.TEMP_DIR); - if (!tempDir.exists()) { - tempDir.mkdirs(); - } - return tempDir; - } - private void shareProfile(@NonNull File file, @NonNull ApplicationMode profile) { try { final Intent sendIntent = new Intent(); diff --git a/OsmAnd/src/net/osmand/plus/settings/fragments/GeneralProfileSettingsFragment.java b/OsmAnd/src/net/osmand/plus/settings/fragments/GeneralProfileSettingsFragment.java index e870fed149..0cd1e628bb 100644 --- a/OsmAnd/src/net/osmand/plus/settings/fragments/GeneralProfileSettingsFragment.java +++ b/OsmAnd/src/net/osmand/plus/settings/fragments/GeneralProfileSettingsFragment.java @@ -123,7 +123,7 @@ public class GeneralProfileSettingsFragment extends BaseSettingsFragment impleme if (settings.isSystemDefaultThemeUsedForMode(mode)) { iconId = R.drawable.ic_action_android; } else { - iconId = settings.isLightContent() ? R.drawable.ic_action_sun : R.drawable.ic_action_moon; + iconId = settings.isLightContentForMode(mode) ? R.drawable.ic_action_sun : R.drawable.ic_action_moon; } return getActiveIcon(iconId); } diff --git a/OsmAnd/src/net/osmand/plus/settings/fragments/ImportCompleteFragment.java b/OsmAnd/src/net/osmand/plus/settings/fragments/ImportCompleteFragment.java index a18e2f6aa9..e152d5ea68 100644 --- a/OsmAnd/src/net/osmand/plus/settings/fragments/ImportCompleteFragment.java +++ b/OsmAnd/src/net/osmand/plus/settings/fragments/ImportCompleteFragment.java @@ -22,6 +22,8 @@ import androidx.recyclerview.widget.RecyclerView; import net.osmand.AndroidUtils; import net.osmand.plus.OsmandApplication; import net.osmand.plus.R; +import net.osmand.plus.helpers.ImportHelper; +import net.osmand.plus.settings.backend.ExportSettingsType; import net.osmand.plus.settings.backend.SettingsHelper.SettingsItem; import net.osmand.plus.UiUtilities; import net.osmand.plus.activities.MapActivity; @@ -31,7 +33,6 @@ import net.osmand.plus.dialogs.SelectMapStyleBottomSheetDialogFragment; import net.osmand.plus.quickaction.QuickActionListFragment; import net.osmand.plus.routepreparationmenu.AvoidRoadsBottomSheetDialogFragment; import net.osmand.plus.search.QuickSearchDialogFragment; -import net.osmand.plus.settings.fragments.ExportImportSettingsAdapter.Type; import java.util.List; @@ -117,11 +118,11 @@ public class ImportCompleteFragment extends BaseOsmAndFragment { if (settingsItems != null) { ImportedSettingsItemsAdapter adapter = new ImportedSettingsItemsAdapter( app, - ImportSettingsFragment.getSettingsToOperate(settingsItems, true), + ImportHelper.getSettingsToOperate(settingsItems, true), nightMode, new ImportedSettingsItemsAdapter.OnItemClickListener() { @Override - public void onItemClick(Type type) { + public void onItemClick(ExportSettingsType type) { navigateTo(type); } }); @@ -137,7 +138,7 @@ public class ImportCompleteFragment extends BaseOsmAndFragment { } } - private void navigateTo(Type type) { + private void navigateTo(ExportSettingsType type) { FragmentManager fm = getFragmentManager(); Activity activity = requireActivity(); if (fm == null || fm.isStateSaved()) { diff --git a/OsmAnd/src/net/osmand/plus/settings/fragments/ImportSettingsFragment.java b/OsmAnd/src/net/osmand/plus/settings/fragments/ImportSettingsFragment.java index 284964b88d..7e8f6bb934 100644 --- a/OsmAnd/src/net/osmand/plus/settings/fragments/ImportSettingsFragment.java +++ b/OsmAnd/src/net/osmand/plus/settings/fragments/ImportSettingsFragment.java @@ -34,11 +34,11 @@ import net.osmand.plus.AppInitializer; import net.osmand.plus.OsmandApplication; import net.osmand.plus.R; import net.osmand.plus.SQLiteTileSource; +import net.osmand.plus.settings.backend.ExportSettingsType; import net.osmand.plus.settings.backend.ApplicationMode.ApplicationModeBean; import net.osmand.plus.settings.backend.SettingsHelper; import net.osmand.plus.settings.backend.SettingsHelper.AvoidRoadsSettingsItem; import net.osmand.plus.settings.backend.SettingsHelper.FileSettingsItem; -import net.osmand.plus.settings.backend.SettingsHelper.FileSettingsItem.FileSubtype; import net.osmand.plus.settings.backend.SettingsHelper.ImportAsyncTask; import net.osmand.plus.settings.backend.SettingsHelper.ImportType; import net.osmand.plus.settings.backend.SettingsHelper.MapSourcesSettingsItem; @@ -53,7 +53,6 @@ import net.osmand.plus.base.BaseOsmAndFragment; import net.osmand.plus.helpers.AvoidSpecificRoads.AvoidRoadInfo; import net.osmand.plus.poi.PoiUIFilter; import net.osmand.plus.quickaction.QuickAction; -import net.osmand.plus.settings.fragments.ExportImportSettingsAdapter.Type; import net.osmand.plus.widgets.TextViewEx; import net.osmand.util.Algorithms; @@ -65,6 +64,8 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import static net.osmand.plus.helpers.ImportHelper.getSettingsToOperate; + public class ImportSettingsFragment extends BaseOsmAndFragment implements View.OnClickListener { @@ -180,7 +181,7 @@ public class ImportSettingsFragment extends BaseOsmAndFragment } adapter = new ExportImportSettingsAdapter(app, nightMode, true); - Map> itemsMap = new HashMap<>(); + Map> itemsMap = new HashMap<>(); if (settingsItems != null) { itemsMap = getSettingsToOperate(settingsItems, false); adapter.updateSettingsList(itemsMap); @@ -196,7 +197,7 @@ public class ImportSettingsFragment extends BaseOsmAndFragment } else { toolbarLayout.setTitle(getString(R.string.shared_string_import)); } - if (itemsMap.size() == 1 && itemsMap.containsKey(Type.PROFILE)) { + if (itemsMap.size() == 1 && itemsMap.containsKey(ExportSettingsType.PROFILE)) { expandableList.expandGroup(0); } } @@ -266,11 +267,7 @@ public class ImportSettingsFragment extends BaseOsmAndFragment FragmentManager fm = getFragmentManager(); if (succeed) { app.getRendererRegistry().updateExternalRenderers(); - AppInitializer.loadRoutingFiles(app, new AppInitializer.LoadRoutingFilesCallback() { - @Override - public void onRoutingFilesLoaded() { - } - }); + AppInitializer.loadRoutingFiles(app, null); if (fm != null && file != null) { ImportCompleteFragment.showInstance(fm, items, file.getName()); } @@ -420,89 +417,6 @@ public class ImportSettingsFragment extends BaseOsmAndFragment return settingsItems; } - public static Map> getSettingsToOperate(List settingsItems, boolean importComplete) { - Map> settingsToOperate = new HashMap<>(); - List profiles = new ArrayList<>(); - List quickActions = new ArrayList<>(); - List poiUIFilters = new ArrayList<>(); - List tileSourceTemplates = new ArrayList<>(); - List routingFilesList = new ArrayList<>(); - List renderFilesList = new ArrayList<>(); - List avoidRoads = new ArrayList<>(); - for (SettingsItem item : settingsItems) { - switch (item.getType()) { - case PROFILE: - profiles.add(((ProfileSettingsItem) item).getModeBean()); - break; - case FILE: - FileSettingsItem fileItem = (FileSettingsItem) item; - if (fileItem.getSubtype() == FileSubtype.RENDERING_STYLE) { - renderFilesList.add(fileItem.getFile()); - } else if (fileItem.getSubtype() == FileSubtype.ROUTING_CONFIG) { - routingFilesList.add(fileItem.getFile()); - } - break; - case QUICK_ACTIONS: - QuickActionsSettingsItem quickActionsItem = (QuickActionsSettingsItem) item; - if (importComplete) { - quickActions.addAll(quickActionsItem.getAppliedItems()); - } else { - quickActions.addAll(quickActionsItem.getItems()); - } - break; - case POI_UI_FILTERS: - PoiUiFiltersSettingsItem poiUiFilterItem = (PoiUiFiltersSettingsItem) item; - if (importComplete) { - poiUIFilters.addAll(poiUiFilterItem.getAppliedItems()); - } else { - poiUIFilters.addAll(poiUiFilterItem.getItems()); - } - break; - case MAP_SOURCES: - MapSourcesSettingsItem mapSourcesItem = (MapSourcesSettingsItem) item; - if (importComplete) { - tileSourceTemplates.addAll(mapSourcesItem.getAppliedItems()); - } else { - tileSourceTemplates.addAll(mapSourcesItem.getItems()); - } - break; - case AVOID_ROADS: - AvoidRoadsSettingsItem avoidRoadsItem = (AvoidRoadsSettingsItem) item; - if (importComplete) { - avoidRoads.addAll(avoidRoadsItem.getAppliedItems()); - } else { - avoidRoads.addAll(avoidRoadsItem.getItems()); - } - break; - default: - break; - } - } - - if (!profiles.isEmpty()) { - settingsToOperate.put(Type.PROFILE, profiles); - } - if (!quickActions.isEmpty()) { - settingsToOperate.put(Type.QUICK_ACTIONS, quickActions); - } - if (!poiUIFilters.isEmpty()) { - settingsToOperate.put(Type.POI_TYPES, poiUIFilters); - } - if (!tileSourceTemplates.isEmpty()) { - settingsToOperate.put(Type.MAP_SOURCES, tileSourceTemplates); - } - if (!renderFilesList.isEmpty()) { - settingsToOperate.put(Type.CUSTOM_RENDER_STYLE, renderFilesList); - } - if (!routingFilesList.isEmpty()) { - settingsToOperate.put(Type.CUSTOM_ROUTING, routingFilesList); - } - if (!avoidRoads.isEmpty()) { - settingsToOperate.put(Type.AVOID_ROADS, avoidRoads); - } - return settingsToOperate; - } - @Override public int getStatusBarColorId() { return nightMode ? R.color.status_bar_color_dark : R.color.status_bar_color_light; diff --git a/OsmAnd/src/net/osmand/plus/settings/fragments/ImportedSettingsItemsAdapter.java b/OsmAnd/src/net/osmand/plus/settings/fragments/ImportedSettingsItemsAdapter.java index 270391c619..e663e6f189 100644 --- a/OsmAnd/src/net/osmand/plus/settings/fragments/ImportedSettingsItemsAdapter.java +++ b/OsmAnd/src/net/osmand/plus/settings/fragments/ImportedSettingsItemsAdapter.java @@ -14,7 +14,7 @@ import net.osmand.plus.OsmandApplication; import net.osmand.plus.R; import net.osmand.plus.UiUtilities; import net.osmand.plus.helpers.FontCache; -import net.osmand.plus.settings.fragments.ExportImportSettingsAdapter.Type; +import net.osmand.plus.settings.backend.ExportSettingsType; import java.util.ArrayList; @@ -25,15 +25,15 @@ import java.util.Map; public class ImportedSettingsItemsAdapter extends RecyclerView.Adapter { - private Map> itemsMap; - private List itemsTypes; + private Map> itemsMap; + private List itemsTypes; private UiUtilities uiUtils; private OsmandApplication app; private boolean nightMode; private OnItemClickListener listener; - ImportedSettingsItemsAdapter(@NonNull OsmandApplication app, Map> itemsMap, - boolean nightMode, OnItemClickListener listener) { + ImportedSettingsItemsAdapter(@NonNull OsmandApplication app, Map> itemsMap, + boolean nightMode, OnItemClickListener listener) { this.app = app; this.itemsMap = itemsMap; this.nightMode = nightMode; @@ -53,7 +53,7 @@ public class ImportedSettingsItemsAdapter extends @Override public void onBindViewHolder(@NonNull ItemViewHolder holder, int position) { - final Type currentItemType = itemsTypes.get(position); + final ExportSettingsType currentItemType = itemsTypes.get(position); boolean isLastItem = itemsTypes.size() - 1 == position; int activeColorRes = nightMode ? R.color.active_color_primary_dark @@ -130,6 +130,6 @@ public class ImportedSettingsItemsAdapter extends } interface OnItemClickListener { - void onItemClick(Type type); + void onItemClick(ExportSettingsType type); } } diff --git a/OsmAnd/src/net/osmand/plus/settings/fragments/MainSettingsFragment.java b/OsmAnd/src/net/osmand/plus/settings/fragments/MainSettingsFragment.java index 6aac50bc9c..1c176e3172 100644 --- a/OsmAnd/src/net/osmand/plus/settings/fragments/MainSettingsFragment.java +++ b/OsmAnd/src/net/osmand/plus/settings/fragments/MainSettingsFragment.java @@ -6,6 +6,7 @@ import android.os.Bundle; import android.view.View; import androidx.annotation.ColorRes; +import androidx.core.content.ContextCompat; import androidx.fragment.app.FragmentActivity; import androidx.fragment.app.FragmentManager; import androidx.preference.Preference; @@ -86,7 +87,8 @@ public class MainSettingsFragment extends BaseSettingsFragment { if (CONFIGURE_PROFILE.equals(key)) { View selectedProfile = holder.itemView.findViewById(R.id.selectable_list_item); if (selectedProfile != null) { - int activeProfileColor = getActiveProfileColor(); + int activeProfileColorId = getSelectedAppMode().getIconColorInfo().getColor(isNightMode()); + int activeProfileColor = ContextCompat.getColor(app, activeProfileColorId); Drawable backgroundDrawable = new ColorDrawable(UiUtilities.getColorWithAlpha(activeProfileColor, 0.15f)); AndroidUtils.setBackground(selectedProfile, backgroundDrawable); } @@ -153,9 +155,8 @@ public class MainSettingsFragment extends BaseSettingsFragment { ApplicationMode selectedMode = app.getSettings().APPLICATION_MODE.get(); String title = selectedMode.toHumanString(); String profileType = getAppModeDescription(getContext(), selectedMode); - int iconRes = selectedMode.getIconRes(); Preference configureProfile = findPreference(CONFIGURE_PROFILE); - configureProfile.setIcon(getPaintedIcon(iconRes, getActiveProfileColor())); + configureProfile.setIcon(getAppProfilesIcon(selectedMode, true)); configureProfile.setTitle(title); configureProfile.setSummary(profileType); } diff --git a/OsmAnd/src/net/osmand/plus/settings/fragments/RouteParametersFragment.java b/OsmAnd/src/net/osmand/plus/settings/fragments/RouteParametersFragment.java index 198e6e13eb..cfd6874d89 100644 --- a/OsmAnd/src/net/osmand/plus/settings/fragments/RouteParametersFragment.java +++ b/OsmAnd/src/net/osmand/plus/settings/fragments/RouteParametersFragment.java @@ -216,8 +216,7 @@ public class RouteParametersFragment extends BaseSettingsFragment implements OnP } if (preferParameters.size() > 0) { String title = getString(R.string.prefer_in_routing_title); - String descr = getString(R.string.prefer_in_routing_descr); - MultiSelectBooleanPreference preferRouting = createRoutingBooleanMultiSelectPref(PREFER_ROUTING_PARAMETER_PREFIX, title, descr, preferParameters); + MultiSelectBooleanPreference preferRouting = createRoutingBooleanMultiSelectPref(PREFER_ROUTING_PARAMETER_PREFIX, title, "", preferParameters); screen.addPreference(preferRouting); } if (reliefFactorParameters.size() > 0) { diff --git a/OsmAnd/src/net/osmand/plus/srtmplugin/SRTMPlugin.java b/OsmAnd/src/net/osmand/plus/srtmplugin/SRTMPlugin.java index 808c541523..3690afbc3d 100644 --- a/OsmAnd/src/net/osmand/plus/srtmplugin/SRTMPlugin.java +++ b/OsmAnd/src/net/osmand/plus/srtmplugin/SRTMPlugin.java @@ -95,7 +95,9 @@ public class SRTMPlugin extends OsmandPlugin { @Override protected boolean pluginAvailable(OsmandApplication app) { - return super.pluginAvailable(app) || InAppPurchaseHelper.isSubscribedToLiveUpdates(app); + return super.pluginAvailable(app) + || InAppPurchaseHelper.isSubscribedToLiveUpdates(app) + || InAppPurchaseHelper.isContourLinesPurchased(app); } @Override @@ -359,7 +361,7 @@ public class SRTMPlugin extends OsmandPlugin { .setTitleId(R.string.shared_string_terrain, mapActivity) .setDescription(app.getString(terrainMode == TerrainMode.HILLSHADE ? R.string.shared_string_hillshade - : R.string.shared_string_slope)) + : R.string.download_slope_maps)) .setSelected(terrainEnabled) .setColor(terrainEnabled ? R.color.osmand_orange : ContextMenuItem.INVALID_ID) .setIcon(R.drawable.ic_action_hillshade_dark) diff --git a/OsmAnd/src/net/osmand/plus/track/ShowStartFinishCard.java b/OsmAnd/src/net/osmand/plus/track/ShowStartFinishCard.java new file mode 100644 index 0000000000..3941bdbb15 --- /dev/null +++ b/OsmAnd/src/net/osmand/plus/track/ShowStartFinishCard.java @@ -0,0 +1,59 @@ +package net.osmand.plus.track; + +import android.view.View; +import android.widget.CompoundButton; +import android.widget.TextView; + +import androidx.annotation.NonNull; + +import net.osmand.plus.R; +import net.osmand.plus.activities.MapActivity; +import net.osmand.plus.helpers.AndroidUiHelper; +import net.osmand.plus.routepreparationmenu.cards.BaseCard; +import net.osmand.plus.settings.backend.OsmandSettings; +import net.osmand.plus.settings.backend.OsmandSettings.OsmandPreference; + +class ShowStartFinishCard extends BaseCard { + + private TrackDrawInfo trackDrawInfo; + private OsmandPreference showStartFinishPreference; + + public ShowStartFinishCard(@NonNull MapActivity mapActivity, @NonNull TrackDrawInfo trackDrawInfo) { + super(mapActivity); + this.showStartFinishPreference = app.getSettings().SHOW_START_FINISH_ICONS; + this.trackDrawInfo = trackDrawInfo; + } + + @Override + public int getCardLayoutId() { + return R.layout.bottom_sheet_item_with_switch; + } + + @Override + protected void updateContent() { + AndroidUiHelper.updateVisibility(view.findViewById(R.id.icon), false); + + TextView titleView = view.findViewById(R.id.title); + titleView.setText(R.string.track_show_start_finish_icons); + + final CompoundButton compoundButton = view.findViewById(R.id.compound_button); + //compoundButton.setChecked(trackDrawInfo.isShowStartFinish()); + compoundButton.setChecked(showStartFinishPreference.get()); + + view.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + boolean checked = !compoundButton.isChecked(); + compoundButton.setChecked(checked); + //trackDrawInfo.setShowStartFinish(checked); + showStartFinishPreference.set(checked); + mapActivity.refreshMap(); + + CardListener listener = getListener(); + if (listener != null) { + listener.onCardPressed(ShowStartFinishCard.this); + } + } + }); + } +} \ No newline at end of file diff --git a/OsmAnd/src/net/osmand/plus/track/TrackAppearanceFragment.java b/OsmAnd/src/net/osmand/plus/track/TrackAppearanceFragment.java index 4381dd61f6..81a43da20f 100644 --- a/OsmAnd/src/net/osmand/plus/track/TrackAppearanceFragment.java +++ b/OsmAnd/src/net/osmand/plus/track/TrackAppearanceFragment.java @@ -62,9 +62,10 @@ import static net.osmand.plus.dialogs.GpxAppearanceAdapter.TRACK_WIDTH_MEDIUM; public class TrackAppearanceFragment extends ContextMenuScrollFragment implements CardListener, ColorPickerListener { public static final String TAG = TrackAppearanceFragment.class.getName(); - private static final Log log = PlatformUtil.getLog(TrackAppearanceFragment.class); + private static final String SHOW_START_FINISH_ICONS_INITIAL_VALUE_KEY = "showStartFinishIconsInitialValueKey"; + private OsmandApplication app; @Nullable @@ -79,6 +80,7 @@ public class TrackAppearanceFragment extends ContextMenuScrollFragment implement private TrackWidthCard trackWidthCard; private SplitIntervalCard splitIntervalCard; private TrackColoringCard trackColoringCard; + private boolean showStartFinishIconsInitialValue; private ImageView trackIcon; private View buttonsShadow; @@ -134,9 +136,12 @@ public class TrackAppearanceFragment extends ContextMenuScrollFragment implement if (!selectedGpxFile.isShowCurrentTrack()) { gpxDataItem = app.getGpxDbHelper().getItem(new File(trackDrawInfo.getFilePath())); } + showStartFinishIconsInitialValue = savedInstanceState.getBoolean(SHOW_START_FINISH_ICONS_INITIAL_VALUE_KEY, + app.getSettings().SHOW_START_FINISH_ICONS.get()); } else if (arguments != null) { String gpxFilePath = arguments.getString(TRACK_FILE_NAME); boolean currentRecording = arguments.getBoolean(CURRENT_RECORDING, false); + showStartFinishIconsInitialValue = app.getSettings().SHOW_START_FINISH_ICONS.get(); if (gpxFilePath == null && !currentRecording) { log.error("Required extra '" + TRACK_FILE_NAME + "' is missing"); @@ -152,7 +157,7 @@ public class TrackAppearanceFragment extends ContextMenuScrollFragment implement selectedGpxFile = app.getSavingTrackHelper().getCurrentTrack(); } else { gpxDataItem = app.getGpxDbHelper().getItem(new File(gpxFilePath)); - trackDrawInfo = new TrackDrawInfo(gpxDataItem, false); + trackDrawInfo = new TrackDrawInfo(app, gpxDataItem, false); selectedGpxFile = app.getSelectedGpxHelper().getSelectedFileByPath(gpxFilePath); } updateTrackColor(); @@ -294,6 +299,7 @@ public class TrackAppearanceFragment extends ContextMenuScrollFragment implement public void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); trackDrawInfo.saveToBundle(outState); + outState.putBoolean(SHOW_START_FINISH_ICONS_INITIAL_VALUE_KEY, showStartFinishIconsInitialValue); } @Override @@ -455,6 +461,7 @@ public class TrackAppearanceFragment extends ContextMenuScrollFragment implement @Override public void onClick(View v) { discardSplitChanges(); + discardShowStartFinishChanges(); FragmentActivity activity = getActivity(); if (activity != null) { activity.onBackPressed(); @@ -520,7 +527,7 @@ public class TrackAppearanceFragment extends ContextMenuScrollFragment implement gpxFile.setSplitInterval(trackDrawInfo.getSplitInterval()); gpxFile.setShowArrows(trackDrawInfo.isShowArrows()); - gpxFile.setShowStartFinish(trackDrawInfo.isShowStartFinish()); + //gpxFile.setShowStartFinish(trackDrawInfo.isShowStartFinish()); if (gpxFile.showCurrentTrack) { app.getSettings().CURRENT_TRACK_COLOR.set(trackDrawInfo.getColor()); @@ -551,6 +558,10 @@ public class TrackAppearanceFragment extends ContextMenuScrollFragment implement } } + private void discardShowStartFinishChanges() { + app.getSettings().SHOW_START_FINISH_ICONS.set(showStartFinishIconsInitialValue); + } + void applySplit(GpxSplitType splitType, int timeSplit, double distanceSplit) { if (splitIntervalCard != null) { splitIntervalCard.updateContent(); @@ -599,6 +610,10 @@ public class TrackAppearanceFragment extends ContextMenuScrollFragment implement directionArrowsCard.setListener(this); cardsContainer.addView(directionArrowsCard.build(mapActivity)); + ShowStartFinishCard showStartFinishCard = new ShowStartFinishCard(mapActivity, trackDrawInfo); + showStartFinishCard.setListener(this); + cardsContainer.addView(showStartFinishCard.build(mapActivity)); + trackColoringCard = new TrackColoringCard(mapActivity, trackDrawInfo, this); trackColoringCard.setListener(this); cardsContainer.addView(trackColoringCard.build(mapActivity)); diff --git a/OsmAnd/src/net/osmand/plus/track/TrackColoringCard.java b/OsmAnd/src/net/osmand/plus/track/TrackColoringCard.java index e8ab755507..99571dad6c 100644 --- a/OsmAnd/src/net/osmand/plus/track/TrackColoringCard.java +++ b/OsmAnd/src/net/osmand/plus/track/TrackColoringCard.java @@ -29,6 +29,7 @@ import net.osmand.plus.OsmandApplication; import net.osmand.plus.R; import net.osmand.plus.UiUtilities; import net.osmand.plus.activities.MapActivity; +import net.osmand.plus.dialogs.GpxAppearanceAdapter; import net.osmand.plus.dialogs.GpxAppearanceAdapter.AppearanceListItem; import net.osmand.plus.dialogs.GpxAppearanceAdapter.GpxAppearanceAdapterType; import net.osmand.plus.helpers.AndroidUiHelper; @@ -41,8 +42,6 @@ import org.apache.commons.logging.Log; import java.util.ArrayList; import java.util.List; -import static net.osmand.plus.dialogs.GpxAppearanceAdapter.getAppearanceItems; - public class TrackColoringCard extends BaseCard implements ColorPickerListener { private static final int MINIMUM_CONTRAST_RATIO = 3; @@ -131,7 +130,7 @@ public class TrackColoringCard extends BaseCard implements ColorPickerListener { selectColor.addView(createDividerView(selectColor)); List colors = new ArrayList<>(); - for (AppearanceListItem appearanceListItem : getAppearanceItems(app, GpxAppearanceAdapterType.TRACK_COLOR)) { + for (AppearanceListItem appearanceListItem : GpxAppearanceAdapter.getAppearanceItems(app, GpxAppearanceAdapterType.TRACK_COLOR)) { if (!colors.contains(appearanceListItem.getColor())) { colors.add(appearanceListItem.getColor()); } diff --git a/OsmAnd/src/net/osmand/plus/track/TrackDrawInfo.java b/OsmAnd/src/net/osmand/plus/track/TrackDrawInfo.java index 3dbacb7b56..18d1c30775 100644 --- a/OsmAnd/src/net/osmand/plus/track/TrackDrawInfo.java +++ b/OsmAnd/src/net/osmand/plus/track/TrackDrawInfo.java @@ -5,6 +5,7 @@ import android.os.Bundle; import androidx.annotation.NonNull; import net.osmand.plus.GPXDatabase.GpxDataItem; +import net.osmand.plus.OsmandApplication; import net.osmand.util.Algorithms; import static net.osmand.plus.activities.TrackActivity.CURRENT_RECORDING; @@ -40,7 +41,7 @@ public class TrackDrawInfo { readBundle(bundle); } - public TrackDrawInfo(GpxDataItem gpxDataItem, boolean currentRecording) { + public TrackDrawInfo(@NonNull OsmandApplication app, @NonNull GpxDataItem gpxDataItem, boolean currentRecording) { filePath = gpxDataItem.getFile().getPath(); width = gpxDataItem.getWidth(); gradientScaleType = gpxDataItem.getGradientScaleType(); diff --git a/OsmAnd/src/net/osmand/plus/views/layers/GPXLayer.java b/OsmAnd/src/net/osmand/plus/views/layers/GPXLayer.java index 3bb49859be..641944ce21 100644 --- a/OsmAnd/src/net/osmand/plus/views/layers/GPXLayer.java +++ b/OsmAnd/src/net/osmand/plus/views/layers/GPXLayer.java @@ -453,7 +453,7 @@ public class GPXLayer extends OsmandMapLayer implements IContextMenuProvider, IM if (segment.points.size() >= 2) { WptPt start = segment.points.get(0); WptPt end = segment.points.get(segment.points.size() - 1); - drawStartEndPoints(canvas, tileBox, start, end); + drawStartEndPoints(canvas, tileBox, start, selectedGpxFile.isShowCurrentTrack() ? null : end); } } } @@ -461,24 +461,28 @@ public class GPXLayer extends OsmandMapLayer implements IContextMenuProvider, IM } } - private void drawStartEndPoints(Canvas canvas, RotatedTileBox tileBox, WptPt start, WptPt end) { - int startX = (int) tileBox.getPixXFromLatLon(start.lat, start.lon); - int startY = (int) tileBox.getPixYFromLatLon(start.lat, start.lon); - int endX = (int) tileBox.getPixXFromLatLon(end.lat, end.lon); - int endY = (int) tileBox.getPixYFromLatLon(end.lat, end.lon); + private void drawStartEndPoints(@NonNull Canvas canvas, @NonNull RotatedTileBox tileBox, @Nullable WptPt start, @Nullable WptPt end) { + int startX = start != null ? (int) tileBox.getPixXFromLatLon(start.lat, start.lon) : 0; + int startY = start != null ? (int) tileBox.getPixYFromLatLon(start.lat, start.lon) : 0; + int endX = end != null ? (int) tileBox.getPixXFromLatLon(end.lat, end.lon) : 0; + int endY = end != null ? (int) tileBox.getPixYFromLatLon(end.lat, end.lon) : 0; int iconSize = AndroidUtils.dpToPx(view.getContext(), 14); QuadRect startRectWithoutShadow = calculateRect(startX, startY, iconSize, iconSize); QuadRect endRectWithoutShadow = calculateRect(endX, endY, iconSize, iconSize); - if (QuadRect.intersects(startRectWithoutShadow, endRectWithoutShadow)) { + if (start != null && end != null && QuadRect.intersects(startRectWithoutShadow, endRectWithoutShadow)) { QuadRect startAndFinishRect = calculateRect(startX, startY, startAndFinishIcon.getIntrinsicWidth(), startAndFinishIcon.getIntrinsicHeight()); drawPoint(canvas, startAndFinishRect, startAndFinishIcon); } else { - QuadRect startRect = calculateRect(startX, startY, startPointIcon.getIntrinsicWidth(), startPointIcon.getIntrinsicHeight()); - QuadRect endRect = calculateRect(endX, endY, finishPointIcon.getIntrinsicWidth(), finishPointIcon.getIntrinsicHeight()); - drawPoint(canvas, startRect, startPointIcon); - drawPoint(canvas, endRect, finishPointIcon); + if (start != null) { + QuadRect startRect = calculateRect(startX, startY, startPointIcon.getIntrinsicWidth(), startPointIcon.getIntrinsicHeight()); + drawPoint(canvas, startRect, startPointIcon); + } + if (end != null) { + QuadRect endRect = calculateRect(endX, endY, finishPointIcon.getIntrinsicWidth(), finishPointIcon.getIntrinsicHeight()); + drawPoint(canvas, endRect, finishPointIcon); + } } } @@ -711,6 +715,8 @@ public class GPXLayer extends OsmandMapLayer implements IContextMenuProvider, IM } private boolean isShowStartFinishForTrack(GPXFile gpxFile) { + return view.getApplication().getSettings().SHOW_START_FINISH_ICONS.get(); + /* if (hasTrackDrawInfoForTrack(gpxFile)) { return trackDrawInfo.isShowStartFinish(); } else if (gpxFile.showCurrentTrack) { @@ -718,6 +724,7 @@ public class GPXLayer extends OsmandMapLayer implements IContextMenuProvider, IM } else { return gpxFile.isShowStartFinish(); } + */ } private boolean hasTrackDrawInfoForTrack(GPXFile gpxFile) { diff --git a/OsmAnd/src/net/osmand/plus/views/layers/geometry/GeometryWayDrawer.java b/OsmAnd/src/net/osmand/plus/views/layers/geometry/GeometryWayDrawer.java index db45a9b55c..0bfa86f43b 100644 --- a/OsmAnd/src/net/osmand/plus/views/layers/geometry/GeometryWayDrawer.java +++ b/OsmAnd/src/net/osmand/plus/views/layers/geometry/GeometryWayDrawer.java @@ -31,22 +31,28 @@ public class GeometryWayDrawer { int h = tb.getPixHeight(); int w = tb.getPixWidth(); - int left = -w / 4; + int left = -w / 4; int right = w + w / 4; - int top = - h/4; - int bottom = h + h/4; + int top = -h / 4; + int bottom = h + h / 4; boolean hasStyles = styles != null && styles.size() == tx.size(); double zoomCoef = tb.getZoomAnimation() > 0 ? (Math.pow(2, tb.getZoomAnimation() + tb.getZoomFloatPart())) : 1f; - Bitmap arrow = context.getArrowBitmap(); - int arrowHeight = arrow.getHeight(); - double pxStep = arrowHeight * 4f * zoomCoef; - double pxStepRegular = arrowHeight * 4f * zoomCoef; + + int startIndex = tx.size() - 2; + double defaultPxStep; + if (hasStyles && styles.get(startIndex) != null) { + defaultPxStep = styles.get(startIndex).getPointStepPx(zoomCoef); + } else { + Bitmap arrow = context.getArrowBitmap(); + defaultPxStep = arrow.getHeight() * 4f * zoomCoef; + } + double pxStep = defaultPxStep; double dist = 0; if (distPixToFinish != 0) { dist = distPixToFinish - pxStep * ((int) (distPixToFinish / pxStep)); // dist < 1 } - for (int i = tx.size() - 2; i >= 0; i --) { + for (int i = startIndex; i >= 0; i--) { GeometryWayStyle style = hasStyles ? styles.get(i) : null; float px = tx.get(i); float py = ty.get(i); @@ -57,7 +63,7 @@ public class GeometryWayDrawer { if (distSegment == 0) { continue; } - pxStep = style != null ? style.getPointStepPx(zoomCoef) : pxStepRegular; + pxStep = style != null ? style.getPointStepPx(zoomCoef) : defaultPxStep; if (dist >= pxStep) { dist = 0; } diff --git a/build.gradle b/build.gradle index 1cd0b221b4..a64480346c 100644 --- a/build.gradle +++ b/build.gradle @@ -4,6 +4,9 @@ buildscript { google() mavenCentral() jcenter() + maven { + url 'https://developer.huawei.com/repo/' + } } dependencies { //classpath 'com.android.tools.build:gradle:2.+' @@ -11,6 +14,9 @@ buildscript { 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" + if (gradle.startParameter.taskNames.toString().contains("huawei")) { + classpath 'com.huawei.agconnect:agcp:1.4.1.300' + } } } @@ -32,5 +38,8 @@ allprojects { maven { url "https://jitpack.io" } + maven { + url 'https://developer.huawei.com/repo/' + } } }