Skip to content

Commit

Permalink
Merge branch 'templates_improvement'
Browse files Browse the repository at this point in the history
  • Loading branch information
simonpoole committed Aug 20, 2021
2 parents 29212a8 + 6be5bdc commit 0e4f8d6
Show file tree
Hide file tree
Showing 13 changed files with 248 additions and 68 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/android.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,10 @@ jobs:
- name: run tests
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 29
api-level: 28
script: ./gradlew connectedCheck
- name: Upload Test Results
if: ${{ always() }}
uses: actions/upload-artifact@v2
with:
name: Test output
Expand Down
3 changes: 2 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ buildscript {
google()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.1.0'
classpath 'com.android.tools.build:gradle:4.2.0'
classpath 'org.jacoco:org.jacoco.core:0.8.7'
classpath 'com.github.ksoichiro:gradle-eclipse-aar-plugin:0.3.1'
}
}
Expand Down
3 changes: 2 additions & 1 deletion lib/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@
/bin/
/project.properties
/libs/
/.externalToolBuilders/
/.externalToolBuilders/
/jacoco.exec
26 changes: 19 additions & 7 deletions lib/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,9 @@ android {
showCauses true
showStackTraces true
}
systemProperty 'robolectric.logging', 'stdout'
}
unitTests.includeAndroidResources = true
}

lintOptions {
Expand Down Expand Up @@ -99,14 +101,19 @@ dependencies {
implementation "ch.poole.android:rangebar:0.1.6"
implementation 'cn.carbswang.android:NumberPickerView:1.2.0'
implementation "com.google.code.gson:gson:2.8.5"

// Instrumentation tests
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'

// Instrumentation tests
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test:rules:1.1.1'
androidTestImplementation "org.hamcrest:hamcrest-library:1.3"
// androidTestImplementation "com.android.support.test.espresso:espresso-core:2.2.2"
androidTestImplementation "org.hamcrest:hamcrest-library:1.3"
androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0'

// Unit tests
testImplementation "junit:junit:4.13"
testImplementation 'org.robolectric:robolectric:4.4'
testImplementation 'androidx.test.ext:junit:1.1.1'
testImplementation 'androidx.test:rules:1.1.1'
}

android.libraryVariants.all { variant ->
Expand Down Expand Up @@ -198,6 +205,7 @@ build.dependsOn replaceVersion
def coverageSourceDirs = ['src/main/java']

// see https://github.com/gradle/gradle/issues/5184
// and https://issuetracker.google.com/issues/178015739
tasks.withType(Test) {
jacoco.includeNoLocationClasses = true
jacoco.excludes = ['jdk.internal.*']
Expand All @@ -214,9 +222,13 @@ task jacocoTestReport(type:JacocoReport, dependsOn: "testDebugUnitTest") {

additionalSourceDirs.from = files(coverageSourceDirs)
sourceDirectories.from = files(coverageSourceDirs)
executionData.from = fileTree(
dir : "$buildDir",
include : ['jacoco/testDebugUnitTest.exec', 'outputs/code_coverage/debugAndroidTest/connected/*coverage.ec', 'spoon-output/currentDebug/coverage/merged-coverage.ec'])
executionData.from = files([
// see https://stackoverflow.com/questions/67530173/upgrade-to-agp-4-2-0-unable-to-generate-jacoco-code-coverage-report
"$project.projectDir/jacoco.exec",
fileTree(
dir : "$buildDir",
include : ['jacoco.exec', 'jacoco/testDebugUnitTest.exec', 'outputs/code_coverage/debugAndroidTest/connected/*coverage.ec', 'spoon-output/currentDebug/coverage/merged-coverage.ec'])
])
reports {
xml.enabled = true
html.enabled = true
Expand Down
13 changes: 12 additions & 1 deletion lib/documentation/docs/help/en/Opening hours.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ _This documentation is preliminary and a work in progress_

In a typical workflow the object you are editing will either already have an opening hours tag (opening_hours, service_times and collection_times) or you can re-apply the preset for the object to get an empty opening hours field. If you need to add the field manually and you are using Vespucci you can enter the key on the details page and then switch back to the form based tab to edit. If you believe that the opening hours tag should have been part of the preset, please open an issue for your editor.

If you have defined a default template (do this via the "Manage templates" menu item) it will be loaded automatically when the editor is started with an empty value. With the "Load template" function you can load any saved template and with the "Save template" menu you can save the current value as a template. You can define separate templates and defaults for the "opening_hours", "collection_times" and "service_times" tags. Further you can limit applicability of a template to a region and a specific identifier, typically an OSM top-level tap (for example amenity=restaurant).
If you have defined a default template (do this via the "Manage templates" menu item) it will be loaded automatically when the editor is started with an empty value. With the "Load template" function you can load any saved template and with the "Save template" menu you can save the current value as a template. You can define separate templates and defaults for specific key, for example "opening_hours", "collection_times" and "service_times" or custom values. Further you can limit applicability of a template to a region and a specific identifier, typically an OSM top-level tag (for example amenity=restaurant).

Naturally you can build an opening hours value from scratch, but we would recommend using one of the existing templates as a starting point.

Expand Down Expand Up @@ -97,3 +97,14 @@ In Android 4.4 and later the following additional functionality is available fro
* __Save to file__: write the contents of the template database to a file.
* __Load from file (replace)__: load templates from a file replacing the current contents of the database.
* __Load from file__: load templates from a file retaining the current contents.

#### Save and edit template dialogs

The dialog allows you to set

* __Name__ a descriptive name for the template.
* __Default__ if checked this will be consider as a default template (typically further constrained by the other fields).
* __Key__ the key this template is relevant for, if set to _Custom key_ you can add a non-standard value in the field below. The key values support SQL wild cards, that is _%_ matches zero or more characters, *_* matches a single character. Both wild card characters can be escaped with _\\_ for literal matches.
* __Region__ the region the template is applicable to.
* __Object__ an application specific string to use for matching.

Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,11 @@ public final class TemplateDatabase {
static final String REGION_FIELD = "region";
static final String OBJECT_FIELD = "object";

static final String QUERY_ALL = "SELECT rowid as _id, key, name, is_default, template, region, object FROM templates";
static final String QUERY_BY_ROWID = "SELECT key, name, is_default, template, region, object FROM templates WHERE rowid=?";
static final String QUERY_BY = "SELECT rowid as _id, key, name, is_default, template, region, object FROM templates WHERE ";
static final String QUERY_TEMPLATE_BY = "SELECT template FROM templates WHERE ";
static final String QUERY_ALL = "SELECT rowid as _id, key, name, is_default, template, region, object FROM templates";
static final String QUERY_BY_ROWID = "SELECT key, name, is_default, template, region, object FROM templates WHERE rowid=?";
static final String QUERY_BY = "SELECT rowid as _id, key, name, is_default, template, region, object FROM templates WHERE ";
static final String QUERY_TEMPLATE_BY = "SELECT template FROM templates WHERE ";
private static final String AND = " AND ";

/**
* Private default constructor
Expand All @@ -60,29 +61,36 @@ private TemplateDatabase() {
public static String getDefault(@NonNull SQLiteDatabase database, @Nullable String key, @Nullable String region, @Nullable String object) {
String result = null;
List<String> params = new ArrayList<>();
List<String> orderCols = new ArrayList<>();
StringBuilder query = new StringBuilder();
if (key != null) {
query.append("(key is NULL OR key=?)");
query.append("(key is NULL OR ? like key escape '\\')");
params.add(key);
orderCols.add("key");
}
if (region != null) {
if (query.length() > 0) {
query.append(" AND ");
query.append(AND);
}
query.append("(region is NULL OR region=?)");
params.add(region);
orderCols.add(REGION_FIELD);
}
if (object != null) {
if (query.length() > 0) {
query.append(" AND ");
query.append(AND);
}
query.append("(object IS NULL OR object LIKE ?)");
params.add(object);
orderCols.add(OBJECT_FIELD);
}
if (query.length() > 0) {
query.append(" AND ");
query.append(AND);
}
query.append("is_default=1");
if (!orderCols.isEmpty()) {
addOrder(query, orderCols);
}
Cursor dbresult = database.rawQuery(TemplateDatabase.QUERY_TEMPLATE_BY + query.toString(), params.toArray(new String[0]));
dbresult.moveToFirst();
if (dbresult.getCount() >= 1) {
Expand All @@ -109,13 +117,13 @@ public static Cursor queryBy(@NonNull SQLiteDatabase database, @Nullable String
StringBuilder query = new StringBuilder();
List<String> orderCols = new ArrayList<>();
if (key != null) {
query.append("(key is NULL OR key=?)");
query.append("(key is NULL OR ? like key escape '\\')");
params.add(key);
orderCols.add("key");
}
if (region != null) {
if (query.length() > 0) {
query.append(" AND ");
query.append(AND);
}
String[] regionParts = region.split("-");
if (regionParts.length > 1) { // this will add a check for the country
Expand All @@ -130,23 +138,33 @@ public static Cursor queryBy(@NonNull SQLiteDatabase database, @Nullable String
}
if (object != null) {
if (query.length() > 0) {
query.append(" AND ");
query.append(AND);
}
query.append("(object IS NULL OR object LIKE ?)");
params.add(object);
orderCols.add(OBJECT_FIELD);
}
query.append(" ORDER BY ");
for (String col : orderCols) {
query.append("LENGTH(");
query.append(col);
query.append(") DESC, ");
}
query.append("is_default DESC");
addOrder(query, orderCols);
return database.rawQuery(TemplateDatabase.QUERY_BY + query.toString(), params.toArray(new String[0]));
}
}

/**
* Add order statements to query
*
* @param query the query
* @param orderCols a list of column names
*/
private static void addOrder(@NonNull StringBuilder query, @NonNull List<String> orderCols) {
query.append(" ORDER BY ");
for (String col : orderCols) {
query.append("LENGTH(");
query.append(col);
query.append(") DESC, ");
}
query.append("is_default DESC");
}

/**
* Add a new template with the given values to the database
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@

public class TemplateDatabaseHelper extends SQLiteOpenHelper {
private static final String DEBUG_TAG = "TemplateDatabase";
private static final String DATABASE_NAME = "openinghours_templates";
private static final int DATABASE_VERSION = 4;
static final String DATABASE_NAME = "openinghours_templates";
private static final int DATABASE_VERSION = 5;

private final Context context;

Expand All @@ -37,8 +37,8 @@ public void onCreate(SQLiteDatabase db) {
TemplateDatabase.add(db, null, context.getString(R.string.weekdays_late_shopping), false,
"Mo,Tu,Th,Fr 09:00-18:30;We 09:00-20:00;Sa 09:00-17:00;PH closed", null, null);
TemplateDatabase.add(db, null, context.getString(R.string.twentyfourseven), false, "24/7", null, null);
TemplateDatabase.add(db, "collection_times", context.getString(R.string.collection_times_weekdays), true, "Mo-Fr 09:00; Sa 07:00; PH closed", null,
null);
TemplateDatabase.add(db, "collection\\_times", context.getString(R.string.collection_times_weekdays), true, "Mo-Fr 09:00; Sa 07:00; PH closed",
null, null);
} catch (SQLException e) {
Log.w(DEBUG_TAG, "Problem creating database", e);
}
Expand All @@ -63,5 +63,9 @@ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
db.execSQL("ALTER TABLE templates ADD COLUMN region TEXT DEFAULT NULL");
db.execSQL("ALTER TABLE templates ADD COLUMN object TEXT DEFAULT NULL");
}
if (oldVersion <= 4 && newVersion >= 5) {
db.rawQuery("SELECT REPLACE('key','_','\\_')", null);
db.rawQuery("SELECT REPLACE('key',':','\\:')", null);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.Spinner;
Expand Down Expand Up @@ -95,6 +97,7 @@ public AppCompatDialog onCreateDialog(Bundle savedInstanceState) {
final CheckBox defaultCheck = (CheckBox) templateView.findViewById(R.id.is_default);
final EditText nameEdit = (EditText) templateView.findViewById(R.id.template_name);
final Spinner keySpinner = (Spinner) templateView.findViewById(R.id.template_key);
final EditText custom = (EditText) templateView.findViewById(R.id.template_custom_key);
final Spinner regionSpinner = (Spinner) templateView.findViewById(R.id.template_region);
final EditText objectEdit = (EditText) templateView.findViewById(R.id.template_object);

Expand All @@ -108,20 +111,20 @@ public AppCompatDialog onCreateDialog(Bundle savedInstanceState) {
String templateRegion = null;
String templateObject = null;
if (existing) {
Cursor cursor = db.rawQuery(TemplateDatabase.QUERY_BY_ROWID, new String[] { Integer.toString(id) });
if (cursor.moveToFirst()) {
boolean isDefault = cursor.getInt(cursor.getColumnIndexOrThrow(TemplateDatabase.DEFAULT_FIELD)) == 1;
defaultCheck.setChecked(isDefault);
String name = cursor.getString(cursor.getColumnIndexOrThrow(TemplateDatabase.NAME_FIELD));
nameEdit.setText(name);
template = cursor.getString(cursor.getColumnIndexOrThrow(TemplateDatabase.TEMPLATE_FIELD));
templateKey = cursor.getString(cursor.getColumnIndexOrThrow(TemplateDatabase.KEY_FIELD));
templateRegion = cursor.getString(cursor.getColumnIndexOrThrow(TemplateDatabase.REGION_FIELD));
templateObject = cursor.getString(cursor.getColumnIndexOrThrow(TemplateDatabase.OBJECT_FIELD));
} else {
Log.e(DEBUG_TAG, "template id " + Integer.toString(id) + " not found");
try (Cursor cursor = db.rawQuery(TemplateDatabase.QUERY_BY_ROWID, new String[] { Integer.toString(id) })) {
if (cursor.moveToFirst()) {
boolean isDefault = cursor.getInt(cursor.getColumnIndexOrThrow(TemplateDatabase.DEFAULT_FIELD)) == 1;
defaultCheck.setChecked(isDefault);
String name = cursor.getString(cursor.getColumnIndexOrThrow(TemplateDatabase.NAME_FIELD));
nameEdit.setText(name);
template = cursor.getString(cursor.getColumnIndexOrThrow(TemplateDatabase.TEMPLATE_FIELD));
templateKey = cursor.getString(cursor.getColumnIndexOrThrow(TemplateDatabase.KEY_FIELD));
templateRegion = cursor.getString(cursor.getColumnIndexOrThrow(TemplateDatabase.REGION_FIELD));
templateObject = cursor.getString(cursor.getColumnIndexOrThrow(TemplateDatabase.OBJECT_FIELD));
} else {
Log.e(DEBUG_TAG, "template id " + Integer.toString(id) + " not found");
}
}
cursor.close();

alertDialog.setTitle(R.string.edit_template);
alertDialog.setNeutralButton(R.string.Delete, (dialog, which) -> {
Expand All @@ -138,6 +141,26 @@ public AppCompatDialog onCreateDialog(Bundle savedInstanceState) {

keySpinner.setSelection(0);
Util.setSpinnerInitialEntryValue(getResources(), R.array.key_values, keySpinner, templateKey);
// hack for custom keys -- requires that the custom key resource is last
final TypedArray keyValues = getContext().getResources().obtainTypedArray(R.array.key_values);
final int customKeyPos = keyValues.length() - 1;
if (templateKey != null && keySpinner.getSelectedItemPosition() == 0) {
keySpinner.setSelection(customKeyPos);
custom.setText(templateKey);
}

keySpinner.setOnItemSelectedListener(new OnItemSelectedListener() {

@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
custom.setVisibility(position == customKeyPos ? View.VISIBLE : View.GONE);
}

@Override
public void onNothingSelected(AdapterView<?> parent) {
// do nothing
}
});

// setting up the region spinner is a bit involved as we want to be able to sort it
final TypedArray values = getResources().obtainTypedArray(R.array.region_values);
Expand Down Expand Up @@ -166,9 +189,8 @@ public AppCompatDialog onCreateDialog(Bundle savedInstanceState) {

final String finalTemplate = template;
alertDialog.setPositiveButton(R.string.Save, (dialog, which) -> {
final TypedArray keyValues = getContext().getResources().obtainTypedArray(R.array.key_values);
int spinnerPos = keySpinner.getSelectedItemPosition();
final String spinnerKey = spinnerPos == 0 ? null : keyValues.getString(spinnerPos);
final String spinnerKey = spinnerPos == 0 ? null : spinnerPos == customKeyPos ? custom.getText().toString() : keyValues.getString(spinnerPos);
keyValues.recycle();
spinnerPos = regionSpinner.getSelectedItemPosition();
final String spinnerRegion = spinnerPos == 0 ? null : regions.get(spinnerPos).getValue();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,14 @@ public void bindView(final View view, final Context context, Cursor cursor) {
}
}

/**
* Find the nice and translatable String for the stored key value
*
* @param valuesId resource id for the values
* @param entriesId resource id for the entries
* @param value the key value
* @return the mapped value or if nothing found the original
*/
private String valueToEntry(int valuesId, int entriesId, @Nullable String value) {
Resources res = getResources();
final TypedArray values = res.obtainTypedArray(valuesId);
Expand All @@ -291,7 +299,7 @@ private String valueToEntry(int valuesId, int entriesId, @Nullable String value)
values.recycle();
entries.recycle();
}
return "Invalid value";
return value;
}
}

Expand Down
Loading

0 comments on commit 0e4f8d6

Please sign in to comment.