--- /dev/null
+/*
+ * Copyright © 2020 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.model;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.Locale;
+
+public class HledgerVersion {
+ private int major;
+ private int minor;
+ private int patch;
+ private boolean isPre_1_20;
+ private boolean hasPatch;
+ public HledgerVersion(int major, int minor) {
+ this.major = major;
+ this.minor = minor;
+ this.isPre_1_20 = false;
+ this.hasPatch = false;
+ }
+ public HledgerVersion(int major, int minor, int patch) {
+ this.major = major;
+ this.minor = minor;
+ this.patch = patch;
+ this.isPre_1_20 = false;
+ this.hasPatch = true;
+ }
+ public HledgerVersion(boolean pre_1_20) {
+ if (!pre_1_20)
+ throw new IllegalArgumentException("pre_1_20 argument must be true");
+ this.major = this.minor = 0;
+ this.isPre_1_20 = true;
+ this.hasPatch = false;
+ }
+ public HledgerVersion(HledgerVersion origin) {
+ this.major = origin.major;
+ this.minor = origin.minor;
+ this.isPre_1_20 = origin.isPre_1_20;
+ this.patch = origin.patch;
+ this.hasPatch = origin.hasPatch;
+ }
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (obj == null)
+ return false;
+ if (!(obj instanceof HledgerVersion))
+ return false;
+ HledgerVersion that = (HledgerVersion) obj;
+
+ return (this.isPre_1_20 == that.isPre_1_20 && this.major == that.major &&
+ this.minor == that.minor && this.patch == that.patch &&
+ this.hasPatch == that.hasPatch);
+ }
+ public boolean isPre_1_20() {
+ return isPre_1_20;
+ }
+ public int getMajor() {
+ return major;
+ }
+ public int getMinor() {
+ return minor;
+ }
+ public int getPatch() {
+ return patch;
+ }
+ @NonNull
+ @Override
+ public String toString() {
+ if (isPre_1_20)
+ return "(before 1.20)";
+ return hasPatch ? String.format(Locale.ROOT, "%d.%d.%d", major, minor, patch)
+ : String.format(Locale.ROOT, "%d.%d", major, minor);
+ }
+}
private FutureDates futureDates = FutureDates.None;
private boolean accountsLoaded;
private boolean transactionsLoaded;
+ private HledgerVersion detectedVersion;
// N.B. when adding new fields, update the copy-constructor below
transient private AccountAndTransactionListSaver accountAndTransactionListSaver;
public MobileLedgerProfile(String uuid) {
defaultCommodity = origin.defaultCommodity;
accountsLoaded = origin.accountsLoaded;
transactionsLoaded = origin.transactionsLoaded;
+ if (origin.detectedVersion != null)
+ detectedVersion = new HledgerVersion(origin.detectedVersion);
}
// loads all profiles into Data.profiles
// returns the profile with the given UUID
"auth_password, permit_posting, theme, order_no, " +
"preferred_accounts_filter, future_dates, api_version, " +
"show_commodity_by_default, default_commodity, " +
- "show_comments_by_default FROM " +
+ "show_comments_by_default, detected_version_pre_1_19, " +
+ "detected_version_major, detected_version_minor FROM " +
"profiles order by order_no", null))
{
while (cursor.moveToNext()) {
item.setShowCommodityByDefault(cursor.getInt(12) == 1);
item.setDefaultCommodity(cursor.getString(13));
item.setShowCommentsByDefault(cursor.getInt(14) == 1);
+ {
+ boolean pre_1_20 = cursor.getInt(15) == 1;
+ int major = cursor.getInt(16);
+ int minor = cursor.getInt(17);
+
+ if (!pre_1_20 && major == 0 && minor == 0) {
+ item.detectedVersion = null;
+ }
+ else if (pre_1_20) {
+ item.detectedVersion = new HledgerVersion(true);
+ }
+ else {
+ item.detectedVersion = new HledgerVersion(major, minor);
+ }
+ }
list.add(item);
if (item.getUuid()
.equals(currentProfileUUID))
db.endTransaction();
}
}
+ public HledgerVersion getDetectedVersion() {
+ return detectedVersion;
+ }
+ public void setDetectedVersion(HledgerVersion detectedVersion) {
+ this.detectedVersion = detectedVersion;
+ }
@Contract(value = "null -> false", pure = true)
@Override
public boolean equals(@Nullable Object obj) {
return false;
if (apiVersion != p.apiVersion)
return false;
+ if (!Objects.equals(detectedVersion, p.detectedVersion))
+ return false;
return futureDates == p.futureDates;
}
public boolean getShowCommentsByDefault() {
db.execSQL("REPLACE INTO profiles(uuid, name, permit_posting, url, " +
"use_authentication, auth_user, auth_password, theme, order_no, " +
"preferred_accounts_filter, future_dates, api_version, " +
- "show_commodity_by_default, default_commodity, show_comments_by_default) " +
- "VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
+ "show_commodity_by_default, default_commodity, show_comments_by_default," +
+ "detected_version_pre_1_19, detected_version_major, " +
+ "detected_version_minor) " +
+ "VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
new Object[]{uuid, name, permitPosting, url, authEnabled,
authEnabled ? authUserName : null,
authEnabled ? authPassword : null, themeHue, orderNo,
preferredAccountsFilter, futureDates.toInt(), apiVersion.toInt(),
- showCommodityByDefault, defaultCommodity, showCommentsByDefault
+ showCommodityByDefault, defaultCommodity, showCommentsByDefault,
+ (detectedVersion != null) && detectedVersion.isPre_1_20(),
+ (detectedVersion == null) ? 0 : detectedVersion.getMajor(),
+ (detectedVersion == null) ? 0 : detectedVersion.getMinor()
});
db.setTransactionSuccessful();
}
apiVersionText = context.findViewById(R.id.api_version_text);
model.observeApiVersion(viewLifecycleOwner,
apiVer -> apiVersionText.setText(apiVer.getDescription(getResources())));
- context.findViewById(R.id.api_version_layout)
- .setOnClickListener(v -> {
- MenuInflater mi = new MenuInflater(context);
- PopupMenu menu = new PopupMenu(context, v);
- menu.inflate(R.menu.api_version);
- menu.setOnMenuItemClickListener(item -> {
- SendTransactionTask.API apiVer;
- switch (item.getItemId()) {
- case R.id.api_version_menu_html:
- apiVer = SendTransactionTask.API.html;
- break;
- case R.id.api_version_menu_post_1_14:
- apiVer = SendTransactionTask.API.post_1_14;
- break;
- case R.id.api_version_menu_pre_1_15:
- apiVer = SendTransactionTask.API.pre_1_15;
- break;
- case R.id.api_version_menu_auto:
- default:
- apiVer = SendTransactionTask.API.auto;
- }
- model.setApiVersion(apiVer);
- apiVersionText.setText(apiVer.getDescription(getResources()));
- return true;
- });
- menu.show();
- });
+ context.findViewById(R.id.api_version_label)
+ .setOnClickListener(this::chooseAPIVersion);
+ context.findViewById(R.id.api_version_text)
+ .setOnClickListener(this::chooseAPIVersion);
+
+ TextView detectedApiVersion = context.findViewById(R.id.detected_version_text);
+ model.observeDetectedVersion(viewLifecycleOwner, ver -> {
+ if (ver == null)
+ detectedApiVersion.setText(context.getResources()
+ .getString(R.string.api_version_unknown_label));
+ else if (ver.isPre_1_20())
+ detectedApiVersion.setText(context.getResources()
+ .getString(R.string.api_pre_1_19));
+ else
+ detectedApiVersion.setText(ver.toString());
+ });
+ detectedApiVersion.setOnClickListener(v -> model.triggerVersionDetection());
+ context.findViewById(R.id.api_version_detect_button)
+ .setOnClickListener(v -> model.triggerVersionDetection());
+
authParams = context.findViewById(R.id.auth_params);
useAuthentication = context.findViewById(R.id.enable_http_auth);
profileName.requestFocus();
}
+ private void chooseAPIVersion(View v) {
+ Activity context = getActivity();
+ ProfileDetailModel model = getModel();
+ MenuInflater mi = new MenuInflater(context);
+ PopupMenu menu = new PopupMenu(context, v);
+ menu.inflate(R.menu.api_version);
+ menu.setOnMenuItemClickListener(item -> {
+ SendTransactionTask.API apiVer;
+ switch (item.getItemId()) {
+ case R.id.api_version_menu_html:
+ apiVer = SendTransactionTask.API.html;
+ break;
+ case R.id.api_version_menu_post_1_14:
+ apiVer = SendTransactionTask.API.post_1_14;
+ break;
+ case R.id.api_version_menu_pre_1_15:
+ apiVer = SendTransactionTask.API.pre_1_15;
+ break;
+ case R.id.api_version_menu_auto:
+ default:
+ apiVer = SendTransactionTask.API.auto;
+ }
+ model.setApiVersion(apiVer);
+ apiVersionText.setText(apiVer.getDescription(getResources()));
+ return true;
+ });
+ menu.show();
+ }
private MobileLedgerProfile.FutureDates futureDatesSettingFromMenuItemId(int itemId) {
switch (itemId) {
case R.id.menu_future_dates_7:
import net.ktnx.mobileledger.async.SendTransactionTask;
import net.ktnx.mobileledger.model.Currency;
+import net.ktnx.mobileledger.model.HledgerVersion;
import net.ktnx.mobileledger.model.MobileLedgerProfile;
import net.ktnx.mobileledger.utils.Colors;
+import net.ktnx.mobileledger.utils.Logger;
import net.ktnx.mobileledger.utils.Misc;
+import net.ktnx.mobileledger.utils.NetworkUtil;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.HttpURLConnection;
+import java.util.Locale;
+import java.util.Objects;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
public class ProfileDetailModel extends ViewModel {
private static final String HTTPS_URL_START = "https://";
private final MutableLiveData<String> authPassword = new MutableLiveData<>(null);
private final MutableLiveData<String> preferredAccountsFilter = new MutableLiveData<>(null);
private final MutableLiveData<Integer> themeId = new MutableLiveData<>(-1);
+ private final MutableLiveData<HledgerVersion> detectedVersion = new MutableLiveData<>(null);
public int initialThemeHue = Colors.DEFAULT_HUE_DEG;
+ private VersionDetectionThread versionDetectionThread;
public ProfileDetailModel() {
}
String getProfileName() {
void observeApiVersion(LifecycleOwner lfo, Observer<SendTransactionTask.API> o) {
apiVersion.observe(lfo, o);
}
+ HledgerVersion getDetectedVersion() { return detectedVersion.getValue(); }
+ void setDetectedVersion(HledgerVersion newValue) {
+ if (!Objects.equals(detectedVersion.getValue(), newValue))
+ detectedVersion.setValue(newValue);
+ }
+ void observeDetectedVersion(LifecycleOwner lfo, Observer<HledgerVersion> o) {
+ detectedVersion.observe(lfo, o);
+ }
String getUrl() {
return url.getValue();
}
authPassword.setValue(mProfile.isAuthEnabled() ? mProfile.getAuthPassword() : "");
preferredAccountsFilter.setValue(mProfile.getPreferredAccountsFilter());
themeId.setValue(mProfile.getThemeHue());
+ detectedVersion.setValue(mProfile.getDetectedVersion());
}
else {
profileName.setValue(null);
authPassword.setValue("");
preferredAccountsFilter.setValue(null);
themeId.setValue(newProfileHue);
+ detectedVersion.setValue(null);
}
-
-
}
void updateProfile(MobileLedgerProfile mProfile) {
mProfile.setName(profileName.getValue());
mProfile.setThemeHue(themeId.getValue());
mProfile.setFutureDates(futureDates.getValue());
mProfile.setApiVersion(apiVersion.getValue());
+ mProfile.setDetectedVersion(detectedVersion.getValue());
+ }
+ synchronized public void triggerVersionDetection() {
+ if (versionDetectionThread != null)
+ versionDetectionThread.interrupt();
+
+ versionDetectionThread = new VersionDetectionThread(this);
+ versionDetectionThread.start();
+ }
+ static class VersionDetectionThread extends Thread {
+ private final Pattern versionPattern =
+ Pattern.compile("^\"(\\d+)\\.(\\d+)(?:\\.(\\d+))?\"$");
+ private final ProfileDetailModel model;
+ public VersionDetectionThread(ProfileDetailModel model) {
+ this.model = model;
+ }
+ @Override
+ public void run() {
+ try {
+ HttpURLConnection http = NetworkUtil.prepareConnection(model.getUrl(), "version",
+ model.getUseAuthentication());
+ switch (http.getResponseCode()) {
+ case 200:
+ break;
+ case 404:
+ model.detectedVersion.postValue(new HledgerVersion(true));
+ return;
+ default:
+ Logger.debug("profile", String.format(Locale.US,
+ "HTTP error detecting hledger-web version: [%d] %s",
+ http.getResponseCode(), http.getResponseMessage()));
+ model.detectedVersion.postValue(null);
+ return;
+ }
+ InputStream stream = http.getInputStream();
+ BufferedReader reader = new BufferedReader(new InputStreamReader(stream));
+ String version = reader.readLine();
+ Matcher m = versionPattern.matcher(version);
+ if (m.matches()) {
+ int major = Integer.parseInt(Objects.requireNonNull(m.group(1)));
+ int minor = Integer.parseInt(Objects.requireNonNull(m.group(2)));
+ final boolean hasPatch = m.groupCount() >= 3;
+ int patch = hasPatch ? Integer.parseInt(Objects.requireNonNull(m.group(3))) : 0;
+
+ model.detectedVersion.postValue(
+ hasPatch ? new HledgerVersion(major, minor, patch)
+ : new HledgerVersion(major, minor));
+ }
+ else {
+ Logger.debug("profile",
+ String.format("Unrecognised version string '%s'", version));
+ }
+ }
+ catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
}
}
public class MobileLedgerDatabase extends SQLiteOpenHelper {
public static final MutableLiveData<Boolean> initComplete = new MutableLiveData<>(false);
private static final String DB_NAME = "MoLe.db";
- private static final int LATEST_REVISION = 40;
+ private static final int LATEST_REVISION = 41;
private static final String CREATE_DB_SQL = "create_db";
private final Application mContext;
/*
- * Copyright © 2019 Damyan Ivanov.
+ * Copyright © 2020 Damyan Ivanov.
* This file is part of MoLe.
* MoLe is free software: you can distribute it and/or modify it
* under the term of the GNU General Public License as published by
package net.ktnx.mobileledger.utils;
+import androidx.annotation.NonNull;
+
import net.ktnx.mobileledger.model.MobileLedgerProfile;
+import org.jetbrains.annotations.NotNull;
+
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
public final class NetworkUtil {
private static final int thirtySeconds = 30000;
- public static HttpURLConnection prepareConnection(MobileLedgerProfile profile, String path)
- throws IOException {
- String url = profile.getUrl();
- final boolean use_auth = profile.isAuthEnabled();
- if (!url.endsWith("/")) url += "/";
- url += path;
- debug("network", "Connecting to " + url);
- HttpURLConnection http = (HttpURLConnection) new URL(url).openConnection();
+ @NotNull
+ public static HttpURLConnection prepareConnection(@NonNull MobileLedgerProfile profile,
+ @NonNull String path) throws IOException {
+ return prepareConnection(profile.getUrl(), path, profile.isAuthEnabled());
+ }
+ public static HttpURLConnection prepareConnection(@NonNull String url, @NonNull String path,
+ boolean authEnabled) throws IOException {
+ String connectURL = url;
+ if (!connectURL.endsWith("/"))
+ connectURL += "/";
+ connectURL += path;
+ debug("network", "Connecting to " + connectURL);
+ HttpURLConnection http = (HttpURLConnection) new URL(connectURL).openConnection();
http.setAllowUserInteraction(true);
http.setRequestProperty("Accept-Charset", "UTF-8");
http.setInstanceFollowRedirects(false);
--- /dev/null
+<!--
+ ~ Copyright Google Inc.
+ ~
+ ~ Licensed under the Apache License, version 2.0 ("the License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the license at:
+ ~
+ ~ https://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distribution 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.
+ ~
+ ~ Modified/adapted by Damyan Ivanov for MoLe
+ -->
+
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:tint="?attr/colorPrimary"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0"
+ >
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M17.65,6.35C16.2,4.9 14.21,4 12,4c-4.42,0 -7.99,3.58 -7.99,8s3.57,8 7.99,8c3.73,0 6.84,-2.55 7.73,-6h-2.08c-0.82,2.33 -3.04,4 -5.65,4 -3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6c1.66,0 3.14,0.69 4.22,1.78L13,11h7V4l-2.35,2.35z"
+ />
+</vector>
</LinearLayout>
- <LinearLayout
+ <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/api_version_layout"
- android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp">
<TextView
+ android:id="@+id/api_version_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/profile_api_version_title"
- android:textAppearance="?android:textAppearanceListItem" />
+ android:textAppearance="?android:textAppearanceListItem"
+ app:layout_constraintTop_toTopOf="parent"
+ />
<TextView
android:id="@+id/api_version_text"
- android:layout_width="match_parent"
+ android:layout_width="0dp"
android:layout_height="wrap_content"
android:textAppearance="?android:textAppearanceListItemSecondary"
- android:textColor="?attr/textColor" />
- </LinearLayout>
+ android:textColor="?attr/textColor"
+ app:layout_constraintEnd_toStartOf="@id/detected_version_text"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/api_version_label"
+ />
+ <TextView
+ android:id="@+id/detected_version_label"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="8dp"
+ android:gravity="end"
+ android:text="@string/detected_version_label"
+ android:textAppearance="?android:textAppearanceListItemSecondary"
+ app:layout_constraintEnd_toStartOf="@id/detected_version_text"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/api_version_text"
+ />
+ <TextView
+ android:id="@+id/detected_version_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="8dp"
+ android:text="@string/api_version_unknown_label"
+ android:textAppearance="?android:textAppearanceListItemSecondary"
+ android:textColor="?attr/textColor"
+ app:layout_constraintEnd_toStartOf="@id/api_version_detect_button"
+ app:layout_constraintTop_toBottomOf="@id/api_version_text"
+ />
+ <TextView
+ android:id="@+id/api_version_detect_button"
+ android:layout_width="24dp"
+ android:layout_height="0dp"
+ android:drawableBottom="@drawable/ic_refresh_primary_24dp"
+ android:foregroundGravity="bottom"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/api_version_label"
+ />
+ </androidx.constraintlayout.widget.ConstraintLayout>
<Switch
android:id="@+id/profile_permit_posting"
--
-- You should have received a copy of the GNU General Public License
-- along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+create table profiles(uuid varchar not null primary key, name not null, url not null, use_authentication boolean not null, auth_user varchar, auth_password varchar, order_no integer, permit_posting boolean default 0, theme integer default -1, preferred_accounts_filter varchar, future_dates integer, api_version integer, show_commodity_by_default boolean default 0, default_commodity varchar, show_comments_by_default boolean default 1, detected_version_pre_1_19 boolean, detected_version_major integer, detected_version_minor integer);
create table accounts(profile varchar not null, name varchar not null, name_upper varchar not null, level integer not null, parent_name varchar, expanded default 1, amounts_expanded boolean default 0, generation integer default 0);
create unique index un_accounts on accounts(profile, name);
create table options(profile varchar not null, name varchar not null, value varchar);
create unique index un_account_values on account_values(profile,account,currency);
create table description_history(description varchar not null primary key, description_upper varchar, generation integer default 0);
create unique index un_description_history on description_history(description_upper);
-create table profiles(uuid varchar not null primary key, name not null, url not null, use_authentication boolean not null, auth_user varchar, auth_password varchar, order_no integer, permit_posting boolean default 0, theme integer default -1, preferred_accounts_filter varchar, future_dates integer, api_version integer, show_commodity_by_default boolean default 0, default_commodity varchar, show_comments_by_default boolean default 1);
create table transactions(profile varchar not null, id integer not null, data_hash varchar not null, year integer not null, month integer not null, day integer not null, description varchar not null, comment varchar, generation integer default 0);
create unique index un_transactions_id on transactions(profile,id);
create unique index un_transactions_data_hash on transactions(profile,data_hash);
--- /dev/null
+-- Copyright © 2020 Damyan Ivanov.
+-- This file is part of MoLe.
+-- MoLe is free software: you can distribute it and/or modify it
+-- under the term of the GNU General Public License as published by
+-- the Free Software Foundation, either version 3 of the License, or
+-- (at your opinion), any later version.
+--
+-- MoLe is distributed in the hope that it will be useful,
+-- but WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+-- GNU General Public License terms for details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+alter table profiles add detected_version_pre_1_19 boolean;
+alter table profiles add detected_version_major integer;
+alter table profiles add detected_version_minor integer;
\ No newline at end of file
<string name="nav_header_desc">Заглавна част на страничния панел</string>
<string name="transaction_count_summary">%,d движения към %s</string>
<string name="account_count_summary">%,d сметки към %s</string>
+ <string name="api_version_unknown_label">Неизвестна</string>
+ <string name="api_pre_1_19">Преди 1.20.?</string>
+ <string name="detected_version_label">Открита версия</string>
</resources>
<string name="sub_accounts_expand_collapse_trigger_description">Sub-accounts expand/collapse trigger</string>
<string name="transaction_count_summary">%,d transactions as of %s</string>
<string name="account_count_summary">%,d accounts as of %s</string>
+ <string name="api_version_unknown_label">Unknown</string>
+ <string name="api_pre_1_19">Before 1.20.?</string>
+ <string name="detected_version_label">Detected version</string>
</resources>