diff --git a/README.md b/README.md index c1b3327a..825c7e86 100644 --- a/README.md +++ b/README.md @@ -67,3 +67,20 @@ Quick description of the content in the root folder: |-- tables -- The most relevant Java code lives here |-- androidTest -- Source tree for Android implementation tests + + + + +## How to contribute +If you’re new to ODK-X you can check out the documentation: +- [https://docs.odk-x.org](https://docs.odk-x.org) + +Once you’re up and running, you can choose an issue to start working on from here:  +- [https://github.com/odk-x/tool-suite-X/issues](https://github.com/odk-x/tool-suite-X/issues) + +Issues tagged as [good first issue](https://github.com/odk-x/tool-suite-X/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) should be a good place to start. + +Pull requests are welcome, though please submit them against the development branch. We prefer verbose descriptions of the change you are submitting. If you are fixing a bug please provide steps to reproduce it or a link to a an issue that provides that information. If you are submitting a new feature please provide a description of the need or a link to a forum discussion about it. + +## Links for users +This document is aimed at helping developers and technical contributors. For information on how to get started as a user of ODK-X, see our [online documentation](https://docs.odk-x.org), or to learn more about the Open Data Kit project, visit [https://odk-x.org](https://odk-x.org). diff --git a/build.gradle b/build.gradle index df5908d7..d079b2d5 100644 --- a/build.gradle +++ b/build.gradle @@ -2,21 +2,20 @@ buildscript { repositories { google() - jcenter() mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:4.1.3' - classpath 'org.jfrog.buildinfo:build-info-extractor-gradle:4.9.8' - classpath 'com.google.gms:google-services:4.3.5' - classpath 'com.google.firebase:firebase-crashlytics-gradle:2.5.2' + classpath 'com.android.tools.build:gradle:4.2.2' + classpath 'org.jfrog.buildinfo:build-info-extractor-gradle:4.23.4' + classpath 'com.google.gms:google-services:4.3.10' + classpath 'com.google.firebase:firebase-crashlytics-gradle:2.7.1' } } allprojects { repositories { google() - jcenter() + mavenCentral() maven { url "https://jitpack.io" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 3f4f7a32..39ef716e 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip diff --git a/settings.gradle b/settings.gradle index cbbf0126..d92d3fd5 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,16 +1,8 @@ -gradle.ext.gradleConfigVersion = 150 - -if ( !gradle.ext.has('workspacePath') ) { - def env = System.getProperties(); - logger.warn("tables/settings.gradle System.getProperties().stringPropertyNames(): " + env.stringPropertyNames()); - def path = System.getProperty('com.android.studio.gradle.project.path'); - if ( path != null ) { - logger.warn("tables/settings.gradle Found value for System.getProperty('com.android.studio.gradle.project.path')"); - gradle.ext.workspacePath = (new File(path)).getParentFile().getAbsolutePath(); - } else { - logger.warn("tables/settings.gradle No value found for System.getProperty('com.android.studio.gradle.project.path')"); - gradle.ext.workspacePath = new File("..").getAbsolutePath(); - } +gradle.ext.gradleConfigVersion = 153 + +if (!gradle.ext.has('workspacePath')) { + logger.warn("rootDir: " + rootDir.getAbsolutePath()); + gradle.ext.workspacePath = rootDir.getParentFile().getAbsolutePath(); } logger.warn('tables/settings.gradle -- gradle.ext.workspacePath: ' + gradle.ext.workspacePath) diff --git a/tables_app/build.gradle b/tables_app/build.gradle index 0cdc5a8a..b9faa552 100644 --- a/tables_app/build.gradle +++ b/tables_app/build.gradle @@ -30,8 +30,6 @@ android { testApplicationId(groupId + tablesName + testNameSuffix) testInstrumentationRunner(instrumentationRunner) - - multiDexEnabled true } flavorDimensions "stage", "testing" @@ -126,17 +124,16 @@ allprojects { dependencies { implementation 'androidx.annotation:annotation:1.2.0' implementation 'androidx.legacy:legacy-support-v4:1.0.0' - implementation 'androidx.fragment:fragment:1.3.2' + implementation 'androidx.fragment:fragment:1.3.6' implementation 'androidx.preference:preference:1.1.1' - implementation 'androidx.appcompat:appcompat:1.2.0' - implementation 'androidx.recyclerview:recyclerview:1.1.0' - implementation 'com.google.android.material:material:1.3.0' + implementation 'androidx.appcompat:appcompat:1.3.1' + implementation 'androidx.recyclerview:recyclerview:1.2.1' + implementation 'com.google.android.material:material:1.4.0' implementation 'com.github.wbrunette:ProgressWheel:3.4' - implementation 'com.google.firebase:firebase-analytics:18.0.2' - implementation 'com.google.firebase:firebase-crashlytics:17.4.1' + implementation 'com.google.firebase:firebase-analytics:19.0.2' + implementation 'com.google.firebase:firebase-crashlytics:18.2.3' - implementation 'androidx.multidex:multidex:2.0.1' if (libraryProjectPath.exists() && gradle.useLocal) { // Local project is favoured implementation project(libraryProjectName) @@ -160,15 +157,15 @@ dependencies { classifier: snapshotRelease, version: latestVersion, ext: 'aar') } - implementation 'com.google.android.gms:play-services-maps:17.0.0' + implementation 'com.google.android.gms:play-services-maps:17.0.1' //for Espresso - androidTestUitestImplementation 'androidx.test:runner:1.3.0' - androidTestUitestImplementation 'androidx.test:rules:1.3.0' - androidTestUitestImplementation 'androidx.test.espresso:espresso-core:3.3.0' - androidTestUitestImplementation 'androidx.test.espresso:espresso-intents:3.3.0' - androidTestUitestImplementation 'androidx.test.espresso:espresso-web:3.3.0' - androidTestUitestImplementation 'androidx.test.espresso:espresso-contrib:3.3.0' + androidTestUitestImplementation 'androidx.test:runner:1.4.0' + androidTestUitestImplementation 'androidx.test:rules:1.4.0' + androidTestUitestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + androidTestUitestImplementation 'androidx.test.espresso:espresso-intents:3.4.0' + androidTestUitestImplementation 'androidx.test.espresso:espresso-web:3.4.0' + androidTestUitestImplementation 'androidx.test.espresso:espresso-contrib:3.4.0' androidTestUitestImplementation 'androidx.annotation:annotation:1.2.0' //for UI Automator diff --git a/tables_app/src/main/java/org/opendatakit/tables/activities/ImportCSVActivity.java b/tables_app/src/main/java/org/opendatakit/tables/activities/ImportCSVActivity.java index cded6c3b..98d02711 100644 --- a/tables_app/src/main/java/org/opendatakit/tables/activities/ImportCSVActivity.java +++ b/tables_app/src/main/java/org/opendatakit/tables/activities/ImportCSVActivity.java @@ -18,8 +18,13 @@ import android.content.ActivityNotFoundException; import android.content.Intent; import android.net.Uri; +import android.os.Build; import android.os.Bundle; + +import androidx.annotation.RequiresApi; import androidx.core.content.ContextCompat; + +import android.provider.DocumentsContract; import android.text.InputType; import android.view.View; import android.view.View.OnClickListener; @@ -30,6 +35,8 @@ import android.widget.ScrollView; import android.widget.TextView; import android.widget.Toast; + +import org.opendatakit.activities.utils.FilePickerUtil; import org.opendatakit.consts.IntentConsts; import org.opendatakit.logging.WebLogger; import org.opendatakit.tables.R; @@ -39,6 +46,7 @@ import org.opendatakit.tables.tasks.ImportTask; import org.opendatakit.tables.utils.TableFileUtils; import org.opendatakit.utilities.ODKFileUtils; +import org.opendatakit.utilities.ODKXFileUriUtils; import java.io.File; @@ -56,6 +64,8 @@ public class ImportCSVActivity extends AbsBaseActivity { // The button to import a table. private Button mImportButton; + private Uri csvUri; + /** * Sets the app name and sets the view (what clicking the buttons should do, etc..) * @@ -63,6 +73,7 @@ public class ImportCSVActivity extends AbsBaseActivity { * this classes parents */ public void onCreate(Bundle savedInstanceState) { + csvUri = null; ImportExportDialogFragment.fragman = getSupportFragmentManager(); super.onCreate(savedInstanceState); appName = getIntent().getStringExtra(IntentConsts.INTENT_KEY_APP_NAME); @@ -137,13 +148,15 @@ private View getView() { */ private void importSubmission() { - String filenamePath = filenameValField.getText().toString().trim(); + if (csvUri == null) { + Toast.makeText(this, "Invalid csv filename: " + filenameValField.getText(), Toast.LENGTH_LONG) + .show(); + return; + } ImportRequest request = null; - String assetsCsvRelativePath = ODKFileUtils - .asRelativePath(appName, new File(ODKFileUtils.getAssetsCsvFolder(appName))); - if (filenamePath.startsWith(assetsCsvRelativePath)) { - String remainingPath = filenamePath.substring(assetsCsvRelativePath.length() + 1); + + String remainingPath = ODKXFileUriUtils.ODKXRemainingPath(appName, csvUri); String[] terms = remainingPath.split("\\."); if (terms.length == 2 && terms[1].equals("csv")) { String tableId = terms[0]; @@ -164,7 +177,6 @@ private void importSubmission() { String fileQualifier = terms[1]; request = new ImportRequest(tableId, fileQualifier); } - } if (request == null) { Toast.makeText(this, "Invalid csv filename: " + filenameValField.getText(), Toast.LENGTH_LONG) @@ -194,54 +206,46 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (resultCode == RESULT_CANCELED) { return; - } - Uri fileUri = data.getData(); - String filepath = fileUri.getPath(); - File csvFile = new File(filepath); - // We have to first hand this off to account for the difference in - // external storage directories on different versions of android. - String relativePath; - try { - relativePath = ODKFileUtils.asRelativePath(appName, csvFile); - } catch (IllegalArgumentException iae) { - WebLogger.getLogger(getAppName()).printStackTrace(iae); - Toast.makeText(this, - getString(R.string.file_not_under_app_dir, ODKFileUtils.getAppFolder(getAppName())), - Toast.LENGTH_LONG).show(); - return; + } else if (FilePickerUtil.isSuccessfulFilePickerResponse(requestCode, resultCode)) { + Uri resultUri = FilePickerUtil.getUri(data); - } - WebLogger.getLogger(appName).d(TAG, "relative path of import file: " + relativePath); - File assetCsv = new File(ODKFileUtils.getAssetsCsvFolder(appName)); - String assetRelativePath = ODKFileUtils.asRelativePath(appName, assetCsv); - if (relativePath.startsWith(assetRelativePath)) { - String name = csvFile.getName(); - String[] terms = name.split("\\."); - if (terms.length < 2 || terms.length > 4) { - Toast.makeText(this, - "Import filename must be of the form tableId.csv, tableId.definition.csv, tableId.properties.csv or tableId.qualifier.csv", - Toast.LENGTH_LONG).show(); - return; - } else { - if (!"csv".equals(terms[terms.length - 1])) { - Toast.makeText(this, "Import filename must end in .csv", Toast.LENGTH_LONG).show(); - return; - } - if (terms.length == 4 && !("properties".equals(terms[2]) || "definition" - .equals(terms[2]))) { + WebLogger.getLogger(appName).d(TAG, "uri of CSV import file: " + resultUri); + String remainingCSVPath = ODKXFileUriUtils.ODKXRemainingPath(appName, resultUri); + if (remainingCSVPath != null) { + String[] terms = remainingCSVPath.split("\\."); + if (terms.length < 2 || terms.length > 4) { Toast.makeText(this, - "Import filename must be of the form tableId.qualifier.properties.csv or tableId.qualifier.definition.csv", - Toast.LENGTH_LONG).show(); + "Import filename must be of the form tableId.csv, tableId.definition.csv, tableId.properties.csv or tableId.qualifier.csv", + Toast.LENGTH_LONG).show(); return; + } else { + if (!"csv".equals(terms[terms.length - 1])) { + Toast.makeText(this, "Import filename must end in .csv", Toast.LENGTH_LONG).show(); + return; + } + if (terms.length == 4 && !("properties".equals(terms[2]) || "definition" + .equals(terms[2]))) { + Toast.makeText(this, + "Import filename must be of the form tableId.qualifier.properties.csv or tableId.qualifier.definition.csv", + Toast.LENGTH_LONG).show(); + return; + } + } + csvUri = resultUri; + if(csvUri != null) { + filenameValField.setText(csvUri.getPath()); } + } else { + Toast.makeText(this, "Import file must reside in opendatakit folder", + Toast.LENGTH_LONG).show(); } - filenameValField.setText(relativePath); - } else { - Toast.makeText(this, "Import file must reside in " + assetRelativePath + File.separator, - Toast.LENGTH_LONG).show(); + + } + } + /** * enables the import button if the database is available */ @@ -280,11 +284,10 @@ private class PickFileButtonListener implements OnClickListener { */ @Override public void onClick(View v) { - Intent intent = new Intent("org.openintents.action.PICK_FILE"); - intent.putExtra("org.openintents.extra.TITLE_KEY", title); - intent.putExtra("org.openintents.extra.DIR_PATH", ODKFileUtils.getAssetsCsvFolder(appName)); + String startingDirectory = ODKFileUtils.getAssetsCsvFolder(appName); + Intent intent = FilePickerUtil.createFilePickerIntent(title, "*/*", startingDirectory); try { - startActivityForResult(intent, 1); + startActivityForResult(intent, FilePickerUtil.FILE_PICKER_CODE); } catch (ActivityNotFoundException e) { WebLogger.getLogger(getAppName()).printStackTrace(e); Toast.makeText(ImportCSVActivity.this, getString(R.string.file_picker_not_found), diff --git a/tables_app/src/main/java/org/opendatakit/tables/application/Tables.java b/tables_app/src/main/java/org/opendatakit/tables/application/Tables.java index 0294db44..0fd5cdae 100644 --- a/tables_app/src/main/java/org/opendatakit/tables/application/Tables.java +++ b/tables_app/src/main/java/org/opendatakit/tables/application/Tables.java @@ -16,8 +16,6 @@ import android.content.Context; -import androidx.multidex.MultiDex; - import com.google.firebase.analytics.FirebaseAnalytics; import org.opendatakit.application.CommonApplication; import org.opendatakit.tables.R; @@ -45,14 +43,7 @@ public static Tables getInstance() { throw new IllegalStateException("not possible"); return ref.get(); } - - @Override - protected void attachBaseContext(Context base) { - super.attachBaseContext(base); - MultiDex.install(this); - } - - + @Override public int getApkDisplayNameResourceId() { return R.string.app_name; diff --git a/tables_app/src/main/java/org/opendatakit/tables/fragments/TablePreferenceFragment.java b/tables_app/src/main/java/org/opendatakit/tables/fragments/TablePreferenceFragment.java index 6fd11681..a50b07c1 100644 --- a/tables_app/src/main/java/org/opendatakit/tables/fragments/TablePreferenceFragment.java +++ b/tables_app/src/main/java/org/opendatakit/tables/fragments/TablePreferenceFragment.java @@ -28,6 +28,7 @@ import org.opendatakit.activities.BaseActivity; import org.opendatakit.activities.IAppAwareActivity; +import org.opendatakit.activities.utils.FilePickerUtil; import org.opendatakit.consts.RequestCodeConsts; import org.opendatakit.data.ColorRuleGroup; import org.opendatakit.data.TableViewType; @@ -50,6 +51,7 @@ import org.opendatakit.tables.utils.Constants; import org.opendatakit.tables.utils.PreferenceUtil; import org.opendatakit.utilities.ODKFileUtils; +import org.opendatakit.utilities.ODKXFileUriUtils; import java.io.File; @@ -116,20 +118,7 @@ public void onResume() { */ @Override public void onActivityResult(final int requestCode, final int resultCode, final Intent data) { - // If the database isn't up, defer handling of the result until later because - // setListViewFileName calls atomicSetListViewFilename which needs the database to be up to work - /* this is the old way to do it, which sucked - if (!dbUp) { - if (savedIntent != null) { - // crash tables :( - throw new IllegalStateException("only queueing one activity result at a time"); - } - savedReq = requestCode; savedRes = resultCode; savedIntent = data; - return; - } else { - savedIntent = null; - } - */ + // this way still sucks, just slightly less if (Tables.getInstance().getDatabase() == null) { //WebLogger.getLogger(getAppName()).i(TAG, "Database not up yet! Sleeping"); @@ -154,7 +143,6 @@ public void run() { return; } //WebLogger.getLogger(getAppName()).i(TAG, "Database now up, attempting"); - String fullPath; String relativePath; // temp //WebLogger.getLogger(getAppName()).i(TAG, String.format(Locale.getDefault(), "%d", requestCode)); @@ -163,10 +151,12 @@ public void run() { case RequestCodeConsts.RequestCodes.CHOOSE_LIST_FILE: if (data != null) { try { - fullPath = getFullPathFromIntent(data); - relativePath = getRelativePathOfFile(fullPath); - //WebLogger.getLogger(getAppName()).i(TAG, "Setting list file to " + relativePath); - this.setListViewFileName(relativePath); + Uri resultUri = FilePickerUtil.getUri(data); + if(resultUri != null) { + relativePath = ODKXFileUriUtils.ODKXRemainingPath(getAppName(), resultUri); + //WebLogger.getLogger(getAppName()).i(TAG, "Setting list file to " + relativePath); + this.setListViewFileName(relativePath); + } //WebLogger.getLogger(getAppName()).i(TAG, "success"); } catch (IllegalArgumentException e) { //WebLogger.getLogger(getAppName()).e(TAG, "failure"); @@ -180,9 +170,11 @@ public void run() { case RequestCodeConsts.RequestCodes.CHOOSE_DETAIL_FILE: if (data != null) { try { - fullPath = getFullPathFromIntent(data); - relativePath = getRelativePathOfFile(fullPath); - this.setDetailViewFileName(relativePath); + Uri resultUri = FilePickerUtil.getUri(data); + if(resultUri != null) { + relativePath = ODKXFileUriUtils.ODKXRemainingPath(getAppName(), resultUri); + this.setDetailViewFileName(relativePath); + } } catch (IllegalArgumentException e) { WebLogger.getLogger(getAppName()).printStackTrace(e); Toast.makeText(getActivity(), @@ -194,9 +186,11 @@ public void run() { case RequestCodeConsts.RequestCodes.CHOOSE_MAP_FILE: if (data != null) { try { - fullPath = getFullPathFromIntent(data); - relativePath = getRelativePathOfFile(fullPath); - this.setMapListViewFileName(relativePath); + Uri resultUri = FilePickerUtil.getUri(data); + if(resultUri != null) { + relativePath = ODKXFileUriUtils.ODKXRemainingPath(getAppName(), resultUri); + this.setMapListViewFileName(relativePath); + } } catch (IllegalArgumentException e) { WebLogger.getLogger(getAppName()).printStackTrace(e); Toast.makeText(getActivity(), @@ -658,16 +652,6 @@ public boolean onPreferenceClick(Preference preference) { }); } - /** - * Helper method to get the relative path of a file from the full path - * - * @param fullPath the path to the file - * @return the relative path to fullPath - */ - private String getRelativePathOfFile(String fullPath) { - return ODKFileUtils - .asRelativePath(((IAppAwareActivity) getActivity()).getAppName(), new File(fullPath)); - } //private boolean dbUp; //private int savedReq, savedRes; private Intent savedIntent = null; diff --git a/tables_app/src/main/java/org/opendatakit/tables/preferences/FileSelectorPreference.java b/tables_app/src/main/java/org/opendatakit/tables/preferences/FileSelectorPreference.java index 769eaae6..bb137d28 100644 --- a/tables_app/src/main/java/org/opendatakit/tables/preferences/FileSelectorPreference.java +++ b/tables_app/src/main/java/org/opendatakit/tables/preferences/FileSelectorPreference.java @@ -25,6 +25,8 @@ import androidx.preference.EditTextPreference; import android.util.AttributeSet; import android.widget.Toast; + +import org.opendatakit.activities.utils.FilePickerUtil; import org.opendatakit.logging.WebLogger; import org.opendatakit.tables.R; import org.opendatakit.utilities.ODKFileUtils; @@ -87,19 +89,12 @@ private void helperStartActivityForResult(Intent intent) { @Override protected void onClick() { - if (hasFilePicker()) { - Intent intent = new Intent("org.openintents.action.PICK_FILE"); + String startingDirectory = null; if (getText() != null) { File fullFile = ODKFileUtils.asAppFile(this.mAppName, getText()); - try { - intent.setData(Uri.parse("file://" + fullFile.getCanonicalPath())); - } catch (IOException e) { - WebLogger.getLogger(mAppName).printStackTrace(e); - Toast.makeText(mFragment.getActivity(), - this.mFragment.getString(R.string.file_not_found, fullFile.getAbsolutePath()), - Toast.LENGTH_LONG).show(); - } + startingDirectory = fullFile.getAbsolutePath(); } + Intent intent = FilePickerUtil.createFilePickerIntent(null, "*/*", startingDirectory); try { this.helperStartActivityForResult(intent); } catch (ActivityNotFoundException e) { @@ -107,22 +102,8 @@ protected void onClick() { Toast.makeText(mFragment.getActivity(), mFragment.getString(R.string.file_picker_not_found), Toast.LENGTH_LONG).show(); } - } else { - super.onClick(); - Toast.makeText(mFragment.getActivity(), mFragment.getString(R.string.file_picker_not_found), - Toast.LENGTH_LONG).show(); - } - } - /** - * @return True if the phone has a file picker installed, false otherwise. - */ - private boolean hasFilePicker() { - PackageManager packageManager = mFragment.getActivity().getPackageManager(); - Intent intent = new Intent("org.openintents.action.PICK_FILE"); - List list = packageManager - .queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); - return !list.isEmpty(); } + } diff --git a/tables_app/src/main/res/raw/configzip b/tables_app/src/main/res/raw/configzip index 0fc680d4..c12200c8 100644 Binary files a/tables_app/src/main/res/raw/configzip and b/tables_app/src/main/res/raw/configzip differ diff --git a/tables_app/src/main/res/raw/systemzip b/tables_app/src/main/res/raw/systemzip index 04764f9c..bab70b8c 100644 Binary files a/tables_app/src/main/res/raw/systemzip and b/tables_app/src/main/res/raw/systemzip differ